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

Merge branch 'master' into support/gw7739-vrt-search

Mudana-Grune 3 лет назад
Родитель
Сommit
c4aac8a40e
47 измененных файлов с 300 добавлено и 250 удалено
  1. 1 0
      packages/app/config/webpack.common.js
  2. 1 0
      packages/app/resource/locales/en_US/translation.json
  3. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  4. 2 1
      packages/app/resource/locales/zh_CN/translation.json
  5. 2 0
      packages/app/src/client/base.jsx
  6. 60 0
      packages/app/src/client/installer.jsx
  7. 2 1
      packages/app/src/client/legacy/crowi.js
  8. 67 65
      packages/app/src/client/nologin.jsx
  9. 0 15
      packages/app/src/client/services/AppContainer.js
  10. 2 3
      packages/app/src/client/services/CommentContainer.js
  11. 8 1
      packages/app/src/client/services/ContextExtractor.tsx
  12. 5 3
      packages/app/src/client/services/PageContainer.js
  13. 7 0
      packages/app/src/client/util/apiv1-client.ts
  14. 12 3
      packages/app/src/client/util/apiv3-client.ts
  15. 2 1
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  16. 2 2
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  17. 1 4
      packages/app/src/components/Admin/App/AwsSetting.jsx
  18. 3 13
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  19. 9 6
      packages/app/src/components/Common/CountBadge.tsx
  20. 6 9
      packages/app/src/components/Drawio.tsx
  21. 6 3
      packages/app/src/components/InstallerForm.jsx
  22. 11 6
      packages/app/src/components/LoginForm.jsx
  23. 5 16
      packages/app/src/components/Me/ProfileImageSettings.tsx
  24. 1 1
      packages/app/src/components/MyDraftList/Draft.jsx
  25. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  26. 6 1
      packages/app/src/components/Page/RevisionBody.jsx
  27. 2 2
      packages/app/src/components/Page/RevisionRenderer.jsx
  28. 2 2
      packages/app/src/components/PageComment/Comment.jsx
  29. 5 6
      packages/app/src/components/PageComment/CommentEditor.jsx
  30. 15 20
      packages/app/src/components/PageComment/CommentPreview.jsx
  31. 9 12
      packages/app/src/components/PageEditor.tsx
  32. 5 2
      packages/app/src/components/PageEditor/Preview.tsx
  33. 1 9
      packages/app/src/components/PasswordResetExecutionForm.jsx
  34. 1 12
      packages/app/src/components/PasswordResetRequestForm.jsx
  35. 0 6
      packages/app/src/server/crowi/index.js
  36. 6 0
      packages/app/src/server/models/errors.ts
  37. 5 0
      packages/app/src/server/models/external-account.js
  38. 2 1
      packages/app/src/server/routes/apiv3/page.js
  39. 0 1
      packages/app/src/server/routes/index.js
  40. 7 1
      packages/app/src/server/routes/login-passport.js
  41. 0 4
      packages/app/src/server/routes/page.js
  42. 4 3
      packages/app/src/server/views/installer.html
  43. 4 0
      packages/app/src/stores/context.tsx
  44. 2 2
      packages/plugin-attachment-refs/src/client-entry.js
  45. 3 3
      packages/plugin-lsx/src/client-entry.js
  46. 0 1
      packages/plugin-lsx/src/client/js/components/Lsx.jsx
  47. 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',

+ 1 - 0
packages/app/resource/locales/en_US/translation.json

@@ -984,6 +984,7 @@
     "application_already_installed": "Application already installed.",
     "email_address_could_not_be_used": "This email address could not be used. (Make sure the allowed email address)",
     "user_id_is_not_available":"This User ID is not available.",
+    "username_should_not_be_null":"Username should not be null. Please check Authentication Mechanism Settings on admin page",
     "email_address_is_already_registered":"This email address is already registered.",
     "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
     "failed_to_register":"Failed to register.",

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -977,6 +977,7 @@
     "application_already_installed": "アプリケーションのインストールが完了しました。",
     "email_address_could_not_be_used":"このメールアドレスは使用できません。(許可されたメールアドレスを確認してください。)",
     "user_id_is_not_available":"このユーザーIDは使用できません。",
