فهرست منبع

Merge branch 'master' into imprv/master-gw4303

白石誠 5 سال پیش
والد
کامیت
7ddeeab614

+ 2 - 0
resource/locales/en_US/admin/admin.json

@@ -46,6 +46,8 @@
     "fixed_by_env_var": "This is fixed by the env var <code>FILE_UPLOAD={{fileUploadType}}</code>.",
     "fixed_by_env_var": "This is fixed by the env var <code>FILE_UPLOAD={{fileUploadType}}</code>.",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
+    "local_label": "Local",
+    "gridfs_label": "MongoDB(GridFS)",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "ses_settings":"SES settings",
     "ses_settings":"SES settings",
     "test_connection": "Test connection to mail",
     "test_connection": "Test connection to mail",

+ 2 - 0
resource/locales/ja_JP/admin/admin.json

@@ -45,6 +45,8 @@
     "file_upload_method":"ファイルアップロード方法",
     "file_upload_method":"ファイルアップロード方法",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
+    "local_label": "Local",
+    "gridfs_label": "MongoDB(GridFS)",
     "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
     "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
     "ses_settings":"SES設定",
     "ses_settings":"SES設定",

+ 2 - 2
resource/locales/zh_CN/admin/admin.json

@@ -45,8 +45,8 @@
     "file_upload_method":"文件上传方法",
     "file_upload_method":"文件上传方法",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
-		"fixed_by_env_var": "这是由env var<code>FILE_UPLOAD={{fileUploadType}}</code>修复的。",
-    "file_upload": "这是文件上传设定。完成了文件上传设定以后,文件上传功能、档案头像功能将会被开启。",
+    "local_label": "Local",
+    "gridfs_label": "MongoDB(GridFS)",
     "ses_settings":"SES设置",
     "ses_settings":"SES设置",
     "test_connection": "测试邮件服务器连接",
     "test_connection": "测试邮件服务器连接",
 		"": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
 		"": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",

+ 3 - 1
src/client/js/app.jsx

@@ -29,6 +29,7 @@ import BookmarkList from './components/PageList/BookmarkList';
 import SeenUserList from './components/User/SeenUserList';
 import SeenUserList from './components/User/SeenUserList';
 import LikerList from './components/User/LikerList';
 import LikerList from './components/User/LikerList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
+import PageAccessories from './components/PageAccessories';
 import UserInfo from './components/User/UserInfo';
 import UserInfo from './components/User/UserInfo';
 import Fab from './components/Fab';
 import Fab from './components/Fab';
 
 
@@ -107,7 +108,8 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-management': <PageManagement />,
     'page-management': <PageManagement />,
-    'revision-toc': <TableOfContents isGuestUserMode={appContainer.currentUser == null} />,
+    'page-accessories': <PageAccessories />,
+    'revision-toc': <TableOfContents />,
     'seen-user-list': <SeenUserList />,
     'seen-user-list': <SeenUserList />,
     'liker-list': <LikerList />,
     'liker-list': <LikerList />,
 
 

+ 3 - 5
src/client/js/components/Admin/App/FileUploadSetting.jsx

