Browse Source

Merge remote-tracking branch 'origin/reactify-admin/CustomizePage' into create-customize-title

sooouh 6 years ago
parent
commit
6cca3eba5b
39 changed files with 1150 additions and 525 deletions
  1. 30 0
      .github/workflows/main.yml
  2. 14 0
      CHANGES.md
  3. 28 83
      README.md
  4. 6 0
      bin/github-actions/bump-version.sh
  5. 80 0
      docker/Dockerfile
  6. 142 0
      docker/README.md
  7. 21 0
      docker/bin/remove-unnecessary-files.sh
  8. 9 0
      docker/bin/symlink-for-uploading-to-local.sh
  9. 5 0
      docker/nocdn/env.prod.js
  10. 6 3
      resource/locales/en-US/translation.json
  11. 4 2
      resource/locales/ja/translation.json
  12. 122 0
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  13. 15 83
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  14. 122 0
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  15. 86 0
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  16. 15 5
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  17. 24 4
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  18. 65 0
      src/client/js/services/MarkDownSettingContainer.js
  19. 0 1
      src/client/styles/agile-admin/inverse/colors/antarctic.scss
  20. 33 0
      src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  21. 2 0
      src/server/models/config.js
  22. 1 1
      src/server/models/page-tag-relation.js
  23. 0 39
      src/server/routes/admin.js
  24. 5 6
      src/server/routes/apiv3/healthcheck.js
  25. 251 5
      src/server/routes/apiv3/markdown-setting.js
  26. 1 4
      src/server/routes/index.js
  27. 7 10
      src/server/routes/page.js
  28. 6 0
      src/server/service/config-loader.js
  29. 1 1
      src/server/service/file-uploader/aws.js
  30. 3 2
      src/server/service/file-uploader/gcs.js
  31. 1 1
      src/server/service/file-uploader/gridfs.js
  32. 1 1
      src/server/service/file-uploader/local.js
  33. 4 0
      src/server/service/file-uploader/uploader.js
  34. 2 1
      src/server/service/global-notification/global-notification-mail.js
  35. 24 13
      src/server/service/search-delegator/elasticsearch.js
  36. 1 1
      src/server/service/search.js
  37. 1 1
      src/server/util/swigFunctions.js
  38. 2 248
      src/server/views/admin/markdown.html
  39. 10 10
      wercker.yml

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

@@ -0,0 +1,30 @@
+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 .

+ 14 - 0
CHANGES.md

@@ -14,10 +14,24 @@ 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
+* Fix: Appending tag is failed by wrong index of PageTagRelation
+    * Introduced by 3.5.20
 * Support: Support Node.js v12
 * Support: Support Node.js v12
 * Support: Upgrade libs
 * Support: Upgrade libs
     * growi-commons
     * growi-commons
 
 
+## 3.5.23
+
+* Fix: Global Notification failed to send e-mail
+* Fix: Pagination is not working for trash list
+* Fix: Healthcheck API with `?connectToMiddlewares` returns error
+* Support: Upgrade libs
+    * growi-commons
+
+## 3.5.22
+
+* Improvement: Add `FILE_UPLOAD_DISABLED` env var
+
 ## 3.5.21
 ## 3.5.21
 
 
 * Improvement: Cache control when retrieving attachment data
 * Improvement: Cache control when retrieving attachment data

+ 28 - 83
README.md

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

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

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

+ 80 - 0
docker/Dockerfile

@@ -0,0 +1,80 @@
+# syntax = docker/dockerfile:experimental
+
+ARG flavor=default
+
+
+
+##
+## setupper-default
+##
+FROM node:12-slim AS setupper-default
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+RUN mkdir -p ${appDir}
+RUN mv .* ${appDir}/
+
+WORKDIR ${appDir}
+
+# setup
+RUN yarn config set network-timeout 300000
+RUN yarn
+# install official plugins
+RUN yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+# install peerDependencies
+RUN yarn add -D react-images react-motion
+
+
+
+##
+## setupper-nocdn
+##
+FROM setupper-default AS setupper-nocdn
+
+# replace env.prod.js for NO_CDN
+COPY nocdn/env.prod.js config
+
+
+
+##
+## builder
+##
+FROM setupper-${flavor} AS builder
+
+ENV appDir /opt/growi
+
+# build
+RUN yarn build:prod
+# shrink dependencies for production
+RUN yarn install --production
+
+# remove unnecessary files
+WORKDIR /tmp
+RUN --mount=target=. sh bin/remove-unnecessary-files.sh
+WORKDIR ${appDir}
+
+
+
+##
+## release
+##
+FROM node:12-alpine
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV appDir /opt/growi
+
+# install tini
+RUN apk add --no-cache tini
+
+COPY --from=builder ${appDir} ${appDir}
+
+# create symlink for FILE_UPLOAD=local
+WORKDIR /tmp
+RUN --mount=target=. sh bin/symlink-for-uploading-to-local.sh
+WORKDIR ${appDir}
+
+USER node
+VOLUME /data
+EXPOSE 3000
+
+ENTRYPOINT ["/sbin/tini", "-e", "143", "--"]
+CMD ["yarn", "server:prod"]

