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

Merge branch 'master' into imprv/reactify-admin

# Conflicts:
#	package.json
#	resource/locales/en-US/translation.json
#	resource/locales/ja/translation.json
#	src/server/views/admin/user-groups.html
#	src/server/views/admin/users.html
harukatokutake 6 лет назад
Родитель
Сommit
e472ef2650
77 измененных файлов с 1106 добавлено и 541 удалено
  1. 0 4
      .eslintrc.js
  2. 48 9
      CHANGES.md
  3. 5 0
      README.md
  4. 3 1
      bin/download-cdn-resources.js
  5. 1 1
      config/env.dev.js
  6. 2 5
      config/jest.config.js
  7. 18 16
      package.json
  8. 2 0
      resource/cdn-manifests.js
  9. 21 20
      resource/locales/en-US/translation.json
  10. 1 1
      resource/locales/en-US/welcome.md
  11. 15 15
      resource/locales/ja/translation.json
  12. 0 2
      src/client/js/app.jsx
  13. 0 0
      src/client/js/components/Admin/CustomCssEditor.jsx
  14. 0 0
      src/client/js/components/Admin/CustomHeaderEditor.jsx
  15. 0 0
      src/client/js/components/Admin/CustomScriptEditor.jsx
  16. 29 25
      src/client/js/components/InstallerForm.jsx
  17. 0 0
      src/client/js/components/Page/PagePath.jsx
  18. 0 0
      src/client/js/components/Page/RevisionBody.jsx
  19. 12 18
      src/client/js/components/Page/RevisionLoader.jsx
  20. 0 0
      src/client/js/components/PageAttachment/Attachment.jsx
  21. 0 0
      src/client/js/components/PageAttachment/DeleteAttachmentModal.jsx
  22. 0 0
      src/client/js/components/PageAttachment/PageAttachmentList.jsx
  23. 3 2
      src/client/js/components/PageEditor.jsx
  24. 0 0
      src/client/js/components/PageEditor/MarkdownTableUtil.js
  25. 2 2
      src/client/js/components/PageEditor/ScrollSyncHelper.js
  26. 0 0
      src/client/js/components/PageList/ListView.jsx
  27. 9 1
      src/client/js/components/PageList/Page.jsx
  28. 0 0
      src/client/js/components/PageList/PageListMeta.jsx
  29. 0 0
      src/client/js/components/PageList/PagePath.jsx
  30. 1 1
      src/client/js/components/SavePageControls/GrantSelector.jsx
  31. 0 0
      src/client/js/components/SearchForm.jsx
  32. 0 0
      src/client/js/components/SearchPage.jsx
  33. 0 0
      src/client/js/components/SearchPage/DeletePageListModal.jsx
  34. 0 0
      src/client/js/components/SearchPage/SearchPageForm.jsx
  35. 0 2
      src/client/js/components/SearchPage/SearchResult.jsx
  36. 2 2
      src/client/js/components/SearchPage/SearchResultList.jsx
  37. 0 0
      src/client/js/components/SearchTypeahead.jsx
  38. 3 3
      src/client/js/components/StaffCredit/StaffCredit.jsx
  39. 21 22
      src/client/js/legacy/crowi.js
  40. 4 2
      src/client/js/models/MarkdownTable.js
  41. 4 0
      src/client/styles/scss/_login.scss
  42. 8 5
      src/client/styles/scss/_on-edit.scss
  43. 6 4
      src/client/styles/scss/_search.scss
  44. 3 1
      src/client/styles/scss/_vendor.scss
  45. 15 12
      src/lib/service/cdn-resources-service.js
  46. 3 1
      src/lib/service/logger/stream.prod.js
  47. 32 0
      src/migrations/20190619055421-adjust-page-grant.js
  48. 1 1
      src/server/form/admin/securityGeneral.js
  49. 10 2
      src/server/models/GlobalNotificationSetting/index.js
  50. 1 1
      src/server/models/config.js
  51. 5 1
      src/server/models/page-tag-relation.js
  52. 18 14
      src/server/models/page.js
  53. 28 47
      src/server/models/user.js
  54. 30 19
      src/server/routes/admin.js
  55. 12 7
      src/server/routes/page.js
  56. 38 12
      src/server/service/acl.js
  57. 9 1
      src/server/service/config-loader.js
  58. 4 2
      src/server/util/middlewares.js
  59. 1 1
      src/server/util/search.js
  60. 1 1
      src/server/util/swigFunctions.js
  61. 3 3
      src/server/views/admin/external-accounts.html
  62. 12 4
      src/server/views/admin/security.html
  63. 4 4
      src/server/views/admin/user-group-detail.html
  64. 1 1
      src/server/views/installer.html
  65. 1 1
      src/server/views/layout/layout.html
  66. 1 1
      src/server/views/login.html
  67. 1 1
      src/server/views/me/external-accounts.html
  68. 13 11
      src/server/views/modal/delete.html
  69. 21 9
      src/server/views/modal/rename.html
  70. 1 1
      src/server/views/search.html
  71. 1 1
      src/server/views/widget/page_tabs.html
  72. 1 1
      src/server/views/widget/page_tabs_kibela.html
  73. 0 8
      src/test/models/page.test.js
  74. 22 20
      src/test/models/user.test.js
  75. 205 0
      src/test/service/acl.test.js
  76. 186 0
      src/test/util/middlewares.test.js
  77. 202 189
      yarn.lock

+ 0 - 4
.eslintrc.js

@@ -30,10 +30,6 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
         FunctionExpression: { body: 1, parameters: 2 },
       },
       },
     ],
     ],
-    'react/jsx-filename-extension': [
-      'warn',
-      { extensions: ['.jsx']},
-    ],
     // eslint-plugin-import rules
     // eslint-plugin-import rules
     'import/no-unresolved': [2, { ignore: ['^@'] }], // ignore @alias/..., @commons/..., ...
     'import/no-unresolved': [2, { ignore: ['^@'] }], // ignore @alias/..., @commons/..., ...
   },
   },

+ 48 - 9
CHANGES.md

@@ -1,20 +1,41 @@
 # CHANGES
 # CHANGES
 
 
-## 3.5.1-RC
+## 3.5.5-RC
 
 
-### BREAKING CHANGES
+* Fix: Profile images are broken in User Management
+* Support: Upgrade libs
+    * csv-to-markdown-table
+    * express-validator
+    * markdown-it
+    * mini-css-extract-plugin
+    * react-hotkeys
 
 
-* GROWI no longer supports plugins with schema version 2
-* The restriction mode of the root page (`/`) will be set 'Public'
-* The restriction mode of the root page (`/`) can not be changed after v 3.5.1
+## 3.5.4
 
 
-### Updates
+* Fix: List private pages wrongly
+* Fix: Global Notification Trigger Path does not parse glob correctly
+* Fix: Consecutive page deletion requests cause unexpected complete page deletion
 
 
-* Support: Use Babel 7
-* Support: Support plugins with schema version 3
+## 3.5.3
+
+* Improvement: Calculate string width when save with Spreadsheet like GUI (Handsontable)
+* Fix: Search Result Page doesn't work
+* Fix: Create/Update page API returns data includes author's password hash
+* Fix: Dropdown to copy page path/URL/MarkdownLink shows under CodeMirror vscrollbar
+* Fix: Link to /trash in Dropdown menu
 
 
+## 3.5.2
 
 
-## 3.5.0
+* Feature: Remain metadata option when Move/Rename page
+* Improvement: Support code highlight for Swift and Kotlin
+* Fix: Couldn't restrict page with user group permission
+* Fix: Couldn't duplicate a page when it restricted by a user group permission
+* Fix: Consider timezone on admin page
+* Fix: Editor doesn't work on Microsoft Edge
+* Support: Upgrade libs
+    * growi-commons
+
+## 3.5.1
 
 
 ### BREAKING CHANGES
 ### BREAKING CHANGES
 
 
