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

Merge pull request #1463 from weseek/master

release v3.6.0
Yuki Takei 6 лет назад
Родитель
Сommit
03ac9beef5
54 измененных файлов с 1755 добавлено и 984 удалено
  1. 1 0
      .eslintignore
  2. 46 0
      .github/workflows/build-rc.yml
  3. 74 0
      .github/workflows/build.yml
  4. 164 0
      .github/workflows/ci.yml
  5. 37 0
      .github/workflows/release.yml
  6. 2 1
      .stylelintrc.json
  7. 10 0
      CHANGES.md
  8. 32 85
      README.md
  9. 6 0
      bin/github-actions/bump-version.sh
  10. 0 9
      bin/wercker/init-git.sh
  11. 0 40
      bin/wercker/trigger-growi-docker.sh
  12. 0 28
      bin/wercker/trigger-growi-docs.sh
  13. 18 8
      config/migrate.js
  14. 117 0
      docker/Dockerfile
  15. 14 0
      docker/Dockerfile.dockerignore
  16. 142 0
      docker/README.md
  17. 10 0
      docker/bin/remove-except-artifacts.sh
  18. 14 0
      docker/docker-entrypoint.sh
  19. 5 0
      docker/nocdn/env.prod.js
  20. 4 4
      package.json
  21. 6 3
      resource/locales/en-US/translation.json
  22. 4 2
      resource/locales/ja/translation.json
  23. 2 2
      src/client/js/app.jsx
  24. 0 0
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  25. 129 0
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  26. 0 115
      src/client/js/components/Admin/MarkdownSetting/LineBreakSetting.jsx
  27. 33 80
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  28. 122 0
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  29. 86 0
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  30. 27 17
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  31. 35 15
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  32. 137 0
      src/client/js/services/AdminMarkDownContainer.js
  33. 0 41
      src/client/js/services/MarkDownSettingContainer.js
  34. 0 1
      src/client/styles/agile-admin/inverse/colors/antarctic.scss
  35. 21 0
      src/linter-checker/test.js
  36. 23 0
      src/linter-checker/test.scss
  37. 28 0
      src/migrations/20191102223900-drop-configs-indices.js
  38. 29 0
      src/migrations/20191102223901-drop-pages-indices.js
  39. 41 0
      src/migrations/20191126173016-adjust-pages-path.js
  40. 33 0
      src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  41. 0 8
      src/server/form/admin/markdown.js
  42. 0 8
      src/server/form/admin/markdownPresentation.js
  43. 0 10
      src/server/form/admin/markdownXss.js
  44. 0 3
      src/server/form/index.js
  45. 2 0
      src/server/models/config.js
  46. 1 1
      src/server/models/page-tag-relation.js
  47. 0 54
      src/server/routes/admin.js
  48. 246 5
      src/server/routes/apiv3/markdown-setting.js
  49. 1 4
      src/server/routes/index.js
  50. 12 3
      src/server/routes/page.js
  51. 2 2
      src/server/service/search-delegator/elasticsearch.js
  52. 2 248
      src/server/views/admin/markdown.html
  53. 37 0
      src/test/config/migrate.test.js
  54. 0 187
      wercker.yml

+ 1 - 0
.eslintignore