+ 142 - 0
docker/README.md

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

+ 21 - 0
docker/bin/remove-unnecessary-files.sh

@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+rm -rf \
+  ${appDir}/.github \
+  ${appDir}/.vscode \
+  ${appDir}/bin \
+  ${appDir}/docker \
+  ${appDir}/src/client \
+  ${appDir}/src/linter-checker \
+  ${appDir}/src/test \
+  ${appDir}/.editorconfig \
+  ${appDir}/.eslint* \
+  ${appDir}/.gitignore \
+  ${appDir}/.prettier* \
+  ${appDir}/.stylelint* \
+  ${appDir}/app.json \
+  ${appDir}/babel.config.js \
+  ${appDir}/Procfile \
+  ${appDir}/wercker.yml

+ 9 - 0
docker/bin/symlink-for-uploading-to-local.sh

@@ -0,0 +1,9 @@
+#!/bin/sh
+
+set -e
+
+# Corresponds to `FILE_UPLOAD=local`
+mkdir -p /data/uploads
+if [ ! -e "$appDir/public/uploads" ]; then
+  ln -s /data/uploads $appDir/public/uploads
+fi

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

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

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

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

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

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

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

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

+ 15 - 83
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

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

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

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

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

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

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

@@ -10,16 +10,26 @@ import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer
 
 
 class WhiteListInput extends React.Component {
 class WhiteListInput extends React.Component {
 
 
-  renderRecommendBtn() {
-    const { t } = this.props;
+  renderRecommendTagBtn() {
+    const { t, markDownSettingContainer } = this.props;
 
 
     return (
     return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary">
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ tagWhiteList: tags }) }}>
         { t('markdown_setting.import_recommended', 'tags') }
         { t('markdown_setting.import_recommended', 'tags') }
       </p>
       </p>
     );
     );
   }
   }
 
 