@@ -22,6 +43,13 @@
     * Protection system with Basic Authentication
     * Protection system with Basic Authentication
     * Crowi Classic Authentication Mechanism
     * Crowi Classic Authentication Mechanism
     * [Crowi Template syntax](https://medium.com/crowi-book/crowi-v1-5-0-5a62e7c6be90)
     * [Crowi Template syntax](https://medium.com/crowi-book/crowi-v1-5-0-5a62e7c6be90)
+* GROWI no lonnger supports plugins with schema version 2
+    * Upgrade [weseek/growi-plugin-lsx](https://github.com/weseek/growi-plugin-lsx) to v3.0.0 or above
+    * Upgrade [weseek/growi-plugin-pukiwiki-like-linker
+](https://github.com/weseek/growi-plugin-pukiwiki-like-linker
+) to v3.0.0 or above
+* The restriction mode of the root page (`/`) will be set 'Public'
+* The restriction mode of the root page (`/`) can not be changed after v 3.5.1
 
 
 Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
 Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
 
 
@@ -35,15 +63,24 @@ Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
 * Improvement Draft list
 * Improvement Draft list
 * Fix: Deleting page completely
 * Fix: Deleting page completely
 * Fix: Search with `prefix:` param with CJK pathname
 * Fix: Search with `prefix:` param with CJK pathname
+* Fix: Could not edit UserGroup even if `PUBLIC_WIKI_ONLY` is not set
 * I18n: User Management Details
 * I18n: User Management Details
 * I18n: Group Management Details
 * I18n: Group Management Details
 * Support: Apply unstated
 * Support: Apply unstated
+* Support: Use Babel 7
+* Support: Support plugins with schema version 3
 * Support: Abolish Old Config API
 * Support: Abolish Old Config API
 * Support: Apply Jest for Tests
 * Support: Apply Jest for Tests
 * Support: Upgrade libs
 * Support: Upgrade libs
     * async
     * async
     * axios
     * axios
     * connect-mongo
     * connect-mongo
+    * css-loader
+    * eslint
+    * eslint-config-weseek
+    * eslint-plugin-import
+    * eslint-plugin-jest
+    * eslint-plugin-react
     * file-loader
     * file-loader
     * googleapis
     * googleapis
     * i18next
     * i18next
@@ -54,6 +91,8 @@ Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
     * mongoose-unique-validator
     * mongoose-unique-validator
     * null-loader
     * null-loader
 
 
+## 3.5.0 (Missing number)
+
 ## 3.4.7
 ## 3.4.7
 
 
 * Improvement: Handle private pages on group deletion
 * Improvement: Handle private pages on group deletion

+ 5 - 0
README.md

@@ -172,6 +172,11 @@ Environment Variables
     * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
     * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
     * 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.
     * 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`.
     * 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**
 * **Option to integrate with external systems**
     * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
     * 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/guide/admin-cookbook/integrate-with-hackmd.html).**
         * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html).**

+ 3 - 1
bin/download-cdn-resources.js

@@ -7,8 +7,10 @@ require('module-alias/register');
 
 
 const logger = require('@alias/logger')('growi:bin:download-cdn-resources');
 const logger = require('@alias/logger')('growi:bin:download-cdn-resources');
 
 
+const { envUtils } = require('growi-commons');
+
 // check env var
 // check env var
-const noCdn = !!process.env.NO_CDN;
+const noCdn = envUtils.toBoolean(process.env.NO_CDN);
 if (!noCdn) {
 if (!noCdn) {
   logger.info('Using CDN. No resources are downloaded.');
   logger.info('Using CDN. No resources are downloaded.');
   // exit
   // exit

+ 1 - 1
config/env.dev.js

@@ -13,5 +13,5 @@ module.exports = {
   // PUBLISH_OPEN_API: true,
   // PUBLISH_OPEN_API: true,
   // USER_UPPER_LIMIT: 0,
   // USER_UPPER_LIMIT: 0,
   // DEV_HTTPS: true,
   // DEV_HTTPS: true,
-  // PUBLIC_WIKI_ONLY: true,
+  // FORCE_WIKI_MODE: 'private', // 'public', 'private', undefined
 };
 };

+ 2 - 5
config/jest.config.js

@@ -7,11 +7,6 @@ module.exports = {
 
 
   rootDir: '../',
   rootDir: '../',
 
 
-  // Automatically clear mock calls and instances between every test
-  clearMocks: true,
-  // Automatically reset mock state between every test
-  resetMocks: true,
-
   projects: [
   projects: [
     {
     {
       displayName: 'server',
       displayName: 'server',
@@ -19,6 +14,8 @@ module.exports = {
       rootDir: '.',
       rootDir: '.',
       setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
       setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
       testMatch: ['<rootDir>/src/test/**/*.test.js'],
       testMatch: ['<rootDir>/src/test/**/*.test.js'],
+      // Automatically clear mock calls and instances between every test
+      clearMocks: true,
       // A map from regular expressions to module names that allow to stub out resources with a single module
       // A map from regular expressions to module names that allow to stub out resources with a single module
       moduleNameMapper: {
       moduleNameMapper: {
         '@root/(.+)': '<rootDir>/$1',
         '@root/(.+)': '<rootDir>/$1',

+ 18 - 16
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.5.1-RC",
+  "version": "3.5.5-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -32,8 +32,8 @@
     "clean:report": "rimraf -- report",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "clean": "npm-run-all -p clean:*",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
-    "lint:js:fix": "eslint '**/*.{js,jsx}' --fix",
-    "lint:js": "eslint '**/*.{js,jsx}'",
+    "lint:js:fix": "eslint \"**/*.{js,jsx}\" --fix",
+    "lint:js": "eslint \"**/*.{js,jsx}\"",
     "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
     "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint": "npm-run-all -p lint:js lint:styles",
     "lint": "npm-run-all -p lint:js lint:styles",
@@ -62,6 +62,7 @@
   "dependencies": {
   "dependencies": {
     "//": [
     "//": [
       "check-node-version: see https://github.com/parshap/check-node-version/issues/35",
       "check-node-version: see https://github.com/parshap/check-node-version/issues/35",
+      "entities: markdown-it@9.0.1 depends on entities@~1.1.1",
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0"
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0"
     ],
     ],
     "async": "^3.0.1",
     "async": "^3.0.1",
@@ -89,10 +90,10 @@
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
     "express-sanitizer": "^1.0.4",
     "express-sanitizer": "^1.0.4",
     "express-session": "^1.16.1",
     "express-session": "^1.16.1",
-    "express-validator": "^5.3.1",
+    "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^4.0.1",
+    "growi-commons": "^4.0.3",
     "helmet": "^3.13.0",
     "helmet": "^3.13.0",
     "i18next": "^17.0.3",
     "i18next": "^17.0.3",
     "i18next-express-middleware": "^1.4.1",
     "i18next-express-middleware": "^1.4.1",
@@ -152,28 +153,29 @@
     "colors": "^1.2.5",
     "colors": "^1.2.5",
     "commander": "^2.11.0",
     "commander": "^2.11.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "core-js": "^2.6.9",
-    "css-loader": "^1.0.0",
-    "csv-to-markdown-table": "^0.5.0",
+    "core-js": "=2.6.9",
+    "css-loader": "^3.0.0",
+    "csv-to-markdown-table": "^1.0.1",
     "date-fns": "^1.29.0",
     "date-fns": "^1.29.0",
     "diff2html": "^2.3.3",
     "diff2html": "^2.3.3",
     "eazy-logger": "^3.0.2",
     "eazy-logger": "^3.0.2",
-    "eslint": "^5.15.1",
-    "eslint-config-weseek": "^1.0.1",
-    "eslint-plugin-import": "^2.16.0",
-    "eslint-plugin-jest": "^22.6.4",
-    "eslint-plugin-react": "^7.12.4",
+    "eslint": "^6.0.1",
+    "eslint-config-weseek": "^1.0.2",
+    "eslint-plugin-import": "^2.18.0",
+    "eslint-plugin-jest": "^22.7.1",
+    "eslint-plugin-react": "^7.14.2",
     "file-loader": "^4.0.0",
     "file-loader": "^4.0.0",
     "handsontable": "^6.0.1",
     "handsontable": "^6.0.1",
     "i18next-browser-languagedetector": "^3.0.1",
     "i18next-browser-languagedetector": "^3.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
     "jest": "^24.8.0",
     "jest": "^24.8.0",
+    "jest-each": "^24.8.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "lodash-webpack-plugin": "^0.11.5",
-    "markdown-it": "^8.4.0",
+    "markdown-it": "^9.0.1",
     "markdown-it-blockdiag": "^1.0.2",
     "markdown-it-blockdiag": "^1.0.2",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-footnote": "^3.0.1",
@@ -184,7 +186,7 @@
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "markdown-table": "^1.1.1",
     "metismenu": "^3.0.3",
     "metismenu": "^3.0.3",
-    "mini-css-extract-plugin": "^0.7.0",
+    "mini-css-extract-plugin": "^0.8.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
     "node-dev": "^4.0.0",
     "node-sass": "^4.11.0",
     "node-sass": "^4.11.0",
@@ -204,7 +206,7 @@
     "react-dom": "^16.8.3",
     "react-dom": "^16.8.3",
     "react-dropzone": "^10.1.3",
     "react-dropzone": "^10.1.3",
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
-    "react-hotkeys": "^1.1.4",
+    "react-hotkeys": "^2.0.0",
     "react-i18next": "^10.6.1",
     "react-i18next": "^10.6.1",
     "react-waypoint": "^9.0.0",
     "react-waypoint": "^9.0.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",

+ 2 - 0
resource/cdn-manifests.js

@@ -28,6 +28,8 @@ module.exports = {
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/scss.min.js,'
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/scss.min.js,'
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/typescript.min.js,'
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/typescript.min.js,'
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js,'
         + 'gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/swift.min.js,'
+        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/kotlin.min.js,'
         + 'npm/highlightjs-line-numbers.js@2.6.0/dist/highlightjs-line-numbers.min.js',
         + 'npm/highlightjs-line-numbers.js@2.6.0/dist/highlightjs-line-numbers.min.js',
       args: {
       args: {
         async: true,
         async: true,

+ 21 - 20
resource/locales/en-US/translation.json

@@ -6,7 +6,7 @@
   "Duplicate": "Duplicate",
   "Duplicate": "Duplicate",
   "Copy": "Copy",
   "Copy": "Copy",
   "Click to copy": "Click to copy",
   "Click to copy": "Click to copy",
-  "Move": "Move",
+  "Move/Rename": "Move/Rename",
   "Moved": "Moved",
   "Moved": "Moved",
   "Unlinked": "Unlinked",
   "Unlinked": "Unlinked",
   "Like!": "Like!",
   "Like!": "Like!",
@@ -27,6 +27,8 @@
   "Category": "Category",
   "Category": "Category",
   "User": "User",
   "User": "User",
   "status":"Status",
   "status":"Status",
+  "account_id": "Account Id",
+
 
 
   "Update": "Update",
   "Update": "Update",
   "Update Page": "Update Page",
   "Update Page": "Update Page",
@@ -105,13 +107,12 @@
   "Customize": "Customize",
   "Customize": "Customize",
   "Notification Settings": "Notification Settings",
   "Notification Settings": "Notification Settings",
   "User_Management": "User Management",
   "User_Management": "User Management",
-  "External Account management": "External Account management",
+  "external_account_management": "External Account Management",
   "UserGroup Management": "UserGroup Management",
   "UserGroup Management": "UserGroup Management",
   "Full Text Search Management": "Full Text Search Management",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Import Data": "Import Data",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
   "Basic authentication": "Basic authentication",
-  "Guest users access": "Guest users access",
   "Register limitation": "Register limitation",
   "Register limitation": "Register limitation",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "Public": "Public",
   "Public": "Public",
@@ -267,15 +268,17 @@
 
 
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
-      "Rename page": "Rename page",
+      "Move/Rename page": "Move/Rename page",
       "New page name": "New page name",
       "New page name": "New page name",
       "Current page name": "Current page name",
       "Current page name": "Current page name",
-      "Move recursively": "Move recursively",
+      "Recursively": "Recursively",
+      "Do not update metadata": "Do not update metadata",
       "Redirect": "Redirect"
       "Redirect": "Redirect"
     },
     },
     "help": {
     "help": {
       "redirect": "Redirect to new page if someone accesses <code>%s</code>",
       "redirect": "Redirect to new page if someone accesses <code>%s</code>",
-      "recursive": "Rename children of under <code>%s</code> recursively"
+      "metadata": "Remains last update user and updated date as is",
+      "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
     }
   },
   },
 
 
@@ -285,9 +288,9 @@
   "modal_delete": {
   "modal_delete": {
     "delete_page": "Delete Page",
     "delete_page": "Delete Page",
     "deleting_page": "Deleting Page",
     "deleting_page": "Deleting Page",
-    "delete_recursively": "Delete child pages under %s recursively.",
+    "delete_recursively": "Delete child pages recursively.",
     "delete_completely": "Delete Completely",
     "delete_completely": "Delete Completely",
-    "delete_completely_restriction": "You have no admin to delete completely.",
+    "delete_completely_restriction": "You don't have the authority to delete pages completely.",
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
@@ -433,14 +436,12 @@
   },
   },
 
 
   "security_setting": {
   "security_setting": {
-		"Basic authentication": "Basic Authentication",
 		"Security settings": "Security settings",
 		"Security settings": "Security settings",
-		"Guest users access": "Guest users access",
-		"Register limitation": "Register limitation",
+    "Guest Users Access": "Guest Users Access",
+    "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
+    "Register limitation": "Register limitation",
+    "Register limitation desc": "Restricts ways to register new user.",
 		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
 		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-		"common_authentication": "If you set the basic authentication, common authentication is applied on the whole page.",
-		"without_encryption": "Please be noted that your ID and Password will be sent wihtout encryption.",
-		"basic_acl_disable": "Because of Public Wiki  setting, basic authentication can not be used.",
 		"users_without_account": "Users without account is not accessible",
 		"users_without_account": "Users without account is not accessible",
     "example": "Example",
     "example": "Example",
     "restrict_emails": "You can restrict registerable e-mail address.",
     "restrict_emails": "You can restrict registerable e-mail address.",
@@ -469,13 +470,13 @@
     "clientID": "Client ID",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
     "client_secret": "Client Secret",
     "guest_mode": {
     "guest_mode": {
-      "deny": "Deny Unregistered Users",
-      "readonly": "View Only"
+      "deny": "Deny (Registered Users Only)",
+      "readonly": "Accept (Guests can read only)"
     },
     },
     "registration_mode": {
     "registration_mode": {
-      "open": "Anyone",
-      "restricted": "Require Admin permission",
-      "closed": "Invitation Only"
+      "open": "Open (Anyone can registre)",
+      "restricted": "Restricted (Requires approval by administrators)",
+      "closed": "Closed (Invitation Only)"
     },
     },
     "configuration": " Configuration",
     "configuration": " Configuration",
     "optional": "Optional",
     "optional": "Optional",
@@ -718,7 +719,7 @@
     "no_pages": "There are no pages the group has view permission",
     "no_pages": "There are no pages the group has view permission",
     "how_to_add1": "Enter a username to add",
     "how_to_add1": "Enter a username to add",
     "how_to_add2": "Select a user from user list",
     "how_to_add2": "Select a user from user list",
-    "remove_from_group": "Remove this group"
+    "remove_from_group": "Remove this user"
   },
   },
 
 
   "importer_management": {
   "importer_management": {

+ 1 - 1
resource/locales/en-US/welcome.md

@@ -7,7 +7,7 @@
   <div class="panel-heading">Tips</div>
   <div class="panel-heading">Tips</div>
   <div class="panel-body"><ul>
   <div class="panel-body"><ul>
     <li>Ctrl(⌘)-/ to show quick help</li>
     <li>Ctrl(⌘)-/ to show quick help</li>
-    <li>You can <a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> to write HTML tags.</li>
+    <li>You can write HTML with <a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a>.</li>
   </ul></div>
   </ul></div>
 </div>
 </div>
 
 

+ 15 - 15
resource/locales/ja/translation.json

@@ -6,7 +6,7 @@
   "Duplicate": "複製",
   "Duplicate": "複製",
   "Copy": "コピー",
   "Copy": "コピー",
   "Click to copy": "クリックでコピー",
   "Click to copy": "クリックでコピー",
-  "Move": "移動",
+  "Move/Rename": "移動/名前変更",
   "Moved": "移動しました",
   "Moved": "移動しました",
   "Unlinked": "リダイレクト削除",
   "Unlinked": "リダイレクト削除",
   "Like!": "いいね!",
   "Like!": "いいね!",
@@ -27,6 +27,7 @@
   "Category": "カテゴリー",
   "Category": "カテゴリー",
   "User": "ユーザー",
   "User": "ユーザー",
   "status": "ステータス",
   "status": "ステータス",
+  "account_id": "アカウントID",
 
 
   "Update": "更新",
   "Update": "更新",
   "Update Page": "ページを更新",
   "Update Page": "ページを更新",
@@ -105,13 +106,11 @@
   "Customize": "カスタマイズ",
   "Customize": "カスタマイズ",
   "Notification Settings": "通知設定",
   "Notification Settings": "通知設定",
   "User_Management": "ユーザー管理",
   "User_Management": "ユーザー管理",
-  "External Account management": "外部アカウント管理",
+  "external_account_management": "外部アカウント管理",
   "UserGroup Management": "グループ管理",
   "UserGroup Management": "グループ管理",
   "Full Text Search Management": "全文検索管理",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
   "Import Data": "データインポート",
   "Basic Settings": "基本設定",
   "Basic Settings": "基本設定",
-  "Basic authentication": "Basic認証",
-  "Guest users access": "ゲストユーザーのアクセス",
   "Register limitation": "登録の制限",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "Public": "公開",
   "Public": "公開",
@@ -267,15 +266,17 @@
 
 
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
-      "Rename page": "ページを移動する",
+      "Move/Rename page": "ページを移動/名前変更する",
       "New page name": "移動先のページ名",
       "New page name": "移動先のページ名",
       "Current page name": "現在のページ名",
       "Current page name": "現在のページ名",
-      "Move recursively": "再帰的に移動",
+      "Recursively": "再帰的に移動/名前変更",
+      "Do not update metadata": "メタデータを更新しない",
       "Redirect": "リダイレクトする"
       "Redirect": "リダイレクトする"
     },
     },
     "help": {
     "help": {
       "redirect": "<code>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
       "redirect": "<code>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
-      "recursive": "<code>%s</code> 配下のページも移動します"
+      "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
+      "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
     }
     }
   },
   },
 
 
@@ -433,13 +434,11 @@
    },
    },
 
 
   "security_setting": {
   "security_setting": {
-    "Basic authentication": "Basic認証",
-    "Guest users access": "ゲストユーザーのアクセス",
+    "Guest Users Access": "ゲストユーザーのアクセス",
+    "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
     "Register limitation": "登録の制限",
     "Register limitation": "登録の制限",
+    "Register limitation desc": "新しいユーザーを登録する方法を制限します.",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
-    "common_authentication": "Basic認証を設定すると、ページ全体に共通の認証がかかります。",
-    "without_encryption": "IDとパスワードは暗号化されずに送信されるのでご注意下さい。",
-    "basic_acl_disable": "Public Wiki の設定のため、Basic認証は利用できません。",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
@@ -465,8 +464,8 @@
     "clientID": "クライアントID",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
     "client_secret": "クライアントシークレット",
     "guest_mode": {
     "guest_mode": {
-      "deny": "アカウントを持たないユーザーはアクセス不可",
-      "readonly": "閲覧のみ可"
+      "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
+      "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
     },
     },
     "registration_mode": {
     "registration_mode": {
       "open": "公開 (だれでも登録可能)",
       "open": "公開 (だれでも登録可能)",
@@ -690,7 +689,8 @@
     "group_example": "例: Group1",
     "group_example": "例: Group1",
     "created_group": "グループを作成しました",
     "created_group": "グループを作成しました",
     "add_user": "グループへのユーザー追加",
     "add_user": "グループへのユーザー追加",
-    "deny_create_group": "現在の設定では新規グループの作成はできません。",
+    "deny_create_group": "新規グループの作成はできません。",
+    "is_loading_data": "データを取得中です...",
     "choose_action": "削除するグループの限定公開ページの処理を選択してください",
     "choose_action": "削除するグループの限定公開ページの処理を選択してください",
     "delete_group": "グループの削除",
     "delete_group": "グループの削除",
     "group_name": "グループ名",
     "group_name": "グループ名",

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

@@ -1,5 +1,3 @@
-/* eslint-disable max-len */
-
 import React from 'react';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { Provider } from 'unstated';

+ 0 - 0
src/client/js/components/Admin/CustomCssEditor.js → src/client/js/components/Admin/CustomCssEditor.jsx


+ 0 - 0
src/client/js/components/Admin/CustomHeaderEditor.js → src/client/js/components/Admin/CustomHeaderEditor.jsx


+ 0 - 0
src/client/js/components/Admin/CustomScriptEditor.js → src/client/js/components/Admin/CustomScriptEditor.jsx


+ 29 - 25
src/client/js/components/InstallerForm.jsx

@@ -1,8 +1,12 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+
 import i18next from 'i18next';
 import i18next from 'i18next';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import Radio from 'react-bootstrap/es/Radio';
+
 class InstallerForm extends React.Component {
 class InstallerForm extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -10,6 +14,7 @@ class InstallerForm extends React.Component {
 
 
     this.state = {
     this.state = {
       isValidUserName: true,
       isValidUserName: true,
+      checkedBtn: 'en-US',
     };
     };
     this.checkUserName = this.checkUserName.bind(this);
     this.checkUserName = this.checkUserName.bind(this);
   }
   }
@@ -32,6 +37,7 @@ class InstallerForm extends React.Component {
 
 
   changeLanguage(locale) {
   changeLanguage(locale) {
     i18next.changeLanguage(locale);
     i18next.changeLanguage(locale);
+    this.setState({ checkedBtn: locale });
   }
   }
 
 
   render() {
   render() {
@@ -40,6 +46,8 @@ class InstallerForm extends React.Component {
       ? ''
       ? ''
       : <span><i className="icon-fw icon-ban" />{ this.props.t('installer.unavaliable_user_id') }</span>;
       : <span><i className="icon-fw icon-ban" />{ this.props.t('installer.unavaliable_user_id') }</span>;
 
 
+    const checkedBtn = this.state.checkedBtn;
+
     return (
     return (
       <div className={`login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4${hasErrorClass}`}>
       <div className={`login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4${hasErrorClass}`}>
         <p className="alert alert-success">
         <p className="alert alert-success">
@@ -48,30 +56,26 @@ class InstallerForm extends React.Component {
         </p>
         </p>
 
 
         <form role="form" action="/installer" method="post" id="register-form">
         <form role="form" action="/installer" method="post" id="register-form">
-          <div className="input-group m-t-20 m-b-20 mx-auto">
-            <div className="radio radio-primary radio-inline">
-              <input
-                type="radio"
-                id="radioLangEn"
-                name="registerForm[app:globalLang]"
-                value="en-US"
-                defaultChecked
-                onClick={() => { return this.changeLanguage('en-US') }}
-              />
-              <label htmlFor="radioLangEn">English</label>
-            </div>
-            <div className="radio radio-primary radio-inline">
-              <input
-                type="radio"
-                id="radioLangJa"
-                name="registerForm[app:globalLang]"
-                value="ja"
-                defaultChecked={false}
-                onClick={() => { return this.changeLanguage('ja') }}
-              />
-              <label htmlFor="radioLangJa">日本語</label>
-            </div>
-          </div>
+          <FormGroup className="text-center">
+            <Radio
+              name="registerForm[app:globalLang]"
+              value="en-US"
+              checked={checkedBtn === 'en-US'}
+              inline
+              onClick={() => { return this.changeLanguage('en-US') }}
+            >
+              English
+            </Radio>
+            <Radio
+              name="registerForm[app:globalLang]"
+              value="ja"
+              checked={checkedBtn === 'ja'}
+              inline
+              onClick={() => { return this.changeLanguage('ja') }}
+            >
+              日本語
+            </Radio>
+          </FormGroup>
 
 
           <div className={`input-group${hasErrorClass}`}>
           <div className={`input-group${hasErrorClass}`}>
             <span className="input-group-addon"><i className="icon-user" /></span>
             <span className="input-group-addon"><i className="icon-user" /></span>
@@ -127,7 +131,7 @@ class InstallerForm extends React.Component {
           <div className="input-group m-t-30 m-b-20 d-flex justify-content-center">
           <div className="input-group m-t-30 m-b-20 d-flex justify-content-center">
             <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
             <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
               <span className="btn-label"><i className="icon-user-follow" /></span>
               <span className="btn-label"><i className="icon-user-follow" /></span>
-              { this.props.t('Create') }
+              <span className="btn-label-text">{ this.props.t('Create') }</span>
             </button>
             </button>
           </div>
           </div>
 
 

+ 0 - 0
src/client/js/components/Page/PagePath.js → src/client/js/components/Page/PagePath.jsx


+ 0 - 0
src/client/js/components/Page/RevisionBody.js → src/client/js/components/Page/RevisionBody.jsx


+ 12 - 18
src/client/js/components/Page/RevisionLoader.jsx

@@ -35,7 +35,7 @@ class RevisionLoader extends React.Component {
     }
     }
   }
   }
 
 
-  loadData() {
+  async loadData() {
     if (!this.state.isLoaded && !this.state.isLoading) {
     if (!this.state.isLoaded && !this.state.isLoading) {
       this.setState({ isLoading: true });
       this.setState({ isLoading: true });
     }
     }
@@ -46,23 +46,17 @@ class RevisionLoader extends React.Component {
     };
     };
 
 
     // load data with REST API
     // load data with REST API
-    this.props.appContainer.apiGet('/revisions.get', requestData)
-      .then((res) => {
-        if (!res.ok) {
-          throw new Error(res.error);
-        }
-
-        this.setState({
-          markdown: res.revision.body,
-          error: null,
-        });
-      })
-      .catch((err) => {
-        this.setState({ error: err });
-      })
-      .finally(() => {
-        this.setState({ isLoaded: true, isLoading: false });
-      });
+    const res = await this.props.appContainer.apiGet('/revisions.get', requestData);
+    this.setState({ isLoaded: true, isLoading: false });
+
+    if (res != null && !res.ok) {
+      throw new Error(res.error);
+    }
+
+    this.setState({
+      markdown: res.revision.body,
+      error: null,
+    });
   }
   }
 
 
   onWaypointChange(event) {
   onWaypointChange(event) {

+ 0 - 0
src/client/js/components/PageAttachment/Attachment.js → src/client/js/components/PageAttachment/Attachment.jsx


+ 0 - 0
src/client/js/components/PageAttachment/DeleteAttachmentModal.js → src/client/js/components/PageAttachment/DeleteAttachmentModal.jsx


+ 0 - 0
src/client/js/components/PageAttachment/PageAttachmentList.js → src/client/js/components/PageAttachment/PageAttachmentList.jsx


+ 3 - 2
src/client/js/components/PageEditor.jsx

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
+import { envUtils } from 'growi-commons';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
@@ -161,7 +162,7 @@ class PageEditor extends React.Component {
 
 
       // when if created newly
       // when if created newly
       if (res.pageCreated) {
       if (res.pageCreated) {
-        logger.info('Page is created', res.pageCreated._id);
+        logger.info('Page is created', res.page._id);
         pageContainer.updateStateAfterSave(res.page);
         pageContainer.updateStateAfterSave(res.page);
       }
       }
     }
     }
@@ -321,7 +322,7 @@ class PageEditor extends React.Component {
 
 
   render() {
   render() {
     const config = this.props.appContainer.getConfig();
     const config = this.props.appContainer.getConfig();
-    const noCdn = !!config.env.NO_CDN;
+    const noCdn = envUtils.toBoolean(config.env.NO_CDN);
     const emojiStrategy = this.props.appContainer.getEmojiStrategy();
     const emojiStrategy = this.props.appContainer.getEmojiStrategy();
 
 
     return (
     return (

+ 0 - 0
src/client/js/components/PageEditor/MarkdownTableUtil.jsx → src/client/js/components/PageEditor/MarkdownTableUtil.js


+ 2 - 2
src/client/js/components/PageEditor/ScrollSyncHelper.js

@@ -144,7 +144,7 @@ class ScrollSyncHelper {
 
 
       scrollTo -= this.getParentElementOffset(previewElement);
       scrollTo -= this.getParentElementOffset(previewElement);
 
 
-      previewElement.scroll(0, previewElement.scrollTop + scrollTo);
+      previewElement.scrollTop += scrollTo;
     }
     }
   }
   }
 
 
@@ -176,7 +176,7 @@ class ScrollSyncHelper {
         return;
         return;
       }
       }
 
 
-      previewElement.scroll(0, scrollTo);
+      previewElement.scrollTop = scrollTo;
     }
     }
   }
   }
 
 

+ 0 - 0
src/client/js/components/PageList/ListView.js → src/client/js/components/PageList/ListView.jsx


+ 9 - 1
src/client/js/components/PageList/Page.js → src/client/js/components/PageList/Page.jsx

@@ -18,6 +18,8 @@ export default class Page extends React.Component {
       flex: 1,
       flex: 1,
     };
     };
 
 
+    const hasChildren = this.props.children != null;
+
     return (
     return (
       <li className="page-list-li d-flex align-items-center">
       <li className="page-list-li d-flex align-items-center">
         <UserPicture user={page.lastUpdateUser} />
         <UserPicture user={page.lastUpdateUser} />
@@ -25,7 +27,12 @@ export default class Page extends React.Component {
           <PagePath page={page} excludePathString={this.props.excludePathString} />
           <PagePath page={page} excludePathString={this.props.excludePathString} />
         </a>
         </a>
         <PageListMeta page={page} />
         <PageListMeta page={page} />
-        <div style={styleFlex}></div>
+        { hasChildren && (
+          <React.Fragment>
+            <a style={styleFlex} href={link}>&nbsp;</a>
+            {this.props.children}
+          </React.Fragment>
+        ) }
       </li>
       </li>
     );
     );
   }
   }
@@ -36,6 +43,7 @@ Page.propTypes = {
   page: PropTypes.object.isRequired,
   page: PropTypes.object.isRequired,
   linkTo: PropTypes.string,
   linkTo: PropTypes.string,
   excludePathString: PropTypes.string,
   excludePathString: PropTypes.string,
+  children: PropTypes.array,
 };
 };
 
 
 Page.defaultProps = {
 Page.defaultProps = {

+ 0 - 0
src/client/js/components/PageList/PageListMeta.js → src/client/js/components/PageList/PageListMeta.jsx


+ 0 - 0
src/client/js/components/PageList/PagePath.js → src/client/js/components/PageList/PagePath.jsx


+ 1 - 1
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -277,7 +277,7 @@ class GrantSelector extends React.Component {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
         { this.renderGrantSelector() }
         { this.renderGrantSelector() }
-        { this.props.disabled && this.renderSelectGroupModal() }
+        { !this.props.disabled && this.renderSelectGroupModal() }
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 0 - 0
src/client/js/components/SearchForm.js → src/client/js/components/SearchForm.jsx


+ 0 - 0
src/client/js/components/SearchPage.js → src/client/js/components/SearchPage.jsx


+ 0 - 0
src/client/js/components/SearchPage/DeletePageListModal.js → src/client/js/components/SearchPage/DeletePageListModal.jsx


+ 0 - 0
src/client/js/components/SearchPage/SearchPageForm.js → src/client/js/components/SearchPage/SearchPageForm.jsx


+ 0 - 2
src/client/js/components/SearchPage/SearchResult.js → src/client/js/components/SearchPage/SearchResult.jsx

@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/label-has-associated-control */
-/* eslint-disable jsx-a11y/label-has-for */
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';

+ 2 - 2
src/client/js/components/SearchPage/SearchResultList.js → src/client/js/components/SearchPage/SearchResultList.jsx

@@ -16,8 +16,8 @@ class SearchResultList extends React.Component {
   render() {
   render() {
     const resultList = this.props.pages.map((page) => {
     const resultList = this.props.pages.map((page) => {
       return (
       return (
-        <div id={page._id} key={page._id} className="search-result-page">
-          <h2 className="inline"><a href={page.path}>{page.path}</a></h2>
+        <div id={page._id} key={page._id} className="search-result-page mb-5">
+          <h2><a href={page.path}>{page.path}</a></h2>
           { page.tags.length > 0 && (
           { page.tags.length > 0 && (
             <span><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</span>
             <span><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</span>
           )}
           )}

+ 0 - 0
src/client/js/components/SearchTypeahead.js → src/client/js/components/SearchTypeahead.jsx


+ 3 - 3
src/client/js/components/StaffCredit/StaffCredit.jsx

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { HotKeys } from 'react-hotkeys';
+import { GlobalHotKeys } from 'react-hotkeys';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
@@ -112,9 +112,9 @@ export default class StaffCredit extends React.Component {
     const keyMap = { check: ['up', 'down', 'right', 'left', 'b', 'a'] };
     const keyMap = { check: ['up', 'down', 'right', 'left', 'b', 'a'] };
     const handlers = { check: (event) => { return this.check(event) } };
     const handlers = { check: (event) => { return this.check(event) } };
     return (
     return (
-      <HotKeys focused attach={window} keyMap={keyMap} handlers={handlers}>
+      <GlobalHotKeys keyMap={keyMap} handlers={handlers}>
         {this.renderContributors()}
         {this.renderContributors()}
-      </HotKeys>
+      </GlobalHotKeys>
     );
     );
   }
   }
 
 

+ 21 - 22
src/client/js/legacy/crowi.js

@@ -1,5 +1,4 @@
-/* global location */
-/* eslint no-restricted-globals: ['error', 'locaion'] */
+/* eslint-disable react/jsx-filename-extension */
 
 
 import React from 'react';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
@@ -341,7 +340,7 @@ $(() => {
     if (input2 === '') {
     if (input2 === '') {
       prefix2 = prefix2.slice(0, -1);
       prefix2 = prefix2.slice(0, -1);
     }
     }
-    top.location.href = `${prefix1 + input1 + prefix2 + input2}#edit`;
+    window.location.href = `${prefix1 + input1 + prefix2 + input2}#edit`;
     return false;
     return false;
   });
   });
 
 