@@ -4,4 +4,5 @@
 /public/**
 /src/client/js/legacy/thirdparty-js/**
 /src/client/js/util/reveal/plugins/markdown.js
+/src/linter-checker/**
 /tmp/**

+ 46 - 0
.github/workflows/build-rc.yml

@@ -0,0 +1,46 @@
+name: Release Docker Images for RC
+
+on:
+  push:
+    branches:
+      - rc/*
+
+jobs:
+
+  build-rc:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Set up Docker Buildx
+      uses: crazy-max/ghaction-docker-buildx@v1.0.4
+
+    - 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:3
+        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
+      run: |
+        semver=`npm run version --silent`
+        echo ::set-env name=SEMVER::$semver
+
+    - name: Docker Tags by SemVer
+      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
+      with:
+        source: growi
+        target: weseek/growi
+        semver: ${{ env.SEMVER }}
+        publish: true

+ 74 - 0
.github/workflows/build.yml

@@ -0,0 +1,74 @@
+name: Release Docker Images
+
+on:
+  push:
+    tags:
+      - v3.*
+
+jobs:
+
+  build:
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        flavor: [default, nocdn]
+
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Determine suffix
+      run: |
+        [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
+        echo ::set-env name=SUFFIX::$suffix
+
+    - name: Set up Docker Buildx
+      uses: crazy-max/ghaction-docker-buildx@v1.0.4
+
+    - 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:3${{ env.SUFFIX }}
+        docker buildx build \
+          --tag growi${{ env.SUFFIX }} \
+          --build-arg flavor=${{ matrix.flavor }} \
+          --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
+      run: |
+        semver=`npm run version --silent`
+        echo ::set-env name=SEMVER::$semver
+
+    - name: Docker Tags by SemVer
+      uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
+      with:
+        source: growi${{ env.SUFFIX }}
+        target: weseek/growi
+        semver: ${{ env.SEMVER }}
+        suffix: ${{ env.SUFFIX }}
+        additional-tags: 'latest'
+        publish: true
+
+  publish-desc:
+
+    runs-on: ubuntu-latest
+    needs: build
+
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Update Docker Hub Description
+      uses: peter-evans/dockerhub-description@v2.1.0
+      env:
+        DOCKERHUB_USERNAME: wsmoogle
+        DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
+        DOCKERHUB_REPOSITORY: weseek/growi
+        README_FILEPATH: ./docker/README.md

+ 164 - 0
.github/workflows/ci.yml

@@ -0,0 +1,164 @@
+name: Node CI
+
+on: [push]
+
+jobs:
+
+  resolve-dependencies:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      id: cache
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: Install dependencies
+      if: steps.cache.outputs.cache-hit != 'true'
+      run: |
+        yarn
+    - name: Install plugins
+      if: steps.cache.outputs.cache-hit != 'true'
+      run: |
+        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+        yarn add -D react-images react-motion
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+
+
+  test:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: yarn lint
+      run: |
+        yarn lint
+    - name: Launch MongoDB
+      uses: wbari/start-mongoDB@v0.2
+      with:
+        mongoDBVersion: 3.6
+    - name: yarn test
+      run: |
+        yarn test
+      env:
+        MONGO_URI: mongodb://localhost:27017/growi_test
+
+    - name: Slack Notification
+      uses: homoluctus/slatify@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*test (${{ matrix.node-version }})*'
+        channel: '#ci'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+  build-dev:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: yarn build:dev
+      run: |
+        yarn build:dev
+
+    - name: Slack Notification
+      uses: homoluctus/slatify@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*build-dev (${{ matrix.node-version }})*'
+        channel: '#ci'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  build-prod:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: Launch MongoDB
+      uses: wbari/start-mongoDB@v0.2
+      with:
+        mongoDBVersion: 3.6
+    - name: yarn build:prod:analyze
+      run: |
+        yarn build:prod:analyze
+    - name: Shrink dependencies for production
+      run: |
+        yarn install --production
+    - name: yarn server:prod:ci
+      run: |
+        yarn server:prod:ci
+      env:
+        MONGO_URI: mongodb://localhost:27017/growi
+
+    - name: Upload reports
+      uses: actions/upload-artifact@v1
+      if: success()
+      with:
+        name: report
+        path: report
+    - name: Slack Notification
+      uses: homoluctus/slatify@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*build-prod (${{ matrix.node-version }})*'
+        channel: '#ci'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 37 - 0
.github/workflows/release.yml

@@ -0,0 +1,37 @@
+name: GitHub Release
+
+on:
+  push:
+    branches:
+      - release/current
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v1
+      with:
+        fetch-depth: 1
+
+    - name: Init Git
+      run: |
+        git config --local user.name "GitHub Action"
+        git config --local user.email "info@weseek.co.jp"
+
+    - name: Bump version
+      run: |
+        npm version patch
+        sh ./bin/github-actions/bump-version.sh
+
+    - name: Commit
+      run: |
+        git commit -am "Release $RELEASE_VERSION"
+
+    - name: Push tag
+      uses: ad-m/github-push-action@master
+      with:
+        branch: null
+        github_token: ${{ secrets. GITHUB_TOKEN }}
+

+ 2 - 1
.stylelintrc.json

@@ -4,7 +4,8 @@
     "./node_modules/prettier-stylelint/config.js"
   ],
   "ignoreFiles": [
-    "src/client/styles/scss/_override-bootstrap-variables.scss"
+    "src/client/styles/scss/_override-bootstrap-variables.scss",
+    "src/linter-checker/test.scss"
   ],
   "rules": {
     "indentation": 2,

+ 10 - 0
CHANGES.md

@@ -4,6 +4,7 @@
 
 ### BREAKING CHANGES
 
+* GROWI v3.6.0 no longer support Node.js v8.x
 * The name of database that is storing migrations meta data has been changed
     * This affects **only when `MONGO_URI` has parameters**
     * v3.5.x or above has a bug ([#1361](https://github.com/weseek/growi/issues/1361))
@@ -13,6 +14,14 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 ### Updates
 
 * Improvement: Drop unnecessary MongoDB collection indexes
+* Improvement: Accessibility of Antarctic theme
+* Fix: Appending tag is failed by wrong index of PageTagRelation
+    * Introduced by 3.5.20
+* Fix: Pages without heading slash is invalid but creatable
+* Fix: Connect to Elasticsearch with `httpAuth` param
+* Support: Support Node.js v12
+* Support: Upgrade libs
+    * growi-commons
 
 ## 3.5.25
 
@@ -38,6 +47,7 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 
 ## 3.5.21
 
+* Improvement: Cache control when retrieving attachment data
 * Fix: Inviting user doesn't work
     * Introduced by 3.5.20
 

+ 32 - 85
README.md

@@ -19,74 +19,74 @@
 GROWI 
 ===========
 
-[![wercker status](https://app.wercker.com/status/595b761d0e26796ddb304679f7bf27de/s/master "wercker status")](https://app.wercker.com/project/byKey/595b761d0e26796ddb304679f7bf27de)
+[![Actions Status](https://github.com/weseek/growi/workflows/Node%20CI/badge.svg)](https://github.com/weseek/growi/actions)
 [![dependencies status](https://david-dm.org/weseek/growi.svg)](https://david-dm.org/weseek/growi)
 [![devDependencies Status](https://david-dm.org/weseek/growi/dev-status.svg)](https://david-dm.org/weseek/growi?type=dev)
 [![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
 
+| demonstration |
+| :-: |
+|![sample image](https://user-images.githubusercontent.com/42988650/70600974-6b29cc80-1c34-11ea-94ef-33c39c6a00dc.gif)|
+
 - [Features](#features)
 - [Quick Start for Production](#quick-start-for-production)
+    - [Heroku](#heroku)
+    - [docker-compose](#docker-compose)
     - [On-premise](#on-premise)
-    - [Using Heroku](#using-heroku)
-    - [Using docker-compose](#using-docker-compose)
 - [Environment Variables](#environment-variables)
 - [Documentation](#documentation)
 - [License](#license)
 
-
 Features
 ========
 
-* **Pluggable**
-  * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
 * **Features**
-  * Create hierarchical pages with markdown
+  * Create hierarchical pages with markdown -> [HERE](https://docs.growi.org/en/guide/getting-started/five_minutes.html) is 5 minutes tutorial
   * Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
+      * [GROWI Docs: HackMD(CodiMD) Integration](https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html)
   * Support Authentication with LDAP / Active Directory, OAuth
   * SSO(Single Sign On) with SAML
   * Slack/Mattermost, IFTTT Integration
-  * [Miscellaneous features](https://github.com/weseek/growi/wiki/Additional-Features)
+  * [GROWI Docs: Features](https://docs.growi.org/en/guide/features/page_layout.html)
+* **Pluggable**
+  * You can find plugins from [npm](https://www.npmjs.com/browse/keyword/growi-plugin) or [github](https://github.com/search?q=topic%3Agrowi-plugin)!
 * **[Docker Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
-  * [Multiple sites example](https://github.com/weseek/growi-docker-compose/tree/master/examples/multi-app)
-  * [HTTPS(with Let's Encrypt) proxy integration example](https://github.com/weseek/growi-docker-compose/tree/master/examples/https-portal)
-* Support IE11 (Experimental)
+  * [GROWI Docs: Multiple sites](https://docs.growi.org/en/admin-guide/admin-cookbook/multi-app.html)
+  * [GROWI Docs: HTTPS(with Let's Encrypt) proxy integration](https://docs.growi.org/en/admin-guide/admin-cookbook/lets-encrypt.html)
 
 Quick Start for Production
 ===========================
 
-Using Heroku
-------------
+### Heroku
+
+- [GROWI Docs: Launch on Heroku](https://docs.growi.org/en/admin-guide/getting-started/heroku.html) ([en](https://docs.growi.org/en/admin-guide/getting-started/heroku.html)/[ja](https://docs.growi.org/ja/admin-guide/getting-started/heroku.html))
 
-1. Go to https://heroku.com/deploy
-2. (Optional) Input INSTALL_PLUGINS to install plugins
+### docker-compose
 
-Using docker-compose
----------------------
+- [GROWI Docs: Launch with docker-compose](https://docs.growi.org/en/admin-guide/getting-started/docker-compose.html) ([en](https://docs.growi.org/en/admin-guide/getting-started/docker-compose.html)/[ja](https://docs.growi.org/ja/admin-guide/getting-started/docker-compose.html))
 
-```bash
-git clone https://github.com/weseek/growi-docker-compose.git growi
-cd growi
-docker-compose up
-```
+### On-premise
 
-See also [weseek/growi-docker-compose][docker-compose]
+**[Migration Guide from Crowi](https://docs.growi.org/en/admin-guide/migration-guide/from-crowi-onpremise.html) ([en](https://docs.growi.org/en/admin-guide/migration-guide/from-crowi-onpremise.html)/[ja](https://docs.growi.org/ja/admin-guide/migration-guide/from-crowi-onpremise.html))** is here.
 
-On-premise
-----------
+- [GROWI Docs: Install on Ubuntu Server](https://docs.growi.org/en/admin-guide/getting-started/ubuntu-server.html)
+- [GROWI Docs: Install on CentOS](https://docs.growi.org/en/admin-guide/getting-started/centos.html)
 
-[**Migration Guide from Crowi** is here](https://docs.growi.org/guide/migration-guide/from-crowi-onpremise.html).
 
-### Dependencies
+Development
+==========
+
+## Dependencies
 
 - Node.js v10.x (DON'T USE 11.x)
 - npm 6.x
 - yarn
 - MongoDB 3.x
 
-See [confirmed versions](https://docs.growi.org/dev/startup/dev-env.html#versions-confirmed-to-work).
+See [confirmed versions](https://docs.growi.org/en/dev/startup/dev-env.html#set-up-node-js-environment).
 
-#### Optional Dependencies
+### Optional Dependencies
 
 - Redis 3.x
 - ElasticSearch 6.x (needed when using Full-text search)
@@ -94,30 +94,7 @@ See [confirmed versions](https://docs.growi.org/dev/startup/dev-env.html#version
       - [Japanese (kuromoji) Analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
       - [ICU Analysis Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html)
 
-### How to start
-
-#### Build and run the app
-
-```bash
-git clone https://github.com/weseek/growi.git
-cd growi
-yarn
-MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi npm start
-```
-
-**DO NOT USE `npm install`**, use `yarn` instead.
-
-If you launch growi with ElasticSearch, add environment variables before `npm start` like following:
-
-```
-export MONGO_URI=mongodb://MONGO_HOST:MONGO_PORT/growi
-export ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi
-npm start
-```
-
-For more info, see [Developers Guide](https://docs.growi.org/dev/).
-
-#### Command details
+## Command details
 
 |command|desc|
 |--|--|
@@ -125,28 +102,7 @@ For more info, see [Developers Guide](https://docs.growi.org/dev/).
 |`npm run server:prod`|Launch the server|
 |`npm start`|Invoke `npm run build:prod` and `npm run server:prod`|
 
-### How to upgrade
-
-```bash
-git pull
-yarn
-npm start
-```
-
-### How to install plugins
-
-* Stop server if server is running
-* `yarn add` to install plugin or `npm install`
-* `npm start` to build client app and start server
-
-#### Examples
-
-```bash
-yarn add growi-plugin-lsx
-npm start
-```
-
-For more info, see [Developers Guide](https://docs.growi.org/dev/) on docs.growi.org.
+For more info, see [GROWI Docs: List of npm Commands](https://docs.growi.org/en/dev/startup/launch.html#list-of-npm-commands).
 
 
 Environment Variables
@@ -182,6 +138,7 @@ Environment Variables
     * FILE_UPLOAD_TOTAL_LIMIT: Total capacity limit for uploads (bytes). default: `Infinity`
     * GCS_API_KEY_JSON_PATH: Path of the JSON file that contains [service account key to authenticate to GCP API](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
     * GCS_BUCKET: Name of the GCS bucket
+    * GCS_UPLOAD_NAMESPACE: Directory name to create in the bucket
     * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
         * MONGO_GRIDFS_TOTAL_LIMIT setting takes precedence over FILE_UPLOAD_TOTAL_LIMIT.
 * **Option to integrate with external systems**
@@ -239,16 +196,6 @@ Repository. If you would like to *implement* a new feature, firstly please submi
 It also allows us to coordinate better, prevent duplication of work and help you to create the change so it can be successfully accepted into the project.
 * **Small Features** can be created and directly [submitted as a Pull Request][pulls].
 
-Translation
---------------
-
-We have some Transifex Projects.
-
-* [GROWI (Internationalize)](https://www.transifex.com/weseek-inc/growi)
-* [GROWI Docs (Internationalize)](https://www.transifex.com/weseek-inc/growi-docs)
-
-Please join to our team!
-
 
 Language on GitHub
 ------------------

+ 6 - 0
bin/github-actions/bump-version.sh

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

+ 0 - 9
bin/wercker/init-git.sh

@@ -1,9 +0,0 @@
-#!/bin/sh
-
-git config --global user.name "wercker"
-git config --global user.email "info@weseek.co.jp"
-
-# reconfigure origin
-GITHUB_ORIGIN=https://yuki-takei:$GITHUB_TOKEN@$WERCKER_GIT_DOMAIN/$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY.git
-git remote rm origin
-git remote add origin $GITHUB_ORIGIN

+ 0 - 40
bin/wercker/trigger-growi-docker.sh

@@ -1,40 +0,0 @@
-#!/bin/sh
-
-# Trigger a new run
-# see: http://devcenter.wercker.com/docs/api/endpoints/runs#trigger-a-run
-
-# exec curl
-#
-# require
-#   - $WERCKER_TOKEN
-#   - $GROWI_DOCKER_PIPELINE_ID
-#   - $RELEASE_VERSION
-#   - $RELEASE_GIT_COMMIT
-#
-RESPONSE=`curl -X POST \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer $WERCKER_TOKEN" \
-  https://app.wercker.com/api/v3/runs -d '{ \
-    "pipelineId": "'$GROWI_DOCKER_PIPELINE_ID'", \
-    "branch": "master", \
-    "envVars": [ \
-      { \
-        "key": "RELEASE_VERSION", \
-        "value": "'$RELEASE_VERSION'" \
-      }, \
-      { \
-        "key": "GROWI_REPOS_GIT_COMMIT", \
-        "value": "'$RELEASE_GIT_COMMIT'" \
-      } \
-    ] \
-  }' \
-`
-
-echo $RESPONSE | jq .
-
-# get wercker run id
-RUN_ID=`echo $RESPONSE | jq .id`
-# exit with failure status
-if [ "$RUN_ID" = "null" ]; then
-  exit 1
-fi

+ 0 - 28
bin/wercker/trigger-growi-docs.sh

@@ -1,28 +0,0 @@
-#!/bin/sh
-
-# Trigger a new run
-# see: http://devcenter.wercker.com/docs/api/endpoints/runs#trigger-a-run
-
-# exec curl
-#
-# require
-#   - $WERCKER_TOKEN
-#   - $GROWI_DOCS_PIPELINE_ID
-#
-RESPONSE=`curl -X POST \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer $WERCKER_TOKEN" \
-  https://app.wercker.com/api/v3/runs -d '{ \
-    "pipelineId": "'$GROWI_DOCS_PIPELINE_ID'", \
-    "branch": "master"
-  }' \
-`
-
-echo $RESPONSE | jq .
-
-# get wercker run id
-RUN_ID=`echo $RESPONSE | jq .id`
-# exit with failure status
-if [ "$RUN_ID" = "null" ]; then
-  exit 1
-fi

+ 18 - 8
config/migrate.js

@@ -7,20 +7,30 @@
 
 require('module-alias/register');
 
+const { URL } = require('url');
+
 const { getMongoUri } = require('@commons/util/mongoose-utils');
 
 const mongoUri = getMongoUri();
-const match = mongoUri.match(/^(.+)\/([^/]+)$/);
+
+// parse url
+const url = new URL(mongoUri);
+
+const authStr = (url.username.length > 0 && url.password.length > 0)
+  ? `${url.username}:${url.password}@`
+  : '';
+
+const mongodb = {
+  url: `${url.protocol}//${authStr}${url.host}${url.search}`,
+  databaseName: url.pathname.substring(1), // omit heading slash
+  options: {
+    useNewUrlParser: true, // removes a deprecation warning when connecting
+  },
+};
 
 module.exports = {
   mongoUri,
-  mongodb: {
-    url: match[0],
-    databaseName: match[2],
-    options: {
-      useNewUrlParser: true, // removes a deprecation warning when connecting
-    },
-  },
+  mongodb,
   migrationsDir: 'src/migrations/',
   changelogCollectionName: 'migrations',
 };

+ 117 - 0
docker/Dockerfile

@@ -0,0 +1,117 @@
+# syntax = docker/dockerfile:experimental
+
+ARG flavor=default
+
+
+
+##
+## deps-resolver
+##
+FROM node:12-slim AS deps-resolver
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV appDir /opt/growi
+
+COPY ./package.json ${appDir}/
+COPY ./yarn.lock ${appDir}/
+WORKDIR ${appDir}
+
+# setup
+RUN yarn config set network-timeout 300000
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn
+# install official plugins
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+# install peerDependencies
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn add -D react-images react-motion
+
+
+
+##
+## deps-resolver-prod
+##
+FROM deps-resolver AS deps-resolver-prod
+
+# shrink dependencies for production
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn install --production
+
+
+
+##
+## prebuilder-default
+##
+FROM node:12-slim AS prebuilder-default
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV appDir /opt/growi
+
+COPY . ${appDir}
+
+
+
+##
+## prebuilder-nocdn
+##
+FROM prebuilder-default AS prebuilder-nocdn
+
+# replace env.prod.js for NO_CDN
+COPY docker/nocdn/env.prod.js ${appDir}/config/
+
+
+
+##
+## prebuilder (alias)
+##
+FROM prebuilder-${flavor} AS prebuilder
+
+
+
+##
+## builder
+##
+FROM deps-resolver AS builder
+
+ENV appDir /opt/growi
+
+COPY --from=prebuilder ${appDir} ${appDir}
+
+# build
+RUN yarn build:prod
+
+# remove except artifacts
+WORKDIR /tmp
+RUN --mount=target=. sh docker/bin/remove-except-artifacts.sh
+WORKDIR ${appDir}
+
+
+
+##
+## release
+##
+FROM node:12-alpine
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV appDir /opt/growi
+
+# install tini
+RUN --mount=type=cache,target=/var/cache/apk \
+  apk add tini su-exec
+
+COPY docker/docker-entrypoint.sh /
+RUN chmod 700 /docker-entrypoint.sh
+
+COPY --from=deps-resolver-prod --chown=node:node \
+  ${appDir}/node_modules ${appDir}/node_modules
+COPY --from=builder --chown=node:node \
+  ${appDir} ${appDir}
+
+WORKDIR ${appDir}
+
+VOLUME /data
+EXPOSE 3000
+
+ENTRYPOINT ["/sbin/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
+CMD ["yarn", "server:prod"]

+ 14 - 0
docker/Dockerfile.dockerignore

@@ -0,0 +1,14 @@
+.git
+.github
+.vscode
+node_modules
+src/linter-checker
+src/test
+.editorconfig
+.eslint*
+.gitignore
+.prettier*
+.stylelint*
+app.json
+Procfile
+wercker.yml

+ 142 - 0
docker/README.md

@@ -0,0 +1,142 @@
+
+growi-docker
+===========
+
+[![wercker status](https://app.wercker.com/status/592aa5cebb59c67c4c266ce1f33b6a7e/s/ "wercker status")](https://app.wercker.com/project/byKey/592aa5cebb59c67c4c266ce1f33b6a7e) [![docker-build-automated](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/weseek/growi/) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
+
+![GROWI-x-docker](https://user-images.githubusercontent.com/1638767/38307565-105956e2-384f-11e8-8534-b1128522d68d.png)
+
+
+Supported tags and respective Dockerfile links
+------------------------------------------------
+
+* [`3.5.13`, `3.5`, `3`, `latest` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.13/Dockerfile)
+* [`3.5.13-nocdn`, `3.5-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.5.13/nocdn/Dockerfile)
+* [`3.4.7`, `3.4`, `3`, (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.4.7/Dockerfile)
+* [`3.4.7-nocdn`, `3.4-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi-docker/blob/v3.4.7/nocdn/Dockerfile)
+
+
+What is GROWI?
+-------------
+
+GROWI is a team collaboration software and it forked from [crowi](https://github.com/weseek/crowi/crowi)
+
+see: [weseek/growi](https://github.com/weseek/growi)
+
+What is growi-docker?
+-------------------
+
+The GROWI official docker image for production use which concludes several official plugins.
+
+- [growi-plugin-lsx](https://www.npmjs.com/package/growi-plugin-lsx)
+- [growi-plugin-pukiwiki-like-linker](https://www.npmjs.com/package/growi-plugin-pukiwiki-like-linker)
+- [growi-plugin-attachment-refs](https://www.npmjs.com/package/growi-plugin-attachment-refs)
+
+
+
+Requirements
+-------------
+
+* MongoDB (>= 3.6)
+
+### Optional Dependencies
+
+* ElasticSearch (>= 6.6)
+  * Japanese (kuromoji) Analysis plugin
+  * ICU Analysis Plugin
+
+
+Usage
+-----
+
+```bash
+docker run -d \
+    -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
+    weseek/growi
+```
+
+and go to `http://localhost:3000/` .
+
+If you use ElasticSearch, type this:
+
+```bash
+docker run -d \
+    -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
+    -e ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi \
+    weseek/growi
+```
+
+
+### docker-compose
+
+Using docker-compose is the fastest and the most convenient way to boot GROWI.
+
+see: [weseek/growi-docker-compose](https://github.com/weseek/growi-docker-compose)
+
+
+Environment Variables
+-------------------
+
+* **Required**
+    * MONGO_URI: URI to connect to MongoDB.
+* **Option**
+    * NODE_ENV: `production` OR `development`.
+    * PORT: Server port. default: `3000`
+    * ELASTICSEARCH_URI: URI to connect to Elasticearch.
+    * REDIS_URI: URI to connect to Redis (use it as a session store instead of MongoDB).
+    * PASSWORD_SEED: A password seed used by password hash generator.
+    * SECRET_TOKEN: A secret key for verifying the integrity of signed cookies.
+    * SESSION_NAME: The name of the session ID cookie to set in the response by Express. default: `connect.sid`
+    * FILE_UPLOAD: Attached files storage. default: `aws`
+        * `aws` : AWS S3 (needs AWS settings on Admin page)
+        * `mongodb` : MongoDB GridFS (Setting-less)
+        * `local` : Server's Local file system (Setting-less)
+        * `none` : Disable file uploading
+    * MAX_FILE_SIZE: The maximum file size limit for uploads (bytes). default: `Infinity`
+    * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
+    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: If `true`, the system uses only the value of the environment variable as the value of the SAML option that can be set via the environment variable.
+    * PUBLISH_OPEN_API: Publish GROWI OpenAPI resources with [ReDoc](https://github.com/Rebilly/ReDoc). Visit `/api-docs`.
+    * FORCE_WIKI_MODE: Forces wiki mode. default: undefined
+      * `public`  : Forces all pages to become public
+      * `private` : Forces all pages to become private
+      * undefined : Publicity will be configured by the admin security page settings
+    * FORMAT_NODE_LOG: If `false`, Output server log as JSON. defautl: `true` (Enabled only when `NODE_ENV=production`)
+* **Option to integrate with external systems**
+    * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
+        * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/management-cookbook/integrate-with-hackmd).**
+    * HACKMD_URI_FOR_SERVER: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server from GROWI Express server. If not set, `HACKMD_URI` will be used.
+    * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
+    * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.
+* **Option (Overwritable in admin page)**
+    * APP_SITE_URL: Site URL. e.g. `https://example.com`, `https://example.com:8080`
+    * LOCAL_STRATEGY_ENABLED: Enable or disable ID/Pass login
+    * LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some ID/Pass login options
+    * SAML_ENABLED: Enable or disable SAML
+    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some SAML options
+    * SAML_ENTRY_POINT: IdP entry point
+    * SAML_ISSUER: Issuer string to supply to IdP
+    * SAML_ATTR_MAPPING_ID: Attribute map for id
+    * SAML_ATTR_MAPPING_USERNAME: Attribute map for username
+    * SAML_ATTR_MAPPING_MAIL: Attribute map for email
+    * SAML_ATTR_MAPPING_FIRST_NAME: Attribute map for first name
+    * SAML_ATTR_MAPPING_LAST_NAME:  Attribute map for last name
+    * SAML_CERT: PEM-encoded X.509 signing certificate string to validate the response from IdP
+    * OAUTH_GOOGLE_CLIENT_ID: Google API client id for OAuth login.
+    * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
+    * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
+    * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_KEY: Twitter consumer key(API key) for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_SECRET: Twitter consumer secret(API secret) for OAuth login.
+
+Other Documentation
+--------------------
+
+* [GROWI Github wiki](https://github.com/weseek/growi/wiki)
+  * [Questions and Answers](https://github.com/weseek/growi/wiki/Questions-and-Answers)
+
+
+Issues
+------
+
+If you have any issues or questions about this image, please contact us through  [GitHub issue](https://github.com/weseek/growi-docker/issues).
+

+ 10 - 0
docker/bin/remove-except-artifacts.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+rm -rf \
+  ${appDir}/bin \
+  ${appDir}/docker \
+  ${appDir}/node_modules \
+  ${appDir}/src/client \
+  ${appDir}/babel.config.js \

+ 14 - 0
docker/docker-entrypoint.sh

@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -e
+
+# Support `FILE_UPLOAD=local`
+mkdir -p /data/uploads
+if [ ! -e "$appDir/public/uploads" ]; then
+  ln -s /data/uploads $appDir/public/uploads
+fi
+
+chown node:node /data/uploads
+chown -h node:node $appDir/public/uploads
+
+su-exec node $@

+ 5 - 0
docker/nocdn/env.prod.js

@@ -0,0 +1,5 @@
+module.exports = {
+  NODE_ENV: 'production',
+  NO_CDN: true,
+  // FORMAT_NODE_LOG: false,
+};

+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.5.25-RC",
+  "version": "3.6.0-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -248,8 +248,8 @@
     "debug": "src/lib/service/logger/alias-for-debug"
   },
   "engines": {
-    "node": ">=8.11.1 <11",
-    "npm": ">=5.6.0 <7",
-    "yarn": ">=1.5.1 <2"
+    "node": ">=10.17.0 <13",
+    "npm": ">=6.11.3 <7",
+    "yarn": ">=1.19.1 <2"
   }
 }

+ 6 - 3
resource/locales/en-US/translation.json

@@ -602,9 +602,9 @@
     "line_break_setting": "Line Break Setting",
     "line_break_setting_desc": "You can change line break settings.",
     "Enable Line Break": "Enable Line Break",
-    "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
     "Enable Line Break for comment": "Enable Line Break in comment",
-    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break for comment desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML",
     "presentation_setting": "Presentation Setting",
     "presentation_setting_desc": "You can change presentation settings.",
     "Page break setting": "Page break Setting",
@@ -625,7 +625,10 @@
     "Custom Whitelist": "Custom Whitelist",
     "Tag names":"Tag names",
     "Tag attributes":"Tag attributes",
-    "import_recommended": "Import recommended %s"
+    "import_recommended": "Import recommended %s",
+    "updated_lineBreak": "Succeeded to update line braek setting",
+    "updated_presentation": "Succeeded to update presentation setting",
+    "updated_xss": "Succeeded to update XSS setting"
   },
 
   "notification_setting": {

+ 4 - 2
resource/locales/ja/translation.json

@@ -609,7 +609,10 @@
     "Custom Whitelist": "カスタムホワイトリスト",
     "Tag names": "タグ名のホワイトリスト",
     "Tag attributes": "タグ属性のホワイトリスト",
-    "import_recommended": "おすすめをインポート"
+    "import_recommended": "おすすめをインポート",
+    "updated_lineBreak": "改行設定を更新しました",
+    "updated_presentation": "プレゼンテーション設定を更新しました",
+    "updated_xss": "XSS設定を更新しました"
   },
 
   "notification_setting": {
@@ -705,7 +708,6 @@
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_external_user_success": "{{accountId}}を削除しました "
-
   },
 
   "user_group_management": {

+ 2 - 2
src/client/js/app.jsx

@@ -55,7 +55,7 @@ import TagContainer from './services/TagContainer';
 import UserGroupDetailContainer from './services/UserGroupDetailContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
 import WebsocketContainer from './services/WebsocketContainer';
-import MarkDownSettingContainer from './services/MarkDownSettingContainer';
+import AdminMarkDownContainer from './services/AdminMarkDownContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
 
 const logger = loggerFactory('growi:app');
@@ -199,7 +199,7 @@ if (adminUserGroupDetailElem != null) {
 
 const adminMarkDownSettingElem = document.getElementById('admin-markdown-setting');
 if (adminMarkDownSettingElem != null) {
-  const markDownSettingContainer = new MarkDownSettingContainer(appContainer);
+  const markDownSettingContainer = new AdminMarkDownContainer(appContainer);
   ReactDOM.render(
     <Provider inject={[injectableContainers, markDownSettingContainer]}>
       <I18nextProvider i18n={i18n}>

+ 0 - 0
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx


+ 129 - 0
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -0,0 +1,129 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+
+const logger = loggerFactory('growi:importer');
+
+class LineBreakForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+
+  async onClickSubmit() {
+    const { t } = this.props;
+
+    try {
+      await this.props.adminMarkDownContainer.updateLineBreakSetting();
+      toastSuccess(t('markdown_setting.updated_lineBreak'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  renderLineBreakOption() {
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaks } = adminMarkDownContainer.state;
+
+    const helpLineBreak = { __html: t('markdown_setting.Enable Line Break desc') };
+
+    return (
+      <div className="form-group row">
+        <div className="col-xs-offset-4 col-xs-6 text-left">
+          <div className="checkbox checkbox-success">
+            <input
+              type="checkbox"
+              id="isEnabledLinebreaks"
+              checked={isEnabledLinebreaks}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+            />
+            <label htmlFor="isEnabledLinebreaks">
+              {t('markdown_setting.Enable Line Break')}
+            </label>
+          </div>
+          <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
+        </div>
+      </div>
+    );
+  }
+
+  renderLineBreakInCommentOption() {
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
+
+    const helpLineBreakInComment = { __html: t('markdown_setting.Enable Line Break for comment desc') };
+
+    return (
+      <div className="form-group row">
+        <div className="col-xs-offset-4 col-xs-6 text-left">
+          <div className="checkbox checkbox-success">
+            <input
+              type="checkbox"
+              id="isEnabledLinebreaksInComments"
+              checked={isEnabledLinebreaksInComments}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+            />
+            <label htmlFor="isEnabledLinebreaksInComments">
+              {t('markdown_setting.Enable Line Break for comment')}
+            </label>
+          </div>
+          <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const { t, adminMarkDownContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <fieldset className="row">
+          {this.renderLineBreakOption()}
+          {this.renderLineBreakInCommentOption()}
+        </fieldset>
+        <div className="form-group my-3">
+          <div className="col-xs-offset-4 col-xs-5">
+            <button
+              type="submit"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={adminMarkDownContainer.state.retrieveError != null}
+            >
+              {t('Update')}
+            </button>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const LineBreakFormWrapper = (props) => {
+  return createSubscribedElement(LineBreakForm, props, [AppContainer, AdminMarkDownContainer]);
+};
+
+LineBreakForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+};
+
+export default withTranslation()(LineBreakFormWrapper);

+ 0 - 115
src/client/js/components/Admin/MarkdownSetting/LineBreakSetting.jsx

@@ -1,115 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '@alias/logger';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-const logger = loggerFactory('growi:importer');
-
-class LineBreakSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const { appContainer } = this.props;
-
-    this.state = {
-      isEnabledLinebreaks: appContainer.config.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: appContainer.config.isEnabledLinebreaksInComments,
-    };
-    this.onChangeEnableLineBreaks = this.onChangeEnableLineBreaks.bind(this);
-    this.onChangeEnableLineBreaksInComments = this.onChangeEnableLineBreaksInComments.bind(this);
-    this.changeLineBreakSettings = this.changeLineBreakSettings.bind(this);
-  }
-
-
-  onChangeEnableLineBreaks() {
-    this.setState({ isEnabledLinebreaks: !this.state.isEnabledLinebreaks });
-  }
-
-  onChangeEnableLineBreaksInComments() {
-    this.setState({ isEnabledLinebreaksInComments: !this.state.isEnabledLinebreaksInComments });
-  }
-
-  async changeLineBreakSettings() {
-    const { appContainer } = this.props;
-    const params = {
-      isEnabledLinebreaks: this.state.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
-    };
-    try {
-      await appContainer.apiPost('/admin/markdown/lineBreaksSetting', { params });
-      toastSuccess('Success change line braek setting');
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row my-3">
-          <div className="form-group">
-            <legend>{ t('markdown_setting.line_break_setting') }</legend>
-            <p className="well">{ t('markdown_setting.line_break_setting_desc') }</p>
-            <fieldset className="row">
-              <div className="form-group">
-                <div className="col-xs-4 text-right">
-                  <div className="checkbox checkbox-success" onChange={this.onChangeEnableLineBreaks}>
-                    <input type="checkbox" name="isEnabledLinebreaks" checked={this.state.isEnabledLinebreaks} />
-                    <label>
-                      { t('markdown_setting.Enable Line Break') }
-                    </label>
-                    <p className="help-block">{ t('markdown_setting.Enable Line Break desc') }</p>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-            <fieldset className="row">
-              <div className="form-group my-3">
-                <div className="col-xs-4 text-right">
-                  <div className="checkbox checkbox-success" onChange={this.onChangeEnableLineBreaksInComments}>
-                    <input type="checkbox" name="isEnabledLinebreaksInComments" checked={this.state.isEnabledLinebreaksInComments} />
-                    <label>
-                      { t('markdown_setting.Enable Line Break for comment') }
-                    </label>
-                    <p className="help-block">{ t('markdown_setting.Enable Line Break for comment desc') }</p>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-          </div>
-          <div className="form-group my-3">
-            <div className="col-xs-offset-4 col-xs-5">
-              <button type="submit" className="btn btn-primary" onClick={this.changeLineBreakSettings}>{ t('Update') }</button>
-            </div>
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const LineBreakSettingWrapper = (props) => {
-  return createSubscribedElement(LineBreakSetting, props, [AppContainer]);
-};
-
-LineBreakSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(LineBreakSettingWrapper);

+ 33 - 80
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,106 +1,58 @@
-/* eslint-disable max-len */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import loggerFactory from '@alias/logger';
+
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
-import LineBreakSetting from './LineBreakSetting';
+import LineBreakForm from './LineBreakForm';
+import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
-class MarkdownSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
+const logger = loggerFactory('growi:MarkDown');
 