+    "username_should_not_be_null":"Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
     "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
     "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
     "failed_to_register":"登録に失敗しました。",

+ 2 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -986,7 +986,8 @@
 		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
 		"application_already_installed": "应用程序已安装。",
 		"email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
-		"user_id_is_not_available": "此用户ID不可用。",
+    "user_id_is_not_available": "此用户ID不可用。",
+    "username_should_not_be_null":"用户名不应为空。请检查管理页面上的身份验证机制设置",
 		"email_address_is_already_registered": "此电子邮件地址已注册。",
 		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
 		"failed_to_register": "注册失败。",

+ 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);
   }
 
 }

+ 8 - 1
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);
 

+ 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);

+ 9 - 6
packages/app/src/components/Common/CountBadge.tsx

@@ -1,16 +1,19 @@
 import React, { FC } from 'react';
 
 type CountProps = {
-  count: number
+  count?: number,
+  offset?: number,
 }
 
 const CountBadge: FC<CountProps> = (props:CountProps) => {
+  const { count, offset = 0 } = props;
+
+
   return (
-    <>
-      <span className="grw-count-badge px-2 badge badge-pill badge-light">
-        {props.count}
-      </span>
-    </>
+    <span className="grw-count-badge px-2 badge badge-pill badge-light">
+      { count == null && <span className="text-muted">―</span> }
+      { count != null && count + offset }
+    </span>
   );
 };
 

+ 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/DisplaySwitcher.tsx

@@ -76,7 +76,7 @@ const DisplaySwitcher = (): JSX.Element => {
                           <PageListIcon />
                         </div>
                         {t('page_list')}
-                        {currentPage?.descendantCount != null && <CountBadge count={currentPage.descendantCount + 1} />}
+                        <CountBadge count={currentPage?.descendantCount} offset={1} />
                       </button>
                     ) }
                   </div>
@@ -91,7 +91,7 @@ const DisplaySwitcher = (): JSX.Element => {
                       >
                         <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
                         <span>Comments</span>
-                        {currentPage?.commentCount != null && <CountBadge count={currentPage.commentCount} />}
+                        <CountBadge count={currentPage?.commentCount} />
                       </button>
                     </div>
                   ) }

+ 6 - 1
packages/app/src/components/Page/RevisionBody.jsx