@@ -353,7 +352,7 @@ $(() => {
     if (name.match(/.+\/$/)) {
     if (name.match(/.+\/$/)) {
       name = name.substr(0, name.length - 1);
       name = name.substr(0, name.length - 1);
     }
     }
-    top.location.href = `${pathUtils.encodePagePath(name)}#edit`;
+    window.location.href = `${pathUtils.encodePagePath(name)}#edit`;
     return false;
     return false;
   });
   });
 
 
@@ -388,7 +387,7 @@ $(() => {
         }
         }
         else {
         else {
           const page = res.page;
           const page = res.page;
-          top.location.href = `${page.path}?renamed=${pagePath}`;
+          window.location.href = `${page.path}?renamed=${pagePath}`;
         }
         }
       });
       });
 
 
@@ -425,7 +424,7 @@ $(() => {
       }
       }
       else {
       else {
         const page = res.page;
         const page = res.page;
-        top.location.href = `${page.path}?duplicated=${pagePath}`;
+        window.location.href = `${page.path}?duplicated=${pagePath}`;
       }
       }
     });
     });
 
 
@@ -457,7 +456,7 @@ $(() => {
       }
       }
       else {
       else {
         const page = res.page;
         const page = res.page;
-        top.location.href = page.path;
+        window.location.href = page.path;
       }
       }
     });
     });
 
 
@@ -482,7 +481,7 @@ $(() => {
       }
       }
       else {
       else {
         const page = res.page;
         const page = res.page;
-        top.location.href = page.path;
+        window.location.href = page.path;
       }
       }
     });
     });
 
 
@@ -501,7 +500,7 @@ $(() => {
           $('#delete-errors').addClass('alert-danger');
           $('#delete-errors').addClass('alert-danger');
         }
         }
         else {
         else {
-          top.location.href = `${res.path}?unlinked=true`;
+          window.location.href = `${res.path}?unlinked=true`;
         }
         }
       });
       });
 
 
@@ -528,7 +527,7 @@ $(() => {
     $('#edit').removeClass('active');
     $('#edit').removeClass('active');
     $('body').removeClass('on-edit');
     $('body').removeClass('on-edit');
     $('body').removeClass('builtin-editor');
     $('body').removeClass('builtin-editor');
-    location.hash = '#';
+    window.location.hash = '#';
   });
   });
 
 
   /*
   /*
@@ -599,7 +598,7 @@ $(() => {
 
 
       const editorContainer = appContainer.getContainer('EditorContainer');
       const editorContainer = appContainer.getContainer('EditorContainer');
       editorContainer.saveDraft(path, template);
       editorContainer.saveDraft(path, template);
-      top.location.href = `${path}#edit`;
+      window.location.href = `${path}#edit`;
     });
     });
 
 
     if (!isSeen) {
     if (!isSeen) {
@@ -676,7 +675,7 @@ $(() => {
     $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
     $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
       // couln't solve https://github.com/weseek/crowi-plus/issues/119 completely -- 2017.07.03 Yuki Takei
       // couln't solve https://github.com/weseek/crowi-plus/issues/119 completely -- 2017.07.03 Yuki Takei
       window.location.hash = '#';
       window.location.hash = '#';
-      window.history.replaceState('', '', location.href);
+      window.history.replaceState('', '', window.location.href);
     });
     });
   }
   }
   else {
   else {
@@ -690,7 +689,7 @@ $(() => {
       window.history.replaceState('', 'HackMD', '#hackmd');
       window.history.replaceState('', 'HackMD', '#hackmd');
     });
     });
     $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
     $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
-      window.history.replaceState('', '', location.href.replace(location.hash, ''));
+      window.history.replaceState('', '', window.location.href.replace(window.location.hash, ''));
     });
     });
     // replace all href="#edit" link behaviors
     // replace all href="#edit" link behaviors
     $(document).on('click', 'a[href="#edit"]', () => {
     $(document).on('click', 'a[href="#edit"]', () => {
@@ -708,8 +707,8 @@ window.addEventListener('load', (e) => {
   const { appContainer } = window;
   const { appContainer } = window;
 
 
   // hash on page
   // hash on page
-  if (location.hash) {
-    if ((location.hash === '#edit' || location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
+  if (window.location.hash) {
+    if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
       appContainer.setState({ editorMode: 'builtin' });
       appContainer.setState({ editorMode: 'builtin' });
 
 
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
@@ -719,14 +718,14 @@ window.addEventListener('load', (e) => {
       // focus
       // focus
       Crowi.setCaretLineAndFocusToEditor();
       Crowi.setCaretLineAndFocusToEditor();
     }
     }
-    else if (location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
+    else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
       appContainer.setState({ editorMode: 'hackmd' });
       appContainer.setState({ editorMode: 'hackmd' });
 
 
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('body').addClass('on-edit');
       $('body').addClass('on-edit');
       $('body').addClass('hackmd');
       $('body').addClass('hackmd');
     }
     }
-    else if (location.hash === '#revision-history' && $('.tab-pane#revision-history').length > 0) {
+    else if (window.location.hash === '#revision-history' && $('.tab-pane#revision-history').length > 0) {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
     }
   }
   }
@@ -768,7 +767,7 @@ window.addEventListener('load', (e) => {
     });
     });
   }
   }
 
 
-  Crowi.highlightSelectedSection(location.hash);
+  Crowi.highlightSelectedSection(window.location.hash);
   Crowi.modifyScrollTop();
   Crowi.modifyScrollTop();
   Crowi.initSlimScrollForRevisionToc();
   Crowi.initSlimScrollForRevisionToc();
   Crowi.initAffix();
   Crowi.initAffix();
@@ -781,14 +780,14 @@ window.addEventListener('hashchange', (e) => {
   Crowi.modifyScrollTop();
   Crowi.modifyScrollTop();
 
 
   // hash on page
   // hash on page
-  if (location.hash) {
-    if (location.hash === '#edit') {
+  if (window.location.hash) {
+    if (window.location.hash === '#edit') {
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
     }
     }
-    else if (location.hash === '#hackmd') {
+    else if (window.location.hash === '#hackmd') {
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
     }
     }
-    else if (location.hash === '#revision-history') {
+    else if (window.location.hash === '#revision-history') {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
     }
   }
   }

+ 4 - 2
src/client/js/models/MarkdownTable.js

@@ -11,6 +11,8 @@ const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; //
 // set up DOMParser
 // set up DOMParser
 const domParser = new (window.DOMParser)();
 const domParser = new (window.DOMParser)();
 
 
+const defaultOptions = { stringLength: stringWidth };
+
 /**
 /**
  * markdown table class for markdown-table module
  * markdown table class for markdown-table module
  *   ref. https://github.com/wooorm/markdown-table
  *   ref. https://github.com/wooorm/markdown-table
@@ -19,7 +21,7 @@ export default class MarkdownTable {
 
 
   constructor(table, options) {
   constructor(table, options) {
     this.table = table || [];
     this.table = table || [];
-    this.options = options || {};
+    this.options = Object.assign(options || {}, defaultOptions);
 
 
     this.toString = this.toString.bind(this);
     this.toString = this.toString.bind(this);
   }
   }
@@ -139,7 +141,7 @@ export default class MarkdownTable {
         contents.push(row);
         contents.push(row);
       }
       }
     }
     }
-    return (new MarkdownTable(contents, { align: aligns, stringLength: stringWidth }));
+    return (new MarkdownTable(contents, { align: aligns }));
   }
   }
 
 
 }
 }

+ 4 - 0
src/client/styles/scss/_login.scss

@@ -187,6 +187,10 @@
     .btn-label {
     .btn-label {
       background-color: rgba($brand-success, 0.4);
       background-color: rgba($brand-success, 0.4);
     }
     }
+    .btn-label-text {
+      display: inline-block;
+      min-width: 45px;
+    }
     &:after {
     &:after {
       background-color: #3f7263;
       background-color: #3f7263;
     }
     }

+ 8 - 5
src/client/styles/scss/_on-edit.scss

@@ -90,7 +90,7 @@ body.on-edit {
 
 
     position: absolute;
     position: absolute;
     left: $left-margin;
     left: $left-margin;
-    z-index: 1;
+    z-index: 7; // forward than .CodeMirror-vscrollbar
     width: calc(100% - #{$left-margin} - #{$right-margin});
     width: calc(100% - #{$left-margin} - #{$right-margin});
     padding-top: 3px;
     padding-top: 3px;
     pointer-events: none; // disable pointer-events because it becomes an obstacle
     pointer-events: none; // disable pointer-events because it becomes an obstacle
@@ -184,13 +184,16 @@ body.on-edit {
       .autoformat-markdown-table-activated .CodeMirror-cursor {
       .autoformat-markdown-table-activated .CodeMirror-cursor {
         &:after {
         &:after {
           position: relative;
           position: relative;
-          top: -16px;
-          left: 5px;
+          top: -1.1em;
+          left: 0.3em;
           display: block;
           display: block;
-          width: 14px;
-          height: 14px;
+          width: 1em;
+          height: 1em;
           content: ' ';
           content: ' ';
+
           background-image: url(/images/icons/editor/table.svg);
           background-image: url(/images/icons/editor/table.svg);
+          background-repeat: no-repeat;
+          background-size: 1em;
         }
         }
       }
       }
 
 

+ 6 - 4
src/client/styles/scss/_search.scss

@@ -148,7 +148,7 @@
       padding-right: 0;
       padding-right: 0;
 
 
       &.affix {
       &.affix {
-        top: 58px;
+        top: 64px;
         width: 33%;
         width: 33%;
         height: 100%;
         height: 100%;
         padding-right: 5px;
         padding-right: 5px;
@@ -188,9 +188,8 @@
       margin-top: -48px;
       margin-top: -48px;
 
 
       > h2 {
       > h2 {
-        display: inline;
         margin-right: 10px;
         margin-right: 10px;
-        font-size: 20px;
+        font-size: 22px;
         line-height: 1em;
         line-height: 1em;
       }
       }
 
 
@@ -212,7 +211,10 @@
   position: sticky;
   position: sticky;
   top: 0;
   top: 0;
   z-index: 99;
   z-index: 99;
-  padding: 10px 0;
+
+  // for sticky layout
+  padding-top: 15px;
+  margin-bottom: 15px;
 
 
   .input-group-btn .btn {
   .input-group-btn .btn {
     height: 34px;
     height: 34px;

+ 3 - 1
src/client/styles/scss/_vendor.scss

@@ -1,5 +1,7 @@
 // import bootstrap
 // import bootstrap
-$bootstrap-sass-asset-helper: true;
+// see https://github.com/webpack-contrib/css-loader/issues/909#issuecomment-472828905
+$bootstrap-sass-asset-helper: false;
+$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/';
 @import '~bootstrap-sass/assets/stylesheets/bootstrap';
 @import '~bootstrap-sass/assets/stylesheets/bootstrap';
 
 
 // import bootstrap4 utility classes
 // import bootstrap4 utility classes

+ 15 - 12
src/lib/service/cdn-resources-service.js

@@ -3,6 +3,8 @@ const urljoin = require('url-join');
 
 
 const helpers = require('@commons/util/helpers');
 const helpers = require('@commons/util/helpers');
 
 
+const { envUtils } = require('growi-commons');
+
 const cdnLocalScriptRoot = 'public/js/cdn';
 const cdnLocalScriptRoot = 'public/js/cdn';
 const cdnLocalScriptWebRoot = '/js/cdn';
 const cdnLocalScriptWebRoot = '/js/cdn';
 const cdnLocalStyleRoot = 'public/styles/cdn';
 const cdnLocalStyleRoot = 'public/styles/cdn';
@@ -14,7 +16,6 @@ class CdnResourcesService {
   constructor() {
   constructor() {
     this.logger = require('@alias/logger')('growi:service:CdnResourcesService');
     this.logger = require('@alias/logger')('growi:service:CdnResourcesService');
 
 
-    this.noCdn = !!process.env.NO_CDN;
     this.loadManifests();
     this.loadManifests();
   }
   }
 
 
@@ -23,6 +24,10 @@ class CdnResourcesService {
     this.logger.debug('manifest data loaded : ', this.cdnManifests);
     this.logger.debug('manifest data loaded : ', this.cdnManifests);
   }
   }
 
 
+  noCdn() {
+    return envUtils.toBoolean(process.env.NO_CDN);
+  }
+
   getScriptManifestByName(name) {
   getScriptManifestByName(name) {
     const manifests = this.cdnManifests.js
     const manifests = this.cdnManifests.js
       .filter((manifest) => { return manifest.name === name });
       .filter((manifest) => { return manifest.name === name });
@@ -72,9 +77,8 @@ class CdnResourcesService {
    * Generate script tag string
    * Generate script tag string
    *
    *
    * @param {Object} manifest
    * @param {Object} manifest
-   * @param {boolean} noCdn
    */
    */
