Преглед изворни кода

Merge branch 'master' into feat/GW-6710-slackbot-togetter-command

hakumizuki пре 4 година
родитељ
комит
780a8170c5
32 измењених фајлова са 357 додато и 108 уклоњено
  1. 3 2
      .github/workflows/release.yml
  2. 15 5
      CHANGES.md
  3. 1 1
      package.json
  4. 2 1
      packages/app/package.json
  5. 1 0
      packages/slack/package.json
  6. 3 2
      packages/slack/src/middlewares/verify-growi-to-slack-request.ts
  7. 5 4
      packages/slack/src/middlewares/verify-slack-request.ts
  8. 1 2
      packages/slackbot-proxy/package.json
  9. 11 13
      packages/slackbot-proxy/src/Server.ts
  10. 9 8
      packages/slackbot-proxy/src/controllers/growi-to-slack.ts
  11. 22 0
      packages/slackbot-proxy/src/filters/CustomHttpErrorFilter.ts
  12. 28 0
      packages/slackbot-proxy/src/middlewares/GlobalHttpErrorHandlingMiddleware.ts
  13. 13 0
      packages/slackbot-proxy/src/models/errors.ts
  14. 10 1
      resource/locales/en_US/admin/admin.json
  15. 10 1
      resource/locales/ja_JP/admin/admin.json
  16. 10 1
      resource/locales/zh_CN/admin/admin.json
  17. 6 2
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  18. 6 2
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  19. 6 3
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  20. 7 2
      src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  21. 2 3
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  22. 12 6
      src/client/js/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  23. 30 4
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  24. 19 0
      src/client/js/util/codemirror/gfm-growi.mode.js
  25. 31 20
      src/client/styles/scss/_override-codemirror.scss
  26. 28 8
      src/client/styles/scss/_wiki.scss
  27. 0 1
      src/server/crowi/express-init.js
  28. 12 0
      src/server/crowi/index.js
  29. 32 0
      src/server/middlewares/http-error-handler.js
  30. 1 1
      src/server/routes/apiv3/pages.js
  31. 1 0
      src/server/service/app.js
  32. 20 15
      yarn.lock

+ 3 - 2
.github/workflows/release.yml

@@ -104,8 +104,9 @@ jobs:
       uses: actions/cache@v2
       uses: actions/cache@v2
       with:
       with:
         path: /tmp/.buildx-cache
         path: /tmp/.buildx-cache
-        key: ${{ runner.os }}-buildx-app-${{ github.sha }}
+        key: ${{ runner.os }}-buildx-app-${{ matrix.flavor }}-${{ github.sha }}
         restore-keys: |
         restore-keys: |
+          ${{ runner.os }}-buildx-app-${{ matrix.flavor }}-
           ${{ runner.os }}-buildx-app-
           ${{ runner.os }}-buildx-app-
 
 
     - name: Build and push
     - name: Build and push
@@ -137,7 +138,7 @@ jobs:
       with:
       with:
         channel: '#release'
         channel: '#release'
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
-        created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ env.SUFFIX }}'
+        created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ steps.suffix.outputs.SUFFIX }}'
 
 
     - name: Check whether workspace is clean
     - name: Check whether workspace is clean
       run: |
       run: |

+ 15 - 5
CHANGES.md

@@ -1,16 +1,26 @@
 # CHANGES
 # CHANGES
 
 
-## v4.3.2-RC
+## v4.3.3-RC
 
 
-* 
+*
+* Support: Upgrade libs
+    * escape-string-regexp
+## v4.3.2
+
+* Feature: Hufflpuff theme
+* Improvement: CodeMirror header styles
+* Improvement: CodeMirror syntax-highlighting fenced code blocks
+* Improvement: Slack Integration Settings
+    * Error behavior when getting connection statuses
+    * Add links to docs
+* Improvement: /_api/v3/recent can be accessed with access token
+* Support: Using http-errors
 
 
 ## v4.3.1
 ## v4.3.1
 
 
 * Fix: Build script for production
 * Fix: Build script for production
 
 
-
-
-## v4.3.0-RC
+## v4.3.0
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES
 
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.3.2-RC",
+  "version": "4.3.3-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 2 - 1
packages/app/package.json

@@ -43,7 +43,7 @@
     "entities": "^2.0.0",
     "entities": "^2.0.0",
     "env-cmd": "^10.0.1",
     "env-cmd": "^10.0.1",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
-    "escape-string-regexp": "^2.0.0",
+    "escape-string-regexp": "^5.0.0",
     "express": "^4.16.1",
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
@@ -57,6 +57,7 @@
     "growi-plugin-lsx": "^4.0.3",
     "growi-plugin-lsx": "^4.0.3",
     "growi-plugin-pukiwiki-like-linker": "^3.1.0",
     "growi-plugin-pukiwiki-like-linker": "^3.1.0",
     "helmet": "^3.13.0",
     "helmet": "^3.13.0",