@@ -58,7 +58,12 @@ export default class RevisionBody extends React.PureComponent {
     const additionalClassName = this.props.additionalClassName || '';
     return (
       <div
-        ref={this.props.inputRef}
+        ref={(elem) => {
+          this.element = elem;
+          if (this.props.inputRef != null) {
+            this.props.inputRef.current = elem;
+          }
+        }}
         id="wiki"
         className={`wiki ${additionalClassName}`}
         // eslint-disable-next-line react/no-danger

+ 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);

+ 5 - 6
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { UserPicture } from '@growi/ui';
 import PropTypes from 'prop-types';
@@ -224,7 +224,6 @@ class CommentEditor extends React.Component {
   getCommentHtml() {
     return (
       <CommentPreview
-        inputRef={(el) => { this.previewElement = el }}
         html={this.state.html}
       />
     );
@@ -236,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(() => {
@@ -281,7 +280,7 @@ class CommentEditor extends React.Component {
   }
 
   renderReady() {
-    const { appContainer, commentContainer, isMobile } = this.props;
+    const { isMobile } = this.props;
     const { activeTab } = this.state;
 
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
@@ -446,9 +445,9 @@ const CommentEditorWrapper = (props) => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
 
-  const onSlackEnabledFlagChange = (isSlackEnabled) => {
+  const onSlackEnabledFlagChange = useCallback((isSlackEnabled) => {
     mutateIsSlackEnabled(isSlackEnabled, false);
-  };
+  }, [mutateIsSlackEnabled]);
 
   return (
     <CommentEditorHOCWrapper

+ 15 - 20
packages/app/src/components/PageComment/CommentPreview.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
 import RevisionBody from '../Page/RevisionBody';
@@ -6,29 +7,23 @@ import RevisionBody from '../Page/RevisionBody';
 /**
  * Wrapper component for Page/RevisionBody
  */
-export default class CommentPreview extends React.Component {
-
-  render() {
-    return (
-      <div
-        className="page-comment-preview-body"
-        ref={(elm) => {
-          this.previewElement = elm;
-          this.props.inputRef(elm);
-        }}
-      >
+const CommentPreview = (props) => {
 
-        <RevisionBody
-          {...this.props}
-          additionalClassName="comment"
-        />
-      </div>
-    );
-  }
+  return (
+    <div className="page-comment-preview-body">
+      <RevisionBody
+        html={props.html}
+        additionalClassName="comment"
+        isMathJaxEnabled
+        renderMathJaxInRealtime
+      />
+    </div>
+  );
 
-}
+};
 
 CommentPreview.propTypes = {
   html: PropTypes.string,
-  inputRef: PropTypes.func.isRequired, // for getting div element
 };
+
+export default CommentPreview;

+ 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;
 };

+ 6 - 0
packages/app/src/server/models/errors.ts

@@ -10,3 +10,9 @@ export class PathAlreadyExistsError extends ExtensibleCustomError {
   }
 
 }
+
+
+/*
+* User Authentication
+*/
+export class NullUsernameToBeRegisteredError extends ExtensibleCustomError {}

+ 5 - 0
packages/app/src/server/models/external-account.js

@@ -1,5 +1,6 @@
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
+import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 
 const debug = require('debug')('growi:models:external-account');
 const mongoose = require('mongoose');
@@ -104,6 +105,10 @@ class ExternalAccount {
           return account;
         }
 
+        if (usernameToBeRegistered == null) {
+          throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
+        }
+
         const User = ExternalAccount.crowi.model('User');
 
         let promise = User.findOne({ username: usernameToBeRegistered });

+ 2 - 1
packages/app/src/server/routes/apiv3/page.js

@@ -246,7 +246,8 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/Page'
    */
   router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
-    const { pageId, path, user } = req.query;
+    const { user } = req;
+    const { pageId, path } = req.query;
 
     if (pageId == null && path == null) {
       return res.apiv3Err(new ErrorV3('Parameter path or pageId is required.', 'invalid-request'));

+ 0 - 1
packages/app/src/server/routes/index.js

@@ -78,7 +78,6 @@ module.exports = function(crowi, app) {
   app.get('/login/invited'            , applicationInstalled, login.invited);
   app.post('/login/activateInvited'   , apiLimiter , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrf, login.invited);
   app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
-  app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   app.post('/register'                , apiLimiter , applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrf, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);

+ 7 - 1
packages/app/src/server/routes/login-passport.js

@@ -1,3 +1,4 @@
+import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 
 /* eslint-disable no-use-before-define */
@@ -112,6 +113,7 @@ module.exports = function(crowi, app) {
     const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
     const nameToBeRegistered = ldapAccountInfo[attrMapName];
     const mailToBeRegistered = ldapAccountInfo[attrMapMail];
+
     const userInfo = {
       id: ldapAccountId,
       username: usernameToBeRegistered,
@@ -578,7 +580,11 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       /* eslint-disable no-else-return */
-      if (err.name === 'DuplicatedUsernameException') {
+      if (err instanceof NullUsernameToBeRegisteredError) {
+        req.flash('warningMessage', req.t(`message.${err.message}`));
+        return;
+      }
+      else if (err.name === 'DuplicatedUsernameException') {
         if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
           // associate to existing user
           debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);

+ 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);
   };
 

+ 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);
 };

+ 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,
     );
   }