-    this.state = {
-      // TODO GW-220 get correct BreakOption value
-      pageBreakOption: 1,
-      // TODO GW-258 get correct custom regular expression
-      customRegularExpression: '',
-    };
+class MarkdownSetting extends React.Component {
 
-    this.handleInputChange = this.handleInputChange.bind(this);
-  }
+  async componentDidMount() {
+    const { adminMarkDownContainer } = this.props;
 
-  // TODO Delete after component split
-  handleInputChange(e) {
-    const target = e.target;
-    const value = target.type === 'checkbox' ? target.checked : target.value;
-    const name = target.name;
+    try {
+      await adminMarkDownContainer.retrieveMarkdownData();
+    }
+    catch (err) {
+      toastError(err);
+      adminMarkDownContainer.setState({ retrieveError: err });
+      logger.error(err);
+    }
 
-    this.setState({ [name]: value });
   }
 
   render() {
     const { t } = this.props;
 
     return (
-      // TODO GW-322 adjust layout
       <React.Fragment>
-        <div>
-          {/* Line Break Setting */}
-          <LineBreakSetting />
+        {/* Line Break Setting */}
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.line_break_setting') }</h2>
+          <p className="well">{ t('markdown_setting.line_break_setting_desc') }</p>
+          <LineBreakForm />
         </div>
 
-        <div className="row my-3">
-          <div className="form-group">
-            <legend>{ t('markdown_setting.presentation_setting') }</legend>
-            <p className="well">{ t('markdown_setting.presentation_setting_desc') }</p>
-          </div>
-          <fieldset className="form-group row my-2">
-
-            <label className="col-xs-3 control-label text-right">
-              { t('markdown_setting.Page break setting') }
-            </label>
-
-            <div className="col-xs-3 radio radio-primary">
-              <input type="radio" id="pageBreakOption1" name="pageBreakOption" value="1" checked={this.state.pageBreakOption === 1} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption1">
-                <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Preset one separator desc') }
-                  <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
-                </p>
-              </label>
-            </div>
-
-            <div className="col-xs-3 radio radio-primary mt-3">
-              <input type="radio" id="pageBreakOption2" name="pageBreakOption" value="2" checked={this.state.pageBreakOption === 2} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption2">
-                <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Preset two separator desc') }
-                  <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
-                </p>
-              </label>
-            </div>
-
-            <div className="col-xs-3 radio radio-primary mt-3">
-              <input type="radio" id="pageBreakOption3" name="pageBreakOption" value="3" checked={this.state.pageBreakOption === 3} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption3">
-                <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Custom separator desc') }
-                  <div>
-                    <input className="form-control" name="customRegularExpression" value={this.state.customRegularExpression} onChange={this.handleInputChange} />
-                  </div>
-                </p>
-              </label>
-            </div>
-
-            <div className="form-group my-3">
-              <div className="col-xs-offset-4 col-xs-5">
-                <button type="submit" className="btn btn-primary">{ t('Update') }</button>
-              </div>
-            </div>
-
-          </fieldset>
+        {/* Presentation Setting */}
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.presentation_setting') }</h2>
+          <p className="well">{ t('markdown_setting.presentation_setting_desc') }</p>
+          <PresentationForm />
         </div>
+
         {/* XSS Setting */}
-        <div className="row my-3">
-          <h2>{ t('markdown_setting.XSS_setting') }</h2>
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.XSS_setting') }</h2>
           <p className="well">{ t('markdown_setting.XSS_setting_desc') }</p>
           <XssForm />
         </div>
@@ -111,12 +63,13 @@ class MarkdownSetting extends React.Component {
 }
 
 const MarkdownSettingWrapper = (props) => {
-  return createSubscribedElement(MarkdownSetting, props, [AppContainer]);
+  return createSubscribedElement(MarkdownSetting, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 MarkdownSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
 

+ 122 - 0
src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -0,0 +1,122 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+
+const logger = loggerFactory('growi:markdown:presentation');
+
+class PresentationForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t } = this.props;
+
+    try {
+      await this.props.adminMarkDownContainer.updatePresentationSetting();
+      toastSuccess(t('markdown_setting.updated_presentation'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+
+  render() {
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
+
+    return (
+      <fieldset className="form-group row my-2">
+
+        <label className="col-xs-3 control-label text-right">
+          { t('markdown_setting.Page break setting') }
+        </label>
+
+        <div className="col-xs-3 radio radio-primary">
+          <input
+            type="radio"
+            id="pageBreakOption1"
+            checked={pageBreakSeparator === 1}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
+          />
+          <label htmlFor="pageBreakOption1">
+            <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Preset one separator desc') }
+              <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
+            </div>
+          </label>
+        </div>
+
+        <div className="col-xs-3 radio radio-primary mt-3">
+          <input
+            type="radio"
+            id="pageBreakOption2"
+            checked={pageBreakSeparator === 2}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
+          />
+          <label htmlFor="pageBreakOption2">
+            <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Preset two separator desc') }
+              <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
+            </div>
+          </label>
+        </div>
+
+        <div className="col-xs-3 radio radio-primary mt-3">
+          <input
+            type="radio"
+            id="pageBreakOption3"
+            checked={pageBreakSeparator === 3}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
+          />
+          <label htmlFor="pageBreakOption3">
+            <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Custom separator desc') }
+              <input
+                className="form-control"
+                defaultValue={pageBreakCustomSeparator}
+                onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
+              />
+            </div>
+          </label>
+        </div>
+
+        <div className="form-group my-3">
+          <div className="col-xs-offset-4 col-xs-5">
+            <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}>{ t('Update') }</div>
+          </div>
+        </div>
+
+      </fieldset>
+    );
+  }
+
+}
+
+const PresentationFormWrapper = (props) => {
+  return createSubscribedElement(PresentationForm, props, [AppContainer, AdminMarkDownContainer]);
+};
+
+PresentationForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+
+};
+
+export default withTranslation()(PresentationFormWrapper);

+ 86 - 0
src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx

@@ -0,0 +1,86 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+
+class PresentationLineBreakOptions extends React.Component {
+
+  render() {
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakOption, customRegularExpression } = adminMarkDownContainer.state;
+
+    return (
+      <Fragment>
+        <div className="col-xs-3 radio radio-primary">
+          <input
+            type="radio"
+            id="pageBreakOption1"
+            checked={pageBreakOption === 1}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 1 }) }}
+          />
+          <label htmlFor="pageBreakOption1">
+            <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Preset one separator desc') }
+              <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
+            </div>
+          </label>
+        </div>
+
+        <div className="col-xs-3 radio radio-primary mt-3">
+          <input
+            type="radio"
+            id="pageBreakOption2"
+            checked={pageBreakOption === 2}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 2 }) }}
+          />
+          <label htmlFor="pageBreakOption2">
+            <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Preset two separator desc') }
+              <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
+            </div>
+          </label>
+        </div>
+
+        <div className="col-xs-3 radio radio-primary mt-3">
+          <input
+            type="radio"
+            id="pageBreakOption3"
+            checked={pageBreakOption === 3}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 3 }) }}
+          />
+          <label htmlFor="pageBreakOption3">
+            <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
+            <div className="mt-3">
+              { t('markdown_setting.Custom separator desc') }
+              <input
+                className="form-control"
+                defaultValue={customRegularExpression}
+                onChange={(e) => { adminMarkDownContainer.setState({ customRegularExpression: e.target.value }) }}
+              />
+            </div>
+          </label>
+        </div>
+      </Fragment>
+    );
+  }
+
+}
+
+const PresentationLineBreakOptionsWrapper = (props) => {
+  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, AdminMarkDownContainer]);
+};
+
+PresentationLineBreakOptions.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+
+};
+
+export default withTranslation()(PresentationLineBreakOptionsWrapper);