@@ -16,7 +16,7 @@ function FileUploadSetting(props) {
 
 
   const { t, adminAppContainer } = props;
   const { t, adminAppContainer } = props;
   const { fileUploadType } = adminAppContainer.state;
   const { fileUploadType } = adminAppContainer.state;
-  const fileUploadTypes = ['aws', 'gcs'];
+  const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
 
 
   async function submitHandler() {
   async function submitHandler() {
     const { t } = props;
     const { t } = props;
@@ -42,7 +42,7 @@ function FileUploadSetting(props) {
         </span>
         </span>
       </p>
       </p>
 
 
-      <div className="row form-group mb-5">
+      <div className="row form-group mb-3">
         <label className="text-left text-md-right col-md-3 col-form-label">
         <label className="text-left text-md-right col-md-3 col-form-label">
           {t('admin:app_setting.file_upload_method')}
           {t('admin:app_setting.file_upload_method')}
         </label>
         </label>
@@ -58,9 +58,7 @@ function FileUploadSetting(props) {
                     id={`file-upload-type-radio-${type}`}
                     id={`file-upload-type-radio-${type}`}
                     checked={adminAppContainer.state.fileUploadType === type}
                     checked={adminAppContainer.state.fileUploadType === type}
                     disabled={adminAppContainer.state.isFixedFileUploadByEnvVar}
                     disabled={adminAppContainer.state.isFixedFileUploadByEnvVar}
-                    onChange={(e) => {
-                    adminAppContainer.changeFileUploadType(type);
-                  }}
+                    onChange={() => { adminAppContainer.changeFileUploadType(type) }}
                   />
                   />
                   <label className="custom-control-label" htmlFor={`file-upload-type-radio-${type}`}>{t(`admin:app_setting.${type}_label`)}</label>
                   <label className="custom-control-label" htmlFor={`file-upload-type-radio-${type}`}>{t(`admin:app_setting.${type}_label`)}</label>
                 </div>
                 </div>

+ 1 - 1
src/client/js/components/Admin/App/GcsSettings.jsx

@@ -19,7 +19,7 @@ function GcsSetting(props) {
         <p
         <p
           className="alert alert-info"
           className="alert alert-info"
           // eslint-disable-next-line react/no-danger
           // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('admin:app_setting.note_for_the_only_env_option', { env: 'IS_GCS_ENV_PRIORITIZED' }) }}
+          dangerouslySetInnerHTML={{ __html: t('admin:app_setting.note_for_the_only_env_option', { env: 'GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
         />
         />
       )}
       )}
       <table className={`table settings-table ${gcsUseOnlyEnvVars && 'use-only-env-vars'}`}>
       <table className={`table settings-table ${gcsUseOnlyEnvVars && 'use-only-env-vars'}`}>

+ 41 - 0
src/client/js/components/PageAccessories.jsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import PageAccessoriesModalControl from './PageAccessoriesModalControl';
+import PageAccessoriesModal from './PageAccessoriesModal';
+
+import { withUnstatedContainers } from './UnstatedUtils';
+import AppContainer from '../services/AppContainer';
+import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
+
+const PageAccessories = (props) => {
+  const { appContainer, pageAccessoriesContainer } = props;
+  const isGuestUserMode = appContainer.currentUser == null;
+
+  // not render only when this page is shared and user is not login.
+  if (appContainer.isSharedUser && isGuestUserMode) {
+    return null;
+  }
+
+  return (
+    <>
+      <PageAccessoriesModalControl isGuestUserMode={isGuestUserMode} />
+      <PageAccessoriesModal
+        isGuestUserMode={isGuestUserMode}
+        isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
+        onClose={pageAccessoriesContainer.closePageAccessoriesModal}
+      />
+    </>
+  );
+};
+/**
+ * Wrapper component for using unstated
+ */
+const PageAccessoriesWrapper = withUnstatedContainers(PageAccessories, [AppContainer, PageAccessoriesContainer]);
+
+PageAccessories.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
+};
+
+export default PageAccessoriesWrapper;

+ 91 - 0
src/client/js/components/PageAccessoriesModalControl.jsx

@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { UncontrolledTooltip } from 'reactstrap';
+import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
+
+import PageListIcon from './Icons/PageListIcon';
+import TimeLineIcon from './Icons/TimeLineIcon';
+import HistoryIcon from './Icons/HistoryIcon';
+import AttachmentIcon from './Icons/AttachmentIcon';
+import ShareLinkIcon from './Icons/ShareLinkIcon';
+
+import { withUnstatedContainers } from './UnstatedUtils';
+
+const PageAccessoriesModalControl = (props) => {
+  const { t, pageAccessoriesContainer, isGuestUserMode } = props;
+
+  return (
+    <div className="top-of-table-contents d-flex align-items-end pb-1">
+      <button
+        type="button"
+        className="btn btn-link grw-btn-top-of-table"
+        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pagelist')}
+      >
+        <PageListIcon />
+      </button>
+
+      <button
+        type="button"
+        className="btn btn-link grw-btn-top-of-table"
+        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('timeline')}
+      >
+        <TimeLineIcon />
+      </button>
+
+      <button
+        type="button"
+        className="btn btn-link grw-btn-top-of-table"
+        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
+      >
+        <HistoryIcon />
+      </button>
+
+      <button
+        type="button"
+        className="btn btn-link grw-btn-top-of-table"
+        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('attachment')}
+      >
+        <AttachmentIcon />
+      </button>
+
+      <div id="shareLink-btn-wrapper-for-tooltip">
+        <button
+          type="button"
+          className={`btn btn-link grw-btn-top-of-table ${isGuestUserMode && 'disabled'}`}
+          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('shareLink')}
+        >
+          <ShareLinkIcon />
+        </button>
+      </div>
+      {isGuestUserMode && (
+        <UncontrolledTooltip placement="top" target="shareLink-btn-wrapper-for-tooltip" fade={false}>
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      )}
+      <div
+        id="seen-user-list"
+        data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
+        data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
+        className="grw-seen-user-list ml-1 pl-1"
+      >
+      </div>
+    </div>
+  );
+};
+/**
+ * Wrapper component for using unstated
+ */
+const PageAccessoriesModalControlWrapper = withUnstatedContainers(PageAccessoriesModalControl, [PageAccessoriesContainer]);
+
+PageAccessoriesModalControl.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+
+  pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
+
+  isGuestUserMode: PropTypes.bool.isRequired,
+};
+
+export default withTranslation()(PageAccessoriesModalControlWrapper);

+ 2 - 5
src/client/js/components/TableOfContents.jsx

@@ -8,7 +8,7 @@ import PageContainer from '../services/PageContainer';
 import NavigationContainer from '../services/NavigationContainer';
 import NavigationContainer from '../services/NavigationContainer';
 
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
-import TopOfTableContents from './TopOfTableContents';
+
 import StickyStretchableScroller from './StickyStretchableScroller';
 import StickyStretchableScroller from './StickyStretchableScroller';
 
 
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
@@ -23,7 +23,7 @@ const WIKI_HEADER_LINK = 120;
  */
  */
 const TableOfContents = (props) => {
 const TableOfContents = (props) => {
 
 
-  const { pageContainer, navigationContainer, isGuestUserMode } = props;
+  const { pageContainer, navigationContainer } = props;
   const { pageUser } = pageContainer.state;
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
   const isUserPage = pageUser != null;
 
 
@@ -54,7 +54,6 @@ const TableOfContents = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <TopOfTableContents isGuestUserMode={isGuestUserMode} />
       <StickyStretchableScroller
       <StickyStretchableScroller
         contentsElemSelector=".revision-toc .markdownIt-TOC"
         contentsElemSelector=".revision-toc .markdownIt-TOC"
         stickyElemSelector="#revision-toc"
         stickyElemSelector="#revision-toc"
@@ -103,8 +102,6 @@ const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageCont
 TableOfContents.propTypes = {
 TableOfContents.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-
-  isGuestUserMode: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(TableOfContentsWrapper);
 export default withTranslation()(TableOfContentsWrapper);

+ 0 - 106
src/client/js/components/TopOfTableContents.jsx

@@ -1,106 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import { UncontrolledTooltip } from 'reactstrap';
-import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
-
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
-import HistoryIcon from './Icons/HistoryIcon';
-import AttachmentIcon from './Icons/AttachmentIcon';
-import ShareLinkIcon from './Icons/ShareLinkIcon';
-
-import PageAccessoriesModal from './PageAccessoriesModal';
-
-import { withUnstatedContainers } from './UnstatedUtils';
-
-const TopOfTableContents = (props) => {
-  const { t, pageAccessoriesContainer, isGuestUserMode } = props;
-
-  function renderModal() {
-    return (
-      <PageAccessoriesModal
-        isGuestUserMode={isGuestUserMode}
-        isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
-        onClose={pageAccessoriesContainer.closePageAccessoriesModal}
-      />
-    );
-  }
-
-  return (
-    <>
-      <div className="top-of-table-contents d-flex align-items-end pb-1">
-        <button
-          type="button"
-          className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pagelist')}
-        >
-          <PageListIcon />
-        </button>
-
-        <button
-          type="button"
-          className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('timeline')}
-        >
-          <TimeLineIcon />
-        </button>
-
-        <button
-          type="button"
-          className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
-        >
-          <HistoryIcon />
-        </button>
-
-        <button
-          type="button"
-          className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('attachment')}
-        >
-          <AttachmentIcon />
-        </button>
-
-        <div id="shareLink-btn-wrapper-for-tooltip">
-          <button
-            type="button"
-            className={`btn btn-link grw-btn-top-of-table ${isGuestUserMode && 'disabled'}`}
-            onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('shareLink')}
-          >
-            <ShareLinkIcon />
-          </button>
-        </div>
-        {isGuestUserMode && (
-          <UncontrolledTooltip placement="top" target="shareLink-btn-wrapper-for-tooltip" fade={false}>
-            {t('Not available for guest')}
-          </UncontrolledTooltip>
-        )}
-        <div
-          id="seen-user-list"
-          data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
-          data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
-          className="grw-seen-user-list ml-1 pl-1"
-        >
-        </div>
-      </div>
-      {renderModal()}
-    </>
-  );
-};
-/**
- * Wrapper component for using unstated
- */
-const TopOfTableContentsWrapper = withUnstatedContainers(TopOfTableContents, [PageAccessoriesContainer]);
-
-TopOfTableContents.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-
-  pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
-
-  isGuestUserMode: PropTypes.bool.isRequired,
-};
-
-export default withTranslation()(TopOfTableContentsWrapper);