+    "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-node-fs-backend": "^2.1.0",

+ 1 - 0
packages/slack/package.json

@@ -20,6 +20,7 @@
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
+    "http-errors": "^1.8.0",
     "universal-bunyan": "^0.9.2"
     "universal-bunyan": "^0.9.2"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 3 - 2
packages/slack/src/middlewares/verify-growi-to-slack-request.ts

@@ -1,5 +1,6 @@
 import { Response, NextFunction } from 'express';
 import { Response, NextFunction } from 'express';
 
 
+import createError from 'http-errors';
 import loggerFactory from '../utils/logger';
 import loggerFactory from '../utils/logger';
 import { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
 import { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
 
 
@@ -15,14 +16,14 @@ export const verifyGrowiToSlackRequest = (req: RequestFromGrowi, res: Response,
   if (str == null) {
   if (str == null) {
     const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
     const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
     logger.warn(message, { body: req.body });
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
   }
 
 
   const tokens = str.split(',').map(value => value.trim());
   const tokens = str.split(',').map(value => value.trim());
   if (tokens.length === 0) {
   if (tokens.length === 0) {
     const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
     const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
     logger.warn(message, { body: req.body });
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
   }
 
 
   req.tokenGtoPs = tokens;
   req.tokenGtoPs = tokens;

+ 5 - 4
packages/slack/src/middlewares/verify-slack-request.ts

@@ -2,6 +2,7 @@ import { createHmac, timingSafeEqual } from 'crypto';
 import { stringify } from 'qs';
 import { stringify } from 'qs';
 import { Response, NextFunction } from 'express';
 import { Response, NextFunction } from 'express';
 
 
+import createError from 'http-errors';
 import loggerFactory from '../utils/logger';
 import loggerFactory from '../utils/logger';
 import { RequestFromSlack } from '../interfaces/request-from-slack';
 import { RequestFromSlack } from '../interfaces/request-from-slack';
 
 
@@ -17,7 +18,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (signingSecret == null) {
   if (signingSecret == null) {
     const message = 'No signing secret.';
     const message = 'No signing secret.';
     logger.warn(message, { body: req.body });
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
   }
 
 
   // take out slackSignature and timestamp from header
   // take out slackSignature and timestamp from header
@@ -27,7 +28,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (slackSignature == null || timestamp == null) {
   if (slackSignature == null || timestamp == null) {
     const message = 'Forbidden. Enter from Slack workspace';
     const message = 'Forbidden. Enter from Slack workspace';
     logger.warn(message, { body: req.body });
     logger.warn(message, { body: req.body });
-    return res.status(403).send({ message });
+    return next(createError(403, message));
   }
   }
 
 
   // protect against replay attacks
   // protect against replay attacks
@@ -35,7 +36,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (Math.abs(time - timestamp) > 300) {
   if (Math.abs(time - timestamp) > 300) {
     const message = 'Verification failed.';
     const message = 'Verification failed.';
     logger.warn(message, { body: req.body });
     logger.warn(message, { body: req.body });
-    return res.status(403).send({ message });
+    return next(createError(403, message));
   }
   }
 
 
   // generate growi signature
   // generate growi signature
@@ -52,5 +53,5 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
 
 
   const message = 'Verification failed.';
   const message = 'Verification failed.';
   logger.warn(message, { body: req.body });
   logger.warn(message, { body: req.body });
-  return res.status(403).send({ message });
+  return next(createError(403, message));
 };
 };

+ 1 - 2
packages/slackbot-proxy/package.json

@@ -24,7 +24,6 @@
     "version": "node -p \"require('./package.json').version\""
     "version": "node -p \"require('./package.json').version\""
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
-    "express-graceful-exit": "0.5.2 includes a typings file error: https://github.com/emostar/express-graceful-exit/issues/24"
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.8.0",
     "@godaddy/terminus": "^4.8.0",
@@ -44,9 +43,9 @@
     "cross-env": "^7.0.0",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
-    "express-graceful-exit": "=0.5.0",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
+    "http-errors": "^1.8.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
     "mysql2": "^2.2.5",
     "typeorm": "^0.2.31",
     "typeorm": "^0.2.31",

+ 11 - 13
packages/slackbot-proxy/src/Server.ts

@@ -11,13 +11,14 @@ import methodOverride from 'method-override';
 import helmet from 'helmet';
 import helmet from 'helmet';
 import { Express } from 'express';
 import { Express } from 'express';
 import expressBunyanLogger from 'express-bunyan-logger';
 import expressBunyanLogger from 'express-bunyan-logger';
-import gracefulExit from 'express-graceful-exit';
 
 
-import { ConnectionOptions } from 'typeorm';
+import { ConnectionOptions, getConnectionManager } from 'typeorm';
 import { createTerminus } from '@godaddy/terminus';
 import { createTerminus } from '@godaddy/terminus';
 
 
 import swaggerSettingsForDev from '~/config/swagger/config.dev';
 import swaggerSettingsForDev from '~/config/swagger/config.dev';
 import swaggerSettingsForProd from '~/config/swagger/config.prod';
 import swaggerSettingsForProd from '~/config/swagger/config.prod';
+import { GlobalHttpErrorHandlingMiddleware } from './middlewares/GlobalHttpErrorHandlingMiddleware';
+import './filters/CustomHttpErrorFilter';
 import './filters/ResourceNotFoundFilter';
 import './filters/ResourceNotFoundFilter';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -105,6 +106,7 @@ const helmetOptions = isProduction ? {} : {
     ],
     ],
   },
   },
 })
 })
