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

Merge branch 'support/95275-test-forceCreateBySystem' of https://github.com/weseek/growi into support/95275-test-forceCreateBySystem

yuken 3 лет назад
Родитель
Сommit
f0ffffb2d6
39 измененных файлов с 541 добавлено и 500 удалено
  1. 1 0
      packages/app/config/webpack.common.js
  2. 2 0
      packages/app/src/client/base.jsx
  3. 60 0
      packages/app/src/client/installer.jsx
  4. 2 1
      packages/app/src/client/legacy/crowi.js
  5. 67 65
      packages/app/src/client/nologin.jsx
  6. 0 15
      packages/app/src/client/services/AppContainer.js
  7. 2 3
      packages/app/src/client/services/CommentContainer.js
  8. 10 2
      packages/app/src/client/services/ContextExtractor.tsx
  9. 5 3
      packages/app/src/client/services/PageContainer.js
  10. 7 0
      packages/app/src/client/util/apiv1-client.ts
  11. 12 3
      packages/app/src/client/util/apiv3-client.ts
  12. 2 1
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  13. 2 2
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  14. 1 4
      packages/app/src/components/Admin/App/AwsSetting.jsx
  15. 3 13
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  16. 6 9
      packages/app/src/components/Drawio.tsx
  17. 6 3
      packages/app/src/components/InstallerForm.jsx
  18. 11 6
      packages/app/src/components/LoginForm.jsx
  19. 5 16
      packages/app/src/components/Me/ProfileImageSettings.tsx
  20. 1 1
      packages/app/src/components/MyDraftList/Draft.jsx
  21. 2 2
      packages/app/src/components/Page/RevisionRenderer.jsx
  22. 2 2
      packages/app/src/components/PageComment/Comment.jsx
  23. 1 1
      packages/app/src/components/PageComment/CommentEditor.jsx
  24. 9 12
      packages/app/src/components/PageEditor.tsx
  25. 5 2
      packages/app/src/components/PageEditor/Preview.tsx
  26. 1 9
      packages/app/src/components/PasswordResetExecutionForm.jsx
  27. 1 12
      packages/app/src/components/PasswordResetRequestForm.jsx
  28. 0 6
      packages/app/src/server/crowi/index.js
  29. 0 4
      packages/app/src/server/routes/page.js
  30. 18 16
      packages/app/src/server/service/page.ts
  31. 4 3
      packages/app/src/server/views/installer.html
  32. 4 0
      packages/app/src/stores/context.tsx
  33. 11 7
      packages/app/src/stores/websocket.tsx
  34. 0 264
      packages/app/test/integration/models/v5.page.test.js
  35. 270 0
      packages/app/test/integration/service/page.test.js
  36. 2 2
      packages/plugin-attachment-refs/src/client-entry.js
  37. 3 3
      packages/plugin-lsx/src/client-entry.js
  38. 0 1
      packages/plugin-lsx/src/client/js/components/Lsx.jsx
  39. 3 7
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

+ 1 - 0
packages/app/config/webpack.common.js

@@ -25,6 +25,7 @@ module.exports = (options) => {
       'js/app':                       './src/client/app',
       'js/admin':                     './src/client/admin',
       'js/nologin':                   './src/client/nologin',
+      'js/installer':                   './src/client/installer',
       'js/legacy':                    './src/client/legacy/crowi',
       'js/legacy-presentation':       './src/client/legacy/crowi-presentation',
       'js/plugin':                    './src/client/plugin',

+ 2 - 0
packages/app/src/client/base.jsx

@@ -6,6 +6,7 @@ import AppContainer from '~/client/services/AppContainer';
 import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
 import PutbackPageModal from '~/components/PutbackPageModal';
+import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 
@@ -31,6 +32,7 @@ const xss = new Xss();
 window.xss = xss;
 
 window.globalEmitter = new EventEmitter();
+window.interceptorManager = new InterceptorManager();
 
 // create unstated container instance
 const appContainer = new AppContainer();

+ 60 - 0
packages/app/src/client/installer.jsx

@@ -0,0 +1,60 @@
+import React from 'react';
+
+import ReactDOM from 'react-dom';
+import { I18nextProvider } from 'react-i18next';
+import { SWRConfig } from 'swr';
+
+
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
+
+import InstallerForm from '../components/InstallerForm';
+
+import ContextExtractor from './services/ContextExtractor';
+import { i18nFactory } from './util/i18n';
+
+const i18n = i18nFactory();
+
+const componentMappings = {};
+
+// render InstallerForm
+const installerFormContainerElem = document.getElementById('installer-form-container');
+if (installerFormContainerElem) {
+  const userName = installerFormContainerElem.dataset.userName;
+  const name = installerFormContainerElem.dataset.name;
+  const email = installerFormContainerElem.dataset.email;
+
+  Object.assign(componentMappings, {
+    'installer-form-container': <InstallerForm userName={userName} name={name} email={email} />,
+  });
+}
+
+const renderMainComponents = () => {
+  Object.keys(componentMappings).forEach((key) => {
+    const elem = document.getElementById(key);
+    if (elem) {
+      ReactDOM.render(
+        <I18nextProvider i18n={i18n}>
+          <SWRConfig value={swrGlobalConfiguration}>
+            {componentMappings[key]}
+          </SWRConfig>
+        </I18nextProvider>,
+        elem,
+      );
+    }
+  });
+};
+
+// extract context before rendering main components
+const elem = document.getElementById('growi-context-extractor');
+if (elem != null) {
+  ReactDOM.render(
+    <SWRConfig value={swrGlobalConfiguration}>
+      <ContextExtractor></ContextExtractor>
+    </SWRConfig>,
+    elem,
+    renderMainComponents,
+  );
+}
+else {
+  renderMainComponents();
+}

+ 2 - 1
packages/app/src/client/legacy/crowi.js

@@ -13,7 +13,8 @@ if (!window) {
 window.Crowi = Crowi;
 
 Crowi.setCaretLine = function(line) {
-  window.globalEmitter.emit('setCaretLine', line);
+  // eslint-disable-next-line no-undef
+  globalEmitter.emit('setCaretLine', line);
 };
 
 // original: middleware.swigFilter

+ 67 - 65
packages/app/src/client/nologin.jsx

@@ -2,42 +2,32 @@ import React from 'react';
 
 import ReactDOM from 'react-dom';
 import { I18nextProvider } from 'react-i18next';
+import { SWRConfig } from 'swr';
 import { Provider } from 'unstated';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
-import InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
 import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
 import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
 
+import ContextExtractor from './services/ContextExtractor';
 import { i18nFactory } from './util/i18n';
 
 const i18n = i18nFactory();
 
-// render InstallerForm
-const installerFormContainerElem = document.getElementById('installer-form-container');
-if (installerFormContainerElem) {
-  const userName = installerFormContainerElem.dataset.userName;
-  const name = installerFormContainerElem.dataset.name;
-  const email = installerFormContainerElem.dataset.email;
-  const csrf = installerFormContainerElem.dataset.csrf;
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
-    </I18nextProvider>,
-    installerFormContainerElem,
-  );
-}
+
+const componentMappings = {};
+
+const appContainer = new AppContainer();
+appContainer.initApp();
 
 // render loginForm
 const loginFormElem = document.getElementById('login-form');
 if (loginFormElem) {
-  const appContainer = new AppContainer();
-  appContainer.initApp();
-
   const username = loginFormElem.dataset.username;
   const name = loginFormElem.dataset.name;
   const email = loginFormElem.dataset.email;
@@ -65,78 +55,90 @@ if (loginFormElem) {
     basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
   };
 
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <LoginForm
-          username={username}
-          name={name}
-          email={email}
-          isRegistrationEnabled={isRegistrationEnabled}
-          isEmailAuthenticationEnabled={isEmailAuthenticationEnabled}
-          registrationMode={registrationMode}
-          registrationWhiteList={registrationWhiteList}
-          isPasswordResetEnabled={isPasswordResetEnabled}
-          isLocalStrategySetup={isLocalStrategySetup}
-          isLdapStrategySetup={isLdapStrategySetup}
-          objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
-        />
-      </Provider>
-    </I18nextProvider>,
-    loginFormElem,
-  );
+  Object.assign(componentMappings, {
+    [loginFormElem.id]: (
+      <LoginForm
+        username={username}
+        name={name}
+        email={email}
+        isRegistrationEnabled={isRegistrationEnabled}
+        isEmailAuthenticationEnabled={isEmailAuthenticationEnabled}
+        registrationMode={registrationMode}
+        registrationWhiteList={registrationWhiteList}
+        isPasswordResetEnabled={isPasswordResetEnabled}
+        isLocalStrategySetup={isLocalStrategySetup}
+        isLdapStrategySetup={isLdapStrategySetup}
+        objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
+      />
+    ),
+  });
 }
 