+ 26 - 47
src/client/js/services/AdminAppContainer.js

@@ -92,6 +92,7 @@ export default class AdminAppContainer extends Container {
 
 
       fileUploadType: appSettingsParams.fileUploadType,
       fileUploadType: appSettingsParams.fileUploadType,
       envFileUploadType: appSettingsParams.envFileUploadType,
       envFileUploadType: appSettingsParams.envFileUploadType,
+      useOnlyEnvVarForFileUploadType: appSettingsParams.useOnlyEnvVarForFileUploadType,
 
 
       s3Region: appSettingsParams.s3Region,
       s3Region: appSettingsParams.s3Region,
       s3CustomEndpoint: appSettingsParams.s3CustomEndpoint,
       s3CustomEndpoint: appSettingsParams.s3CustomEndpoint,
@@ -108,22 +109,15 @@ export default class AdminAppContainer extends Container {
       isEnabledPlugins: appSettingsParams.isEnabledPlugins,
       isEnabledPlugins: appSettingsParams.isEnabledPlugins,
     });
     });
 
 
-    // check is file upload type forced
-    if (this.isFixedFileUploadByEnvVar(appSettingsParams.envFileUploadType)) {
+    // if useOnlyEnvVarForFileUploadType is true, get fileUploadType from only env var and make the forms fixed.
+    // and if env var 'FILE_UPLOAD' is null, envFileUploadType is 'aws' that is default value of 'FILE_UPLOAD'.
+    if (appSettingsParams.useOnlyEnvVarForFileUploadType) {
       this.setState({ fileUploadType: appSettingsParams.envFileUploadType });
       this.setState({ fileUploadType: appSettingsParams.envFileUploadType });
       this.setState({ isFixedFileUploadByEnvVar: true });
       this.setState({ isFixedFileUploadByEnvVar: true });
     }
     }
 
 
   }
   }
 
 