+  renderRecommendAttrBtn() {
+    const { t, markDownSettingContainer } = this.props;
+
+    return (
+      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { markDownSettingContainer.setState({ attrWhiteList: attrs }) }}>
+        { t('markdown_setting.import_recommended', 'Attrs') }
+      </p>
+    );
+  }
+
   renderTagValue() {
   renderTagValue() {
     const { customizable, markDownSettingContainer } = this.props;
     const { customizable, markDownSettingContainer } = this.props;
 
 
@@ -48,7 +58,7 @@ class WhiteListInput extends React.Component {
         <div className="m-t-15">
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
             { t('markdown_setting.Tag names') }
             { t('markdown_setting.Tag names') }
-            {customizable && this.renderRecommendBtn()}
+            {customizable && this.renderRecommendTagBtn()}
           </div>
           </div>
           <textarea
           <textarea
             className="form-control xss-list"
             className="form-control xss-list"
@@ -63,7 +73,7 @@ class WhiteListInput extends React.Component {
         <div className="m-t-15">
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
             { t('markdown_setting.Tag attributes') }
             { t('markdown_setting.Tag attributes') }
-            {customizable && this.renderRecommendBtn()}
+            {customizable && this.renderRecommendAttrBtn()}
           </div>
           </div>
           <textarea
           <textarea
             className="form-control xss-list"
             className="form-control xss-list"

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

@@ -1,14 +1,18 @@
 import React from 'react';
 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 { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
 import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
 
 
 import WhiteListInput from './WhiteListInput';
 import WhiteListInput from './WhiteListInput';
 
 
+const logger = loggerFactory('growi:importer');
+
 class XssForm extends React.Component {
 class XssForm extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -18,7 +22,16 @@ class XssForm extends React.Component {
   }
   }
 
 
   async onClickSubmit() {
   async onClickSubmit() {
-    // TODO GW-303 create apiV3 of update setting
+    const { t } = this.props;
+
+    try {
+      await this.props.markDownSettingContainer.updateXssSetting();
+      toastSuccess(t('markdown_setting.updated_xss'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
   }
   }
 
 
   xssOptions() {
   xssOptions() {
@@ -82,9 +95,16 @@ class XssForm extends React.Component {
       <React.Fragment>
       <React.Fragment>
         <form className="row">
         <form className="row">
           <div className="form-group">
           <div className="form-group">
-            <div className="col-xs-4 text-right">
-              <div className="checkbox checkbox-success" onChange={markDownSettingContainer.switchEnableXss}>
-                <input type="checkbox" id="XssEnable" className="form-check-input" name="isEnabledXss" checked={isEnabledXss} />
+            <div className="col-xs-offset-4 col-xs-4 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  type="checkbox"
+                  id="XssEnable"
+                  className="form-check-input"
+                  name="isEnabledXss"
+                  checked={isEnabledXss}
+                  onChange={markDownSettingContainer.switchEnableXss}
+                />
                 <label htmlFor="XssEnable">
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
                   { t('markdown_setting.Enable XSS prevention') }
                 </label>
                 </label>

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

@@ -12,6 +12,12 @@ 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),
       isEnabledXss: (appContainer.config.xssOption != null),
       xssOption: appContainer.config.xssOption,
       xssOption: appContainer.config.xssOption,
       tagWhiteList: appContainer.config.tagWhiteList || '',
       tagWhiteList: appContainer.config.tagWhiteList || '',
@@ -28,6 +34,20 @@ export default class MarkDownSettingContainer extends Container {
     return 'MarkDownSettingContainer';
     return 'MarkDownSettingContainer';
   }
   }
 
 
+  /**
+   * Switch PageBreakSeparator
+   */
+  switchPageBreakSeparator(pageBreakSeparator) {
+    this.setState({ pageBreakSeparator });
+  }
+
+  /**
+   * Set PageBreakCustomSeparator
+   */
+  setPageBreakCustomSeparator(pageBreakCustomSeparator) {
+    this.setState({ pageBreakCustomSeparator });
+  }
+
   /**
   /**
    * Switch enableXss
    * Switch enableXss
    */
    */
@@ -38,4 +58,49 @@ export default class MarkDownSettingContainer extends Container {
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
   }
   }
 
 
+  /**
+   * Update LineBreak Setting
+   */
+  async updateLineBreakSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/lineBreak', {
+      isEnabledLinebreaks: this.state.isEnabledLinebreaks,
+      isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
+    });
+
+    return response;
+  }
+
+  /**
+   * Update Xss Setting
+   */
+  async updateXssSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
+      isEnabledXss: this.state.isEnabledXss,
+      xssOption: this.state.xssOption,
+      tagWhiteList: this.state.tagWhiteList,
+      attrWhiteList: this.state.attrWhiteList,
+    });
+
+    return response;
+  }
+
+  /**
+   * Update Presentation Setting
+   */
+  async updatePresentationSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/presentation', {
+      pageBreakSeparator: this.state.pageBreakSeparator,
+      pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
+    });
+
+    this.setState({
+      pageBreakSeparator: response.data.presentationParams.pageBreakSeparator,
+      pageBreakCustomSeparator: response.data.presentationParams.pageBreakCustomSeparator,
+    });
+    return response;
+  }
+
 }
 }

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

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

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

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

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

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

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

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

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

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

+ 5 - 6
src/server/routes/apiv3/healthcheck.js

