Explorar o código

Merge branch 'master' into feat/show-suggestions-for-joining-user-group

itizawa %!s(int64=6) %!d(string=hai) anos
pai
achega
37f968228a
Modificáronse 44 ficheiros con 758 adicións e 660 borrados
  1. 46 0
      .github/workflows/build-rc.yml
  2. 74 0
      .github/workflows/build.yml
  3. 164 0
      .github/workflows/ci.yml
  4. 0 30
      .github/workflows/main.yml
  5. 37 0
      .github/workflows/release.yml
  6. 0 95
      .github/workflows/test.yml
  7. 13 0
      CHANGES.md
  8. 5 2
      README.md
  9. 1 1
      bin/heroku/install-packages.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. 1 0
      package.json
  14. 2 2
      src/client/js/app.jsx
  15. 10 1
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  16. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  17. 21 14
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  18. 22 1
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  19. 12 12
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  20. 10 10
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  21. 16 16
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  22. 13 13
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  23. 1 1
      src/client/js/components/PageEditor/HandsontableModal.jsx
  24. 23 7
      src/client/js/components/User/UserPicture.jsx
  25. 9 1
      src/client/js/components/User/Username.jsx
  26. 43 12
      src/client/js/services/AdminMarkDownContainer.js
  27. 41 0
      src/migrations/20191126173016-adjust-pages-path.js
  28. 0 8
      src/server/form/admin/markdown.js
  29. 0 8
      src/server/form/admin/markdownPresentation.js
  30. 0 10
      src/server/form/admin/markdownXss.js
  31. 0 3
      src/server/form/index.js
  32. 0 15
      src/server/routes/admin.js
  33. 2 3
      src/server/routes/apiv3/import.js
  34. 78 74
      src/server/routes/apiv3/markdown-setting.js
  35. 2 1
      src/server/routes/apiv3/mongo.js
  36. 32 0
      src/server/routes/apiv3/overwrite-params/attachmentFiles.chunks.js
  37. 1 1
      src/server/routes/attachment.js
  38. 12 3
      src/server/routes/page.js
  39. 4 0
      src/server/service/export.js
  40. 4 23
      src/server/service/growi-bridge.js
  41. 51 26
      src/server/service/import.js
  42. 2 2
      src/server/service/search-delegator/elasticsearch.js
  43. 0 187
      wercker.yml
  44. 5 0
      yarn.lock

+ 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 }}

+ 0 - 30
.github/workflows/main.yml

@@ -1,30 +0,0 @@
-name: CI
-
-on:
-  push:
-    branches:
-      - support/github-actions
-
-jobs:
-  build:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v1
-
-    - name: Bump version
-      run: sh ./bin/github-actions/bump-version.sh
-
-    - name: Set up Docker Buildx
-      uses: crazy-max/ghaction-docker-buildx@v1.0.4
-      with:
-        # Buildx version. Example: v0.3.0
-        version: # optional, default is latest
-
-    - name: Build Docker Image
-      run: |
-        docker buildx build \
-          --platform linux/amd64 \
-          --output "type=image,push=false" \
-          --file ./docker/Dockerfile .

+ 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 }}
+

+ 0 - 95
.github/workflows/test.yml

@@ -1,95 +0,0 @@
-name: Node CI
-
-# on: [push]
-on:
-  push:
-    branches:
-      - support/github-actions
-
-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: install dependencies
-      run: |
-        yarn
-    - name: install plugins
-      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
-
-
-  lint:
-    runs-on: ubuntu-latest
-    needs: resolve-dependencies
-
-    steps:
-    - uses: actions/checkout@v1
-    - name: yarn lint
-      run: |
-        yarn lint
-
-
-  test:
-    runs-on: ubuntu-latest
-    needs: resolve-dependencies
-
-    steps:
-    - 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
-
-
-  build-dev:
-    runs-on: ubuntu-latest
-    needs: resolve-dependencies
-
-    steps:
-    - name: yarn build:dev
-      run: |
-        yarn build:dev
-
-
-  build-prod:
-    runs-on: ubuntu-latest
-    needs: resolve-dependencies
-
-    steps:
-    - 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

+ 13 - 0
CHANGES.md

@@ -14,12 +14,25 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 ### Updates
 ### Updates
 
 
 * Improvement: Drop unnecessary MongoDB collection indexes
 * Improvement: Drop unnecessary MongoDB collection indexes
+* Improvement: Accessibility of Antarctic theme
 * Fix: Appending tag is failed by wrong index of PageTagRelation
 * Fix: Appending tag is failed by wrong index of PageTagRelation
     * Introduced by 3.5.20
     * 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: Support Node.js v12
 * Support: Upgrade libs
 * Support: Upgrade libs
     * growi-commons
     * growi-commons
 
 
+## 3.5.25
+
+* Improvement: Disable ESC key to close Handsontable Modal
+* Fix: Exported data of empty collection is broken
+* Fix: Some components crash after when the page with attachment has exported/imported
+
+## 3.5.24
+
+* Fix: Plugins are not working on Heroku
+
 ## 3.5.23
 ## 3.5.23
 
 
 * Fix: Global Notification failed to send e-mail
 * Fix: Global Notification failed to send e-mail

+ 5 - 2
README.md

