فهرست منبع

Merge branch 'master' into feat/duplicate-with-subordinate-page

itizawa 5 سال پیش
والد
کامیت
6e2360783f

+ 9 - 1
CHANGES.md

@@ -2,7 +2,15 @@
 
 
 ## v4.2.3-RC
 ## v4.2.3-RC
 
 
-* 
+* Feature: Auto reconnecting to search service
+* Improvement: New style of params for Healthcheck API
+* Fix: Referencing attachments when `FILE_UPLOAD_DISABLED` is true
+* Fix: The message of timeline for restricted pages
+* Fix: Parameter validation for Import/Export Archive API
+* Fix: Prevent regexp for Search Tags API
+* Fix: Add `Content-Security-Policy` when referencing attachments
+* Fix: Sanitize at presentation time
+* Fix: Remove page path string from message for page lists and timeline when there is no contents
 
 
 ## v4.2.2
 ## v4.2.2
 
 

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

@@ -43,6 +43,11 @@
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
     "file_upload_settings":"File Upload Settings",
     "file_upload_settings":"File Upload Settings",
     "file_upload_method":"File Upload Method",
     "file_upload_method":"File Upload Method",
+    "file_delivery_method":"File Delivery Method",
+    "file_delivery_method_redirect":"Redirect",
+    "file_delivery_method_relay":"Internal System Relay",
+    "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
+    "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
     "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)",

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

@@ -43,6 +43,11 @@
     "confirm_to_initialize_mail_settings": "一度初期化した設定は戻せません。本当に初期化しますか?",
     "confirm_to_initialize_mail_settings": "一度初期化した設定は戻せません。本当に初期化しますか?",
     "file_upload_settings":"ファイルアップロード設定",
     "file_upload_settings":"ファイルアップロード設定",
     "file_upload_method":"ファイルアップロード方法",
     "file_upload_method":"ファイルアップロード方法",
+    "file_delivery_method":"ファイルの配信方法",
+    "file_delivery_method_redirect":"リダイレクト",
+    "file_delivery_method_relay":"内部システム中継",
+    "file_delivery_method_redirect_info":"リダイレクト: GROWIサーバーを介さずに署名付きURLにリダイレクトされるため、優れたパフォーマンスを出します。",
+    "file_delivery_method_relay_info":"内部システム中継: GROWIサーバーがクライアントに配信するため、完全なセキュリティーを提供します。",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "local_label": "Local",

+ 5 - 0
resource/locales/zh_CN/admin/admin.json

@@ -43,6 +43,11 @@
     "confirm_to_initialize_mail_settings": "当前设置将被清空且不可恢复。确认重置?",
     "confirm_to_initialize_mail_settings": "当前设置将被清空且不可恢复。确认重置?",
     "file_upload_settings":"文件上传设置",
     "file_upload_settings":"文件上传设置",
     "file_upload_method":"文件上传方法",
     "file_upload_method":"文件上传方法",
+    "file_delivery_method":"File Delivery Method",
+    "file_delivery_method_redirect":"Redirect",
+    "file_delivery_method_relay":"Internal System Relay",
+    "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
+    "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "local_label": "Local",