@@ -25,7 +25,7 @@ module.exports = (crowi) => {
    *      parameters:
    *      parameters:
    *        - name: connectToMiddlewares
    *        - name: connectToMiddlewares
    *          in: query
    *          in: query
-   *          description: Check also MongoDB and Elasticsearch
+   *          description: Check also MongoDB and SearchService
    *          schema:
    *          schema:
    *            type: boolean
    *            type: boolean
    *      responses:
    *      responses:
@@ -38,14 +38,13 @@ module.exports = (crowi) => {
    *                  mongo:
    *                  mongo:
    *                    type: string
    *                    type: string
    *                    description: 'OK'
    *                    description: 'OK'
-   *                  esInfo:
+   *                  searchInfo:
    *                    type: object
    *                    type: object
-   *                    description: A result of `client.info()` of Elasticsearch Info API
    */
    */
   router.get('/', helmet.noCache(), async(req, res) => {
   router.get('/', helmet.noCache(), async(req, res) => {
     const connectToMiddlewares = req.query.connectToMiddlewares;
     const connectToMiddlewares = req.query.connectToMiddlewares;
 
 
-    // return 200 w/o connecting to MongoDB and Elasticsearch
+    // return 200 w/o connecting to MongoDB and SearchService
     if (connectToMiddlewares == null) {
     if (connectToMiddlewares == null) {
       res.status(200).send({ status: 'OK' });
       res.status(200).send({ status: 'OK' });
       return;
       return;
@@ -57,9 +56,9 @@ module.exports = (crowi) => {
       await Config.findOne({});
       await Config.findOne({});
       // connect to Elasticsearch
       // connect to Elasticsearch
       const search = crowi.getSearcher();
       const search = crowi.getSearcher();
-      const esInfo = await search.getInfo();
+      const searchInfo = await search.getInfo();
 
 
-      res.status(200).send({ mongo: 'OK', esInfo });
+      res.status(200).send({ mongo: 'OK', searchInfo });
     }
     }
     catch (err) {
     catch (err) {
       res.status(503).send({ err });
       res.status(503).send({ err });

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

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

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

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

+ 7 - 10
src/server/routes/page.js

@@ -72,9 +72,6 @@ module.exports = function(crowi, app) {
   }
   }
 
 
   function generatePager(offset, limit, totalCount) {
   function generatePager(offset, limit, totalCount) {
-    let next = null;
-
-
     let prev = null;
     let prev = null;
 
 
     if (offset > 0) {
     if (offset > 0) {
@@ -84,12 +81,10 @@ module.exports = function(crowi, app) {
       }
       }
     }
     }
 
 
-    if (totalCount < limit) {
+    let next = offset + limit;
+    if (totalCount < next) {
       next = null;
       next = null;
     }
     }
-    else {
-      next = offset + limit;
-    }
 
 
     return {
     return {
       prev,
       prev,
@@ -164,7 +159,7 @@ module.exports = function(crowi, app) {
 
 
     const queryOptions = {
     const queryOptions = {
       offset,
       offset,
-      limit: limit + 1,
+      limit,
       includeTrashed: path.startsWith('/trash/'),
       includeTrashed: path.startsWith('/trash/'),
       isRegExpEscapedFromPath,
       isRegExpEscapedFromPath,
     };
     };
@@ -469,13 +464,15 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.deletedPageListShow = async function(req, res) {
   actions.deletedPageListShow = async function(req, res) {
-    const path = `/trash${getPathFromRequest(req)}`;
+    // normalizePath makes '/trash/' -> '/trash'
+    const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
+
     const limit = 50;
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
     const offset = parseInt(req.query.offset) || 0;
 
 
     const queryOptions = {
     const queryOptions = {
       offset,
       offset,
-      limit: limit + 1,
+      limit,
       includeTrashed: true,
       includeTrashed: true,
     };
     };
 
 

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

@@ -130,6 +130,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.NUMBER,
     type:    TYPES.NUMBER,
     default: Infinity,
     default: Infinity,
   },
   },
+  FILE_UPLOAD_DISABLED: {
+    ns:      'crowi',
+    key:     'app:fileUploadDisabled',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
   ELASTICSEARCH_URI: {
   ELASTICSEARCH_URI: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'app:elasticsearchUri',
     key:     'app:elasticsearchUri',

+ 1 - 1
src/server/service/file-uploader/aws.js

@@ -49,7 +49,7 @@ module.exports = function(crowi) {
     return filePath;
     return filePath;
   }
   }
 
 
-  lib.getIsUploadable = function() {
+  lib.isValidUploadSettings = function() {
     return this.configManager.getConfig('crowi', 'aws:accessKeyId') != null
     return this.configManager.getConfig('crowi', 'aws:accessKeyId') != null
       && this.configManager.getConfig('crowi', 'aws:secretAccessKey') != null
       && this.configManager.getConfig('crowi', 'aws:secretAccessKey') != null
       && (
       && (

+ 3 - 2
src/server/service/file-uploader/gcs.js

@@ -38,8 +38,9 @@ module.exports = function(crowi) {
     return filePath;
     return filePath;
   }
   }
 
 
-  lib.getIsUploadable = function() {
-    return this.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath') != null && this.configManager.getConfig('crowi', 'gcs:bucket') != null;
+  lib.isValidUploadSettings = function() {
+    return this.configManager.getConfig('crowi', 'gcs:apiKeyJsonPath') != null
+      && this.configManager.getConfig('crowi', 'gcs:bucket') != null;
   };
   };
 
 
   lib.deleteFile = async function(attachment) {
   lib.deleteFile = async function(attachment) {

+ 1 - 1
src/server/service/file-uploader/gridfs.js

@@ -22,7 +22,7 @@ module.exports = function(crowi) {
   AttachmentFile.promisifiedWrite = util.promisify(AttachmentFile.write).bind(AttachmentFile);
   AttachmentFile.promisifiedWrite = util.promisify(AttachmentFile.write).bind(AttachmentFile);
   AttachmentFile.promisifiedUnlink = util.promisify(AttachmentFile.unlink).bind(AttachmentFile);
   AttachmentFile.promisifiedUnlink = util.promisify(AttachmentFile.unlink).bind(AttachmentFile);
 
 
-  lib.getIsUploadable = function() {
+  lib.isValidUploadSettings = function() {
     return true;
     return true;
   };
   };
 
 

+ 1 - 1
src/server/service/file-uploader/local.js

@@ -25,7 +25,7 @@ module.exports = function(crowi) {
     return filePath;
     return filePath;
   }
   }
 
 
-  lib.getIsUploadable = function() {
+  lib.isValidUploadSettings = function() {
     return true;
     return true;
   };
   };
 
 

+ 4 - 0
src/server/service/file-uploader/uploader.js

@@ -9,6 +9,10 @@ class Uploader {
   }
   }
 
 
   getIsUploadable() {
   getIsUploadable() {
+    return !this.configManager.getConfig('crowi', 'app:fileUploadDisabled') && this.isValidUploadSettings();
+  }
+
+  isValidUploadSettings() {
     throw new Error('Implement this');
     throw new Error('Implement this');
   }
   }
 
 

+ 2 - 1
src/server/service/global-notification/global-notification-mail.js

@@ -47,12 +47,13 @@ class GlobalNotificationMailService {
    * @return  {{ subject: string, template: string, vars: object }}
    * @return  {{ subject: string, template: string, vars: object }}
    */
    */
   generateOption(event, path, triggeredBy, { comment, oldPath }) {
   generateOption(event, path, triggeredBy, { comment, oldPath }) {
+    const defaultLang = this.crowi.configManager.getConfig('crowi', 'app:globalLang');
     // validate for all events
     // validate for all events
     if (event == null || path == null || triggeredBy == null) {
     if (event == null || path == null || triggeredBy == null) {
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
     }
     }
 
 
-    const template = nodePath.join(this.crowi.localeDir, `${this.defaultLang}/notifications/${event}.txt`);
+    const template = nodePath.join(this.crowi.localeDir, `${defaultLang}/notifications/${event}.txt`);
     let subject;
     let subject;
     let vars = {
     let vars = {
       appTitle: this.crowi.appService.getAppTitle(),
       appTitle: this.crowi.appService.getAppTitle(),

+ 24 - 13
src/server/service/search-delegator/elasticsearch.js

@@ -21,12 +21,8 @@ class ElasticsearchDelegator {
     this.configManager = configManager;
     this.configManager = configManager;
     this.searchEvent = searchEvent;
     this.searchEvent = searchEvent;
 
 
-    this.esNodeName = '-';
-    this.esNodeNames = [];
     this.esVersion = 'unknown';
     this.esVersion = 'unknown';
-    this.esVersions = [];
-    this.esPlugin = [];
-    this.esPlugins = [];
+    this.esNodeInfos = {};
 
 
     this.client = null;
     this.client = null;
 
 
@@ -72,6 +68,13 @@ class ElasticsearchDelegator {
     this.indexName = indexName;
     this.indexName = indexName;
   }
   }
 
 
+  getInfo() {
+    return {
+      esVersion: this.esVersion,
+      esNodeInfos: this.esNodeInfos,
+    };
+  }
+
   /**
   /**
    * return information object to connect to ES
    * return information object to connect to ES
    * @return {object} { host, httpAuth, indexName}
    * @return {object} { host, httpAuth, indexName}
@@ -158,18 +161,26 @@ class ElasticsearchDelegator {
    */
    */
   async checkESVersion() {
   async checkESVersion() {
     try {
     try {
-      const nodes = await this.client.nodes.info();
-      if (!nodes._nodes || !nodes.nodes) {
+      const info = await this.client.nodes.info();
+      if (!info._nodes || !info.nodes) {
         throw new Error('no nodes info');
         throw new Error('no nodes info');
       }
       }
 
 
-      for (const [nodeName, nodeInfo] of Object.entries(nodes.nodes)) {
-        this.esNodeName = nodeName;
-        this.esNodeNames.push(nodeName);
+      for (const [nodeName, nodeInfo] of Object.entries(info.nodes)) {
         this.esVersion = nodeInfo.version;
         this.esVersion = nodeInfo.version;
-        this.esVersions.push(nodeInfo.version);
-        this.esPlugin = nodeInfo.plugins;
-        this.esPlugins.push(nodeInfo.plugins);
+
+        const filteredInfo = {
+          name: nodeInfo.name,
+          version: nodeInfo.version,
+          plugins: nodeInfo.plugins.map((pluginInfo) => {
+            return {
+              name: pluginInfo.name,
+              version: pluginInfo.version,
+            };
+          }),
+        };
+
+        this.esNodeInfos[nodeName] = filteredInfo;
       }
       }
     }
     }
     catch (error) {
     catch (error) {

+ 1 - 1
src/server/service/search.js

@@ -65,7 +65,7 @@ class SearchService {
   }
   }
 
 
   getInfo() {
   getInfo() {
-    return this.delegator.info({});
+    return this.delegator.getInfo();
   }
   }
 
 
   async buildIndex() {
   async buildIndex() {

+ 1 - 1
src/server/util/swigFunctions.js

@@ -156,7 +156,7 @@ module.exports = function(crowi, req, locals) {
 
 
   locals.isTrashPage = function() {
   locals.isTrashPage = function() {
     const path = req.path || '';
     const path = req.path || '';
-    if (path.match(/^\/trash\/.*/)) {
+    if (path.match(/^\/trash(\/.*)?$/)) {
       return true;
       return true;
     }
     }
 
 

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

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

+ 10 - 10
wercker.yml

@@ -29,16 +29,16 @@ test:
         yarn list --depth=0
         yarn list --depth=0
 
 
     - script:
     - script:
-      name: npm run lint
+      name: yarn lint
       code: |
       code: |
-        npm run lint
+        yarn lint
 
 
     - script:
     - script:
-      name: npm test
+      name: yarn test
       code: |
       code: |
         export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi_test
         export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi_test
         echo "export MONGO_URI=$MONGO_URI"
         echo "export MONGO_URI=$MONGO_URI"
-        npm test
+        yarn test
 
 
   after-steps:
   after-steps:
     - slack-notifier:
     - slack-notifier:
@@ -55,9 +55,9 @@ build-prod:
       code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
       code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
 
 
     - script:
     - script:
-      name: npm run build:prod:analyze
+      name: yarn build:prod:analyze
       code: |
       code: |
-        npm run build:prod:analyze
+        yarn build:prod:analyze
 
 
     - script:
     - script:
       name: shrink dependencies for production
       name: shrink dependencies for production
@@ -65,11 +65,11 @@ build-prod:
         yarn install --production
         yarn install --production
 
 
     - script:
     - script:
-      name: npm run server:prod:ci
+      name: yarn server:prod:ci
       code: |
       code: |
         export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi
         export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi
         echo "export MONGO_URI=$MONGO_URI"
         echo "export MONGO_URI=$MONGO_URI"
-        npm run server:prod:ci
+        yarn server:prod:ci
 
 
   after-steps:
   after-steps:
     - script:
     - script:
@@ -91,9 +91,9 @@ build-dev:
       code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
       code: yarn config set cache-folder $WERCKER_CACHE_DIR/yarn
 
 
     - script:
     - script:
-      name: npm run build:dev
+      name: yarn build:dev
       code: |
       code: |
-        npm run build:dev
+        yarn build:dev
 
 
   after-steps:
   after-steps:
     - slack-notifier:
     - slack-notifier: