ソースを参照

Merge branch 'reactify-admin/security' into reactify-security-setting-api

# Conflicts:
#	src/client/js/services/AdminSecurityContainer.js
WESEEK Kaito 6 年 前
コミット
c178d368c7
54 ファイル変更1960 行追加664 行削除
  1. 1 0
      .eslintignore
  2. 30 0
      .github/workflows/main.yml
  3. 2 1
      .stylelintrc.json
  4. 14 0
      CHANGES.md
  5. 28 83
      README.md
  6. 6 0
      bin/github-actions/bump-version.sh
  7. 14 8
      config/migrate.js
  8. 80 0
      docker/Dockerfile
  9. 142 0
      docker/README.md
  10. 21 0
      docker/bin/remove-unnecessary-files.sh
  11. 9 0
      docker/bin/symlink-for-uploading-to-local.sh
  12. 5 0
      docker/nocdn/env.prod.js
  13. 4 4
      package.json
  14. 11 6
      resource/locales/en-US/translation.json
  15. 9 5
      resource/locales/ja/translation.json
  16. 6 4
      src/client/js/app.jsx
  17. 0 0
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  18. 122 0
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  19. 0 115
      src/client/js/components/Admin/MarkdownSetting/LineBreakSetting.jsx
  20. 15 83
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  21. 122 0
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  22. 86 0
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  23. 15 5
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  24. 24 4
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  25. 351 0
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  26. 134 0
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  27. 22 12
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  28. 2 1
      src/client/js/components/Admin/Users/UserRemoveButton.jsx
  29. 69 0
      src/client/js/services/AdminGeneralSecurityContainer.js
  30. 135 0
      src/client/js/services/AdminLdapSecurityContainer.js
  31. 4 4
      src/client/js/services/AdminUsersContainer.js
  32. 65 0
      src/client/js/services/MarkDownSettingContainer.js
  33. 0 1
      src/client/styles/agile-admin/inverse/colors/antarctic.scss
  34. 21 0
      src/linter-checker/test.js
  35. 23 0
      src/linter-checker/test.scss
  36. 28 0
      src/migrations/20191102223900-drop-configs-indices.js
  37. 29 0
      src/migrations/20191102223901-drop-pages-indices.js
  38. 2 0
      src/server/models/config.js
  39. 0 39
      src/server/routes/admin.js
  40. 251 5
      src/server/routes/apiv3/markdown-setting.js
  41. 0 1
      src/server/routes/apiv3/users.js
  42. 22 10
      src/server/routes/attachment.js
  43. 1 4
      src/server/routes/index.js
  44. 6 0
      src/server/service/config-loader.js
  45. 1 1
      src/server/service/file-uploader/aws.js
  46. 3 2
      src/server/service/file-uploader/gcs.js
  47. 1 1
      src/server/service/file-uploader/gridfs.js
  48. 1 1
      src/server/service/file-uploader/local.js
  49. 4 0
      src/server/service/file-uploader/uploader.js
  50. 1 1
      src/server/service/search.js
  51. 2 248
      src/server/views/admin/markdown.html
  52. 1 0
      src/server/views/admin/widget/passport/local.html
  53. 11 11
      wercker.yml
  54. 4 4
      yarn.lock

+ 1 - 0
.eslintignore

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

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

+ 2 - 1
.stylelintrc.json

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

+ 14 - 0
CHANGES.md

@@ -4,6 +4,7 @@
 
 ### BREAKING CHANGES
 
+* GROWI v3.6.0 no longer support Node.js v8.x
 * The name of database that is storing migrations meta data has been changed
     * This affects **only when `MONGO_URI` has parameters**
     * v3.5.x or above has a bug ([#1361](https://github.com/weseek/growi/issues/1361))
@@ -13,6 +14,19 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 ### Updates
 
 * Improvement: Drop unnecessary MongoDB collection indexes
+* Support: Support Node.js v12
+* Support: Upgrade libs
+    * growi-commons
+
+## 3.5.22
+
+* Improvement: Add `FILE_UPLOAD_DISABLED` env var
+
+## 3.5.21
+
+* Improvement: Cache control when retrieving attachment data
+* Fix: Inviting user doesn't work
+    * Introduced by 3.5.20
 
 ## 3.5.20
 

+ 28 - 83
README.md

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

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

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

+ 14 - 8
config/migrate.js

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

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

+ 4 - 4
package.json

@@ -101,7 +101,7 @@
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^4.0.7",
+    "growi-commons": "^4.0.8",
     "helmet": "^3.13.0",
     "i18next": "^19.0.0",
     "i18next-express-middleware": "^1.4.1",
@@ -247,8 +247,8 @@
     "debug": "src/lib/service/logger/alias-for-debug"
   },
   "engines": {
-    "node": ">=8.11.1 <11",
-    "npm": ">=5.6.0 <7",
-    "yarn": ">=1.5.1 <2"
+    "node": ">=10.17.0 <13",
+    "npm": ">=6.11.3 <7",
+    "yarn": ">=1.19.1 <2"
   }
 }

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

@@ -486,17 +486,19 @@
     },
     "configuration": " Configuration",
     "optional": "Optional",