+
 export class Server {
 export class Server {
 
 
   @Inject()
   @Inject()
@@ -122,19 +124,11 @@ export class Server {
     if (serverUri === undefined) {
     if (serverUri === undefined) {
       throw new Error('The environment variable \'SERVER_URI\' must be defined.');
       throw new Error('The environment variable \'SERVER_URI\' must be defined.');
     }
     }
-
-    const server = this.injector.get<HttpServer>(HttpServer);
-
-    // init express-graceful-exit
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    gracefulExit.init(server!);
   }
   }
 
 
   $beforeRoutesInit(): void {
   $beforeRoutesInit(): void {
-    const expressApp = this.app.getApp();
 
 
     this.app
     this.app
-      .use(gracefulExit.middleware(expressApp))
       .use(cookieParser())
       .use(cookieParser())
       .use(compress({}))
       .use(compress({}))
       .use(methodOverride())
       .use(methodOverride())
@@ -146,16 +140,20 @@ export class Server {
     this.setupLogger();
     this.setupLogger();
   }
   }
 
 
+  $afterRoutesInit(): void {
+    this.app.use(GlobalHttpErrorHandlingMiddleware);
+  }
+
   $beforeListen(): void {
   $beforeListen(): void {
-    const expressApp = this.app.getApp();
     const server = this.injector.get<HttpServer>(HttpServer);
     const server = this.injector.get<HttpServer>(HttpServer);
 
 
     // init terminus
     // init terminus
     createTerminus(server, {
     createTerminus(server, {
       onSignal: async() => {
       onSignal: async() => {
         logger.info('server is starting cleanup');
         logger.info('server is starting cleanup');
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        gracefulExit.gracefulExitHandler(expressApp, server!);
+        const connectionManager = getConnectionManager();
+        const defaultConnection = connectionManager.get('default');
+        await defaultConnection.close();
       },
       },
       onShutdown: async() => {
       onShutdown: async() => {
         logger.info('cleanup finished, server is shutting down');
         logger.info('cleanup finished, server is shutting down');

+ 9 - 8
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -2,6 +2,7 @@ import {
   Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams,
   Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams,
 } from '@tsed/common';
 } from '@tsed/common';
 import axios from 'axios';
 import axios from 'axios';
+import createError from 'http-errors';
 
 
 import { WebAPICallResult } from '@slack/web-api';
 import { WebAPICallResult } from '@slack/web-api';
 
 
@@ -99,7 +100,7 @@ export class GrowiToSlackCtrl {
     const { tokenGtoPs } = req;
     const { tokenGtoPs } = req;
 
 
     if (tokenGtoPs.length !== 1) {
     if (tokenGtoPs.length !== 1) {
-      return res.status(400).send({ message: 'installation is invalid' });
+      throw createError(400, 'installation is invalid');
     }
     }
 
 
     const tokenGtoP = tokenGtoPs[0];
     const tokenGtoP = tokenGtoPs[0];
@@ -116,7 +117,7 @@ export class GrowiToSlackCtrl {
 
 
       const token = relation.installation.data.bot?.token;
       const token = relation.installation.data.bot?.token;
       if (token == null) {
       if (token == null) {
-        return res.status(400).send({ message: 'installation is invalid' });
+        throw createError(400, 'installation is invalid');
       }
       }
 
 
       try {
       try {
@@ -124,12 +125,12 @@ export class GrowiToSlackCtrl {
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
+        throw createError(400, `failed to request to GROWI. err: ${err.message}`);
       }
       }
 
 
       const status = await getConnectionStatus(token);
       const status = await getConnectionStatus(token);
       if (status.error != null) {
       if (status.error != null) {
-        return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
+        throw createError(400, `failed to get connection. err: ${status.error}`);
       }
       }
 
 
       return res.send({ relation, slackBotToken: token });
       return res.send({ relation, slackBotToken: token });
@@ -143,7 +144,7 @@ export class GrowiToSlackCtrl {
       .getOne();
       .getOne();
 
 
     if (order == null || order.isExpired()) {
     if (order == null || order.isExpired()) {
-      return res.status(400).send({ message: 'order has expired or does not exist.' });
+      throw createError(400, 'order has expired or does not exist.');
     }
     }
 
 
     // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
     // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
@@ -152,19 +153,19 @@ export class GrowiToSlackCtrl {
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
+      throw createError(400, `failed to request to GROWI. err: ${err.message}`);
     }
     }
 
 
     logger.debug('order found', order);
     logger.debug('order found', order);
 
 
     const token = order.installation.data.bot?.token;
     const token = order.installation.data.bot?.token;
     if (token == null) {
     if (token == null) {
-      return res.status(400).send({ message: 'installation is invalid' });
+      throw createError(400, 'installation is invalid');
     }
     }
 
 
     const status = await getConnectionStatus(token);
     const status = await getConnectionStatus(token);
     if (status.error != null) {
     if (status.error != null) {
-      return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
+      throw createError(400, `failed to get connection. err: ${status.error}`);
     }
     }
 
 
     logger.debug('relation test is success', order);
     logger.debug('relation test is success', order);

+ 22 - 0
packages/slackbot-proxy/src/filters/CustomHttpErrorFilter.ts

@@ -0,0 +1,22 @@
+import {
+  Catch, ExceptionFilterMethods, PlatformContext, PlatformResponse,
+} from '@tsed/common';
+
+import { CustomHttpError } from '~/models/errors';
+
+@Catch(CustomHttpError)
+export class CustomHttpErrorFilter implements ExceptionFilterMethods {
+
+  async catch(exception: CustomHttpError, ctx: PlatformContext): Promise<PlatformResponse<any>> {
+    const { httpError } = exception;
+    const { response } = ctx;
+
+    return response
+      .status(httpError.status)
+      .body({
+        status: httpError.status,
+        message: httpError.message,
+      });
+  }
+
+}

+ 28 - 0
packages/slackbot-proxy/src/middlewares/GlobalHttpErrorHandlingMiddleware.ts

@@ -0,0 +1,28 @@
+import {
+  Err, Middleware, Next, PlatformContext, PlatformResponse,
+} from '@tsed/common';
+
+import { HttpError, isHttpError } from 'http-errors';
+
+@Middleware()
+export class GlobalHttpErrorHandlingMiddleware {
+
+  use(@Err() err: unknown, @Next() next: Next, ctx: PlatformContext): PlatformResponse<any>|void {
+
+    // handle if the err is a HttpError instance
+    if (isHttpError(err)) {
+      const httpError = err as HttpError;
+      const { response } = ctx;
+
+      return response
+        .status(httpError.status)
+        .body({
+          status: httpError.status,
+          message: httpError.message,
+        });
+    }
+
+    next(err);
+  }
+
+}

+ 13 - 0
packages/slackbot-proxy/src/models/errors.ts

@@ -1,5 +1,7 @@
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
 
 
+import { HttpError } from 'http-errors';
+
 export class InvalidUrlError extends ExtensibleCustomError {
 export class InvalidUrlError extends ExtensibleCustomError {
 
 
   constructor(url: string) {
   constructor(url: string) {
@@ -7,3 +9,14 @@ export class InvalidUrlError extends ExtensibleCustomError {
   }
   }
 
 
 }
 }
+
+export class CustomHttpError extends Error {
+
+  httpError: HttpError
+
+  constructor(httpError: HttpError) {
+    super(httpError.message);
+    this.httpError = httpError;
+  }
+
+}

+ 10 - 1
resource/locales/en_US/admin/admin.json

@@ -339,7 +339,16 @@
       "integration_some_ws_is_not_complete": "Some work spaces is not linked"
       "integration_some_ws_is_not_complete": "Some work spaces is not linked"
     },
     },
     "custom_bot_with_proxy_integration": "Custom Bot With Proxy Integration",
     "custom_bot_with_proxy_integration": "Custom Bot With Proxy Integration",
-    "official_bot_integration": "Official bot integration"
+    "official_bot_integration": "Official bot integration",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#official-bot-%E3%80%90recommended%E3%80%91",
+      "custom_bot_without_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
   },
   "user_management": {
   "user_management": {
     "invite_users": "Temporarily issue a new user",
     "invite_users": "Temporarily issue a new user",

+ 10 - 1
resource/locales/ja_JP/admin/admin.json

@@ -337,7 +337,16 @@
 
 
     },
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 連携",
     "custom_bot_with_proxy_integration": "Custom bot with proxy 連携",
-    "official_bot_integration": "Official bot 連携"
+    "official_bot_integration": "Official bot 連携",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#official-bot-%E6%8E%A8%E5%A5%A8",
+      "custom_bot_without_proxy": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
   },
   "user_management": {
   "user_management": {
     "invite_users": "新規ユーザーの仮発行",
     "invite_users": "新規ユーザーの仮発行",

+ 10 - 1
resource/locales/zh_CN/admin/admin.json

@@ -346,7 +346,16 @@
       "integration_some_ws_is_not_complete": "有的工作空间未能连接"
       "integration_some_ws_is_not_complete": "有的工作空间未能连接"
     },
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 一体化",
     "custom_bot_with_proxy_integration": "Custom bot with proxy 一体化",
-    "official_bot_integration": "Official bot 一体化"
+    "official_bot_integration": "Official bot 一体化",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#official-bot-%E3%80%90recommended%E3%80%91",
+      "custom_bot_without_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
   },
 	"user_management": {
 	"user_management": {
 		"invite_users": "临时发布新用户",
 		"invite_users": "临时发布新用户",

+ 6 - 2
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -66,8 +66,12 @@ const CustomBotWithProxySettings = (props) => {
   return (
   return (
     <>
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.custom_bot_with_proxy')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+          />
+        </a>
       </h2>
       </h2>
 
 
       {slackAppIntegrations.length !== 0 && (
       {slackAppIntegrations.length !== 0 && (

+ 6 - 2
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -21,8 +21,12 @@ const CustomBotWithoutProxySettings = (props) => {
   return (
   return (
     <>
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.custom_bot_without_proxy')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+          />
+        </a>
       </h2>
       </h2>
 
 
       <CustomBotWithoutProxyConnectionStatus
       <CustomBotWithoutProxyConnectionStatus

+ 6 - 3
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -76,15 +76,18 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             {t('admin:slack_integration.accordion.create_bot')}
             {t('admin:slack_integration.accordion.create_bot')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </button>
           </button>
-          {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-          {/* <a href="#">
+          <a
+            href={t('admin:slack_integration.docs_url.custom_bot_without_proxy_setting')}
+            target="_blank"
+            rel="noopener noreferrer"
+          >
             <p className="text-center mt-1">
             <p className="text-center mt-1">
               <small>
               <small>
                 {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                 {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
               </small>
               </small>
             </p>
             </p>
-          </a> */}
+          </a>
         </div>
         </div>
       </Accordion>
       </Accordion>
       <Accordion
       <Accordion

+ 7 - 2
src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -47,8 +47,13 @@ const OfficialBotSettings = (props) => {
   return (
   return (
     <>
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.official_bot')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+            onClick={() => window.open(`${t('admin:slack_integration.docs_url.official_bot')}`, '_blank')}
+          />
+        </a>
       </h2>
       </h2>
 
 
       {slackAppIntegrations.length !== 0 && (
       {slackAppIntegrations.length !== 0 && (

+ 2 - 3
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -191,11 +191,10 @@ const SlackIntegration = (props) => {
       <div className="selecting-bot-type mb-5">
       <div className="selecting-bot-type mb-5">
         <h2 className="admin-setting-header mb-4">
         <h2 className="admin-setting-header mb-4">
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
-          {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-          {/* <a className="ml-2 btn-link" href="#">
+          <a className="ml-2 btn-link" href={t('admin:slack_integration.docs_url.slack_integration')} target="_blank" rel="noopener noreferrer">
             {t('admin:slack_integration.selecting_bot_types.detailed_explanation')}
             {t('admin:slack_integration.selecting_bot_types.detailed_explanation')}
             <i className="fa fa-external-link ml-1" aria-hidden="true"></i>
             <i className="fa fa-external-link ml-1" aria-hidden="true"></i>
-          </a> */}
+          </a>
         </h2>
         </h2>
 
 
         { errorCode && (
         { errorCode && (

+ 12 - 6
src/client/js/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -22,15 +22,18 @@ const BotCreateProcess = () => {
         {t('admin:slack_integration.accordion.create_bot')}
         {t('admin:slack_integration.accordion.create_bot')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
       </button>
-      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-      {/* <a href="#">
+      <a
+        href={t('admin:slack_integration.docs_url.custom_bot_with_proxy_setting')}
+        target="_blank"
+        rel="noopener noreferrer"
+      >
         <p className="text-center mt-1">
         <p className="text-center mt-1">
           <small>
           <small>
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
           </small>
         </p>
         </p>
-      </a> */}
+      </a>
     </div>
     </div>
   );
   );
 };
 };
@@ -43,15 +46,18 @@ const BotInstallProcessForOfficialBot = () => {
         {t('admin:slack_integration.accordion.install_now')}
         {t('admin:slack_integration.accordion.install_now')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
       </button>
-      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-      {/* <a href="#">
+      <a
+        href={t('admin:slack_integration.docs_url.official_bot_setting')}
+        target="_blank"
+        rel="noopener noreferrer"
+      >
         <p className="text-center mt-1">
         <p className="text-center mt-1">
           <small>
           <small>
             {t('admin:slack_integration.accordion.how_to_install')}
             {t('admin:slack_integration.accordion.how_to_install')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
           </small>
         </p>
         </p>
-      </a> */}
+      </a>
     </div>
     </div>
   );
   );
 };
 };

+ 30 - 4
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -54,8 +54,36 @@ require('codemirror/addon/fold/foldgutter.css');
 require('codemirror/addon/fold/markdown-fold');
 require('codemirror/addon/fold/markdown-fold');
 require('codemirror/addon/fold/brace-fold');
 require('codemirror/addon/fold/brace-fold');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/display/placeholder');
-require('codemirror/mode/gfm/gfm');
 require('../../util/codemirror/autorefresh.ext');
 require('../../util/codemirror/autorefresh.ext');
+require('../../util/codemirror/gfm-growi.mode');
+// import modes to highlight
+require('codemirror/mode/clike/clike');
+require('codemirror/mode/css/css');
+require('codemirror/mode/django/django');
+require('codemirror/mode/erlang/erlang');
+require('codemirror/mode/gfm/gfm');
+require('codemirror/mode/go/go');
+require('codemirror/mode/javascript/javascript');
+require('codemirror/mode/jsx/jsx');
+require('codemirror/mode/mathematica/mathematica');
+require('codemirror/mode/nginx/nginx');
+require('codemirror/mode/perl/perl');
+require('codemirror/mode/php/php');
+require('codemirror/mode/python/python');
+require('codemirror/mode/r/r');
+require('codemirror/mode/ruby/ruby');
+require('codemirror/mode/rust/rust');
+require('codemirror/mode/sass/sass');
+require('codemirror/mode/shell/shell');
+require('codemirror/mode/sql/sql');
+require('codemirror/mode/stex/stex');
+require('codemirror/mode/stylus/stylus');
+require('codemirror/mode/swift/swift');
+require('codemirror/mode/toml/toml');
+require('codemirror/mode/vb/vb');
+require('codemirror/mode/vue/vue');
+require('codemirror/mode/xml/xml');
+require('codemirror/mode/yaml/yaml');
 
 
 
 
 const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
 const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
@@ -820,7 +848,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
   }
 
 
   render() {
   render() {
-    const mode = this.state.isGfmMode ? 'gfm' : undefined;
+    const mode = this.state.isGfmMode ? 'gfm-growi' : undefined;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
 
 
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
@@ -857,8 +885,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
             gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
             gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
             // match-highlighter, matchesonscrollbar, annotatescrollbar options
             // match-highlighter, matchesonscrollbar, annotatescrollbar options
             highlightSelectionMatches: { annotateScrollbar: true },
             highlightSelectionMatches: { annotateScrollbar: true },
-            // markdown mode options
-            highlightFormatting: true,
             // continuelist, indentlist
             // continuelist, indentlist
             extraKeys: {
             extraKeys: {
               Enter: this.handleEnterKey,
               Enter: this.handleEnterKey,

+ 19 - 0
src/client/js/util/codemirror/gfm-growi.mode.js

@@ -0,0 +1,19 @@
+// https://discuss.codemirror.net/t/cm-header-margin-padding-height/75/5
+window.CodeMirror.defineMode('gfm-growi', (cmConfig, modeCfg) => {
+  // based on Markdown (GitHub-flavour) mode
+  // https://codemirror.net/doc/manual.html#option_mode
+  // https://codemirror.net/mode/index.html
+  modeCfg.name = 'gfm';
+  modeCfg.highlightFormatting = true;
+  const mode = window.CodeMirror.getMode(cmConfig, modeCfg);
+
+  const origToken = mode.token;
+  mode.token = function(stream, state) {
+    let classes = origToken(stream, state) || '';
+    // https://regex101.com/r/Fep0w2/1
+    classes = classes.replace(/(^| )header(\S*)/g, '$1header$2 line-grw-cm-header-line');
+    return /^\s*$/.test(classes) ? null : classes;
+  };
+
+  return mode;
+});

+ 31 - 20
src/client/styles/scss/_override-codemirror.scss

@@ -1,22 +1,38 @@
 .CodeMirror {
 .CodeMirror {
-  .cm-header-1 {
-    font-size: 1.8em;
-  }
-  .cm-header-2 {
-    font-size: 1.4em;
-  }
-  .cm-header-3 {
-    font-size: 1.2em;
-  }
-  .cm-header-4 {
-    font-size: 1.1em;
-  }
-  .cm-header-5 {
-    font-size: 1.05em;
+  pre.CodeMirror-line.grw-cm-header-line {
+    padding-top: 0.16em;
+    padding-bottom: 0.08em;
+    font-family: $font-family-monospace;
+
+    // '#'
+    .cm-formatting-header {
+      font-style: italic;
+      font-weight: bold;
+      opacity: 0.5;
+    }
+
+    .cm-header-1 {
+      font-size: 1.9em;
+    }
+    .cm-header-2 {
+      font-size: 1.6em;
+    }
+    .cm-header-3 {
+      font-size: 1.4em;
+    }
+    .cm-header-4 {
+      font-size: 1.35em;
+    }
+    .cm-header-5 {
+      font-size: 1.25em;
+    }
+    .cm-header-6 {
+      font-size: 1.2em;
+    }
   }
   }
 
 
   .cm-matchhighlight {
   .cm-matchhighlight {
-    color: $gray-900;
+    color: $gray-900 !important;
     background-color: cyan;
     background-color: cyan;
   }
   }
 
 
@@ -28,9 +44,4 @@
   pre.CodeMirror-placeholder {
   pre.CodeMirror-placeholder {
     color: $text-muted;
     color: $text-muted;
   }
   }
-
-  // overwrite .CodeMirror pre
-  pre.CodeMirror-line {
-    font-family: $font-family-monospace;
-  }
 }
 }

+ 28 - 8
src/client/styles/scss/_wiki.scss

@@ -8,6 +8,15 @@ div.body {
 }
 }
 
 
 .wiki {
 .wiki {
+  @mixin add-left-border($width) {
+    &:before {
+      margin-right: 0.4em;
+      content: '';
+      border-left: $width solid;
+      opacity: 0.2;
+    }
+  }
+
   @extend .text-break;
   @extend .text-break;
 
 
   font-size: 15px;
   font-size: 15px;
@@ -29,29 +38,40 @@ div.body {
   h1 {
   h1 {
     padding: 0.5em 0;
     padding: 0.5em 0;
     margin-top: 2em;
     margin-top: 2em;
-    font-size: 1.8em;
+    font-size: 1.9em;
     line-height: 1.1em;
     line-height: 1.1em;
+    // style
     border-bottom: solid 1px transparent;
     border-bottom: solid 1px transparent;
   }
   }
   h2 {
   h2 {
-    // padding-top: 0.5em;
-    padding-bottom: 0.5em;
-    font-size: 1.4em;
+    padding-bottom: 0.3em;
+    font-size: 1.6em;
     font-weight: bold;
     font-weight: bold;
     line-height: 1.225;
     line-height: 1.225;
-    border-bottom: 1px solid transparent;
+    // style
+    border-bottom: solid 1px transparent;
   }
   }
   h3 {
   h3 {
-    font-size: 1.2em;
+    font-size: 1.4em;
     font-weight: bold;
     font-weight: bold;
   }
   }
   h4 {
   h4 {
-    font-size: 1.1em;
+    font-size: 1.35em;
     font-weight: normal;
     font-weight: normal;
+    // style
+    @include add-left-border(6px);
   }
   }
   h5 {
   h5 {
-    font-size: 1.05em;
+    font-size: 1.25em;
+    font-weight: normal;
+    // style
+    @include add-left-border(4px);
+  }
+  h6 {
+    font-size: 1.2em;
     font-weight: normal;
     font-weight: normal;
+    // style
+    @include add-left-border(2px);
   }
   }
 
 
   p {
   p {

+ 0 - 1
src/server/crowi/express-init.js

@@ -1,5 +1,4 @@
 
 
-
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   const debug = require('debug')('growi:crowi:express-init');
   const debug = require('debug')('growi:crowi:express-init');
   const path = require('path');
   const path = require('path');

+ 12 - 0
src/server/crowi/index.js

@@ -13,6 +13,7 @@ const mongoose = require('mongoose');
 const models = require('../models');
 const models = require('../models');
 
 
 const PluginService = require('../plugins/plugin.service');
 const PluginService = require('../plugins/plugin.service');
+const httpErrorHandler = require('../middlewares/http-error-handler');
 
 
 const sep = path.sep;
 const sep = path.sep;
 
 
@@ -451,6 +452,9 @@ Crowi.prototype.start = async function() {
   // setup Express Routes
   // setup Express Routes
   this.setupRoutesAtLast();
   this.setupRoutesAtLast();
 
 
+  // setup Global Error Handlers
+  this.setupGlobalErrorHandlers();
+
   return serverListening;
   return serverListening;
 };
 };
 
 
@@ -486,6 +490,14 @@ Crowi.prototype.setupRoutesAtLast = function() {
   require('../routes')(this, this.express);
   require('../routes')(this, this.express);
 };
 };
 
 
+/**
+ * setup global error handlers
+ * !! this must be after the Routes setup !!
+ */
+Crowi.prototype.setupGlobalErrorHandlers = function() {
+  this.express.use(httpErrorHandler);
+};
+
 /**
 /**
  * require API for plugins
  * require API for plugins
  *
  *

+ 32 - 0
src/server/middlewares/http-error-handler.js

@@ -0,0 +1,32 @@
+const { HttpError } = require('../../../node_modules/http-errors');
+
+const isHttpError = (val) => {
+  if (!val || typeof val !== 'object') {
+    return false;
+  }
+
+  if (val instanceof HttpError) {
+    return true;
+  }
+
+  return val instanceof Error
+    && typeof val.expose === 'boolean'
+    && typeof val.statusCode === 'number'
+    && val.status === val.statusCode;
+};
+
+module.exports = async(err, req, res, next) => {
+  // handle if the err is a HttpError instance
+  if (isHttpError(err)) {
+    const httpError = err;
+
+    return res
+      .status(httpError.status)
+      .send({
+        status: httpError.status,
+        message: httpError.message,
+      });
+  }
+
+  next(err);
+};

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

@@ -283,7 +283,7 @@ module.exports = (crowi) => {
    *            description: Return pages recently updated
    *            description: Return pages recently updated
    *
    *
    */
    */
-  router.get('/recent', loginRequired, async(req, res) => {
+  router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
     const limit = 20;
     const limit = 20;
     const offset = parseInt(req.query.offset) || 0;
     const offset = parseInt(req.query.offset) || 0;
 
 

+ 1 - 0
src/server/service/app.js

@@ -113,6 +113,7 @@ class AppService extends S2sMessageHandlable {
   async setupAfterInstall() {
   async setupAfterInstall() {
     this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupRoutesAtLast();
+    this.crowi.setupGlobalErrorHandlers();
 
 
     // remove message handler
     // remove message handler
     const { s2sMessagingService } = this;
     const { s2sMessagingService } = this;

+ 20 - 15
yarn.lock

@@ -7514,9 +7514,10 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 
-escape-string-regexp@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+escape-string-regexp@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
+  integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
 
 
 escodegen@^1.11.1:
 escodegen@^1.11.1:
   version "1.14.1"
   version "1.14.1"
@@ -7947,13 +7948,6 @@ express-form@~0.12.0:
     object-additions "^0.5.1"
     object-additions "^0.5.1"
     validator "^2.1.0"
     validator "^2.1.0"
 
 
-express-graceful-exit@=0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/express-graceful-exit/-/express-graceful-exit-0.5.0.tgz#fcde6495af84f361e66f119bf0e3eca69955f9f1"
-  integrity sha512-gZkxdmgYz6VVHjZiMTaO59N875ugmBnzopF95q8Fzmws86JLNWMwGjLVG4i+99WO4v9L5wl6vKYFyGoHKoNJ7A==
-  dependencies:
-    underscore "^1.4.4"
-
 express-mongo-sanitize@^2.1.0:
 express-mongo-sanitize@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/express-mongo-sanitize/-/express-mongo-sanitize-2.1.0.tgz#a8c647787c25ded6e97b5e864d113e7687c5d471"
   resolved "https://registry.yarnpkg.com/express-mongo-sanitize/-/express-mongo-sanitize-2.1.0.tgz#a8c647787c25ded6e97b5e864d113e7687c5d471"
@@ -9653,6 +9647,17 @@ http-errors@1.7.3, http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
     toidentifier "1.0.0"
 
 
+http-errors@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
+  integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.2.0"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1:
 http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
   resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
@@ -17066,6 +17071,11 @@ setprototypeof@1.1.1:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
 
 
+setprototypeof@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
 sha.js@^2.4.0, sha.js@^2.4.8:
 sha.js@^2.4.0, sha.js@^2.4.8:
   version "2.4.9"
   version "2.4.9"
   resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
   resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
@@ -19024,11 +19034,6 @@ umask@^1.1.0:
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
   integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
   integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
 
 
-underscore@^1.4.4:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
-  integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
-
 unherit@^1.0.4:
 unherit@^1.0.4:
   version "1.1.2"
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.2.tgz#14f1f397253ee4ec95cec167762e77df83678449"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.2.tgz#14f1f397253ee4ec95cec167762e77df83678449"