-  /**
-   * get isFixedFileUploadByEnvVar
-   * @return {bool} isFixedFileUploadByEnvVar
-   */
-  isFixedFileUploadByEnvVar(envFileUploadType) {
-    return envFileUploadType != null;
-  }
-
   /**
   /**
    * Change title
    * Change title
    */
    */
@@ -359,48 +353,33 @@ export default class AdminAppContainer extends Container {
   }
   }
 
 
   /**
   /**
-   * Update file upload setting
+   * Update updateFileUploadSettingHandler
    * @memberOf AdminAppContainer
    * @memberOf AdminAppContainer
    */
    */
-  updateFileUploadSettingHandler() {
-    if (this.state.fileUploadType === 'aws') {
-      return this.updateAwsSettingHandler();
+  async updateFileUploadSettingHandler() {
+    const { fileUploadType } = this.state;
+
+    const requestParams = {
+      fileUploadType,
+    };
+
+    if (fileUploadType === 'gcs') {
+      requestParams.gcsApiKeyJsonPath = this.state.gcsApiKeyJsonPath;
+      requestParams.gcsBucket = this.state.gcsBucket;
+      requestParams.gcsUploadNamespace = this.state.gcsUploadNamespace;
     }
     }
-    return this.updateGcsSettingHandler();
-  }
 
 
-  /**
-   * Update AWS setting
-   * @memberOf AdminAppContainer
-   * @return {Array} Appearance
-   */
-  async updateAwsSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/aws-setting', {
-      fileUploadType: this.state.fileUploadType,
-      s3Region: this.state.s3Region,
-      s3CustomEndpoint: this.state.s3CustomEndpoint,
-      s3Bucket: this.state.s3Bucket,
-      s3AccessKeyId: this.state.s3AccessKeyId,
-      s3SecretAccessKey: this.state.s3SecretAccessKey,
-    });
-    const { awsSettingParams } = response.data;
-    return awsSettingParams;
-  }
+    if (fileUploadType === 'aws') {
+      requestParams.s3Region = this.state.s3Region;
+      requestParams.s3CustomEndpoint = this.state.s3CustomEndpoint;
+      requestParams.s3Bucket = this.state.s3Bucket;
+      requestParams.s3AccessKeyId = this.state.s3AccessKeyId;
+      requestParams.s3SecretAccessKey = this.state.s3SecretAccessKey;
+    }
 
 
-  /**
-   * Update GCS setting
-   * @memberOf AdminAppContainer
-   * @return {Array} Appearance
-   */
-  async updateGcsSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/gcs-setting', {
-      fileUploadType: this.state.fileUploadType,
-      gcsApiKeyJsonPath: this.state.gcsApiKeyJsonPath,
-      gcsBucket: this.state.gcsBucket,
-      gcsUploadNamespace: this.state.gcsUploadNamespace,
-    });
-    const { awsSettingParams } = response.data;
-    return awsSettingParams;
+    const response = await this.appContainer.apiv3.put('/app-settings/file-upload-setting', requestParams);
+    const { responseParams } = response.data;
+    return this.setState(responseParams);
   }
   }
 
 
   /**
   /**

+ 1 - 1
src/client/styles/scss/_on-edit.scss

@@ -72,7 +72,7 @@ body.on-edit {
   }
   }
 
 
   // hide unnecessary elements for growi layout
   // hide unnecessary elements for growi layout
-  .revision-toc-container {
+  .side-contents-container {
     display: none !important;
     display: none !important;
   }
   }
 
 

+ 60 - 88
src/server/routes/apiv3/app-settings.js

@@ -86,10 +86,13 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          secretAccessKey:
  *          secretAccessKey:
  *            type: string
  *            type: string
  *            description: secret key for authentification of AWS
  *            description: secret key for authentification of AWS
- *      AwsSettingParams:
- *        description: AwsSettingParams
+ *      FileUploadSettingParams:
+ *        description: FileUploadTypeParams
  *        type: object
  *        type: object
  *        properties:
  *        properties:
+ *          fileUploadType:
+ *            type: string
+ *            description: fileUploadType
  *          region:
  *          region:
  *            type: string
  *            type: string
  *            description: region of AWS S3
  *            description: region of AWS S3
@@ -105,10 +108,6 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          secretAccessKey:
  *          secretAccessKey:
  *            type: string
  *            type: string
  *            description: secret key for authentification of AWS
  *            description: secret key for authentification of AWS
- *      GcsSettingParams:
- *        description: GcsSettingParams
- *        type: object
- *        properties:
  *          gcsApiKeyJsonPath:
  *          gcsApiKeyJsonPath:
  *            type: string
  *            type: string
  *            description: apiKeyJsonPath of gcp
  *            description: apiKeyJsonPath of gcp
@@ -167,17 +166,18 @@ module.exports = (crowi) => {
       body('sesAccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('sesAccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('sesSecretAccessKey').trim(),
       body('sesSecretAccessKey').trim(),
     ],
     ],
-    awsSetting: [
-      body('s3Region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage((value, { req }) => req.t('validation.aws_region')),
-      body('s3CustomEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage((value, { req }) => req.t('validation.aws_custom_endpoint')),
-      body('s3Bucket').trim(),
-      body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
-      body('s3SecretAccessKey').trim(),
-    ],
-    gcsSetting: [
+    fileUploadSetting: [
+      body('fileUploadType').isIn(['aws', 'gcs', 'local', 'gridfs']),
       body('gcsApiKeyJsonPath').trim(),
       body('gcsApiKeyJsonPath').trim(),
       body('gcsBucket').trim(),
       body('gcsBucket').trim(),
       body('gcsUploadNamespace').trim(),
       body('gcsUploadNamespace').trim(),
+      body('s3Region').trim().if(value => value !== '').matches(/^[a-z]+-[a-z]+-\d+$/)
+        .withMessage((value, { req }) => req.t('validation.aws_region')),
+      body('s3CustomEndpoint').trim().if(value => value !== '').matches(/^(https?:\/\/[^/]+|)$/)
+        .withMessage((value, { req }) => req.t('validation.aws_custom_endpoint')),
+      body('s3Bucket').trim(),
+      body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
+      body('s3SecretAccessKey').trim(),
     ],
     ],
     pluginSetting: [
     pluginSetting: [
       body('isEnabledPlugins').isBoolean(),
       body('isEnabledPlugins').isBoolean(),
@@ -225,13 +225,14 @@ module.exports = (crowi) => {
 
 
       fileUploadType: crowi.configManager.getConfig('crowi', 'app:fileUploadType'),
       fileUploadType: crowi.configManager.getConfig('crowi', 'app:fileUploadType'),
       envFileUploadType: crowi.configManager.getConfigFromEnvVars('crowi', 'app:fileUploadType'),
       envFileUploadType: crowi.configManager.getConfigFromEnvVars('crowi', 'app:fileUploadType'),
+      useOnlyEnvVarForFileUploadType: crowi.configManager.getConfig('crowi', 'app:useOnlyEnvVarForFileUploadType'),
 
 
       s3Region: crowi.configManager.getConfig('crowi', 'aws:s3Region'),
       s3Region: crowi.configManager.getConfig('crowi', 'aws:s3Region'),
       s3CustomEndpoint: crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
       s3CustomEndpoint: crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
       s3Bucket: crowi.configManager.getConfig('crowi', 'aws:s3Bucket'),
       s3Bucket: crowi.configManager.getConfig('crowi', 'aws:s3Bucket'),
       s3AccessKeyId: crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
       s3AccessKeyId: crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
       s3SecretAccessKey: crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
       s3SecretAccessKey: crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
-      gcsUseOnlyEnvVars: crowi.configManager.getConfig('crowi', 'gcs:isGcsEnvPrioritizes'),
+      gcsUseOnlyEnvVars: crowi.configManager.getConfig('crowi', 'gcs:useOnlyEnvVarsForSomeOptions'),
       gcsApiKeyJsonPath: crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath'),
       gcsApiKeyJsonPath: crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath'),
       gcsBucket: crowi.configManager.getConfig('crowi', 'gcs:bucket'),
       gcsBucket: crowi.configManager.getConfig('crowi', 'gcs:bucket'),
       gcsUploadNamespace: crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace'),
       gcsUploadNamespace: crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace'),
@@ -549,105 +550,76 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /app-settings/aws-setting:
+   *    /app-settings/file-upload-settings:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingAwsSetting
-   *        summary: /app-settings/aws-setting
-   *        description: Update aws setting
+   *        operationId: updateAppSettingFileUploadSetting
+   *        summary: /app-settings/file-upload-setting
+   *        description: Update fileUploadSetting
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
    *          content:
    *          content:
    *            application/json:
    *            application/json:
    *              schema:
    *              schema:
-   *                $ref: '#/components/schemas/AwsSettingParams'
+   *                $ref: '#/components/schemas/FileUploadSettingParams'
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Succeeded to update aws setting
+   *            description: Succeeded to update fileUploadSetting
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  $ref: '#/components/schemas/AwsSettingParams'
+   *                  $ref: '#/components/schemas/FileUploadSettingParams'
    */
    */