-    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
-    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
     "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
     "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
     "Use env var if empty": "Use env var <code>%s</code> if empty",
     "Use default if both are empty": "If both ​​are empty, the default value <code>%s</code> is used.",
     "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
     "Local": {
-      "name": "ID/Password"
+      "name": "ID/Password",
+      "enable_local": "enable ID/Password"
     },
     "ldap": {
+      "enable_ldap": "enable LDAP",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
       "bind_manager": "Manager Bind",
@@ -602,9 +604,9 @@
     "line_break_setting": "Line Break Setting",
     "line_break_setting_desc": "You can change line break settings.",
     "Enable Line Break": "Enable Line Break",
-    "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
     "Enable Line Break for comment": "Enable Line Break in comment",
-    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break for comment desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML",
     "presentation_setting": "Presentation Setting",
     "presentation_setting_desc": "You can change presentation settings.",
     "Page break setting": "Page break Setting",
@@ -625,7 +627,10 @@
     "Custom Whitelist": "Custom Whitelist",
     "Tag names":"Tag names",
     "Tag attributes":"Tag attributes",
-    "import_recommended": "Import recommended %s"
+    "import_recommended": "Import recommended %s",
+    "updated_lineBreak": "Succeeded to update line braek setting",
+    "updated_presentation": "Succeeded to update presentation setting",
+    "updated_xss": "Succeeded to update XSS setting"
   },
 
   "notification_setting": {

+ 9 - 5
resource/locales/ja/translation.json

@@ -481,17 +481,19 @@
     },
     "configuration": "設定",
     "optional": "オプション",