+ 60 - 12
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -9,9 +9,55 @@ import AdminAppContainer from '../../../services/AdminAppContainer';
 
 
 function AwsSetting(props) {
 function AwsSetting(props) {
   const { t, adminAppContainer } = props;
   const { t, adminAppContainer } = props;
+  const { s3ReferenceFileWithRelayMode } = adminAppContainer.state;
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
+
+      <div className="row form-group my-3">
+        <label className="text-left text-md-right col-md-3 col-form-label">
+          {t('admin:app_setting.file_delivery_method')}
+        </label>
+
+        <div className="col-md-6">
+          <div className="dropdown">
+            <button
+              className="btn btn-outline-secondary dropdown-toggle"
+              type="button"
+              id="ddS3ReferenceFileWithRelayMode"
+              data-toggle="dropdown"
+              aria-haspopup="true"
+              aria-expanded="true"
+            >
+              {s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_relay')}
+              {!s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_redirect')}
+            </button>
+            <div className="dropdown-menu" aria-labelledby="ddS3ReferenceFileWithRelayMode">
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { adminAppContainer.changeS3ReferenceFileWithRelayMode(true) }}
+              >
+                {t('admin:app_setting.file_delivery_method_relay')}
+              </button>
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { adminAppContainer.changeS3ReferenceFileWithRelayMode(false) }}
+              >
+                { t('admin:app_setting.file_delivery_method_redirect')}
+              </button>
+            </div>
+
+            <p className="form-text text-muted small">
+              {t('admin:app_setting.file_delivery_method_redirect_info')}
+              <br />
+              {t('admin:app_setting.file_delivery_method_relay_info')}
+            </p>
+          </div>
+        </div>
+      </div>
+
       <div className="row form-group">
       <div className="row form-group">
         <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.region')}
           {t('admin:app_setting.region')}
@@ -22,8 +68,8 @@ function AwsSetting(props) {
             placeholder={`${t('eg')} ap-northeast-1`}
             placeholder={`${t('eg')} ap-northeast-1`}
             defaultValue={adminAppContainer.state.s3Region || ''}
             defaultValue={adminAppContainer.state.s3Region || ''}
             onChange={(e) => {
             onChange={(e) => {
-                adminAppContainer.changeS3Region(e.target.value);
-              }}
+              adminAppContainer.changeS3Region(e.target.value);
+            }}
           />
           />
         </div>
         </div>
       </div>
       </div>
@@ -39,8 +85,8 @@ function AwsSetting(props) {
             placeholder={`${t('eg')} http://localhost:9000`}
             placeholder={`${t('eg')} http://localhost:9000`}
             defaultValue={adminAppContainer.state.s3CustomEndpoint || ''}
             defaultValue={adminAppContainer.state.s3CustomEndpoint || ''}
             onChange={(e) => {
             onChange={(e) => {
-                adminAppContainer.changeS3CustomEndpoint(e.target.value);
-              }}
+              adminAppContainer.changeS3CustomEndpoint(e.target.value);
+            }}
           />
           />
           <p className="form-text text-muted">{t('admin:app_setting.custom_endpoint_change')}</p>
           <p className="form-text text-muted">{t('admin:app_setting.custom_endpoint_change')}</p>
         </div>
         </div>
@@ -57,15 +103,15 @@ function AwsSetting(props) {
             placeholder={`${t('eg')} crowi`}
             placeholder={`${t('eg')} crowi`}
             defaultValue={adminAppContainer.state.s3Bucket || ''}
             defaultValue={adminAppContainer.state.s3Bucket || ''}
             onChange={(e) => {
             onChange={(e) => {
-                adminAppContainer.changeS3Bucket(e.target.value);
-              }}
+              adminAppContainer.changeS3Bucket(e.target.value);
+            }}
           />
           />
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row form-group">
       <div className="row form-group">
         <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">
-            Access key ID
+          Access key ID
         </label>
         </label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -73,15 +119,15 @@ function AwsSetting(props) {
             type="text"
             type="text"
             defaultValue={adminAppContainer.state.s3AccessKeyId || ''}
             defaultValue={adminAppContainer.state.s3AccessKeyId || ''}
             onChange={(e) => {
             onChange={(e) => {
-                adminAppContainer.changeS3AccessKeyId(e.target.value);
-              }}
+              adminAppContainer.changeS3AccessKeyId(e.target.value);
+            }}
           />
           />
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row form-group">
       <div className="row form-group">
         <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">
-            Secret access key
+          Secret access key
         </label>
         </label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -89,11 +135,13 @@ function AwsSetting(props) {
             type="text"
             type="text"
             defaultValue={adminAppContainer.state.s3SecretAccessKey || ''}
             defaultValue={adminAppContainer.state.s3SecretAccessKey || ''}
             onChange={(e) => {
             onChange={(e) => {
-                adminAppContainer.changeS3SecretAccessKey(e.target.value);
-              }}
+              adminAppContainer.changeS3SecretAccessKey(e.target.value);
+            }}
           />
           />
         </div>
         </div>
       </div>
       </div>
+
+
     </React.Fragment>
     </React.Fragment>
   );
   );
 }
 }

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

@@ -11,10 +11,55 @@ import AdminAppContainer from '../../../services/AdminAppContainer';
 
 
 function GcsSetting(props) {
 function GcsSetting(props) {
   const { t, adminAppContainer } = props;
   const { t, adminAppContainer } = props;
-  const { gcsUseOnlyEnvVars } = adminAppContainer.state;
+  const { gcsReferenceFileWithRelayMode, gcsUseOnlyEnvVars } = adminAppContainer.state;
 
 
   return (
   return (
     <>
     <>
+
+      <div className="row form-group my-3">
+        <label className="text-left text-md-right col-md-3 col-form-label">
+          {t('admin:app_setting.file_delivery_method')}
+        </label>
+
+        <div className="col-md-6">
+          <div className="dropdown">
+            <button
+              className="btn btn-outline-secondary dropdown-toggle"
+              type="button"
+              id="ddGcsReferenceFileWithRelayMode"
+              data-toggle="dropdown"
+              aria-haspopup="true"
+              aria-expanded="true"
+            >
+              {gcsReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_relay')}
+              {!gcsReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_redirect')}
+            </button>
+            <div className="dropdown-menu" aria-labelledby="ddGcsReferenceFileWithRelayMode">
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { adminAppContainer.changeGcsReferenceFileWithRelayMode(true) }}
+              >
+                {t('admin:app_setting.file_delivery_method_relay')}
+              </button>
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { adminAppContainer.changeGcsReferenceFileWithRelayMode(false) }}
+              >
+                { t('admin:app_setting.file_delivery_method_redirect')}
+              </button>
+            </div>
+
+            <p className="form-text text-muted small">
+              {t('admin:app_setting.file_delivery_method_redirect_info')}
+              <br />
+              {t('admin:app_setting.file_delivery_method_relay_info')}
+            </p>
+          </div>
+        </div>
+      </div>
+
       {gcsUseOnlyEnvVars && (
       {gcsUseOnlyEnvVars && (
         <p
         <p
           className="alert alert-info"
           className="alert alert-info"
@@ -98,6 +143,7 @@ function GcsSetting(props) {
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>
+
     </>
     </>
   );
   );
 
 

+ 0 - 1
src/client/js/components/Navbar/GrowiNavbar.jsx

@@ -53,7 +53,6 @@ class GrowiNavbar extends React.Component {
         </span>
         </span>
         <UncontrolledTooltip
         <UncontrolledTooltip
           placement="bottom"
           placement="bottom"
-          trigger="click"
           target="confidentialTooltip"
           target="confidentialTooltip"
           className="d-md-none"
           className="d-md-none"
         >
         >

+ 7 - 5
src/client/js/components/Page/TrashPageAlert.jsx

@@ -72,7 +72,7 @@ const TrashPageAlert = (props) => {
         </button>
         </button>
         <button
         <button
           type="button"
           type="button"
-          className="btn btn-danger rounded-pill btn-sm mr-2"
+          className="btn btn-danger rounded-pill btn-sm"
           disabled={!isAbleToDeleteCompletely}
           disabled={!isAbleToDeleteCompletely}
           onClick={openPageDeleteModalHandler}
           onClick={openPageDeleteModalHandler}
         >
         >
@@ -107,13 +107,15 @@ const TrashPageAlert = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <div className="alert alert-warning py-3 px-4 d-flex align-items-center">
-        <div>
+      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row">
+        <div className="flex-grow-1">
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           {isDeleted && <span><br /><UserPicture user={{ username: lastUpdateUsername }} /> Deleted by {lastUpdateUsername} at {updatedAt}</span>}
           {isDeleted && <span><br /><UserPicture user={{ username: lastUpdateUsername }} /> Deleted by {lastUpdateUsername} at {updatedAt}</span>}
         </div>
         </div>
-        { pageContainer.isAbleToShowEmptyTrashButton && renderEmptyButton()}
-        { pageContainer.isAbleToShowTrashPageManagementButtons && renderTrashPageManagementButtons()}
+        <div className="pt-1 d-flex align-items-end align-items-lg-center">
+          <span>{ pageContainer.isAbleToShowEmptyTrashButton && renderEmptyButton()}</span>
+          { pageContainer.isAbleToShowTrashPageManagementButtons && renderTrashPageManagementButtons()}
+        </div>
       </div>
       </div>
       {renderModals()}
       {renderModals()}
     </>
     </>

+ 9 - 6
src/client/js/components/PageAccessoriesModalControl.jsx

@@ -26,29 +26,34 @@ const PageAccessoriesModalControl = (props) => {
         name: 'pagelist',
         name: 'pagelist',
         Icon: <PageListIcon />,
         Icon: <PageListIcon />,
         disabled: isSharedUser,
         disabled: isSharedUser,
+        i18n: t('page_list'),
       },
       },
       {
       {
         name: 'timeline',
         name: 'timeline',
         Icon: <TimeLineIcon />,
         Icon: <TimeLineIcon />,
         disabled: isSharedUser,
         disabled: isSharedUser,
+        i18n: t('Timeline View'),
       },
       },
       {
       {
         name: 'pageHistory',
         name: 'pageHistory',
         Icon: <HistoryIcon />,
         Icon: <HistoryIcon />,
         disabled: isGuestUser || isSharedUser,
         disabled: isGuestUser || isSharedUser,
+        i18n: t('History'),
       },
       },
       {
       {
         name: 'attachment',
         name: 'attachment',
         Icon: <AttachmentIcon />,
         Icon: <AttachmentIcon />,
         disabled: false,
         disabled: false,
+        i18n: t('attachment_data'),
       },
       },
       {
       {
         name: 'shareLink',
         name: 'shareLink',
         Icon: <ShareLinkIcon />,
         Icon: <ShareLinkIcon />,
         disabled: isGuestUser || isSharedUser,
         disabled: isGuestUser || isSharedUser,
+        i18n: t('share_links.share_link_management'),
       },
       },
     ];
     ];
-  }, [isGuestUser, isSharedUser]);
+  }, [t, isGuestUser, isSharedUser]);
 
 
   return (
   return (
     <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
     <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
@@ -64,11 +69,9 @@ const PageAccessoriesModalControl = (props) => {
                 {accessory.Icon}
                 {accessory.Icon}
               </button>
               </button>
             </div>
             </div>
-            {accessory.disabled && (
-              <UncontrolledTooltip placement="top" target={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`} fade={false}>
-                {t('Not available for guest')}
-              </UncontrolledTooltip>
-            )}
+            <UncontrolledTooltip placement="top" target={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`} fade={false}>
+              {accessory.disabled ? t('Not available for guest') : accessory.i18n}
+            </UncontrolledTooltip>
           </Fragment>
           </Fragment>
         );
         );
       })}
       })}

+ 21 - 0
src/client/js/services/AdminAppContainer.js

@@ -46,12 +46,14 @@ export default class AdminAppContainer extends Container {
       envGcsBucket: '',
       envGcsBucket: '',
       gcsUploadNamespace: '',
       gcsUploadNamespace: '',
       envGcsUploadNamespace: '',
       envGcsUploadNamespace: '',
+      gcsReferenceFileWithRelayMode: false,
 
 
       s3Region: '',
       s3Region: '',
       s3CustomEndpoint: '',
       s3CustomEndpoint: '',
       s3Bucket: '',
       s3Bucket: '',
       s3AccessKeyId: '',
       s3AccessKeyId: '',
       s3SecretAccessKey: '',
       s3SecretAccessKey: '',
+      s3ReferenceFileWithRelayMode: false,
 
 
       isEnabledPlugins: true,
       isEnabledPlugins: true,
     };
     };
@@ -99,10 +101,13 @@ export default class AdminAppContainer extends Container {
       s3Bucket: appSettingsParams.s3Bucket,
       s3Bucket: appSettingsParams.s3Bucket,
       s3AccessKeyId: appSettingsParams.s3AccessKeyId,
       s3AccessKeyId: appSettingsParams.s3AccessKeyId,
       s3SecretAccessKey: appSettingsParams.s3SecretAccessKey,
       s3SecretAccessKey: appSettingsParams.s3SecretAccessKey,
+      s3ReferenceFileWithRelayMode: appSettingsParams.s3ReferenceFileWithRelayMode,
+
       gcsUseOnlyEnvVars: appSettingsParams.gcsUseOnlyEnvVars,
       gcsUseOnlyEnvVars: appSettingsParams.gcsUseOnlyEnvVars,
       gcsApiKeyJsonPath: appSettingsParams.gcsApiKeyJsonPath,
       gcsApiKeyJsonPath: appSettingsParams.gcsApiKeyJsonPath,
       gcsBucket: appSettingsParams.gcsBucket,
       gcsBucket: appSettingsParams.gcsBucket,
       gcsUploadNamespace: appSettingsParams.gcsUploadNamespace,
       gcsUploadNamespace: appSettingsParams.gcsUploadNamespace,
+      gcsReferenceFileWithRelayMode: appSettingsParams.gcsReferenceFileWithRelayMode,
       envGcsApiKeyJsonPath: appSettingsParams.envGcsApiKeyJsonPath,
       envGcsApiKeyJsonPath: appSettingsParams.envGcsApiKeyJsonPath,
       envGcsBucket: appSettingsParams.envGcsBucket,
       envGcsBucket: appSettingsParams.envGcsBucket,
       envGcsUploadNamespace: appSettingsParams.envGcsUploadNamespace,
       envGcsUploadNamespace: appSettingsParams.envGcsUploadNamespace,
@@ -238,6 +243,13 @@ export default class AdminAppContainer extends Container {
     this.setState({ s3SecretAccessKey });
     this.setState({ s3SecretAccessKey });
   }
   }
 
 
+  /**
+   * Change s3ReferenceFileWithRelayMode
+   */
+  changeS3ReferenceFileWithRelayMode(s3ReferenceFileWithRelayMode) {
+    this.setState({ s3ReferenceFileWithRelayMode });
+  }
+
   /**
   /**
    * Change gcsApiKeyJsonPath
    * Change gcsApiKeyJsonPath
    */
    */
@@ -259,6 +271,13 @@ export default class AdminAppContainer extends Container {
     this.setState({ gcsUploadNamespace });
     this.setState({ gcsUploadNamespace });
   }
   }
 
 
+  /**
+   * Change gcsReferenceFileWithRelayMode
+   */
+  changeGcsReferenceFileWithRelayMode(gcsReferenceFileWithRelayMode) {
+    this.setState({ gcsReferenceFileWithRelayMode });
+  }
+
   /**
   /**
    * Change secret key
    * Change secret key
    */
    */
@@ -367,6 +386,7 @@ export default class AdminAppContainer extends Container {
       requestParams.gcsApiKeyJsonPath = this.state.gcsApiKeyJsonPath;
       requestParams.gcsApiKeyJsonPath = this.state.gcsApiKeyJsonPath;
       requestParams.gcsBucket = this.state.gcsBucket;
       requestParams.gcsBucket = this.state.gcsBucket;
       requestParams.gcsUploadNamespace = this.state.gcsUploadNamespace;
       requestParams.gcsUploadNamespace = this.state.gcsUploadNamespace;
+      requestParams.gcsReferenceFileWithRelayMode = this.state.gcsReferenceFileWithRelayMode;
     }
     }
 
 
     if (fileUploadType === 'aws') {
     if (fileUploadType === 'aws') {
@@ -375,6 +395,7 @@ export default class AdminAppContainer extends Container {
       requestParams.s3Bucket = this.state.s3Bucket;
       requestParams.s3Bucket = this.state.s3Bucket;
       requestParams.s3AccessKeyId = this.state.s3AccessKeyId;
       requestParams.s3AccessKeyId = this.state.s3AccessKeyId;
       requestParams.s3SecretAccessKey = this.state.s3SecretAccessKey;
       requestParams.s3SecretAccessKey = this.state.s3SecretAccessKey;
+      requestParams.s3ReferenceFileWithRelayMode = this.state.s3ReferenceFileWithRelayMode;
     }
     }
 
 
     const response = await this.appContainer.apiv3.put('/app-settings/file-upload-setting', requestParams);
     const response = await this.appContainer.apiv3.put('/app-settings/file-upload-setting', requestParams);

+ 8 - 6
src/client/styles/scss/_search.scss

@@ -32,12 +32,6 @@
   .rbt-menu {
   .rbt-menu {
     max-height: none !important;
     max-height: none !important;
     margin-top: 3px;
     margin-top: 3px;
-    @extend .dropdown-menu-right;
-    @extend .dropdown-menu-md-left;
-    @include media-breakpoint-down(sm) {
-      left: auto !important;
-      width: 90vw;
-    }
 
 
     li a span {
     li a span {
       .page-path {
       .page-path {
@@ -82,6 +76,14 @@
       border-top-right-radius: 40px;
       border-top-right-radius: 40px;
       border-bottom-right-radius: 40px;
       border-bottom-right-radius: 40px;
     }
     }
+    .rbt-menu {
+      @extend .dropdown-menu-right;
+      @extend .dropdown-menu-md-left;
+      @include media-breakpoint-down(sm) {
+        left: auto !important;
+        width: 90vw;
+      }
+    }
   }
   }
 
 
   // using react-bootstrap-typeahead
   // using react-bootstrap-typeahead

+ 32 - 0
src/server/middlewares/auto-reconnect-to-search.js

@@ -0,0 +1,32 @@
+const loggerFactory = require('@alias/logger');
+
+const { ReconnectContext, nextTick } = require('../service/search-reconnect-context/reconnect-context');
+
+const logger = loggerFactory('growi:middlewares:auto-reconnect-to-search');
+
+module.exports = (crowi) => {
+  const { searchService } = crowi;
+  const reconnectContext = new ReconnectContext();
+
+  const reconnectHandler = async() => {
+    try {
+      logger.info('Auto reconnection is started.');
+      await searchService.reconnectClient();
+    }
+    catch (err) {
+      logger.error('Auto reconnection failed.');
+    }
+
+    return searchService.isReachable;
+  };
+
+  return (req, res, next) => {
+    if (searchService != null && !searchService.isReachable) {
+      // NON-BLOCKING CALL
+      // for the latency of the response
+      nextTick(reconnectContext, reconnectHandler);
+    }
+
+    return next();
+  };
+};

+ 24 - 0
src/server/models/attachment.js

@@ -8,6 +8,7 @@ const path = require('path');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 const uniqueValidator = require('mongoose-unique-validator');
 const uniqueValidator = require('mongoose-unique-validator');
 const mongoosePaginate = require('mongoose-paginate-v2');
 const mongoosePaginate = require('mongoose-paginate-v2');
+const { addSeconds } = require('date-fns');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 
@@ -28,6 +29,8 @@ module.exports = function(crowi) {
     fileFormat: { type: String, required: true },
     fileFormat: { type: String, required: true },
     fileSize: { type: Number, default: 0 },
     fileSize: { type: Number, default: 0 },
     createdAt: { type: Date, default: Date.now },
     createdAt: { type: Date, default: Date.now },
+    temporaryUrlCached: { type: String },
+    temporaryUrlExpiredAt: { type: Date },
   });
   });
   attachmentSchema.plugin(uniqueValidator);
   attachmentSchema.plugin(uniqueValidator);
   attachmentSchema.plugin(mongoosePaginate);
   attachmentSchema.plugin(mongoosePaginate);