-  router.put('/aws-setting', loginRequiredStrictly, adminRequired, csrf, validator.awsSetting, apiV3FormValidator, async(req, res) => {
-    const requestAwsSettingParams = {
-      'app:fileUploadType': req.body.fileUploadType,
-      'aws:s3Region': req.body.s3Region,
-      'aws:s3CustomEndpoint': req.body.s3CustomEndpoint,
-      'aws:s3Bucket': req.body.s3Bucket,
-      'aws:s3AccessKeyId': req.body.s3AccessKeyId,
-      'aws:s3SecretAccessKey': req.body.s3SecretAccessKey,
-    };
+  router.put('/file-upload-setting', loginRequiredStrictly, adminRequired, csrf, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
+    const { fileUploadType } = req.body;
 
 
-    try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestAwsSettingParams, true);
-      await crowi.setUpFileUpload(true);
-      crowi.fileUploaderSwitchService.publishUpdatedMessage();
+    const requestParams = {
+      'app:fileUploadType': fileUploadType,
+    };
 
 
-      const awsSettingParams = {
-        s3Region: crowi.configManager.getConfig('crowi', 'aws:s3Region'),
-        s3CustomEndpoint: crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
-        s3Bucket: crowi.configManager.getConfig('crowi', 'aws:s3Bucket'),
-        s3AccessKeyId: crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
-        s3SecretAccessKey: crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
-      };
-      return res.apiv3({ awsSettingParams });
+    if (fileUploadType === 'gcs') {
+      requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
+      requestParams['gcs:bucket'] = req.body.gcsBucket;
+      requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
     }
     }
-    catch (err) {
-      const msg = 'Error occurred in updating aws setting';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-awsSetting-failed'));
-    }
-
-  });
 
 
-  /**
-   * @swagger
-   *
-   *    /app-settings/gcs-setting:
-   *      put:
-   *        tags: [AppSettings]
-   *        operationId: updateAppSettingGcsSetting
-   *        summary: /app-settings/gcs-setting
-   *        description: Update gcs setting
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/GcsSettingParams'
-   *        responses:
-   *          200:
-   *            description: Succeeded to update gcs setting
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/GcsSettingParams'
-   */
-  router.put('/gcs-setting', loginRequiredStrictly, adminRequired, csrf, validator.gcsSetting, apiV3FormValidator, async(req, res) => {
-    const requestGcsSettingParams = {
-      'app:fileUploadType': req.body.fileUploadType,
-      'gcs:apiKeyJsonPath': req.body.gcsApiKeyJsonPath,
-      'gcs:bucket': req.body.gcsBucket,
-      'gcs:uploadNamespace': req.body.gcsUploadNamespace,
-    };
+    if (fileUploadType === 'aws') {
+      requestParams['aws:s3Region'] = req.body.s3Region;
+      requestParams['aws:s3CustomEndpoint'] = req.body.s3CustomEndpoint;
+      requestParams['aws:s3Bucket'] = req.body.s3Bucket;
+      requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
+      requestParams['aws:s3SecretAccessKey'] = req.body.s3SecretAccessKey;
+    }
 
 
     try {
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestGcsSettingParams, true);
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
       await crowi.setUpFileUpload(true);
       await crowi.setUpFileUpload(true);
       crowi.fileUploaderSwitchService.publishUpdatedMessage();
       crowi.fileUploaderSwitchService.publishUpdatedMessage();
 
 
-      const gcsSettingParams = {
-        gcsApiKeyJsonPath: crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath'),
-        gcsBucket: crowi.configManager.getConfig('crowi', 'gcs:bucket'),
-        gcsUploadNamespace: crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace'),
+      const responseParams = {
+        fileUploadType: crowi.configManager.getConfig('crowi', 'app:fileUploadType'),
       };
       };
-      return res.apiv3({ gcsSettingParams });
+
+      if (fileUploadType === 'gcs') {
+        responseParams.gcsApiKeyJsonPath = crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath');
+        responseParams.gcsBucket = crowi.configManager.getConfig('crowi', 'gcs:bucket');
+        responseParams.gcsUploadNamespace = crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace');
+      }
+
+      if (fileUploadType === 'aws') {
+        responseParams.s3Region = crowi.configManager.getConfig('crowi', 'aws:s3Region');
+        responseParams.s3CustomEndpoint = crowi.configManager.getConfig('crowi', 'aws:s3CustomEndpoint');
+        responseParams.s3Bucket = crowi.configManager.getConfig('crowi', 'aws:s3Bucket');
+        responseParams.s3AccessKeyId = crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId');
+        responseParams.s3SecretAccessKey = crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey');
+      }
+
+      return res.apiv3({ responseParams });
     }
     }
     catch (err) {
     catch (err) {
-      const msg = 'Error occurred in updating aws setting';
+      const msg = 'Error occurred in updating fileUploadType';
       logger.error('Error', err);
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-awsSetting-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
     }
     }
 
 
   });
   });