-    "Treat username matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
-    "Treat username matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
+    "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat username matching as identical_warn": "警告: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
     "Treat email matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
     "Treat email matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
     "Use env var if empty": "空の場合、環境変数 <code>%s</code> を利用します",
     "Use default if both are empty": "どちらの値も空の場合、デフォルト値 <code>%s</code> を利用します",
     "missing mandatory configs": "以下の必須項目の値がデータベースと環境変数のどちらにも設定されていません",
     "Local": {
-      "name": "ID/Password"
+      "name": "ID/Password",
+      "enable_local": "ID/Password を有効にする"
     },
     "ldap": {
+      "enable_ldap": "LDAP を有効にする",
       "server_url_detail": "LDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code> の形式で入力してください。",
       "bind_mode": "Bind モード",
       "bind_manager": "管理者 Bind",
@@ -609,7 +611,10 @@
     "Custom Whitelist": "カスタムホワイトリスト",
     "Tag names": "タグ名のホワイトリスト",
     "Tag attributes": "タグ属性のホワイトリスト",
-    "import_recommended": "おすすめをインポート"
+    "import_recommended": "おすすめをインポート",
+    "updated_lineBreak": "改行設定を更新しました",
+    "updated_presentation": "プレゼンテーション設定を更新しました",
+    "updated_xss": "XSS設定を更新しました"
   },
 
   "notification_setting": {
@@ -705,7 +710,6 @@
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_external_user_success": "{{accountId}}を削除しました "
-
   },
 
   "user_group_management": {

+ 6 - 4
src/client/js/app.jsx

@@ -40,7 +40,6 @@ import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import UserManagement from './components/Admin/UserManagement';
-import ManageExternalAccount from './components/Admin/Users/ManageExternalAccount';
 import SecurityManagement from './components/Admin/Security/SecurityManagement';
 import ManageExternalAccount from './components/Admin/ManageExternalAccount';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
@@ -56,7 +55,8 @@ import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import UserGroupDetailContainer from './services/UserGroupDetailContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
-import AdminSecurityContainer from './services/AdminSecurityContainer';
+import AdminGeneralSecurityContainer from './services/AdminGeneralSecurityContainer';
+import AdminLdapSecurityContainer from './services/AdminLdapSecurityContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import MarkDownSettingContainer from './services/MarkDownSettingContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
@@ -215,9 +215,11 @@ if (adminMarkDownSettingElem != null) {
 
 const adminSecuritySettingElem = document.getElementById('admin-security-setting');
 if (adminSecuritySettingElem != null) {
-  const adminSecurityContainer = new AdminSecurityContainer(appContainer);
+  const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer(appContainer);
+  const adminLdapSecurityContainer = new AdminLdapSecurityContainer(appContainer);
+  const adminSecurityContainers = [adminGeneralSecurityContainer, adminLdapSecurityContainer];
   ReactDOM.render(
-    <Provider inject={[injectableContainers, adminSecurityContainer]}>
+    <Provider inject={[injectableContainers, adminSecurityContainers]}>
       <I18nextProvider i18n={i18n}>
         <SecurityManagement />
       </I18nextProvider>

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


+ 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);

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

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

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

@@ -1,4 +1,3 @@
-/* eslint-disable max-len */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
@@ -6,101 +5,34 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 import AppContainer from '../../../services/AppContainer';
-import LineBreakSetting from './LineBreakSetting';
+import LineBreakForm from './LineBreakForm';
+import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
 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() {
     const { t } = this.props;
 
     return (
-      // TODO GW-322 adjust layout
       <React.Fragment>
-        <div>
-          {/* Line Break Setting */}
-          <LineBreakSetting />
+        {/* Line Break Setting */}
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.line_break_setting') }</h2>
+          <p className="well">{ t('markdown_setting.line_break_setting_desc') }</p>
+          <LineBreakForm />
         </div>
 
-        <div className="row my-3">
-          <div className="form-group">
-            <legend>{ t('markdown_setting.presentation_setting') }</legend>
-            <p className="well">{ t('markdown_setting.presentation_setting_desc') }</p>
-          </div>
-          <fieldset className="form-group row my-2">
-
-            <label className="col-xs-3 control-label text-right">
-              { t('markdown_setting.Page break setting') }
-            </label>
-
-            <div className="col-xs-3 radio radio-primary">
-              <input type="radio" id="pageBreakOption1" name="pageBreakOption" value="1" checked={this.state.pageBreakOption === 1} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption1">
-                <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Preset one separator desc') }
-                  <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
-                </p>
-              </label>
-            </div>
-
-            <div className="col-xs-3 radio radio-primary mt-3">
-              <input type="radio" id="pageBreakOption2" name="pageBreakOption" value="2" checked={this.state.pageBreakOption === 2} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption2">
-                <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Preset two separator desc') }
-                  <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
-                </p>
-              </label>
-            </div>
-
-            <div className="col-xs-3 radio radio-primary mt-3">
-              <input type="radio" id="pageBreakOption3" name="pageBreakOption" value="3" checked={this.state.pageBreakOption === 3} onChange={this.handleInputChange} />
-              <label htmlFor="pageBreakOption3">
-                <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
-                <p className="mt-3">
-                  { t('markdown_setting.Custom separator desc') }
-                  <div>
-                    <input className="form-control" name="customRegularExpression" value={this.state.customRegularExpression} onChange={this.handleInputChange} />
-                  </div>
-                </p>
-              </label>
-            </div>
-
-            <div className="form-group my-3">
-              <div className="col-xs-offset-4 col-xs-5">
-                <button type="submit" className="btn btn-primary">{ t('Update') }</button>
-              </div>
-            </div>
-
-          </fieldset>
+        {/* Presentation Setting */}
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.presentation_setting') }</h2>
+          <p className="well">{ t('markdown_setting.presentation_setting_desc') }</p>
+          <PresentationForm />
         </div>
+
         {/* XSS Setting */}
-        <div className="row my-3">
-          <h2>{ t('markdown_setting.XSS_setting') }</h2>
+        <div className="row mb-5">
+          <h2 className="border-bottom">{ t('markdown_setting.XSS_setting') }</h2>
           <p className="well">{ t('markdown_setting.XSS_setting_desc') }</p>
           <XssForm />
         </div>

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

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

@@ -1,14 +1,18 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
 import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
 
 import WhiteListInput from './WhiteListInput';
 
+const logger = loggerFactory('growi:importer');
+
 class XssForm extends React.Component {
 
   constructor(props) {
@@ -18,7 +22,16 @@ class XssForm extends React.Component {
   }
 
   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() {
@@ -82,9 +95,16 @@ class XssForm extends React.Component {
       <React.Fragment>
         <form className="row">
           <div className="form-group">
-            <div className="col-xs-4 text-right">
-              <div className="checkbox checkbox-success" onChange={markDownSettingContainer.switchEnableXss}>
-                <input type="checkbox" id="XssEnable" className="form-check-input" name="isEnabledXss" checked={isEnabledXss} />
+            <div className="col-xs-offset-4 col-xs-4 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  type="checkbox"
+                  id="XssEnable"
+                  className="form-check-input"
+                  name="isEnabledXss"
+                  checked={isEnabledXss}
+                  onChange={markDownSettingContainer.switchEnableXss}
+                />
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
                 </label>

+ 351 - 0
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -0,0 +1,351 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
+
+class LdapSecuritySetting extends React.Component {
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminLdapSecurityContainer } = this.props;
+    const { isLdapEnabled } = adminGeneralSecurityContainer.state;
+
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          LDAP { t('security_setting.configuration') }
+        </h2>
+
+        <div className="row mb-5">
+          <strong className="col-xs-3 text-right">Use LDAP</strong>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isLdapEnabled"
+                type="checkbox"
+                checked={isLdapEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
+              />
+              <label htmlFor="isLdapEnabled">
+                { t('security_setting.ldap.enable_ldap') }
+              </label>
+            </div>
+          </div>
+        </div>
+
+
+        {isLdapEnabled && (
+          <React.Fragment>
+            <div className="row mb-5">
+              <label htmlFor="serverUrl" className="col-xs-3 control-label text-right">Server URL</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="serverUrl"
+                  value={adminLdapSecurityContainer.state.serverUrl}
+                  onChange={e => adminLdapSecurityContainer.changeServerUrl(e.target.value)}
+                />
+                <small>
+                  <p
+                    className="help-block"
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
+                  />
+                  { t('security_setting.example') }: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
+                </small>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{ t('security_setting.ldap.bind_mode') }</strong>
+              <div className="col-xs-6 text-left">
+                <div className="my-0 btn-group">
+                  <div className="dropdown">
+                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      <span className="pull-left">{t(`security_setting.ldap.bind_${adminLdapSecurityContainer.state.bindMode}`)}</span>
+                      <span className="bs-caret pull-right">
+                        <span className="caret" />
+                      </span>
+                    </button>
+                    {/* TODO adjust dropdown after BS4 */}
+                    <ul className="dropdown-menu" role="menu">
+                      <li key="manager" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode('manager') }}>
+                        <a role="menuitem">{ t('security_setting.ldap.bind_manager') }</a>
+                      </li>
+                      <li key="user" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode('user') }}>
+                        <a role="menuitem">{ t('security_setting.ldap.bind_user') }</a>
+                      </li>
+                    </ul>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">Bind DN</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="bindDN"
+                  value={adminLdapSecurityContainer.state.bindDN}
+                  onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
+                />
+                {(adminLdapSecurityContainer.state.bindMode === 'manager') ? (
+                  <p className="help-block passport-ldap-managerbind">
+                    <small>
+                      { t('security_setting.ldap.bind_DN_manager_detail') }<br />
+                      { t('security_setting.example') }1: <code>uid=admin,dc=domain,dc=com</code><br />
+                      { t('security_setting.example') }2: <code>admin@domain.com</code>
+                    </small>
+                  </p>
+                ) : (
+                  <p className="help-block passport-ldap-userbind">
+                    <small>
+                      { t('security_setting.ldap.bind_DN_user_detail1')}<br />
+                      {/* eslint-disable-next-line react/no-danger */}
+                      <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.bind_DN_user_detail2') }} /><br />
+                      { t('security_setting.example') }1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
+                      { t('security_setting.example') }2: <code>{'{{ username }}'}@domain.com</code>
+                    </small>
+                  </p>
+                )}
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="bindDNPassword" className="col-xs-3 text-right">{ t('security_setting.ldap.bind_DN_password') }</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control passport-ldap-managerbind"
+                  type="password"
+                  name="bindDNPassword"
+                  value={adminLdapSecurityContainer.state.bindDNPassword}
+                  onChange={e => adminLdapSecurityContainer.changeBindDNPassword(e.target.value)}
+                />
+                {(adminLdapSecurityContainer.state.bindMode === 'manager') ? (
+                  <p className="help-block passport-ldap-managerbind">
+                    <small>
+                      { t('security_setting.ldap.bind_DN_password_manager_detail') }
+                    </small>
+                  </p>
+                ) : (
+                  <p className="help-block passport-ldap-userbind">
+                    <small>
+                      { t('security_setting.ldap.bind_DN_password_user_detail') }
+                    </small>
+                  </p>
+                )}
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{ t('security_setting.ldap.search_filter') }</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="searchFilter"
+                  value={adminLdapSecurityContainer.state.searchFilter}
+                  onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    { t('security_setting.ldap.search_filter_detail1') }<br />
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail2') }} /><br />
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail3') }} />
+                  </small>
+                </p>
+                <p className="help-block">
+                  <small>
+                    { t('security_setting.example') }1 - { t('security_setting.ldap.search_filter_example1') }:
+                    <code>(|(uid={'{{ username }}'})(mail={'{{ username }}'}))</code><br />
+                    { t('security_setting.example') }2 - { t('security_setting.ldap.search_filter_example2') }:
+                    <code>(sAMAccountName={'{{ username }}'})</code>
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping ({ t('security_setting.optional') })
+            </h3>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapUsername" className="col-xs-3 text-right">{t('username')}</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: uid"
+                  name="attrMapUsername"
+                  value={adminLdapSecurityContainer.state.attrMapUsername}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
+                />
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="cbSameUsernameTreatedAsIdenticalUser"
+                    type="checkbox"
+                    checked={adminLdapSecurityContainer.state.cbSameUsernameTreatedAsIdenticalUser}
+                    onChange={() => { adminLdapSecurityContainer.switchCbSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="cbSameUsernameTreatedAsIdenticalUser"
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapMail" className="col-xs-3 text-right">{ t('Email') }</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: mail"
+                  name="attrMapMail"
+                  value={adminLdapSecurityContainer.state.attrMapMail}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    { t('security_setting.ldap.mail_detail') }
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="attrMapName" className="col-xs-3 text-right">{ t('Name') }</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="attrMapName"
+                  value={adminLdapSecurityContainer.state.attrMapName}
+                  onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    { t('security_setting.ldap.name_detail') }
+                  </small>
+                </p>
+              </div>
+            </div>
+
+
+            <h3 className="alert-anchor border-bottom">
+              { t('security_setting.ldap.group_search_filter') } ({ t('security_setting.optional') })
+            </h3>
+
+            <div className="row mb-5">
+              <strong htmlFor="groupSearchBase" className="col-xs-3 text-right">{ t('security_setting.ldap.group_search_base_DN') }</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="groupSearchBase"
+                  value={adminLdapSecurityContainer.state.groupSearchBase}
+                  onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_base_DN_detail') }} /><br />
+                    { t('security_setting.example') }: <code>ou=groups,dc=domain,dc=com</code>
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <strong htmlFor="groupSearchFilter" className="col-xs-3 text-right">{ t('security_setting.ldap.group_search_filter') }</strong>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="groupSearchFilter"
+                  value={adminLdapSecurityContainer.state.groupSearchFilter}
+                  onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
+                />
+                <p className="help-block">
+                  <small>
+                    {/* eslint-disable react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail1') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail2') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail3') }} />
+                    {/* eslint-enable react/no-danger */}
+                  </small>
+                </p>
+                <p className="help-block">
+                  <small>
+                    { t('security_setting.example') }:
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail4') }} />
+                  </small>
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <label htmlFor="groupDnProperty" className="col-xs-3 text-right">{ t('security_setting.ldap.group_search_user_DN_property') }</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  placeholder="Default: uid"
+                  name="groupDnProperty"
+                  value={adminLdapSecurityContainer.state.groupDnProperty}
+                  onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
+                />
+                <p className="help-block">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
+                </p>
+              </div>
+            </div>
+
+          </React.Fragment>
+        )}
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+LdapSecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
+};
+
+const LdapSecuritySettingWrapper = (props) => {
+  return createSubscribedElement(LdapSecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer, AdminLdapSecurityContainer]);
+};
+
+export default withTranslation()(LdapSecuritySettingWrapper);

+ 134 - 0
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -0,0 +1,134 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+
+class LocalSecuritySetting extends React.Component {
+
+  render() {
+    const { t, adminGeneralSecurityContainer } = this.props;
+
+    return (
+      <React.Fragment>
+
+        <h2 className="alert-anchor border-bottom">
+          { t('security_setting.Local.name') } { t('security_setting.configuration') }
+        </h2>
+
+        {adminGeneralSecurityContainer.state.useOnlyEnvVarsForSomeOptions && (
+        <p className="alert alert-info">
+          { t('security_setting.Local.note for the only env option', 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS') }
+        </p>
+        )}
+
+        <div className="row mb-5">
+          <strong className="col-xs-3 text-right">{ t('security_setting.Local.name') }</strong>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isLocalEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isLocalEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsLocalEnabled() }}
+              />
+              <label htmlFor="isLocalEnabled">
+                { t('security_setting.Local.enable_local') }
+              </label>
+            </div>
+          </div>
+        </div>
+
+        {adminGeneralSecurityContainer.state.isLocalEnabled && (
+          <div>
+            <div className="row mb-5">
+              <strong className="col-xs-3 text-right">{ t('Register limitation') }</strong>
+              <div className="col-xs-9 text-left">
+                <div className="my-0 btn-group">
+                  <div className="dropdown">
+                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                      <span className="pull-left">{t(`security_setting.registration_mode.${adminGeneralSecurityContainer.state.registrationMode}`)}</span>
+                      <span className="bs-caret pull-right">
+                        <span className="caret" />
+                      </span>
+                    </button>
+                    {/* TODO adjust dropdown after BS4 */}
+                    <ul className="dropdown-menu" role="menu">
+                      <li
+                        key="open"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeRegistrationMode('open') }}
+                      >
+                        <a role="menuitem">{ t('security_setting.registration_mode.open') }</a>
+                      </li>
+                      <li
+                        key="restricted"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeRegistrationMode('restricted') }}
+                      >
+                        <a role="menuitem">{ t('security_setting.registration_mode.restricted') }</a>
+                      </li>
+                      <li
+                        key="closed"
+                        role="presentation"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeRegistrationMode('closed') }}
+                      >
+                        <a role="menuitem">{ t('security_setting.registration_mode.closed') }</a>
+                      </li>
+                    </ul>
+                  </div>
+                  <p className="help-block">
+                    { t('security_setting.Register limitation desc') }
+                  </p>
+                </div>
+              </div>
+            </div>
+            <div className="row mb-5">
+              {/* eslint-disable-next-line react/no-danger */}
+              <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
+              <div className="col-xs-6">
+                <div>
+                  <textarea
+                    className="form-control"
+                    type="textarea"
+                    name="registrationWhiteList"
+                    placeholder={adminGeneralSecurityContainer.state.registrationWhiteList}
+                  />
+                  <p className="help-block small">{ t('security_setting.restrict_emails') }<br />{ t('security_setting.for_instance') }
+                    <code>@growi.org</code>{ t('security_setting.only_those') }<br />
+                    { t('security_setting.insert_single') }
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+        )}
+
+        {/*  TODO replace component */}
+        <div className="col-xs-offset-3 col-xs-6 mb-5">
+          <button type="submit" className="btn btn-primary">{ t('Update') }</button>
+        </div>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+LocalSecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+};
+
+const LocalSecuritySettingWrapper = (props) => {
+  return createSubscribedElement(LocalSecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer]);
+};
+
+export default withTranslation()(LocalSecuritySettingWrapper);

+ 22 - 12
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -5,6 +5,8 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 import AppContainer from '../../../services/AppContainer';
+import LdapSecuritySetting from './LdapSecuritySetting';
+import LocalSecuritySetting from './LocalSecuritySetting';
 
 class SecurityManagement extends React.Component {
 
@@ -17,7 +19,7 @@ class SecurityManagement extends React.Component {
     const { t } = this.props;
     return (
       <Fragment>
-        {/* TODO GW-540 */}
+        {/* TODO GW-582 reactify-admin */}
         <fieldset>
           <legend className="alert-anchor">{ t('security_settings') }</legend>
           <div className="form-group">
@@ -93,16 +95,20 @@ class SecurityManagement extends React.Component {
             </div>
           </div>
         </fieldset>
-        <div className="mt-5">
-          <legend>{ t('security_setting.xss_prevent_setting') }</legend>
+
+        {/* XSS configuration link */}
+        <div className="mb-5">
+          <h2 className="border-bottom">{ t('security_setting.xss_prevent_setting') }</h2>
           <div className="text-center">
-            <a className="flexbox" style={{ fontSize: 'large' }} href="/admin/markdown/#preventXSS">
+            <a style={{ fontSize: 'large' }} href="/admin/markdown/#preventXSS">
               <i className="fa-fw icon-login"></i> { t('security_setting.xss_prevent_setting_link') }
             </a>
           </div>
         </div>
+
+        {/* TODO 542~550 reactify-admin */}
         <div className="auth-mechanism-configurations m-t-10">
-          <legend>{ t('security_setting.Authentication mechanism settings') }</legend>
+          <h2 className="border-bottom">{ t('security_setting.Authentication mechanism settings') }</h2>
           <div className="passport-settings">
             <ul className="nav nav-tabs" role="tablist">
               <li className="active">
@@ -135,31 +141,35 @@ class SecurityManagement extends React.Component {
             </ul>
             <div className="tab-content p-t-10">
               <div id="passport-local" className="tab-pane active" role="tabpanel">
+                <LocalSecuritySetting />
               </div>
               <div id="passport-ldap" className="tab-pane" role="tabpanel">
+                <LdapSecuritySetting />
               </div>
               <div id="passport-saml" className="tab-pane" role="tabpanel">
+                {/* TODO GW-544 reactify saml.html */}
               </div>
               <div id="passport-oidc" className="tab-pane" role="tabpanel">
+                {/* TODO GW-545 reactify oidc.html */}
               </div>
               <div id="passport-basic" className="tab-pane" role="tabpanel">
+                {/* TODO GW-546 reactify basic.html */}
               </div>
               <div id="passport-google-oauth" className="tab-pane" role="tabpanel">
+                {/* TODO GW-547 reactify google-oauth.html */}
               </div>
-              <div id="passport-facebook" className="tab-pane" role="tabpanel">
+              <div id="passport-github" className="tab-pane" role="tabpanel">
+                {/* TODO GW-548 reactify github.html */}
               </div>
               <div id="passport-twitter" className="tab-pane" role="tabpanel">
+                {/* TODO GW-549 reactify twitter.html */}
               </div>
-              <div id="passport-github" className="tab-pane" role="tabpanel">
+              <div id="passport-facebook" className="tab-pane" role="tabpanel">
+                {/* TODO GW-550 reactify facebook.html */}
               </div>
             </div>
           </div>
         </div>
-        <div>
-          <script>
-            {/* TODO GW-542~GW-549 */}
-          </script>
-        </div>
       </Fragment>
     );
   }

+ 2 - 1
src/client/js/components/Admin/Users/UserRemoveButton.jsx

@@ -19,7 +19,8 @@ class UserRemoveButton extends React.Component {
     const { t } = this.props;
 
     try {
-      const username = await this.props.adminUsersContainer.removeUser(this.props.user._id);
+      await this.props.adminUsersContainer.removeUser(this.props.user._id);
+      const { username } = this.props.user;
       toastSuccess(t('user_management.remove_user_success', { username }));
     }
     catch (err) {

+ 69 - 0
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -0,0 +1,69 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:security:AdminGeneralSecurityContainer');
+
+/**
+ * Service container for admin security page (SecurityManagement.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminGeneralSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      // TODO GW-583 set value
+      useOnlyEnvVarsForSomeOptions: true,
+      isLocalEnabled: true,
+      registrationMode: 'open',
+      registrationWhiteList: '',
+      isLdapEnabled: true,
+    };
+
+    this.init();
+
+    this.switchIsLocalEnabled = this.switchIsLocalEnabled.bind(this);
+    this.changeRegistrationMode = this.changeRegistrationMode.bind(this);
+  }
+
+  init() {
+    // TODO GW-583 fetch config value with api
+  }
+
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminGeneralSecurityContainer';
+  }
+
+  /**
+   * Switch local enabled
+   */
+  switchIsLocalEnabled() {
+    this.setState({ isLocalEnabled: !this.state.isLocalEnabled });
+  }
+
+  /**
+   * Change registration mode
+   */
+  changeRegistrationMode(value) {
+    this.setState({ registrationMode: value });
+  }
+
+  // LDAP function
+
+  /**
+   * Switch local enabled
+   */
+  switchIsLdapEnabled() {
+    this.setState({ isLdapEnabled: !this.state.isLdapEnabled });
+  }
+
+}

+ 135 - 0
src/client/js/services/AdminLdapSecurityContainer.js

@@ -0,0 +1,135 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:security:AdminLdapSecurityLdapContainer');
+
+/**
+ * Service container for admin security page (SecurityLdapSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminLdapSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      // TODO GW-583 set value
+      serverUrl: '',
+      bindMode: 'manager',
+      bindDN: '',
+      bindDNPassword: '',
+      searchFilter: '',
+      attrMapUsername: '',
+      cbSameUsernameTreatedAsIdenticalUser: true,
+      attrMapMail: '',
+      attrMapName: '',
+      groupSearchBase: '',
+      groupSearchFilter: '',
+      groupDnProperty: '',
+    };
+
+    this.init();
+
+  }
+
+  init() {
+    // TODO GW-583 fetch config value with api
+  }
+
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminLdapSecurityContainer';
+  }
+
+  /**
+   * Change server url
+   */
+  changeServerUrl(inputValue) {
+    this.setState({ serverUrl: inputValue });
+  }
+
+  /**
+   * Change ldap bind mode
+   */
+  changeLdapBindMode(mode) {
+    this.setState({ bindMode: mode });
+  }
+
+  /**
+   * Change bind DN
+   */
+  changeBindDN(inputValue) {
+    this.setState({ bindDN: inputValue });
+  }
+
+  /**
+   * Change bind DN password
+   */
+  changeBindDNPassword(inputValue) {
+    this.setState({ bindDNPassword: inputValue });
+  }
+
+  /**
+   * Change search filter
+   */
+  changeSearchFilter(inputValue) {
+    this.setState({ searchFilter: inputValue });
+  }
+
+  /**
+   * Change attr map username
+   */
+  changeAttrMapUsername(inputValue) {
+    this.setState({ attrMapUsername: inputValue });
+  }
+
+  /**
+   * Switch cb same username treated as identical user
+   */
+  switchCbSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ cbSameUsernameTreatedAsIdenticalUser: !this.state.cbSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Change attr map email
+   */
+  changeAttrMapMail(inputValue) {
+    this.setState({ attrMapMail: inputValue });
+  }
+
+  /**
+   * Change attr map name
+   */
+  changeAttrMapName(inputValue) {
+    this.setState({ attrMapName: inputValue });
+  }
+
+  /**
+   * Change group search base
+   */
+  changeGroupSearchBase(inputValue) {
+    this.setState({ groupSearchBase: inputValue });
+  }
+
+  /**
+   * Change group search filter
+   */
+  changeGroupSearchFilter(inputValue) {
+    this.setState({ groupSearchFilter: inputValue });
+  }
+
+  /**
+   * Change group dn property
+   */
+  changeGroupDnProperty(inputValue) {
+    this.setState({ groupDnProperty: inputValue });
+  }
+
+}

+ 4 - 4
src/client/js/services/AdminUsersContainer.js

@@ -74,9 +74,9 @@ export default class AdminUsersContainer extends Container {
       shapedEmailList,
       sendEmail,
     });
-    const { emailList } = response.data;
     await this.retrieveUsersByPagingNum(this.state.activePage);
-    return emailList;
+    const { invitedUserList } = response.data;
+    return invitedUserList;
   }
 
   /**
@@ -163,13 +163,13 @@ export default class AdminUsersContainer extends Container {
    * remove user
    * @memberOf AdminUsersContainer
    * @param {string} userId
-   * @return {string} username
+   * @return {object} removedUserData
    */
   async removeUser(userId) {
     const response = await this.appContainer.apiv3.delete(`/users/${userId}/remove`);
     const removedUserData = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
-    return removedUserData.username;
+    return removedUserData;
   }
 
 }

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

@@ -12,6 +12,12 @@ export default class MarkDownSettingContainer extends Container {
     this.appContainer = appContainer;
 
     this.state = {
+      isEnabledLinebreaks: appContainer.config.isEnabledLinebreaks,
+      isEnabledLinebreaksInComments: appContainer.config.isEnabledLinebreaksInComments,
+      pageBreakSeparator: appContainer.config.pageBreakSeparator,
+      pageBreakCustomSeparator: appContainer.config.pageBreakCustomSeparator || '',
+      // pageBreakOption: appContainer.config.pageBreakOption,
+      customRegularExpression: appContainer.config.customRegularExpression || '',
       isEnabledXss: (appContainer.config.xssOption != null),
       xssOption: appContainer.config.xssOption,
       tagWhiteList: appContainer.config.tagWhiteList || '',
@@ -28,6 +34,20 @@ export default class MarkDownSettingContainer extends Container {
     return 'MarkDownSettingContainer';
   }
 
+  /**
+   * Switch PageBreakSeparator
+   */
+  switchPageBreakSeparator(pageBreakSeparator) {
+    this.setState({ pageBreakSeparator });
+  }
+
+  /**
+   * Set PageBreakCustomSeparator
+   */
+  setPageBreakCustomSeparator(pageBreakCustomSeparator) {
+    this.setState({ pageBreakCustomSeparator });
+  }
+
   /**
    * Switch enableXss
    */
@@ -38,4 +58,49 @@ export default class MarkDownSettingContainer extends Container {
     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,
   $alpha: 0.8,
 );
-$info: $subthemecolor;
 
 @import 'apply-colors';
 @import 'apply-colors-light';

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

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

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

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

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

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

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

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

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

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

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

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

@@ -7,19 +7,265 @@ const express = require('express');
 
 const router = express.Router();
 
+const { body } = require('express-validator/check');
+
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const validator = {
+  lineBreak: [
+    body('isEnabledLinebreaks').isBoolean(),
+    body('isEnabledLinebreaksInComments').isBoolean(),
+  ],
+  presentationSetting: [
+    body('pageBreakSeparator').isInt().not().isEmpty(),
+  ],
+  xssSetting: [
+    body('isEnabledXss').isBoolean(),
+    body('tagWhiteList').toArray(),
+    body('attrWhiteList').toArray(),
+  ],
+};
+
+
 /**
  * @swagger
  *  tags:
  *    name: MarkDownSetting
  */
 
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      LineBreakParams:
+ *        type: object
+ *        properties:
+ *          isEnabledLinebreaks:
+ *            type: boolean
+ *            description: enable lineBreak
+ *          isEnabledLinebreaksInComments:
+ *            type: boolean
+ *            description: enable lineBreak in comment
+ *      PresentationParams:
+ *        type: object
+ *        properties:
+ *          pageBreakSeparator:
+ *            type: number
+ *            description: number of pageBreakSeparator
+ *          pageBreakCustomSeparator:
+ *            type: string
+ *            description: string of pageBreakCustomSeparator
+ *      XssParams:
+ *        type: object
+ *        properties:
+ *          isEnabledPrevention:
+ *            type: boolean
+ *            description: enable xss
+ *          xssOption:
+ *            type: number
+ *            description: number of xss option
+ *          tagWhiteList:
+ *            type: array
+ *            description: array of tag whiteList
+ *            items:
+ *              type: string
+ *              description: tag whitelist
+ *          attrWhiteList:
+ *            type: array
+ *            description: array of attr whiteList
+ *            items:
+ *              type: string
+ *              description: attr whitelist
+ */
+
 module.exports = (crowi) => {
-  // const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
-  // const adminRequired = require('../../middleware/admin-required')(crowi);
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+  /**
+   * @swagger
+   *
+   *    /markdown-setting/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;
 };

+ 0 - 1
src/server/routes/apiv3/users.js

@@ -392,7 +392,6 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: A result of `ExtenralAccount.findByIdAndRemove`
    */
-
   router.delete('/external-accounts/:id/remove', loginRequiredStrictly, adminRequired, ApiV3FormValidator, async(req, res) => {
     const { id } = req.params;
 

+ 22 - 10
src/server/routes/attachment.js

@@ -47,21 +47,32 @@ module.exports = function(crowi, app) {
   /**
    * Common method to response
    *
+   * @param {Request} req
    * @param {Response} res
    * @param {User} user
    * @param {Attachment} attachment
    * @param {boolean} forceDownload
    */
-  async function responseForAttachment(res, user, attachment, forceDownload) {
+  async function responseForAttachment(req, res, attachment, forceDownload) {
     if (attachment == null) {
       return res.json(ApiResponse.error('attachment not found'));
     }
 
+    const user = req.user;
     const isAccessible = await isAccessibleByViewer(user, attachment);
     if (!isAccessible) {
       return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'`));
     }
 
+    // add headers before evaluating 'req.fresh'
+    setHeaderToRes(res, attachment, forceDownload);
+
+    // return 304 if request is "fresh"
+    // see: http://expressjs.com/en/5x/api.html#req.fresh
+    if (req.fresh) {
+      return res.sendStatus(304);
+    }
+
     let fileStream;
     try {
       fileStream = await fileUploader.findDeliveryFile(attachment);
@@ -71,7 +82,6 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(e.message));
     }
 
-    setHeaderToRes(res, attachment, forceDownload);
     return fileStream.pipe(res);
   }
 
@@ -83,14 +93,16 @@ module.exports = function(crowi, app) {
    * @param {boolean} forceDownload
    */
   function setHeaderToRes(res, attachment, forceDownload) {
+    res.set({
+      ETag: `Attachment-${attachment._id}`,
+      'Last-Modified': attachment.createdAt,
+    });
+
     // download
     if (forceDownload) {
-      const headers = {
-        'Content-Type': 'application/force-download',
-        'Content-Disposition': `inline;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
-      };
-
-      res.writeHead(200, headers);
+      res.set({
+        'Content-Disposition': `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
+      });
     }
     // reference
     else {
@@ -134,7 +146,7 @@ module.exports = function(crowi, app) {
 
     const attachment = await Attachment.findById(id);
 
-    return responseForAttachment(res, req.user, attachment, true);
+    return responseForAttachment(req, res, attachment, true);
   };
 
   /**
@@ -149,7 +161,7 @@ module.exports = function(crowi, app) {
 
     const attachment = await Attachment.findById(id);
 
-    return responseForAttachment(res, req.user, attachment);
+    return responseForAttachment(req, res, attachment);
   };
 
   /**

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

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

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

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

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

@@ -49,7 +49,7 @@ module.exports = function(crowi) {
     return filePath;
   }
 
-  lib.getIsUploadable = function() {
+  lib.isValidUploadSettings = function() {
     return this.configManager.getConfig('crowi', 'aws:accessKeyId') != 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;
   }
 
-  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) {

+ 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.promisifiedUnlink = util.promisify(AttachmentFile.unlink).bind(AttachmentFile);
 
-  lib.getIsUploadable = function() {
+  lib.isValidUploadSettings = function() {
     return true;
   };
 

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

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

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

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

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

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

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

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

+ 1 - 0
src/server/views/admin/widget/passport/local.html

@@ -6,6 +6,7 @@
   {% set isLocalEnabled = getConfig('crowi', 'security:passport-local:isEnabled') %}
   {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions') %}
 
+  <!-- TODO delete file after reactify-admin -->
   {% if useOnlyEnvVars %}
     <p class="alert alert-info">
       {{ t("security_setting.Local.note for the only env option", "LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS") }}

+ 11 - 11
wercker.yml

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

+ 4 - 4
yarn.lock

@@ -5758,10 +5758,10 @@ grapheme-splitter@^1.0.4:
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
-growi-commons@^4.0.7:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.7.tgz#f9ff9c2f6afe3a9982b689d368e8e7a000d137e8"
-  integrity sha512-Ipku+WlfXsSqS+2fuDRfOeCP5Q8dBo/8XYlcjsPFdCvKa9lBr2OsSN2ac9dAXaL9bH4wc+Kz7UR/OLjAMlx0Iw==
+growi-commons@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.8.tgz#3040f2759f5eb13084b101e303c11028af2a8faf"
+  integrity sha512-AL/vm3R3LqiWwCbuEHPsDP8hapiz7ulKZXyW4399WHorx/7oEkSWb6sGdkvJiqSnRWxEcdPkfw6K0kgzilMpig==
 
 growly@^1.3.0:
   version "1.3.0"