@@ -66,5 +69,26 @@ module.exports = function(crowi) {
   };
   };
 
 
 
 
+  attachmentSchema.methods.getValidTemporaryUrl = function() {
+    if (this.temporaryUrlExpiredAt == null) {
+      return null;
+    }
+    // return null when expired url
+    if (this.temporaryUrlExpiredAt.getTime() < new Date().getTime()) {
+      return null;
+    }
+    return this.temporaryUrlCached;
+  };
+
+  attachmentSchema.methods.cashTemporaryUrlByProvideSec = function(temporaryUrl, provideSec) {
+    if (temporaryUrl == null) {
+      throw new Error('url is required.');
+    }
+    this.temporaryUrlCached = temporaryUrl;
+    this.temporaryUrlExpiredAt = addSeconds(new Date(), provideSec);
+
+    return this.save();
+  };
+
   return mongoose.model('Attachment', attachmentSchema);
   return mongoose.model('Attachment', attachmentSchema);
 };
 };

+ 21 - 5
src/server/routes/apiv3/app-settings.js

@@ -93,21 +93,24 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          fileUploadType:
  *          fileUploadType:
  *            type: string
  *            type: string
  *            description: fileUploadType
  *            description: fileUploadType
- *          region:
+ *          s3Region:
  *            type: string
  *            type: string
  *            description: region of AWS S3
  *            description: region of AWS S3
- *          customEndpoint:
+ *          s3CustomEndpoint:
  *            type: string
  *            type: string
  *            description: custom endpoint of AWS S3
  *            description: custom endpoint of AWS S3
- *          bucket:
+ *          s3Bucket:
  *            type: string
  *            type: string
  *            description: AWS S3 bucket name
  *            description: AWS S3 bucket name
- *          accessKeyId:
+ *          s3AccessKeyId:
  *            type: string
  *            type: string
  *            description: accesskey id for authentification of AWS
  *            description: accesskey id for authentification of AWS
- *          secretAccessKey:
+ *          s3SecretAccessKey:
  *            type: string
  *            type: string
  *            description: secret key for authentification of AWS
  *            description: secret key for authentification of AWS
+ *          s3ReferenceFileWithRelayMode:
+ *            type: boolean
+ *            description: is enable internal stream system for s3 file request
  *          gcsApiKeyJsonPath:
  *          gcsApiKeyJsonPath:
  *            type: string
  *            type: string
  *            description: apiKeyJsonPath of gcp
  *            description: apiKeyJsonPath of gcp
@@ -117,6 +120,9 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          gcsUploadNamespace:
  *          gcsUploadNamespace:
  *            type: string
  *            type: string
  *            description: name space of gcs
  *            description: name space of gcs
+ *          gcsReferenceFileWithRelayMode:
+ *            type: boolean
+ *            description: is enable internal stream system for gcs file request
  *          envGcsApiKeyJsonPath:
  *          envGcsApiKeyJsonPath:
  *            type: string
  *            type: string
  *            description: Path of the JSON file that contains service account key to authenticate to GCP API
  *            description: Path of the JSON file that contains service account key to authenticate to GCP API