+ 27 - 17
src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -6,49 +6,59 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 class WhiteListInput extends React.Component {
 
-  renderRecommendBtn() {
-    const { t } = this.props;
+  renderRecommendTagBtn() {
+    const { t, adminMarkDownContainer } = this.props;
 
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary">
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ tagWhiteList: tags }) }}>
         { t('markdown_setting.import_recommended', 'tags') }
       </p>
     );
   }
 
+  renderRecommendAttrBtn() {
+    const { t, adminMarkDownContainer } = this.props;
+
+    return (
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ attrWhiteList: attrs }) }}>
+        { t('markdown_setting.import_recommended', 'Attrs') }
+      </p>
+    );
+  }
+
   renderTagValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
     if (customizable) {
-      return markDownSettingContainer.state.tagWhiteList;
+      return adminMarkDownContainer.state.tagWhiteList;
     }
 
     return tags;
   }
 
   renderAttrValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
     if (customizable) {
-      return markDownSettingContainer.state.attrWhiteList;
+      return adminMarkDownContainer.state.attrWhiteList;
     }
 
     return attrs;
   }
 
   render() {
-    const { t, customizable, markDownSettingContainer } = this.props;
+    const { t, customizable, adminMarkDownContainer } = this.props;
 
     return (
       <>
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
             { t('markdown_setting.Tag names') }
-            {customizable && this.renderRecommendBtn()}
+            {customizable && this.renderRecommendTagBtn()}
           </div>
           <textarea
             className="form-control xss-list"
@@ -56,14 +66,14 @@ class WhiteListInput extends React.Component {
             rows="6"
             cols="40"
             readOnly={!customizable}
-            value={this.renderTagValue()}
-            onChange={(e) => { markDownSettingContainer.setState({ tagWhiteList: e.target.value }) }}
+            defaultValue={this.renderTagValue()}
+            onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
           />
         </div>
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
             { t('markdown_setting.Tag attributes') }
-            {customizable && this.renderRecommendBtn()}
+            {customizable && this.renderRecommendAttrBtn()}
           </div>
           <textarea
             className="form-control xss-list"
@@ -71,8 +81,8 @@ class WhiteListInput extends React.Component {
             rows="6"
             cols="40"
             readOnly={!customizable}
-            value={this.renderAttrValue()}
-            onChange={(e) => { markDownSettingContainer.setState({ attrWhiteList: e.target.value }) }}
+            defaultValue={this.renderAttrValue()}
+            onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
           />
         </div>
       </>
@@ -82,13 +92,13 @@ class WhiteListInput extends React.Component {
 }
 
 const WhiteListWrapper = (props) => {
-  return createSubscribedElement(WhiteListInput, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(WhiteListInput, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
   customizable: PropTypes.bool.isRequired,
 };

+ 35 - 15
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,14 +1,18 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 import WhiteListInput from './WhiteListInput';
 
+const logger = loggerFactory('growi:importer');
+
 class XssForm extends React.Component {
 
   constructor(props) {
@@ -18,12 +22,21 @@ class XssForm extends React.Component {
   }
 
   async onClickSubmit() {
-    // TODO GW-303 create apiV3 of update setting
+    const { t } = this.props;
+
+    try {
+      await this.props.adminMarkDownContainer.updateXssSetting();
+      toastSuccess(t('markdown_setting.updated_xss'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
   }
 
   xssOptions() {
-    const { t, markDownSettingContainer } = this.props;
-    const { xssOption } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { xssOption } = adminMarkDownContainer.state;
 
     return (
       <fieldset className="form-group col-xs-12 my-3">
@@ -33,7 +46,7 @@ class XssForm extends React.Component {
             id="xssOption1"
             name="XssOption"
             checked={xssOption === 1}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 1 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
           />
           <label htmlFor="xssOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
@@ -49,7 +62,7 @@ class XssForm extends React.Component {
             id="xssOption2"
             name="XssOption"
             checked={xssOption === 2}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 2 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
           />
           <label htmlFor="xssOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
@@ -63,7 +76,7 @@ class XssForm extends React.Component {
             id="xssOption3"
             name="XssOption"
             checked={xssOption === 3}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 3 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
           />
           <label htmlFor="xssOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
@@ -75,16 +88,23 @@ class XssForm extends React.Component {
   }
 
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledXss } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledXss } = adminMarkDownContainer.state;
 
     return (
       <React.Fragment>
         <form className="row">
           <div className="form-group">
-            <div className="col-xs-4 text-right">
-              <div className="checkbox checkbox-success" onChange={markDownSettingContainer.switchEnableXss}>
-                <input type="checkbox" id="XssEnable" className="form-check-input" name="isEnabledXss" checked={isEnabledXss} />
+            <div className="col-xs-offset-4 col-xs-4 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  type="checkbox"
+                  id="XssEnable"
+                  className="form-check-input"
+                  name="isEnabledXss"
+                  checked={isEnabledXss}
+                  onChange={adminMarkDownContainer.switchEnableXss}
+                />
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
                 </label>
@@ -94,7 +114,7 @@ class XssForm extends React.Component {
           </div>
           <div className="form-group my-3">
             <div className="col-xs-offset-4 col-xs-5">
-              <div className="btn btn-primary" onClick={this.onClickSubmit}>{ t('Update') }</div>
+              <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}> {t('Update')}</div>
             </div>
           </div>
         </form>
@@ -105,13 +125,13 @@ class XssForm extends React.Component {
 }
 
 const XssFormWrapper = (props) => {
-  return createSubscribedElement(XssForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(XssForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
 export default withTranslation()(XssFormWrapper);

+ 137 - 0
src/client/js/services/AdminMarkDownContainer.js

@@ -0,0 +1,137 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+const logger = loggerFactory('growi:services:AdminMarkdownContainer');
+
+/**
+ * Service container for admin markdown setting page (MarkDownSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminMarkDownContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      isEnabledLinebreaks: false,
+      isEnabledLinebreaksInComments: false,
+      pageBreakSeparator: 1,
+      pageBreakCustomSeparator: '',
+      isEnabledXss: false,
+      xssOption: 1,
+      tagWhiteList: '',
+      attrWhiteList: '',
+    };
+
+    this.switchEnableXss = this.switchEnableXss.bind(this);
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminMarkDownContainer';
+  }
+
+  /**
+   * retrieve markdown data
+   */
+  async retrieveMarkdownData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/markdown-setting/');
+      const { markdownParams } = response.data;
+
+      this.setState({
+        isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
+        isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
+        pageBreakSeparator: markdownParams.pageBreakSeparator,
+        pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
+        isEnabledXss: markdownParams.isEnabledXss,
+        xssOption: markdownParams.xssOption,
+        tagWhiteList: markdownParams.tagWhiteList || '',
+        attrWhiteList: markdownParams.attrWhiteList || '',
+      });
+
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to fetch data'));
+    }
+  }
+
+  /**
+   * Switch PageBreakSeparator
+   */
+  switchPageBreakSeparator(pageBreakSeparator) {
+    this.setState({ pageBreakSeparator });
+  }
+
+  /**
+   * Set PageBreakCustomSeparator
+   */
+  setPageBreakCustomSeparator(pageBreakCustomSeparator) {
+    this.setState({ pageBreakCustomSeparator });
+  }
+
+  /**
+   * Switch enableXss
+   */
+  switchEnableXss() {
+    if (this.state.isEnabledXss) {
+      this.setState({ xssOption: null });
+    }
+    this.setState({ isEnabledXss: !this.state.isEnabledXss });
+  }
+
+  /**
+   * Update LineBreak Setting
+   */
+  async updateLineBreakSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/lineBreak', {
+      isEnabledLinebreaks: this.state.isEnabledLinebreaks,
+      isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
+    });
+
+    return response;
+  }
+
+  /**
+   * Update Xss Setting
+   */
+  async updateXssSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
+      isEnabledXss: this.state.isEnabledXss,
+      xssOption: this.state.xssOption,
+      tagWhiteList: this.state.tagWhiteList,
+      attrWhiteList: this.state.attrWhiteList,
+    });
+
+    return response;
+  }
+
+  /**
+   * Update Presentation Setting
+   */
+  async updatePresentationSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/presentation', {
+      pageBreakSeparator: this.state.pageBreakSeparator,
+      pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
+    });
+
+    this.setState({
+      pageBreakSeparator: response.data.presentationParams.pageBreakSeparator,
+      pageBreakCustomSeparator: response.data.presentationParams.pageBreakCustomSeparator,
+    });
+    return response;
+  }
+
+}

+ 0 - 41
src/client/js/services/MarkDownSettingContainer.js

@@ -1,41 +0,0 @@
-import { Container } from 'unstated';
-
-/**
- * Service container for admin markdown setting page (MarkDownSetting.jsx)
- * @extends {Container} unstated Container
- */
-export default class MarkDownSettingContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-
-    this.state = {
-      isEnabledXss: (appContainer.config.xssOption != null),
-      xssOption: appContainer.config.xssOption,
-      tagWhiteList: appContainer.config.tagWhiteList || '',
-      attrWhiteList: appContainer.config.attrWhiteList || '',
-    };
-
-    this.switchEnableXss = this.switchEnableXss.bind(this);
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'MarkDownSettingContainer';
-  }
-
-  /**
-   * Switch enableXss
-   */
-  switchEnableXss() {
-    if (this.state.isEnabledXss) {
-      this.setState({ xssOption: null });
-    }
-    this.setState({ isEnabledXss: !this.state.isEnabledXss });
-  }
-
-}

+ 0 - 1
src/client/styles/agile-admin/inverse/colors/antarctic.scss

@@ -25,7 +25,6 @@ $background-color: rgba(
   $color: $themelight,
   $alpha: 0.8,
 );
-$info: $subthemecolor;
 
 @import 'apply-colors';
 @import 'apply-colors-light';

+ 21 - 0
src/linter-checker/test.js

@@ -0,0 +1,21 @@
+/*
+ * VSCode の Eslint 設定チェック方法
+ *
+ * 1. VSCode で以下のエラーが表示されていることを確認
+ *   - constructor で eslint(space-before-blocks)
+ *   - ファイル末尾の ";" で eslint(eol-last)
+ *
+ * 2. VSCode で上書き保存
+ *
+ * 3. 以下のように整形され、全てのエラーが消えていることを確認
+ *   - "constructor() {" のように間にスペースが入る
+ *   - ファイル末尾に空行が入る
+ *
+ */
+class EslintTest {
+  constructor(){
+    this.i = 0;
+  }
+}
+
+module.exports = EslintTest;

+ 23 - 0
src/linter-checker/test.scss

@@ -0,0 +1,23 @@
+/*
+ * VSCode の Stylelint 設定チェック方法
+ *
+ * 1. VSCode で以下のエラーが表示されていることを確認
+ *   - color で stylelint(order/properties-order)
+ *   - ul で stylelint(selector-combinator-space-after)
+ *
+ * 2. VSCode で上書き保存
+ *
+ * 3. 以下のように整形され、全てのエラーが消えていることを確認
+ *   - color が background の上の行にくる
+ *   - ul と li の間にスペースが入る
+ *
+ */
+
+.test {
+  background: #ccc;
+  color: #333;
+
+  ul>li {
+    margin-left: 0;
+  }
+}

+ 28 - 0
src/migrations/20191102223900-drop-configs-indices.js

@@ -0,0 +1,28 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-configs-indices');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(collection, indexName) {
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const collection = db.collection('configs');
+    await dropIndexIfExists(collection, 'ns_1');
+    await dropIndexIfExists(collection, 'key_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 29 - 0
src/migrations/20191102223901-drop-pages-indices.js

@@ -0,0 +1,29 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-pages-indices');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(collection, indexName) {
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const collection = db.collection('pages');
+    await dropIndexIfExists(collection, 'lastUpdateUser_1');
+    await dropIndexIfExists(collection, 'liker_1');
+    await dropIndexIfExists(collection, 'seenUsers_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 41 - 0
src/migrations/20191126173016-adjust-pages-path.js

@@ -0,0 +1,41 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:adjust-pages-path');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const pathUtils = require('growi-commons').pathUtils;
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Page = require('@server/models/page')();
+
+    // retrieve target data
+    const pages = await Page.find({ path: /^(?!\/)/ });
+
+
+    // create requests for bulkWrite
+    const requests = pages.map((page) => {
+      const adjustedPath = pathUtils.addHeadingSlash(page.path);
+      return {
+        updateOne: {
+          filter: { _id: page._id },
+          update: { $set: { path: adjustedPath } },
+        },
+      };
+    });
+
+    if (requests.length > 0) {
+      await db.collection('pages').bulkWrite(requests);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 33 - 0
src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -0,0 +1,33 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-wrong-index-of-page-tag-relation');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(db, collectionName, indexName) {
+  // check existence of the collection
+  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  if (items.length === 0) {
+    return;
+  }
+
+  const collection = await db.collection(collectionName);
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 0 - 8
src/server/form/admin/markdown.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:isEnabledLinebreaks]').trim().toBooleanStrict(),
-  field('markdownSetting[markdown:isEnabledLinebreaksInComments]').trim().toBooleanStrict(),
-);

+ 0 - 8
src/server/form/admin/markdownPresentation.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:presentation:pageBreakSeparator]').trim().toInt(),
-  field('markdownSetting[markdown:presentation:pageBreakCustomSeparator]').trim(),
-);

+ 0 - 10
src/server/form/admin/markdownXss.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:xss:isEnabledPrevention]').trim().toBooleanStrict(),
-  field('markdownSetting[markdown:xss:option]').trim().toInt(),
-  field('markdownSetting[markdown:xss:tagWhiteList]').trim(),
-  field('markdownSetting[markdown:xss:attrWhiteList]').trim(),
-);

+ 0 - 3
src/server/form/index.js

@@ -25,9 +25,6 @@ module.exports = {
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),
     securityPassportOidc: require('./admin/securityPassportOidc'),
-    markdown: require('./admin/markdown'),
-    markdownXss: require('./admin/markdownXss'),
-    markdownPresentation: require('./admin/markdownPresentation'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customheader: require('./admin/customheader'),

+ 2 - 0
src/server/models/config.js

@@ -176,6 +176,8 @@ module.exports = function(crowi) {
       layoutType: crowi.configManager.getConfig('crowi', 'customize:layout'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      pageBreakSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
+      pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
       isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),

+ 1 - 1
src/server/models/page-tag-relation.js

@@ -27,7 +27,7 @@ const schema = new mongoose.Schema({
   },
 });
 // define unique compound index
-schema.index({ page: 1, user: 1 }, { unique: true });
+schema.index({ relatedPage: 1, relatedTag: 1 }, { unique: true });
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 

+ 0 - 54
src/server/routes/admin.js

@@ -137,60 +137,6 @@ module.exports = function(crowi, app) {
     });
   };
 