-const appContainer = new AppContainer();
-appContainer.initApp();
-
-
 // render PasswordResetRequestForm
 const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 if (passwordResetRequestFormElem) {
-
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <PasswordResetRequestForm />
-      </Provider>
-    </I18nextProvider>,
-    passwordResetRequestFormElem,
-  );
+  Object.assign(componentMappings, {
+    [passwordResetRequestFormElem.id]: <PasswordResetRequestForm />,
+  });
 }
 
 // render PasswordResetExecutionForm
 const passwordResetExecutionFormElem = document.getElementById('password-reset-execution-form');
 if (passwordResetExecutionFormElem) {
-
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <PasswordResetExecutionForm />
-      </Provider>
-    </I18nextProvider>,
-    passwordResetExecutionFormElem,
-  );
+  Object.assign(componentMappings, {
+    [passwordResetExecutionFormElem.id]: <PasswordResetExecutionForm />,
+  });
 }
 
 // render UserActivationForm
 const UserActivationForm = document.getElementById('user-activation-form');
 if (UserActivationForm) {
-
   const messageErrors = UserActivationForm.dataset.messageErrors;
   const inputs = UserActivationForm.dataset.inputs;
   const email = UserActivationForm.dataset.email;
   const token = UserActivationForm.dataset.token;
 
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
+  Object.assign(componentMappings, {
+    [UserActivationForm.id]: (
       <CompleteUserRegistrationForm
         messageErrors={messageErrors}
         inputs={inputs}
         email={email}
         token={token}
       />
-    </I18nextProvider>,
-    UserActivationForm,
+    ),
+  });
+}
+
+const renderMainComponents = () => {
+  Object.keys(componentMappings).forEach((key) => {
+    const elem = document.getElementById(key);
+    if (elem) {
+      ReactDOM.render(
+        <I18nextProvider i18n={i18n}>
+          <SWRConfig value={swrGlobalConfiguration}>
+            <Provider inject={[appContainer]}>
+              {componentMappings[key]}
+            </Provider>
+          </SWRConfig>
+        </I18nextProvider>,
+        elem,
+      );
+    }
+  });
+};
+
+// extract context before rendering main components
+const elem = document.getElementById('growi-context-extractor');
+if (elem != null) {
+  ReactDOM.render(
+    <SWRConfig value={swrGlobalConfiguration}>
+      <ContextExtractor></ContextExtractor>
+    </SWRConfig>,
+    elem,
+    renderMainComponents,
   );
 }
+else {
+  renderMainComponents();
+}

+ 0 - 15
packages/app/src/client/services/AppContainer.js

@@ -1,6 +1,5 @@
 import { Container } from 'unstated';
 
-import InterceptorManager from '~/services/interceptor-manager';
 
 import GrowiRenderer from '../util/GrowiRenderer';
 import { i18nFactory } from '../util/i18n';
@@ -14,11 +13,6 @@ export default class AppContainer extends Container {
   constructor() {
     super();
 
-    // get csrf token from body element
-    // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
-    const body = document.querySelector('body');
-    this.csrfToken = body.dataset.csrftoken;
-
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
     const currentUserElem = document.getElementById('growi-current-user');
@@ -52,8 +46,6 @@ export default class AppContainer extends Container {
 
     this.originRenderer = new GrowiRenderer(this);
 
-    this.interceptorManager = new InterceptorManager();
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -93,13 +85,6 @@ export default class AppContainer extends Container {
     return this.currentUser.username;
   }
 
-  /**
-   * @return {Object} window.Crowi (js/legacy/crowi.js)
-   */
-  getCrowiForJquery() {
-    return window.Crowi;
-  }
-
   getConfig() {
     return this.config;
   }

+ 2 - 3
packages/app/src/client/services/CommentContainer.js

@@ -2,7 +2,7 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
-import { apiGet, apiPost } from '../util/apiv1-client';
+import { apiGet, apiPost, apiPostForm } from '../util/apiv1-client';
 import { apiv3Put } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:services:CommentContainer');
@@ -157,12 +157,11 @@ export default class CommentContainer extends Container {
 
     const endpoint = '/attachments.add';
     const formData = new FormData();
-    formData.append('_csrf', this.appContainer.csrfToken);
     formData.append('file', file);
     formData.append('path', pagePath);
     formData.append('page_id', pageId);
 
-    return apiPost(endpoint, formData);
+    return apiPostForm(endpoint, formData);
   }
 
 }

+ 10 - 2
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,7 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -32,6 +32,11 @@ const ContextExtractorOnce: FC = () => {
   const notFoundContent = document.getElementById('growi-not-found-context');
   const forbiddenContent = document.getElementById('forbidden-page');
 
+  // get csrf token from body element
+  // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
+  const body = document.querySelector('body');
+  const csrfToken = body?.dataset.csrftoken;
+
   /*
    * App Context from DOM
    */
@@ -94,6 +99,8 @@ const ContextExtractorOnce: FC = () => {
   /*
    * use static swr
    */
+  useCsrfToken(csrfToken);
+
   // App
   useCurrentUser(currentUser);
 
@@ -166,7 +173,8 @@ const ContextExtractorOnce: FC = () => {
 
   // Global Socket
   useSetupGlobalSocket();
-  useSetupGlobalAdminSocket();
+  const shouldInitAdminSock = !!currentUser?.isAdmin;
+  useSetupGlobalAdminSocket(shouldInitAdminSock);
 
   return null;
 };

+ 5 - 3
packages/app/src/client/services/PageContainer.js

@@ -99,7 +99,7 @@ export default class PageContainer extends Container {
       logger.warn('The data of \'data-page-revision-author\' is invalid', e);
     }
 
-    const { interceptorManager } = this.appContainer;
+    const { interceptorManager } = window;
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(), 10); // process as soon as possible
     interceptorManager.addInterceptor(new DrawioInterceptor(), 20);
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(), 900); // process as late as possible
@@ -219,7 +219,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', newState.markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', newState.markdown);
     }
 
     // PageEditorByHackmd component
@@ -459,7 +460,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', markdown);
     }
 
     editorContainer.setState({ tags: res.tags });

+ 7 - 0
packages/app/src/client/util/apiv1-client.ts

@@ -58,6 +58,13 @@ export async function apiPost(path: string, params: any & ParamWithCsrfKey = {})
   return apiRequest('post', path, params);
 }
 
+export async function apiPostForm(path: string, formData: FormData): Promise<unknown> {
+  if (formData.get('_csrf') == null && csrfToken != null) {
+    formData.append('_csrf', csrfToken);
+  }
+  return apiPost(path, formData);
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
   if (params._csrf == null) {

+ 12 - 3
packages/app/src/client/util/apiv3-client.ts

@@ -1,11 +1,12 @@
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
 import * as urljoin from 'url-join';
 
 // eslint-disable-next-line no-restricted-imports
-import { AxiosResponse } from 'axios';
 
-import loggerFactory from '~/utils/logger';
-import axios from '~/utils/axios';
 import { toArrayIfNot } from '~/utils/array-utils';
+import axios from '~/utils/axios';
+import loggerFactory from '~/utils/logger';
 
 const apiv3Root = '/_api/v3';
 
@@ -57,6 +58,14 @@ export async function apiv3Post<T = any>(path: string, params: any & ParamWithCs
   return apiv3Request('post', path, params);
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3PostForm<T = any>(path: string, formData: FormData): Promise<AxiosResponse<T>> {
+  if (formData.get('_csrf') == null && csrfToken != null) {
+    formData.append('_csrf', csrfToken);
+  }
+  return apiv3Post<T>(path, formData);
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function apiv3Put<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
   if (params._csrf == null) {

+ 2 - 1
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -18,7 +18,8 @@ export default class TocAndAnchorConfigurer {
     // set toc render function
     md.set({
       tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-        window.globalEmitter.emit('renderTocHtml', tocHtml);
+        // eslint-disable-next-line no-undef
+        globalEmitter.emit('renderTocHtml', tocHtml);
       },
     });
   }

+ 2 - 2
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -28,7 +28,7 @@
       const section = sections[i];
       const markdown = marked.getMarkdownFromSlide(section);
       const context = { markdown };
-      const interceptorManager = appContainer.interceptorManager;
+      const { interceptorManager } = window.parent;
       let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
       // replace string '\n' to LF code.
       dataSeparator = dataSeparator.replace(/\\n/g, '\n');
@@ -51,7 +51,7 @@
   function convertSlides() {
     const sections = document.querySelectorAll('[data-markdown]');
     let markdown;
-    const interceptorManager = appContainer.interceptorManager;
+    const { interceptorManager } = window.parent;
 
     for (let i = 0, len = sections.length; i < len; i++) {
       const section = sections[i];

+ 1 - 4
packages/app/src/components/Admin/App/AwsSetting.jsx

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -153,11 +152,9 @@ function AwsSetting(props) {
 /**
  * Wrapper component for using unstated
  */
-const AwsSettingWrapper = withUnstatedContainers(AwsSetting, [AppContainer, AdminAppContainer]);
+const AwsSettingWrapper = withUnstatedContainers(AwsSetting, [AdminAppContainer]);
 
 AwsSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 

+ 3 - 13
packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -3,11 +3,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
-import { apiv3Post } from '~/client/util/apiv3-client';
-
-import { withUnstatedContainers } from '../../../UnstatedUtils';
+import { apiv3PostForm } from '~/client/util/apiv3-client';
 
 class UploadForm extends React.Component {
 
@@ -31,11 +28,10 @@ class UploadForm extends React.Component {
     e.preventDefault();
 
     const formData = new FormData();
-    formData.append('_csrf', this.props.appContainer.csrfToken);
     formData.append('file', this.inputRef.current.files[0]);
 
     try {
-      const { data } = await apiv3Post('/import/upload', formData);
+      const { data } = await apiv3PostForm('/import/upload', formData);
       // TODO: toastSuccess, toastError
       this.props.onUpload(data);
     }
@@ -96,15 +92,9 @@ class UploadForm extends React.Component {
 
 UploadForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   onUpload: PropTypes.func.isRequired,
   isTheSameVersion: PropTypes.bool,
   onVersionMismatch: PropTypes.func,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const UploadFormWrapper = withUnstatedContainers(UploadForm, [AppContainer]);
-
-export default withTranslation()(UploadFormWrapper);
+export default withTranslation()(UploadForm);

+ 6 - 9
packages/app/src/components/Drawio.tsx

@@ -10,11 +10,9 @@ import { debounce } from 'throttle-debounce';
 import NotAvailableForGuest from './NotAvailableForGuest';
 
 
-declare let window: {
-  globalEmitter: EventEmitter,
-  GraphViewer: {
-    createViewerForElement: (Element) => void,
-  };
+declare const globalEmitter: EventEmitter;
+declare const GraphViewer: {
+  createViewerForElement: (Element) => void,
 };
 
 type Props = {
@@ -35,7 +33,7 @@ const Drawio = (props: Props): JSX.Element => {
 
   const editButtonClickHandler = useCallback(() => {
     const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
-    window.globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
+    globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
   }, [rangeLineNumberOfMarkdown]);
 
   const renderDrawio = useCallback(() => {
@@ -50,21 +48,20 @@ const Drawio = (props: Props): JSX.Element => {
 
       if (div != null) {
         div.innerHTML = '';
-        window.GraphViewer.createViewerForElement(div);
+        GraphViewer.createViewerForElement(div);
       }
     }
   }, []);
 
   const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
 
-  const { GraphViewer } = window;
   useEffect(() => {
     if (GraphViewer == null) {
       return;
     }
 
     renderDrawioWithDebounce();
-  }, [GraphViewer, renderDrawioWithDebounce]);
+  }, [renderDrawioWithDebounce]);
 
   return (
     <div className="editable-with-drawio position-relative">

+ 6 - 3
packages/app/src/components/InstallerForm.jsx

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
 import { localeMetadatas } from '~/client/util/i18n';
+import { useCsrfToken } from '~/stores/context';
 
 class InstallerForm extends React.Component {
 
@@ -175,7 +176,7 @@ class InstallerForm extends React.Component {
               />
             </div>
 
-            <input type="hidden" name="_csrf" value={this.props.csrf} />
+            <input type="hidden" name="_csrf" value={this.props.csrfToken} />
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
               <button
@@ -211,12 +212,14 @@ InstallerForm.propTypes = {
   userName: PropTypes.string,
   name: PropTypes.string,
   email: PropTypes.string,
-  csrf: PropTypes.string,
+  csrfToken: PropTypes.string,
 };
 
 const InstallerFormWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <InstallerForm t={t} {...props} />;
+  const { data: csrfToken } = useCsrfToken();
+
+  return <InstallerForm t={t} csrfToken={csrfToken} {...props} />;
 };
 
 export default InstallerFormWrapperFC;

+ 11 - 6
packages/app/src/components/LoginForm.jsx

@@ -5,6 +5,7 @@ import ReactCardFlip from 'react-card-flip';
 import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
+import { useCsrfToken } from '~/stores/context';
 
 import { withUnstatedContainers } from './UnstatedUtils';
 
@@ -36,12 +37,12 @@ class LoginForm extends React.Component {
 
   handleLoginWithExternalAuth(e) {
     const auth = e.currentTarget.id;
-    const { csrf } = this.props.appContainer;
-    window.location.href = `/passport/${auth}?_csrf=${csrf}`;
+    const { csrfToken } = this.props;
+    window.location.href = `/passport/${auth}?_csrf=${csrfToken}`;
   }
 
   renderLocalOrLdapLoginForm() {
-    const { t, appContainer, isLdapStrategySetup } = this.props;
+    const { t, csrfToken, isLdapStrategySetup } = this.props;
 
     return (
       <form role="form" action="/login" method="post">
@@ -71,7 +72,7 @@ class LoginForm extends React.Component {
         </div>
 
         <div className="input-group my-4">
-          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+          <input type="hidden" name="_csrf" value={csrfToken} />
           <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
             <div className="eff"></div>
             <span className="btn-label">
@@ -149,6 +150,7 @@ class LoginForm extends React.Component {
     const {
       t,
       appContainer,
+      csrfToken,
       isEmailAuthenticationEnabled,
       username,
       name,
@@ -252,7 +254,7 @@ class LoginForm extends React.Component {
           )}
 
           <div className="input-group justify-content-center my-4">
-            <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+            <input type="hidden" name="_csrf" value={csrfToken} />
             <button type="submit" className="btn btn-fill rounded-0" id="register" disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}>
               <div className="eff"></div>
               <span className="btn-label">
@@ -333,6 +335,7 @@ LoginForm.propTypes = {
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
+  csrfToken: PropTypes.string,
   isRegistering: PropTypes.bool,
   username: PropTypes.string,
   name: PropTypes.string,
@@ -349,7 +352,9 @@ LoginForm.propTypes = {
 
 const LoginFormWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <LoginForm t={t} {...props} />;
+  const { data: csrfToken } = useCsrfToken();
+
+  return <LoginForm t={t} csrfToken={csrfToken} {...props} />;
 };
 
 /**

+ 5 - 16
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -3,29 +3,20 @@ import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiPost } from '~/client/util/apiv1-client';
+import { apiPost, apiPostForm } from '~/client/util/apiv1-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { useCurrentUser } from '~/stores/context';
 import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-
 import ImageCropModal from './ImageCropModal';
 
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
 
-type Props = {
-  appContainer: AppContainer,
-}
-
-const ProfileImageSettings = (props: Props): JSX.Element => {
+const ProfileImageSettings = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { appContainer } = props;
-
   const { data: currentUser } = useCurrentUser();
 
   const [isGravatarEnabled, setGravatarEnabled] = useState(currentUser?.isGravatarEnabled);
@@ -55,9 +46,7 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      formData.append('_csrf', appContainer.csrfToken!);
-      const response = await apiPost('/attachments.uploadProfileImage', formData);
+      const response = await apiPostForm('/attachments.uploadProfileImage', formData);
 
       toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
 
@@ -70,7 +59,7 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
     catch (err) {
       toastError(err);
     }
-  }, [appContainer.csrfToken, t]);
+  }, [t]);
 
   const deleteImageHandler = useCallback(async() => {
     try {
@@ -185,4 +174,4 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
 
 };
 
-export default withUnstatedContainers(ProfileImageSettings, [AppContainer]);
+export default ProfileImageSettings;

+ 1 - 1
packages/app/src/components/MyDraftList/Draft.jsx

@@ -59,7 +59,7 @@ class Draft extends React.Component {
     };
 
     const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
+    const { interceptorManager } = window;
     await interceptorManager.process('prePreProcess', context)
       .then(() => {
         context.markdown = growiRenderer.preProcess(context.markdown, context);

+ 2 - 2
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -58,7 +58,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
     const HeaderLinkArray = Array.from(HeaderLink);
     addSmoothScrollEvent(HeaderLinkArray, blinkElem);
 
-    const { interceptorManager } = this.props.appContainer;
+    const { interceptorManager } = window;
 
     interceptorManager.process('postRenderHtml', this.currentRenderingContext);
   }
@@ -134,7 +134,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
       highlightKeywords,
     } = this.props;
 
-    const { interceptorManager } = appContainer;
+    const { interceptorManager } = window;
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRender', context);

+ 2 - 2
packages/app/src/components/PageComment/Comment.jsx

@@ -69,7 +69,7 @@ class Comment extends React.PureComponent {
       return;
     }
 
-    const { interceptorManager } = this.props.appContainer;
+    const { interceptorManager } = window;
 
     interceptorManager.process('postRenderCommentHtml', this.currentRenderingContext);
   }
@@ -133,7 +133,7 @@ class Comment extends React.PureComponent {
   async renderHtml() {
 
     const { growiRenderer, appContainer } = this.props;
-    const { interceptorManager } = appContainer;
+    const { interceptorManager } = window;
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRenderComment', context);

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -235,7 +235,7 @@ class CommentEditor extends React.Component {
     };
 
     const { growiRenderer } = this.props;
-    const interceptorManager = this.props.appContainer.interceptorManager;
+    const { interceptorManager } = window;
     interceptorManager.process('preRenderCommnetPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {

+ 9 - 12
packages/app/src/components/PageEditor.tsx

@@ -11,7 +11,7 @@ import { throttle, debounce } from 'throttle-debounce';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { apiGet, apiPost } from '~/client/util/apiv1-client';
+import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { useIsEditable, useIsIndentSizeForced, useCurrentPagePath } from '~/stores/context';
 import {
@@ -36,9 +36,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 const logger = loggerFactory('growi:PageEditor');
 
 
-declare let window: {
-  globalEmitter: EventEmitter,
-};
+declare const globalEmitter: EventEmitter;
+
 
 type EditorRef = {
   setValue: (markdown: string) => void,
@@ -165,8 +164,6 @@ const PageEditor = (props: Props): JSX.Element => {
 
       const formData = new FormData();
       const { pageId, path } = pageContainer.state;
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      formData.append('_csrf', appContainer.csrfToken!);
       formData.append('file', file);
       if (path != null) {
         formData.append('path', path);
@@ -175,7 +172,7 @@ const PageEditor = (props: Props): JSX.Element => {
         formData.append('page_id', pageId);
       }
 
-      res = await apiPost('/attachments.add', formData);
+      res = await apiPostForm('/attachments.add', formData);
       const attachment = res.attachment;
       const fileName = attachment.originalName;
 
@@ -201,7 +198,7 @@ const PageEditor = (props: Props): JSX.Element => {
     finally {
       editorRef.current.terminateUploadingState();
     }
-  }, [appContainer.csrfToken, editorMode, mutateGrant, pageContainer]);
+  }, [editorMode, mutateGrant, pageContainer]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
@@ -322,10 +319,10 @@ const PageEditor = (props: Props): JSX.Element => {
         scrollSyncHelper.scrollPreview(previewRef.current, line);
       }
     };
-    window.globalEmitter.on('setCaretLine', handler);
+    globalEmitter.on('setCaretLine', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('setCaretLine', handler);
+      globalEmitter.removeListener('setCaretLine', handler);
     };
   }, []);
 
@@ -343,10 +340,10 @@ const PageEditor = (props: Props): JSX.Element => {
         editorRef.current.setValue(markdown);
       }
     };
-    window.globalEmitter.on('updateEditorValue', handler);
+    globalEmitter.on('updateEditorValue', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('updateEditorValue', handler);
+      globalEmitter.removeListener('updateEditorValue', handler);
     };
   }, []);
 

+ 5 - 2
packages/app/src/components/PageEditor/Preview.tsx

@@ -4,12 +4,16 @@ import React, {
 
 
 import AppContainer from '~/client/services/AppContainer';
+import InterceptorManager from '~/services/interceptor-manager';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+declare const interceptorManager: InterceptorManager;
+
+
 type Props = {
   appContainer: AppContainer,
 
@@ -34,8 +38,7 @@ const Preview = (props: Props): JSX.Element => {
 
   const { data: editorSettings } = useEditorSettings();
 
-  const { interceptorManager } = appContainer;
-  const growiRenderer = props.appContainer.getRenderer('editor');
+  const growiRenderer = appContainer.getRenderer('editor');
 
   const context = useMemo(() => {
     return {

+ 1 - 9
packages/app/src/components/PasswordResetExecutionForm.jsx

@@ -1,21 +1,16 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 const logger = loggerFactory('growi:passwordReset');
 
 
 const PasswordResetExecutionForm = (props) => {
   const { t } = useTranslation();
-  const { appContainer } = props;
 
   const [newPassword, setNewPassword] = useState('');
   const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
@@ -91,10 +86,7 @@ const PasswordResetExecutionForm = (props) => {
   );
 };
 
-const PasswordResetExecutionFormWrapper = withUnstatedContainers(PasswordResetExecutionForm, [AppContainer]);
-
 PasswordResetExecutionForm.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default PasswordResetExecutionFormWrapper;
+export default PasswordResetExecutionForm;

+ 1 - 12
packages/app/src/components/PasswordResetRequestForm.jsx

@@ -1,18 +1,13 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 
 const PasswordResetRequestForm = (props) => {
   const { t } = useTranslation();
-  const { appContainer } = props;
   const [email, setEmail] = useState('');
 
   const changeEmail = (inputValue) => {
@@ -57,13 +52,7 @@ const PasswordResetRequestForm = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PasswordResetRequestFormWrapper = withUnstatedContainers(PasswordResetRequestForm, [AppContainer]);
-
 PasswordResetRequestForm.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default PasswordResetRequestFormWrapper;
+export default PasswordResetRequestForm;

+ 0 - 6
packages/app/src/server/crowi/index.js

@@ -11,7 +11,6 @@ import mongoose from 'mongoose';
 import pkg from '^/package.json';
 
 import CdnResourcesService from '~/services/cdn-resources-service';
-import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
@@ -72,7 +71,6 @@ function Crowi() {
   this.pageService = null;
   this.syncPageStatusService = null;
   this.cdnResourcesService = new CdnResourcesService();
-  this.interceptorManager = new InterceptorManager();
   this.slackIntegrationService = null;
   this.inAppNotificationService = null;
   this.activityService = null;
@@ -313,10 +311,6 @@ Crowi.prototype.getSlackLegacy = function() {
   return this.slackLegacy;
 };
 
-Crowi.prototype.getInterceptorManager = function() {
-  return this.interceptorManager;
-};
-
 Crowi.prototype.getGlobalNotificationService = function() {
   return this.globalNotificationService;
 };

+ 0 - 4
packages/app/src/server/routes/page.js

@@ -153,7 +153,6 @@ module.exports = function(crowi, app) {
   const getToday = require('../util/getToday');
 
   const { configManager, xssService } = crowi;
-  const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const userNotificationService = crowi.getUserNotificationService();
 
@@ -404,7 +403,6 @@ module.exports = function(crowi, app) {
 
     await addRenderVarsForPageTree(renderVars, portalPath, req.user);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }
 
@@ -466,7 +464,6 @@ module.exports = function(crowi, app) {
 
     await addRenderVarsForPageTree(renderVars, path, req.user);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }
 
@@ -536,7 +533,6 @@ module.exports = function(crowi, app) {
     addRenderVarsForPage(renderVars, page);
     addRenderVarsForScope(renderVars, page);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render('layout-growi/shared_page', renderVars);
   };
 

+ 18 - 16
packages/app/src/server/service/page.ts

@@ -2638,10 +2638,7 @@ class PageService {
     return isUnique;
   }
 
-  // TODO: use socket to send status to the client
   async normalizeAllPublicPages() {
-    // const socket = this.crowi.socketIoService.getAdminSocket();
-
     let isUnique;
     try {
       isUnique = await this._isPagePathIndexUnique();
@@ -2658,7 +2655,6 @@ class PageService {
       }
       catch (err) {
         logger.error('V5 index normalization failed.', err);
-        // socket.emit('v5IndexNormalizationFailed', { error: err.message });
         throw err;
       }
     }
@@ -2669,7 +2665,6 @@ class PageService {
     }
     catch (err) {
       logger.error('V5 initial miration failed.', err);
-      // socket.emit('v5InitialMirationFailed', { error: err.message });
 
       throw err;
     }
@@ -2713,7 +2708,7 @@ class PageService {
    * @param user To be used to filter pages to update. If null, only public pages will be updated.
    * @returns Promise<void>
    */
-  async normalizeParentRecursively(paths: string[], user: any | null): Promise<number> {
+  async normalizeParentRecursively(paths: string[], user: any | null, shouldEmit = false): Promise<number> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, []));
@@ -2732,7 +2727,7 @@ class PageService {
 
     const grantFiltersByUser: { $or: any[] } = Page.generateGrantCondition(user, userGroups);
 
-    return this._normalizeParentRecursively(pathAndRegExpsToNormalize, ancestorPaths, grantFiltersByUser, user);
+    return this._normalizeParentRecursively(pathAndRegExpsToNormalize, ancestorPaths, grantFiltersByUser, user, shouldEmit);
   }
 
   private buildFilterForNormalizeParentRecursively(pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }) {
@@ -2778,12 +2773,19 @@ class PageService {
   }
 
   private async _normalizeParentRecursively(
-      pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }, user, count = 0, skiped = 0, isFirst = true,
+      pathOrRegExps: (RegExp | string)[],
+      publicPathsToNormalize: string[],
+      grantFiltersByUser: { $or: any[] },
+      user,
+      shouldEmit = false,
+      count = 0,
+      skiped = 0,
+      isFirst = true,
   ): Promise<number> {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
 
-    const socket = this.crowi.socketIoService.getAdminSocket();
+    const socket = shouldEmit ? this.crowi.socketIoService.getAdminSocket() : null;
 
     const Page = mongoose.model('Page') as unknown as PageModel;
     const { PageQueryBuilder } = Page;
@@ -2805,7 +2807,7 @@ class PageService {
     // Limit pages to get
     const total = await Page.countDocuments(matchFilter);
     if (isFirst) {
-      socket.emit(SocketEventName.PMStarted, { total });
+      socket?.emit(SocketEventName.PMStarted, { total });
     }
     if (total > PAGES_LIMIT) {
       baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
@@ -2912,13 +2914,13 @@ class PageService {
           nextSkiped += res.result.writeErrors.length;
           logger.info(`Page migration processing: (migratedPages=${res.result.nModified})`);
 
-          socket.emit(SocketEventName.PMMigrating, { count: nextCount });
-          socket.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
+          socket?.emit(SocketEventName.PMMigrating, { count: nextCount });
+          socket?.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
 
           // Throw if any error is found
           if (res.result.writeErrors.length > 0) {
             logger.error('Failed to migrate some pages', res.result.writeErrors);
-            socket.emit(SocketEventName.PMEnded, { isSucceeded: false });
+            socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
             throw Error('Failed to migrate some pages');
           }
 
@@ -2926,7 +2928,7 @@ class PageService {
           if (res.result.nModified === 0 && res.result.nMatched === 0) {
             shouldContinue = false;
             logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
-            socket.emit(SocketEventName.PMEnded, { isSucceeded: false });
+            socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
           }
         }
         catch (err) {
@@ -2948,11 +2950,11 @@ class PageService {
     await streamToPromise(migratePagesStream);
 
     if (await Page.exists(matchFilter) && shouldContinue) {
-      return this._normalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser, user, nextCount, nextSkiped, false);
+      return this._normalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser, user, shouldEmit, nextCount, nextSkiped, false);
     }
 
     // End
-    socket.emit(SocketEventName.PMEnded, { isSucceeded: true });
+    socket?.emit(SocketEventName.PMEnded, { isSucceeded: true });
 
     return nextCount;
   }

+ 4 - 3
packages/app/src/server/views/installer.html

@@ -31,7 +31,7 @@
 
   {{ cdnStyleTagsByGroup('basis') }}
 
-  <script src="{{ webpack_asset('js/nologin.js') }}" defer></script>
+  <script src="{{ webpack_asset('js/installer.js') }}" defer></script>
 
 </head>
 {% endblock %}
@@ -43,6 +43,8 @@
   data-csrftoken="{{ csrf() }}"
  >
 
+<div id="growi-context-extractor"></div>
+
 <div id="wrapper">
 
   <!-- Page Content -->
@@ -72,8 +74,7 @@
           <div id="installer-form-container"
             data-user-name="{{ req.body.registerForm.username }}"
             data-name="{{ req.body.registerForm.name }}"
-            data-email="{{ req.body.registerForm.email }}"
-            data-csrf="{{ csrf() }}">
+            data-email="{{ req.body.registerForm.email }}">
           </div>
         </div>
       </div>{# /.row #}

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -12,6 +12,10 @@ import { useStaticSWR } from './use-static-swr';
 type Nullable<T> = T | null;
 
 
+export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR<string, Error>('csrfToken', initialData);
+};
+
 export const useSiteUrl = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR<string, Error>('siteUrl', initialData);
 };

+ 11 - 7
packages/app/src/stores/websocket.tsx

@@ -33,15 +33,19 @@ export const useGlobalSocket = (): SWRResponse<Socket, Error> => {
 /*
  * Global Admin Socket
  */
-export const useSetupGlobalAdminSocket = (): SWRResponse<Socket, Error> => {
-  const socket = io(GLOBAL_ADMIN_SOCKET_NS, {
-    transports: ['websocket'],
-  });
+export const useSetupGlobalAdminSocket = (shouldInit: boolean): SWRResponse<Socket, Error> => {
+  let socket: Socket | undefined;
 
-  socket.on('error', (err) => { logger.error(err) });
-  socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+  if (shouldInit) {
+    socket = io(GLOBAL_ADMIN_SOCKET_NS, {
+      transports: ['websocket'],
+    });
+
+    socket.on('error', (err) => { logger.error(err) });
+    socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+  }
 
-  return useStaticSWR(GLOBAL_ADMIN_SOCKET_KEY, socket);
+  return useStaticSWR(shouldInit ? GLOBAL_ADMIN_SOCKET_KEY : null, socket);
 };
 
 export const useGlobalAdminSocket = (): SWRResponse<Socket, Error> => {

+ 0 - 264
packages/app/test/integration/models/v5.page.test.js

@@ -348,92 +348,6 @@ describe('Page', () => {
       },
     ]);
 
-    /**
-     * getParentAndFillAncestors
-     */
-    const pageIdPAF1 = new mongoose.Types.ObjectId();
-    const pageIdPAF2 = new mongoose.Types.ObjectId();
-    const pageIdPAF3 = new mongoose.Types.ObjectId();
-
-    await Page.insertMany([
-      {
-        _id: pageIdPAF1,
-        path: '/PAF1',
-        grant: Page.GRANT_PUBLIC,
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1._id,
-        isEmpty: false,
-        parent: rootPage._id,
-        descendantCount: 0,
-      },
-      {
-        _id: pageIdPAF2,
-        path: '/emp_anc3',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-        descendantCount: 1,
-        parent: rootPage._id,
-      },
-      {
-        path: '/emp_anc3/PAF3',
-        grant: Page.GRANT_PUBLIC,
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1._id,
-        isEmpty: false,
-        descendantCount: 0,
-        parent: pageIdPAF2,
-      },
-      {
-        _id: pageIdPAF3,
-        path: '/emp_anc4',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-        descendantCount: 1,
-        parent: rootPage._id,
-      },
-      {
-        path: '/emp_anc4/PAF4',
-        grant: Page.GRANT_PUBLIC,
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1._id,
-        isEmpty: false,
-        descendantCount: 0,
-        parent: pageIdPAF3,
-      },
-      {
-        path: '/emp_anc4',
-        grant: Page.GRANT_OWNER,
-        grantedUsers: [dummyUser1._id],
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1._id,
-        isEmpty: false,
-      },
-      {
-        path: '/get_parent_A',
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1,
-        parent: null,
-      },
-      {
-        path: '/get_parent_A/get_parent_B',
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1,
-        parent: null,
-      },
-      {
-        path: '/get_parent_C',
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1,
-        parent: rootPage._id,
-      },
-      {
-        path: '/get_parent_C/get_parent_D',
-        creator: dummyUser1,
-        lastUpdateUser: dummyUser1,
-        parent: null,
-      },
-    ]);
-
   });
 
   describe('update', () => {
@@ -647,182 +561,4 @@ describe('Page', () => {
     });
 
   });
-
-  describe('getParentAndFillAncestors', () => {
-    test('return parent if exist', async() => {
-      const page1 = await Page.findOne({ path: '/PAF1' });
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, page1.path);
-      expect(parent).toBeTruthy();
-      expect(page1.parent).toStrictEqual(parent._id);
-    });
-    test('create parent and ancestors when they do not exist, and return the new parent', async() => {
-      const path1 = '/emp_anc1';
-      const path2 = '/emp_anc1/emp_anc2';
-      const path3 = '/emp_anc1/emp_anc2/PAF2';
-      const _page1 = await Page.findOne({ path: path1 }); // not exist
-      const _page2 = await Page.findOne({ path: path2 }); // not exist
-      const _page3 = await Page.findOne({ path: path3 }); // not exist
-      expect(_page1).toBeNull();
-      expect(_page2).toBeNull();
-      expect(_page3).toBeNull();
-
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, path3);
-      const page1 = await Page.findOne({ path: path1 });
-      const page2 = await Page.findOne({ path: path2 });
-      const page3 = await Page.findOne({ path: path3 });
-
-      expect(parent._id).toStrictEqual(page2._id);
-      expect(parent.path).toStrictEqual(page2.path);
-      expect(parent.parent).toStrictEqual(page2.parent);
-
-      expect(parent).toBeTruthy();
-      expect(page1).toBeTruthy();
-      expect(page2).toBeTruthy();
-      expect(page3).toBeNull();
-
-      expect(page1.parent).toStrictEqual(rootPage._id);
-      expect(page2.parent).toStrictEqual(page1._id);
-    });
-    test('return parent even if the parent page is empty', async() => {
-      const path1 = '/emp_anc3';
-      const path2 = '/emp_anc3/PAF3';
-      const _page1 = await Page.findOne({ path: path1, isEmpty: true });
-      const _page2 = await Page.findOne({ path: path2, isEmpty: false });
-      expect(_page1).toBeTruthy();
-      expect(_page2).toBeTruthy();
-
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, _page2.path);
-      const page1 = await Page.findOne({ path: path1, isEmpty: true }); // parent
-      const page2 = await Page.findOne({ path: path2, isEmpty: false });
-
-      // check for the parent (should be the same as page1)
-      expect(parent._id).toStrictEqual(page1._id);
-      expect(parent.path).toStrictEqual(page1.path);
-      expect(parent.parent).toStrictEqual(page1.parent);
-
-      expect(page1.parent).toStrictEqual(rootPage._id);
-      expect(page2.parent).toStrictEqual(page1._id);
-    });
-    test('should find parent while NOT updating private legacy page\'s parent', async() => {
-      const path1 = '/emp_anc4';
-      const path2 = '/emp_anc4/PAF4';
-      const _page1 = await Page.findOne({ path: path1, isEmpty: true, grant: Page.GRANT_PUBLIC });
-      const _page2 = await Page.findOne({ path: path2, isEmpty: false, grant: Page.GRANT_PUBLIC });
-      const _page3 = await Page.findOne({ path: path1, isEmpty: false, grant: Page.GRANT_OWNER });
-      expect(_page1).toBeTruthy();
-      expect(_page2).toBeTruthy();
-      expect(_page3).toBeTruthy();
-      expect(_page3.parent).toBeNull();
-
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, _page2.path);
-      const page1 = await Page.findOne({ path: path1, isEmpty: true, grant: Page.GRANT_PUBLIC });
-      const page2 = await Page.findOne({ path: path2, isEmpty: false, grant: Page.GRANT_PUBLIC });
-      const page3 = await Page.findOne({ path: path1, isEmpty: false, grant: Page.GRANT_OWNER });
-      expect(page1).toBeTruthy();
-      expect(page2).toBeTruthy();
-      expect(page3).toBeTruthy();
-      expect(page3.parent).toBeNull(); // parent property of page in private legacy pages should be null
-
-      expect(page1._id).toStrictEqual(parent._id);
-      expect(page2.parent).toStrictEqual(parent._id);
-
-    });
-    test('should find parent while NOT creating unnecessary empty pages with all v4 public pages', async() => {
-      // All pages does not have parent (v4 schema)
-      const _pageA = await Page.findOne({
-        path: '/get_parent_A',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: false,
-        parent: null,
-      });
-      const _pageAB = await Page.findOne({
-        path: '/get_parent_A/get_parent_B',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: false,
-        parent: null,
-      });
-      const _emptyA = await Page.findOne({
-        path: '/get_parent_A',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-      });
-      const _emptyAB = await Page.findOne({
-        path: '/get_parent_A/get_parent_B',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-      });
-
-      expect(_pageA).not.toBeNull();
-      expect(_pageAB).not.toBeNull();
-      expect(_emptyA).toBeNull();
-      expect(_emptyAB).toBeNull();
-
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, '/get_parent_A/get_parent_B/get_parent_C');
-
-      const pageA = await Page.findOne({ path: '/get_parent_A', grant: Page.GRANT_PUBLIC, isEmpty: false });
-      const pageAB = await Page.findOne({ path: '/get_parent_A/get_parent_B', grant: Page.GRANT_PUBLIC, isEmpty: false });
-      const emptyA = await Page.findOne({ path: '/get_parent_A', grant: Page.GRANT_PUBLIC, isEmpty: true });
-      const emptyAB = await Page.findOne({ path: '/get_parent_A/get_parent_B', grant: Page.GRANT_PUBLIC, isEmpty: true });
-
-      // -- Check existance
-      expect(parent).not.toBeNull();
-      expect(pageA).not.toBeNull();
-      expect(pageAB).not.toBeNull();
-      expect(emptyA).toBeNull();
-      expect(emptyAB).toBeNull();
-
-      // -- Check parent
-      expect(pageA.parent).not.toBeNull();
-      expect(pageAB.parent).not.toBeNull();
-    });
-    test('should find parent while NOT creating unnecessary empty pages with some v5 public pages', async() => {
-      const _pageC = await Page.findOne({
-        path: '/get_parent_C',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: false,
-        parent: { $ne: null },
-      });
-      const _pageCD = await Page.findOne({
-        path: '/get_parent_C/get_parent_D',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: false,
-      });
-      const _emptyC = await Page.findOne({
-        path: '/get_parent_C',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-      });
-      const _emptyCD = await Page.findOne({
-        path: '/get_parent_C/get_parent_D',
-        grant: Page.GRANT_PUBLIC,
-        isEmpty: true,
-      });
-
-      expect(_pageC).not.toBeNull();
-      expect(_pageCD).not.toBeNull();
-      expect(_emptyC).toBeNull();
-      expect(_emptyCD).toBeNull();
-
-      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, '/get_parent_C/get_parent_D/get_parent_E');
-
-      const pageC = await Page.findOne({ path: '/get_parent_C', grant: Page.GRANT_PUBLIC, isEmpty: false });
-      const pageCD = await Page.findOne({ path: '/get_parent_C/get_parent_D', grant: Page.GRANT_PUBLIC, isEmpty: false });
-      const emptyC = await Page.findOne({ path: '/get_parent_C', grant: Page.GRANT_PUBLIC, isEmpty: true });
-      const emptyCD = await Page.findOne({ path: '/get_parent_C/get_parent_D', grant: Page.GRANT_PUBLIC, isEmpty: true });
-
-      // -- Check existance
-      expect(parent).not.toBeNull();
-      expect(pageC).not.toBeNull();
-      expect(pageCD).not.toBeNull();
-      expect(emptyC).toBeNull();
-      expect(emptyCD).toBeNull();
-
-      // -- Check parent attribute
-      expect(pageC.parent).toStrictEqual(rootPage._id);
-      expect(pageCD.parent).toStrictEqual(pageC._id);
-
-      // -- Check the found parent
-      expect(parent.toObject()).toStrictEqual(pageCD.toObject());
-    });
-  });
 });

+ 270 - 0
packages/app/test/integration/service/page.test.js

@@ -8,6 +8,8 @@ const mongoose = require('mongoose');
 
 const { getInstance } = require('../setup-crowi');
 
+let rootPage;
+let dummyUser1;
 let testUser1;
 let testUser2;
 let parentTag;
@@ -79,6 +81,10 @@ describe('PageService', () => {
     testUser1 = await User.findOne({ username: 'someone1' });
     testUser2 = await User.findOne({ username: 'someone2' });
 
+    dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
+
+    rootPage = await Page.findOne({ path: '/' });
+
     await Page.insertMany([
       {
         path: '/parentForRename1',
@@ -290,6 +296,92 @@ describe('PageService', () => {
     ]);
 
     xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+
+    /**
+     * getParentAndFillAncestors
+     */
+    const pageIdPAF1 = new mongoose.Types.ObjectId();
+    const pageIdPAF2 = new mongoose.Types.ObjectId();
+    const pageIdPAF3 = new mongoose.Types.ObjectId();
+
+    await Page.insertMany([
+      {
+        _id: pageIdPAF1,
+        path: '/PAF1',
+        grant: Page.GRANT_PUBLIC,
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1._id,
+        isEmpty: false,
+        parent: rootPage._id,
+        descendantCount: 0,
+      },
+      {
+        _id: pageIdPAF2,
+        path: '/emp_anc3',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+        descendantCount: 1,
+        parent: rootPage._id,
+      },
+      {
+        path: '/emp_anc3/PAF3',
+        grant: Page.GRANT_PUBLIC,
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1._id,
+        isEmpty: false,
+        descendantCount: 0,
+        parent: pageIdPAF2,
+      },
+      {
+        _id: pageIdPAF3,
+        path: '/emp_anc4',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+        descendantCount: 1,
+        parent: rootPage._id,
+      },
+      {
+        path: '/emp_anc4/PAF4',
+        grant: Page.GRANT_PUBLIC,
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1._id,
+        isEmpty: false,
+        descendantCount: 0,
+        parent: pageIdPAF3,
+      },
+      {
+        path: '/emp_anc4',
+        grant: Page.GRANT_OWNER,
+        grantedUsers: [dummyUser1._id],
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1._id,
+        isEmpty: false,
+      },
+      {
+        path: '/get_parent_A',
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1,
+        parent: null,
+      },
+      {
+        path: '/get_parent_A/get_parent_B',
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1,
+        parent: null,
+      },
+      {
+        path: '/get_parent_C',
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1,
+        parent: rootPage._id,
+      },
+      {
+        path: '/get_parent_C/get_parent_D',
+        creator: dummyUser1,
+        lastUpdateUser: dummyUser1,
+        parent: null,
+      },
+    ]);
   });
 
   describe('rename page without using renameDescendantsWithStreamSpy', () => {
@@ -723,4 +815,182 @@ describe('PageService', () => {
     });
   });
 
+  describe('getParentAndFillAncestors', () => {
+    test('return parent if exist', async() => {
+      const page1 = await Page.findOne({ path: '/PAF1' });
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, page1.path);
+      expect(parent).toBeTruthy();
+      expect(page1.parent).toStrictEqual(parent._id);
+    });
+    test('create parent and ancestors when they do not exist, and return the new parent', async() => {
+      const path1 = '/emp_anc1';
+      const path2 = '/emp_anc1/emp_anc2';
+      const path3 = '/emp_anc1/emp_anc2/PAF2';
+      const _page1 = await Page.findOne({ path: path1 }); // not exist
+      const _page2 = await Page.findOne({ path: path2 }); // not exist
+      const _page3 = await Page.findOne({ path: path3 }); // not exist
+      expect(_page1).toBeNull();
+      expect(_page2).toBeNull();
+      expect(_page3).toBeNull();
+
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, path3);
+      const page1 = await Page.findOne({ path: path1 });
+      const page2 = await Page.findOne({ path: path2 });
+      const page3 = await Page.findOne({ path: path3 });
+
+      expect(parent._id).toStrictEqual(page2._id);
+      expect(parent.path).toStrictEqual(page2.path);
+      expect(parent.parent).toStrictEqual(page2.parent);
+
+      expect(parent).toBeTruthy();
+      expect(page1).toBeTruthy();
+      expect(page2).toBeTruthy();
+      expect(page3).toBeNull();
+
+      expect(page1.parent).toStrictEqual(rootPage._id);
+      expect(page2.parent).toStrictEqual(page1._id);
+    });
+    test('return parent even if the parent page is empty', async() => {
+      const path1 = '/emp_anc3';
+      const path2 = '/emp_anc3/PAF3';
+      const _page1 = await Page.findOne({ path: path1, isEmpty: true });
+      const _page2 = await Page.findOne({ path: path2, isEmpty: false });
+      expect(_page1).toBeTruthy();
+      expect(_page2).toBeTruthy();
+
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, _page2.path);
+      const page1 = await Page.findOne({ path: path1, isEmpty: true }); // parent
+      const page2 = await Page.findOne({ path: path2, isEmpty: false });
+
+      // check for the parent (should be the same as page1)
+      expect(parent._id).toStrictEqual(page1._id);
+      expect(parent.path).toStrictEqual(page1.path);
+      expect(parent.parent).toStrictEqual(page1.parent);
+
+      expect(page1.parent).toStrictEqual(rootPage._id);
+      expect(page2.parent).toStrictEqual(page1._id);
+    });
+    test('should find parent while NOT updating private legacy page\'s parent', async() => {
+      const path1 = '/emp_anc4';
+      const path2 = '/emp_anc4/PAF4';
+      const _page1 = await Page.findOne({ path: path1, isEmpty: true, grant: Page.GRANT_PUBLIC });
+      const _page2 = await Page.findOne({ path: path2, isEmpty: false, grant: Page.GRANT_PUBLIC });
+      const _page3 = await Page.findOne({ path: path1, isEmpty: false, grant: Page.GRANT_OWNER });
+      expect(_page1).toBeTruthy();
+      expect(_page2).toBeTruthy();
+      expect(_page3).toBeTruthy();
+      expect(_page3.parent).toBeNull();
+
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, _page2.path);
+      const page1 = await Page.findOne({ path: path1, isEmpty: true, grant: Page.GRANT_PUBLIC });
+      const page2 = await Page.findOne({ path: path2, isEmpty: false, grant: Page.GRANT_PUBLIC });
+      const page3 = await Page.findOne({ path: path1, isEmpty: false, grant: Page.GRANT_OWNER });
+      expect(page1).toBeTruthy();
+      expect(page2).toBeTruthy();
+      expect(page3).toBeTruthy();
+      expect(page3.parent).toBeNull(); // parent property of page in private legacy pages should be null
+
+      expect(page1._id).toStrictEqual(parent._id);
+      expect(page2.parent).toStrictEqual(parent._id);
+
+    });
+    test('should find parent while NOT creating unnecessary empty pages with all v4 public pages', async() => {
+      // All pages does not have parent (v4 schema)
+      const _pageA = await Page.findOne({
+        path: '/get_parent_A',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: false,
+        parent: null,
+      });
+      const _pageAB = await Page.findOne({
+        path: '/get_parent_A/get_parent_B',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: false,
+        parent: null,
+      });
+      const _emptyA = await Page.findOne({
+        path: '/get_parent_A',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+      });
+      const _emptyAB = await Page.findOne({
+        path: '/get_parent_A/get_parent_B',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+      });
+
+      expect(_pageA).not.toBeNull();
+      expect(_pageAB).not.toBeNull();
+      expect(_emptyA).toBeNull();
+      expect(_emptyAB).toBeNull();
+
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, '/get_parent_A/get_parent_B/get_parent_C');
+
+      const pageA = await Page.findOne({ path: '/get_parent_A', grant: Page.GRANT_PUBLIC, isEmpty: false });
+      const pageAB = await Page.findOne({ path: '/get_parent_A/get_parent_B', grant: Page.GRANT_PUBLIC, isEmpty: false });
+      const emptyA = await Page.findOne({ path: '/get_parent_A', grant: Page.GRANT_PUBLIC, isEmpty: true });
+      const emptyAB = await Page.findOne({ path: '/get_parent_A/get_parent_B', grant: Page.GRANT_PUBLIC, isEmpty: true });
+
+      // -- Check existance
+      expect(parent).not.toBeNull();
+      expect(pageA).not.toBeNull();
+      expect(pageAB).not.toBeNull();
+      expect(emptyA).toBeNull();
+      expect(emptyAB).toBeNull();
+
+      // -- Check parent
+      expect(pageA.parent).not.toBeNull();
+      expect(pageAB.parent).not.toBeNull();
+    });
+    test('should find parent while NOT creating unnecessary empty pages with some v5 public pages', async() => {
+      const _pageC = await Page.findOne({
+        path: '/get_parent_C',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: false,
+        parent: { $ne: null },
+      });
+      const _pageCD = await Page.findOne({
+        path: '/get_parent_C/get_parent_D',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: false,
+      });
+      const _emptyC = await Page.findOne({
+        path: '/get_parent_C',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+      });
+      const _emptyCD = await Page.findOne({
+        path: '/get_parent_C/get_parent_D',
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+      });
+
+      expect(_pageC).not.toBeNull();
+      expect(_pageCD).not.toBeNull();
+      expect(_emptyC).toBeNull();
+      expect(_emptyCD).toBeNull();
+
+      const parent = await crowi.pageService.getParentAndFillAncestorsByUser(dummyUser1, '/get_parent_C/get_parent_D/get_parent_E');
+
+      const pageC = await Page.findOne({ path: '/get_parent_C', grant: Page.GRANT_PUBLIC, isEmpty: false });
+      const pageCD = await Page.findOne({ path: '/get_parent_C/get_parent_D', grant: Page.GRANT_PUBLIC, isEmpty: false });
+      const emptyC = await Page.findOne({ path: '/get_parent_C', grant: Page.GRANT_PUBLIC, isEmpty: true });
+      const emptyCD = await Page.findOne({ path: '/get_parent_C/get_parent_D', grant: Page.GRANT_PUBLIC, isEmpty: true });
+
+      // -- Check existance
+      expect(parent).not.toBeNull();
+      expect(pageC).not.toBeNull();
+      expect(pageCD).not.toBeNull();
+      expect(emptyC).toBeNull();
+      expect(emptyCD).toBeNull();
+
+      // -- Check parent attribute
+      expect(pageC.parent).toStrictEqual(rootPage._id);
+      expect(pageCD.parent).toStrictEqual(pageC._id);
+
+      // -- Check the found parent
+      expect(parent.toObject()).toStrictEqual(pageCD.toObject());
+    });
+  });
+
 });

+ 2 - 2
packages/plugin-attachment-refs/src/client-entry.js

@@ -1,9 +1,9 @@
 import RefsPostRenderInterceptor from './client/js/util/Interceptor/RefsPostRenderInterceptor';
 import RefsPreRenderInterceptor from './client/js/util/Interceptor/RefsPreRenderInterceptor';
 
-export default (appContainer) => {
+export default () => {
   // add interceptors
-  appContainer.interceptorManager.addInterceptors([
+  global.interceptorManager.addInterceptors([
     new RefsPreRenderInterceptor(),
     new RefsPostRenderInterceptor(),
   ]);

+ 3 - 3
packages/plugin-lsx/src/client-entry.js

@@ -2,11 +2,11 @@ import { LsxLogoutInterceptor } from './client/js/util/Interceptor/LsxLogoutInte
 import { LsxPostRenderInterceptor } from './client/js/util/Interceptor/LsxPostRenderInterceptor';
 import { LsxPreRenderInterceptor } from './client/js/util/Interceptor/LsxPreRenderInterceptor';
 
-export default (appContainer) => {
+export default () => {
   // add interceptors
-  appContainer.interceptorManager.addInterceptors([
+  global.interceptorManager.addInterceptors([
     new LsxLogoutInterceptor(),
     new LsxPreRenderInterceptor(),
-    new LsxPostRenderInterceptor(appContainer),
+    new LsxPostRenderInterceptor(),
   ]);
 };

+ 0 - 1
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -239,7 +239,6 @@ export class Lsx extends React.Component {
 }
 
 Lsx.propTypes = {
-  appContainer: PropTypes.object.isRequired,
   lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
 
   forceToFetchData: PropTypes.bool,

+ 3 - 7
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

@@ -1,5 +1,6 @@
-import { BasicInterceptor } from '@growi/core';
 import React from 'react';
+
+import { BasicInterceptor } from '@growi/core';
 import ReactDOM from 'react-dom';
 
 
@@ -13,11 +14,6 @@ import { LsxContext } from '../LsxContext';
  */
 export class LsxPostRenderInterceptor extends BasicInterceptor {
 
-  constructor(appContainer) {
-    super();
-    this.appContainer = appContainer;
-  }
-
   /**
    * @inheritdoc
    */
@@ -54,7 +50,7 @@ export class LsxPostRenderInterceptor extends BasicInterceptor {
 
   renderReactDOM(lsxContext, elem, isPreview) {
     ReactDOM.render(
-      <Lsx appContainer={this.appContainer} lsxContext={lsxContext} forceToFetchData={!isPreview} />,
+      <Lsx lsxContext={lsxContext} forceToFetchData={!isPreview} />,
       elem,
     );
   }