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

Merge pull request #3992 from weseek/master

Release
itizawa 5 лет назад
Родитель
Сommit
d05b15ddbf
43 измененных файлов с 563 добавлено и 335 удалено
  1. 2 2
      .github/workflows/list-unhealthy-branches.yml
  2. 24 32
      .github/workflows/release-rc.yml
  3. 32 38
      .github/workflows/release.yml
  4. 29 2
      CHANGES.md
  5. 2 2
      bin/github-actions/update-readme.sh
  6. 4 1
      docker/Dockerfile
  7. 4 6
      docker/README.md
  8. 1 1
      package.json
  9. 8 8
      packages/app/package.json
  10. 4 4
      packages/slack/src/utils/reshape-contents-body.ts
  11. 1 0
      packages/slackbot-proxy/package.json
  12. 11 0
      packages/slackbot-proxy/src/Server.ts
  13. 20 0
      packages/slackbot-proxy/src/controllers/term.ts
  14. 22 0
      packages/slackbot-proxy/src/filters/ResourceNotFoundFilter.ts
  15. 32 24
      packages/slackbot-proxy/src/services/RegisterService.ts
  16. 9 0
      packages/slackbot-proxy/src/views/commons/head.ejs
  17. 1 3
      packages/slackbot-proxy/src/views/privacy.ejs
  18. 79 0
      packages/slackbot-proxy/src/views/term.ejs
  19. 42 17
      packages/slackbot-proxy/src/views/top.ejs
  20. 4 0
      resource/Contributor.js
  21. 2 2
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  22. 1 3
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.jsx
  23. 13 11
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  24. 2 2
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  25. 3 3
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  26. 3 40
      src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  27. 4 4
      src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx
  28. 8 12
      src/client/js/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  29. 2 4
      src/client/js/components/Page/RevisionRenderer.jsx
  30. 33 17
      src/client/js/components/PageCreateModal.jsx
  31. 2 0
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  32. 7 5
      src/client/js/components/PageEditor/LinkEditModal.jsx
  33. 1 1
      src/client/js/components/PagePathAutoComplete.jsx
  34. 0 21
      src/client/styles/scss/_on-edit.scss
  35. 36 0
      src/client/styles/scss/_override-codemirror.scss
  36. 8 0
      src/client/styles/scss/_wiki.scss
  37. 3 0
      src/client/styles/scss/style-app.scss
  38. 23 1
      src/lib/util/path-utils.js
  39. 5 3
      src/server/models/slack-app-integration.js
  40. 0 7
      src/server/models/user.js
  41. 1 1
      src/server/plugins/plugin-utils.js
  42. 8 1
      src/server/routes/apiv3/slack-integration-settings.js
  43. 67 57
      yarn.lock

+ 2 - 2
.github/workflows/list-unhealthy-branches.yml

@@ -37,7 +37,7 @@ jobs:
         status: custom
         payload: |
           {
-            text: '<!channel> There is some *illegal named branches* on GitHub.',
+            text: '<!channel> There is some branches *with illegal names* on GitHub.',
             channel: '#ci',
             attachments: ${{ steps.list-branches.outputs.SLACK_ATTACHMENTS_ILLEGAL }}
           }
@@ -51,7 +51,7 @@ jobs:
         status: custom
         payload: |
           {
-            text: '<!channel> There is some *illegal named branches* on GitHub.',
+            text: '<!channel> There is some branches *that are no longer updated* on GitHub.',
             channel: '#ci',
             attachments: ${{ steps.list-branches.outputs.SLACK_ATTACHMENTS_INACTIVE }}
           }

+ 24 - 32
.github/workflows/release-rc.yml

@@ -14,36 +14,24 @@ jobs:
     steps:
     - uses: actions/checkout@v2
 
-    - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
-
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
-
-    - name: Build Docker Image
-      run: |
-        CACHE_REF=weseek/growi-cache:4
-        docker buildx build \
-          --tag growi \
-          --platform linux/amd64 \
-          --load \
-          --cache-from=type=registry,ref=$CACHE_REF \
-          --cache-to=type=registry,ref=$CACHE_REF,mode=max \
-          --file ./docker/Dockerfile .
-
-    - name: Get SemVer
+    - name: Setup semver
+      id: semver
       run: |
         semver=`npm run version --silent`
-        echo "SEMVER=$semver" >> $GITHUB_ENV
+        echo "::set-output name=SEMVER::$semver"
 
-    - name: Docker Tags by SemVer
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
+    - name: Docker meta
+      id: meta
+      uses: docker/metadata-action@v3
       with:
-        source: growi
-        target: weseek/growi
-        semver: ${{ env.SEMVER }}
-        publish: true
+        images: weseek/growi,ghcr.io/weseek/growi
+        tags: |
+          type=raw,value=${{ steps.semver.outputs.SEMVER }}
+          type=raw,value=${{ steps.semver.outputs.SEMVER }}.{{sha}}
+
+    - name: Login to docker.io registry
+      run: |
+        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
     - name: Login to GitHub Container Registry
       uses: docker/login-action@v1
@@ -52,13 +40,17 @@ jobs:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
-    - name: Docker Tags by SemVer in Github Container Registry
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v1
+
+    - name: Build and push
+      uses: docker/build-push-action@v2
       with:
-        source: growi
-        target: ghcr.io/weseek/growi
-        semver: ${{ env.SEMVER }}
-        publish: true
+        context: .
+        file: ./docker/Dockerfile
+        platforms: linux/amd64
+        push: true
+        tags: ${{ steps.meta.outputs.tags }}
 
     - name: Check whether workspace is clean
       run: |

+ 32 - 38
.github/workflows/release.yml

@@ -67,36 +67,47 @@ jobs:
         git fetch --tags
         git checkout refs/tags/v${{ needs.github-release.outputs.RELEASE_VERSION }}
 
-    - name: Determine suffix
+    - name: Setup suffix
+      id: suffix
       run: |
         [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo "SUFFIX=$suffix" >> $GITHUB_ENV
+        echo "::set-output name=SUFFIX::$suffix"
 
-    - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+    - name: Docker meta
+      id: meta
+      uses: docker/metadata-action@v3
+      with:
+        images: weseek/growi,ghcr.io/weseek/growi
+        flavor: |
+          suffix=${{ steps.suffix.outputs.SUFFIX }}
+        tags: |
+          type=raw,value=latest
+          type=semver,value=${{ needs.github-release.outputs.RELEASE_VERSION }},pattern={{major}}
+          type=semver,value=${{ needs.github-release.outputs.RELEASE_VERSION }},pattern={{major}}.{{minor}}
+          type=semver,value=${{ needs.github-release.outputs.RELEASE_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
 
     - name: Login to docker.io registry
       run: |
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
-    - name: Build Docker Image
-      run: |
-        docker buildx build \
-          --tag growi${{ env.SUFFIX }} \
-          --build-arg flavor=${{ matrix.flavor }} \
-          --platform linux/amd64 \
-          --load \
-          --file ./docker/Dockerfile .
-
-    - name: Docker Tags by SemVer
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v1
       with:
-        source: growi${{ env.SUFFIX }}
-        target: weseek/growi
-        semver: ${{ needs.github-release.outputs.RELEASE_VERSION }}
-        suffix: ${{ env.SUFFIX }}
-        additional-tags: 'latest'
-        publish: true
+        registry: ghcr.io
+        username: wsmoogle
+        password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
+
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v1
+
+    - name: Build and push
+      uses: docker/build-push-action@v2
+      with:
+        context: .
+        file: ./docker/Dockerfile
+        platforms: linux/amd64
+        push: true
+        tags: ${{ steps.meta.outputs.tags }}
 
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v2
@@ -106,23 +117,6 @@ jobs:
         repository: weseek/growi
         readme-filepath: ./docker/README.md
 
-    - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
-      with:
-        registry: ghcr.io
-        username: wsmoogle
-        password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
-
-    - name: Docker Tags by SemVer in Github Container Registry
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
-      with:
-        source: growi${{ env.SUFFIX }}
-        target: ghcr.io/weseek/growi
-        semver: ${{ needs.github-release.outputs.RELEASE_VERSION }}
-        suffix: ${{ env.SUFFIX }}
-        additional-tags: 'latest'
-        publish: true
-
     - name: Slack Notification
       uses: weseek/ghaction-release-slack-notification@master
       with:

+ 29 - 2
CHANGES.md

@@ -1,14 +1,41 @@
 # CHANGES
 
-## v4.2.21-RC
+## v4.3.0-RC
 
+* Support: Upgrade libs
+    * striptags
+
+### BREAKING CHANGES
+
+None.
+
+Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/43x.html>
+
+### Updates
+
+* Feature: New Slack Integration with Slack Bot
+    * Searching GROWI pages from Slack
+    * Creating GROWI pages from Slack
+        * Easy record conversations
+
+## v4.2.21
+
+* Improvement: Headers style on built-in editor
+* Improvement: Codemirror is now scrollable one editor height of empty space into view at the bottom of the editor
 * Improvement: Upgrade mongodb driver to fix [NODE-2784](https://jira.mongodb.org/browse/NODE-2784)
 * Support: Upgrade libs
     * connect-mongo
+    * i18next
     * migrate-mongo
     * mongoose
     * stream-to-promise
-
+    * validator
+    * ws
+    * nodemailer
+    * i18next-express-middleware
+    * growi-commons
+    * growi-plugin-attachment-refs
+    * growi-plugin-lsx
 
 ## v4.2.20
 

+ 2 - 2
bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.2\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.2-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.3\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.3-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

+ 4 - 1
docker/Dockerfile

@@ -85,6 +85,7 @@ ENV appDir /opt/growi
 WORKDIR ${appDir}
 
 COPY ./package.json ./
+COPY ./yarn.lock ./
 COPY ./lerna.json ./
 COPY ./tsconfig.base.json ./
 COPY ./babel.config.js ./
@@ -99,16 +100,18 @@ COPY packages/slack packages/slack
 COPY packages/app packages/app
 
 # build
-RUN yarn build:prod
+RUN yarn lerna run build
 
 # make artifacts
 RUN tar cf packages.tar \
   package.json \
+  yarn.lock \
   config \
   public \
   resource \
   src \
   tmp \
+  packages/app/package.json \
   packages/slack/package.json \
   packages/slack/dist
 

+ 4 - 6
docker/README.md

@@ -10,12 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.2.0`, `4.2`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
-* [`4.2.0-nocdn`, `4.2-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
-* [`4.1.10`, `4.1` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.10/docker/Dockerfile)
-* [`4.1.10-nocdn`, `4.1-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.10/docker/Dockerfile)
-* [`3.8.0`, `3.8`, `3` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
-* [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
+* [`4.3.0`, `4.3`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.0/docker/Dockerfile)
+* [`4.3.0-nocdn`, `4.3-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.0/docker/Dockerfile)
+* [`4.2.0`, `4.2` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
+* [`4.2.0-nocdn`, `4.2-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
 
 
 What is GROWI?

+ 1 - 1
package.json

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

+ 8 - 8
packages/app/package.json

@@ -51,13 +51,13 @@
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^5.0.3",
-    "growi-plugin-attachment-refs": "^2.0.1",
-    "growi-plugin-lsx": "^4.0.2",
+    "growi-commons": "^5.0.4",
+    "growi-plugin-attachment-refs": "^2.0.2",
+    "growi-plugin-lsx": "^4.0.3",
     "growi-plugin-pukiwiki-like-linker": "^3.1.0",
     "helmet": "^3.13.0",
-    "i18next": "^19.0.0",
-    "i18next-express-middleware": "^1.4.1",
+    "i18next": "^20.3.2",
+    "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "is-iso-date": "^0.0.1",
@@ -73,7 +73,7 @@
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
-    "nodemailer": "^6.0.0",
+    "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
@@ -100,8 +100,8 @@
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
-    "validator": "^12.0.0",
-    "ws": "^7.3.1",
+    "validator": "^13.6.0",
+    "ws": "^7.4.6",
     "xss": "^1.0.6"
   },
   "// comments for defDependencies": {

+ 4 - 4
packages/slack/src/utils/reshape-contents-body.ts

@@ -62,8 +62,8 @@ export const reshapeContentsBody = (str: string): string => {
       if (matched !== null && matched.length > 0) {
         time = matched[0];
       }
-      // </div><div class="slack-talk-bubble">##*username*  HH:mm AM
-      copyline = '</div>\n<div class="slack-talk-bubble">\n\n## **'.concat(copyline);
+      // ##*username*  HH:mm AM
+      copyline = '\n## **'.concat(copyline);
       copyline = copyline.replace(regexpTime, '**'.concat(time));
     }
     // Check 3: Is this line a short time(HH:mm)?
@@ -81,8 +81,8 @@ export const reshapeContentsBody = (str: string): string => {
   });
   // remove all blanks
   const blanksRemoved = reshapedArray.filter(line => line !== '');
-  // delete the first </div> and add </div> to the last row
-  blanksRemoved[0] = blanksRemoved[0].replace(/<\/div>/g, '');
+  // add <div> to the first line & add </div> to the last line
+  blanksRemoved[0] = '\n<div class="grw-togetter">\n'.concat(blanksRemoved[0]);
   blanksRemoved.push('</div>');
   // Add 2 spaces and 1 enter to all lines
   const completedArray = blanksRemoved.map(line => line.concat('  \n'));

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

@@ -34,6 +34,7 @@
     "@tsed/swagger": "^6.43.0",
     "@tsed/typeorm": "^6.43.0",
     "axios": "^0.21.1",
+    "bootstrap": "^5.0.2",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "compression": "^1.7.4",

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

@@ -18,6 +18,7 @@ import { createTerminus } from '@godaddy/terminus';
 
 import swaggerSettingsForDev from '~/config/swagger/config.dev';
 import swaggerSettingsForProd from '~/config/swagger/config.prod';
+import './filters/ResourceNotFoundFilter';
 import loggerFactory from '~/utils/logger';
 
 export const rootDir = __dirname;
@@ -102,6 +103,16 @@ const helmetOptions = isProduction ? {} : {
         root: `${rootDir}/public`,
       },
     ],
+    '/css': [
+      {
+        root: `${rootDir}/../node_modules/bootstrap/dist/css`,
+      },
+    ],
+    '/js': [
+      {
+        root: `${rootDir}/../node_modules/bootstrap/dist/js`,
+      },
+    ],
   },
 })
 export class Server {

+ 20 - 0
packages/slackbot-proxy/src/controllers/term.ts

@@ -0,0 +1,20 @@
+import { Controller, PlatformRouter } from '@tsed/common';
+import { Request, Response } from 'express';
+
+const isOfficialMode = process.env.OFFICIAL_MODE === 'true';
+
+@Controller('/term')
+export class TermCtrl {
+
+  constructor(router: PlatformRouter) {
+    if (isOfficialMode) {
+      router.get('/', this.getTerm);
+    }
+  }
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  getTerm(req: Request, res: Response): string|void {
+    res.render('term.ejs');
+  }
+
+}

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

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

+ 32 - 24
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,5 +1,5 @@
 import { Inject, Service } from '@tsed/di';
-import { WebClient, LogLevel } from '@slack/web-api';
+import { WebClient, LogLevel, Block } from '@slack/web-api';
 import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
@@ -8,6 +8,7 @@ import { Installation } from '~/entities/installation';
 import { InvalidUrlError } from '../models/errors';
 
 const isProduction = process.env.NODE_ENV === 'production';
+const isOfficialMode = process.env.OFFICIAL_MODE === 'true';
 
 @Service()
 export class RegisterService implements GrowiCommandProcessor {
@@ -47,6 +48,18 @@ export class RegisterService implements GrowiCommandProcessor {
     });
   }
 
+  async replyToSlack(client: WebClient, channel: string, user: string, text: string, blocks: Array<Block>): Promise<void> {
+    await client.chat.postEphemeral({
+      channel,
+      user,
+      // Recommended including 'text' to provide a fallback when using blocks
+      // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
+      text,
+      blocks,
+    });
+    return;
+  }
+
   async insertOrderRecord(
       installation: Installation | undefined,
       // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -67,18 +80,10 @@ export class RegisterService implements GrowiCommandProcessor {
     }
     catch (error) {
       const invalidErrorMsg = 'Please enter a valid URL';
-
-      await client.chat.postEphemeral({
-        channel,
-        user: payload.user.id,
-        // Recommended including 'text' to provide a fallback when using blocks
-        // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
-        text: 'Invalid URL',
-        blocks: [
-          generateMarkdownSectionBlock(invalidErrorMsg),
-        ],
-      });
-
+      const blocks = [
+        generateMarkdownSectionBlock(invalidErrorMsg),
+      ];
+      await this.replyToSlack(client, channel, payload.user.id, 'Invalid URL', blocks);
       throw new InvalidUrlError(growiUrl);
     }
 
@@ -98,17 +103,20 @@ export class RegisterService implements GrowiCommandProcessor {
 
     const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
 
-    await client.chat.postEphemeral({
-      channel,
-      user: payload.user.id,
-      // Recommended including 'text' to provide a fallback when using blocks
-      // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
-      text: 'Proxy URL',
-      blocks: [
-        generateMarkdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
-        generateMarkdownSectionBlock(`Proxy URL: ${serverUri}`),
-      ],
-    });
+    if (isOfficialMode) {
+      const blocks = [
+        generateMarkdownSectionBlock('Successfully registered with the proxy! Please check test connection in your GROWI'),
+      ];
+      await this.replyToSlack(client, channel, payload.user.id, 'Proxy URL', blocks);
+      return;
+
+    }
+
+    const blocks = [
+      generateMarkdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
+      generateMarkdownSectionBlock(`Proxy URL: ${serverUri}`),
+    ];
+    await this.replyToSlack(client, channel, payload.user.id, 'Proxy URL', blocks);
     return;
   }
 

+ 9 - 0
packages/slackbot-proxy/src/views/commons/head.ejs

@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <meta name="description" content="GROWI is a wiki that can be written in Markdown and easy to share information with everyone.">
+  <link rel="shortcut icon" href="/images/growi-bot.png">
+  <title>GROWI Official Bot</title>
+  <link href="./css/bootstrap.min.css" rel="stylesheet" >
+</html>

+ 1 - 3
packages/slackbot-proxy/src/views/privacy.ejs

@@ -1,6 +1,4 @@
-<head>
-  <meta name="viewport" content="width=device-width,initial-scale=1">
-</head>
+<%- include('commons/head'); %>
 
 <body style="max-width: 600px; padding-top:100px; margin: 0 auto;">
   <h1 style="text-align:center;">Privacy Policy</h1>

+ 79 - 0
packages/slackbot-proxy/src/views/term.ejs

@@ -0,0 +1,79 @@
+<%- include('commons/head'); %>
+
+<body class="pt-5 m-3 w-50 mx-auto d-flex flex-column">
+  <h1 class="text-center">Terms of Service </h1>
+    <h2 class="text-center">At First</h2>
+      <p>This Terms of Use Agreement(hereinafter referred to as the "Agreement") stipulates the terms and conditions of use for the services provided by WESEEK,Inc. on GROWI Slack-Bot(hereinafter referred to as the "Services"). All registered users(hereinafter referred to as "Users") are required to follow these Terms of Service.</p>
+
+  <h2 class="text-center">Application of Terms</h2>
+    <p>This Agreement shall apply to all relationships between the User and WESEEK,Inc. regarding the use of the Service.  In addition to this Agreement, WESEEK,Inc. may make various provisions regarding the Service, such as rules for use (hereinafter referred to as "Individual Provisions"). These individual regulations may be called by any name. These Individual Regulations, regardless of their names, shall constitute a part of these Terms. In the event that the provisions of these Terms of Use conflict with the provisions of the Individual Provisions of the preceding article, the provisions of the Individual Provisions shall take precedence unless otherwise specified in the Individual Provisions.</p>
+
+  <h2 class="text-center">User Registration</h2>
+   <p>Registration for this service shall be completed when the applicant agrees to these Terms of Use, applies for registration in accordance with the method specified by WESEEK,Inc. approves the application. If WESEEK,Inc. determines that the applicant has any of the following reasons, WESEEK,Inc. may not approve the application for registration, and WESEEK,Inc. shall not be obligated to disclose any of the reasons.</p>
+    <ul>
+      <li>If the applicant has provided false information in the application for registration.</li>
+      <li>If the application is from a person who has violated this agreement.</li>
+      <li>In addition, when WESEEK,Inc. deems that the registration is not appropriate.</li>
+    </ul>
+
+  <h2 class="text-center">Account</h2>
+    <p>Users are responsible for maintaining the privacy and security of their accounts. We will not be liable for any damage or loss caused by your failure to protect your login information, including your password.</p>
+
+  <h2 class="text-center">Prohibited Matters</h2>
+    <p>Users shall not commit any of the following acts when using the Service.</p>
+    <ul>
+      <li>Acts that violate laws or public order and morals</li>
+      <li>Acts related to criminal acts</li>
+      <li>Acts that destroy or interfere with the functions of the servers or networks of WESEEK,Inc., other users, or other third parties.</li>
+      <li>Acts that may interfere with the operation of WESEEK,Inc.'s services.</li>
+      <li>Unauthorized access or attempts to do so.</li>
+      <li>Acts that collect or accumulate personal information about other users.</li>
+      <li>Acts of using this service for illegal purposes.</li>
+      <li>Actions that cause disadvantage, damage, or discomfort to other users of this service or other third parties.</li>
+      <li>Act of impersonating other users.</li>
+      <li>Advertising, solicitation, or business activities on this service that are not authorized by WESEEK,Inc..</li>
+      <li>Directly or indirectly providing benefits to antisocial forces in relation to WESEEK,Inc.'s services.</li>
+      <li>Any other acts that WESEEK,Inc. deems inappropriate.</li>
+    </ul>
+
+  <h2 class="text-center">Suspension of Provision of the Service, etc.</h2>
+    <p>WESEEK,Inc. may suspend or interrupt the provision of all or part of the Service without prior notice to the User if WESEEK,Inc. deems any of the following to be the case When performing maintenance, inspection, or updating of the computer system for this service</p>
+    <ul>
+      <li>In the event that the provision of the Service becomes difficult due to force majeure such as earthquakes, lightning, fire, power outages, or natural disasters.</li>
+      <li>When a computer or communication line is stopped due to an accident</li>
+      <li>In any other cases where WESEEK,Inc. deems it difficult to provide the Service.</li>
+    </ul>
+    <p>WESEEK,Inc. shall not be liable for any disadvantage or damage incurred by the User or any third party due to the suspension or interruption of the provision of the Service.</p>
+
+  <h2 class="text-center">Restriction of Use and Cancellation of Registration</h2>
+    <p>In the event that a User falls under any of the following, WESEEK,Inc. may, without prior notice, restrict the User from using all or part of the Service, or cancel the User's registration.</P>
+    <ul>
+      <li>If the user violates any of the provisions of this agreement.</li>
+      <li>When it is found that there is a false fact in the registered information.</li>
+      <li>When there is no response to communication from WESEEK,Inc. for a certain period of time.</li>
+      <li>When there is no use of this service for a certain period of time after the last use.</li>
+      <li>In any other cases where WESEEK,Inc. deems the use of the Service to be inappropriate.</li>
+    </ul>
+    <p>WESEEK,Inc. shall not be liable for any damages incurred by the User as a result of any action taken by WESEEK,Inc. in accordance with this Article.</p>
+
+  <h2 class="text-center">No Warranty and Disclaimer</h2>
+    <p>WESEEK,Inc does not warrant, expressly or impliedly, that the Service is free from defects in fact or in law (including defects in safety, reliability, accuracy, completeness, effectiveness, fitness for a particular purpose, security, errors or bugs, infringement of rights, etc.).
+      WESEEK,Inc shall not be liable for any damages incurred by the User due to the Service. WESEEK,Inc shall not be liable for any transactions, communications, or disputes that may arise between the User and other users or third parties in relation to the Service.</p>
+  <h2 class="text-center">Modification of Service Contents, etc.</h2>
+    <p>WESEEK,Inc. may change the contents of the Service or discontinue the provision of the Service without notice to the User, and shall not be liable for any damages incurred by the User as a result of such changes.</p>
+
+  <h2 class="text-center">Modification of the Terms of Service, etx.</h2>
+    <p>WESEEK,Inc. may change the Terms of Use at any time without notice to the User, if WESEEK,Inc. deems it necessary. In the event that a User begins to use the Service after a change to the Terms, the User shall be deemed to have agreed to the changed Terms.</p>
+
+  <h2 class="text-center">Handling of Personal Information</h2>
+    <p>WESEEK,Inc. shall handle personal information obtained through the use of the Service in an appropriate manner in accordance with WESEEK,Inc.'s "Privacy Policy".</p>
+
+  <h2 class="text-center">Notification or Communication</h2>
+    <p>Notification or communication between the User and WESEEK,Inc. shall be conducted in a manner determined by WESEEK,Inc.. Unless the User notifies WESEEK,Inc. of a change in the method specified separately by WESEEK,Inc., WESEEK,Inc. will assume that the currently registered contact information is valid and send notifications or communications to said contact information, and these notifications or communications will be deemed to have reached the User at the time they are sent.</p>
+
+  <h2 class="text-center">Prohibition of Transfer of Rights and Obligations</h2>
+    <p>The User may not assign or pledge to a third party his/her position in the User Agreement or rights or obligations under this Agreement without the prior written consent of WESEEK,Inc..</p>
+
+  <h2 class="text-center">Governing Law</h2>
+    <p>These Terms of Use shall be governed by and construed in accordance with the laws of Japan. In the event of a dispute regarding the Service, the court having jurisdiction over the location of the head office of WESEEK,Inc. shall have exclusive jurisdiction.</p>
+</body>

+ 42 - 17
packages/slackbot-proxy/src/views/top.ejs

@@ -1,20 +1,45 @@
-<head>
-  <meta name="viewport" content="width=device-width,initial-scale=1">
-</head>
+<%- include('commons/head'); %>
 
-<body style="padding-top:100px; text-align:center;">
-  <h1 >GROWI Bot</h1>
-  <div>
-    <img height="300" width="300" alt="GROWi Bot" src="/images/growi-bot.png" />
-  </div>
-  <div style="display:flex; justify-content: space-around; max-width: 500px; margin:30px auto;">
-    <a href=<%- url %>>
-      <img alt="Add to Slack" height="40" width="139" src="/images/add-to-slack.png"/>
-    </a>
-    <% if (isOfficialMode) { %>
-      <a href="/privacy">
-        Privacy Policy
-      </a>
-    <% } %>
+<body class="pt-3">
+  <div class="container border p-5">
+    <div class="text-center">
+      <h1>GROWI Bot</h1>
+      <h4>GROWI is a wiki that can be written in Markdown and easy to share information with everyone.</h4>
+
+      <div>
+        <img height="300" width="300" alt="GROWi Bot" src="/images/growi-bot.png" />
+      </div>
+      <div class="m-3">
+        <a href=<%- url %>>
+          <img alt="Add to Slack" height="40" width="139" src="/images/add-to-slack.png"/>
+        </a>
+      </div>
+      <div class="d-flex justify-content-evenly my-3">
+        <% if (isOfficialMode) { %>
+          <a href="/privacy">
+            Privacy Policy
+          </a>
+          <a href="/term">
+            Term of Service
+          </a>
+        <% } %>
+      </div>
+    </div>
+    <div class="m-3">
+      <div class="d-flex flex-column align-items-center justify-content-center">
+          <ul>
+            With GROWI for Slack, you can:
+            <ul>
+              <li>Search pages for multiple GROWI apps.</li>
+              <li>Create pages easily.</li>
+            </ul>
+            </li>
+          </ul>
+        <div class="mt-3">
+          GROWI is open-source software developed by WESEEK, Inc and we are looking for contributors who can work with us.<br>
+          Please <a href="https://growi-slackin.weseek.co.jp/">join</a> Slack and feel free to talk to WESEEK members!
+        </div>
+      </div>
+    </div>
   </div>
 </body>

+ 4 - 0
resource/Contributor.js

@@ -43,6 +43,9 @@ const contributors = [
           { name: 'oshikishintaro' },
           { name: 'makotoshiraishi' },
           { name: 'yamagai' },
+          { name: 'stevenfukase' },
+          { name: 'miya' },
+          { name: 'kaho819' },
         ],
       },
     ],
@@ -109,6 +112,7 @@ const contributors = [
           { position: 'Flatt Security', name: 'Azara/Norihide Saito' },
           { position: 'CyberAgent, Inc.', name: 'Daisuke Takahashi' },
           { position: 'Mitsui Bussan Secure Directions, Inc.', name: 'Yuji Tounai' },
+          { name: 'yy0931' },
         ],
       },
     ],

+ 2 - 2
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -28,8 +28,8 @@ const AdminNavigation = (props) => {
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
       case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('External_Notification')}</>;
-      case 'slack-integration-legacy': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
-      case 'slack-integration':        return <><i className="fa fa-slack mr-2"></i>             { t('slack_integration') }</>;
+      case 'slack-integration':        return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration') }</>;
+      case 'slack-integration-legacy': return <><i className="icon-fw icon-shuffle"></i>         { t('Legacy_Slack_Integration')}</>;
       case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
       case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
       case 'search':                   return <><i className="icon-fw icon-magnifier"></i>       { t('Full Text Search Management') }</>;

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

@@ -36,9 +36,7 @@ const CustomBotWithProxyConnectionStatus = (props) => {
       </div>
 
       <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
-        <h5 className="card-title font-weight-bold mt-3 ml-3">GROWI App
-          <a className="icon-fw fa fa-repeat float-md-right ml-2 mr-sm-3 fa-sm-lg"></a>
-        </h5>
+        <h5 className="card-title font-weight-bold mt-3 ml-3">GROWI App</h5>
         <div className="card-body text-center">
           <div className="mx-md-3 my-4 my-lg-5 p-2 border bg-primary text-light">
             {siteName}

+ 13 - 11
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -66,8 +66,8 @@ const CustomBotWithProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
-        {/* TODO: add an appropriate links by GW-5614 */}
-        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+        {/* 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> */}
       </h2>
 
       {slackAppIntegrations.length !== 0 && (
@@ -127,15 +127,17 @@ const CustomBotWithProxySettings = (props) => {
             </React.Fragment>
           );
         })}
-        <div className="row justify-content-center my-5">
-          <button
-            type="button"
-            className="btn btn-outline-primary"
-            onClick={addSlackAppIntegrationHandler}
-          >
-            {`+ ${t('admin:slack_integration.accordion.add_slack_workspace')}`}
-          </button>
-        </div>
+        {slackAppIntegrations.length < 10 && (
+          <div className="row justify-content-center my-5">
+            <button
+              type="button"
+              className="btn btn-outline-primary"
+              onClick={addSlackAppIntegrationHandler}
+            >
+              {`+ ${t('admin:slack_integration.accordion.add_slack_workspace')}`}
+            </button>
+          </div>
+        )}
       </div>
       <DeleteSlackBotSettingsModal
         isResetAll={false}

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

@@ -21,8 +21,8 @@ const CustomBotWithoutProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
-        {/* TODO: add an appropriate links by GW-5614 */}
-        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+        {/* 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> */}
       </h2>
 
       <CustomBotWithoutProxyConnectionStatus

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

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

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

@@ -13,19 +13,12 @@ const logger = loggerFactory('growi:SlackBotSettings');
 
 const OfficialBotSettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
+    appContainer, slackAppIntegrations, onClickAddSlackWorkspaceBtn, connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const { t } = useTranslation();
 
-  const [newProxyServerUri, setNewProxyServerUri] = useState();
-
-  // componentDidUpdate
-  useEffect(() => {
-    setNewProxyServerUri(proxyServerUri);
-  }, [proxyServerUri, slackAppIntegrations]);
-
   const addSlackAppIntegrationHandler = async() => {
     if (onClickAddSlackWorkspaceBtn != null) {
       onClickAddSlackWorkspaceBtn();
@@ -46,19 +39,6 @@ const OfficialBotSettings = (props) => {
     }
   };
 
-  const updateProxyUri = async() => {
-    try {
-      await appContainer.apiv3.put('/slack-integration-settings/proxy-uri', {
-        proxyUri: newProxyServerUri,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('Proxy URL') }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  };
-
   useEffect(() => {
     const siteName = appContainer.config.crowi.title;
     setSiteName(siteName);
@@ -67,8 +47,8 @@ const OfficialBotSettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
-        {/* TODO: add an appropriate links by GW-5614 */}
-        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+        {/* 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> */}
       </h2>
 
       {slackAppIntegrations.length !== 0 && (
@@ -78,22 +58,6 @@ const OfficialBotSettings = (props) => {
             connectionStatuses={connectionStatuses}
           />
 
-          <div className="form-group row my-4">
-            <label className="text-left text-md-right col-md-3 col-form-label mt-3">Proxy URL</label>
-            <div className="col-md-6 mt-3">
-              <input
-                className="form-control"
-                type="text"
-                name="settingForm[proxyUrl]"
-                defaultValue={newProxyServerUri}
-                onChange={(e) => { setNewProxyServerUri(e.target.value) }}
-              />
-            </div>
-            <div className="col-md-2 mt-3 text-center text-md-left">
-              <button type="button" className="btn btn-primary" onClick={updateProxyUri}>{ t('Update') }</button>
-            </div>
-          </div>
-
           <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
         </>
       )}
@@ -159,7 +123,6 @@ OfficialBotSettings.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   slackAppIntegrations: PropTypes.array,
-  proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
   onDeleteSlackAppIntegration: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,

+ 4 - 4
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -15,6 +15,7 @@ import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 const botTypes = ['officialBot', 'customBotWithoutProxy', 'customBotWithProxy'];
 
 const SlackIntegration = (props) => {
+
   const { appContainer } = props;
   const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
@@ -124,7 +125,6 @@ const SlackIntegration = (props) => {
       settingsComponent = (
         <OfficialBotSettings
           slackAppIntegrations={slackAppIntegrations}
-          proxyServerUri={proxyServerUri}
           onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
           onDeleteSlackAppIntegration={fetchSlackIntegrationData}
           connectionStatuses={connectionStatuses}
@@ -187,11 +187,11 @@ const SlackIntegration = (props) => {
       <div className="selecting-bot-type mb-5">
         <h2 className="admin-setting-header mb-4">
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
-          {/* TODO: add an appropriate link by GW-5614 */}
-          <a className="ml-2 btn-link" href="#">
+          {/* 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="#">
             {t('admin:slack_integration.selecting_bot_types.detailed_explanation')}
             <i className="fa fa-external-link ml-1" aria-hidden="true"></i>
-          </a>
+          </a> */}
         </h2>
 
         <div className="d-flex justify-content-end">

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

@@ -22,15 +22,15 @@ const BotCreateProcess = () => {
         {t('admin:slack_integration.accordion.create_bot')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
-      {/* TODO: Insert DOCS link */}
-      <a href="#">
+      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
+      {/* <a href="#">
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
         </p>
-      </a>
+      </a> */}
     </div>
   );
 };
@@ -43,15 +43,15 @@ const BotInstallProcessForOfficialBot = () => {
         {t('admin:slack_integration.accordion.install_now')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
-      {/* TODO: Insert DOCS link */}
-      <a href="#">
+      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
+      {/* <a href="#">
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_install')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
         </p>
-      </a>
+      </a> */}
     </div>
   );
 };
@@ -169,8 +169,8 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
                 // eslint-disable-next-line react/no-danger
               dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.paste_growi_url') }}
             />
-            <div className="input-group align-items-center ml-2 mb-3">
-              <div className="input-group-prepend mx-1">
+            <div className="input-group align-items-center pl-2 mb-3">
+              <div className="input-group-prepend w-75">
                 <input className="form-control" type="text" value={props.growiUrl} readOnly />
                 <CopyToClipboard text={props.growiUrl} onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
                   <div className="btn input-group-text">
@@ -303,10 +303,6 @@ const WithProxyAccordions = (props) => {
       />,
     },
     '③': {
-      title: 'set_proxy_url_on_growi',
-      content: <RegisteringProxyUrlProcess />,
-    },
-    '④': {
       title: 'test_connection',
       content: <TestProcess
         apiv3Post={props.appContainer.apiv3.post}

+ 2 - 4
src/client/js/components/Page/RevisionRenderer.jsx

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
-import PageContainer from '../../services/PageContainer';
 import NavigationContainer from '../../services/NavigationContainer';
 import GrowiRenderer from '../../util/GrowiRenderer';
 
@@ -25,7 +24,7 @@ class RevisionRenderer extends React.PureComponent {
   initCurrentRenderingContext() {
     this.currentRenderingContext = {
       markdown: this.props.markdown,
-      currentPagePath: this.props.pageContainer.state.path,
+      currentPagePath: decodeURIComponent(window.location.pathname),
     };
   }
 
@@ -121,11 +120,10 @@ class RevisionRenderer extends React.PureComponent {
 /**
  * Wrapper component for using unstated
  */
-const RevisionRendererWrapper = withUnstatedContainers(RevisionRenderer, [AppContainer, PageContainer, NavigationContainer]);
+const RevisionRendererWrapper = withUnstatedContainers(RevisionRenderer, [AppContainer, NavigationContainer]);
 
 RevisionRenderer.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   markdown: PropTypes.string.isRequired,

+ 33 - 17
src/client/js/components/PageCreateModal.jsx

@@ -6,14 +6,16 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
-import urljoin from 'url-join';
 
-import { userPageRoot, isCreatablePage } from '@commons/util/path-utils';
+import {
+  userPageRoot, isCreatablePage, generateEditorPath,
+} from '@commons/util/path-utils';
 import { pathUtils } from 'growi-commons';
 
 import AppContainer from '../services/AppContainer';
 import NavigationContainer from '../services/NavigationContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
+import { toastError } from '../util/apiNotification';
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
@@ -70,6 +72,20 @@ const PageCreateModal = (props) => {
     setTemplate(value);
   }
 
+  /**
+   * join path, check if creatable, then redirect
+   * @param {string} paths
+   */
+  async function redirectToEditor(...paths) {
+    try {
+      const editorPath = await generateEditorPath(...paths);
+      window.location.href = editorPath;
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
   /**
    * access today page
    */
@@ -78,14 +94,14 @@ const PageCreateModal = (props) => {
     if (tmpTodayInput1 === '') {
       tmpTodayInput1 = t('Memo');
     }
-    window.location.href = encodeURI(urljoin(userPageRootPath, tmpTodayInput1, now, todayInput2, '#edit'));
+    redirectToEditor(userPageRootPath, tmpTodayInput1, now, todayInput2);
   }
 
   /**
    * access input page
    */
   function createInputPage() {
-    window.location.href = encodeURI(urljoin(pageNameInput, '#edit'));
+    redirectToEditor(pageNameInput);
   }
 
   function ppacInputChangeHandler(value) {
@@ -101,14 +117,14 @@ const PageCreateModal = (props) => {
    */
   function createTemplatePage(e) {
     const pageName = (template === 'children') ? '_template' : '__template';
-    window.location.href = encodeURI(urljoin(pathname, pageName, '#edit'));
+    redirectToEditor(pathname, pageName);
   }
 
   function renderCreateTodayForm() {
     return (
       <div className="row">
         <fieldset className="col-12 mb-4">
-          <h3 className="grw-modal-head pb-2">{ t("Create today's") }</h3>
+          <h3 className="grw-modal-head pb-2">{t("Create today's")}</h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
 
@@ -139,7 +155,7 @@ const PageCreateModal = (props) => {
 
             <div className="d-flex justify-content-end mt-1 mt-sm-0">
               <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createTodayPage}>
-                <i className="icon-fw icon-doc"></i>{ t('Create') }
+                <i className="icon-fw icon-doc"></i>{t('Create')}
               </button>
             </div>
 
@@ -154,7 +170,7 @@ const PageCreateModal = (props) => {
     return (
       <div className="row">
         <fieldset className="col-12 mb-4">
-          <h3 className="grw-modal-head pb-2">{ t('Create under') }</h3>
+          <h3 className="grw-modal-head pb-2">{t('Create under')}</h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
 
@@ -185,7 +201,7 @@ const PageCreateModal = (props) => {
 
             <div className="d-flex justify-content-end mt-1 mt-sm-0">
               <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createInputPage}>
-                <i className="icon-fw icon-doc"></i>{ t('Create') }
+                <i className="icon-fw icon-doc"></i>{t('Create')}
               </button>
             </div>
 
@@ -202,7 +218,7 @@ const PageCreateModal = (props) => {
         <fieldset className="col-12">
 
           <h3 className="grw-modal-head pb-2">
-            { t('template.modal_label.Create template under')}<br />
+            {t('template.modal_label.Create template under')}<br />
             <code className="h6">{pathname}</code>
           </h3>
 
@@ -210,18 +226,18 @@ const PageCreateModal = (props) => {
 
             <div id="dd-template-type" className="dropdown flex-fill">
               <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle w-100" data-toggle="dropdown">
-                {template == null && t('template.option_label.select') }
+                {template == null && t('template.option_label.select')}
                 {template === 'children' && t('template.children.label')}
                 {template === 'decendants' && t('template.decendants.label')}
               </button>
               <div className="dropdown-menu" aria-labelledby="userMenu">
                 <button className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
-                  { t('template.children.label') } (_template)<br className="d-block d-md-none" />
-                  <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
+                  {t('template.children.label')} (_template)<br className="d-block d-md-none" />
+                  <small className="text-muted text-wrap">- {t('template.children.desc')}</small>
                 </button>
                 <button className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
-                  { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
-                  <small className="text-muted">- { t('template.decendants.desc') }</small>
+                  {t('template.decendants.label')} (__template) <br className="d-block d-md-none" />
+                  <small className="text-muted">- {t('template.decendants.desc')}</small>
                 </button>
               </div>
             </div>
@@ -232,7 +248,7 @@ const PageCreateModal = (props) => {
                 className={`grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3 ${template == null && 'disabled'}`}
                 onClick={createTemplatePage}
               >
-                <i className="icon-fw icon-doc"></i>{ t('Edit') }
+                <i className="icon-fw icon-doc"></i>{t('Edit')}
               </button>
             </div>
 
@@ -252,7 +268,7 @@ const PageCreateModal = (props) => {
       autoFocus={false}
     >
       <ModalHeader tag="h4" toggle={navigationContainer.closePageCreateModal} className="bg-primary text-light">
-        { t('New Page') }
+        {t('New Page')}
       </ModalHeader>
       <ModalBody>
         {renderCreateTodayForm()}

+ 2 - 0
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -47,6 +47,7 @@ require('codemirror/addon/search/searchcursor');
 require('codemirror/addon/search/match-highlighter');
 require('codemirror/addon/selection/active-line');
 require('codemirror/addon/scroll/annotatescrollbar');
+require('codemirror/addon/scroll/scrollpastend');
 require('codemirror/addon/fold/foldcode');
 require('codemirror/addon/fold/foldgutter');
 require('codemirror/addon/fold/foldgutter.css');
@@ -845,6 +846,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             tabSize: 4,
             indentUnit: this.props.indentSize,
             lineWrapping: true,
+            scrollPastEnd: true,
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoCloseTags: true,
             placeholder,

+ 7 - 5
src/client/js/components/PageEditor/LinkEditModal.jsx

@@ -152,8 +152,8 @@ class LinkEditModal extends React.PureComponent {
     const { t } = this.props;
     const path = this.state.linkInputValue;
     let markdown = '';
-    let previewError = '';
     let permalink = '';
+    let previewError = '';
 
     if (path.startsWith('/')) {
       const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
@@ -163,8 +163,7 @@ class LinkEditModal extends React.PureComponent {
       try {
         const { page } = await this.props.appContainer.apiGet('/pages.get', { path: pathWithoutFragment, page_id: pageId });
         markdown = page.revision.body;
-        // create permanent link only if path isn't permanent link because checkbox for isUsePermanentLink is disabled when permalink is ''.
-        permalink = !isPermanentLink ? `${window.location.origin}/${page.id}` : '';
+        permalink = page.id;
       }
       catch (err) {
         previewError = err.message;
@@ -217,7 +216,8 @@ class LinkEditModal extends React.PureComponent {
   handleChangeTypeahead(selected) {
     const page = selected[0];
     if (page != null) {
-      this.setState({ linkInputValue: page.path });
+      const permalink = `${window.location.origin}/${page.id}`;
+      this.setState({ linkInputValue: page.path, permalink });
     }
   }
 
@@ -230,7 +230,9 @@ class LinkEditModal extends React.PureComponent {
     if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) {
       isUseRelativePath = false;
     }
-    this.setState({ linkInputValue: link, isUseRelativePath, isUsePermanentLink: false });
+    this.setState({
+      linkInputValue: link, isUseRelativePath, isUsePermanentLink: false, permalink: '',
+    });
   }
 
   handleSelecteLinkerType(linkerType) {

+ 1 - 1
src/client/js/components/PagePathAutoComplete.jsx

@@ -41,7 +41,7 @@ const PagePathAutoComplete = (props) => {
       onChange={inputChangeHandler}
       onInputChange={props.onInputChange}
       inputName="new_path"
-      emptyLabelExceptError={null}
+      behaviorOfResetBtn="clear"
       placeholder="Input page path"
       keywordOnInit={getKeywordOnInit(initializedPath)}
       autoFocus={props.autoFocus}

+ 0 - 21
src/client/styles/scss/_on-edit.scss

@@ -187,17 +187,6 @@ body.on-edit {
     .page-editor-editor-container {
       border-right: 1px solid transparent;
 
-      // override CodeMirror styles
-      .CodeMirror {
-        .cm-matchhighlight {
-          background-color: cyan;
-        }
-
-        .CodeMirror-selection-highlight-scrollbar {
-          background-color: darkcyan;
-        }
-      }
-
       // add icon on cursor
       .markdown-table-activated,
       .markdown-link-activated {
@@ -313,11 +302,6 @@ body.on-edit {
   }
 }
 
-// overwrite .CodeMirror pre
-.CodeMirror pre.CodeMirror-line {
-  font-family: $font-family-monospace;
-}
-
 // overwrite .CodeMirror-hints
 .CodeMirror-hints {
   max-height: 30em !important;
@@ -342,11 +326,6 @@ body.on-edit {
   }
 }
 
-// overwrite .CodeMirror-placeholder
-.CodeMirror pre.CodeMirror-placeholder {
-  color: $text-muted;
-}
-
 #tag-edit-button-tooltip {
   .tooltip-inner {
     color: black;

+ 36 - 0
src/client/styles/scss/_override-codemirror.scss

@@ -0,0 +1,36 @@
+.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;
+  }
+
+  .cm-matchhighlight {
+    color: $gray-900;
+    background-color: cyan;
+  }
+
+  .CodeMirror-selection-highlight-scrollbar {
+    background-color: darkcyan;
+  }
+
+  // overwrite .CodeMirror-placeholder
+  pre.CodeMirror-placeholder {
+    color: $text-muted;
+  }
+
+  // overwrite .CodeMirror pre
+  pre.CodeMirror-line {
+    font-family: $font-family-monospace;
+  }
+}

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

@@ -218,6 +218,14 @@ div.body {
       }
     }
   }
+
+  .grw-togetter {
+    padding: 7%;
+    padding-bottom: 3%;
+    margin: 0 7%;
+    background-color: rgba(200, 200, 200, 0.2);
+    border-radius: 10px;
+  }
 }
 
 // mobile

+ 3 - 0
src/client/styles/scss/style-app.scss

@@ -10,6 +10,9 @@
 // override bootstrap
 @import 'override-bootstrap';
 
+// override codemirror
+@import 'override-codemirror';
+
 // override react-bootstrap-typeahead styles
 @import 'override-rbt';
 

+ 23 - 1
src/lib/util/path-utils.js

@@ -38,7 +38,7 @@ const isUserPage = (path) => {
 };
 
 const forbiddenPages = [
-  /\^|\$|\*|\+|#|%/,
+  /\^|\$|\*|\+|#|%|\?/,
   /^\/-\/.*/,
   /^\/_r\/.*/,
   /^\/_apix?(\/.*)?/,
@@ -111,6 +111,27 @@ function encodeSpaces(path) {
   return path.replace(/ /g, '%20').replace(/\u3000/g, '%E3%80%80');
 }
 
+/**
+ * Generate editor path
+ * @param {string} paths
+ * @returns {string}
+ */
+function generateEditorPath(...paths) {
+  const joinedPath = [...paths].join('/');
+
+  if (!isCreatablePage(joinedPath)) {
+    throw new Error('Invalid characters on path');
+  }
+
+  try {
+    const url = new URL(joinedPath, 'https://dummy');
+    return `${url.pathname}#edit`;
+  }
+  catch (err) {
+    throw new Error('Invalid path format');
+  }
+}
+
 module.exports = {
   isTopPage,
   isTrashPage,
@@ -119,4 +140,5 @@ module.exports = {
   userPageRoot,
   convertToNewAffiliationPath,
   encodeSpaces,
+  generateEditorPath,
 };

+ 5 - 3
src/server/models/slack-app-integration.js

@@ -5,14 +5,16 @@ const schema = new mongoose.Schema({
   tokenGtoP: { type: String, required: true, unique: true },
   tokenPtoG: { type: String, required: true, unique: true },
 });
+
 class SlackAppIntegration {
 
   static generateAccessTokens() {
+    const now = new Date().getTime();
     const hasher1 = crypto.createHash('sha512');
     const hasher2 = crypto.createHash('sha512');
-    const tokenGtoP = hasher1.update(new Date().getTime().toString() + process.env.SALT_FOR_GTOP_TOKEN);
-    const tokenPtoG = hasher2.update(new Date().getTime().toString() + process.env.SALT_FOR_PTOG_TOKEN);
-    return [tokenGtoP.digest('base64'), tokenPtoG.digest('base64')];
+    const tokenGtoP = hasher1.update(`gtop${now.toString()}${process.env.SALT_FOR_GTOP_TOKEN}`).digest('base64');
+    const tokenPtoG = hasher2.update(`ptog${now.toString()}${process.env.SALT_FOR_PTOG_TOKEN}`).digest('base64');
+    return [tokenGtoP, tokenPtoG];
   }
 
   static async generateUniqueAccessTokens() {

+ 0 - 7
src/server/models/user.js

@@ -335,13 +335,6 @@ module.exports = function(crowi) {
     return this.save();
   };
 
-  userSchema.methods.updateGoogleId = function(googleId, callback) {
-    this.googleId = googleId;
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
-  };
-
   userSchema.statics.getUserStatusLabels = function() {
     const userStatus = {};
     userStatus[STATUS_REGISTERED] = 'Approval Pending';

+ 1 - 1
src/server/plugins/plugin-utils.js

@@ -57,7 +57,7 @@ class PluginUtils {
    * @memberOf PluginService
    */
   listPlugins(rootDir) {
-    const packagePath = path.join(rootDir, 'package.json');
+    const packagePath = path.join(rootDir, 'packages/app/package.json');
 
     // Make sure package.json exists
     if (!fs.existsSync(packagePath)) {

+ 8 - 1
src/server/routes/apiv3/slack-integration-settings.js

@@ -107,6 +107,9 @@ module.exports = (crowi) => {
 
   async function postRelationTest(token) {
     const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
+    if (proxyUri == null) {
+      throw new Error('Proxy URL is not registered');
+    }
 
     const result = await axios.get(urljoin(proxyUri, '/g2s/relation-test'), {
       headers: {
@@ -273,6 +276,10 @@ module.exports = (crowi) => {
     await resetAllBotSettings();
     const requestParams = { 'slackbot:currentBotType': currentBotType };
 
+    if (currentBotType === 'officialBot') {
+      requestParams['slackbot:proxyServerUri'] = 'https://slackbot-proxy.growi.org';
+    }
+
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackBotService.publishUpdatedMessage();
@@ -422,7 +429,7 @@ module.exports = (crowi) => {
 
     try {
       const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
-      const slackAppTokens = await SlackAppIntegration.findOneAndUpdate({ _id: slackAppIntegrationId }, { tokenGtoP, tokenPtoG });
+      const slackAppTokens = await SlackAppIntegration.findByIdAndUpdate(slackAppIntegrationId, { tokenGtoP, tokenPtoG });
 
       return res.apiv3(slackAppTokens, 200);
     }

+ 67 - 57
yarn.lock

@@ -1123,6 +1123,13 @@
   dependencies:
     regenerator-runtime "^0.12.0"
 
+"@babel/runtime@^7.12.0":
+  version "7.14.6"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
+  integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.12.5":
   version "7.13.17"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec"
@@ -4627,6 +4634,11 @@ bootstrap@^4.5.0:
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.0.tgz#97d9dbcb5a8972f8722c9962483543b907d9b9ec"
   integrity sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==
 
+bootstrap@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.2.tgz#aff23d5e0e03c31255ad437530ee6556e78e728e"
+  integrity sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==
+
 bowser@^1.7.3:
   version "1.9.4"
   resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
@@ -6110,6 +6122,7 @@ cookiejar@2.0.6:
 cookies@0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b"
+  integrity sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=
   dependencies:
     depd "~1.1.1"
     keygrip "~1.0.2"
@@ -7772,7 +7785,7 @@ etag@1.8.1, etag@^1.8.1, etag@~1.8.1:
 
 event-stream@^3.3.2, event-stream@~3.3.0:
   version "3.3.4"
-  resolved "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+  resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
   dependencies:
     duplexer "~0.1.1"
     from "~0"
@@ -8060,7 +8073,7 @@ express@^4.16.1:
 
 express@^4.16.3:
   version "4.16.3"
-  resolved "http://registry.npmjs.org/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
+  resolved "https://registry.npmjs.org/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
   dependencies:
     accepts "~1.3.5"
     array-flatten "1.1.1"
@@ -8356,7 +8369,7 @@ finalhandler@1.1.0:
 
 finalhandler@1.1.1:
   version "1.1.1"
-  resolved "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
+  resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
   dependencies:
     debug "2.6.9"
     encodeurl "~1.0.2"
@@ -9215,24 +9228,24 @@ grapheme-splitter@^1.0.4:
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
-growi-commons@^5.0.2, growi-commons@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-5.0.3.tgz#8ec745dcb2d3d003e3acd7d6fbec85cf0967354b"
-  integrity sha512-lV2jmqxWPiuzuVaetSY1uq7viwgRCNwLMnU78GaYix4jfXNcYzdrfedTlU3ZRiVIjLggSFb87EhYqtM/4RfjmA==
+growi-commons@^5.0.4:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-5.0.4.tgz#1235b7955a3f492803e8c714fef5c4a797e442b7"
+  integrity sha512-K282Pe97SnJgbZWAuMz9pNDTmvmw4JYPf/oYQaPmBsUjaxG4FDwd7+p5UFc5GqZUWcLwXvtJZQZMZEH/xpg+nA==
 
-growi-plugin-attachment-refs@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/growi-plugin-attachment-refs/-/growi-plugin-attachment-refs-2.0.1.tgz#118c4928a6df93afd0c5fab7a7eab569977dcd55"
-  integrity sha512-Kyi5YQoc/5cuan9ldEvnjzh9rYi9hKjKyy75xHn0/4BZZ3aMz9AzzUcln99rqldOGwig3fuobYCCH9YnW/voZw==
+growi-plugin-attachment-refs@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/growi-plugin-attachment-refs/-/growi-plugin-attachment-refs-2.0.2.tgz#341d95e6cf2b91ebcff5f9573518e15fa12ccb46"
+  integrity sha512-7BTH6ERpybPKAEy0+rLylF9Rap1jHod9d0mPF62HXJmycynGlS66hyqpM20mKq5rSKxEWucmbUdtceQjs139+Q==
   dependencies:
-    growi-commons "^5.0.3"
+    growi-commons "^5.0.4"
 
-growi-plugin-lsx@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/growi-plugin-lsx/-/growi-plugin-lsx-4.0.2.tgz#cdf9caf6bb74a50c2eae0a64ea7b413160c32057"
-  integrity sha512-qF3Yx5sbXnf5C9cZshrtd3irPjs4SC5K3CXEhFiQuo6SZN66+uqQ6twuIgMthkTrBw5boHSoevmIiVqfIUsdIw==
+growi-plugin-lsx@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/growi-plugin-lsx/-/growi-plugin-lsx-4.0.3.tgz#feaf5a8687ac009f0f15ee76f7f0cba4efe6a1d3"
+  integrity sha512-x9N50WQRyFL9xY8FJ8ajF3v67eHma5brEASdsxI0t2CSzPlRhqJti3jTrbauevCNb8/na8Woq6B5rr6kPOFwIQ==
   dependencies:
-    growi-commons "^5.0.2"
+    growi-commons "^5.0.4"
     url "^0.11.0"
 
 growi-plugin-pukiwiki-like-linker@^3.1.0:
@@ -9726,9 +9739,10 @@ i18next-browser-languagedetector@^4.0.1:
   dependencies:
     "@babel/runtime" "^7.5.5"
 
-i18next-express-middleware@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-1.4.1.tgz#273c4a490ad688ce246815ce1288689c63fa7de1"
+i18next-express-middleware@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-2.0.0.tgz#e6ab3be8d2db3c715dc084880d100d235b6fd62e"
+  integrity sha512-TGlSkYsQHikggv4mIp5B+CiXsZzwbpHaZgmOkRNGStLOdKHABH5cHr136g2PC1+p2VPMf3y3UoQZ1TfPfVOrgg==
   dependencies:
     cookies "0.7.1"
 
@@ -9743,12 +9757,12 @@ i18next-sprintf-postprocessor@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/i18next-sprintf-postprocessor/-/i18next-sprintf-postprocessor-0.2.2.tgz#2e409f1043579382698b6a2da70cdaa551d67ea4"
 
-i18next@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.0.0.tgz#5418207d7286128e6cfe558e659fa8c60d89794b"
-  integrity sha512-xxNKNOqLdGP/M+/fzzBOhcc9hCAqv6gDhHq0xbYz/Vlz5PlMfr9P1LbBvmk7RkZjYoh/kyM1tnfSl+sJ2VaD0Q==
+i18next@^20.3.2:
+  version "20.3.2"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.2.tgz#5195e76b9e0848a1c198001bf6c7fc72995a55f1"
+  integrity sha512-e8CML2R9Ng2sSQOM80wb/PrM2j8mDm84o/T4Amzn9ArVyNX5/ENWxxAXkRpZdTQNDaxKImF93Wep4mAoozFrKw==
   dependencies:
-    "@babel/runtime" "^7.3.1"
+    "@babel/runtime" "^7.12.0"
 
 iconv-lite@0.4.19, iconv-lite@~0.4.13:
   version "0.4.19"
@@ -11323,8 +11337,9 @@ kareem@2.3.2:
   integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==
 
 keygrip@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
+  integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==
 
 keyv@3.0.0:
   version "3.0.0"
@@ -13209,9 +13224,10 @@ nodemailer-ses-transport@~1.5.0:
   dependencies:
     aws-sdk "^2.2.36"
 
-nodemailer@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.0.0.tgz#d9761128771739dc87c1fdd747f569b7f135cb02"
+nodemailer@^6.6.2:
+  version "6.6.2"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114"
+  integrity sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q==
 
 noop6@^1.0.1:
   version "1.0.8"
@@ -14465,7 +14481,7 @@ pify@^5.0.0:
 
 pikaday@1.5.1:
   version "1.5.1"
-  resolved "http://registry.npmjs.org/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
+  resolved "https://registry.npmjs.org/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
   optionalDependencies:
     moment "2.x"
 
@@ -16675,7 +16691,7 @@ rxjs@^6.6.0:
   dependencies:
     tslib "^1.9.0"
 
-safe-buffer@5.1.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1:
+safe-buffer@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
 
@@ -16684,7 +16700,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
-safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2:
+safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -17991,9 +18007,9 @@ strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
 striptags@>=3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.1.1.tgz#c8c3e7fdd6fb4bb3a32a3b752e5b5e3e38093ebd"
-  integrity sha1-yMPn/db7S7OjKjt1LltePjgJPr0=
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
+  integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
 
 strong-log-transformer@^2.1.0:
   version "2.1.0"
@@ -19423,10 +19439,10 @@ validator@^11.0.0:
   resolved "https://registry.yarnpkg.com/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4"
   integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==
 
-validator@^12.0.0:
-  version "12.0.0"
-  resolved "https://registry.yarnpkg.com/validator/-/validator-12.0.0.tgz#fb33221f5320abe2422cda2f517dc3838064e813"
-  integrity sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng==
+validator@^13.6.0:
+  version "13.6.0"
+  resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059"
+  integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg==
 
 validator@^2.1.0:
   version "2.1.0"
@@ -19907,21 +19923,21 @@ write@1.0.3:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.0.0.tgz#eaa494aded00ac4289d455bac8d84c7c651cef35"
+ws@^6.0.0, ws@~6.1.0:
+  version "6.1.4"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
   dependencies:
     async-limiter "~1.0.0"
 
-ws@^7.0.0:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
-  integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
+ws@^7.0.0, ws@^7.1.2:
+  version "7.4.6"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
+  integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
 
-ws@^7.1.2, ws@^7.3.1:
-  version "7.3.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
-  integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
+ws@^7.4.6:
+  version "7.5.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66"
+  integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==
 
 ws@~3.3.1:
   version "3.3.3"
@@ -19931,12 +19947,6 @@ ws@~3.3.1:
     safe-buffer "~5.1.0"
     ultron "~1.1.0"
 
-ws@~6.1.0:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
-  dependencies:
-    async-limiter "~1.0.0"
-
 x-is-string@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"