@@ -171,6 +177,7 @@ module.exports = (crowi) => {
       body('gcsApiKeyJsonPath').trim(),
       body('gcsApiKeyJsonPath').trim(),
       body('gcsBucket').trim(),
       body('gcsBucket').trim(),
       body('gcsUploadNamespace').trim(),
       body('gcsUploadNamespace').trim(),
+      body('gcsReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
       body('s3Region').trim().if(value => value !== '').matches(/^[a-z]+-[a-z]+-\d+$/)
       body('s3Region').trim().if(value => value !== '').matches(/^[a-z]+-[a-z]+-\d+$/)
         .withMessage((value, { req }) => req.t('validation.aws_region')),
         .withMessage((value, { req }) => req.t('validation.aws_region')),
       body('s3CustomEndpoint').trim().if(value => value !== '').matches(/^(https?:\/\/[^/]+|)$/)
       body('s3CustomEndpoint').trim().if(value => value !== '').matches(/^(https?:\/\/[^/]+|)$/)
@@ -178,6 +185,7 @@ module.exports = (crowi) => {
       body('s3Bucket').trim(),
       body('s3Bucket').trim(),
       body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('s3SecretAccessKey').trim(),
       body('s3SecretAccessKey').trim(),
+      body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
     ],
     ],
     pluginSetting: [
     pluginSetting: [
       body('isEnabledPlugins').isBoolean(),
       body('isEnabledPlugins').isBoolean(),
@@ -232,10 +240,14 @@ module.exports = (crowi) => {
       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'),
+      s3ReferenceFileWithRelayMode: crowi.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode'),
+
       gcsUseOnlyEnvVars: crowi.configManager.getConfig('crowi', 'gcs:useOnlyEnvVarsForSomeOptions'),
       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'),
+      gcsReferenceFileWithRelayMode: crowi.configManager.getConfig('crowi', 'gcs:referenceFileWithRelayMode'),
+
       envGcsApiKeyJsonPath: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:apiKeyJsonPath'),
       envGcsApiKeyJsonPath: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:apiKeyJsonPath'),
       envGcsBucket: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:bucket'),
       envGcsBucket: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:bucket'),
       envGcsUploadNamespace: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:uploadNamespace'),
       envGcsUploadNamespace: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:uploadNamespace'),
@@ -581,6 +593,7 @@ module.exports = (crowi) => {
       requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
       requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
       requestParams['gcs:bucket'] = req.body.gcsBucket;
       requestParams['gcs:bucket'] = req.body.gcsBucket;
       requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
       requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
+      requestParams['gcs:referenceFileWithRelayMode'] = req.body.gcsReferenceFileWithRelayMode;
     }
     }
 
 
     if (fileUploadType === 'aws') {
     if (fileUploadType === 'aws') {
@@ -589,6 +602,7 @@ module.exports = (crowi) => {
       requestParams['aws:s3Bucket'] = req.body.s3Bucket;
       requestParams['aws:s3Bucket'] = req.body.s3Bucket;
       requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
       requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
       requestParams['aws:s3SecretAccessKey'] = req.body.s3SecretAccessKey;
       requestParams['aws:s3SecretAccessKey'] = req.body.s3SecretAccessKey;
+      requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
     }
     }
 
 
     try {
     try {
@@ -604,6 +618,7 @@ module.exports = (crowi) => {
         responseParams.gcsApiKeyJsonPath = crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath');
         responseParams.gcsApiKeyJsonPath = crowi.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath');
         responseParams.gcsBucket = crowi.configManager.getConfig('crowi', 'gcs:bucket');
         responseParams.gcsBucket = crowi.configManager.getConfig('crowi', 'gcs:bucket');
         responseParams.gcsUploadNamespace = crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace');
         responseParams.gcsUploadNamespace = crowi.configManager.getConfig('crowi', 'gcs:uploadNamespace');
+        responseParams.gcsReferenceFileWithRelayMode = crowi.configManager.getConfig('crowi', 'gcs:referenceFileWithRelayMode ');
       }
       }
 
 
       if (fileUploadType === 'aws') {
       if (fileUploadType === 'aws') {
@@ -612,6 +627,7 @@ module.exports = (crowi) => {
         responseParams.s3Bucket = crowi.configManager.getConfig('crowi', 'aws:s3Bucket');
         responseParams.s3Bucket = crowi.configManager.getConfig('crowi', 'aws:s3Bucket');
         responseParams.s3AccessKeyId = crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId');
         responseParams.s3AccessKeyId = crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId');
         responseParams.s3SecretAccessKey = crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey');
         responseParams.s3SecretAccessKey = crowi.configManager.getConfig('crowi', 'aws:s3SecretAccessKey');
+        responseParams.s3ReferenceFileWithRelayMode = crowi.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
       }
       }
 
 
       return res.apiv3({ responseParams });
       return res.apiv3({ responseParams });

+ 1 - 0
src/server/routes/apiv3/healthcheck.js

@@ -66,6 +66,7 @@ module.exports = (crowi) => {
     if (searchService.isConfigured) {
     if (searchService.isConfigured) {
       try {
       try {
         info.searchInfo = await searchService.getInfoForHealth();
         info.searchInfo = await searchService.getInfoForHealth();
+        searchService.resetErrorStatus();
       }
       }
       catch (err) {
       catch (err) {
         errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));
         errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));

+ 1 - 1
src/server/routes/apiv3/search.js

@@ -77,7 +77,7 @@ module.exports = (crowi) => {
     }
     }
 
 
     try {
     try {
-      await searchService.initClient();
+      await searchService.reconnectClient();
       return res.status(200).send();
       return res.status(200).send();
     }
     }
     catch (err) {
     catch (err) {

+ 3 - 2
src/server/routes/index.js

@@ -4,6 +4,7 @@ const autoReap = require('multer-autoreap');
 autoReap.options.reapOnError = true; // continue reaping the file even if an error occurs
 autoReap.options.reapOnError = true; // continue reaping the file even if an error occurs
 
 
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
+  const autoReconnectToSearch = require('../middlewares/auto-reconnect-to-search')(crowi);
   const applicationNotInstalled = require('../middlewares/application-not-installed')(crowi);
   const applicationNotInstalled = require('../middlewares/application-not-installed')(crowi);
   const applicationInstalled = require('../middlewares/application-installed')(crowi);
   const applicationInstalled = require('../middlewares/application-installed')(crowi);
   const accessTokenParser = require('../middlewares/access-token-parser')(crowi);
   const accessTokenParser = require('../middlewares/access-token-parser')(crowi);
@@ -32,7 +33,7 @@ module.exports = function(crowi, app) {
 
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
 
-  app.get('/'                        , applicationInstalled, loginRequired , page.showTopPage);
+  app.get('/'                        , applicationInstalled, loginRequired , autoReconnectToSearch, page.showTopPage);
 
 
   // API v3
   // API v3
   app.use('/api-docs', require('./apiv3/docs')(crowi));
   app.use('/api-docs', require('./apiv3/docs')(crowi));
@@ -173,6 +174,6 @@ module.exports = function(crowi, app) {
   app.get('/share/:linkId', page.showSharedPage);
   app.get('/share/:linkId', page.showSharedPage);
 
 
   app.get('/*/$'                   , loginRequired , page.showPageWithEndOfSlash, page.notFound);
   app.get('/*/$'                   , loginRequired , page.showPageWithEndOfSlash, page.notFound);
-  app.get('/*'                     , loginRequired , page.showPage, page.notFound);
+  app.get('/*'                     , loginRequired , autoReconnectToSearch, page.showPage, page.notFound);
 
 
 };
 };

+ 24 - 0
src/server/service/config-loader.js

@@ -332,6 +332,18 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },
+  S3_REFERENCE_FILE_WITH_RELAY_MODE: {
+    ns:      'crowi',
+    key:     'aws:referenceFileWithRelayMode',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
+  S3_LIFETIME_SEC_FOR_TEMPORARY_URL: {
+    ns:      'crowi',
+    key:     'aws:lifetimeSecForTemporaryUrl',
+    type:    TYPES.NUMBER,
+    default: 120,
+  },
   GCS_API_KEY_JSON_PATH: {
   GCS_API_KEY_JSON_PATH: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'gcs:apiKeyJsonPath',
     key:     'gcs:apiKeyJsonPath',
@@ -350,12 +362,24 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },
+  GCS_REFERENCE_FILE_WITH_RELAY_MODE: {
+    ns:      'crowi',
+    key:     'gcs:referenceFileWithRelayMode',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
   GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
   GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'gcs:useOnlyEnvVarsForSomeOptions',
     key:     'gcs:useOnlyEnvVarsForSomeOptions',
     type:    TYPES.BOOLEAN,
     type:    TYPES.BOOLEAN,
     default: false,
     default: false,
   },
   },
+  GCS_LIFETIME_SEC_FOR_TEMPORARY_URL: {
+    ns:      'crowi',
+    key:     'gcs:lifetimeSecForTemporaryUrl',
+    type:    TYPES.NUMBER,
+    default: 120,
+  },
 };
 };
 
 
 class ConfigLoader {
 class ConfigLoader {

+ 37 - 0
src/server/service/file-uploader/aws.js

@@ -72,6 +72,43 @@ module.exports = function(crowi) {
       && this.configManager.getConfig('crowi', 'aws:s3Bucket') != null;
       && this.configManager.getConfig('crowi', 'aws:s3Bucket') != null;
   };
   };
 
 
+  lib.canRespond = function() {
+    return !this.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
+  };
+
+  lib.respond = async function(res, attachment) {
+    if (!this.getIsUploadable()) {
+      throw new Error('AWS is not configured.');
+    }
+    const temporaryUrl = attachment.getValidTemporaryUrl();
+    if (temporaryUrl != null) {
+      return res.redirect(temporaryUrl);
+    }
+
+    const s3 = S3Factory();
+    const awsConfig = getAwsConfig();
+    const filePath = getFilePathOnStorage(attachment);
+    const lifetimeSecForTemporaryUrl = this.configManager.getConfig('crowi', 'aws:lifetimeSecForTemporaryUrl');
+
+    // issue signed url (default: expires 120 seconds)
+    // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
+    const params = {
+      Bucket: awsConfig.bucket,
+      Key: filePath,
+      Expires: lifetimeSecForTemporaryUrl,
+    };
+    const signedUrl = s3.getSignedUrl('getObject', params);
+
+    res.redirect(signedUrl);
+
+    try {
+      return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
+    }
+    catch (err) {
+      logger.error(err);
+    }
+
+  };
 
 
   lib.deleteFile = async function(attachment) {
   lib.deleteFile = async function(attachment) {
     const filePath = getFilePathOnStorage(attachment);
     const filePath = getFilePathOnStorage(attachment);

+ 37 - 0
src/server/service/file-uploader/gcs.js

@@ -50,6 +50,43 @@ module.exports = function(crowi) {
       && this.configManager.getConfig('crowi', 'gcs:bucket') != null;
       && this.configManager.getConfig('crowi', 'gcs:bucket') != null;
   };
   };
 
 
+  lib.canRespond = function() {
+    return !this.configManager.getConfig('crowi', 'gcs:referenceFileWithRelayMode');
+  };
+
+  lib.respond = async function(res, attachment) {
+    if (!this.getIsUploadable()) {
+      throw new Error('GCS is not configured.');
+    }
+    const temporaryUrl = attachment.getValidTemporaryUrl();
+    if (temporaryUrl != null) {
+      return res.redirect(temporaryUrl);
+    }
+
+    const gcs = getGcsInstance();
+    const myBucket = gcs.bucket(getGcsBucket());
+    const filePath = getFilePathOnStorage(attachment);
+    const file = myBucket.file(filePath);
+    const lifetimeSecForTemporaryUrl = this.configManager.getConfig('crowi', 'gcs:lifetimeSecForTemporaryUrl');
+
+    // issue signed url (default: expires 120 seconds)
+    // https://cloud.google.com/storage/docs/access-control/signed-urls
+    const signedUrl = await file.getSignedUrl({
+      action: 'read',
+      expires: Date.now() + lifetimeSecForTemporaryUrl * 1000,
+    });
+
+    res.redirect(signedUrl);
+
+    try {
+      return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
+    }
+    catch (err) {
+      logger.error(err);
+    }
+
+  };
+
   lib.deleteFile = async function(attachment) {
   lib.deleteFile = async function(attachment) {
     const filePath = getFilePathOnStorage(attachment);
     const filePath = getFilePathOnStorage(attachment);
     return lib.deleteFileByFilePath(filePath);
     return lib.deleteFileByFilePath(filePath);

+ 80 - 0
src/server/service/search-reconnect-context/reconnect-context.js

@@ -0,0 +1,80 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:service:search-reconnect-context:reconnect-context');
+
+
+const RECONNECT_INTERVAL_SEC = 120;
+
+class ReconnectContext {
+
+  constructor() {
+    this.lastEvalDate = null;
+
+    this.reset(true);
+  }
+
+  reset() {
+    this.counter = 0;
+    this.stage = 0;
+  }
+
+  incrementCount() {
+    this.counter++;
+  }
+
+  incrementStage() {
+    this.counter = 0; // reset counter
+    this.stage++;
+  }
+
+  get shouldReconnectByCount() {
+    // https://www.google.com/search?q=10log10(x)-1+graph
+    const thresholdOfThisStage = 10 * Math.log10(this.stage) - 1;
+    return this.counter > thresholdOfThisStage;
+  }
+
+  get shouldReconnectByTime() {
+    if (this.lastEvalDate == null) {
+      this.lastEvalDate = new Date();
+      return true;
+    }
+
+    const thres = this.lastEvalDate.setSeconds(this.lastEvalDate.getSeconds() + RECONNECT_INTERVAL_SEC);
+    return thres < new Date();
+  }
+
+  get shouldReconnect() {
+    if (this.shouldReconnectByTime) {
+      logger.info('Server should reconnect by time');
+      return true;
+    }
+    if (this.shouldReconnectByCount) {
+      logger.info('Server should reconnect by count');
+      return true;
+    }
+    return false;
+  }
+
+}
+
+async function nextTick(context, reconnectHandler) {
+  context.incrementCount();
+
+  if (context.shouldReconnect) {
+    const isSuccessToReconnect = await reconnectHandler();
+
+    // success to reconnect
+    if (isSuccessToReconnect) {
+      context.reset();
+    }
+    // fail to reconnect
+    else {
+      context.incrementStage();
+    }
+  }
+}
+
+module.exports = {
+  ReconnectContext,
+  nextTick,
+};

+ 17 - 6
src/server/service/search.js

@@ -11,7 +11,7 @@ class SearchService {
     this.isErrorOccuredOnSearching = null;
     this.isErrorOccuredOnSearching = null;
 
 
     try {
     try {
-      this.delegator = this.initDelegator();
+      this.delegator = this.generateDelegator();
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -39,7 +39,7 @@ class SearchService {
     return this.configManager.getConfig('crowi', 'app:elasticsearchUri') != null;
     return this.configManager.getConfig('crowi', 'app:elasticsearchUri') != null;
   }
   }
 
 
-  initDelegator() {
+  generateDelegator() {
     logger.info('Initializing search delegator');
     logger.info('Initializing search delegator');
 
 
     if (this.isSearchboxEnabled) {
     if (this.isSearchboxEnabled) {
@@ -52,7 +52,6 @@ class SearchService {
       const ElasticsearchDelegator = require('./search-delegator/elasticsearch.js');
       const ElasticsearchDelegator = require('./search-delegator/elasticsearch.js');
       return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
       return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
     }
     }
-
   }
   }
 
 
   registerUpdateEvent() {
   registerUpdateEvent() {
@@ -69,12 +68,24 @@ class SearchService {
     tagEvent.on('update', this.delegator.syncTagChanged.bind(this.delegator));
     tagEvent.on('update', this.delegator.syncTagChanged.bind(this.delegator));
   }
   }
 
 
-  async initClient() {
-    // reset error flag
+  resetErrorStatus() {
     this.isErrorOccuredOnHealthcheck = false;
     this.isErrorOccuredOnHealthcheck = false;
     this.isErrorOccuredOnSearching = false;
     this.isErrorOccuredOnSearching = false;
+  }
+
+  async reconnectClient() {
+    logger.info('Try to reconnect...');
+    this.delegator.initClient();
+
+    try {
+      await this.getInfoForHealth();
 
 
-    return this.delegator.initClient();
+      logger.info('Reconnecting succeeded.');
+      this.resetErrorStatus();
+    }
+    catch (err) {
+      throw err;
+    }
   }
   }
 
 
   async getInfo() {
   async getInfo() {

+ 2 - 0
src/server/views/login/error.html

@@ -17,6 +17,8 @@
 {% endblock %}
 {% endblock %}
 {% block sidebar %}
 {% block sidebar %}
 {% endblock %}
 {% endblock %}
+{% block head_warn_alert_siteurl_undefined %}
+{% endblock %}