-  generateScriptTag(manifest, noCdn) {
+  generateScriptTag(manifest) {
     const attrs = [];
     const attrs = [];
     const args = manifest.args || {};
     const args = manifest.args || {};
 
 
@@ -87,7 +91,7 @@ class CdnResourcesService {
 
 
     // TODO process integrity
     // TODO process integrity
 
 
-    const url = noCdn
+    const url = this.noCdn()
       ? `${urljoin(cdnLocalScriptWebRoot, manifest.name)}.js`
       ? `${urljoin(cdnLocalScriptWebRoot, manifest.name)}.js`
       : manifest.url;
       : manifest.url;
     return `<script src="${url}" ${attrs.join(' ')}></script>`;
     return `<script src="${url}" ${attrs.join(' ')}></script>`;
@@ -95,7 +99,7 @@ class CdnResourcesService {
 
 
   getScriptTagByName(name) {
   getScriptTagByName(name) {
     const manifest = this.getScriptManifestByName(name);
     const manifest = this.getScriptManifestByName(name);
-    return this.generateScriptTag(manifest, this.noCdn);
+    return this.generateScriptTag(manifest);
   }
   }
 
 
   getScriptTagsByGroup(group) {
   getScriptTagsByGroup(group) {
@@ -104,7 +108,7 @@ class CdnResourcesService {
         return manifest.groups != null && manifest.groups.includes(group);
         return manifest.groups != null && manifest.groups.includes(group);
       })
       })
       .map((manifest) => {
       .map((manifest) => {
-        return this.generateScriptTag(manifest, this.noCdn);
+        return this.generateScriptTag(manifest);
       });
       });
   }
   }
 
 
@@ -112,9 +116,8 @@ class CdnResourcesService {
    * Generate style tag string
    * Generate style tag string
    *
    *
    * @param {Object} manifest
    * @param {Object} manifest
-   * @param {boolean} noCdn
    */
    */
-  generateStyleTag(manifest, noCdn) {
+  generateStyleTag(manifest) {
     const attrs = [];
     const attrs = [];
     const args = manifest.args || {};
     const args = manifest.args || {};
 
 
@@ -127,7 +130,7 @@ class CdnResourcesService {
 
 
     // TODO process integrity
     // TODO process integrity
 
 
-    const url = noCdn
+    const url = this.noCdn()
       ? `${urljoin(cdnLocalStyleWebRoot, manifest.name)}.css`
       ? `${urljoin(cdnLocalStyleWebRoot, manifest.name)}.css`
       : manifest.url;
       : manifest.url;
 
 
@@ -136,7 +139,7 @@ class CdnResourcesService {
 
 
   getStyleTagByName(name) {
   getStyleTagByName(name) {
     const manifest = this.getStyleManifestByName(name);
     const manifest = this.getStyleManifestByName(name);
-    return this.generateStyleTag(manifest, this.noCdn);
+    return this.generateStyleTag(manifest);
   }
   }
 
 
   getStyleTagsByGroup(group) {
   getStyleTagsByGroup(group) {
@@ -145,7 +148,7 @@ class CdnResourcesService {
         return manifest.groups != null && manifest.groups.includes(group);
         return manifest.groups != null && manifest.groups.includes(group);
       })
       })
       .map((manifest) => {
       .map((manifest) => {
-        return this.generateStyleTag(manifest, this.noCdn);
+        return this.generateStyleTag(manifest);
       });
       });
   }
   }
 
 
@@ -160,7 +163,7 @@ class CdnResourcesService {
       manifest = Object.assign(manifest, { url: url.toString() });
       manifest = Object.assign(manifest, { url: url.toString() });
     }
     }
 
 
-    return this.generateStyleTag(manifest, this.noCdn);
+    return this.generateStyleTag(manifest);
   }
   }
 
 
 }
 }

+ 3 - 1
src/lib/service/logger/stream.prod.js

@@ -1,3 +1,5 @@
+const { envUtils } = require('growi-commons');
+
 const isBrowser = typeof window !== 'undefined';
 const isBrowser = typeof window !== 'undefined';
 
 
 let stream;
 let stream;
@@ -9,7 +11,7 @@ if (isBrowser) {
 }
 }
 // node settings
 // node settings
 else {
 else {
-  const isFormat = !(process.env.FORMAT_NODE_LOG === 'false');
+  const isFormat = (process.env.FORMAT_NODE_LOG == null) || envUtils.toBoolean(process.env.FORMAT_NODE_LOG);
 
 
   if (isFormat) {
   if (isFormat) {
     const bunyanFormat = require('bunyan-format');
     const bunyanFormat = require('bunyan-format');

+ 32 - 0
src/migrations/20190619055421-adjust-page-grant.js

@@ -0,0 +1,32 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:adjust-page-grant');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+module.exports = {
+
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Page = require('@server/models/page')();
+
+    await Page.bulkWrite([
+      {
+        updateMany:
+         {
+           filter: { grant: null },
+           update: { $set: { grant: Page.GRANT_PUBLIC } },
+         },
+      },
+    ]);
+
+    logger.info('Migration has successfully applied');
+
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 1 - 1
src/server/form/admin/securityGeneral.js

@@ -5,7 +5,7 @@ const stringToArray = require('../../util/formUtil').stringToArrayFilter;
 const normalizeCRLF = require('../../util/formUtil').normalizeCRLFFilter;
 const normalizeCRLF = require('../../util/formUtil').normalizeCRLFFilter;
 
 
 module.exports = form(
 module.exports = form(
-  field('settingForm[security:restrictGuestMode]').required(),
+  field('settingForm[security:restrictGuestMode]'),
   field('settingForm[security:registrationMode]').required(),
   field('settingForm[security:registrationMode]').required(),
   field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray),
   field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray),
   field('settingForm[security:list-policy:hideRestrictedByOwner]').trim().toBooleanStrict(),
   field('settingForm[security:list-policy:hideRestrictedByOwner]').trim().toBooleanStrict(),

+ 10 - 2
src/server/models/GlobalNotificationSetting/index.js

@@ -1,5 +1,6 @@
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 const nodePath = require('path');
 const nodePath = require('path');
+const { pathUtils } = require('growi-commons');
 
 
 /**
 /**
  * parent schema for GlobalNotificationSetting model
  * parent schema for GlobalNotificationSetting model
@@ -10,7 +11,9 @@ const globalNotificationSettingSchema = new mongoose.Schema({
   triggerEvents: { type: [String] },
   triggerEvents: { type: [String] },
 });
 });
 
 
-
+/*
+* e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
+*/
 const generatePathsOnTree = (path, pathList) => {
 const generatePathsOnTree = (path, pathList) => {
   pathList.push(path);
   pathList.push(path);
 
 
@@ -23,11 +26,16 @@ const generatePathsOnTree = (path, pathList) => {
   return generatePathsOnTree(newPath, pathList);
   return generatePathsOnTree(newPath, pathList);
 };
 };
 
 
+/*
+* e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
+*/
 const generatePathsToMatch = (originalPath) => {
 const generatePathsToMatch = (originalPath) => {
   const pathList = generatePathsOnTree(originalPath, []);
   const pathList = generatePathsOnTree(originalPath, []);
   return pathList.map((path) => {
   return pathList.map((path) => {
+    // except for the original trigger path ("/a/b/c"), append "*" to find all matches
+    // e.g. ["/a/b/c", "/a/b", "/a", "/"] => ["/a/b/c", "/a/b/*", "/a/*", "/*"]
     if (path !== originalPath) {
     if (path !== originalPath) {
-      return `${path}/*`;
+      return `${pathUtils.addTrailingSlash(path)}*`;
     }
     }
 
 
     return path;
     return path;

+ 1 - 1
src/server/models/config.js

@@ -188,7 +188,7 @@ module.exports = function(crowi) {
         NO_CDN: env.NO_CDN || null,
         NO_CDN: env.NO_CDN || null,
       },
       },
       recentCreatedLimit: crowi.configManager.getConfig('crowi', 'customize:showRecentCreatedNumber'),
       recentCreatedLimit: crowi.configManager.getConfig('crowi', 'customize:showRecentCreatedNumber'),
-      isAclEnabled: !crowi.aclService.getIsPublicWikiOnly(),
+      isAclEnabled: crowi.aclService.isAclEnabled(),
       globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
       globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     };
     };
 
 

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

@@ -38,12 +38,15 @@ class PageTagRelation {
   }
   }
 
 
   static async createTagListWithCount(option) {
   static async createTagListWithCount(option) {
+    const Tag = mongoose.model('Tag');
     const opt = option || {};
     const opt = option || {};
     const sortOpt = opt.sortOpt || {};
     const sortOpt = opt.sortOpt || {};
     const offset = opt.offset || 0;
     const offset = opt.offset || 0;
     const limit = opt.limit || 50;
     const limit = opt.limit || 50;
 
 
+    const existTagIds = await Tag.find().distinct('_id');
     const tags = await this.aggregate()
     const tags = await this.aggregate()
+      .match({ relatedTag: { $in: existTagIds } })
       .group({ _id: '$relatedTag', count: { $sum: 1 } })
       .group({ _id: '$relatedTag', count: { $sum: 1 } })
       .sort(sortOpt);
       .sort(sortOpt);
 
 
@@ -54,7 +57,8 @@ class PageTagRelation {
   }
   }
 
 
   static async listTagsByPage(pageId) {
   static async listTagsByPage(pageId) {
-    return this.find({ relatedPage: pageId }).populate('relatedTag').select('-_id relatedTag');
+    const tags = await this.find({ relatedPage: pageId }).populate('relatedTag').select('-_id relatedTag');
+    return tags.filter((tag) => { return tag.relatedTag !== null });
   }
   }
 
 
   static async listTagNamesByPage(pageId) {
   static async listTagNamesByPage(pageId) {

+ 18 - 14
src/server/models/page.js

@@ -809,7 +809,7 @@ module.exports = function(crowi) {
 
 
     // determine User condition
     // determine User condition
     const hidePagesRestrictedByOwner = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
     const hidePagesRestrictedByOwner = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
-    const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hidePagesRestrictedByGroupInList');
+    const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
 
 
     // determine UserGroup condition
     // determine UserGroup condition
     let userGroups = null;
     let userGroups = null;
@@ -995,9 +995,8 @@ module.exports = function(crowi) {
     let savedPage = await page.save();
     let savedPage = await page.save();
     const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
     const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
     const revision = await pushRevision(savedPage, newRevision, user);
     const revision = await pushRevision(savedPage, newRevision, user);
-    savedPage = await this.findByPath(revision.path)
-      .populate('revision')
-      .populate('creator');
+    savedPage = await this.findByPath(revision.path);
+    await savedPage.populateDataToShowRevision();
 
 
     if (socketClientId != null) {
     if (socketClientId != null) {
       pageEvent.emit('create', savedPage, user, socketClientId);
       pageEvent.emit('create', savedPage, user, socketClientId);
@@ -1021,9 +1020,8 @@ module.exports = function(crowi) {
     let savedPage = await pageData.save();
     let savedPage = await pageData.save();
     const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
     const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
     const revision = await pushRevision(savedPage, newRevision, user);
     const revision = await pushRevision(savedPage, newRevision, user);
-    savedPage = await this.findByPath(revision.path)
-      .populate('revision')
-      .populate('creator');
+    savedPage = await this.findByPath(revision.path);
+    await savedPage.populateDataToShowRevision();
 
 
     if (isSyncRevisionToHackmd) {
     if (isSyncRevisionToHackmd) {
       savedPage = await this.syncRevisionToHackmd(savedPage);
       savedPage = await this.syncRevisionToHackmd(savedPage);
@@ -1063,11 +1061,12 @@ module.exports = function(crowi) {
     const newPath = this.getDeletedPageName(pageData.path);
     const newPath = this.getDeletedPageName(pageData.path);
     const isTrashed = checkIfTrashed(pageData.path);
     const isTrashed = checkIfTrashed(pageData.path);
 
 
+    if (isTrashed) {
+      throw new Error('This method does NOT support deleting trashed pages.');
+    }
+
     const socketClientId = options.socketClientId || null;
     const socketClientId = options.socketClientId || null;
     if (this.isDeletableName(pageData.path)) {
     if (this.isDeletableName(pageData.path)) {
-      if (isTrashed) {
-        return this.completelyDeletePage(pageData, user, options);
-      }
 
 
       pageData.status = STATUS_DELETED;
       pageData.status = STATUS_DELETED;
       const updatedPageData = await this.rename(pageData, newPath, user, { socketClientId, createRedirectPage: true });
       const updatedPageData = await this.rename(pageData, newPath, user, { socketClientId, createRedirectPage: true });
@@ -1086,7 +1085,7 @@ module.exports = function(crowi) {
     const isTrashed = checkIfTrashed(targetPage.path);
     const isTrashed = checkIfTrashed(targetPage.path);
 
 
     if (isTrashed) {
     if (isTrashed) {
-      return this.completelyDeletePageRecursively(targetPage, user, options);
+      throw new Error('This method does NOT supports deleting trashed pages.');
     }
     }
 
 
     const findOpts = { includeRedirect: true };
     const findOpts = { includeRedirect: true };
@@ -1157,6 +1156,7 @@ module.exports = function(crowi) {
     const Bookmark = crowi.model('Bookmark');
     const Bookmark = crowi.model('Bookmark');
     const Attachment = crowi.model('Attachment');
     const Attachment = crowi.model('Attachment');
     const Comment = crowi.model('Comment');
     const Comment = crowi.model('Comment');
+    const PageTagRelation = crowi.model('PageTagRelation');
     const Revision = crowi.model('Revision');
     const Revision = crowi.model('Revision');
     const pageId = pageData._id;
     const pageId = pageData._id;
     const socketClientId = options.socketClientId || null;
     const socketClientId = options.socketClientId || null;
@@ -1166,6 +1166,7 @@ module.exports = function(crowi) {
     await Bookmark.removeBookmarksByPageId(pageId);
     await Bookmark.removeBookmarksByPageId(pageId);
     await Attachment.removeAttachmentsByPageId(pageId);
     await Attachment.removeAttachmentsByPageId(pageId);
     await Comment.removeCommentsByPageId(pageId);
     await Comment.removeCommentsByPageId(pageId);
+    await PageTagRelation.remove({ relatedPage: pageId });
     await Revision.removeRevisionsByPath(pageData.path);
     await Revision.removeRevisionsByPath(pageData.path);
     await this.findByIdAndRemove(pageId);
     await this.findByIdAndRemove(pageId);
     await this.removeRedirectOriginPageByPath(pageData.path);
     await this.removeRedirectOriginPageByPath(pageData.path);
@@ -1228,7 +1229,8 @@ module.exports = function(crowi) {
     const Page = this;
     const Page = this;
     const Revision = crowi.model('Revision');
     const Revision = crowi.model('Revision');
     const path = pageData.path;
     const path = pageData.path;
-    const createRedirectPage = options.createRedirectPage || 0;
+    const createRedirectPage = options.createRedirectPage || false;
+    const updateMetadata = options.updateMetadata || false;
     const socketClientId = options.socketClientId || null;
     const socketClientId = options.socketClientId || null;
 
 
     // sanitize path
     // sanitize path
@@ -1236,8 +1238,10 @@ module.exports = function(crowi) {
 
 
     // update Page
     // update Page
     pageData.path = newPagePath;
     pageData.path = newPagePath;
-    pageData.lastUpdateUser = user;
-    pageData.updatedAt = Date.now();
+    if (updateMetadata) {
+      pageData.lastUpdateUser = user;
+      pageData.updatedAt = Date.now();
+    }
     const updatedPageData = await pageData.save();
     const updatedPageData = await pageData.save();
 
 
     // update Rivisions
     // update Rivisions

+ 28 - 47
src/server/models/user.js

@@ -65,6 +65,18 @@ module.exports = function(crowi) {
     createdAt: { type: Date, default: Date.now },
     createdAt: { type: Date, default: Date.now },
     lastLoginAt: { type: Date },
     lastLoginAt: { type: Date },
     admin: { type: Boolean, default: 0, index: true },
     admin: { type: Boolean, default: 0, index: true },
+  }, {
+    toObject: {
+      transform: (doc, ret, opt) => {
+        // omit password
+        delete ret.password;
+        // omit email
+        if (!doc.isEmailPublished) {
+          delete ret.email;
+        }
+        return ret;
+      },
+    },
   });
   });
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(uniqueValidator);
   userSchema.plugin(uniqueValidator);
@@ -374,24 +386,6 @@ module.exports = function(crowi) {
     return true;
     return true;
   };
   };
 
 
-  userSchema.statics.filterToPublicFields = function(user) {
-    debug('User is', typeof user, user);
-    if (typeof user !== 'object' || !user._id) {
-      return user;
-    }
-
-    const filteredUser = {};
-    const fields = USER_PUBLIC_FIELDS.split(' ');
-    for (let i = 0; i < fields.length; i++) {
-      const key = fields[i];
-      if (user[key]) {
-        filteredUser[key] = user[key];
-      }
-    }
-
-    return filteredUser;
-  };
-
   userSchema.statics.findUsers = function(options, callback) {
   userSchema.statics.findUsers = function(options, callback) {
     const sort = options.sort || { status: 1, createdAt: 1 };
     const sort = options.sort || { status: 1, createdAt: 1 };
 
 
@@ -444,17 +438,14 @@ module.exports = function(crowi) {
   };
   };
 
 
   userSchema.statics.findUsersWithPagination = async function(options) {
   userSchema.statics.findUsersWithPagination = async function(options) {
-    const sort = options.sort || { status: 1, username: 1, createdAt: 1 };
+    const defaultOptions = {
+      sort: { status: 1, username: 1, createdAt: 1 },
+      page: 1,
+      limit: PAGE_ITEMS,
+    };
+    const mergedOptions = Object.assign(defaultOptions, options);
 
 
-    // eslint-disable-next-line no-return-await
-    return await this.paginate({ status: { $ne: STATUS_DELETED } }, { page: options.page || 1, limit: options.limit || PAGE_ITEMS }, (err, result) => {
-      if (err) {
-        debug('Error on pagination:', err);
-        throw new Error(err);
-      }
-
-      return result;
-    }, { sortBy: sort });
+    return this.paginate({ status: { $ne: STATUS_DELETED } }, mergedOptions);
   };
   };
 
 
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
@@ -607,28 +598,18 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  userSchema.statics.resetPasswordByRandomString = function(id) {
-    const User = this;
+  userSchema.statics.resetPasswordByRandomString = async function(id) {
+    const user = await this.findById(id);
 
 
-    return new Promise(((resolve, reject) => {
-      User.findById(id, (err, userData) => {
-        if (!userData) {
-          return reject(new Error('User not found'));
-        }
+    if (!user) {
+      throw new Error('User not found');
+    }
 
 
-        // is updatable check
-        // if (userData.isUp
-        const newPassword = generateRandomTempPassword();
-        userData.setPassword(newPassword);
-        userData.save((err, userData) => {
-          if (err) {
-            return reject(err);
-          }
+    const newPassword = generateRandomTempPassword();
+    user.setPassword(newPassword);
+    await user.save();
 
 
-          resolve({ user: userData, newPassword });
-        });
-      });
-    }));
+    return newPassword;
   };
   };
 
 
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {

+ 30 - 19
src/server/routes/admin.js

@@ -105,7 +105,13 @@ module.exports = function(crowi, app) {
   // app.get('/admin/security'                  , admin.security.index);
   // app.get('/admin/security'                  , admin.security.index);
   actions.security = {};
   actions.security = {};
   actions.security.index = function(req, res) {
   actions.security.index = function(req, res) {
-    return res.render('admin/security');
+    const isWikiModeForced = aclService.isWikiModeForced();
+    const guestModeValue = aclService.getGuestModeValue();
+
+    return res.render('admin/security', {
+      isWikiModeForced,
+      guestModeValue,
+    });
   };
   };
 
 
   // app.get('/admin/markdown'                  , admin.markdown.index);
   // app.get('/admin/markdown'                  , admin.markdown.index);
@@ -412,7 +418,12 @@ module.exports = function(crowi, app) {
 
 
     const page = parseInt(req.query.page) || 1;
     const page = parseInt(req.query.page) || 1;
 
 
-    const result = await User.findUsersWithPagination({ page });
+    const result = await User.findUsersWithPagination({
+      page,
+      select: User.USER_PUBLIC_FIELDS,
+      populate: User.IMAGE_POPULATION,
+    });
+
     const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
     const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
 
 
     return res.render('admin/users', {
     return res.render('admin/users', {
@@ -571,19 +582,22 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   // app.post('/_api/admin/users.resetPassword' , admin.api.usersResetPassword);
   // app.post('/_api/admin/users.resetPassword' , admin.api.usersResetPassword);
-  actions.user.resetPassword = function(req, res) {
+  actions.user.resetPassword = async function(req, res) {
     const id = req.body.user_id;
     const id = req.body.user_id;
     const User = crowi.model('User');
     const User = crowi.model('User');
 
 
-    User.resetPasswordByRandomString(id)
-      .then((data) => {
-        data.user = User.filterToPublicFields(data.user);
-        return res.json(ApiResponse.success(data));
-      })
-      .catch((err) => {
-        debug('Error on reseting password', err);
-        return res.json(ApiResponse.error('Error'));
-      });
+    try {
+      const newPassword = await User.resetPasswordByRandomString(id);
+
+      const user = await User.findById(id);
+
+      const result = { user: user.toObject(), newPassword };
+      return res.json(ApiResponse.success(result));
+    }
+    catch (err) {
+      debug('Error on reseting password', err);
+      return res.json(ApiResponse.error(err));
+    }
   };
   };
 
 
   actions.externalAccount = {};
   actions.externalAccount = {};
@@ -624,7 +638,7 @@ module.exports = function(crowi, app) {
   actions.userGroup = {};
   actions.userGroup = {};
   actions.userGroup.index = function(req, res) {
   actions.userGroup.index = function(req, res) {
     const page = parseInt(req.query.page) || 1;
     const page = parseInt(req.query.page) || 1;
-    const isAclEnabled = aclService.getIsPublicWikiOnly();
+    const isAclEnabled = aclService.isAclEnabled();
     const renderVar = {
     const renderVar = {
       userGroups: [],
       userGroups: [],
       userGroupRelations: new Map(),
       userGroupRelations: new Map(),
@@ -856,12 +870,9 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     const form = req.form.settingForm;
     const form = req.form.settingForm;
-    if (aclService.getIsPublicWikiOnly()) {
-      const guestMode = form['security:restrictGuestMode'];
-      if (guestMode === 'Deny') {
-        req.form.errors.push('Private Wikiへの設定変更はできません。');
-        return res.json({ status: false, message: req.form.errors.join('\n') });
-      }
+    if (aclService.isWikiModeForced()) {
+      logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
+      delete form['security:restrictGuestMode'];
     }
     }
 
 
     try {
     try {

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

@@ -47,6 +47,14 @@ module.exports = function(crowi, app) {
     if (page.revisionHackmdSynced != null && page.revisionHackmdSynced._id != null) {
     if (page.revisionHackmdSynced != null && page.revisionHackmdSynced._id != null) {
       returnObj.revisionHackmdSynced = page.revisionHackmdSynced._id;
       returnObj.revisionHackmdSynced = page.revisionHackmdSynced._id;
     }
     }
+
+    if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+      returnObj.lastUpdateUser = page.lastUpdateUser.toObject();
+    }
+    if (page.creator != null && page.creator instanceof User) {
+      returnObj.creator = page.creator.toObject();
+    }
+
     return returnObj;
     return returnObj;
   }
   }
 
 
@@ -585,8 +593,6 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     const result = { page: serializeToObj(createdPage), tags: savedTags };
     const result = { page: serializeToObj(createdPage), tags: savedTags };
-    result.page.lastUpdateUser = User.filterToPublicFields(createdPage.lastUpdateUser);
-    result.page.creator = User.filterToPublicFields(createdPage.creator);
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // update scopes for descendants
     // update scopes for descendants
@@ -674,7 +680,6 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     const result = { page: serializeToObj(page), tags: savedTags };
     const result = { page: serializeToObj(page), tags: savedTags };
-    result.page.lastUpdateUser = User.filterToPublicFields(page.lastUpdateUser);
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // update scopes for descendants
     // update scopes for descendants
@@ -1047,11 +1052,11 @@ module.exports = function(crowi, app) {
     const previousRevision = req.body.revision_id || null;
     const previousRevision = req.body.revision_id || null;
     const newPagePath = pathUtils.normalizePath(req.body.new_path);
     const newPagePath = pathUtils.normalizePath(req.body.new_path);
     const options = {
     const options = {
-      createRedirectPage: req.body.create_redirect || 0,
-      moveUnderTrees: req.body.move_trees || 0,
+      createRedirectPage: (req.body.create_redirect != null),
+      updateMetadata: (req.body.remain_metadata == null),
       socketClientId: +req.body.socketClientId || undefined,
       socketClientId: +req.body.socketClientId || undefined,
     };
     };
-    const isRecursively = req.body.recursively || 0;
+    const isRecursively = (req.body.recursively != null);
 
 
     if (!Page.isCreatableName(newPagePath)) {
     if (!Page.isCreatableName(newPagePath)) {
       return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
       return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
@@ -1124,7 +1129,7 @@ module.exports = function(crowi, app) {
     req.body.body = page.revision.body;
     req.body.body = page.revision.body;
     req.body.grant = page.grant;
     req.body.grant = page.grant;
     req.body.grantedUsers = page.grantedUsers;
     req.body.grantedUsers = page.grantedUsers;
-    req.body.grantedGroup = page.grantedGroup;
+    req.body.grantUserGroupId = page.grantedGroup;
     req.body.pageTags = originTags;
     req.body.pageTags = originTags;
 
 
     return api.create(req, res);
     return api.create(req, res);

+ 38 - 12
src/server/service/acl.js

@@ -16,24 +16,50 @@ class AclService {
     };
     };
   }
   }
 
 
-  getIsPublicWikiOnly() {
-    const publicWikiOnly = process.env.PUBLIC_WIKI_ONLY;
-    return !!publicWikiOnly;
+  /**
+   * @returns Whether Access Control is enabled or not
+   */
+  isAclEnabled() {
+    const wikiMode = this.configManager.getConfig('crowi', 'security:wikiMode');
+    return wikiMode !== 'public';
   }
   }
 
 
-  getIsGuestAllowedToRead() {
-    // return true if puclic wiki mode
-    if (this.getIsPublicWikiOnly()) {
-      return true;
-    }
+  /**
+   * @returns Whether wiki mode is set
+   */
+  isWikiModeForced() {
+    const wikiMode = this.configManager.getConfig('crowi', 'security:wikiMode');
+    const isPrivateOrPublic = wikiMode === 'private' || wikiMode === 'public';
+
+    return isPrivateOrPublic;
+  }
+
+  /**
+   * @returns Whether guest users are allowed to read public pages
+   */
+  isGuestAllowedToRead() {
+    const wikiMode = this.configManager.getConfig('crowi', 'security:wikiMode');
 
 
-    // return false if undefined
-    const isRestrictGuestMode = this.configManager.getConfig('crowi', 'security:restrictGuestMode');
-    if (isRestrictGuestMode) {
+    // return false if private wiki mode
+    if (wikiMode === 'private') {
       return false;
       return false;
     }
     }
+    // return true if public wiki mode
+    if (wikiMode === 'public') {
+      return true;
+    }
+
+    const guestMode = this.configManager.getConfig('crowi', 'security:restrictGuestMode');
+
+    // 'Readonly' => returns true (allow access to guests)
+    // 'Deny', null, undefined, '', ... everything else => returns false (requires login)
+    return guestMode === this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY;
+  }
 
 
-    return this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY === isRestrictGuestMode;
+  getGuestModeValue() {
+    return this.isGuestAllowedToRead()
+      ? this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY
+      : this.labels.SECURITY_RESTRICT_GUEST_MODE_DENY;
   }
   }
 
 
   getRestrictGuestModeLabels() {
   getRestrictGuestModeLabels() {

+ 9 - 1
src/server/service/config-loader.js

@@ -1,9 +1,11 @@
 const debug = require('debug')('growi:service:ConfigLoader');
 const debug = require('debug')('growi:service:ConfigLoader');
 
 
+const { envUtils } = require('growi-commons');
+
 const TYPES = {
 const TYPES = {
   NUMBER:  { parse: (v) => { return parseInt(v, 10) } },
   NUMBER:  { parse: (v) => { return parseInt(v, 10) } },
   STRING:  { parse: (v) => { return v } },
   STRING:  { parse: (v) => { return v } },
-  BOOLEAN: { parse: (v) => { return /^(true|1)$/i.test(v) } },
+  BOOLEAN: { parse: (v) => { return envUtils.toBoolean(v) } },
 };
 };
 
 
 /**
 /**
@@ -134,6 +136,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.NUMBER,
     type:    TYPES.NUMBER,
     default: Infinity,
     default: Infinity,
   },
   },
+  FORCE_WIKI_MODE: {
+    ns:      'crowi',
+    key:     'security:wikiMode',
+    type:    TYPES.STRING,
+    default: undefined,
+  },
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',

+ 4 - 2
src/server/util/middlewares.js

@@ -192,16 +192,18 @@ module.exports = (crowi) => {
    */
    */
   middlewares.loginRequired = function(isStrictly = true) {
   middlewares.loginRequired = function(isStrictly = true) {
     return function(req, res, next) {
     return function(req, res, next) {
-      const User = crowi.model('User');
 
 
       // when the route is not strictly restricted
       // when the route is not strictly restricted
       if (!isStrictly) {
       if (!isStrictly) {
         // when allowed to read
         // when allowed to read
-        if (crowi.aclService.getIsGuestAllowedToRead()) {
+        if (crowi.aclService.isGuestAllowedToRead()) {
+          logger.debug('Allowed to read: ', req.path);
           return next();
           return next();
         }
         }
       }
       }
 
 
+      const User = crowi.model('User');
+
       // check the user logged in
       // check the user logged in
       //  make sure that req.user isn't username/email string to login which is set by basic-auth-connect
       //  make sure that req.user isn't username/email string to login which is set by basic-auth-connect
       if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {

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

@@ -555,7 +555,7 @@ SearchClient.prototype.appendCriteriaForQueryString = function(query, queryStrin
 
 
 SearchClient.prototype.filterPagesByViewer = async function(query, user, userGroups) {
 SearchClient.prototype.filterPagesByViewer = async function(query, user, userGroups) {
   const showPagesRestrictedByOwner = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
   const showPagesRestrictedByOwner = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
-  const showPagesRestrictedByGroup = !this.configManager.getConfig('crowi', 'security:list-policy:hidePagesRestrictedByGroupInList');
+  const showPagesRestrictedByGroup = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
 
 
   query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
   query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
 

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

@@ -70,7 +70,7 @@ module.exports = function(crowi, req, locals) {
   locals.customizeService = customizeService;
   locals.customizeService = customizeService;
 
 
   locals.noCdn = function() {
   locals.noCdn = function() {
-    return !!process.env.NO_CDN;
+    return cdnResourcesService.noCdn();
   };
   };
 
 
   locals.cdnScriptTag = function(name) {
   locals.cdnScriptTag = function(name) {

+ 3 - 3
src/server/views/admin/external-accounts.html

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('External Account management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('external_account_management')) }}{% endblock %}
 
 
 {% block content_header %}
 {% block content_header %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('User_management') }}/{{ t('External Account management') }}</h1>
+    <h1 id="admin-title" class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
   </header>
   </header>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
@@ -92,7 +92,7 @@
               </span>
               </span>
               {% endif %}
               {% endif %}
             </td>
             </td>
-            <td>{{ account.createdAt|date('Y-m-d', account.createdAt.getTimezoneOffset()) }}</td>
+            <td>{{ account.createdAt|datetz('Y-m-d') }}</td>
             <td>
             <td>
               <div class="btn-group admin-user-menu">
               <div class="btn-group admin-user-menu">
 
 

+ 12 - 4
src/server/views/admin/security.html

@@ -41,13 +41,21 @@
         <legend class="alert-anchor">{{ t('security_settings') }}</legend>
         <legend class="alert-anchor">{{ t('security_settings') }}</legend>
 
 
           <div class="form-group">
           <div class="form-group">
-            <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('Guest users access') }}</label>
+            <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('security_setting.Guest Users Access') }}</label>
             <div class="col-xs-6">
             <div class="col-xs-6">
-              <select class="form-control selectpicker" name="settingForm[security:restrictGuestMode]" value="{{ getConfig('crowi', 'security:restrictGuestMode') }}">
+              {% set selectedValue = guestModeValue %}
+              <select class="form-control selectpicker" {% if isWikiModeForced %}disabled{% endif %}
+                  name="settingForm[security:restrictGuestMode]" value="{{ getConfig('crowi', 'security:restrictGuestMode') }}">
                 {% for modeValue, modeLabel in consts.restrictGuestMode %}
                 {% for modeValue, modeLabel in consts.restrictGuestMode %}
-                <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:restrictGuestMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
+                  <option value="{{ t(modeValue) }}" {% if modeValue == selectedValue %}selected{% endif %}>{{ t(modeLabel) }}</option>
                 {% endfor %}
                 {% endfor %}
               </select>
               </select>
+              {% if isWikiModeForced %}
+              <p class="alert alert-warning mt-2">
+                <i class="icon-exclamation icon-fw"></i><b>FIXED</b><br>
+                {{ t('security_setting.Fixed by env var', 'FORCE_WIKI_MODE', getConfig('crowi', 'security:wikiMode')) }}
+              </p>
+              {% endif %}
             </div>
             </div>
           </div>
           </div>
 
 
@@ -59,7 +67,7 @@
                 <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:registrationMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
                 <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:registrationMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
                 {% endfor %}
                 {% endfor %}
               </select>
               </select>
-              <p class="help-block small">{{ t('The contents entered here will be shown in the header etc') }}</p>
+              <p class="help-block small">{{ t('security_setting.Register limitation desc') }}</p>
             </div>
             </div>
           </div>
           </div>
 
 

+ 4 - 4
src/server/views/admin/user-group-detail.html

@@ -101,7 +101,7 @@
             <div class="form-group">
             <div class="form-group">
               <label class="col-sm-2 control-label">{{ t('Created') }}</label>
               <label class="col-sm-2 control-label">{{ t('Created') }}</label>
               <div class="col-sm-4">
               <div class="col-sm-4">
-                <input class="form-control" type="text" disabled value="{{userGroup.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}">
+                <input class="form-control" type="text" disabled value="{{userGroup.createdAt|datetz('Y-m-d') }}">
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
@@ -125,7 +125,7 @@
             </th>
             </th>
             <th>{{ t('Name') }}</th>
             <th>{{ t('Name') }}</th>
             <th width="100px">{{ t('Created') }}</th>
             <th width="100px">{{ t('Created') }}</th>
-            <th width="150px">{{ t('Last Login')}}</th>
+            <th width="160px">{{ t('Last Login')}}</th>
             <th width="70px"></th>
             <th width="70px"></th>
           </tr>
           </tr>
         </thead>
         </thead>
@@ -140,9 +140,9 @@
               <strong>{{ sRelation.relatedUser.username }}</strong>
               <strong>{{ sRelation.relatedUser.username }}</strong>
             </td>
             </td>
             <td>{{ sRelation.relatedUser.name }}</td>
             <td>{{ sRelation.relatedUser.name }}</td>
-            <td>{{ sRelation.relatedUser.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}</td>
+            <td>{{ sRelation.relatedUser.createdAt|datetz('Y-m-d') }}</td>
             <td>
             <td>
-              {% if sRelation.relatedUser.lastLoginAt %} {{ sRelation.relatedUser.lastLoginAt|date('Y-m-d H:i', sRelation.relatedUser.createdAt.getTimezoneOffset()) }} {% endif %}
+              {% if sRelation.relatedUser.lastLoginAt %} {{ sRelation.relatedUser.lastLoginAt|datetz('Y-m-d H:i:s') }} {% endif %}
             </td>
             </td>
             <td>
             <td>
               <div class="btn-group admin-user-menu">
               <div class="btn-group admin-user-menu">

+ 1 - 1
src/server/views/installer.html

@@ -25,7 +25,7 @@
   <!-- styles -->
   <!-- styles -->
   {% include './widget/headers/styles-for-app.html' %}
   {% include './widget/headers/styles-for-app.html' %}
   {% block theme_css_block %}
   {% block theme_css_block %}
-    {% include './widget/headers/styles-theme.html' with {theme: 'default'} %}
+    {% include './widget/headers/styles-theme.html' with {themeName: 'default'} %}
   {% endblock %}
   {% endblock %}
 
 
   {{ cdnStyleTagsByGroup('basis') }}
   {{ cdnStyleTagsByGroup('basis') }}

+ 1 - 1
src/server/views/layout/layout.html

@@ -145,7 +145,7 @@
             <li><a href="/me"><i class="icon-fw icon-wrench"></i>{{ t('User Settings') }}</a></li>
             <li><a href="/me"><i class="icon-fw icon-wrench"></i>{{ t('User Settings') }}</a></li>
             <li role="separator" class="divider"></li>
             <li role="separator" class="divider"></li>
             <li><a href="/user/{{ user.username }}#user-draft-list"><i class="icon-fw icon-docs"></i>{{ t('List Drafts') }}</a></li>
             <li><a href="/user/{{ user.username }}#user-draft-list"><i class="icon-fw icon-docs"></i>{{ t('List Drafts') }}</a></li>
-            <li><a href="/user"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
+            <li><a href="/trash"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
             <li role="separator" class="divider"></li>
             <li role="separator" class="divider"></li>
             <li><a href="/logout"><i class="icon-fw icon-power"></i>{{ t('Sign out') }}</a></li>
             <li><a href="/logout"><i class="icon-fw icon-power"></i>{{ t('Sign out') }}</a></li>
           </ul>
           </ul>

+ 1 - 1
src/server/views/login.html

@@ -299,7 +299,7 @@
           <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
           <div class="input-group m-t-30 m-b-20 d-flex justify-content-center">
             <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
             <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
               <span class="btn-label"><i class="icon-user-follow"></i></span>
               <span class="btn-label"><i class="icon-user-follow"></i></span>
-              {{ t('Sign up') }}
+              <span class="btn-label-text">{{ t('Sign up') }}</span>
             </button>
             </button>
           </div>
           </div>
         </form>
         </form>

+ 1 - 1
src/server/views/me/external-accounts.html

@@ -86,7 +86,7 @@
             <td>
             <td>
               <strong>{{ account.accountId }}</strong>
               <strong>{{ account.accountId }}</strong>
             </td>
             </td>
-            <td>{{ account.createdAt|date('Y-m-d', account.createdAt.getTimezoneOffset()) }}</td>
+            <td>{{ account.createdAt|datetz('Y-m-d') }}</td>
             <td class="text-center">
             <td class="text-center">
               <button class="btn btn-default btn-sm btn-danger"
               <button class="btn btn-default btn-sm btn-danger"
                   data-toggle="modal" data-target="#diassociate-external-account" data-provider-type="{{ account.providerType }}" data-account-id="{{ account.accountId }}">
                   data-toggle="modal" data-target="#diassociate-external-account" data-provider-type="{{ account.providerType }}" data-account-id="{{ account.accountId }}">

+ 13 - 11
src/server/views/modal/delete.html

@@ -4,7 +4,7 @@
 
 
       <form role="form" id="delete-page-form" onsubmit="return false;">
       <form role="form" id="delete-page-form" onsubmit="return false;">
 
 
-        <div class="modal-header {% if page.isDeleted() %}bg-danger{% endif %}">
+        <div class="modal-header {% if page.isDeleted() %}bg-danger{% else %}bg-primary{% endif %}">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
           <div class="modal-title">
           <div class="modal-title">
             {% if page.isDeleted() %}
             {% if page.isDeleted() %}
@@ -25,19 +25,21 @@
           {% if page.grant != 2 %}
           {% if page.grant != 2 %}
           <div class="checkbox checkbox-warning">
           <div class="checkbox checkbox-warning">
             <input name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
             <input name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
-            <label for="cbDeleteRecursively">{{ t('modal_delete.delete_recursively') }}</label>
-            <p class="help-block"> {{ t('modal_delete.recursively', page.path) }}
-            </p>
+            <label for="cbDeleteRecursively">
+              {{ t('modal_delete.delete_recursively') }}
+              <p class="help-block mt-0"> {{ t('modal_delete.recursively', page.path) }}</p>
+            </label>
           </div>
           </div>
           {% endif %}
           {% endif %}
           {% if not page.isDeleted() %}
           {% if not page.isDeleted() %}
           <div class="checkbox checkbox-danger">
           <div class="checkbox checkbox-danger">
-          <input name="completely" id="cbDeleteCompletely" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} value="1"  type="checkbox">
-            <label for="cbDeleteCompletely" class="text-danger">{{ t('modal_delete.delete_completely') }}</label>
+            <input name="completely" id="cbDeleteCompletely" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} value="1"  type="checkbox">
+            <label for="cbDeleteCompletely" class="text-danger">
+              {{ t('modal_delete.delete_completely') }}
+              <p class="help-block mt-0"> {{ t('modal_delete.completely') }}</p>
+            </label>
             {% if !user.canDeleteCompletely(page.creator._id) %}
             {% if !user.canDeleteCompletely(page.creator._id) %}
-              <p class="bg-danger text-white p-2 mt-2"> <i class="icon-ban" ></i>{{ t('modal_delete.delete_completely_restriction') }}</p>
-            {% else %}
-            <p class="help-block"> {{ t('modal_delete.completely') }}</p>
+              <p class="alert alert-warning p-2 my-0"><i class="icon-ban icon-fw" ></i>{{ t('modal_delete.delete_completely_restriction') }}</p>
             {% endif %}
             {% endif %}
           </div>
           </div>
           {% endif %}
           {% endif %}
@@ -52,12 +54,12 @@
               <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
               <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
               {% if page.isDeleted() %}
               {% if page.isDeleted() %}
                 <input type="hidden" name="completely" value="true">
                 <input type="hidden" name="completely" value="true">
-                <button type="submit" class="m-l-10 btn btn-sm btn-danger delete-button">
+                <button type="submit" class="m-l-10 btn btn-danger delete-button">
                   <i class="icon-fire" aria-hidden="true"></i>
                   <i class="icon-fire" aria-hidden="true"></i>
                   {{ t('delete_completely') }}
                   {{ t('delete_completely') }}
                 </button>
                 </button>
               {% else %}
               {% else %}
-                <button type="submit" class="m-l-10 btn btn-sm btn-default delete-button">
+                <button type="submit" class="m-l-10 btn btn-primary delete-button">
                   <i class="icon-trash" aria-hidden="true"></i>
                   <i class="icon-trash" aria-hidden="true"></i>
                   {{ t('Delete') }}
                   {{ t('Delete') }}
                 </button>
                 </button>

+ 21 - 9
src/server/views/modal/rename.html

@@ -6,7 +6,7 @@
 
 
         <div class="modal-header bg-primary">
         <div class="modal-header bg-primary">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">{{ t('modal_rename.label.Rename page') }}</div>
+          <div class="modal-title">{{ t('modal_rename.label.Move/Rename page') }}</div>
         </div>
         </div>
         <div class="modal-body">
         <div class="modal-body">
           <div class="form-group">
           <div class="form-group">
@@ -29,16 +29,28 @@
 
 
           <div class="checkbox checkbox-warning">
           <div class="checkbox checkbox-warning">
             <input name="recursively" id="cbRenameRecursively" value="1" type="checkbox" checked>
             <input name="recursively" id="cbRenameRecursively" value="1" type="checkbox" checked>
-            <label for="cbRenameRecursively">{{ t('modal_rename.label.Move recursively') }}</label>
-            <p class="help-block"> {{ t('modal_rename.help.recursive', page.path) }}
-            </p>
+            <label for="cbRenameRecursively">
+              {{ t('modal_rename.label.Recursively') }}
+              <p class="help-block mt-0">{{ t('modal_rename.help.recursive', page.path) }}</p>
+            </label>
           </div>
           </div>
-          <div class="checkbox checkbox-info">
-            <input name="create_redirect" id="cbRenameRedirect" value="1"  type="checkbox">
-              <label for="cbRenameRedirect">{{ t('modal_rename.label.Redirect') }}</label>
-              <p class="help-block"> {{ t('modal_rename.help.redirect', page.path) }}
-              </p>
+
+          <div class="checkbox checkbox-success">
+            <input name="create_redirect" id="cbRenameRedirect" value="1" type="checkbox">
+            <label for="cbRenameRedirect">
+              {{ t('modal_rename.label.Redirect') }}
+              <p class="help-block mt-0">{{ t('modal_rename.help.redirect', page.path) }}</p>
+            </label>
+          </div>
+
+          <div class="checkbox checkbox-inverse">
+            <input name="remain_metadata" id="cbRenameMetadata" value="1" type="checkbox">
+            <label for="cbRenameMetadata">
+              {{ t('modal_rename.label.Do not update metadata') }}
+              <p class="help-block mt-0">{{ t('modal_rename.help.metadata') }}</p>
+            </label>
           </div>
           </div>
+
         </div>
         </div>
         <div class="modal-footer">
         <div class="modal-footer">
           <div class="d-flex justify-content-between">
           <div class="d-flex justify-content-between">

+ 1 - 1
src/server/views/search.html

@@ -14,7 +14,7 @@
 <div class="container-fluid">
 <div class="container-fluid">
 
 
   <div class="row">
   <div class="row">
-    <div id="main" class="main m-t-15 col-md-12 search-page">
+    <div id="main" class="main col-md-12 search-page">
       <div class="" id="search-page"></div>
       <div class="" id="search-page"></div>
     </div>
     </div>
   </div>
   </div>

+ 1 - 1
src/server/views/widget/page_tabs.html

@@ -51,7 +51,7 @@
         <i class="icon-options-vertical"></i>
         <i class="icon-options-vertical"></i>
       </a>
       </a>
       <ul class="dropdown-menu">
       <ul class="dropdown-menu">
-        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move') }}</a></li>
+        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a></li>
         <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
         <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
         <li class="divider"></li>
         <li class="divider"></li>
         <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
         <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>

+ 1 - 1
src/server/views/widget/page_tabs_kibela.html

@@ -48,7 +48,7 @@
         <i class="icon-options-vertical"></i>
         <i class="icon-options-vertical"></i>
       </a>
       </a>
       <ul class="dropdown-menu">
       <ul class="dropdown-menu">
-        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move') }}</a></li>
+        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a></li>
         <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
         <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
         <li class="divider"></li>
         <li class="divider"></li>
         <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
         <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>

+ 0 - 8
src/test/models/page.test.js

@@ -23,14 +23,6 @@ describe('Page', () => {
     UserGroupRelation = mongoose.model('UserGroupRelation');
     UserGroupRelation = mongoose.model('UserGroupRelation');
     Page = mongoose.model('Page');
     Page = mongoose.model('Page');
 
 
-    // remove all
-    await Promise.all([
-      Page.remove({}),
-      User.remove({}),
-      UserGroup.remove({}),
-      UserGroupRelation.remove({}),
-    ]);
-
     await User.insertMany([
     await User.insertMany([
       { name: 'Anon 0', username: 'anonymous0', email: 'anonymous0@example.com' },
       { name: 'Anon 0', username: 'anonymous0', email: 'anonymous0@example.com' },
       { name: 'Anon 1', username: 'anonymous1', email: 'anonymous1@example.com' },
       { name: 'Anon 1', username: 'anonymous1', email: 'anonymous1@example.com' },

+ 22 - 20
src/test/models/user.test.js

@@ -10,40 +10,42 @@ describe('User', () => {
 
 
   beforeAll(async(done) => {
   beforeAll(async(done) => {
     crowi = await getInstance();
     crowi = await getInstance();
-    done();
-  });
-
-  beforeEach(async(done) => {
     User = mongoose.model('User');
     User = mongoose.model('User');
+
+    await User.create({
+      name: 'Example for User Test',
+      username: 'usertest',
+      email: 'usertest@example.com',
+      password: 'usertestpass',
+      lang: 'en',
+    });
+
     done();
     done();
   });
   });
 
 
   describe('Create and Find.', () => {
   describe('Create and Find.', () => {
     describe('The user', () => {
     describe('The user', () => {
-      test('should created', (done) => {
-        User.createUserByEmailAndPassword('Aoi Miyazaki', 'aoi', 'aoi@example.com', 'hogefuga11', 'en', (err, userData) => {
+      test('should created with createUserByEmailAndPassword', (done) => {
+        User.createUserByEmailAndPassword('Example2 for User Test', 'usertest2', 'usertest2@example.com', 'usertest2pass', 'en', (err, userData) => {
           expect(err).toBeNull();
           expect(err).toBeNull();
           expect(userData).toBeInstanceOf(User);
           expect(userData).toBeInstanceOf(User);
+          expect(userData.name).toBe('Example2 for User Test');
           done();
           done();
         });
         });
       });
       });
 
 
-      test('should be found by findUserByUsername', (done) => {
-        User.findUserByUsername('aoi')
-          .then((userData) => {
-            expect(userData).toBeInstanceOf(User);
-            done();
-          });
+      test('should be found by findUserByUsername', async() => {
+        const user = await User.findUserByUsername('usertest');
+        expect(user).toBeInstanceOf(User);
+        expect(user.name).toBe('Example for User Test');
       });
       });
 
 
-      test('should be found by findUsersByPartOfEmail', (done) => {
-        User.findUsersByPartOfEmail('ao', {})
-          .then((userData) => {
-            expect(userData).toBeInstanceOf(Array);
-            expect(userData[0]).toBeInstanceOf(User);
-            expect(userData[0].email).toEqual('aoi@example.com');
-            done();
-          });
+      test('should be found by findUsersByPartOfEmail', async() => {
+        const users = await User.findUsersByPartOfEmail('usert', {});
+        expect(users).toBeInstanceOf(Array);
+        expect(users.length).toBe(2);
+        expect(users[0]).toBeInstanceOf(User);
+        expect(users[1]).toBeInstanceOf(User);
       });
       });
     });
     });
   });
   });

+ 205 - 0
src/test/service/acl.test.js

@@ -0,0 +1,205 @@
+import each from 'jest-each';
+
+const { getInstance } = require('../setup-crowi');
+
+describe('AclService test', () => {
+  let crowi;
+
+  const initialEnv = process.env;
+
+  beforeEach(async(done) => {
+    crowi = await getInstance();
+    process.env = initialEnv;
+    done();
+  });
+
+
+  describe('isAclEnabled()', () => {
+
+    test('to be false when FORCE_WIKI_MODE is undefined', async() => {
+      delete process.env.FORCE_WIKI_MODE;
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isAclEnabled();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe(undefined);
+      expect(result).toBe(true);
+    });
+
+    test('to be false when FORCE_WIKI_MODE is dummy string', async() => {
+      process.env.FORCE_WIKI_MODE = 'dummy string';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isAclEnabled();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('dummy string');
+      expect(result).toBe(true);
+    });
+
+    test('to be true when FORCE_WIKI_MODE=private', async() => {
+      process.env.FORCE_WIKI_MODE = 'private';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isAclEnabled();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('private');
+      expect(result).toBe(true);
+    });
+
+    test('to be false when FORCE_WIKI_MODE=public', async() => {
+      process.env.FORCE_WIKI_MODE = 'public';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isAclEnabled();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('public');
+      expect(result).toBe(false);
+    });
+
+  });
+
+
+  describe('isWikiModeForced()', () => {
+
+    test('to be false when FORCE_WIKI_MODE is undefined', async() => {
+      delete process.env.FORCE_WIKI_MODE;
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isWikiModeForced();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe(undefined);
+      expect(result).toBe(false);
+    });
+
+    test('to be false when FORCE_WIKI_MODE is dummy string', async() => {
+      process.env.FORCE_WIKI_MODE = 'dummy string';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isWikiModeForced();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('dummy string');
+      expect(result).toBe(false);
+    });
+
+    test('to be true when FORCE_WIKI_MODE=private', async() => {
+      process.env.FORCE_WIKI_MODE = 'private';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isWikiModeForced();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('private');
+      expect(result).toBe(true);
+    });
+
+    test('to be false when FORCE_WIKI_MODE=public', async() => {
+      process.env.FORCE_WIKI_MODE = 'public';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isWikiModeForced();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('public');
+      expect(result).toBe(true);
+    });
+
+  });
+
+
+  describe('isGuestAllowedToRead()', () => {
+    let getConfigSpy;
+
+    beforeEach(async(done) => {
+      // prepare spy for ConfigManager.getConfig
+      getConfigSpy = jest.spyOn(crowi.configManager, 'getConfig');
+      done();
+    });
+
+    test('to be false when FORCE_WIKI_MODE=private', async() => {
+      process.env.FORCE_WIKI_MODE = 'private';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isGuestAllowedToRead();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('private');
+      expect(getConfigSpy).not.toHaveBeenCalledWith('crowi', 'security:restrictGuestMode');
+      expect(result).toBe(false);
+    });
+
+    test('to be true when FORCE_WIKI_MODE=public', async() => {
+      process.env.FORCE_WIKI_MODE = 'public';
+
+      // reload
+      await crowi.configManager.loadConfigs();
+
+      const result = crowi.aclService.isGuestAllowedToRead();
+
+      const wikiMode = crowi.configManager.getConfig('crowi', 'security:wikiMode');
+      expect(wikiMode).toBe('public');
+      expect(getConfigSpy).not.toHaveBeenCalledWith('crowi', 'security:restrictGuestMode');
+      expect(result).toBe(true);
+    });
+
+    each`
+    restrictGuestMode   | expected
+      ${undefined}      | ${false}
+      ${'Deny'}         | ${false}
+      ${'Readonly'}     | ${true}
+      ${'Open'}         | ${false}
+      ${'Restricted'}   | ${false}
+      ${'closed'}       | ${false}
+    `
+      .test('to be $expected when FORCE_WIKI_MODE is undefined'
+          + ' and `security:restrictGuestMode` is \'$restrictGuestMode\'', async({ restrictGuestMode, expected }) => {
+
+        // reload
+        await crowi.configManager.loadConfigs();
+
+        // setup mock implementation
+        getConfigSpy.mockImplementation((ns, key) => {
+          if (ns === 'crowi' && key === 'security:restrictGuestMode') {
+            return restrictGuestMode;
+          }
+          if (ns === 'crowi' && key === 'security:wikiMode') {
+            return undefined;
+          }
+          throw new Error('Unexpected behavior.');
+        });
+
+        const result = crowi.aclService.isGuestAllowedToRead();
+
+        expect(getConfigSpy).toHaveBeenCalledTimes(2);
+        expect(getConfigSpy).toHaveBeenCalledWith('crowi', 'security:wikiMode');
+        expect(getConfigSpy).toHaveBeenCalledWith('crowi', 'security:restrictGuestMode');
+        expect(result).toBe(expected);
+      });
+
+  });
+
+
+});

+ 186 - 0
src/test/util/middlewares.test.js

@@ -0,0 +1,186 @@
+/* eslint-disable arrow-body-style */
+
+import each from 'jest-each';
+
+const { getInstance } = require('../setup-crowi');
+
+describe('middlewares.loginRequired', () => {
+  let crowi;
+  let middlewares;
+
+  beforeEach(async(done) => {
+    crowi = await getInstance();
+    middlewares = require('@server/util/middlewares')(crowi, null);
+    done();
+  });
+
+  // test('returns strict middlware when args is undefined', () => {
+  //   const func = middlewares.loginRequired();
+  //   expect(func).toBe(loginRequiredStrict);
+  // });
+
+  describe('not strict mode', () => {
+    // setup req/res/next
+    const req = {
+      originalUrl: 'original url 1',
+      session: {},
+    };
+    const res = {
+      redirect: jest.fn().mockReturnValue('redirect'),
+    };
+    const next = jest.fn().mockReturnValue('next');
+
+    let loginRequired;
+
+    beforeEach(async(done) => {
+      loginRequired = middlewares.loginRequired(false);
+      done();
+    });
+
+    test('pass guest user when aclService.isGuestAllowedToRead() returns true', () => {
+      // prepare spy for AclService.isGuestAllowedToRead
+      const isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead')
+        .mockImplementation(() => true);
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).toHaveBeenCalledTimes(1);
+      expect(next).toHaveBeenCalled();
+      expect(res.redirect).not.toHaveBeenCalled();
+      expect(result).toBe('next');
+    });
+
+    test('redirect to \'/login\' when aclService.isGuestAllowedToRead() returns false', () => {
+      // prepare spy for AclService.isGuestAllowedToRead
+      const isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead')
+        .mockImplementation(() => false);
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).toHaveBeenCalled();
+      expect(next).not.toHaveBeenCalled();
+      expect(res.redirect).toHaveBeenCalledTimes(1);
+      expect(res.redirect).toHaveBeenCalledWith('/login');
+      expect(result).toBe('redirect');
+    });
+
+  });
+
+
+  describe('strict mode', () => {
+    // setup req/res/next
+    const req = {
+      originalUrl: 'original url 1',
+      session: null,
+    };
+    const res = {
+      redirect: jest.fn().mockReturnValue('redirect'),
+      sendStatus: jest.fn().mockReturnValue('sendStatus'),
+    };
+    const next = jest.fn().mockReturnValue('next');
+
+    let loginRequired;
+    let isGuestAllowedToReadSpy;
+
+    beforeEach(async(done) => {
+      loginRequired = middlewares.loginRequired();
+      // reset session object
+      req.session = {};
+      // spy for AclService.isGuestAllowedToRead
+      isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead');
+      done();
+    });
+
+    test('send status 403 when \'req.path\' starts with \'_api\'', () => {
+      req.path = '/_api/someapi';
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+      expect(next).not.toHaveBeenCalled();
+      expect(res.redirect).not.toHaveBeenCalled();
+      expect(res.sendStatus).toHaveBeenCalledTimes(1);
+      expect(res.sendStatus).toHaveBeenCalledWith(403);
+      expect(result).toBe('sendStatus');
+    });
+
+    test('redirect to \'/login\' when the user does not loggedin', () => {
+      req.path = '/path/that/requires/loggedin';
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+      expect(next).not.toHaveBeenCalled();
+      expect(res.sendStatus).not.toHaveBeenCalled();
+      expect(res.redirect).toHaveBeenCalledTimes(1);
+      expect(res.redirect).toHaveBeenCalledWith('/login');
+      expect(result).toBe('redirect');
+      expect(req.session.jumpTo).toBe('original url 1');
+    });
+
+    test('pass user who logged in', () => {
+      const User = crowi.model('User');
+
+      req.user = {
+        _id: 'user id',
+        status: User.STATUS_ACTIVE,
+      };
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+      expect(res.sendStatus).not.toHaveBeenCalled();
+      expect(res.redirect).not.toHaveBeenCalled();
+      expect(next).toHaveBeenCalledTimes(1);
+      expect(result).toBe('next');
+      expect(req.session.jumpTo).toBe(undefined);
+    });
+
+    each`
+      userStatus  | expectedPath
+      ${1}        | ${'/login/error/registered'}
+      ${3}        | ${'/login/error/suspended'}
+      ${5}        | ${'/login/invited'}
+    `
+      .test('redirect to \'$expectedPath\''
+        + ' when user.status is \'$userStatus\' ', ({ userStatus, expectedPath }) => {
+
+        req.user = {
+          _id: 'user id',
+          status: userStatus,
+        };
+
+        const result = loginRequired(req, res, next);
+
+        expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+        expect(next).not.toHaveBeenCalled();
+        expect(res.sendStatus).not.toHaveBeenCalled();
+        expect(res.redirect).toHaveBeenCalledTimes(1);
+        expect(res.redirect).toHaveBeenCalledWith(expectedPath);
+        expect(result).toBe('redirect');
+        expect(req.session.jumpTo).toBe(undefined);
+      });
+
+    test('redirect to \'/login\' when user.status is \'STATUS_DELETED\'', () => {
+      const User = crowi.model('User');
+
+      req.path = '/path/that/requires/loggedin';
+      req.user = {
+        _id: 'user id',
+        status: User.STATUS_DELETED,
+      };
+
+      const result = loginRequired(req, res, next);
+
+      expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+      expect(next).not.toHaveBeenCalled();
+      expect(res.sendStatus).not.toHaveBeenCalled();
+      expect(res.redirect).toHaveBeenCalledTimes(1);
+      expect(res.redirect).toHaveBeenCalledWith('/login');
+      expect(result).toBe('redirect');
+      expect(req.session.jumpTo).toBe('original url 1');
+    });
+
+  });
+
+});

+ 202 - 189
yarn.lock

@@ -1353,7 +1353,7 @@ ajv@^6.1.0:
     fast-json-stable-stringify "^2.0.0"
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
     json-schema-traverse "^0.3.0"
 
 
-ajv@^6.5.5, ajv@^6.9.1:
+ajv@^6.10.0, ajv@^6.5.5, ajv@^6.9.1:
   version "6.10.0"
   version "6.10.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
   dependencies:
   dependencies:
@@ -1699,14 +1699,6 @@ axios@^0.19.0:
     follow-redirects "1.5.10"
     follow-redirects "1.5.10"
     is-buffer "^2.0.2"
     is-buffer "^2.0.2"
 
 
-babel-code-frame@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
-  dependencies:
-    chalk "^1.1.3"
-    esutils "^2.0.2"
-    js-tokens "^3.0.2"
-
 babel-eslint@^10.0.1:
 babel-eslint@^10.0.1:
   version "10.0.1"
   version "10.0.1"
   resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
   resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
@@ -2331,6 +2323,11 @@ camelcase@^5.0.0:
   version "5.0.0"
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
 
 
+camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
 camelize@1.0.0:
 camelize@1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
@@ -3128,22 +3125,23 @@ css-declaration-sorter@^3.0.0:
     postcss "^6.0.0"
     postcss "^6.0.0"
     timsort "^0.3.0"
     timsort "^0.3.0"
 
 
-css-loader@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
+css-loader@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.0.0.tgz#bdd48a4921eefedf1f0a55266585944d4e5efc63"
+  integrity sha512-WR6KZuCkFbnMhRrGPlkwAA7SSCtwqPwpyXJAPhotYkYsc0mKU9n/fu5wufy4jl2WhBw9Ia8gUQMIp/1w98DuPw==
   dependencies:
   dependencies:
-    babel-code-frame "^6.26.0"
-    css-selector-tokenizer "^0.7.0"
-    icss-utils "^2.1.0"
-    loader-utils "^1.0.2"
-    lodash "^4.17.11"
-    postcss "^6.0.23"
-    postcss-modules-extract-imports "^1.2.0"
-    postcss-modules-local-by-default "^1.2.0"
-    postcss-modules-scope "^1.1.0"
-    postcss-modules-values "^1.3.0"
-    postcss-value-parser "^3.3.0"
-    source-list-map "^2.0.0"
+    camelcase "^5.3.1"
+    cssesc "^3.0.0"
+    icss-utils "^4.1.1"
+    loader-utils "^1.2.3"
+    normalize-path "^3.0.0"
+    postcss "^7.0.17"
+    postcss-modules-extract-imports "^2.0.0"
+    postcss-modules-local-by-default "^3.0.2"
+    postcss-modules-scope "^2.1.0"
+    postcss-modules-values "^3.0.0"
+    postcss-value-parser "^4.0.0"
+    schema-utils "^1.0.0"
 
 
 css-select-base-adapter@~0.1.0:
 css-select-base-adapter@~0.1.0:
   version "0.1.0"
   version "0.1.0"
@@ -3158,14 +3156,6 @@ css-select@~1.3.0-rc0:
     domutils "1.5.1"
     domutils "1.5.1"
     nth-check "^1.0.1"
     nth-check "^1.0.1"
 
 
-css-selector-tokenizer@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
-  dependencies:
-    cssesc "^0.1.0"
-    fastparse "^1.1.1"
-    regexpu-core "^1.0.0"
-
 css-tree@1.0.0-alpha.29:
 css-tree@1.0.0-alpha.29:
   version "1.0.0-alpha.29"
   version "1.0.0-alpha.29"
   resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
   resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
@@ -3192,9 +3182,10 @@ css-what@2.1:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
 
 
-cssesc@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
 
 cssfilter@0.0.10:
 cssfilter@0.0.10:
   version "0.0.10"
   version "0.0.10"
@@ -3280,9 +3271,10 @@ cssstyle@^1.0.0:
   dependencies:
   dependencies:
     cssom "0.3.x"
     cssom "0.3.x"
 
 
-csv-to-markdown-table@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/csv-to-markdown-table/-/csv-to-markdown-table-0.5.0.tgz#df7b5fd2d7d433319cec2fc01f3213c945de99f6"
+csv-to-markdown-table@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/csv-to-markdown-table/-/csv-to-markdown-table-1.0.1.tgz#43da1b0c0c483faa10a23921abc5e47a48e0daba"
+  integrity sha512-sw7oHNTBvmvztdDp5ZdIA3FPOy7fVol08hPgdSfVky4D1bcIoKwSiUeB/3G99mSaHnZh7wgCHcT7wAmyiyiaQA==
 
 
 currently-unhandled@^0.4.1:
 currently-unhandled@^0.4.1:
   version "0.4.1"
   version "0.4.1"
@@ -4024,9 +4016,10 @@ eslint-config-airbnb@^17.1.0:
     object.assign "^4.1.0"
     object.assign "^4.1.0"
     object.entries "^1.0.4"
     object.entries "^1.0.4"
 
 
-eslint-config-weseek@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/eslint-config-weseek/-/eslint-config-weseek-1.0.1.tgz#f8291c0af8f7001cf3c0b7b60c97c01a9cf5422c"
+eslint-config-weseek@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/eslint-config-weseek/-/eslint-config-weseek-1.0.2.tgz#ae96be9d1ef81ac5cdb4c76212badea767f56e00"
+  integrity sha512-Ak3nV0qh3fx8029h05WB9Eql62hQk+bzOAqdeY3jmiPaYo2TI2kRD+gqe4a8wOkMmcKrQDxynuwmP9tmhkGBtw==
   dependencies:
   dependencies:
     eslint-config-airbnb "^17.1.0"
     eslint-config-airbnb "^17.1.0"
 
 
@@ -4037,44 +4030,50 @@ eslint-import-resolver-node@^0.3.2:
     debug "^2.6.9"
     debug "^2.6.9"
     resolve "^1.5.0"
     resolve "^1.5.0"
 
 
-eslint-module-utils@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49"
+eslint-module-utils@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a"
+  integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==
   dependencies:
   dependencies:
     debug "^2.6.8"
     debug "^2.6.8"
     pkg-dir "^2.0.0"
     pkg-dir "^2.0.0"
 
 
-eslint-plugin-import@^2.16.0:
-  version "2.16.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
+eslint-plugin-import@^2.18.0:
+  version "2.18.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678"
+  integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig==
   dependencies:
   dependencies:
+    array-includes "^3.0.3"
     contains-path "^0.1.0"
     contains-path "^0.1.0"
     debug "^2.6.9"
     debug "^2.6.9"
     doctrine "1.5.0"
     doctrine "1.5.0"
     eslint-import-resolver-node "^0.3.2"
     eslint-import-resolver-node "^0.3.2"
-    eslint-module-utils "^2.3.0"
+    eslint-module-utils "^2.4.0"
     has "^1.0.3"
     has "^1.0.3"
     lodash "^4.17.11"
     lodash "^4.17.11"
     minimatch "^3.0.4"
     minimatch "^3.0.4"
     read-pkg-up "^2.0.0"
     read-pkg-up "^2.0.0"
-    resolve "^1.9.0"
+    resolve "^1.11.0"
 
 
-eslint-plugin-jest@^22.6.4:
-  version "22.6.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
-  integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
+eslint-plugin-jest@^22.7.1:
+  version "22.7.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
+  integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
 
 
-eslint-plugin-react@^7.12.4:
-  version "7.12.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz#b1ecf26479d61aee650da612e425c53a99f48c8c"
+eslint-plugin-react@^7.14.2:
+  version "7.14.2"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1"
+  integrity sha512-jZdnKe3ip7FQOdjxks9XPN0pjUKZYq48OggNMd16Sk+8VXx6JOvXmlElxROCgp7tiUsTsze3jd78s/9AFJP2mA==
   dependencies:
   dependencies:
     array-includes "^3.0.3"
     array-includes "^3.0.3"
     doctrine "^2.1.0"
     doctrine "^2.1.0"
     has "^1.0.3"
     has "^1.0.3"
-    jsx-ast-utils "^2.0.1"
+    jsx-ast-utils "^2.1.0"
+    object.entries "^1.1.0"
     object.fromentries "^2.0.0"
     object.fromentries "^2.0.0"
-    prop-types "^15.6.2"
-    resolve "^1.9.0"
+    object.values "^1.1.0"
+    prop-types "^15.7.2"
+    resolve "^1.10.1"
 
 
 eslint-restricted-globals@^0.1.1:
 eslint-restricted-globals@^0.1.1:
   version "0.1.1"
   version "0.1.1"
@@ -4094,9 +4093,10 @@ eslint-scope@^4.0.0:
     esrecurse "^4.1.0"
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
     estraverse "^4.1.1"
 
 
-eslint-scope@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
+eslint-scope@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
+  integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
   dependencies:
   dependencies:
     esrecurse "^4.1.0"
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
     estraverse "^4.1.1"
@@ -4109,31 +4109,33 @@ eslint-visitor-keys@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
 
 
-eslint@^5.15.1:
-  version "5.15.1"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.15.1.tgz#8266b089fd5391e0009a047050795b1d73664524"
+eslint@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.0.1.tgz#4a32181d72cb999d6f54151df7d337131f81cda7"
+  integrity sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==
   dependencies:
   dependencies:
     "@babel/code-frame" "^7.0.0"
     "@babel/code-frame" "^7.0.0"
-    ajv "^6.9.1"
+    ajv "^6.10.0"
     chalk "^2.1.0"
     chalk "^2.1.0"
     cross-spawn "^6.0.5"
     cross-spawn "^6.0.5"
     debug "^4.0.1"
     debug "^4.0.1"
     doctrine "^3.0.0"
     doctrine "^3.0.0"
-    eslint-scope "^4.0.2"
+    eslint-scope "^4.0.3"
     eslint-utils "^1.3.1"
     eslint-utils "^1.3.1"
     eslint-visitor-keys "^1.0.0"
     eslint-visitor-keys "^1.0.0"
-    espree "^5.0.1"
+    espree "^6.0.0"
     esquery "^1.0.1"
     esquery "^1.0.1"
     esutils "^2.0.2"
     esutils "^2.0.2"
     file-entry-cache "^5.0.1"
     file-entry-cache "^5.0.1"
     functional-red-black-tree "^1.0.1"
     functional-red-black-tree "^1.0.1"
-    glob "^7.1.2"
+    glob-parent "^3.1.0"
     globals "^11.7.0"
     globals "^11.7.0"
     ignore "^4.0.6"
     ignore "^4.0.6"
     import-fresh "^3.0.0"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
     imurmurhash "^0.1.4"
     inquirer "^6.2.2"
     inquirer "^6.2.2"
-    js-yaml "^3.12.0"
+    is-glob "^4.0.0"
+    js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.3.0"
     levn "^0.3.0"
     lodash "^4.17.11"
     lodash "^4.17.11"
@@ -4141,7 +4143,6 @@ eslint@^5.15.1:
     mkdirp "^0.5.1"
     mkdirp "^0.5.1"
     natural-compare "^1.4.0"
     natural-compare "^1.4.0"
     optionator "^0.8.2"
     optionator "^0.8.2"
-    path-is-inside "^1.0.2"
     progress "^2.0.0"
     progress "^2.0.0"
     regexpp "^2.0.1"
     regexpp "^2.0.1"
     semver "^5.5.1"
     semver "^5.5.1"
@@ -4150,9 +4151,10 @@ eslint@^5.15.1:
     table "^5.2.3"
     table "^5.2.3"
     text-table "^0.2.0"
     text-table "^0.2.0"
 
 
-espree@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
+espree@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6"
+  integrity sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==
   dependencies:
   dependencies:
     acorn "^6.0.7"
     acorn "^6.0.7"
     acorn-jsx "^5.0.0"
     acorn-jsx "^5.0.0"
@@ -4342,13 +4344,13 @@ express-session@^1.16.1:
     safe-buffer "5.1.2"
     safe-buffer "5.1.2"
     uid-safe "~2.1.5"
     uid-safe "~2.1.5"
 
 
-express-validator@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-5.3.1.tgz#6f42c6d52554441b0360c40ccfb555b1770affe2"
-  integrity sha512-g8xkipBF6VxHbO1+ksC7nxUU7+pWif0+OZXjZTybKJ/V0aTVhuCoHbyhIPgSYVldwQLocGExPtB2pE0DqK4jsw==
+express-validator@^6.1.1:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.1.1.tgz#2ac81c3a11ce670da6f85a39af6f726a587ee2e7"
+  integrity sha512-AF6YOhdDiCU7tUOO/OHp2W++I3qpYX7EInMmEEcRGOjs+qoubwgc5s6Wo3OQgxwsWRGCxXlrF73SIDEmY4y3wg==
   dependencies:
   dependencies:
-    lodash "^4.17.10"
-    validator "^10.4.0"
+    lodash "^4.17.11"
+    validator "^11.0.0"
 
 
 express-webpack-assets@^0.1.0:
 express-webpack-assets@^0.1.0:
   version "0.1.0"
   version "0.1.0"
@@ -4504,10 +4506,6 @@ fast-levenshtein@~2.0.4:
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
 
 
-fastparse@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
-
 fb-watchman@^2.0.0:
 fb-watchman@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
   resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@@ -5140,10 +5138,10 @@ graceful-fs@^4.1.15:
   version "4.1.15"
   version "4.1.15"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
 
 
-growi-commons@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.1.tgz#e0e71c9c286f493e11c0703c809385bcdc6a97a9"
-  integrity sha512-haH4Av1WuQIHic4Jv2RRwDprbKecRKF/3C0wVk9ssBzWtB3V6Oghj5gksajDpYOd7tOKdvkVEqqkFfIV4JQUyQ==
+growi-commons@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.3.tgz#aa8cec9a45854ff5a66d28bdf3b232adc64e0270"
+  integrity sha512-ktf6wdAOykVkrGCMWBArP+jHjZTg8iDFrnPGNNVoCxm1fnWfRVXBNu7a8mFIvB2wQScSvnoHs2RFBKN/GcJJoA==
 
 
 growly@^1.3.0:
 growly@^1.3.0:
   version "1.3.0"
   version "1.3.0"
@@ -5535,15 +5533,12 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
   dependencies:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
     safer-buffer ">= 2.1.2 < 3"
 
 
-icss-replace-symbols@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
-
-icss-utils@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
+icss-utils@^4.0.0, icss-utils@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
+  integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
   dependencies:
   dependencies:
-    postcss "^6.0.1"
+    postcss "^7.0.14"
 
 
 ieee754@^1.1.4:
 ieee754@^1.1.4:
   version "1.1.8"
   version "1.1.8"
@@ -6577,7 +6572,7 @@ js-levenshtein@^1.1.3:
   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
   integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
   integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
 
 
-js-tokens@^3.0.0, js-tokens@^3.0.2:
+js-tokens@^3.0.0:
   version "3.0.2"
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 
 
@@ -6592,7 +6587,7 @@ js-yaml@3.12.0, js-yaml@^3.9.0:
     argparse "^1.0.7"
     argparse "^1.0.7"
     esprima "^4.0.0"
     esprima "^4.0.0"
 
 
-js-yaml@3.13.1:
+js-yaml@3.13.1, js-yaml@^3.13.1:
   version "3.13.1"
   version "3.13.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
   integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
   integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -6767,11 +6762,13 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     json-schema "0.2.3"
     verror "1.10.0"
     verror "1.10.0"
 
 
-jsx-ast-utils@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
+jsx-ast-utils@^2.1.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb"
+  integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==
   dependencies:
   dependencies:
     array-includes "^3.0.3"
     array-includes "^3.0.3"
+    object.assign "^4.1.0"
 
 
 jwa@^1.1.4:
 jwa@^1.1.4:
   version "1.1.5"
   version "1.1.5"
@@ -7054,12 +7051,7 @@ lodash.has@^4.0, lodash.has@^4.5.2:
   version "4.5.2"
   version "4.5.2"
   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
 
 
-lodash.isboolean@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
-  integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
-
-lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
+lodash.isequal@^4.0.0:
   version "4.5.0"
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
@@ -7068,11 +7060,6 @@ lodash.isfinite@^3.3.2:
   version "3.3.2"
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
 
 
-lodash.isobject@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
-  integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
-
 lodash.memoize@^4.1.2:
 lodash.memoize@^4.1.2:
   version "4.1.2"
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7290,15 +7277,16 @@ markdown-it-toc-and-anchor-with-slugid@^1.1.4:
     clone "^2.1.0"
     clone "^2.1.0"
     uslug "^1.0.4"
     uslug "^1.0.4"
 
 
-markdown-it@^8.4.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.0.tgz#e2400881bf171f7018ed1bd9da441dac8af6306d"
+markdown-it@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-9.0.1.tgz#aafe363c43718720b6575fd10625cde6e4ff2d47"
+  integrity sha512-XC9dMBHg28Xi7y5dPuLjM61upIGPJG8AiHNHYqIaXER2KNnn7eKnM5/sF0ImNnyoV224Ogn9b1Pck8VH4k0bxw==
   dependencies:
   dependencies:
     argparse "^1.0.7"
     argparse "^1.0.7"
     entities "~1.1.1"
     entities "~1.1.1"
     linkify-it "^2.0.0"
     linkify-it "^2.0.0"
     mdurl "^1.0.1"
     mdurl "^1.0.1"
-    uc.micro "^1.0.3"
+    uc.micro "^1.0.5"
 
 
 markdown-table@^1.1.0:
 markdown-table@^1.1.0:
   version "1.1.2"
   version "1.1.2"
@@ -7552,10 +7540,10 @@ mimic-response@^1.0.0:
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
 
-mini-css-extract-plugin@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0"
-  integrity sha512-RQIw6+7utTYn8DBGsf/LpRgZCJMpZt+kuawJ/fju0KiOL6nAaTBNmCJwS7HtwSCXfS47gCkmtBFS7HdsquhdxQ==
+mini-css-extract-plugin@^0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
+  integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==
   dependencies:
   dependencies:
     loader-utils "^1.1.0"
     loader-utils "^1.1.0"
     normalize-url "1.9.1"
     normalize-url "1.9.1"
@@ -7786,11 +7774,6 @@ morgan@^1.9.0:
     on-finished "~2.3.0"
     on-finished "~2.3.0"
     on-headers "~1.0.1"
     on-headers "~1.0.1"
 
 
-mousetrap@^1.5.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
-  integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==
-
 move-concurrently@^1.0.1:
 move-concurrently@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -8312,7 +8295,7 @@ object.assign@^4.0.4, object.assign@^4.1.0:
     has-symbols "^1.0.0"
     has-symbols "^1.0.0"
     object-keys "^1.0.11"
     object-keys "^1.0.11"
 
 
-object.entries@^1.0.4:
+object.entries@^1.0.4, object.entries@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
   resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
   dependencies:
   dependencies:
@@ -8359,6 +8342,16 @@ object.values@^1.0.4:
     function-bind "^1.1.0"
     function-bind "^1.1.0"
     has "^1.0.1"
     has "^1.0.1"
 
 
+object.values@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+  integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.12.0"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
 oidc-token-hash@^3.0.1:
 oidc-token-hash@^3.0.1:
   version "3.0.2"
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e"
   resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e"
@@ -8826,7 +8819,7 @@ path-is-absolute@^1.0.0:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
 
 
-path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+path-is-inside@^1.0.1:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
   resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
 
 
@@ -9119,32 +9112,38 @@ postcss-minify-selectors@^4.0.0:
     postcss "^6.0.0"
     postcss "^6.0.0"
     postcss-selector-parser "^3.0.0"
     postcss-selector-parser "^3.0.0"
 
 
-postcss-modules-extract-imports@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a"
+postcss-modules-extract-imports@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+  integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
   dependencies:
   dependencies:
-    postcss "^6.0.1"
+    postcss "^7.0.5"
 
 
-postcss-modules-local-by-default@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+postcss-modules-local-by-default@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915"
+  integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
   dependencies:
   dependencies:
-    css-selector-tokenizer "^0.7.0"
-    postcss "^6.0.1"
+    icss-utils "^4.1.1"
+    postcss "^7.0.16"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.0"
 
 
-postcss-modules-scope@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+postcss-modules-scope@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb"
+  integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==
   dependencies:
   dependencies:
-    css-selector-tokenizer "^0.7.0"
-    postcss "^6.0.1"
+    postcss "^7.0.6"
+    postcss-selector-parser "^6.0.0"
 
 
-postcss-modules-values@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+postcss-modules-values@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
+  integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
   dependencies:
   dependencies:
-    icss-replace-symbols "^1.1.0"
-    postcss "^6.0.1"
+    icss-utils "^4.0.0"
+    postcss "^7.0.6"
 
 
 postcss-normalize-charset@^4.0.0:
 postcss-normalize-charset@^4.0.0:
   version "4.0.0"
   version "4.0.0"
@@ -9291,6 +9290,15 @@ postcss-selector-parser@^3.0.0, postcss-selector-parser@^3.1.0:
     indexes-of "^1.0.1"
     indexes-of "^1.0.1"
     uniq "^1.0.1"
     uniq "^1.0.1"
 
 
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
 postcss-sorting@^4.1.0:
 postcss-sorting@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-4.1.0.tgz#a107f0bf3852977fa64e4442bc340c88d5aacdb3"
   resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-4.1.0.tgz#a107f0bf3852977fa64e4442bc340c88d5aacdb3"
@@ -9319,6 +9327,11 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^
   version "3.3.0"
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
 
 
+postcss-value-parser@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d"
+  integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==
+
 postcss@^5.2.16:
 postcss@^5.2.16:
   version "5.2.18"
   version "5.2.18"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
@@ -9336,7 +9349,7 @@ postcss@^6.0.0:
     source-map "^0.6.1"
     source-map "^0.6.1"
     supports-color "^5.3.0"
     supports-color "^5.3.0"
 
 
-postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.17, postcss@^6.0.23, postcss@^6.0.6, postcss@^6.0.8:
+postcss@^6.0.14, postcss@^6.0.17, postcss@^6.0.23, postcss@^6.0.6, postcss@^6.0.8:
   version "6.0.23"
   version "6.0.23"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   dependencies:
   dependencies:
@@ -9352,6 +9365,15 @@ postcss@^7.0.0:
     source-map "^0.6.1"
     source-map "^0.6.1"
     supports-color "^5.4.0"
     supports-color "^5.4.0"
 
 
+postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.5, postcss@^7.0.6:
+  version "7.0.17"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
+  integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
 postcss@^7.0.2:
 postcss@^7.0.2:
   version "7.0.14"
   version "7.0.14"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5"
@@ -9474,14 +9496,6 @@ prop-types@^15.5.10, prop-types@^15.5.8:
     loose-envify "^1.3.1"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
     object-assign "^4.1.1"
 
 
-prop-types@^15.6.0, prop-types@^15.7.2:
-  version "15.7.2"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
-  dependencies:
-    loose-envify "^1.4.0"
-    object-assign "^4.1.1"
-    react-is "^16.8.1"
-
 prop-types@^15.6.1:
 prop-types@^15.6.1:
   version "15.6.1"
   version "15.6.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
@@ -9490,6 +9504,14 @@ prop-types@^15.6.1:
     loose-envify "^1.3.1"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
     object-assign "^4.1.1"
 
 
+prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
 proxy-addr@~2.0.2:
 proxy-addr@~2.0.2:
   version "2.0.2"
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -9755,16 +9777,12 @@ react-frame-component@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.0.0.tgz#57d51cdb2da3b204cc34577349f9f5bb84a76aac"
   resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.0.0.tgz#57d51cdb2da3b204cc34577349f9f5bb84a76aac"
 
 
-react-hotkeys@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72"
-  integrity sha1-oHEqouDAOnWf14hYCFmEl6TaznI=
+react-hotkeys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f"
+  integrity sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==
   dependencies:
   dependencies:
-    lodash.isboolean "^3.0.3"
-    lodash.isequal "^4.5.0"
-    lodash.isobject "^3.0.2"
-    mousetrap "^1.5.2"
-    prop-types "^15.6.0"
+    prop-types "^15.6.1"
 
 
 react-i18next@^10.6.1:
 react-i18next@^10.6.1:
   version "10.6.1"
   version "10.6.1"
@@ -10016,10 +10034,6 @@ regenerate-unicode-properties@^8.0.2:
   dependencies:
   dependencies:
     regenerate "^1.4.0"
     regenerate "^1.4.0"
 
 
-regenerate@^1.2.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
-
 regenerate@^1.4.0:
 regenerate@^1.4.0:
   version "1.4.0"
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
@@ -10071,14 +10085,6 @@ regexpp@^2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
 
 
-regexpu-core@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
-  dependencies:
-    regenerate "^1.2.1"
-    regjsgen "^0.2.0"
-    regjsparser "^0.1.4"
-
 regexpu-core@^4.5.4:
 regexpu-core@^4.5.4:
   version "4.5.4"
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae"
   resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae"
@@ -10104,21 +10110,11 @@ registry-url@^3.0.3:
   dependencies:
   dependencies:
     rc "^1.0.1"
     rc "^1.0.1"
 
 
-regjsgen@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
-
 regjsgen@^0.5.0:
 regjsgen@^0.5.0:
   version "0.5.0"
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
   integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
   integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
 
 
-regjsparser@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
-  dependencies:
-    jsesc "~0.5.0"
-
 regjsparser@^0.6.0:
 regjsparser@^0.6.0:
   version "0.6.0"
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
   resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
@@ -10377,6 +10373,13 @@ resolve@^1.0.0:
   dependencies:
   dependencies:
     path-parse "^1.0.5"
     path-parse "^1.0.5"
 
 
+resolve@^1.10.1, resolve@^1.11.0:
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
+  integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
+  dependencies:
+    path-parse "^1.0.6"
+
 resolve@^1.3.2:
 resolve@^1.3.2:
   version "1.11.0"
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
@@ -10384,7 +10387,7 @@ resolve@^1.3.2:
   dependencies:
   dependencies:
     path-parse "^1.0.6"
     path-parse "^1.0.6"
 
 
-resolve@^1.5.0, resolve@^1.9.0:
+resolve@^1.5.0:
   version "1.10.0"
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
   dependencies:
   dependencies:
@@ -11920,10 +11923,15 @@ uberproto@^1.1.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/uberproto/-/uberproto-1.2.0.tgz#61d4eab024f909c4e6ea52be867c4894a4beeb76"
   resolved "https://registry.yarnpkg.com/uberproto/-/uberproto-1.2.0.tgz#61d4eab024f909c4e6ea52be867c4894a4beeb76"
 
 
-uc.micro@^1.0.1, uc.micro@^1.0.3:
+uc.micro@^1.0.1:
   version "1.0.5"
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
   resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
 
 
+uc.micro@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+  integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
 uglify-js@2.6.0:
 uglify-js@2.6.0:
   version "2.6.0"
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.6.0.tgz#25eaa1cc3550e39410ceefafd1cfbb6b6d15f001"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.6.0.tgz#25eaa1cc3550e39410ceefafd1cfbb6b6d15f001"
@@ -12253,11 +12261,16 @@ validator@>=11.0.0:
   resolved "https://registry.yarnpkg.com/validator/-/validator-11.0.0.tgz#fb10128bfb1fd14ce4ed36b79fc94289eae70667"
   resolved "https://registry.yarnpkg.com/validator/-/validator-11.0.0.tgz#fb10128bfb1fd14ce4ed36b79fc94289eae70667"
   integrity sha512-+wnGLYqaKV2++nUv60uGzUJyJQwYVOin6pn1tgEiFCeCQO60yeu3Og9/yPccbBX574kxIcEJicogkzx6s6eyag==
   integrity sha512-+wnGLYqaKV2++nUv60uGzUJyJQwYVOin6pn1tgEiFCeCQO60yeu3Og9/yPccbBX574kxIcEJicogkzx6s6eyag==
 
 
-validator@^10.0.0, validator@^10.4.0:
+validator@^10.0.0:
   version "10.11.0"
   version "10.11.0"
   resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
   resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"
   integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
   integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
 
 
+validator@^11.0.0:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/validator/-/validator-11.1.0.tgz#ac18cac42e0aa5902b603d7a5d9b7827e2346ac4"
+  integrity sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==
+
 validator@^2.1.0:
 validator@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/validator/-/validator-2.1.0.tgz#63276570def208adcf1c032c1f4e6a17d2bd8d8b"
   resolved "https://registry.yarnpkg.com/validator/-/validator-2.1.0.tgz#63276570def208adcf1c032c1f4e6a17d2bd8d8b"