-  // app.post('/admin/markdown/lineBreaksSetting' , admin.markdown.lineBreaksSetting);
-  actions.markdown.lineBreaksSetting = async function(req, res) {
-
-    const markdownSetting = req.form.markdownSetting;
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
-      req.flash('successMessage', ['Successfully updated!']);
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-    }
-    return res.redirect('/admin/markdown');
-
-  };
-
-  // app.post('/admin/markdown/presentationSetting' , admin.markdown.presentationSetting);
-  actions.markdown.presentationSetting = async function(req, res) {
-    const markdownSetting = req.form.markdownSetting;
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
-      req.flash('successMessage', ['Successfully updated!']);
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-    }
-
-    return res.redirect('/admin/markdown');
-  };
-
-  // app.post('/admin/markdown/xss-setting' , admin.markdown.xssSetting);
-  actions.markdown.xssSetting = async function(req, res) {
-    const xssSetting = req.form.markdownSetting;
-
-    xssSetting['markdown:xss:tagWhiteList'] = csvToArray(xssSetting['markdown:xss:tagWhiteList']);
-    xssSetting['markdown:xss:attrWhiteList'] = csvToArray(xssSetting['markdown:xss:attrWhiteList']);
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('markdown', xssSetting);
-      req.flash('successMessage', ['Successfully updated!']);
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-    }
-
-    return res.redirect('/admin/markdown');
-  };
-
-  const csvToArray = (string) => {
-    const array = string.split(',');
-    return array.map((item) => { return item.trim() });
-  };
-
   // app.get('/admin/customize' , admin.customize.index);
   actions.customize = {};
   actions.customize.index = function(req, res) {

+ 246 - 5
src/server/routes/apiv3/markdown-setting.js

@@ -7,19 +7,260 @@ const express = require('express');
 
 const router = express.Router();
 
+const { body } = require('express-validator/check');
+
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const validator = {
+  lineBreak: [
+    body('isEnabledLinebreaks').isBoolean(),
+    body('isEnabledLinebreaksInComments').isBoolean(),
+  ],
+  presentationSetting: [
+    body('pageBreakSeparator').isInt().not().isEmpty(),
+  ],
+  xssSetting: [
+    body('isEnabledXss').isBoolean(),
+    body('tagWhiteList').toArray(),
+    body('attrWhiteList').toArray(),
+  ],
+};
+
+
 /**
  * @swagger
  *  tags:
  *    name: MarkDownSetting
  */
 
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      CustomizeParams:
+ *        type: object
+ *      LineBreakParams:
+ *        type: object
+ *        properties:
+ *          isEnabledLinebreaks:
+ *            type: boolean
+ *            description: enable lineBreak
+ *          isEnabledLinebreaksInComments:
+ *            type: boolean
+ *            description: enable lineBreak in comment
+ *      PresentationParams:
+ *        type: object
+ *        properties:
+ *          pageBreakSeparator:
+ *            type: number
+ *            description: number of pageBreakSeparator
+ *          pageBreakCustomSeparator:
+ *            type: string
+ *            description: string of pageBreakCustomSeparator
+ *      XssParams:
+ *        type: object
+ *        properties:
+ *          isEnabledPrevention:
+ *            type: boolean
+ *            description: enable xss
+ *          xssOption:
+ *            type: number
+ *            description: number of xss option
+ *          tagWhiteList:
+ *            type: array
+ *            description: array of tag whiteList
+ *            items:
+ *              type: string
+ *              description: tag whitelist
+ *          attrWhiteList:
+ *            type: array
+ *            description: array of attr whiteList
+ *            items:
+ *              type: string
+ *              description: attr whitelist
+ */
+
 module.exports = (crowi) => {
-  // const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
-  // const adminRequired = require('../../middleware/admin-required')(crowi);
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/:
+   *      get:
+   *        tags: [MarkDownSettind]
+   *        description: Get markdown paramators
+   *        responses:
+   *          200:
+   *            description: params of markdown
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    markdonwParams:
+   *                      $ref: '#/components/schemas/CustomizeParams'
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const markdownParams = {
+      isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+      isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
+      pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
+      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+      tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
+      attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
+    };
+
+    return res.apiv3({ markdownParams });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/lineBreak:
+   *      put:
+   *        tags: [MarkDownSetting]
+   *        description: Update lineBreak setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/LineBreakParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update lineBreak setting
+   *            content:
+   *              application/json:
+   *                schema:
+  *                   $ref: '#/components/schemas/LineBreakParams'
+   */
+  router.put('/lineBreak', loginRequiredStrictly, adminRequired, csrf, validator.lineBreak, ApiV3FormValidator, async(req, res) => {
+
+    const requestLineBreakParams = {
+      'markdown:isEnabledLinebreaks': req.body.isEnabledLinebreaks,
+      'markdown:isEnabledLinebreaksInComments': req.body.isEnabledLinebreaksInComments,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', requestLineBreakParams);
+      const lineBreaksParams = {
+        isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+        isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      };
+      return res.apiv3({ lineBreaksParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating lineBreak';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-lineBreak-failed'));
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/presentation:
+   *      put:
+   *        tags: [MarkDownSetting]
+   *        description: Update presentation
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/PresentationParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update presentation setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/PresentationParams'
+   */
+  router.put('/presentation', loginRequiredStrictly, adminRequired, csrf, validator.presentationSetting, ApiV3FormValidator, async(req, res) => {
+    if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
+      return res.apiv3Err(new ErrorV3('customRegularExpression is required'));
+    }
+
+    const requestPresentationParams = {
+      'markdown:presentation:pageBreakSeparator': req.body.pageBreakSeparator,
+      'markdown:presentation:pageBreakCustomSeparator': req.body.pageBreakCustomSeparator,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', requestPresentationParams);
+      const presentationParams = {
+        pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
+        pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator') || '',
+      };
+      return res.apiv3({ presentationParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating presentation';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-presentation-failed'));
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/xss:
+   *      put:
+   *        tags: [MarkDownSetting]
+   *        description: Update xss
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/XssParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update xss setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/XssParams'
+   */
+  router.put('/xss', loginRequiredStrictly, adminRequired, csrf, validator.xssSetting, ApiV3FormValidator, async(req, res) => {
+    if (req.body.isEnabledXss && req.body.xssOption == null) {
+      return res.apiv3Err(new ErrorV3('xss option is required'));
+    }
+
+    const reqestXssParams = {
+      'markdown:xss:isEnabledPrevention': req.body.isEnabledXss,
+      'markdown:xss:option': req.body.xssOption,
+      'markdown:xss:tagWhiteList': req.body.tagWhiteList,
+      'markdown:xss:attrWhiteList': req.body.attrWhiteList,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', reqestXssParams);
+      const xssParams = {
+        isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+        xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+        tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
+        attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
+      };
+      return res.apiv3({ xssParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating xss';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-xss-failed'));
+    }
 
-  // const {
-  //   Config,
-  // } = crowi.models;
+  });
 
   return router;
 };

+ 1 - 4
src/server/routes/index.js

@@ -91,10 +91,7 @@ module.exports = function(crowi, app) {
   app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback);
 
   // markdown admin
-  app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index); // TODO delete
-  app.post('/admin/markdown/lineBreaksSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting); // change form name
-  app.post('/admin/markdown/xss-setting'      , loginRequiredStrictly , adminRequired , csrf, form.admin.markdownXss, admin.markdown.xssSetting);
-  app.post('/admin/markdown/presentationSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.markdownPresentation, admin.markdown.presentationSetting);
+  app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index);
 
   // markdown admin
   app.get('/admin/customize'                , loginRequiredStrictly , adminRequired , admin.customize.index);

+ 12 - 3
src/server/routes/page.js

@@ -573,7 +573,7 @@ module.exports = function(crowi, app) {
    */
   api.create = async function(req, res) {
     const body = req.body.body || null;
-    const pagePath = req.body.path || null;
+    let pagePath = req.body.path || null;
     const grant = req.body.grant || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
@@ -586,6 +586,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
     }
 
+    // check whether path starts slash
+    pagePath = pathUtils.addHeadingSlash(pagePath);
+
     // check page existence
     const isExist = await Page.count({ path: pagePath }) > 0;
     if (isExist) {
@@ -1064,7 +1067,7 @@ module.exports = function(crowi, app) {
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
-    const newPagePath = pathUtils.normalizePath(req.body.new_path);
+    let newPagePath = pathUtils.normalizePath(req.body.new_path);
     const options = {
       createRedirectPage: (req.body.create_redirect != null),
       updateMetadata: (req.body.remain_metadata == null),
@@ -1076,6 +1079,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
     }
 
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
     const isExist = await Page.count({ path: newPagePath }) > 0;
     if (isExist) {
       // if page found, cannot cannot rename to that path
@@ -1130,7 +1136,7 @@ module.exports = function(crowi, app) {
    */
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
-    const newPagePath = pathUtils.normalizePath(req.body.new_path);
+    let newPagePath = pathUtils.normalizePath(req.body.new_path);
 
     const page = await Page.findByIdAndViewer(pageId, req.user);
 
@@ -1138,6 +1144,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
     }
 
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
     await page.populateDataToShowRevision();
     const originTags = await page.findRelatedTagsById();
 

+ 2 - 2
src/server/service/search-delegator/elasticsearch.js

@@ -91,8 +91,8 @@ class ElasticsearchDelegator {
       host = `${url.protocol}//${url.host}`;
       indexName = url.pathname.substring(1); // omit heading slash
 
-      if (url.auth != null) {
-        httpAuth = url.auth;
+      if (url.username != null && url.password != null) {
+        httpAuth = `${url.username}:${url.password}`;
       }
     }
 

+ 2 - 248
src/server/views/admin/markdown.html

@@ -6,7 +6,7 @@
 {% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Markdown settings') }}</h1>
+    <h1 id="admin-title" class="title">{{ t('Markdown Settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -17,257 +17,11 @@
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'markdown'} %}
     </div>
-    <!-- TODO reactify admin -->
-    <div class="col-md-9">
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-          {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
 
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <form action="/admin/markdown/lineBreaksSetting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
-        <fieldset>
-          <legend>{{ t('markdown_setting.line_break_setting') }}</legend>
-          <p class="well">{{ t("markdown_setting.line_break_setting_desc") }}</p>
-
-          <div class="form-group">
-            <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
-              {{ t('markdown_setting.Enable Line Break') }}
-            </label>
-            <div class="col-xs-5">
-              <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
-                  <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="true" type="radio"
-                      {% if true === markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> ON
-                </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="default">
-                  <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="false" type="radio"
-                      {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
-                </label>
-              </div>
-              <p class="help-block">{{ t("markdown_setting.Enable Line Break desc") }}</p>
-            </div>
-          </div>
-
-          <div class="form-group">
-            <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
-              {{ t("markdown_setting.Enable Line Break for comment") }}
-            </label>
-            <div class="col-xs-5">
-              <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
-                  <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="true" type="radio"
-                      {% if true === markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> ON
-                </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="default">
-                  <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="false" type="radio"
-                      {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
-                </label>
-              </div>
-              <p class="help-block">{{ t("markdown_setting.Enable Line Break for comment desc") }}</p>
-            </div>
-          </div>
-
-          <div class="form-group my-3">
-            <div class="col-xs-offset-4 col-xs-5">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-
-      <form action="/admin/markdown/presentationSetting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
-        <legend>{{ t('markdown_setting.presentation_setting') }}</legend>
-        <p class="well">{{ t("markdown_setting.presentation_setting_desc") }}</p>
-
-        <fieldset class="form-group row my-2">
-          {% set nameForPageBreakOption = "markdownSetting[markdown:presentation:pageBreakSeparator]" %}
-          {% set pageBreakSeparator = markdownSetting['markdown:presentation:pageBreakSeparator'] %}
-
-          <label class="col-xs-3 control-label">
-            {{ t('markdown_setting.Page break setting') }}
-          </label>
-
-          <div class="col-xs-3 radio radio-primary">
-            <input type="radio" id="pageBreakOption1" name="{{nameForPageBreakOption}}" value="1" {% if pageBreakSeparator === 1 %}checked{% endif %}>
-            <label for="pageBreakOption1">
-              <p class="font-weight-bold">{{ t('markdown_setting.Preset one separator') }}</p>
-              <p class="mt-3">
-                {{ t('markdown_setting.Preset one separator desc') }}
-                <pre><code>{{ t('markdown_setting.Preset one separator value') }}</code></pre>
-              </p>
-            </label>
-          </div>
-
-          <div class="col-xs-3 radio radio-primary">
-            <input type="radio" id="pageBreakOption2" name="{{nameForPageBreakOption}}" value="2" {% if pageBreakSeparator === 2 %}checked{% endif %}>
-            <label for="pageBreakOption2">
-              <p class="font-weight-bold">{{ t('markdown_setting.Preset two separator') }}</p>
-              <p class="mt-3">
-                {{ t('markdown_setting.Preset two separator desc') }}
-                <pre><code>{{ t('markdown_setting.Preset two separator value') }}</code></pre>
-              </p>
-            </label>
-          </div>
-
-          <div class="col-xs-3 radio radio-primary">
-            <input type="radio" id="pageBreakOption3" name="{{nameForPageBreakOption}}" value="3" {% if pageBreakSeparator === 3 %}checked{% endif %}>
-            <label for="pageBreakOption3">
-              <p class="font-weight-bold">{{ t('markdown_setting.Custom separator') }}</p>
-              <p class="mt-3">
-                {{ t('markdown_setting.Custom separator desc') }}
-                <div>
-                  <input class="form-control" name="markdownSetting[markdown:presentation:pageBreakCustomSeparator]" value="{{markdownSetting['markdown:presentation:pageBreakCustomSeparator']|default('') }}">
-                </div>
-              </p>
-            </label>
-          </div>
-
-        </fieldset>
-
-        <div class="form-group my-3">
-          <div class="col-xs-offset-4 col-xs-5">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
-          </div>
-        </div>
-      </form>
-
-      <form action="/admin/markdown/xss-setting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
-        {% set nameForIsXssEnabled = "markdownSetting[markdown:xss:isEnabledPrevention]" %}
-        {% set isXssEnabled = markdownSetting['markdown:xss:isEnabledPrevention'] %}
-
-        <legend>{{ t('markdown_setting.XSS_setting') }}</legend>
-        <p class="well">{{ t("markdown_setting.XSS_setting_desc") }}</p>
-
-        <fieldset class="row">
-          <div class="form-group">
-            <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
-              {{ t('markdown_setting.Enable XSS prevention') }}
-            </label>
-            <div class="col-xs-5">
-              <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if isXssEnabled %}active{% endif %}" data-active-class="primary">
-                  <input name="{{nameForIsXssEnabled}}" value="true" type="radio"
-                      {% if isXssEnabled %}checked{% endif %}> ON
-                </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !isXssEnabled %}active{% endif %}" data-active-class="default">
-                  <input name="{{nameForIsXssEnabled}}" value="false" type="radio"
-                      {% if !isXssEnabled %}checked{% endif %}> OFF
-                </label>
-              </div>
-            </div>
-          </div>
-        </fieldset>
-
-        <fieldset class="form-group row my-3" id="xss-hide-when-disabled" {% if !isXssEnabled %}style="display: none;"{% endif %}>
-          {% set nameForXssOption = "markdownSetting[markdown:xss:option]" %}
-          {% set xssOption = markdownSetting['markdown:xss:option'] %}
-
-          <div class="col-xs-4 radio radio-primary">
-            <input type="radio" id="xssOption1" name="{{nameForXssOption}}" value="1" {% if xssOption === 1 %}checked{% endif %}>
-            <label for="xssOption1">
-              <p class="font-weight-bold">{{ t('markdown_setting.Ignore all tags') }}</p>
-              <div class="m-t-15">
-                  {{ t('markdown_setting.Ignore all tags desc') }}
-              </div>
-            </label>
-          </div>
-
-          <div class="col-xs-4 radio radio-primary">
-            <input type="radio" id="xssOption2" name="{{nameForXssOption}}" value="2" {% if xssOption === 2 %}checked{% endif %}>
-            <label for="xssOption2">
-              <p class="font-weight-bold">{{ t('markdown_setting.Recommended setting') }}</p>
-              <div class="m-t-15">
-                {{ t('markdown_setting.Tag names') }}
-                <textarea class="form-control xss-list" name="recommendedTags" rows="6" cols="40" readonly>{{ recommendedWhitelist.tags }}</textarea>
-              </div>
-              <div class="m-t-15">
-                {{ t('markdown_setting.Tag attributes') }}
-                <textarea class="form-control xss-list" name="recommendedAttrs" rows="6" cols="40" readonly>{{ recommendedWhitelist.attrs }}</textarea>
-              </div>
-            </label>
-          </div>
-
-          <div class="col-xs-4 radio radio-primary">
-            <input type="radio" id="xssOption3" name="{{nameForXssOption}}" value="3" {% if xssOption === 3 %}checked{% endif %}>
-            <label for="xssOption3">
-              <p class="font-weight-bold">{{ t('markdown_setting.Custom Whitelist') }}</p>
-              <div class="m-t-15">
-                <div class="d-flex justify-content-between">
-                  {{ t('markdown_setting.Tag names') }}
-                  <p id="btn-import-tags" class="btn btn-xs btn-primary">
-                    {{ t('markdown_setting.import_recommended', 'tags') }}
-                  </p>
-                </div>
-                <textarea class="form-control xss-list" type="text" name="markdownSetting[markdown:xss:tagWhiteList]" rows="6" cols="40" placeholder="e.g. iframe, script, video...">{{ markdownSetting['markdown:xss:tagWhiteList'] }}</textarea>
-              </div>
-              <div class="m-t-15">
-                <div class="d-flex justify-content-between">
-                  {{ t('markdown_setting.Tag attributes') }}
-                  <p id="btn-import-attrs" class="btn btn-xs btn-primary">
-                    {{ t('markdown_setting.import_recommended', 'attributes') }}
-                  </p>
-                </div>
-                <textarea class="form-control xss-list" name="markdownSetting[markdown:xss:attrWhiteList]" rows="6" cols="40" placeholder="e.g. src, id, name...">{{ markdownSetting['markdown:xss:attrWhiteList'] }}</textarea>
-              </div>
-            </label>
-          </div>
-
-        </fieldset>
-
-        <div class="form-group row">
-          <div class="col-xs-12 d-flex justify-content-center">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
-          </div>
-        </div>
-
-      </form>
-    </div>
+    <div class="col-md-9" id="admin-markdown-setting"></div>
   </div>
 
 </div>
-
-<script>
-  // give a space between items in textarea(',' => ', ')
-  for (var i = 0; i < $('textarea.xss-list').length; i++) {
-    $($('textarea.xss-list')[i]).val($($('textarea.xss-list')[i]).val().replace(/,/g, ', '));
-  };
-
-  $('input[name="markdownSetting[markdown:xss:isEnabledPrevention]"]').change(function() {
-    if ($(this).val() === 'true') {
-      $('#xss-hide-when-disabled').slideDown();
-    }
-    else {
-      $('#xss-hide-when-disabled').slideUp();
-    }
-  });
-
-  $('#btn-import-tags').on('click', () => {
-    var $tagWhiteList = $('textarea[name="markdownSetting[markdown:xss:tagWhiteList]"]');
-    var $recommendedTagList = $('textarea[name="recommendedTags"]');
-    $tagWhiteList.val($recommendedTagList.val());
-  });
-  $('#btn-import-attrs').on('click', () => {
-    var $attrWhiteList = $('textarea[name="markdownSetting[markdown:xss:attrWhiteList]"]');
-    var $recommendedAttrList = $('textarea[name="recommendedAttrs"]');
-    $attrWhiteList.val($recommendedAttrList.val());
-  });
-</script>
 {% endblock content_main %}
 
 {% block content_footer %}

+ 37 - 0
src/test/config/migrate.test.js

@@ -0,0 +1,37 @@
+describe('config/migrate.js', () => {
+
+  beforeEach(async(done) => {
+    jest.resetModules();
+    done();
+  });
+
+  /* eslint-disable indent */
+  describe.each`
+    MONGO_URI                                         | expectedUrl                                     | expectedDbName
+    ${'mongodb://example.com/growi'}                  | ${'mongodb://example.com'}                      | ${'growi'}
+    ${'mongodb://user:pass@example.com/growi'}        | ${'mongodb://user:pass@example.com'}            | ${'growi'}
+    ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'mongodb://example.com?replicaSet=mySet'}     | ${'growi'}
+  `('returns', ({ MONGO_URI, expectedUrl, expectedDbName }) => {
+    test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
+
+      // mock for mongoose-utils
+      jest.doMock('@commons/util/mongoose-utils', () => {
+        return {
+          getMongoUri: () => {
+            return MONGO_URI;
+          },
+        };
+      });
+
+      const { mongoUri, mongodb } = require('@root/config/migrate');
+
+      jest.dontMock('@commons/util/mongoose-utils');
+
+      expect(mongoUri).toBe(MONGO_URI);
+      expect(mongodb.url).toBe(expectedUrl);
+      expect(mongodb.databaseName).toBe(expectedDbName);
+    });
+  });
+  /* eslint-enable indent */
+
+});

+ 0 - 187
wercker.yml

@@ -1,187 +0,0 @@
-box: node:10
-
-services:
-  - mongo:3.6
-
-
-test:
-  steps:
-    - script:
-      name: set yarn cache-folder
-      code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
-
-    - script:
-      name: install dependencies
-      code: |
-        yarn
-
-    - script:
-      name: install plugins
-      code: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-        yarn add -D react-images react-motion
-
-    - script:
-      name: print dependencies
-      code: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-
-    - script:
-      name: npm run lint
-      code: |
-        npm run lint
-
-    - script:
-      name: npm test
-      code: |
-        export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi_test
-        echo "export MONGO_URI=$MONGO_URI"
-        npm test
-
-  after-steps:
-    - slack-notifier:
-      url: $SLACK_WEBHOOK_URL
-      channel: ci
-      username: wercker
-      notify_on: "failed"
-
-
-build-prod:
-  steps:
-    - script:
-      name: set yarn cache-folder
-      code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
-
-    - script:
-      name: npm run build:prod:analyze
-      code: |
-        npm run build:prod:analyze
-
-    - script:
-      name: shrink dependencies for production
-      code: |
-        yarn install --production
-
-    - script:
-      name: npm run server:prod:ci
-      code: |
-        export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi
-        echo "export MONGO_URI=$MONGO_URI"
-        npm run server:prod:ci
-
-  after-steps:
-    - script:
-      name: copy report to artifacts
-      code: |
-        cp -r report $WERCKER_REPORT_ARTIFACTS_DIR
-
-    - slack-notifier:
-      url: $SLACK_WEBHOOK_URL
-      channel: ci
-      username: wercker
-      notify_on: "failed"
-
-
-build-dev:
-  steps:
-    - script:
-      name: set yarn cache-folder
-      code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
-
-    - script:
-      name: npm run build:dev
-      code: |
-        npm run build:dev
-
-  after-steps:
-    - slack-notifier:
-      url: $SLACK_WEBHOOK_URL
-      channel: ci
-      username: wercker
-      notify_on: "failed"
-
-
-release: # would be run on release branch
-  steps:
-    - install-packages:
-      packages: jq
-
-    - script:
-      name: bump version
-      code: |
-        sh ./bin/wercker/init-git.sh
-        # git reset
-        git reset --hard
-        # npm version to bump version
-        npm version patch
-
-    - script:
-      name: get RELEASE_VERSION
-      code: |
-        export RELEASE_VERSION=`npm run version --silent`
-        echo "export RELEASE_VERSION=$RELEASE_VERSION"
-
-    - script:
-      name: commit and push
-      code: |
-        TMP_RELEASE_BRANCH=tmp/release-$RELEASE_VERSION
-        git checkout -B $TMP_RELEASE_BRANCH
-        git push -u origin HEAD:$TMP_RELEASE_BRANCH
-        export RELEASE_GIT_COMMIT=`git rev-parse HEAD`
-
-    - github-create-release:
-      token: $GITHUB_TOKEN
-      tag: v$RELEASE_VERSION
-      target-commitish: $RELEASE_GIT_COMMIT
-
-    - script:
-      name: remove temporary release branch
-      code: |
-        git push --delete origin $TMP_RELEASE_BRANCH
-
-    - script:
-      name: trigger growi-docker release pipeline
-      code: GROWI_DOCKER_PIPELINE_ID=$GROWI_DOCKER_PIPELINE_ID_CDN sh ./bin/wercker/trigger-growi-docker.sh
-
-    - script:
-      name: trigger growi-docker release-nocdn pipeline
-      code: GROWI_DOCKER_PIPELINE_ID=$GROWI_DOCKER_PIPELINE_ID_NOCDN sh ./bin/wercker/trigger-growi-docker.sh
-
-    - script:
-      name: trigger growi-docs deploy pipeline
-      code: sh ./bin/wercker/trigger-growi-docs.sh
-
-  after-steps:
-    - slack-notifier:
-      url: $SLACK_WEBHOOK_URL
-      channel: ci
-      username: wercker
-      notify_on: "failed"
-
-
-release-rc: # would be run on rc/* branches
-  steps:
-    - install-packages:
-      packages: jq
-
-    - script:
-      name: get RELEASE_VERSION
-      code: |
-        export RELEASE_VERSION=`npm run version --silent`
-        export RELEASE_GIT_COMMIT=$WERCKER_GIT_COMMIT
-        echo "export RELEASE_VERSION=$RELEASE_VERSION"
-        echo "export RELEASE_GIT_COMMIT=$RELEASE_GIT_COMMIT"
-
-    - script:
-      name: trigger growi-docker release-rc pipeline
-      code: sh ./bin/wercker/trigger-growi-docker.sh
-
-  after-steps:
-    - slack-notifier:
-      url: $SLACK_WEBHOOK_URL
-      channel: ci
-      username: wercker
-      notify_on: "failed"
-