+ 5 - 2
src/server/routes/attachment.js

@@ -129,7 +129,7 @@ module.exports = function(crowi, app) {
   const Attachment = crowi.model('Attachment');
   const Attachment = crowi.model('Attachment');
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
-  const { fileUploadService, attachmentService, globalNotificationService } = crowi;
+  const { attachmentService, globalNotificationService } = crowi;
 
 
   /**
   /**
    * Check the user is accessible to the related page
    * Check the user is accessible to the related page
@@ -176,6 +176,8 @@ module.exports = function(crowi, app) {
    * @param {boolean} forceDownload
    * @param {boolean} forceDownload
    */
    */
   async function responseForAttachment(req, res, attachment, forceDownload) {
   async function responseForAttachment(req, res, attachment, forceDownload) {
+    const { fileUploadService } = crowi;
+
     if (attachment == null) {
     if (attachment == null) {
       return res.json(ApiResponse.error('attachment not found'));
       return res.json(ApiResponse.error('attachment not found'));
     }
     }
@@ -286,7 +288,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} pageId, fileName
    * @apiParam {String} pageId, fileName
    */
    */
   api.obsoletedGetForMongoDB = async function(req, res) {
   api.obsoletedGetForMongoDB = async function(req, res) {
-    if (process.env.FILE_UPLOAD !== 'mongodb') {
+    if (crowi.configManager.getConfig('crowi', 'app:fileUploadType') !== 'mongodb') {
       return res.status(400);
       return res.status(400);
     }
     }
 
 
@@ -341,6 +343,7 @@ module.exports = function(crowi, app) {
    * @apiGroup Attachment
    * @apiGroup Attachment
    */
    */
   api.limit = async function(req, res) {
   api.limit = async function(req, res) {
+    const { fileUploadService } = crowi;
     const fileSize = Number(req.query.fileSize);
     const fileSize = Number(req.query.fileSize);
     return res.json(ApiResponse.success(await fileUploadService.checkLimit(fileSize)));
     return res.json(ApiResponse.success(await fileUploadService.checkLimit(fileSize)));
   };
   };

+ 1 - 2
src/server/routes/page.js

@@ -433,7 +433,6 @@ module.exports = function(crowi, app) {
     const revisionId = req.query.revision;
     const revisionId = req.query.revision;
 
 
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
-    const view = `layout-${layoutName}/shared_page`;
 
 
     const shareLink = await ShareLink.findOne({ _id: linkId }).populate('relatedPage');
     const shareLink = await ShareLink.findOne({ _id: linkId }).populate('relatedPage');
 
 
@@ -471,7 +470,7 @@ module.exports = function(crowi, app) {
     addRenderVarsForScope(renderVars, page);
     addRenderVarsForScope(renderVars, page);
 
 
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
-    return res.render(view, renderVars);
+    return res.render(`layout-${layoutName}/shared_page`, renderVars);
   };
   };
 
 
   /**
   /**

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

@@ -29,6 +29,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: 'aws',
     default: 'aws',
   },
   },
+  FILE_UPLOAD_USES_ONLY_ENV_VAR_FOR_FILE_UPLOAD_TYPE: {
+    ns:      'crowi',
+    key:     'app:useOnlyEnvVarForFileUploadType',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
   // HACKMD_URI: {
   // HACKMD_URI: {
   //   ns:      ,
   //   ns:      ,
   //   key:     ,
   //   key:     ,
@@ -344,9 +350,9 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },
-  IS_GCS_ENV_PRIORITIZED: {
+  GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'gcs:isGcsEnvPrioritizes',
+    key:     'gcs:useOnlyEnvVarsForSomeOptions',
     type:    TYPES.BOOLEAN,
     type:    TYPES.BOOLEAN,
     default: false,
     default: false,
   },
   },

+ 5 - 3
src/server/service/config-manager.js

@@ -229,12 +229,14 @@ class ConfigManager extends S2sMessageHandlable {
         && this.defaultSearch('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions')
         && this.defaultSearch('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions')
       )
       )
       // file upload option
       // file upload option
-      // [TODO GW-4173] control with the env var gcs:isFileUploadEnvPrioritizes
-      || KEYS_FOR_FIEL_UPLOAD_USE_ONLY_ENV_OPTION.includes(key)
+      || (
+        KEYS_FOR_FIEL_UPLOAD_USE_ONLY_ENV_OPTION.includes(key)
+        && this.searchOnlyFromEnvVarConfigs('crowi', 'app:useOnlyEnvVarForFileUploadType')
+      )
       // gcs option
       // gcs option
       || (
       || (
         KEYS_FOR_GCS_USE_ONLY_ENV_OPTION.includes(key)
         KEYS_FOR_GCS_USE_ONLY_ENV_OPTION.includes(key)
-        && this.searchOnlyFromEnvVarConfigs('crowi', 'gcs:isGcsEnvPrioritizes')
+        && this.searchOnlyFromEnvVarConfigs('crowi', 'gcs:useOnlyEnvVarsForSomeOptions')
       )
       )
     ));
     ));
   }
   }

+ 1 - 1
src/server/service/file-uploader/local.js

@@ -99,7 +99,7 @@ module.exports = function(crowi) {
    */
    */
   lib.canRespond = () => {
   lib.canRespond = () => {
     // Check whether to use internal redirect of nginx or Apache.
     // Check whether to use internal redirect of nginx or Apache.
-    return process.env.FILE_UPLOAD === 'local' && lib.configManager.getConfig('crowi', 'fileUpload:local:useInternalRedirect');
+    return lib.configManager.getConfig('crowi', 'fileUpload:local:useInternalRedirect');
   };
   };
 
 
   /**
   /**

+ 2 - 1
src/server/views/widget/page_content.html

@@ -54,7 +54,8 @@
   <div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
   <div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
 </div>
 </div>
 
 
-<div class="d-none d-lg-block revision-toc-container ml-4">
+<div class="d-none d-lg-block side-contents-container ml-4">
+  <div id="page-accessories" class="page-accessories"></div>
   <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="123">
   <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="123">
     <div id="revision-toc-content" class="revision-toc-content"></div>
     <div id="revision-toc-content" class="revision-toc-content"></div>
   </div>
   </div>

+ 1 - 1
src/test/models/shareLink.test.js → src/test/models/share-link.test.js

@@ -98,7 +98,7 @@ describe('ShareLink', () => {
       expect(findOneResult.populate).toHaveBeenCalled();
       expect(findOneResult.populate).toHaveBeenCalled();
       expect(res.render).toHaveBeenCalled();
       expect(res.render).toHaveBeenCalled();
       expect(response.page).toEqual('layout-growi/expired_shared_page');
       expect(response.page).toEqual('layout-growi/expired_shared_page');
-      expect(response.renderVars).toEqual(null);
+      expect(response.renderVars).not.toEqual(null);
     });
     });
 
 
     test('share link is found, and it has the page you can see', async() => {
     test('share link is found, and it has the page you can see', async() => {

+ 0 - 0
src/test/models/updatePost.test.js → src/test/models/update-post.test.js