@@ -19,11 +19,15 @@
 GROWI 
 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)
 [![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)
 [![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/)
 [![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)
 - [Features](#features)
 - [Quick Start for Production](#quick-start-for-production)
 - [Quick Start for Production](#quick-start-for-production)
     - [Heroku](#heroku)
     - [Heroku](#heroku)
@@ -33,7 +37,6 @@ GROWI
 - [Documentation](#documentation)
 - [Documentation](#documentation)
 - [License](#license)
 - [License](#license)
 
 
-
 Features
 Features
 ========
 ========
 
 

+ 1 - 1
bin/heroku/install-packages.sh

@@ -1,3 +1,3 @@
 #!/bin/sh
 #!/bin/sh
 
 
-yarn add -D $ADDITIONAL_PACKAGES
+yarn add $ADDITIONAL_PACKAGES

+ 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

+ 1 - 0
package.json

@@ -107,6 +107,7 @@
     "i18next-express-middleware": "^1.4.1",
     "i18next-express-middleware": "^1.4.1",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
+    "is-iso-date": "^0.0.1",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "migrate-mongo": "^7.0.1",
     "migrate-mongo": "^7.0.1",

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

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

+ 10 - 1
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -15,6 +15,10 @@ import ProgressBar from './Common/ProgressBar';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 
 
+const IGNORED_COLLECTION_NAMES = [
+  'sessions',
+];
+
 class ExportArchiveDataPage extends React.Component {
 class ExportArchiveDataPage extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -46,9 +50,14 @@ class ExportArchiveDataPage extends React.Component {
     ]);
     ]);
     // TODO: toastSuccess, toastError
     // TODO: toastSuccess, toastError
 
 
+    // filter only not ignored collection names
+    const filteredCollections = collections.filter((collectionName) => {
+      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+    });
+
     const { zipFileStats, isExporting, progressList } = status;
     const { zipFileStats, isExporting, progressList } = status;
     this.setState({
     this.setState({
-      collections,
+      collections: filteredCollections,
       zipFileStats,
       zipFileStats,
       isExporting,
       isExporting,
       progressList,
       progressList,

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -57,7 +57,7 @@ class UploadForm extends React.Component {
                 type="file"
                 type="file"
                 name="file"
                 name="file"
                 className="form-control-file"
                 className="form-control-file"
-                accept=".growi.zip"
+                accept=".zip"
                 ref={this.inputRef}
                 ref={this.inputRef}
                 onChange={this.changeFileName}
                 onChange={this.changeFileName}
               />
               />

+ 21 - 14
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -8,7 +8,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
@@ -25,7 +25,7 @@ class LineBreakForm extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     try {
     try {
-      await this.props.markDownSettingContainer.updateLineBreakSetting();
+      await this.props.adminMarkDownContainer.updateLineBreakSetting();
       toastSuccess(t('markdown_setting.updated_lineBreak'));
       toastSuccess(t('markdown_setting.updated_lineBreak'));
     }
     }
     catch (err) {
     catch (err) {
@@ -35,8 +35,8 @@ class LineBreakForm extends React.Component {
   }
   }
 
 
   renderLineBreakOption() {
   renderLineBreakOption() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledLinebreaks } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaks } = adminMarkDownContainer.state;
 
 
     const helpLineBreak = { __html: t('markdown_setting.Enable Line Break desc') };
     const helpLineBreak = { __html: t('markdown_setting.Enable Line Break desc') };
 
 
@@ -48,10 +48,10 @@ class LineBreakForm extends React.Component {
               type="checkbox"
               type="checkbox"
               id="isEnabledLinebreaks"
               id="isEnabledLinebreaks"
               checked={isEnabledLinebreaks}
               checked={isEnabledLinebreaks}
-              onChange={() => { markDownSettingContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
             />
             />
             <label htmlFor="isEnabledLinebreaks">
             <label htmlFor="isEnabledLinebreaks">
-              { t('markdown_setting.Enable Line Break') }
+              {t('markdown_setting.Enable Line Break')}
             </label>
             </label>
           </div>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
@@ -61,8 +61,8 @@ class LineBreakForm extends React.Component {
   }
   }
 
 
   renderLineBreakInCommentOption() {
   renderLineBreakInCommentOption() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledLinebreaksInComments } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
 
 
     const helpLineBreakInComment = { __html: t('markdown_setting.Enable Line Break for comment desc') };
     const helpLineBreakInComment = { __html: t('markdown_setting.Enable Line Break for comment desc') };
 
 
@@ -74,10 +74,10 @@ class LineBreakForm extends React.Component {
               type="checkbox"
               type="checkbox"
               id="isEnabledLinebreaksInComments"
               id="isEnabledLinebreaksInComments"
               checked={isEnabledLinebreaksInComments}
               checked={isEnabledLinebreaksInComments}
-              onChange={() => { markDownSettingContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
             />
             />
             <label htmlFor="isEnabledLinebreaksInComments">
             <label htmlFor="isEnabledLinebreaksInComments">
-              { t('markdown_setting.Enable Line Break for comment') }
+              {t('markdown_setting.Enable Line Break for comment')}
             </label>
             </label>
           </div>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
@@ -87,7 +87,7 @@ class LineBreakForm extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { t } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
@@ -97,7 +97,14 @@ class LineBreakForm extends React.Component {
         </fieldset>
         </fieldset>
         <div className="form-group my-3">
         <div className="form-group my-3">
           <div className="col-xs-offset-4 col-xs-5">
           <div className="col-xs-offset-4 col-xs-5">
-            <button type="submit" className="btn btn-primary" onClick={this.onClickSubmit}>{ t('Update') }</button>
+            <button
+              type="submit"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={adminMarkDownContainer.state.retrieveError != null}
+            >
+              {t('Update')}
+            </button>
           </div>
           </div>
         </div>
         </div>
       </React.Fragment>
       </React.Fragment>
@@ -110,13 +117,13 @@ class LineBreakForm extends React.Component {
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const LineBreakFormWrapper = (props) => {
 const LineBreakFormWrapper = (props) => {
-  return createSubscribedElement(LineBreakForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(LineBreakForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 LineBreakForm.propTypes = {
 LineBreakForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(LineBreakFormWrapper);
 export default withTranslation()(LineBreakFormWrapper);

+ 22 - 1
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -2,15 +2,35 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import loggerFactory from '@alias/logger';
+
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import LineBreakForm from './LineBreakForm';
 import LineBreakForm from './LineBreakForm';
 import PresentationForm from './PresentationForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 import XssForm from './XssForm';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+
+const logger = loggerFactory('growi:MarkDown');
 
 
 class MarkdownSetting extends React.Component {
 class MarkdownSetting extends React.Component {
 
 
+  async componentDidMount() {
+    const { adminMarkDownContainer } = this.props;
+
+    try {
+      await adminMarkDownContainer.retrieveMarkdownData();
+    }
+    catch (err) {
+      toastError(err);
+      adminMarkDownContainer.setState({ retrieveError: err });
+      logger.error(err);
+    }
+
+  }
+
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
 
 
@@ -43,12 +63,13 @@ class MarkdownSetting extends React.Component {
 }
 }
 
 
 const MarkdownSettingWrapper = (props) => {
 const MarkdownSettingWrapper = (props) => {
-  return createSubscribedElement(MarkdownSetting, props, [AppContainer]);
+  return createSubscribedElement(MarkdownSetting, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 MarkdownSetting.propTypes = {
 MarkdownSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 
 };
 };
 
 

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

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 const logger = loggerFactory('growi:markdown:presentation');
 const logger = loggerFactory('growi:markdown:presentation');
 
 
@@ -23,7 +23,7 @@ class PresentationForm extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     try {
     try {
-      await this.props.markDownSettingContainer.updatePresentationSetting();
+      await this.props.adminMarkDownContainer.updatePresentationSetting();
       toastSuccess(t('markdown_setting.updated_presentation'));
       toastSuccess(t('markdown_setting.updated_presentation'));
     }
     }
     catch (err) {
     catch (err) {
@@ -34,8 +34,8 @@ class PresentationForm extends React.Component {
 
 
 
 
   render() {
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { pageBreakSeparator, pageBreakCustomSeparator } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
 
 
     return (
     return (
       <fieldset className="form-group row my-2">
       <fieldset className="form-group row my-2">
@@ -49,7 +49,7 @@ class PresentationForm extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption1"
             id="pageBreakOption1"
             checked={pageBreakSeparator === 1}
             checked={pageBreakSeparator === 1}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(1)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
           />
           />
           <label htmlFor="pageBreakOption1">
           <label htmlFor="pageBreakOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
@@ -65,7 +65,7 @@ class PresentationForm extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption2"
             id="pageBreakOption2"
             checked={pageBreakSeparator === 2}
             checked={pageBreakSeparator === 2}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(2)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
           />
           />
           <label htmlFor="pageBreakOption2">
           <label htmlFor="pageBreakOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
@@ -81,7 +81,7 @@ class PresentationForm extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption3"
             id="pageBreakOption3"
             checked={pageBreakSeparator === 3}
             checked={pageBreakSeparator === 3}
-            onChange={() => markDownSettingContainer.switchPageBreakSeparator(3)}
+            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
           />
           />
           <label htmlFor="pageBreakOption3">
           <label htmlFor="pageBreakOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
@@ -89,8 +89,8 @@ class PresentationForm extends React.Component {
               { t('markdown_setting.Custom separator desc') }
               { t('markdown_setting.Custom separator desc') }
               <input
               <input
                 className="form-control"
                 className="form-control"
-                value={pageBreakCustomSeparator}
-                onChange={(e) => { markDownSettingContainer.setPageBreakCustomSeparator(e.target.value) }}
+                defaultValue={pageBreakCustomSeparator}
+                onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
               />
               />
             </div>
             </div>
           </label>
           </label>
@@ -98,7 +98,7 @@ class PresentationForm extends React.Component {
 
 
         <div className="form-group my-3">
         <div className="form-group my-3">
           <div className="col-xs-offset-4 col-xs-5">
           <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>
         </div>
         </div>
 
 
@@ -109,13 +109,13 @@ class PresentationForm extends React.Component {
 }
 }
 
 
 const PresentationFormWrapper = (props) => {
 const PresentationFormWrapper = (props) => {
-  return createSubscribedElement(PresentationForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(PresentationForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 PresentationForm.propTypes = {
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 
 };
 };
 
 

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

@@ -5,13 +5,13 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 class PresentationLineBreakOptions extends React.Component {
 class PresentationLineBreakOptions extends React.Component {
 
 
   render() {
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { pageBreakOption, customRegularExpression } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { pageBreakOption, customRegularExpression } = adminMarkDownContainer.state;
 
 
     return (
     return (
       <Fragment>
       <Fragment>
@@ -20,7 +20,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption1"
             id="pageBreakOption1"
             checked={pageBreakOption === 1}
             checked={pageBreakOption === 1}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 1 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 1 }) }}
           />
           />
           <label htmlFor="pageBreakOption1">
           <label htmlFor="pageBreakOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
@@ -36,7 +36,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption2"
             id="pageBreakOption2"
             checked={pageBreakOption === 2}
             checked={pageBreakOption === 2}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 2 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 2 }) }}
           />
           />
           <label htmlFor="pageBreakOption2">
           <label htmlFor="pageBreakOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
@@ -52,7 +52,7 @@ class PresentationLineBreakOptions extends React.Component {
             type="radio"
             type="radio"
             id="pageBreakOption3"
             id="pageBreakOption3"
             checked={pageBreakOption === 3}
             checked={pageBreakOption === 3}
-            onChange={() => { markDownSettingContainer.setState({ pageBreakOption: 3 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 3 }) }}
           />
           />
           <label htmlFor="pageBreakOption3">
           <label htmlFor="pageBreakOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
@@ -60,8 +60,8 @@ class PresentationLineBreakOptions extends React.Component {
               { t('markdown_setting.Custom separator desc') }
               { t('markdown_setting.Custom separator desc') }
               <input
               <input
                 className="form-control"
                 className="form-control"
-                value={customRegularExpression}
-                onChange={(e) => { markDownSettingContainer.setState({ customRegularExpression: e.target.value }) }}
+                defaultValue={customRegularExpression}
+                onChange={(e) => { adminMarkDownContainer.setState({ customRegularExpression: e.target.value }) }}
               />
               />
             </div>
             </div>
           </label>
           </label>
@@ -73,13 +73,13 @@ class PresentationLineBreakOptions extends React.Component {
 }
 }
 
 
 const PresentationLineBreakOptionsWrapper = (props) => {
 const PresentationLineBreakOptionsWrapper = (props) => {
-  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 PresentationLineBreakOptions.propTypes = {
 PresentationLineBreakOptions.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 
 };
 };
 
 

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

@@ -6,52 +6,52 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 class WhiteListInput extends React.Component {
 class WhiteListInput extends React.Component {
 
 
   renderRecommendTagBtn() {
   renderRecommendTagBtn() {
-    const { t, markDownSettingContainer } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
 
     return (
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ tagWhiteList: tags }) }}>
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ tagWhiteList: tags }) }}>
         { t('markdown_setting.import_recommended', 'tags') }
         { t('markdown_setting.import_recommended', 'tags') }
       </p>
       </p>
     );
     );
   }
   }
 
 
   renderRecommendAttrBtn() {
   renderRecommendAttrBtn() {
-    const { t, markDownSettingContainer } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
 
     return (
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ attrWhiteList: attrs }) }}>
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ attrWhiteList: attrs }) }}>
         { t('markdown_setting.import_recommended', 'Attrs') }
         { t('markdown_setting.import_recommended', 'Attrs') }
       </p>
       </p>
     );
     );
   }
   }
 
 
   renderTagValue() {
   renderTagValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
 
     if (customizable) {
     if (customizable) {
-      return markDownSettingContainer.state.tagWhiteList;
+      return adminMarkDownContainer.state.tagWhiteList;
     }
     }
 
 
     return tags;
     return tags;
   }
   }
 
 
   renderAttrValue() {
   renderAttrValue() {
-    const { customizable, markDownSettingContainer } = this.props;
+    const { customizable, adminMarkDownContainer } = this.props;
 
 
     if (customizable) {
     if (customizable) {
-      return markDownSettingContainer.state.attrWhiteList;
+      return adminMarkDownContainer.state.attrWhiteList;
     }
     }
 
 
     return attrs;
     return attrs;
   }
   }
 
 
   render() {
   render() {
-    const { t, customizable, markDownSettingContainer } = this.props;
+    const { t, customizable, adminMarkDownContainer } = this.props;
 
 
     return (
     return (
       <>
       <>
@@ -66,8 +66,8 @@ class WhiteListInput extends React.Component {
             rows="6"
             rows="6"
             cols="40"
             cols="40"
             readOnly={!customizable}
             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>
         <div className="m-t-15">
         <div className="m-t-15">
@@ -81,8 +81,8 @@ class WhiteListInput extends React.Component {
             rows="6"
             rows="6"
             cols="40"
             cols="40"
             readOnly={!customizable}
             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>
         </div>
       </>
       </>
@@ -92,13 +92,13 @@ class WhiteListInput extends React.Component {
 }
 }
 
 
 const WhiteListWrapper = (props) => {
 const WhiteListWrapper = (props) => {
-  return createSubscribedElement(WhiteListInput, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(WhiteListInput, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 WhiteListInput.propTypes = {
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 
   customizable: PropTypes.bool.isRequired,
   customizable: PropTypes.bool.isRequired,
 };
 };

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

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 import WhiteListInput from './WhiteListInput';
 import WhiteListInput from './WhiteListInput';
 
 
@@ -25,7 +25,7 @@ class XssForm extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     try {
     try {
-      await this.props.markDownSettingContainer.updateXssSetting();
+      await this.props.adminMarkDownContainer.updateXssSetting();
       toastSuccess(t('markdown_setting.updated_xss'));
       toastSuccess(t('markdown_setting.updated_xss'));
     }
     }
     catch (err) {
     catch (err) {
@@ -35,8 +35,8 @@ class XssForm extends React.Component {
   }
   }
 
 
   xssOptions() {
   xssOptions() {
-    const { t, markDownSettingContainer } = this.props;
-    const { xssOption } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { xssOption } = adminMarkDownContainer.state;
 
 
     return (
     return (
       <fieldset className="form-group col-xs-12 my-3">
       <fieldset className="form-group col-xs-12 my-3">
@@ -46,7 +46,7 @@ class XssForm extends React.Component {
             id="xssOption1"
             id="xssOption1"
             name="XssOption"
             name="XssOption"
             checked={xssOption === 1}
             checked={xssOption === 1}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 1 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
           />
           />
           <label htmlFor="xssOption1">
           <label htmlFor="xssOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
@@ -62,7 +62,7 @@ class XssForm extends React.Component {
             id="xssOption2"
             id="xssOption2"
             name="XssOption"
             name="XssOption"
             checked={xssOption === 2}
             checked={xssOption === 2}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 2 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
           />
           />
           <label htmlFor="xssOption2">
           <label htmlFor="xssOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
@@ -76,7 +76,7 @@ class XssForm extends React.Component {
             id="xssOption3"
             id="xssOption3"
             name="XssOption"
             name="XssOption"
             checked={xssOption === 3}
             checked={xssOption === 3}
-            onChange={() => { markDownSettingContainer.setState({ xssOption: 3 }) }}
+            onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
           />
           />
           <label htmlFor="xssOption3">
           <label htmlFor="xssOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
             <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
@@ -88,8 +88,8 @@ class XssForm extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { t, markDownSettingContainer } = this.props;
-    const { isEnabledXss } = markDownSettingContainer.state;
+    const { t, adminMarkDownContainer } = this.props;
+    const { isEnabledXss } = adminMarkDownContainer.state;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
@@ -103,7 +103,7 @@ class XssForm extends React.Component {
                   className="form-check-input"
                   className="form-check-input"
                   name="isEnabledXss"
                   name="isEnabledXss"
                   checked={isEnabledXss}
                   checked={isEnabledXss}
-                  onChange={markDownSettingContainer.switchEnableXss}
+                  onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 />
                 <label htmlFor="XssEnable">
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
                   { t('markdown_setting.Enable XSS prevention') }
@@ -114,7 +114,7 @@ class XssForm extends React.Component {
           </div>
           </div>
           <div className="form-group my-3">
           <div className="form-group my-3">
             <div className="col-xs-offset-4 col-xs-5">
             <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>
           </div>
           </div>
         </form>
         </form>
@@ -125,13 +125,13 @@ class XssForm extends React.Component {
 }
 }
 
 
 const XssFormWrapper = (props) => {
 const XssFormWrapper = (props) => {
-  return createSubscribedElement(XssForm, props, [AppContainer, MarkDownSettingContainer]);
+  return createSubscribedElement(XssForm, props, [AppContainer, AdminMarkDownContainer]);
 };
 };
 
 
 XssForm.propTypes = {
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(XssFormWrapper);
 export default withTranslation()(XssFormWrapper);

+ 1 - 1
src/client/js/components/PageEditor/HandsontableModal.jsx

@@ -412,7 +412,7 @@ export default class HandsontableModal extends React.PureComponent {
     const dialogClassName = dialogClassNames.join(' ');
     const dialogClassName = dialogClassNames.join(' ');
 
 
     return (
     return (
-      <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName}>
+      <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName} keyboard={false}>
         <Modal.Header closeButton>
         <Modal.Header closeButton>
           { this.renderExpandOrContractButton() }
           { this.renderExpandOrContractButton() }
           <Modal.Title>Edit Table</Modal.Title>
           <Modal.Title>Edit Table</Modal.Title>

+ 23 - 7
src/client/js/components/User/UserPicture.jsx

@@ -2,24 +2,27 @@ import React from 'react';
 import md5 from 'md5';
 import md5 from 'md5';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+const DEFAULT_IMAGE = '/images/icons/user.svg';
+
 // TODO UserComponent?
 // TODO UserComponent?
 export default class UserPicture extends React.Component {
 export default class UserPicture extends React.Component {
 
 
   getUserPicture(user) {
   getUserPicture(user) {
+    let pictPath;
+
     // gravatar
     // gravatar
     if (user.isGravatarEnabled === true) {
     if (user.isGravatarEnabled === true) {
-      return this.generateGravatarSrc(user);
+      pictPath = this.generateGravatarSrc(user);
     }
     }
     // uploaded image
     // uploaded image
     if (user.image != null) {
     if (user.image != null) {
-      return user.image;
+      pictPath = user.image;
     }
     }
     if (user.imageAttachment != null) {
     if (user.imageAttachment != null) {
       return user.imageAttachment.filePathProxied;
       return user.imageAttachment.filePathProxied;
     }
     }
 
 
-    return '/images/icons/user.svg';
-
+    return pictPath || DEFAULT_IMAGE;
   }
   }
 
 
   generateGravatarSrc(user) {
   generateGravatarSrc(user) {
@@ -38,9 +41,22 @@ export default class UserPicture extends React.Component {
     return className.join(' ');
     return className.join(' ');
   }
   }
 
 
+  renderForNull() {
+    return (
+      <img
+        src={DEFAULT_IMAGE}
+        alt="someone"
+        className={this.getClassName()}
+      />
+    );
+  }
+
   render() {
   render() {
     const user = this.props.user;
     const user = this.props.user;
-    const href = `/user/${user.username}`;
+
+    if (user == null) {
+      return this.renderForNull();
+    }
 
 
     const imgElem = (
     const imgElem = (
       <img
       <img
@@ -53,14 +69,14 @@ export default class UserPicture extends React.Component {
     return (
     return (
       (this.props.withoutLink)
       (this.props.withoutLink)
         ? <span>{imgElem}</span>
         ? <span>{imgElem}</span>
-        : <a href={href}>{imgElem}</a>
+        : <a href={`/user/${user.username}`}>{imgElem}</a>
     );
     );
   }
   }
 
 
 }
 }
 
 
 UserPicture.propTypes = {
 UserPicture.propTypes = {
-  user: PropTypes.object.isRequired,
+  user: PropTypes.object,
   size: PropTypes.string,
   size: PropTypes.string,
   withoutLink: PropTypes.bool,
   withoutLink: PropTypes.bool,
 };
 };

+ 9 - 1
src/client/js/components/User/Username.jsx

@@ -3,9 +3,17 @@ import PropTypes from 'prop-types';
 
 
 export default class Username extends React.Component {
 export default class Username extends React.Component {
 
 
+  renderForNull() {
+    return <span>anyone</span>;
+  }
+
   render() {
   render() {
     const { user } = this.props;
     const { user } = this.props;
 
 
+    if (user == null) {
+      return this.renderForNull();
+    }
+
     const name = user.name || '(no name)';
     const name = user.name || '(no name)';
     const username = user.username;
     const username = user.username;
     const href = `/user/${user.username}`;
     const href = `/user/${user.username}`;
@@ -18,5 +26,5 @@ export default class Username extends React.Component {
 }
 }
 
 
 Username.propTypes = {
 Username.propTypes = {
-  user: PropTypes.object.isRequired,
+  user: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), // Possibility of receiving a string of 'null'
 };
 };

+ 43 - 12
src/client/js/services/MarkDownSettingContainer.js → src/client/js/services/AdminMarkDownContainer.js

@@ -1,10 +1,16 @@
 import { Container } from 'unstated';
 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)
  * Service container for admin markdown setting page (MarkDownSetting.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
  */
  */
-export default class MarkDownSettingContainer extends Container {
+export default class AdminMarkDownContainer extends Container {
 
 
   constructor(appContainer) {
   constructor(appContainer) {
     super();
     super();
@@ -12,16 +18,15 @@ export default class MarkDownSettingContainer extends Container {
     this.appContainer = appContainer;
     this.appContainer = appContainer;
 
 
     this.state = {
     this.state = {
-      isEnabledLinebreaks: appContainer.config.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: appContainer.config.isEnabledLinebreaksInComments,
-      pageBreakSeparator: appContainer.config.pageBreakSeparator,
-      pageBreakCustomSeparator: appContainer.config.pageBreakCustomSeparator || '',
-      // pageBreakOption: appContainer.config.pageBreakOption,
-      customRegularExpression: appContainer.config.customRegularExpression || '',
-      isEnabledXss: (appContainer.config.xssOption != null),
-      xssOption: appContainer.config.xssOption,
-      tagWhiteList: appContainer.config.tagWhiteList || '',
-      attrWhiteList: appContainer.config.attrWhiteList || '',
+      retrieveError: null,
+      isEnabledLinebreaks: false,
+      isEnabledLinebreaksInComments: false,
+      pageBreakSeparator: 1,
+      pageBreakCustomSeparator: '',
+      isEnabledXss: false,
+      xssOption: 1,
+      tagWhiteList: '',
+      attrWhiteList: '',
     };
     };
 
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
     this.switchEnableXss = this.switchEnableXss.bind(this);
@@ -31,7 +36,33 @@ export default class MarkDownSettingContainer extends Container {
    * Workaround for the mangling in production build to break constructor.name
    * Workaround for the mangling in production build to break constructor.name
    */
    */
   static getClassName() {
   static getClassName() {
-    return 'MarkDownSettingContainer';
+    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'));
+    }
   }
   }
 
 
   /**
   /**

+ 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
+  },
+};

+ 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'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),
     securityPassportOidc: require('./admin/securityPassportOidc'),
     securityPassportOidc: require('./admin/securityPassportOidc'),
-    markdown: require('./admin/markdown'),
-    markdownXss: require('./admin/markdownXss'),
-    markdownPresentation: require('./admin/markdownPresentation'),
     customcss: require('./admin/customcss'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customscript: require('./admin/customscript'),
     customheader: require('./admin/customheader'),
     customheader: require('./admin/customheader'),

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

@@ -137,21 +137,6 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
-  // 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.get('/admin/customize' , admin.customize.index);
   // app.get('/admin/customize' , admin.customize.index);
   actions.customize = {};
   actions.customize = {};
   actions.customize.index = function(req, res) {
   actions.customize.index = function(req, res) {

+ 2 - 3
src/server/routes/apiv3/import.js

@@ -5,9 +5,6 @@ const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-lin
 const path = require('path');
 const path = require('path');
 const multer = require('multer');
 const multer = require('multer');
 
 
-// eslint-disable-next-line no-unused-vars
-const { ObjectId } = require('mongoose').Types;
-
 const express = require('express');
 const express = require('express');
 
 
 const GrowiArchiveImportOption = require('@commons/models/admin/growi-archive-import-option');
 const GrowiArchiveImportOption = require('@commons/models/admin/growi-archive-import-option');
@@ -54,6 +51,8 @@ const generateOverwriteParams = (collectionName, req, options) => {
       return require('./overwrite-params/pages')(req, options);
       return require('./overwrite-params/pages')(req, options);
     case 'revisions':
     case 'revisions':
       return require('./overwrite-params/revisions')(req, options);
       return require('./overwrite-params/revisions')(req, options);
+    case 'attachmentFiles.chunks':
+      return require('./overwrite-params/attachmentFiles.chunks')(req, options);
     default:
     default:
       return {};
       return {};
   }
   }

+ 78 - 74
src/server/routes/apiv3/markdown-setting.js

@@ -38,45 +38,47 @@ const validator = {
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
- *      LineBreakParams:
+ *      CustomizeParams:
  *        type: object
  *        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
+ *          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) => {
 module.exports = (crowi) => {
@@ -86,6 +88,38 @@ module.exports = (crowi) => {
 
 
   const { ApiV3FormValidator } = crowi.middlewares;
   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
    * @swagger
    *
    *
@@ -100,12 +134,7 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                type: object
    *                type: object
    *                properties:
    *                properties:
-   *                  isEnabledLinebreaks:
-   *                    description: enable lineBreak
-   *                    type: boolean
-   *                  isEnabledLinebreaksInComments:
-   *                    description: enable lineBreak in comment
-   *                    type: boolean
+   *                  $ref: '#/components/schemas/CustomizeParams/LineBreakParams'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to update lineBreak setting
    *            description: Succeeded to update lineBreak setting
@@ -113,8 +142,7 @@ module.exports = (crowi) => {
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/LineBreakParams'
+   *                    $ref: '#/components/schemas/CustomizeParams/LineBreakParams'
    */
    */
   router.put('/lineBreak', loginRequiredStrictly, adminRequired, csrf, validator.lineBreak, ApiV3FormValidator, async(req, res) => {
   router.put('/lineBreak', loginRequiredStrictly, adminRequired, csrf, validator.lineBreak, ApiV3FormValidator, async(req, res) => {
 
 
@@ -153,12 +181,7 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                type: object
    *                type: object
    *                properties:
    *                properties:
-   *                  pageBreakSeparator:
-   *                    description: number of pageBreakSeparator
-   *                    type: number
-   *                  pageBreakCustomSeparator:
-   *                    description: string of pageBreakCustomSeparator
-   *                    type: string
+   *                  $ref: '#/components/schemas/CustomizeParams/PresentationParams'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to update presentation setting
    *            description: Succeeded to update presentation setting
@@ -166,8 +189,7 @@ module.exports = (crowi) => {
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/PresentationParams'
+   *                    $ref: '#/components/schemas/CustomizeParams/PresentationParams'
    */
    */
   router.put('/presentation', loginRequiredStrictly, adminRequired, csrf, validator.presentationSetting, ApiV3FormValidator, async(req, res) => {
   router.put('/presentation', loginRequiredStrictly, adminRequired, csrf, validator.presentationSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
     if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
@@ -209,24 +231,7 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                type: object
    *                type: object
    *                properties:
    *                properties:
-   *                  isEnabledPrevention:
-   *                    description: enable xss
-   *                    type: boolean
-   *                  xssOption:
-   *                    description: number of xss option
-   *                    type: number
-   *                  tagWhiteList:
-   *                    description: array of tag whiteList
-   *                    type: array
-   *                    items:
-   *                      type: string
-   *                      description: tag whitelist
-   *                  attrWhiteList:
-   *                    description: array of attr whiteList
-   *                    type: array
-   *                    items:
-   *                      type: string
-   *                      description: attr whitelist
+   *                  $ref: '#/components/schemas/CustomizeParams/XssParams'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to update xss setting
    *            description: Succeeded to update xss setting
@@ -234,8 +239,7 @@ module.exports = (crowi) => {
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/XssParams'
+   *                    $ref: '#/components/schemas/CustomizeParams/XssParams'
    */
    */
   router.put('/xss', loginRequiredStrictly, adminRequired, csrf, validator.xssSetting, ApiV3FormValidator, async(req, res) => {
   router.put('/xss', loginRequiredStrictly, adminRequired, csrf, validator.xssSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.isEnabledXss && req.body.xssOption == null) {
     if (req.body.isEnabledXss && req.body.xssOption == null) {

+ 2 - 1
src/server/routes/apiv3/mongo.js

@@ -34,7 +34,8 @@ module.exports = (crowi) => {
    *                      type: string
    *                      type: string
    */
    */
   router.get('/collections', async(req, res) => {
   router.get('/collections', async(req, res) => {
-    const collections = Object.keys(mongoose.connection.collections);
+    const listCollectionsResult = await mongoose.connection.db.listCollections().toArray();
+    const collections = listCollectionsResult.map(collectionObj => collectionObj.name);
 
 
     // TODO: use res.apiv3
     // TODO: use res.apiv3
     return res.json({
     return res.json({

+ 32 - 0
src/server/routes/apiv3/overwrite-params/attachmentFiles.chunks.js

@@ -0,0 +1,32 @@
+const { Binary } = require('mongodb');
+const { ObjectId } = require('mongoose').Types;
+
+class AttachmentFilesChunksOverwriteParamsFactory {
+
+  /**
+   * generate overwrite params object
+   * @param {object} req
+   * @param {ImportOptionForPages} option
+   * @return object
+   *  key: property name
+   *  value: any value or a function `(value, { document, schema, propertyName }) => { return newValue }`
+   */
+  static generate(req, option) {
+    const params = {};
+
+    // Date
+    params.files_id = (value, { document, schema, propertyName }) => {
+      return ObjectId(value);
+    };
+
+    // Binary
+    params.data = (value, { document, schema, propertyName }) => {
+      return Binary(value);
+    };
+
+    return params;
+  }
+
+}
+
+module.exports = (req, option) => AttachmentFilesChunksOverwriteParamsFactory.generate(req, option);

+ 1 - 1
src/server/routes/attachment.js

@@ -182,7 +182,7 @@ module.exports = function(crowi, app) {
 
 
     const attachment = await Attachment.findOne({ filePath });
     const attachment = await Attachment.findOne({ filePath });
 
 
-    return responseForAttachment(res, req.user, attachment);
+    return responseForAttachment(req, res, attachment);
   };
   };
 
 
   /**
   /**

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

@@ -573,7 +573,7 @@ module.exports = function(crowi, app) {
    */
    */
   api.create = async function(req, res) {
   api.create = async function(req, res) {
     const body = req.body.body || null;
     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 grant = req.body.grant || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || 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.'));
       return res.json(ApiResponse.error('Parameters body and path are required.'));
     }
     }
 
 
+    // check whether path starts slash
+    pagePath = pathUtils.addHeadingSlash(pagePath);
+
     // check page existence
     // check page existence
     const isExist = await Page.count({ path: pagePath }) > 0;
     const isExist = await Page.count({ path: pagePath }) > 0;
     if (isExist) {
     if (isExist) {
@@ -1064,7 +1067,7 @@ module.exports = function(crowi, app) {
   api.rename = async function(req, res) {
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
     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 = {
     const options = {
       createRedirectPage: (req.body.create_redirect != null),
       createRedirectPage: (req.body.create_redirect != null),
       updateMetadata: (req.body.remain_metadata == 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'));
       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;
     const isExist = await Page.count({ path: newPagePath }) > 0;
     if (isExist) {
     if (isExist) {
       // if page found, cannot cannot rename to that path
       // if page found, cannot cannot rename to that path
@@ -1130,7 +1136,7 @@ module.exports = function(crowi, app) {
    */
    */
   api.duplicate = async function(req, res) {
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
     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);
     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'));
       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();
     await page.populateDataToShowRevision();
     const originTags = await page.findRelatedTagsById();
     const originTags = await page.findRelatedTagsById();
 
 

+ 4 - 0
src/server/service/export.js

@@ -139,6 +139,10 @@ class ExportService {
         callback();
         callback();
       },
       },
       final(callback) {
       final(callback) {
+        // write beginning brace
+        if (isFirst) {
+          this.push('[');
+        }
         // write ending brace
         // write ending brace
         this.push(']');
         this.push(']');
         callback();
         callback();

+ 4 - 23
src/server/service/growi-bridge.js

@@ -11,26 +11,9 @@ const unzipper = require('unzipper');
 class GrowiBridgeService {
 class GrowiBridgeService {
 
 
   constructor(crowi) {
   constructor(crowi) {
+    this.crowi = crowi;
     this.encoding = 'utf-8';
     this.encoding = 'utf-8';
     this.metaFileName = 'meta.json';
     this.metaFileName = 'meta.json';
-
-    // { pages: Page, users: User, ... }
-    this.collectionMap = {};
-    this.initCollectionMap(crowi.models);
-  }
-
-  /**
-   * initialize collection map
-   *
-   * @memberOf GrowiBridgeService
-   * @param {object} models from models/index.js
-   */
-  initCollectionMap(models) {
-    for (const model of Object.values(models)) {
-      if (model.collection != null) {
-        this.collectionMap[model.collection.name] = model;
-      }
-    }
   }
   }
 
 
   /**
   /**
@@ -61,11 +44,9 @@ class GrowiBridgeService {
    * @return {object} instance of mongoose model
    * @return {object} instance of mongoose model
    */
    */
   getModelFromCollectionName(collectionName) {
   getModelFromCollectionName(collectionName) {
-    const Model = this.collectionMap[collectionName];
-
-    if (Model == null) {
-      throw new Error(`cannot find a model for collection name "${collectionName}"`);
-    }
+    const Model = Object.values(this.crowi.models).find((m) => {
+      return m.collection != null && m.collection.name === collectionName;
+    });
 
 
     return Model;
     return Model;
   }
   }

+ 51 - 26
src/server/service/import.js

@@ -2,12 +2,17 @@ const logger = require('@alias/logger')('growi:services:ImportService'); // esli
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 
 
+const isIsoDate = require('is-iso-date');
+const parseISO = require('date-fns/parseISO');
+
 const { Writable, Transform } = require('stream');
 const { Writable, Transform } = require('stream');
 const JSONStream = require('JSONStream');
 const JSONStream = require('JSONStream');
 const streamToPromise = require('stream-to-promise');
 const streamToPromise = require('stream-to-promise');
 const unzipper = require('unzipper');
 const unzipper = require('unzipper');
 
 
-const { ObjectId } = require('mongoose').Types;
+const mongoose = require('mongoose');
+
+const { ObjectId } = mongoose.Types;
 
 
 const { createBatchStream } = require('../util/batch-stream');
 const { createBatchStream } = require('../util/batch-stream');
 const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
 const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
@@ -90,16 +95,27 @@ class ImportService {
    *
    *
    * @memberOf ImportService
    * @memberOf ImportService
    * @param {any} value value from imported document
    * @param {any} value value from imported document
-   * @param {{ document: object, schema: object, key: string }}
+   * @param {{ document: object, schema: object, propertyName: string }}
    * @return {any} new value for the document
    * @return {any} new value for the document
    */
    */
   keepOriginal(value, { document, schema, propertyName }) {
   keepOriginal(value, { document, schema, propertyName }) {
-    let _value;
-    if (schema[propertyName].instance === 'ObjectID' && ObjectId.isValid(value)) {
+    let _value = value;
+
+    // _id
+    if (propertyName === '_id' && ObjectId.isValid(value)) {
       _value = ObjectId(value);
       _value = ObjectId(value);
     }
     }
-    else {
-      _value = value;
+    // Date
+    else if (isIsoDate(value)) {
+      _value = parseISO(value);
+    }
+
+    // Model
+    if (schema != null) {
+      // ObjectID
+      if (schema[propertyName] != null && schema[propertyName].instance === 'ObjectID' && ObjectId.isValid(value)) {
+        _value = ObjectId(value);
+      }
     }
     }
 
 
     return _value;
     return _value;
@@ -177,18 +193,20 @@ class ImportService {
     const execUnorderedBulkOpSafely = this.execUnorderedBulkOpSafely.bind(this);
     const execUnorderedBulkOpSafely = this.execUnorderedBulkOpSafely.bind(this);
     const emitProgressEvent = this.emitProgressEvent.bind(this);
     const emitProgressEvent = this.emitProgressEvent.bind(this);
 
 
+    const collection = mongoose.connection.collection(collectionName);
+
     const { mode, jsonFileName, overwriteParams } = importSettings;
     const { mode, jsonFileName, overwriteParams } = importSettings;
-    const Model = this.growiBridgeService.getModelFromCollectionName(collectionName);
-    const jsonFile = this.getFile(jsonFileName);
     const collectionProgress = this.currentProgressingStatus.progressMap[collectionName];
     const collectionProgress = this.currentProgressingStatus.progressMap[collectionName];
 
 
     try {
     try {
+      const jsonFile = this.getFile(jsonFileName);
+
       // validate options
       // validate options
       this.validateImportSettings(collectionName, importSettings);
       this.validateImportSettings(collectionName, importSettings);
 
 
       // flush
       // flush
       if (mode === 'flushAndInsert') {
       if (mode === 'flushAndInsert') {
-        await Model.remove({});
+        await collection.deleteMany({});
       }
       }
 
 
       // stream 1
       // stream 1
@@ -214,7 +232,7 @@ class ImportService {
       const writeStream = new Writable({
       const writeStream = new Writable({
         objectMode: true,
         objectMode: true,
         async write(batch, encoding, callback) {
         async write(batch, encoding, callback) {
-          const unorderedBulkOp = Model.collection.initializeUnorderedBulkOp();
+          const unorderedBulkOp = collection.initializeUnorderedBulkOp();
 
 
           // documents are not persisted until unorderedBulkOp.execute()
           // documents are not persisted until unorderedBulkOp.execute()
           batch.forEach((document) => {
           batch.forEach((document) => {
@@ -363,7 +381,8 @@ class ImportService {
     }
     }
     catch (err) {
     catch (err) {
       result = err.result;
       result = err.result;
-      errors = err.writeErrors.map((err) => {
+      errors = err.writeErrors || [err];
+      errors.map((err) => {
         const moreDetailErr = err.err;
         const moreDetailErr = err.err;
         return { _id: moreDetailErr.op._id, message: err.errmsg };
         return { _id: moreDetailErr.op._id, message: err.errmsg };
       });
       });
@@ -390,27 +409,33 @@ class ImportService {
    */
    */
   convertDocuments(collectionName, document, overwriteParams) {
   convertDocuments(collectionName, document, overwriteParams) {
     const Model = this.growiBridgeService.getModelFromCollectionName(collectionName);
     const Model = this.growiBridgeService.getModelFromCollectionName(collectionName);
-    const schema = Model.schema.paths;
+    const schema = (Model != null) ? Model.schema.paths : null;
     const convertMap = this.convertMap[collectionName];
     const convertMap = this.convertMap[collectionName];
 
 
-    if (convertMap == null) {
-      throw new Error(`attribute map is not defined for ${collectionName}`);
-    }
-
     const _document = {};
     const _document = {};
 
 
-    // assign value from documents being imported
-    Object.entries(convertMap).forEach(([propertyName, convertedValue]) => {
-      const value = document[propertyName];
+    // not Mongoose Model
+    if (convertMap == null) {
+      // apply keepOriginal to all of properties
+      Object.entries(document).forEach(([propertyName, value]) => {
+        _document[propertyName] = this.keepOriginal(value, { document, propertyName });
+      });
+    }
+    // Mongoose Model
+    else {
+      // assign value from documents being imported
+      Object.entries(convertMap).forEach(([propertyName, convertedValue]) => {
+        const value = document[propertyName];
 
 
-      // distinguish between null and undefined
-      if (value === undefined) {
-        return; // next entry
-      }
+        // distinguish between null and undefined
+        if (value === undefined) {
+          return; // next entry
+        }
 
 
-      const convertFunc = (typeof convertedValue === 'function') ? convertedValue : null;
-      _document[propertyName] = (convertFunc != null) ? convertFunc(value, { document, propertyName, schema }) : convertedValue;
-    });
+        const convertFunc = (typeof convertedValue === 'function') ? convertedValue : null;
+        _document[propertyName] = (convertFunc != null) ? convertFunc(value, { document, propertyName, schema }) : convertedValue;
+      });
+    }
 
 
     // overwrite documents with custom values
     // overwrite documents with custom values
     Object.entries(overwriteParams).forEach(([propertyName, overwriteValue]) => {
     Object.entries(overwriteParams).forEach(([propertyName, overwriteValue]) => {

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

@@ -91,8 +91,8 @@ class ElasticsearchDelegator {
       host = `${url.protocol}//${url.host}`;
       host = `${url.protocol}//${url.host}`;
       indexName = url.pathname.substring(1); // omit heading slash
       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}`;
       }
       }
     }
     }
 
 

+ 0 - 187
wercker.yml

@@ -1,187 +0,0 @@
-box: node:12-slim
-
-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: yarn lint
-      code: |
-        yarn lint
-
-    - script:
-      name: yarn test
-      code: |
-        export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi_test
-        echo "export MONGO_URI=$MONGO_URI"
-        yarn 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: yarn build:prod:analyze
-      code: |
-        yarn build:prod:analyze
-
-    - script:
-      name: shrink dependencies for production
-      code: |
-        yarn install --production
-
-    - script:
-      name: yarn server:prod:ci
-      code: |
-        export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi
-        echo "export MONGO_URI=$MONGO_URI"
-        yarn 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: yarn build:dev
-      code: |
-        yarn 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"
-

+ 5 - 0
yarn.lock

@@ -6616,6 +6616,11 @@ is-installed-globally@^0.1.0:
     global-dirs "^0.1.0"
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
     is-path-inside "^1.0.0"
 
 
+is-iso-date@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/is-iso-date/-/is-iso-date-0.0.1.tgz#d1727b0a4f40cf4dd0dbf95a56a58cc991bb76e2"
+  integrity sha1-0XJ7Ck9Az03Q2/laVqWMyZG7duI=
+
 is-npm@^1.0.0:
 is-npm@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"