Browse Source

Merge branch 'master' into migrate/adjust-pageGrant

itizawa 6 years ago
parent
commit
a6f4f306a8
100 changed files with 2071 additions and 2370 deletions
  1. 0 16
      .babelrc
  2. 3 5
      .eslintrc.js
  3. 36 2
      CHANGES.md
  4. 1 0
      README.md
  5. 1 1
      THIRD-PARTY-NOTICES.md
  6. 23 0
      babel.config.js
  7. 6 1
      bin/generate-plugin-definitions-source.js
  8. 1 1
      bin/templates/plugin-definitions.js.swig
  9. 54 0
      config/jest.config.js
  10. 2 1
      config/logger/config.dev.js
  11. 3 0
      config/migrate.js
  12. 3 2
      config/webpack.common.js
  13. 4 2
      config/webpack.dev.dll.js
  14. 24 25
      package.json
  15. 0 21
      public/images/admin/security/passport-logo.svg
  16. 29 54
      resource/locales/en-US/translation.json
  17. 26 52
      resource/locales/ja/translation.json
  18. 3 11
      src/client/js/app.js
  19. 29 25
      src/client/js/components/InstallerForm.jsx
  20. 3 3
      src/client/js/components/Page/RevisionPath.jsx
  21. 4 2
      src/client/js/components/SavePageControls.jsx
  22. 4 2
      src/client/js/components/SavePageControls/GrantSelector.jsx
  23. 0 2
      src/client/js/components/SearchPage/SearchResult.js
  24. 1 0
      src/client/js/components/StaffCredit/Contributor.js
  25. 1 2
      src/client/js/ie11-polyfill.js
  26. 25 1
      src/client/js/legacy/crowi.js
  27. 10 3
      src/client/js/plugin.js
  28. 28 16
      src/client/styles/agile-admin/inverse/colors/antarctic.scss
  29. 4 0
      src/client/styles/scss/_login.scss
  30. 7 0
      src/client/styles/scss/_shortcuts.scss
  31. 3 1
      src/client/styles/scss/_vendor.scss
  32. 12 0
      src/lib/util/mongoose-utils.js
  33. 3 2
      src/migrations/20180926134048-make-email-unique.js
  34. 13 6
      src/migrations/20180927102719-init-serverurl.js
  35. 11 8
      src/migrations/20181019114028-abolish-page-group-relation.js
  36. 43 0
      src/migrations/20190618055300-abolish-crowi-classic-auth.js
  37. 64 0
      src/migrations/20190618104011-add-config-app-installed.js
  38. 49 0
      src/migrations/20190624110950-fill-last-update-user.js
  39. 29 0
      src/migrations/20190629193445-make-root-page-public.js
  40. 15 50
      src/server/crowi/express-init.js
  41. 130 46
      src/server/crowi/index.js
  42. 1 0
      src/server/form/admin/customfeatures.js
  43. 1 2
      src/server/form/admin/securityGeneral.js
  44. 0 8
      src/server/form/admin/securityGoogle.js
  45. 0 7
      src/server/form/admin/securityMechanism.js
  46. 9 0
      src/server/form/admin/securityPassportBasic.js
  47. 1 2
      src/server/form/index.js
  48. 0 2
      src/server/form/register.js
  49. 2 2
      src/server/models/attachment.js
  50. 59 539
      src/server/models/config.js
  51. 1 0
      src/server/models/index.js
  52. 2 5
      src/server/models/page.js
  53. 0 5
      src/server/models/user-group-relation.js
  54. 39 24
      src/server/models/user.js
  55. 9 3
      src/server/plugins/plugin.service.js
  56. 128 174
      src/server/routes/admin.js
  57. 1 1
      src/server/routes/hackmd.js
  58. 159 171
      src/server/routes/index.js
  59. 13 21
      src/server/routes/installer.js
  60. 47 1
      src/server/routes/login-passport.js
  61. 12 124
      src/server/routes/login.js
  62. 0 67
      src/server/routes/me.js
  63. 21 15
      src/server/routes/page.js
  64. 69 0
      src/server/service/acl.js
  65. 54 0
      src/server/service/app.js
  66. 1 1
      src/server/service/config-loader.js
  67. 86 28
      src/server/service/config-manager.js
  68. 56 0
      src/server/service/customize.js
  69. 11 12
      src/server/service/file-uploader/aws.js
  70. 2 1
      src/server/service/file-uploader/gridfs.js
  71. 1 0
      src/server/service/file-uploader/index.js
  72. 2 1
      src/server/service/file-uploader/local.js
  73. 2 2
      src/server/service/file-uploader/none.js
  74. 34 0
      src/server/service/file-uploader/uploader.js
  75. 1 3
      src/server/service/global-notification.js
  76. 0 22
      src/server/service/notification.js
  77. 99 50
      src/server/service/passport.js
  78. 5 5
      src/server/service/rest-qiita-API.js
  79. 20 0
      src/server/service/slack-notification.js
  80. 23 0
      src/server/service/user-group.js
  81. 71 0
      src/server/service/xss.js
  82. 0 70
      src/server/util/googleAuth.js
  83. 5 3
      src/server/util/importer.js
  84. 15 15
      src/server/util/mailer.js
  85. 171 204
      src/server/util/middlewares.js
  86. 3 5
      src/server/util/search.js
  87. 17 16
      src/server/util/slack.js
  88. 26 169
      src/server/util/swigFunctions.js
  89. 21 21
      src/server/views/admin/app.html
  90. 18 25
      src/server/views/admin/customize.html
  91. 1 1
      src/server/views/admin/external-accounts.html
  92. 1 1
      src/server/views/admin/global-notification-detail.html
  93. 1 1
      src/server/views/admin/importer.html
  94. 1 1
      src/server/views/admin/index.html
  95. 1 1
      src/server/views/admin/markdown.html
  96. 1 1
      src/server/views/admin/notification.html
  97. 1 1
      src/server/views/admin/search.html
  98. 37 172
      src/server/views/admin/security.html
  99. 1 1
      src/server/views/admin/user-group-detail.html
  100. 1 1
      src/server/views/admin/user-groups.html

+ 0 - 16
.babelrc

@@ -1,16 +0,0 @@
-{
-  "plugins": [
-    "lodash",
-    ["transform-runtime", {
-      "regenerator": true
-    }]
-  ],
-  "presets": [
-    ["env", {
-      "targets": {
-        "browsers": ["last 2 versions"]
-      }
-    }],
-    "react"
-  ]
-}

+ 3 - 5
.eslintrc.js

@@ -2,10 +2,11 @@ module.exports = {
   extends: [
     'weseek',
     'weseek/react',
+    "plugin:jest/recommended",
   ],
   env: {
-    mocha: true,
     jquery: true,
+    "jest/globals": true,
   },
   globals: {
     $: true,
@@ -15,7 +16,7 @@ module.exports = {
     window: true,
   },
   plugins: [
-    'chai-friendly',
+    "jest",
   ],
   rules: {
     'indent': [
@@ -35,8 +36,5 @@ module.exports = {
     ],
     // eslint-plugin-import rules
     'import/no-unresolved': [2, { ignore: ['^@'] }], // ignore @alias/..., @commons/..., ...
-    // eslint-plugin-chai-friendly rules
-    'no-unused-expressions': 0,
-    'chai-friendly/no-unused-expressions': 2,
   },
 };

+ 36 - 2
CHANGES.md

@@ -1,22 +1,56 @@
 # CHANGES
 
-## 3.5.0-RC
+## 3.5.1-RC
 
 ### BREAKING CHANGES
 
-* GROWI no longer supports [Crowi Template syntax](https://medium.com/crowi-book/crowi-v1-5-0-5a62e7c6be90)
+* GROWI no longer 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
+
+### Updates
+
+* Support: Use Babel 7
+* Support: Support plugins with schema version 3
+* Fix: Could not edit UserGroup even if `PUBLIC_WIKI_ONLY` is not set
+* Upgrade libs
+    * css-loader
+    * eslint
+    * eslint-config-weseek
+    * eslint-plugin-import
+    * eslint-plugin-jest
+    * eslint-plugin-react
+
+## 3.5.0
+
+### BREAKING CHANGES
+
+* GROWI no longer supports
+    * Protection system with Basic Authentication
+    * Crowi Classic Authentication Mechanism
+    * [Crowi Template syntax](https://medium.com/crowi-book/crowi-v1-5-0-5a62e7c6be90)
+
+Upgrading Guide: https://docs.growi.org/guide/upgrading/35x.html
 
 ### Updates
 
 * Feature: Comment Thread
 * Feature: OpenID Connect authentication
+* Feature: HTTP Basic authentication
 * Feature: Staff Credits with [Konami Code](https://en.wikipedia.org/wiki/Konami_Code)
+* Feature: Restricte Complete Deletion of Pages
 * Improvement Draft list
 * Fix: Deleting page completely
 * Fix: Search with `prefix:` param with CJK pathname
 * I18n: User Management Details
 * I18n: Group Management Details
 * Support: Apply unstated
+* Support: Abolish Old Config API
+* Support: Apply Jest for Tests
 * Support: Upgrade libs
     * async
     * axios

+ 1 - 0
README.md

@@ -201,6 +201,7 @@ Documentation
 ==============
 
 - [GROWI Docs](https://docs.growi.org/)
+- [GROWI Developers Wiki (ja)](https://dev.growi.org/)
 
 
 Contribution

+ 1 - 1
THIRD-PARTY-NOTICES.md

@@ -96,4 +96,4 @@ https://creativecommons.org/licenses/by-sa/3.0/
 
 ```
 Copyright (c) 2018 Stephen Hutchings
-```
+```

+ 23 - 0
babel.config.js

@@ -0,0 +1,23 @@
+module.exports = function(api) {
+  api.cache(true);
+
+  const presets = [
+    [
+      '@babel/preset-env',
+      {
+        targets: {
+          node: 'current',
+        },
+      },
+    ],
+    '@babel/preset-react',
+  ];
+  const plugins = [
+    'lodash',
+  ];
+
+  return {
+    presets,
+    plugins,
+  };
+};

+ 6 - 1
bin/generate-plugin-definitions-source.js

@@ -4,13 +4,14 @@
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
 require('module-alias/register');
+const logger = require('@alias/logger')('growi:bin:generate-plugin-definitions-source');
 
 const fs = require('graceful-fs');
 const normalize = require('normalize-path');
 const swig = require('swig-templates');
 
 const helpers = require('@commons/util/helpers');
-const PluginUtils = require('../src/server/plugins/plugin-utils');
+const PluginUtils = require('@server/plugins/plugin-utils');
 
 const pluginUtils = new PluginUtils();
 
@@ -20,12 +21,16 @@ const OUT = helpers.root('tmp/plugins/plugin-definitions.js');
 
 // list plugin names
 let pluginNames = pluginUtils.listPluginNames(helpers.root());
+logger.info('Detected plugins: ', pluginNames);
+
 // add from PLUGIN_NAMES_TOBE_LOADED when development
 if (process.env.NODE_ENV === 'development'
     && process.env.PLUGIN_NAMES_TOBE_LOADED !== undefined
     && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
   const pluginNamesDev = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
 
+  logger.info('Detected plugins from PLUGIN_NAMES_TOBE_LOADED: ', pluginNamesDev);
+
   // merge and remove duplicates
   if (pluginNamesDev.length > 0) {
     pluginNames = pluginNames.concat(pluginNamesDev);

+ 1 - 1
bin/templates/plugin-definitions.js.swig

@@ -8,7 +8,7 @@ module.exports = [
     meta: require('{{ definition.name }}'),
     entries: [
       {% for entryPath in definition.entries %}
-      require('{{ entryPath }}'),
+      require('{{ entryPath }}').default,
       {% endfor %}
     ]
   },

+ 54 - 0
config/jest.config.js

@@ -0,0 +1,54 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+module.exports = {
+  // Indicates whether each individual test should be reported during the run
+  verbose: true,
+
+  rootDir: '../',
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+  // Automatically reset mock state between every test
+  resetMocks: true,
+
+  projects: [
+    {
+      displayName: 'server',
+      testEnvironment: 'node',
+      rootDir: '.',
+      setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
+      testMatch: ['<rootDir>/src/test/**/*.test.js'],
+      // A map from regular expressions to module names that allow to stub out resources with a single module
+      moduleNameMapper: {
+        '@root/(.+)': '<rootDir>/$1',
+        '@commons/(.+)': '<rootDir>/src/lib/$1',
+        '@server/(.+)': '<rootDir>/src/server/$1',
+        '@alias/logger': '<rootDir>/src/lib/service/logger',
+        // -- doesn't work with unknown error -- 2019.06.19 Yuki Takei
+        // debug: '<rootDir>/src/lib/service/logger/alias-for-debug',
+      },
+    },
+    // {
+    //   displayName: 'client',
+    //   rootDir: '.',
+    //   testMatch: ['<rootDir>/src/test/client/**/*.test.js'],
+    // },
+  ],
+
+
+  // Indicates whether the coverage information should be collected while executing the test
+  // collectCoverage: false,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  collectCoverageFrom: [
+    'src/client/**/*.js',
+    'src/lib/**/*.js',
+    'src/migrations/**/*.js',
+    'src/server/**/*.js',
+  ],
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+};

+ 2 - 1
config/logger/config.dev.js

@@ -15,6 +15,7 @@ module.exports = {
   // 'growi:routes:login': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:service:PassportService': 'debug',
+  // 'growi:service:ConfigManager': 'debug',
   'growi:lib:search': 'debug',
   // 'growi:service:GlobalNotification': 'debug',
   // 'growi:lib:importer': 'debug',
@@ -30,5 +31,5 @@ module.exports = {
    */
   'growi:app': 'debug',
   'growi:services:*': 'debug',
-  'growi:StaffCredit': 'debug',
+  // 'growi:StaffCredit': 'debug',
 };

+ 3 - 0
config/migrate.js

@@ -5,6 +5,8 @@
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
 
+require('module-alias/register');
+
 function getMongoUri(env) {
   return env.MONGOLAB_URI // for B.C.
     || env.MONGODB_URI // MONGOLAB changes their env name
@@ -15,6 +17,7 @@ function getMongoUri(env) {
 
 const mongoUri = getMongoUri(process.env);
 const match = mongoUri.match(/^(.+)\/([^/]+)$/);
+
 module.exports = {
   mongoUri,
   mongodb: {

+ 3 - 2
config/webpack.common.js

@@ -125,6 +125,7 @@ module.exports = (options) => {
 
       // ignore
       new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
+      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
 
       new LodashModuleReplacementPlugin({
         flattening: true,
@@ -155,7 +156,7 @@ module.exports = (options) => {
             enforce: true,
           },
           commons: {
-            test: /src[\\/].*\.jsx?$/,
+            test: /(src|resource)[\\/].*\.(js|jsx|json)$/,
             chunks: 'initial',
             name: 'js/commons',
             minChunks: 2,
@@ -163,7 +164,7 @@ module.exports = (options) => {
             priority: 20,
           },
           vendors: {
-            test: /node_modules[\\/].*\.jsx?$/,
+            test: /node_modules[\\/].*\.(js|jsx|json)$/,
             chunks: (chunk) => {
               // ignore patterns
               return chunk.name != null && !chunk.name.match(/legacy-presentation|ie11-polyfill|hackmd-/);

+ 4 - 2
config/webpack.dev.dll.js

@@ -11,21 +11,23 @@ module.exports = {
     dlls: [
       // Libraries
       'axios',
-      'babel-polyfill',
       'browser-bunyan', 'bunyan-format',
       'codemirror', 'react-codemirror2',
       'date-fns',
       'diff2html',
       'debug',
       'entities',
+      'growi-commons',
       'i18next', 'i18next-browser-languagedetector',
       'jquery-slimscroll',
       'lodash', 'pako',
       'markdown-it', 'csv-to-markdown-table',
       'react', 'react-dom',
-      'react-bootstrap', 'react-bootstrap-typeahead', 'react-i18next', 'react-dropzone', 'react-copy-to-clipboard',
+      'react-bootstrap', 'react-bootstrap-typeahead',
+      'react-i18next', 'react-dropzone', 'react-hotkeys', 'react-copy-to-clipboard', 'react-waypoint',
       'socket.io-client',
       'toastr',
+      'unstated',
       'xss',
     ],
   },

+ 24 - 25
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.5.0-RC",
+  "version": "3.5.1-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -55,11 +55,15 @@
     "server:prod": "env-cmd -f config/env.prod.js node src/server/app.js",
     "server": "npm run server:dev",
     "start": "npm run server:prod",
-    "test": "mocha --timeout 10000 --exit -r src/test/bootstrap.js src/test/**/*.js",
+    "test": "jest --config=config/jest.config.js --passWithNoTests -- ",
     "version": "node -p \"require('./package.json').version\"",
     "webpack": "webpack"
   },
   "dependencies": {
+    "//": [
+      "check-node-version: see https://github.com/parshap/check-node-version/issues/35",
+      "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0"
+    ],
     "async": "^3.0.1",
     "aws-sdk": "^2.88.0",
     "axios": "^0.19.0",
@@ -67,7 +71,6 @@
     "body-parser": "^1.18.2",
     "bunyan": "^1.8.12",
     "bunyan-format": "^0.2.1",
-    "//": "see https://github.com/parshap/check-node-version/issues/35",
     "check-node-version": "=3.3.0",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^3.0.0",
@@ -88,7 +91,6 @@
     "express-session": "^1.16.1",
     "express-validator": "^5.3.1",
     "express-webpack-assets": "^0.1.0",
-    "googleapis": "^40.0.0",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^4.0.1",
     "helmet": "^3.13.0",
@@ -101,7 +103,7 @@
     "migrate-mongo": "^6.0.0",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
-    "mongoose": "^5.6.0",
+    "mongoose": "5.4.4",
     "mongoose-gridfs": "^1.2.2",
     "mongoose-paginate": "^5.0.3",
     "mongoose-unique-validator": "^2.0.3",
@@ -114,12 +116,11 @@
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-google-auth": "^1.0.2",
+    "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^1.0.0",
     "passport-twitter": "^1.0.4",
-    "react-dropzone": "^10.1.3",
-    "react-hotkeys": "^1.1.4",
     "rimraf": "^2.6.1",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
@@ -132,41 +133,41 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.0.16",
+    "@babel/core": "^7.4.5",
+    "@babel/polyfill": "^7.4.4",
+    "@babel/preset-env": "^7.4.5",
+    "@babel/preset-react": "^7.0.0",
     "@handsontable/react": "^2.0.0",
     "autoprefixer": "^9.0.0",
-    "babel-core": "^6.25.0",
     "babel-eslint": "^10.0.1",
-    "babel-loader": "^7.1.1",
-    "babel-plugin-lodash": "^3.3.2",
-    "babel-plugin-transform-runtime": "^6.23.0",
-    "babel-polyfill": "^6.26.0",
-    "babel-preset-env": "^1.6.0",
-    "babel-preset-react": "^6.24.1",
+    "babel-loader": "^8.0.6",
+    "babel-plugin-lodash": "^3.3.4",
     "bootstrap-sass": "^3.4.1",
     "bootstrap-select": "^1.12.4",
     "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
-    "chai": "^4.1.0",
     "cli": "~1.0.1",
     "codemirror": "^5.42.0",
     "colors": "^1.2.5",
     "commander": "^2.11.0",
     "connect-browser-sync": "^2.1.0",
-    "css-loader": "^1.0.0",
+    "core-js": "^2.6.9",
+    "css-loader": "^3.0.0",
     "csv-to-markdown-table": "^0.5.0",
     "date-fns": "^1.29.0",
     "diff2html": "^2.3.3",
     "eazy-logger": "^3.0.2",
-    "eslint": "^5.15.1",
-    "eslint-config-weseek": "^1.0.1",
-    "eslint-plugin-chai-friendly": "^0.4.1",
-    "eslint-plugin-import": "^2.16.0",
-    "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",
     "handsontable": "^6.0.1",
     "i18next-browser-languagedetector": "^3.0.1",
     "imports-loader": "^0.8.0",
+    "jest": "^24.8.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
@@ -184,11 +185,9 @@
     "markdown-table": "^1.1.1",
     "metismenu": "^3.0.3",
     "mini-css-extract-plugin": "^0.7.0",
-    "mocha": "^6.0.1",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
     "node-sass": "^4.11.0",
-    "nodelist-foreach-polyfill": "^1.2.0",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
@@ -203,15 +202,15 @@
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dom": "^16.8.3",
+    "react-dropzone": "^10.1.3",
     "react-frame-component": "^4.0.0",
+    "react-hotkeys": "^1.1.4",
     "react-i18next": "^10.6.1",
     "react-waypoint": "^9.0.0",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "sass-loader": "^7.1.0",
     "simple-load-script": "^1.0.2",
-    "sinon": "^7.2.2",
-    "sinon-chai": "^3.3.0",
     "socket.io-client": "^2.0.3",
     "style-loader": "^0.23.0",
     "stylelint-config-recess-order": "^2.0.1",

+ 0 - 21
public/images/admin/security/passport-logo.svg

@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="400px" height="500px" viewBox="0 0 400 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
-    <!-- Generator: Sketch 3.3.1 (12002) - http://www.bohemiancoding.com/sketch -->
-    <title>Group</title>
-    <desc>Created with Sketch.</desc>
-    <defs/>
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
-        <g id="passport_logo_final" sketch:type="MSLayerGroup" transform="translate(-400.000000, -350.000000)">
-            <g id="Group" sketch:type="MSShapeGroup">
-                <g id="Shape">
-                    <g transform="translate(400.000000, 350.000000)">
-                        <path d="M200,0 C89.5,0 0,89.5 0,200 L100,200 C100,144.8 144.8,100 200,100 L200,0 L200,0 Z" fill="#D6FF00"/>
-                        <path d="M400,200 C400,89.5 310.5,0 200,0 L200,100 C255.2,100 300,144.8 300,200 L400,200 L400,200 Z" fill="#34E27A"/>
-                        <path d="M200,400 C310.5,400 400,310.5 400,200 L300,200 C300,255.2 255.2,300 200,300 L200,400 L200,400 Z" fill="#00B9F1"/>
-                        <path d="M100,400 L100,200 L0,200 L0,500 L200,500 L200,400 L100,400 Z" fill="#FFFFFF"/>
-                    </g>
-                </g>
-            </g>
-        </g>
-    </g>
-</svg>

+ 29 - 54
resource/locales/en-US/translation.json

@@ -81,9 +81,7 @@
   "Delete this image?": "Delete this image?",
   "Updated": "Updated",
   "Upload new image": "Upload new image",
-  "Google Setting": "Google Setting",
   "Connected": "Connected",
-  "Disconnect": "Disconnect",
   "Show": "Show",
   "Hide": "Hide",
   "Disclose E-mail": "Disclose E-mail",
@@ -124,7 +122,6 @@
   "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-  "Selecting authentication mechanism": "Selecting authentication mechanism",
   "Add tags for this page": "Add tags for this page",
   "Edit tags for this page": "Edit tags for this page",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
@@ -149,15 +146,13 @@
   },
 
   "breaking_changes": {
-    "v346_passport_is_not_enabled": "Crowi Classic Authentication mechanism currently in use will <strong>no longer be supported</strong> in the near future. Switch to Passport from %s",
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
   },
 
   "page_register": {
     "notice": {
       "restricted": "Admin approval required.",
-      "restricted_defail": "Once the admin approves your sign up, you'll be able to access this wiki.",
-      "google_account_continue": "Enter your user ID, name and password to continue."
+      "restricted_defail": "Once the admin approves your sign up, you'll be able to access this wiki."
     },
     "form_help": {
       "email": "You must have email address which listed below to sign up to this wiki.",
@@ -169,11 +164,7 @@
   "page_me": {
     "form_help": {
       "profile_image1": "Image upload settings not completed.",
-      "profile_image2": "Set up AWS or enable local uploads.",
-      "google_connect1": "With Google Connect, you can sign in with your Google Account.",
-      "google_connect2": "Only Google Apps accounts with the following email addresses are connectable Google accounts:",
-      "google_disconnect1": "If you disconnect your Google account, you will be unable to sign in using Google Authentication",
-      "google_disconnect2": "After disconnecting your Google account, you can sign in normally using your email and password"
+      "profile_image2": "Set up AWS or enable local uploads."
     }
   },
   "page_me_apitoken": {
@@ -195,7 +186,7 @@
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
 
-  "Security Settings": "Security Settings",
+  "security_settings": "Security Settings",
 
   "API Settings": "API Settings",
   "API Token Settings": "API Token Settings",
@@ -270,7 +261,8 @@
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "already_exists": "New page is already exists.",
-    "outdated": "Page is updated someone and now outdated. "
+    "outdated": "Page is updated someone and now outdated.",
+    "user_not_admin": "Only admin user can delete completely"
   },
 
   "modal_rename": {
@@ -291,15 +283,13 @@
   "Delete Completely": "Delete Completely",
 
   "modal_delete": {
-    "label": {
-      "Delete Page": "Delete Page",
-      "Delete recursively": "Delete recursively",
-      "Delete completely": "Delete completely"
-    },
-    "help": {
-      "recursively": "Delete children of under <code>%s</code> recursively",
-      "completely": "Delete completely instead of putting it into trash"
-    }
+    "delete_page": "Delete Page",
+    "deleting_page": "Deleting Page",
+    "delete_recursively": "Delete child pages under %s recursively.",
+    "delete_completely": "Delete Completely",
+    "delete_completely_restriction": "You have no admin to delete completely.",
+    "recursively": "Delete children of under <code>%s</code> recursively.",
+    "completely": "Delete completely instead of putting it into trash."
   },
 
   "modal_duplicate": {
@@ -325,7 +315,10 @@
       "title": "Global shortcuts",
       "Open/Close shortcut help": "Open/Close shortcut help",
       "Edit Page": "Edit Page",
-      "Create Page": "Create Page"
+      "Create Page": "Create Page",
+      "Show Contributors": "Show Contributors",
+      "Konami Code": "Konami Code",
+      "konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"
     },
     "editor": {
       "title": "Editor shortcuts",
@@ -440,12 +433,11 @@
   },
 
   "security_setting": {
-		"Basic authentication": "Basic authentication",
+		"Basic authentication": "Basic Authentication",
 		"Security settings": "Security settings",
 		"Guest users access": "Guest users access",
 		"Register limitation": "Register limitation",
 		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-		"Selecting authentication mechanism": "Selecting authentication mechanism",
 		"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.",
@@ -459,34 +451,14 @@
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
     "page_listing_2": "Page listing/searching<br>restricted by User Group",
     "page_listing_2_desc": "Show pages that are restricted by User Group when listing/searching",
+    "complete_deletion": "Restrict Complete Deletion of Pages",
+    "complete_deletion_explain": "Restricts users who can completely delete pages.",
+    "admin_only": "Admin Only",
+    "admin_and_author": "Admin and Author",
+    "anyone": "Anyone",
 
-		"Authentication mechanism settings": "Authentication mechanism settings",
-    "note": "Note",
-    "require_server_restart_change_auth": "Restarting the server is required if you switch the auth mechanism.",
-    "auth_mechanism": "authentication mechanism",
-    "recommended": "Recommended",
-    "username_email_password": "Username, Email and Password authentication",
+		"Authentication mechanism settings": "Authentication Mechanism Settings",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
-    "ldap_auth": "LDAP authentication",
-    "saml_auth": "SAML authentication",
-    "google_auth2": "Google OAuth authentication",
-    "google_auth2_by_crowi_desc": "However, this feature does not create new users, butit only makes it possible to login to the existing user who set up the association.",
-    "facebook_auth2": "Facebook OAuth authentication",
-    "twitter_auth2": "Twitter OAuth authentication",
-    "github_auth2": "GitHub OAuth authentication",
-    "crowi_auth": "Crowi classic authentication mechanism",
-		"require_server_restart": "Restarting the server is required.",
-		"server_on_passport_auth": "The server is running with Passport authentication mechanism.",
-		"server_on_crowi_auth": "The server is running with official Crowi authentication mechanism.",
-		"google_setting": "Google Setting",
-    "connect_api_manager": "You can use your Google account to sign up and login after creating OAuth2 ClientId at <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">API Manager on Google Cloud Platform</a>",
-		"access_api_manager": "Access <a href=\"%s\" target=\"_blank\">%s</a>",
-		"create_project": "Create Project if no projects have been created.",
-		"create_auth_to_oauth": "\"Create credentials\" -> \"OAuth clientID\"",
-		"select_webapp": "Select \"Web Application\"",
-    "change_redirect_url": "Enter <code>https://${crowi.host}/google/callback</code> <br>(where <code>${crowi.host}</code> is your host name) for \"Authorized redirect URIs\".",
-    "clientID": "Client ID",
-    "client_secret": "Client Secret",
     "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link":"Go to Markdown settings",
     "callback_URL": "Callback URL",
@@ -494,13 +466,15 @@
     "issuerHost": "Issuer Host",
     "scope": "Scope",
     "desc_of_callback_URL": "Use it in the setting of the %s provider",
+    "clientID": "Client ID",
+    "client_secret": "Client Secret",
     "guest_mode": {
       "deny": "Deny Unregistered Users",
       "readonly": "View Only"
     },
     "registration_mode": {
       "open": "Anyone",
-      "restricted": "Reuqire Admin permission",
+      "restricted": "Require Admin permission",
       "closed": "Invitation Only"
     },
     "configuration": " Configuration",
@@ -536,8 +510,9 @@
       "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
       "group_search_filter": "Group Search Filter",
       "group_search_filter_detail1": "The query used to filter for groups.",
-      "group_search_filter_detail2": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
-      "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
+      "group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
+      "group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
       "group_search_user_DN_property": "User DN Property",
       "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
       "test_config": "Test Saved Configuration"

+ 26 - 52
resource/locales/ja/translation.json

@@ -81,9 +81,7 @@
   "Delete this image?": "削除してよろしいですか?",
   "Updated": "更新しました",
   "Upload new image": "新しい画像をアップロード",
-  "Google Setting": "Google設定",
   "Connected": "接続されています",
-  "Disconnect": "接続を解除",
   "Show": "公開",
   "Hide": "非公開",
   "Disclose E-mail": "メールアドレスの公開",
@@ -124,7 +122,6 @@
   "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
-  "Selecting authentication mechanism": "認証機構選択",
   "Add tags for this page": "タグを付ける",
   "Edit tags for this page": "タグを編集する",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
@@ -149,15 +146,13 @@
   },
 
   "breaking_changes": {
-    "v346_passport_is_not_enabled": "現在利用中の Crowi Classic Authentication mechanism は、近い将来<strong>サポートされなくなります</strong>。%s から Passport に切り替えてください。",
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
   },
 
   "page_register": {
     "notice": {
        "restricted": "この Wiki への新規登録は制限されています。",
-       "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。",
-       "google_account_continue": "ユーザーID、名前、パスワードを決めて登録を継続してください。"
+       "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。"
     },
     "form_help": {
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
@@ -169,11 +164,7 @@
   "page_me": {
     "form_help": {
       "profile_image1": "画像をアップロードをするための設定がされていません。",
-      "profile_image2": "アップロードできるようにするには、AWS またはローカルアップロードの設定をしてください。",
-      "google_connect1": "Googleコネクトをすると、Googleアカウントでログイン可能になります。",
-      "google_connect2": "コネクト可能なGoogleアカウントは、以下のメールアドレスの発行できるGoogle Appsアカウントに限られます。",
-      "google_disconnect1": "接続を解除すると、Googleでログインができなくなります。",
-      "google_disconnect2": "解除後はメールアドレスとパスワードでログインすることができます。"
+      "profile_image2": "アップロードできるようにするには、AWS またはローカルアップロードの設定をしてください。"
     }
   },
   "page_me_apitoken": {
@@ -195,7 +186,7 @@
   "Re-enter new password": "(確認用)",
   "Password is not set": "パスワードが設定されていません",
 
-  "Security Settings": "セキュリティ設定",
+  "security_settings": "セキュリティ設定",
 
   "API Settings": "API設定",
   "API Token Settings": "API Token設定",
@@ -270,7 +261,8 @@
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "already_exists": "新しいページが既に存在しています。",
-    "outdated": "ページが他のユーザーによって更新されました。"
+    "outdated": "ページが他のユーザーによって更新されました。",
+    "user_not_admin": "権限のあるユーザーのみが完全削除できます"
   },
 
   "modal_rename": {
@@ -291,15 +283,13 @@
   "Delete Completely": "完全削除",
 
   "modal_delete": {
-    "label": {
-      "Delete Page": "ページを削除する",
-      "Delete recursively": "全ての子ページも削除",
-      "Delete completely": "完全削除"
-    },
-    "help": {
-      "recursively": "<code>%s</code> 配下のページも削除します",
-      "completely": "ゴミ箱を経由せず、完全に削除します"
-    }
+    "delete_page": "ページを削除する",
+    "deleting_page": "ページパス",
+    "delete_recursively": "全ての子ページも削除",
+    "delete_completely": "完全削除",
+    "delete_completely_restriction": "完全削除をするための権限がありません。",
+    "recursively": "<code>%s</code> 配下のページも削除します",
+    "completely": "ゴミ箱を経由せず、完全に削除します"
   },
 
   "modal_duplicate": {
@@ -325,7 +315,10 @@
       "title": "グローバルショートカット",
       "Open/Close shortcut help": "ショートカットヘルプの表示/非表示",
       "Edit Page": "ページ編集",
-      "Create Page": "ページ作成"
+      "Create Page": "ページ作成",
+      "Show Contributors": "コントリビューターを表示",
+      "Konami Code": "コナミコマンド",
+      "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
     },
     "editor": {
       "titile": "エディターショートカット",
@@ -441,11 +434,9 @@
 
   "security_setting": {
     "Basic authentication": "Basic認証",
-    "Security settings": "セキュリティ設定",
     "Guest users access": "ゲストユーザーのアクセス",
     "Register limitation": "登録の制限",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
-    "Selecting authentication mechanism": "認証機構選択",
     "common_authentication": "Basic認証を設定すると、ページ全体に共通の認証がかかります。",
     "without_encryption": "IDとパスワードは暗号化されずに送信されるのでご注意下さい。",
     "basic_acl_disable": "Public Wiki の設定のため、Basic認証は利用できません。",
@@ -459,38 +450,20 @@
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2_desc": "ページのリスト表示や検索結果において、特定グループにのみ閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
+    "complete_deletion": "ページの完全削除",
+    "complete_deletion_explain": "ページを完全に削除できるユーザーを制限します。",
+    "admin_only": "管理者のみ可能",
+    "admin_and_author": "管理者とページ作者が可能",
+    "anyone": "誰でも可能",
 
     "Authentication mechanism settings":"認証機構設定",
-    "note": "メモ",
-    "require_server_restart_change_auth": "認証機構の変更後はサーバーを再起動してください。",
-    "auth_mechanism": "認証機構",
-    "recommended": "推奨",
-    "username_email_password": "ユーザー名、Eメール、パスワードでの認証",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
-    "ldap_auth": "LDAP 認証",
-    "saml_auth": "SAML 認証",
-    "google_auth2": "Google OAuth 認証",
-    "google_auth2_by_crowi_desc": "ただし、この機能では新たなユーザーは作成されず、関連付け設定を行った既存ユーザーをログインできるようにするだけです。",
-    "facebook_auth2": "Facebook OAuth 認証",
-    "twitter_auth2": "Twitter OAuth 認証",
-    "github_auth2": "GitHub OAuth 認証",
-    "crowi_auth": "Crowi Classic OAuth 認証",
-    "require_server_restart": "サーバーを再起動してください。",
-    "server_on_passport_auth": "Passport 認証機構でサーバーが稼働しています。",
-    "server_on_crowi_auth": "Crowi Classic 認証機構でサーバーが稼働しています。",
-    "google_setting": "Google 設定",
-    "connect_api_manager": "Google Cloud Platform の <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">API Manager</a>から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。",
-    "access_api_manager": "<a href=\"%s\" target=\"_blank\">%s</a> へアクセス",
-    "create_project": "プロジェクトを作成していない場合は作成してください",
-    "create_auth_to_oauth": "「認証情報を作成」-> OAuthクライアントID",
-    "select_webapp": "「ウェブアプリケーション」を選択",
-    "change_redirect_url": "承認済みのリダイレクトURLに、 <code>https://${crowi.host}/google/callback</code> を入力<br>(<code>${crowi.host}</code>は環境に合わせて変更してください)",
-    "clientID": "クライアントID",
-    "client_secret": "クライアントシークレット",
     "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link":"マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
+    "clientID": "クライアントID",
+    "client_secret": "クライアントシークレット",
     "guest_mode": {
       "deny": "アカウントを持たないユーザーはアクセス不可",
       "readonly": "閲覧のみ許可"
@@ -533,8 +506,9 @@
       "group_search_base_DN_detail": "グループ検索を実行するベース DN。利用する場合は <code>グループ検索フィルター</code> も入力する必要があります。",
       "group_search_filter": "グループ検索フィルター",
       "group_search_filter_detail1": "グループフィルターに用いるクエリ",
-      "group_search_filter_detail2": "ログイン対象ユーザーオブジェクトのプロパティーで置換する場合は <code>&#123;&#123;dn&#125;&#125;</code> を用いてください。",
-      "group_search_filter_detail3": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> と、ユーザーの <code>uid</code> を含む <code>memberUid</code> を持つグループにヒットします(<code>ユーザーの DN プロパティー</code> がデフォルトから変更されていない場合)",
+      "group_search_filter_detail2": "このクエリにヒットするグループがあったときのみ、LDAPでのログインが成功します。",
+      "group_search_filter_detail3": "ログイン対象ユーザーオブジェクトのプロパティーで置換する場合は <code>&#123;&#123;dn&#125;&#125;</code> を用いてください。",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> は <code>cn=group1</code> と、ユーザーの <code>uid</code> を含む <code>memberUid</code> を持つグループにヒットします(<code>ユーザーの DN プロパティー</code> がデフォルトから変更されていない場合)",
       "group_search_user_DN_property": "ユーザーの DN プロパティー",
       "group_search_user_DN_property_detail": "<code>グループ検索フィルター</code> 内の <code>&#123;&#123;dn&#125;&#125;</code> で置換される、ユーザーオブジェクトのプロパティー",
       "test_config": "ログインテスト"

+ 3 - 11
src/client/js/app.js

@@ -97,6 +97,8 @@ let componentMappings = {
 
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
+
+  'staff-credit': <StaffCredit />,
 };
 
 // additional definitions if data exists
@@ -121,7 +123,7 @@ if (pageContainer.state.path != null) {
   componentMappings = Object.assign({
     // eslint-disable-next-line quote-props
     'page': <Page />,
-    'revision-path':  <RevisionPath pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} crowi={appContainer} />,
+    'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label':  <TagLabels />,
   }, componentMappings);
 }
@@ -183,16 +185,6 @@ if (adminGrantSelectorElem != null) {
   );
 }
 
-// render for stuff credit
-const pageStuffCreditElem = document.getElementById('staff-credit');
-if (pageStuffCreditElem) {
-  ReactDOM.render(
-    <StaffCredit></StaffCredit>,
-    pageStuffCreditElem,
-  );
-
-}
-
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

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

@@ -1,8 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+
 import i18next from '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 {
 
   constructor(props) {
@@ -10,6 +14,7 @@ class InstallerForm extends React.Component {
 
     this.state = {
       isValidUserName: true,
+      checkedBtn: 'en-US',
     };
     this.checkUserName = this.checkUserName.bind(this);
   }
@@ -32,6 +37,7 @@ class InstallerForm extends React.Component {
 
   changeLanguage(locale) {
     i18next.changeLanguage(locale);
+    this.setState({ checkedBtn: locale });
   }
 
   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>;
 
+    const checkedBtn = this.state.checkedBtn;
+
     return (
       <div className={`login-dialog p-t-10 p-b-10 col-sm-offset-4 col-sm-4${hasErrorClass}`}>
         <p className="alert alert-success">
@@ -48,30 +56,26 @@ class InstallerForm extends React.Component {
         </p>
 
         <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}`}>
             <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">
             <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
               <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>
           </div>
 

+ 3 - 3
src/client/js/components/Page/RevisionPath.jsx

@@ -26,8 +26,8 @@ class RevisionPath extends React.Component {
     this.setState({ isListPage });
 
     // whether set link to '/'
-    const behaviorType = this.props.crowi.getConfig().behaviorType;
-    const isLinkToListPage = (!behaviorType || behaviorType === 'crowi');
+    const { behaviorType } = this.props;
+    const isLinkToListPage = (behaviorType === 'crowi');
     this.setState({ isLinkToListPage });
 
     // generate pages obj
@@ -127,7 +127,7 @@ class RevisionPath extends React.Component {
 
 RevisionPath.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  crowi: PropTypes.object.isRequired,
+  behaviorType: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   pageId: PropTypes.string,
 };

+ 4 - 2
src/client/js/components/SavePageControls.jsx

@@ -65,8 +65,9 @@ class SavePageControls extends React.Component {
   }
 
   render() {
-    const { t, editorContainer } = this.props;
-    const labelSubmitButton = this.props.pageContainer.state.pageId == null ? t('Create') : t('Update');
+    const { t, pageContainer, editorContainer } = this.props;
+    const isRootPage = pageContainer.state.path === '/';
+    const labelSubmitButton = pageContainer.state.pageId == null ? t('Create') : t('Update');
     const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
     return (
@@ -88,6 +89,7 @@ class SavePageControls extends React.Component {
           && (
           <div className="mr-2">
             <GrantSelector
+              disabled={isRootPage}
               grant={editorContainer.state.grant}
               grantGroupId={editorContainer.state.grantGroupId}
               grantGroupName={editorContainer.state.grantGroupName}

+ 4 - 2
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -205,6 +205,7 @@ class GrantSelector extends React.Component {
     return (
       <FormGroup className="grant-selector m-b-0">
         <FormControl
+          disabled={this.props.disabled}
           componentClass="select"
           placeholder="select"
           defaultValue={selectedValue}
@@ -275,8 +276,8 @@ class GrantSelector extends React.Component {
   render() {
     return (
       <React.Fragment>
-        {this.renderGrantSelector()}
-        {this.renderSelectGroupModal()}
+        { this.renderGrantSelector() }
+        { this.props.disabled && this.renderSelectGroupModal() }
       </React.Fragment>
     );
   }
@@ -294,6 +295,7 @@ GrantSelector.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
+  disabled: PropTypes.bool,
   grant: PropTypes.number.isRequired,
   grantGroupId: PropTypes.string,
   grantGroupName: PropTypes.string,

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

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

+ 1 - 0
src/client/js/components/StaffCredit/Contributor.js

@@ -69,6 +69,7 @@ const contributors = [
           { name: 'nekoruri' },
           { name: 'kmyk' },
           { name: 'aximov' },
+          { name: 'tats-u' },
         ],
       },
     ],

+ 1 - 2
src/client/js/ie11-polyfill.js

@@ -1,2 +1 @@
-import 'nodelist-foreach-polyfill';
-import 'babel-polyfill';
+import '@babel/polyfill';

+ 25 - 1
src/client/js/legacy/crowi.js

@@ -1,4 +1,3 @@
-/* global location */
 /* eslint no-restricted-globals: ['error', 'locaion'] */
 
 import React from 'react';
@@ -212,6 +211,30 @@ Crowi.initSlimScrollForRevisionToc = () => {
   });
 };
 
+Crowi.initClassesByOS = function() {
+  // add classes to cmd-key by OS
+  const platform = navigator.platform.toLowerCase();
+  const isMac = (platform.indexOf('mac') > -1);
+
+  document.querySelectorAll('.system-version .cmd-key').forEach((element) => {
+    if (isMac) {
+      element.classList.add('mac');
+    }
+    else {
+      element.classList.add('win');
+    }
+  });
+
+  document.querySelectorAll('#shortcuts-modal .cmd-key').forEach((element) => {
+    if (isMac) {
+      element.classList.add('mac');
+    }
+    else {
+      element.classList.add('win', 'key-longer');
+    }
+  });
+};
+
 Crowi.findHashFromUrl = function(url) {
   let match;
   /* eslint-disable no-cond-assign */
@@ -748,6 +771,7 @@ window.addEventListener('load', (e) => {
   Crowi.modifyScrollTop();
   Crowi.initSlimScrollForRevisionToc();
   Crowi.initAffix();
+  Crowi.initClassesByOS();
 });
 
 window.addEventListener('hashchange', (e) => {

+ 10 - 3
src/client/js/plugin.js

@@ -30,12 +30,19 @@ export default class GrowiPlugin {
       switch (meta.pluginSchemaVersion) {
         // v1 is deprecated
         case 1:
+          logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           break;
-        // v2 or above
-        default:
+        // v2 is deprecated
+        case 2:
+          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+          break;
+        case 3:
           definition.entries.forEach((entry) => {
-            entry(appContainer, originRenderer);
+            entry(appContainer);
           });
+          break;
+        default:
+          logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
       }
     });
 

+ 28 - 16
src/client/styles/agile-admin/inverse/colors/antarctic.scss

@@ -16,20 +16,23 @@ $active-nav-tabs-bgcolor: $themecolor;
 $logo-mark-fill: $themelight;
 $wikilinktext: lighten($themecolor, 5%);
 $wikilinktext-hover: lighten($wikilinktext, 15%);
-$inline-code-color: darken($subthemecolor, 5%);
-$inline-code-bg: lighten($subthemecolor, 70%);
+$inline-code-color: #c7254e;
+$inline-code-bg: #f9f2f4;
 $border: $subthemecolor;
 $border-original: $subthemecolor;
 $navbar-border: $themecolor;
-$background-color: rgba($color: $themelight, $alpha: 0.8);
-$info:$subthemecolor;
+$background-color: rgba(
+  $color: $themelight,
+  $alpha: 0.8,
+);
+$info: $subthemecolor;
 
 @import 'apply-colors';
 @import 'apply-colors-light';
 
 // change color of highlighted header in wiki (default: orange)
 .code-line,
-ul>.text-muted {
+ul > .text-muted {
   color: $subthemecolor;
 }
 
@@ -50,7 +53,7 @@ ul>.text-muted {
 }
 
 // add background-image
-.main-container>#wrapper>#page-wrapper,
+.main-container > #wrapper > #page-wrapper,
 .page-editor-preview-container {
   background-image: url('/images/themes/antarctic/bg.svg');
   background-attachment: fixed;
@@ -99,7 +102,7 @@ header.affix {
   }
 }
 
-#wrapper>.navbar>.navbar-header {
+#wrapper > .navbar > .navbar-header {
   border-bottom: 4px solid $accentcolor;
 }
 
@@ -111,7 +114,7 @@ header.affix {
   .page-comment-main {
     box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
 
-    .page-comment-creator>a {
+    .page-comment-creator > a {
       border-bottom: 1px double $subthemecolor;
     }
   }
@@ -131,7 +134,7 @@ header.affix {
     }
 
     .nav.nav-tabs {
-      >li.active>a {
+      > li.active > a {
         background: $themecolor;
         border-bottom: solid 1px $themecolor;
         border-bottom-color: $themecolor;
@@ -144,21 +147,23 @@ header.affix {
  * Tabs
  */
 
-.nav.nav-tabs>li.active>a {
+.nav.nav-tabs > li.active > a {
   color: $themelight;
 }
 
 .text-info,
 body:not(.on-edit) .nav.nav-tabs {
-  >li>a {
+  > li > a {
     color: $subthemecolor;
   }
 
-  >li.active>a {
+  > li.active > a {
     color: $themelight;
-    background: linear-gradient(rgba($active-nav-tabs-bgcolor, 0) 50%,
+    background: linear-gradient(
+      rgba($active-nav-tabs-bgcolor, 0) 50%,
       rgba($active-nav-tabs-bgcolor, 0) 90%,
-      $active-nav-tabs-bgcolor 100%); // overwrite only the bottom pixel
+      $active-nav-tabs-bgcolor 100%
+    ); // overwrite only the bottom pixel
     background-color: $themecolor;
   }
 }
@@ -199,7 +204,6 @@ body:not(.on-edit) .nav.nav-tabs {
       ul {
         padding-left: 5px;
       }
-
     }
   }
 }
@@ -208,7 +212,7 @@ body:not(.on-edit) .nav.nav-tabs {
  *  Login page
  */
 
-.login-page>#wrapper>#page-wrapper {
+.login-page > #wrapper > #page-wrapper {
   background-image: url('/images/themes/antarctic/topimage.svg');
   background-attachment: fixed;
   background-position: center center;
@@ -222,3 +226,11 @@ body:not(.on-edit) .nav.nav-tabs {
     }
   }
 }
+
+/*
+ *  for Hightlight-js
+ */
+
+.hljs-ln {
+  background-color: transparent;
+}

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

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

+ 7 - 0
src/client/styles/scss/_shortcuts.scss

@@ -51,6 +51,13 @@
     &.key-long {
       width: 72px;
     }
+    &.key-small {
+      width: 24px;
+      height: 24px;
+      margin: 4px 2px;
+      font-size: 18px;
+      line-height: 22px;
+    }
   }
 
   .dl-horizontal {

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

@@ -1,5 +1,7 @@
 // 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 bootstrap4 utility classes

+ 12 - 0
src/lib/util/mongoose-utils.js

@@ -0,0 +1,12 @@
+const mongoose = require('mongoose');
+
+const getModelSafely = (modelName) => {
+  if (mongoose.modelNames().includes(modelName)) {
+    return mongoose.model(modelName);
+  }
+  return null;
+};
+
+module.exports = {
+  getModelSafely,
+};

+ 3 - 2
src/migrations/20180926134048-make-email-unique.js

@@ -1,16 +1,17 @@
-require('module-alias/register');
 const logger = require('@alias/logger')('growi:migrate:make-email-unique');
 
 const mongoose = require('mongoose');
 const config = require('@root/config/migrate');
 
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
 module.exports = {
 
   async up(db, next) {
     logger.info('Start migration');
     mongoose.connect(config.mongoUri, config.mongodb.options);
 
-    const User = require('@server/models/user')();
+    const User = getModelSafely('User') || require('@server/models/user')();
 
     // get all users who has 'deleted@deleted' email
     const users = await User.find({ email: 'deleted@deleted' });

+ 13 - 6
src/migrations/20180927102719-init-serverurl.js

@@ -1,11 +1,12 @@
 
 
-require('module-alias/register');
 const logger = require('@alias/logger')('growi:migrate:init-serverurl');
 
 const mongoose = require('mongoose');
 const config = require('@root/config/migrate');
 
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
 /**
  * check all values of the array are equal
  * @see https://stackoverflow.com/a/35568895
@@ -22,7 +23,7 @@ module.exports = {
     logger.info('Apply migration');
     mongoose.connect(config.mongoUri, config.mongodb.options);
 
-    const Config = require('@server/models/config')();
+    const Config = getModelSafely('Config') || require('@server/models/config')();
 
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
@@ -66,16 +67,22 @@ module.exports = {
     }
 
     if (siteUrl != null) {
-      await Config.findOneAndUpdateByNsAndKey('crowi', 'app:siteUrl', siteUrl);
+      const ns = 'crowi';
+      const key = 'app:siteUrl';
+      await Config.findOneAndUpdate(
+        { ns, key },
+        { ns, key, value: JSON.stringify(siteUrl) },
+        { upsert: true },
+      );
       logger.info('Migration has successfully applied');
     }
   },
 
   async down(db) {
-    logger.info('Undo migration');
+    logger.info('Rollback migration');
     mongoose.connect(config.mongoUri, config.mongodb.options);
 
-    const Config = require('@server/models/config')();
+    const Config = getModelSafely('Config') || require('@server/models/config')();
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
@@ -83,7 +90,7 @@ module.exports = {
       key: 'app:siteUrl',
     });
 
-    logger.info('Migration has successfully undoed');
+    logger.info('Migration has been successfully rollbacked');
   },
 
 };

+ 11 - 8
src/migrations/20181019114028-abolish-page-group-relation.js

@@ -1,9 +1,10 @@
-require('module-alias/register');
 const logger = require('@alias/logger')('growi:migrate:abolish-page-group-relation');
 
 const mongoose = require('mongoose');
 const config = require('@root/config/migrate');
 
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
 
 async function isCollectionExists(db, collectionName) {
   const collections = await db.listCollections({ name: collectionName }).toArray();
@@ -37,8 +38,8 @@ module.exports = {
       return;
     }
 
-    const Page = require('@server/models/page')();
-    const UserGroup = require('@server/models/user-group')();
+    const Page = getModelSafely('Page') || require('@server/models/page')();
+    const UserGroup = getModelSafely('UserGroup') || require('@server/models/user-group')();
 
     // retrieve all documents from 'pagegrouprelations'
     const relations = await db.collection('pagegrouprelations').find().toArray();
@@ -71,11 +72,11 @@ module.exports = {
   },
 
   async down(db) {
-    logger.info('Undo migration');
+    logger.info('Rollback migration');
     mongoose.connect(config.mongoUri, config.mongodb.options);
 
-    const Page = require('@server/models/page')();
-    const UserGroup = require('@server/models/user-group')();
+    const Page = getModelSafely('Page') || require('@server/models/page')();
+    const UserGroup = getModelSafely('UserGroup') || require('@server/models/user-group')();
 
     // retrieve all Page documents which granted by UserGroup
     const relatedPages = await Page.find({ grant: Page.GRANT_USER_GROUP });
@@ -106,9 +107,11 @@ module.exports = {
     }
     /* eslint-enable no-await-in-loop */
 
-    await db.collection('pagegrouprelations').insertMany(insertDocs);
+    if (insertDocs.length > 0) {
+      await db.collection('pagegrouprelations').insertMany(insertDocs);
+    }
 
-    logger.info('Migration has successfully undoed');
+    logger.info('Migration has been successfully rollbacked');
   },
 
 };

+ 43 - 0
src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -0,0 +1,43 @@
+const logger = require('@alias/logger')('growi:migrate:abolish-crowi-classic-auth');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+
+module.exports = {
+  async up(db, next) {
+    logger.info('Start migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    // enable passport and delete configs for crowi classic auth
+    await Promise.all([
+      Config.findOneAndUpdate(
+        { ns: 'crowi', key: 'security:isEnabledPassport' },
+        { ns: 'crowi', key: 'security:isEnabledPassport', value: JSON.stringify(true) },
+        { upsert: true },
+      ),
+      Config.findOneAndUpdate(
+        { ns: 'crowi', key: 'google:clientId' },
+        { ns: 'crowi', key: 'google:clientId', value: JSON.stringify(null) },
+        { upsert: true },
+      ),
+      Config.findOneAndUpdate(
+        { ns: 'crowi', key: 'google:clientSecret' },
+        { ns: 'crowi', key: 'google:clientSecret', value: JSON.stringify(null) },
+        { upsert: true },
+      ),
+    ]);
+
+    logger.info('Migration has successfully terminated');
+    next();
+  },
+
+  async down(db, next) {
+    // do not rollback
+    next();
+  },
+};

+ 64 - 0
src/migrations/20190618104011-add-config-app-installed.js

@@ -0,0 +1,64 @@
+const logger = require('@alias/logger')('growi:migrate:add-config-app-installed');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+
+/**
+ * BEFORE
+ *   - Config document { ns: 'crowi', key: 'app:installed' } does not exist
+ * AFTER
+ *   - Config document { ns: 'crowi', key: 'app:installed' } is created
+ *     - value will be true if one or more users exist
+ *     - value will be false if no users exist
+ */
+module.exports = {
+
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+    const User = getModelSafely('User') || require('@server/models/user')();
+
+    // find 'app:siteUrl'
+    const appInstalled = await Config.findOne({
+      ns: 'crowi',
+      key: 'app:installed',
+    });
+    // exit if exists
+    if (appInstalled != null) {
+      logger.info('\'app:appInstalled\' is already exists. This migration terminates without any changes.');
+      return;
+    }
+
+    const userCount = await User.count();
+
+    if (userCount > 0) {
+      await Config.create({
+        ns: 'crowi',
+        key: 'app:installed',
+        value: true,
+      });
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down(db) {
+    logger.info('Rollback migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+
+    // remote 'app:siteUrl'
+    await Config.findOneAndDelete({
+      ns: 'crowi',
+      key: 'app:installed',
+    });
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 49 - 0
src/migrations/20190624110950-fill-last-update-user.js

@@ -0,0 +1,49 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:abolish-page-group-relation');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+/**
+ * FIX https://github.com/weseek/growi/issues/1067
+ */
+module.exports = {
+
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Page = require('@server/models/page')();
+
+    // see https://stackoverflow.com/questions/3974985/update-mongodb-field-using-value-of-another-field/37280419#37280419
+
+    // retrieve target data
+    const pages = await Page.find({
+      $or: [
+        { lastUpdateUser: { $exists: false } },
+        { lastUpdateUser: { $eq: null } },
+      ],
+    }).select('_id creator');
+
+    // create requests for bulkWrite
+    const requests = pages.map((page) => {
+      return {
+        updateOne: {
+          filter: { _id: page._id },
+          update: { $set: { lastUpdateUser: page.creator } },
+        },
+      };
+    });
+
+    if (requests.length > 0) {
+      await db.collection('pages').bulkWrite(requests);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+
+};

+ 29 - 0
src/migrations/20190629193445-make-root-page-public.js

@@ -0,0 +1,29 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:make-root-page-public');
+
+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.findOneAndUpdate(
+      { path: '/' },
+      {
+        grant: Page.GRANT_PUBLIC,
+        grantedUsers: [],
+        grantedGroup: null,
+      },
+    );
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 15 - 50
src/server/crowi/express-init.js

@@ -11,7 +11,6 @@ module.exports = function(crowi, app) {
   const passport = require('passport');
   const expressSession = require('express-session');
   const sanitizer = require('express-sanitizer');
-  const basicAuth = require('basic-auth-connect');
   const flash = require('connect-flash');
   const swig = require('swig-templates');
   const webpackAssets = require('express-webpack-assets');
@@ -22,15 +21,12 @@ module.exports = function(crowi, app) {
 
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const i18nUserSettingDetector = require('../util/i18nUserSettingDetector');
-  const middleware = require('../util/middlewares');
 
   const env = crowi.node_env;
 
-  // Old type config API
-  const config = crowi.getConfig();
-  const Config = crowi.model('Config');
   // New type config API
   const configManager = crowi.configManager;
+  const getConfig = configManager.getConfig;
 
   const User = crowi.model('User');
   const lngDetector = new i18nMiddleware.LanguageDetector();
@@ -60,7 +56,7 @@ module.exports = function(crowi, app) {
 
   app.use((req, res, next) => {
     const now = new Date();
-    const tzoffset = -(config.crowi['app:timezone'] || 9) * 60;
+    const tzoffset = -(getConfig('crowi', 'app:timezone') || 9) * 60;
     // for datez
 
     const Page = crowi.model('Page');
@@ -68,12 +64,10 @@ module.exports = function(crowi, app) {
     const Config = crowi.model('Config');
     app.set('tzoffset', tzoffset);
 
-    req.config = config;
     req.csrfToken = null;
 
     res.locals.req = req;
-    res.locals.baseUrl = configManager.getSiteUrl();
-    res.locals.config = config;
+    res.locals.baseUrl = crowi.appService.getSiteUrl();
     res.locals.env = env;
     res.locals.now = now;
     res.locals.tzoffset = tzoffset;
@@ -81,10 +75,10 @@ module.exports = function(crowi, app) {
       pageGrants: Page.getGrantLabels(),
       userStatus: User.getUserStatusLabels(),
       language:   User.getLanguageLabels(),
-      restrictGuestMode: Config.getRestrictGuestModeLabels(),
-      registrationMode: Config.getRegistrationModeLabels(),
+      restrictGuestMode: crowi.aclService.getRestrictGuestModeLabels(),
+      registrationMode: crowi.aclService.getRegistrationModeLabels(),
     };
-    res.locals.local_config = Config.getLocalconfig(config); // config for browser context
+    res.locals.local_config = Config.getLocalconfig(); // config for browser context
 
     next();
   });
@@ -118,50 +112,21 @@ module.exports = function(crowi, app) {
     expressSession(crowi.sessionConfig)(req, res, next);
   });
 
-  // Set basic auth middleware
-  app.use((req, res, next) => {
-    if (req.query.access_token || req.body.access_token) {
-      return next();
-    }
-
-    // FIXME:
-    //   healthcheck endpoint exclude from basic authentication.
-    //   however, hard coding is not desirable.
-    //   need refactoring (ex. setting basic authentication for each routes)
-    if (req.path === '/_api/v3/healthcheck') {
-      return next();
-    }
-
-    const basicName = configManager.getConfig('crowi', 'security:basicName');
-    const basicSecret = configManager.getConfig('crowi', 'security:basicSecret');
-    if (basicName && basicSecret) {
-      return basicAuth(basicName, basicSecret)(req, res, next);
-    }
-
-    next();
-  });
-
   // passport
-  if (Config.isEnabledPassport(config)) {
-    debug('initialize Passport');
-    app.use(passport.initialize());
-    app.use(passport.session());
-  }
+  debug('initialize Passport');
+  app.use(passport.initialize());
+  app.use(passport.session());
 
   app.use(flash());
 
-  app.use(middleware.swigFilters(crowi, app, swig));
-  app.use(middleware.swigFunctions(crowi, app));
+  const middlewares = require('../util/middlewares')(crowi, app);
+
+  app.use(middlewares.swigFilters(swig));
+  app.use(middlewares.swigFunctions());
 
-  app.use(middleware.csrfKeyGenerator(crowi, app));
+  app.use(middlewares.csrfKeyGenerator());
 
-  // switch loginChecker
-  if (Config.isEnabledPassport(config)) {
-    app.use(middleware.loginCheckerForPassport(crowi, app));
-  }
-  else {
-    app.use(middleware.loginChecker(crowi, app));
-  }
+  app.use(middlewares.loginCheckerForPassport);
 
   app.use(i18nMiddleware.handle(i18next));
 };

+ 130 - 46
src/server/crowi/index.js

@@ -38,6 +38,11 @@ function Crowi(rootdir) {
   this.mailer = {};
   this.passportService = null;
   this.globalNotificationService = null;
+  this.slackNotificationService = null;
+  this.xssService = null;
+  this.aclService = null;
+  this.appService = null;
+  this.fileUploadService = null;
   this.restQiitaAPIService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
@@ -73,9 +78,17 @@ Crowi.prototype.init = async function() {
   await this.setupDatabase();
   await this.setupModels();
   await this.setupSessionConfig();
-  await this.setupAppConfig();
   await this.setupConfigManager();
 
+  // customizeService depends on AppService and XssService
+  // passportService depends on appService
+  // slack depends on setUpSlacklNotification
+  await Promise.all([
+    this.setUpApp(),
+    this.setUpXss(),
+    this.setUpSlacklNotification(),
+  ]);
+
   await Promise.all([
     this.scanRuntimeVersions(),
     this.setupPassport(),
@@ -84,7 +97,39 @@ Crowi.prototype.init = async function() {
     this.setupSlack(),
     this.setupCsrf(),
     this.setUpGlobalNotification(),
+    this.setUpFileUpload(),
+    this.setUpAcl(),
+    this.setUpCustomize(),
     this.setUpRestQiitaAPI(),
+    this.setupUserGroup(),
+  ]);
+};
+
+Crowi.prototype.initForTest = async function() {
+  await this.setupModels();
+  await this.setupConfigManager();
+
+  // // customizeService depends on AppService and XssService
+  // // passportService depends on appService
+  // // slack depends on setUpSlacklNotification
+  await Promise.all([
+    this.setUpApp(),
+    // this.setUpXss(),
+    // this.setUpSlacklNotification(),
+  ]);
+
+  await Promise.all([
+  //   this.scanRuntimeVersions(),
+  //   this.setupPassport(),
+  //   this.setupSearcher(),
+  //   this.setupMailer(),
+  //   this.setupSlack(),
+  //   this.setupCsrf(),
+  //   this.setUpGlobalNotification(),
+  //   this.setUpFileUpload(),
+    this.setUpAcl(),
+  //   this.setUpCustomize(),
+  //   this.setUpRestQiitaAPI(),
   ]);
 };
 
@@ -180,43 +225,23 @@ Crowi.prototype.setupSessionConfig = function() {
   }));
 };
 
-Crowi.prototype.setupAppConfig = function() {
-  return new Promise((resolve, reject) => {
-    this.model('Config', require('../models/config')(this));
-    const Config = this.model('Config');
-    Config.loadAllConfig((err, doc) => {
-      if (err) {
-        return reject();
-      }
-
-      this.setConfig(doc);
-
-      return resolve();
-    });
-  });
-};
-
 Crowi.prototype.setupConfigManager = async function() {
   const ConfigManager = require('../service/config-manager');
   this.configManager = new ConfigManager(this.model('Config'));
   return this.configManager.loadConfigs();
 };
 
-Crowi.prototype.setupModels = function() {
-  const self = this;
-  return new Promise(((resolve, reject) => {
-    Object.keys(models).forEach((key) => {
-      self.model(key, models[key](self));
-    });
-    resolve();
-  }));
+Crowi.prototype.setupModels = async function() {
+  Object.keys(models).forEach((key) => {
+    return this.model(key, models[key](this));
+  });
 };
 
 Crowi.prototype.getIo = function() {
   return this.io;
 };
 
-Crowi.prototype.scanRuntimeVersions = function() {
+Crowi.prototype.scanRuntimeVersions = async function() {
   const self = this;
 
   const check = require('check-node-version');
@@ -251,15 +276,7 @@ Crowi.prototype.getRestQiitaAPIService = function() {
   return this.restQiitaAPIService;
 };
 
-Crowi.prototype.setupPassport = function() {
-  const config = this.getConfig();
-  const Config = this.model('Config');
-
-  if (!Config.isEnabledPassport(config)) {
-    // disabled
-    return;
-  }
-
+Crowi.prototype.setupPassport = async function() {
   debug('Passport is enabled');
 
   // initialize service
@@ -277,6 +294,7 @@ Crowi.prototype.setupPassport = function() {
     this.passportService.setupTwitterStrategy();
     this.passportService.setupOidcStrategy();
     this.passportService.setupSamlStrategy();
+    this.passportService.setupBasicStrategy();
   }
   catch (err) {
     logger.error(err);
@@ -284,7 +302,7 @@ Crowi.prototype.setupPassport = function() {
   return Promise.resolve();
 };
 
-Crowi.prototype.setupSearcher = function() {
+Crowi.prototype.setupSearcher = async function() {
   const self = this;
   const searcherUri = this.env.ELASTICSEARCH_URI
     || this.env.BONSAI_URL
@@ -303,7 +321,7 @@ Crowi.prototype.setupSearcher = function() {
   }));
 };
 
-Crowi.prototype.setupMailer = function() {
+Crowi.prototype.setupMailer = async function() {
   const self = this;
   return new Promise(((resolve, reject) => {
     self.mailer = require('../util/mailer')(self);
@@ -311,13 +329,11 @@ Crowi.prototype.setupMailer = function() {
   }));
 };
 
-Crowi.prototype.setupSlack = function() {
+Crowi.prototype.setupSlack = async function() {
   const self = this;
-  const config = this.getConfig();
-  const Config = this.model('Config');
 
   return new Promise(((resolve, reject) => {
-    if (Config.hasSlackConfig(config)) {
+    if (this.slackNotificationService.hasSlackConfig()) {
       self.slack = require('../util/slack')(self);
     }
 
@@ -325,7 +341,7 @@ Crowi.prototype.setupSlack = function() {
   }));
 };
 
-Crowi.prototype.setupCsrf = function() {
+Crowi.prototype.setupCsrf = async function() {
   const Tokens = require('csrf');
   this.tokens = new Tokens();
 
@@ -376,8 +392,7 @@ Crowi.prototype.buildServer = function() {
   require('./express-init')(this, express);
 
   // import plugins
-  const Config = this.model('Config');
-  const isEnabledPlugins = Config.isEnabledPlugins(this.config);
+  const isEnabledPlugins = this.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
   if (isEnabledPlugins) {
     debug('Plugins are enabled');
     const PluginService = require('../plugins/plugin.service');
@@ -430,21 +445,90 @@ Crowi.prototype.require = function(modulePath) {
 /**
  * setup GlobalNotificationService
  */
-Crowi.prototype.setUpGlobalNotification = function() {
+Crowi.prototype.setUpGlobalNotification = async function() {
   const GlobalNotificationService = require('../service/global-notification');
   if (this.globalNotificationService == null) {
     this.globalNotificationService = new GlobalNotificationService(this);
   }
 };
 
+/**
+ * setup SlackNotificationService
+ */
+Crowi.prototype.setUpSlacklNotification = async function() {
+  const SlackNotificationService = require('../service/slack-notification');
+  if (this.slackNotificationService == null) {
+    this.slackNotificationService = new SlackNotificationService(this.configManager);
+  }
+};
+
+/**
+ * setup XssService
+ */
+Crowi.prototype.setUpXss = async function() {
+  const XssService = require('../service/xss');
+  if (this.xssService == null) {
+    this.xssService = new XssService(this.configManager);
+  }
+};
+
+/**
+ * setup AclService
+ */
+Crowi.prototype.setUpAcl = async function() {
+  const AclService = require('../service/acl');
+  if (this.aclService == null) {
+    this.aclService = new AclService(this.configManager);
+  }
+};
+
+/**
+ * setup CustomizeService
+ */
+Crowi.prototype.setUpCustomize = async function() {
+  const CustomizeService = require('../service/customize');
+  if (this.customizeService == null) {
+    this.customizeService = new CustomizeService(this.configManager, this.appService, this.xssService);
+    this.customizeService.initCustomCss();
+    this.customizeService.initCustomTitle();
+  }
+};
+
+/**
+ * setup AppService
+ */
+Crowi.prototype.setUpApp = async function() {
+  const AppService = require('../service/app');
+  if (this.appService == null) {
+    this.appService = new AppService(this.configManager);
+  }
+};
+
+/**
+ * setup FileUploadService
+ */
+Crowi.prototype.setUpFileUpload = async function() {
+  if (this.fileUploadService == null) {
+    this.fileUploadService = require('../service/file-uploader')(this);
+  }
+};
+
 /**
  * setup RestQiitaAPIService
  */
-Crowi.prototype.setUpRestQiitaAPI = function() {
+Crowi.prototype.setUpRestQiitaAPI = async function() {
   const RestQiitaAPIService = require('../service/rest-qiita-API');
   if (this.restQiitaAPIService == null) {
     this.restQiitaAPIService = new RestQiitaAPIService(this);
   }
 };
 
+Crowi.prototype.setupUserGroup = async function() {
+  const UserGroupService = require('../service/user-group');
+  if (this.userGroupService == null) {
+    this.userGroupService = new UserGroupService(this);
+    return this.userGroupService.init();
+  }
+};
+
 module.exports = Crowi;

+ 1 - 0
src/server/form/admin/customfeatures.js

@@ -4,6 +4,7 @@ const field = form.field;
 
 module.exports = form(
   field('settingForm[customize:isEnabledTimeline]').trim().toBooleanStrict(),
+  field('settingForm[customize:isEnabledDeleteCompletely]').trim().toBooleanStrict(),
   field('settingForm[customize:isSavedStatesOfTabChanges]').trim().toBooleanStrict(),
   field('settingForm[customize:isEnabledAttachTitleHeader]').trim().toBooleanStrict(),
   field('settingForm[customize:showRecentCreatedNumber]').trim().toInt(),

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

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

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

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[google:clientId]').trim().is(/^[\da-z\-.]+$/),
-  field('settingForm[google:clientSecret]').trim().is(/^[\da-zA-Z\-_]+$/),
-);

+ 0 - 7
src/server/form/admin/securityMechanism.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[security:isEnabledPassport]').trim().toBooleanStrict(),
-);

+ 9 - 0
src/server/form/admin/securityPassportBasic.js

@@ -0,0 +1,9 @@
+const form = require('express-form');
+
+const field = form.field;
+
+module.exports = form(
+  field('settingForm[security:passport-basic:isEnabled]').trim().toBooleanStrict().required(),
+  field('settingForm[security:passport-basic:id]').trim(),
+  field('settingForm[security:passport-basic:password]').trim(),
+);

+ 1 - 2
src/server/form/index.js

@@ -19,10 +19,9 @@ module.exports = {
     importerQiita: require('./admin/importerQiita'),
     plugin: require('./admin/plugin'),
     securityGeneral: require('./admin/securityGeneral'),
-    securityGoogle: require('./admin/securityGoogle'),
-    securityMechanism: require('./admin/securityMechanism'),
     securityPassportLdap: require('./admin/securityPassportLdap'),
     securityPassportSaml: require('./admin/securityPassportSaml'),
+    securityPassportBasic: require('./admin/securityPassportBasic'),
     securityPassportGoogle: require('./admin/securityPassportGoogle'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),

+ 0 - 2
src/server/form/register.js

@@ -7,7 +7,5 @@ module.exports = form(
   field('registerForm.name').required(),
   field('registerForm.email').required(),
   field('registerForm.password').required().is(/^[\x20-\x7F]{6,}$/),
-  field('registerForm.googleId'),
-  field('registerForm.googleImage'),
   field('registerForm[app:globalLang]'),
 );

+ 2 - 2
src/server/models/attachment.js

@@ -11,8 +11,6 @@ const mongoose = require('mongoose');
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 module.exports = function(crowi) {
-  const fileUploader = require('../service/file-uploader')(crowi);
-
   function generateFileHash(fileName) {
     const hash = require('crypto').createHash('md5');
     hash.update(`${fileName}_${Date.now()}`);
@@ -44,6 +42,7 @@ module.exports = function(crowi) {
 
 
   attachmentSchema.statics.create = async function(pageId, user, fileStream, originalName, fileFormat, fileSize) {
+    const fileUploader = require('../service/file-uploader')(crowi);
     const Attachment = this;
 
     const extname = path.extname(originalName);
@@ -94,6 +93,7 @@ module.exports = function(crowi) {
   };
 
   attachmentSchema.statics.removeWithSubstanceById = async function(id) {
+    const fileUploader = require('../service/file-uploader')(crowi);
     // retrieve data from DB to get a completely populated instance
     const attachment = await this.findById(id);
     await fileUploader.deleteFile(attachment);

+ 59 - 539
src/server/models/config.js

@@ -5,17 +5,6 @@
 
 module.exports = function(crowi) {
   const mongoose = require('mongoose');
-  const debug = require('debug')('growi:models:config');
-  const uglifycss = require('uglifycss');
-  const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
-
-  const SECURITY_RESTRICT_GUEST_MODE_DENY = 'Deny';
-  const SECURITY_RESTRICT_GUEST_MODE_READONLY = 'Readonly';
-  const SECURITY_REGISTRATION_MODE_OPEN = 'Open';
-  const SECURITY_REGISTRATION_MODE_RESTRICTED = 'Resricted';
-  const SECURITY_REGISTRATION_MODE_CLOSED = 'Closed';
-
-  let Config;
 
   const configSchema = new mongoose.Schema({
     ns: { type: String, required: true, index: true },
@@ -23,21 +12,15 @@ module.exports = function(crowi) {
     value: { type: String, required: true },
   });
 
-  function validateCrowi() {
-    if (crowi == null) {
-      throw new Error('"crowi" is null. Init Config model with "crowi" argument first.');
-    }
-  }
-
   /**
    * default values when GROWI is cleanly installed
    */
-  function getArrayForInstalling() {
+  function getConfigsForInstalling() {
     const config = getDefaultCrowiConfigs();
 
     // overwrite
+    config['app:installed'] = true;
     config['app:fileUpload'] = true;
-    config['security:isEnabledPassport'] = true;
     config['customize:behavior'] = 'growi';
     config['customize:layout'] = 'growi';
     config['customize:isSavedStatesOfTabChanges'] = false;
@@ -51,8 +34,8 @@ module.exports = function(crowi) {
   function getDefaultCrowiConfigs() {
     /* eslint-disable key-spacing */
     return {
-      // 'app:installed'     : "0.0.0",
-      'app:confidential'  : '',
+      'app:installed'     : false,
+      'app:confidential'  : undefined,
 
       'app:fileUpload'    : false,
       'app:globalLang'    : 'en-US',
@@ -64,8 +47,8 @@ module.exports = function(crowi) {
 
       'security:list-policy:hideRestrictedByOwner' : false,
       'security:list-policy:hideRestrictedByGroup' : false,
+      'security:pageCompleteDeletionAuthority' : undefined,
 
-      'security:isEnabledPassport' : false,
       'security:passport-ldap:isEnabled' : false,
       'security:passport-ldap:serverUrl' : undefined,
       'security:passport-ldap:isUserBind' : undefined,
@@ -85,27 +68,28 @@ module.exports = function(crowi) {
       'security:passport-github:isEnabled' : false,
       'security:passport-twitter:isEnabled' : false,
       'security:passport-oidc:isEnabled' : false,
+      'security:passport-basic:isEnabled' : false,
 
       'aws:bucket'          : 'growi',
       'aws:region'          : 'ap-northeast-1',
-      'aws:accessKeyId'     : '',
-      'aws:secretAccessKey' : '',
+      'aws:accessKeyId'     : undefined,
+      'aws:secretAccessKey' : undefined,
 
-      'mail:from'         : '',
-      'mail:smtpHost'     : '',
-      'mail:smtpPort'     : '',
-      'mail:smtpUser'     : '',
-      'mail:smtpPassword' : '',
+      'mail:from'         : undefined,
+      'mail:smtpHost'     : undefined,
+      'mail:smtpPort'     : undefined,
+      'mail:smtpUser'     : undefined,
+      'mail:smtpPassword' : undefined,
 
-      'google:clientId'     : '',
-      'google:clientSecret' : '',
+      'google:clientId'     : undefined,
+      'google:clientSecret' : undefined,
 
       'plugin:isEnabledPlugins' : true,
 
-      'customize:css' : '',
-      'customize:script' : '',
-      'customize:header' : '',
-      'customize:title' : '',
+      'customize:css' : undefined,
+      'customize:script' : undefined,
+      'customize:header' : undefined,
+      'customize:title' : undefined,
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyleBorder' : false,
       'customize:theme' : 'default',
@@ -116,10 +100,10 @@ module.exports = function(crowi) {
       'customize:isEnabledAttachTitleHeader' : false,
       'customize:showRecentCreatedNumber' : 10,
 
-      'importer:esa:team_name': '',
-      'importer:esa:access_token': '',
-      'importer:qiita:team_name': '',
-      'importer:qiita:access_token': '',
+      'importer:esa:team_name': undefined,
+      'importer:esa:access_token': undefined,
+      'importer:qiita:team_name': undefined,
+      'importer:qiita:access_token': undefined,
     };
     /* eslint-enable key-spacing */
   }
@@ -133,27 +117,24 @@ module.exports = function(crowi) {
       'markdown:isEnabledLinebreaks': false,
       'markdown:isEnabledLinebreaksInComments': true,
       'markdown:presentation:pageBreakSeparator': 1,
-      'markdown:presentation:pageBreakCustomSeparator': '',
+      'markdown:presentation:pageBreakCustomSeparator': undefined,
     };
   }
 
-  function getValueForCrowiNS(config, key) {
-    // return the default value if undefined
-    if (undefined === config.crowi || undefined === config.crowi[key]) {
-      return getDefaultCrowiConfigs()[key];
-    }
-
-    return config.crowi[key];
+  function getDefaultNotificationConfigs() {
+    return {
+      'slack:isIncomingWebhookPrioritized': false,
+      'slack:incomingWebhookUrl': undefined,
+      'slack:token': undefined,
+    };
   }
 
-  function getValueForMarkdownNS(config, key) {
-    // return the default value if undefined
-    if (undefined === config.markdown || undefined === config.markdown[key]) {
-      return getDefaultMarkdownConfigs()[key];
-    }
-
-    return config.markdown[key];
-  }
+  /**
+   * It is deprecated to use this for anything other than AppService#isDBInitialized.
+   */
+  configSchema.statics.getConfigsObjectForInstalling = function() {
+    return getConfigsForInstalling();
+  };
 
   /**
    * It is deprecated to use this for anything other than ConfigLoader#load.
@@ -169,467 +150,36 @@ module.exports = function(crowi) {
     return getDefaultMarkdownConfigs();
   };
 
-  configSchema.statics.getRestrictGuestModeLabels = function() {
-    const labels = {};
-    labels[SECURITY_RESTRICT_GUEST_MODE_DENY] = 'security_setting.guest_mode.deny';
-    labels[SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_setting.guest_mode.readonly';
-
-    return labels;
-  };
-
-  configSchema.statics.getRegistrationModeLabels = function() {
-    const labels = {};
-    labels[SECURITY_REGISTRATION_MODE_OPEN] = 'security_setting.registration_mode.open';
-    labels[SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_setting.registration_mode.restricted';
-    labels[SECURITY_REGISTRATION_MODE_CLOSED] = 'security_setting.registration_mode.closed';
-
-    return labels;
-  };
-
-  configSchema.statics.updateConfigCache = function(ns, config) {
-    validateCrowi();
-
-    const originalConfig = crowi.getConfig();
-    const newNSConfig = originalConfig[ns] || {};
-    Object.keys(config).forEach((key) => {
-      if (config[key] || config[key] === '' || config[key] === false) {
-        newNSConfig[key] = config[key];
-      }
-    });
-
-    originalConfig[ns] = newNSConfig;
-    crowi.setConfig(originalConfig);
-
-    // initialize custom css/script
-    Config.initCustomCss(originalConfig);
-    Config.initCustomScript(originalConfig);
-  };
-
-  // Execute only once for installing application
-  configSchema.statics.applicationInstall = function(callback) {
-    const Config = this;
-    Config.count({ ns: 'crowi' }, (err, count) => {
-      if (count > 0) {
-        return callback(new Error('Application already installed'), null);
-      }
-      Config.updateNamespaceByArray('crowi', getArrayForInstalling(), (err, configs) => {
-        Config.updateConfigCache('crowi', configs);
-        return callback(err, configs);
-      });
-    });
-  };
-
-  configSchema.statics.setupConfigFormData = function(ns, config) {
-    let defaultConfig = {};
-
-    // set Default Settings
-    if (ns === 'crowi') {
-      defaultConfig = getDefaultCrowiConfigs();
-    }
-    else if (ns === 'markdown') {
-      defaultConfig = getDefaultMarkdownConfigs();
-    }
-
-    if (!defaultConfig[ns]) {
-      defaultConfig[ns] = {};
-    }
-    Object.keys(config[ns] || {}).forEach((key) => {
-      if (config[ns][key] !== undefined) {
-        defaultConfig[key] = config[ns][key];
-      }
-    });
-    return defaultConfig;
-  };
-
-
-  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback) {
-    const Config = this;
-    if (configs.length < 0) {
-      return callback(new Error('Argument #1 is not array.'), null);
-    }
-
-    Object.keys(configs).forEach((key) => {
-      const value = configs[key];
-
-      Config.findOneAndUpdate(
-        { ns, key },
-        { ns, key, value: JSON.stringify(value) },
-        { upsert: true },
-        (err, config) => {
-          debug('Config.findAndUpdate', err, config);
-        },
-      );
-    });
-
-    return callback(null, configs);
-  };
-
-  configSchema.statics.findOneAndUpdateByNsAndKey = async function(ns, key, value) {
-    return this.findOneAndUpdate(
-      { ns, key },
-      { ns, key, value: JSON.stringify(value) },
-      { upsert: true },
-    );
-  };
-
-  configSchema.statics.getConfig = function(callback) {
-  };
-
-  configSchema.statics.loadAllConfig = function(callback) {
-    const Config = this;
-
-
-    const config = {};
-    config.crowi = {}; // crowi namespace
-
-    Config.find()
-      .sort({ ns: 1, key: 1 })
-      .exec((err, doc) => {
-        doc.forEach((el) => {
-          if (!config[el.ns]) {
-            config[el.ns] = {};
-          }
-          config[el.ns][el.key] = JSON.parse(el.value);
-        });
-
-        debug('Config loaded', config);
-
-        // initialize custom css/script
-        Config.initCustomCss(config);
-        Config.initCustomScript(config);
-
-        return callback(null, config);
-      });
-  };
-
-  configSchema.statics.appTitle = function(config) {
-    const key = 'app:title';
-    return getValueForCrowiNS(config, key) || 'GROWI';
-  };
-
-  configSchema.statics.globalLang = function(config) {
-    const key = 'app:globalLang';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassport = function(config) {
-    // always true if growi installed cleanly
-    if (Object.keys(config.crowi).length === 0) {
-      return true;
-    }
-
-    const key = 'security:isEnabledPassport';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassportLdap = function(config) {
-    const key = 'security:passport-ldap:isEnabled';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassportGoogle = function(config) {
-    const key = 'security:passport-google:isEnabled';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassportGitHub = function(config) {
-    const key = 'security:passport-github:isEnabled';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassportTwitter = function(config) {
-    const key = 'security:passport-twitter:isEnabled';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPassportOidc = function(config) {
-    const key = 'security:passport-oidc:isEnabled';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isUploadable = function(config) {
-    const method = process.env.FILE_UPLOAD || 'aws';
-
-    if (method === 'aws' && (
-      !config.crowi['aws:accessKeyId']
-        || !config.crowi['aws:secretAccessKey']
-        || !config.crowi['aws:region']
-        || !config.crowi['aws:bucket'])) {
-      return false;
-    }
-
-    return method !== 'none';
-  };
-
-  configSchema.statics.isGuestAllowedToRead = function(config) {
-    // return true if puclic wiki mode
-    if (Config.isPublicWikiOnly(config)) {
-      return true;
-    }
-
-    // return false if undefined
-    if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
-      return false;
-    }
-
-    return SECURITY_RESTRICT_GUEST_MODE_READONLY === config.crowi['security:restrictGuestMode'];
-  };
-
-  configSchema.statics.hidePagesRestrictedByOwnerInList = function(config) {
-    const key = 'security:list-policy:hideRestrictedByOwner';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.hidePagesRestrictedByGroupInList = function(config) {
-    const key = 'security:list-policy:hideRestrictedByGroup';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledPlugins = function(config) {
-    const key = 'plugin:isEnabledPlugins';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledLinebreaks = function(config) {
-    const key = 'markdown:isEnabledLinebreaks';
-    return getValueForMarkdownNS(config, key);
-  };
-
-  configSchema.statics.isEnabledLinebreaksInComments = function(config) {
-    const key = 'markdown:isEnabledLinebreaksInComments';
-    return getValueForMarkdownNS(config, key);
-  };
-  configSchema.statics.isPublicWikiOnly = function(config) {
-    const publicWikiOnly = process.env.PUBLIC_WIKI_ONLY;
-    if (publicWikiOnly === 'true' || publicWikiOnly === 1) {
-      return true;
-    }
-    return false;
-  };
-
-  configSchema.statics.pageBreakSeparator = function(config) {
-    const key = 'markdown:presentation:pageBreakSeparator';
-    return getValueForMarkdownNS(config, key);
-  };
-
-  configSchema.statics.pageBreakCustomSeparator = function(config) {
-    const key = 'markdown:presentation:pageBreakCustomSeparator';
-    return getValueForMarkdownNS(config, key);
-  };
-
-  configSchema.statics.isEnabledXssPrevention = function(config) {
-    const key = 'markdown:xss:isEnabledPrevention';
-    return getValueForMarkdownNS(config, key);
-  };
-
-  configSchema.statics.xssOption = function(config) {
-    const key = 'markdown:xss:option';
-    return getValueForMarkdownNS(config, key);
-  };
-
-  configSchema.statics.tagWhiteList = function(config) {
-    const key = 'markdown:xss:tagWhiteList';
-
-    if (this.isEnabledXssPrevention(config)) {
-      switch (this.xssOption(config)) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return recommendedWhitelist.tags;
-
-        case 3: // custom white list
-          return config.markdown[key];
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  };
-
-  configSchema.statics.attrWhiteList = function(config) {
-    const key = 'markdown:xss:attrWhiteList';
-
-    if (this.isEnabledXssPrevention(config)) {
-      switch (this.xssOption(config)) {
-        case 1: // ignore all: use default option
-          return [];
-
-        case 2: // recommended
-          return recommendedWhitelist.attrs;
-
-        case 3: // custom white list
-          return config.markdown[key];
-
-        default:
-          return [];
-      }
-    }
-    else {
-      return [];
-    }
-  };
-
   /**
-   * initialize custom css strings
-   */
-  configSchema.statics.initCustomCss = function(config) {
-    const key = 'customize:css';
-    const rawCss = getValueForCrowiNS(config, key);
-    // uglify and store
-    this._customCss = uglifycss.processString(rawCss);
-  };
-
-  configSchema.statics.customCss = function(config) {
-    return this._customCss;
-  };
-
-  configSchema.statics.initCustomScript = function(config) {
-    const key = 'customize:script';
-    const rawScript = getValueForCrowiNS(config, key);
-    // store as is
-    this._customScript = rawScript;
-  };
-
-  configSchema.statics.customScript = function(config) {
-    return this._customScript;
-  };
-
-  configSchema.statics.customHeader = function(config) {
-    const key = 'customize:header';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.theme = function(config) {
-    const key = 'customize:theme';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.customTitle = function(config, page) {
-    validateCrowi();
-
-    const key = 'customize:title';
-    let customTitle = getValueForCrowiNS(config, key);
-
-    if (customTitle == null || customTitle.trim().length === 0) {
-      customTitle = '{{page}} - {{sitename}}';
-    }
-
-    // replace
-    customTitle = customTitle
-      .replace('{{sitename}}', this.appTitle(config))
-      .replace('{{page}}', page);
-
-    return crowi.xss.process(customTitle);
-  };
-
-  configSchema.statics.behaviorType = function(config) {
-    const key = 'customize:behavior';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.layoutType = function(config) {
-    const key = 'customize:layout';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.highlightJsStyle = function(config) {
-    const key = 'customize:highlightJsStyle';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.highlightJsStyleBorder = function(config) {
-    const key = 'customize:highlightJsStyleBorder';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledTimeline = function(config) {
-    const key = 'customize:isEnabledTimeline';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isSavedStatesOfTabChanges = function(config) {
-    const key = 'customize:isSavedStatesOfTabChanges';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.isEnabledAttachTitleHeader = function(config) {
-    const key = 'customize:isEnabledAttachTitleHeader';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.showRecentCreatedNumber = function(config) {
-    const key = 'customize:showRecentCreatedNumber';
-    return getValueForCrowiNS(config, key);
-  };
-
-  configSchema.statics.fileUploadEnabled = function(config) {
-    const Config = this;
-
-    if (!Config.isUploadable(config)) {
-      return false;
-    }
-
-    // convert to boolean
-    return !!config.crowi['app:fileUpload'];
-  };
-
-  configSchema.statics.hasSlackConfig = function(config) {
-    return Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config);
-  };
-
-  /**
-   * for Slack Incoming Webhooks
+   * It is deprecated to use this for anything other than ConfigLoader#load.
    */
-  configSchema.statics.hasSlackIwhUrl = function(config) {
-    if (!config.notification) {
-      return false;
-    }
-    return (!!config.notification['slack:incomingWebhookUrl']);
-  };
-
-  configSchema.statics.isIncomingWebhookPrioritized = function(config) {
-    if (!config.notification) {
-      return false;
-    }
-    return (!!config.notification['slack:isIncomingWebhookPrioritized']);
+  configSchema.statics.getDefaultNotificationConfigsObject = function() {
+    return getDefaultNotificationConfigs();
   };
 
-  configSchema.statics.hasSlackToken = function(config) {
-    if (!config.notification) {
-      return false;
-    }
-
-    return (!!config.notification['slack:token']);
-  };
-
-  configSchema.statics.getLocalconfig = function(config) {
-    const Config = this;
+  configSchema.statics.getLocalconfig = function() {
     const env = process.env;
 
     const localConfig = {
       crowi: {
-        title: Config.appTitle(crowi),
-        url: crowi.configManager.getSiteUrl(),
+        title: crowi.appService.getAppTitle(),
+        url: crowi.appService.getSiteUrl(),
       },
       upload: {
-        image: Config.isUploadable(config),
-        file: Config.fileUploadEnabled(config),
+        image: crowi.fileUploadService.getIsUploadable(),
+        file: crowi.fileUploadService.getFileUploadEnabled(),
       },
-      behaviorType: Config.behaviorType(config),
-      layoutType: Config.layoutType(config),
-      isEnabledLinebreaks: Config.isEnabledLinebreaks(config),
-      isEnabledLinebreaksInComments: Config.isEnabledLinebreaksInComments(config),
-      isEnabledXssPrevention: Config.isEnabledXssPrevention(config),
-      xssOption: Config.xssOption(config),
-      tagWhiteList: Config.tagWhiteList(config),
-      attrWhiteList: Config.attrWhiteList(config),
-      highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
-      isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
-      hasSlackConfig: Config.hasSlackConfig(config),
+      behaviorType: crowi.configManager.getConfig('crowi', 'customize:behavior'),
+      layoutType: crowi.configManager.getConfig('crowi', 'customize:layout'),
+      isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+      isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+      xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+      tagWhiteList: crowi.xssService.getTagWhiteList(),
+      attrWhiteList: crowi.xssService.getAttrWhiteList(),
+      highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+      isSavedStatesOfTabChanges: crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
+      hasSlackConfig: crowi.slackNotificationService.hasSlackConfig(),
       env: {
         PLANTUML_URI: env.PLANTUML_URI || null,
         BLOCKDIAG_URI: env.BLOCKDIAG_URI || null,
@@ -637,45 +187,15 @@ module.exports = function(crowi) {
         MATHJAX: env.MATHJAX || null,
         NO_CDN: env.NO_CDN || null,
       },
-      recentCreatedLimit: Config.showRecentCreatedNumber(config),
-      isAclEnabled: !Config.isPublicWikiOnly(config),
-      globalLang: Config.globalLang(config),
+      recentCreatedLimit: crowi.configManager.getConfig('crowi', 'customize:showRecentCreatedNumber'),
+      isAclEnabled: !crowi.aclService.getIsPublicWikiOnly(),
+      globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     };
 
     return localConfig;
   };
 
-  configSchema.statics.userUpperLimit = function(crowi) {
-    const key = 'USER_UPPER_LIMIT';
-    const env = crowi.env[key];
-
-    if (undefined === crowi.env || undefined === crowi.env[key]) {
-      return 0;
-    }
-    return Number(env);
-  };
-
-  /*
-  configSchema.statics.isInstalled = function(config)
-  {
-    if (!config.crowi) {
-      return false;
-    }
-
-    if (config.crowi['app:installed']
-       && config.crowi['app:installed'] !== '0.0.0') {
-      return true;
-    }
-
-    return false;
-  }
-  */
-
-  Config = mongoose.model('Config', configSchema);
-  Config.SECURITY_REGISTRATION_MODE_OPEN = SECURITY_REGISTRATION_MODE_OPEN;
-  Config.SECURITY_REGISTRATION_MODE_RESTRICTED = SECURITY_REGISTRATION_MODE_RESTRICTED;
-  Config.SECURITY_REGISTRATION_MODE_CLOSED = SECURITY_REGISTRATION_MODE_CLOSED;
-
+  const Config = mongoose.model('Config', configSchema);
 
   return Config;
 };

+ 1 - 0
src/server/models/index.js

@@ -1,4 +1,5 @@
 module.exports = {
+  Config: require('./config'),
   Page: require('./page'),
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),

+ 2 - 5
src/server/models/page.js

@@ -807,12 +807,9 @@ module.exports = function(crowi) {
   async function addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink) {
     validateCrowi();
 
-    const Config = crowi.model('Config');
-    const config = crowi.getConfig();
-
     // determine User condition
-    const hidePagesRestrictedByOwner = Config.hidePagesRestrictedByOwnerInList(config);
-    const hidePagesRestrictedByGroup = Config.hidePagesRestrictedByGroupInList(config);
+    const hidePagesRestrictedByOwner = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hidePagesRestrictedByGroupInList');
 
     // determine UserGroup condition
     let userGroups = null;

+ 0 - 5
src/server/models/user-group-relation.js

@@ -41,10 +41,6 @@ class UserGroupRelation {
     return this._crowi;
   }
 
-  static init() {
-    this.removeAllInvalidRelations();
-  }
-
   /**
    * remove all invalid relations that has reference to unlinked document
    */
@@ -288,6 +284,5 @@ module.exports = function(crowi) {
   UserGroupRelation.crowi = crowi;
   schema.loadClass(UserGroupRelation);
   const model = mongoose.model('UserGroupRelation', schema);
-  model.init();
   return model;
 };

+ 39 - 24
src/server/models/user.js

@@ -78,21 +78,20 @@ module.exports = function(crowi) {
   function decideUserStatusOnRegistration() {
     validateCrowi();
 
-    const Config = crowi.model('Config');
+    const { configManager, aclService } = crowi;
 
-
-    const config = crowi.getConfig();
-
-    if (!config.crowi) {
+    const isInstalled = configManager.getConfig('crowi', 'app:installed');
+    if (!isInstalled) {
       return STATUS_ACTIVE; // is this ok?
     }
 
     // status decided depends on registrationMode
-    switch (config.crowi['security:registrationMode']) {
-      case Config.SECURITY_REGISTRATION_MODE_OPEN:
+    const registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
+    switch (registrationMode) {
+      case aclService.labels.SECURITY_REGISTRATION_MODE_OPEN:
         return STATUS_ACTIVE;
-      case Config.SECURITY_REGISTRATION_MODE_RESTRICTED:
-      case Config.SECURITY_REGISTRATION_MODE_CLOSED: // 一応
+      case aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED:
+      case aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED: // 一応
         return STATUS_REGISTERED;
       default:
         return STATUS_ACTIVE; // どっちにすんのがいいんだろうな
@@ -199,6 +198,21 @@ module.exports = function(crowi) {
     });
   };
 
+  userSchema.methods.canDeleteCompletely = function(creatorId) {
+    const pageCompleteDeletionAuthority = crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
+    if (this.admin) {
+      return true;
+    }
+    if (pageCompleteDeletionAuthority === 'anyOne' || pageCompleteDeletionAuthority == null) {
+      return true;
+    }
+    if (pageCompleteDeletionAuthority === 'adminAndAuthor') {
+      return (this._id.equals(creatorId));
+    }
+
+    return false;
+  };
+
   userSchema.methods.updateApiToken = function(callback) {
     const self = this;
 
@@ -348,11 +362,10 @@ module.exports = function(crowi) {
   userSchema.statics.isEmailValid = function(email, callback) {
     validateCrowi();
 
-    const config = crowi.getConfig();
-    const whitelist = config.crowi['security:registrationWhiteList'];
+    const whitelist = crowi.configManager.getConfig('crowi', 'security:registrationWhiteList');
 
     if (Array.isArray(whitelist) && whitelist.length > 0) {
-      return config.crowi['security:registrationWhiteList'].some((allowedEmail) => {
+      return whitelist.some((allowedEmail) => {
         const re = new RegExp(`${allowedEmail}$`);
         return re.test(email);
       });
@@ -511,8 +524,9 @@ module.exports = function(crowi) {
   };
 
   userSchema.statics.isUserCountExceedsUpperLimit = async function() {
-    const Config = crowi.model('Config');
-    const userUpperLimit = Config.userUpperLimit(crowi);
+    const { aclService } = crowi;
+
+    const userUpperLimit = aclService.userUpperLimit();
     if (userUpperLimit === 0) {
       return false;
     }
@@ -620,12 +634,12 @@ module.exports = function(crowi) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
     validateCrowi();
 
+    const configManager = crowi.configManager;
+
     const User = this;
     const createdUserList = [];
-    const Config = crowi.model('Config');
-    const config = crowi.getConfig();
-
     const mailer = crowi.getMailer();
+
     if (!Array.isArray(emailList)) {
       debug('emailList is not array');
     }
@@ -665,7 +679,7 @@ module.exports = function(crowi) {
           newUser.createdAt = Date.now();
           newUser.status = STATUS_INVITED;
 
-          const globalLang = Config.globalLang(config);
+          const globalLang = configManager.getConfig('crowi', 'app:globalLang');
           if (globalLang != null) {
             newUser.lang = globalLang;
           }
@@ -706,15 +720,17 @@ module.exports = function(crowi) {
                 return next();
               }
 
+              const appTitle = crowi.appService.getAppTitle();
+
               mailer.send({
                 to: user.email,
-                subject: `Invitation to ${Config.appTitle(config)}`,
+                subject: `Invitation to ${appTitle}`,
                 template: path.join(crowi.localeDir, 'en-US/admin/userInvitation.txt'),
                 vars: {
                   email: user.email,
                   password: user.password,
-                  url: crowi.configManager.getSiteUrl(),
-                  appTitle: Config.appTitle(config),
+                  url: crowi.appService.getSiteUrl(),
+                  appTitle,
                 },
               },
               (err, s) => {
@@ -759,9 +775,8 @@ module.exports = function(crowi) {
       newUser.setPassword(password);
     }
 
-    const Config = crowi.model('Config');
-    const config = crowi.getConfig();
-    const globalLang = Config.globalLang(config);
+    const configManager = crowi.configManager;
+    const globalLang = configManager.getConfig('crowi', 'app:globalLang');
     if (globalLang != null) {
       newUser.lang = globalLang;
     }

+ 9 - 3
src/server/plugins/plugin.service.js

@@ -34,15 +34,21 @@ class PluginService {
     switch (meta.pluginSchemaVersion) {
       // v1 is deprecated
       case 1:
-        logger.warn('pluginSchemaVersion 1 is deprecated');
+        logger.warn('pluginSchemaVersion 1 is deprecated', definition);
         break;
-      // v2 or above
-      default:
+      // v2 is deprecated
+      case 2:
+        logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+        break;
+      case 3:
         logger.info(`load plugin '${definition.name}'`);
         definition.entries.forEach((entryPath) => {
           const entry = require(entryPath);
           entry(this.crowi, this.app);
         });
+        break;
+      default:
+        logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
     }
   }
 

+ 128 - 174
src/server/routes/admin.js

@@ -9,11 +9,17 @@ module.exports = function(crowi, app) {
   const ExternalAccount = models.ExternalAccount;
   const UserGroup = models.UserGroup;
   const UserGroupRelation = models.UserGroupRelation;
-  const Config = models.Config;
   const GlobalNotificationSetting = models.GlobalNotificationSetting;
   const GlobalNotificationMailSetting = models.GlobalNotificationMailSetting;
   const GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting; // eslint-disable-line no-unused-vars
 
+  const {
+    configManager,
+    aclService,
+    slackNotificationService,
+    customizeService,
+  } = crowi;
+
   const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
   const PluginUtils = require('../plugins/plugin-utils');
   const ApiResponse = require('../util/apiResponse');
@@ -90,11 +96,7 @@ module.exports = function(crowi, app) {
   // app.get('/admin/app'                  , admin.app.index);
   actions.app = {};
   actions.app.index = function(req, res) {
-    const settingForm = Config.setupConfigFormData('crowi', req.config);
-
-    return res.render('admin/app', {
-      settingForm,
-    });
+    return res.render('admin/app');
   };
 
   actions.app.settingUpdate = function(req, res) {
@@ -103,16 +105,13 @@ module.exports = function(crowi, app) {
   // app.get('/admin/security'                  , admin.security.index);
   actions.security = {};
   actions.security.index = function(req, res) {
-    const settingForm = Config.setupConfigFormData('crowi', req.config);
-    const isAclEnabled = !Config.isPublicWikiOnly(req.config);
-    return res.render('admin/security', { settingForm, isAclEnabled });
+    return res.render('admin/security');
   };
 
   // app.get('/admin/markdown'                  , admin.markdown.index);
   actions.markdown = {};
   actions.markdown.index = function(req, res) {
-    const config = crowi.getConfig();
-    const markdownSetting = Config.setupConfigFormData('markdown', config);
+    const markdownSetting = configManager.getConfigByPrefix('markdown', 'markdown:');
 
     return res.render('admin/markdown', {
       markdownSetting,
@@ -121,66 +120,54 @@ module.exports = function(crowi, app) {
   };
 
   // app.post('/admin/markdown/lineBreaksSetting' , admin.markdown.lineBreaksSetting);
-  actions.markdown.lineBreaksSetting = function(req, res) {
+  actions.markdown.lineBreaksSetting = async function(req, res) {
     const markdownSetting = req.form.markdownSetting;
 
-    req.session.markdownSetting = markdownSetting;
     if (req.form.isValid) {
-      Config.updateNamespaceByArray('markdown', markdownSetting, (err, config) => {
-        Config.updateConfigCache('markdown', config);
-        req.session.markdownSetting = null;
-        req.flash('successMessage', ['Successfully updated!']);
-        return res.redirect('/admin/markdown');
-      });
+      await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
+      req.flash('successMessage', ['Successfully updated!']);
     }
     else {
       req.flash('errorMessage', req.form.errors);
-      return res.redirect('/admin/markdown');
     }
+
+    return res.redirect('/admin/markdown');
   };
 
   // app.post('/admin/markdown/presentationSetting' , admin.markdown.presentationSetting);
-  actions.markdown.presentationSetting = function(req, res) {
-    const presentationSetting = req.form.markdownSetting;
+  actions.markdown.presentationSetting = async function(req, res) {
+    const markdownSetting = req.form.markdownSetting;
 
-    req.session.markdownSetting = presentationSetting;
     if (req.form.isValid) {
-      Config.updateNamespaceByArray('markdown', presentationSetting, (err, config) => {
-        Config.updateConfigCache('markdown', config);
-        req.session.markdownSetting = null;
-        req.flash('successMessage', ['Successfully updated!']);
-        return res.redirect('/admin/markdown');
-      });
+      await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
+      req.flash('successMessage', ['Successfully updated!']);
     }
     else {
       req.flash('errorMessage', req.form.errors);
-      return res.redirect('/admin/markdown');
     }
+
+    return res.redirect('/admin/markdown');
   };
 
   // app.post('/admin/markdown/xss-setting' , admin.markdown.xssSetting);
-  actions.markdown.xssSetting = function(req, res) {
+  actions.markdown.xssSetting = async function(req, res) {
     const xssSetting = req.form.markdownSetting;
 
-    xssSetting['markdown:xss:tagWhiteList'] = stringToArray(xssSetting['markdown:xss:tagWhiteList']);
-    xssSetting['markdown:xss:attrWhiteList'] = stringToArray(xssSetting['markdown:xss:attrWhiteList']);
+    xssSetting['markdown:xss:tagWhiteList'] = csvToArray(xssSetting['markdown:xss:tagWhiteList']);
+    xssSetting['markdown:xss:attrWhiteList'] = csvToArray(xssSetting['markdown:xss:attrWhiteList']);
 
-    req.session.markdownSetting = xssSetting;
     if (req.form.isValid) {
-      Config.updateNamespaceByArray('markdown', xssSetting, (err, config) => {
-        Config.updateConfigCache('markdown', config);
-        req.session.xssSetting = null;
-        req.flash('successMessage', ['Successfully updated!']);
-        return res.redirect('/admin/markdown');
-      });
+      await configManager.updateConfigsInTheSameNamespace('markdown', xssSetting);
+      req.flash('successMessage', ['Successfully updated!']);
     }
     else {
       req.flash('errorMessage', req.form.errors);
-      return res.redirect('/admin/markdown');
     }
+
+    return res.redirect('/admin/markdown');
   };
 
-  const stringToArray = (string) => {
+  const csvToArray = (string) => {
     const array = string.split(',');
     return array.map((item) => { return item.trim() });
   };
@@ -188,7 +175,7 @@ module.exports = function(crowi, app) {
   // app.get('/admin/customize' , admin.customize.index);
   actions.customize = {};
   actions.customize.index = function(req, res) {
-    const settingForm = Config.setupConfigFormData('crowi', req.config);
+    const settingForm = configManager.getConfigByPrefix('crowi', 'customize:');
 
     /* eslint-disable quote-props, no-multi-spaces */
     const highlightJsCssSelectorOptions = {
@@ -214,13 +201,12 @@ module.exports = function(crowi, app) {
   // app.get('/admin/notification'               , admin.notification.index);
   actions.notification = {};
   actions.notification.index = async(req, res) => {
-    const config = crowi.getConfig();
     const UpdatePost = crowi.model('UpdatePost');
-    let slackSetting = Config.setupConfigFormData('notification', config);
-    const hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
-    const hasSlackToken = Config.hasSlackToken(config);
+    let slackSetting = configManager.getConfigByPrefix('notification', 'slack:');
+    const hasSlackIwhUrl = !!configManager.getConfig('notification', 'slack:incomingWebhookUrl');
+    const hasSlackToken = !!configManager.getConfig('notification', 'slack:token');
 
-    if (!Config.hasSlackIwhUrl(req.config)) {
+    if (!hasSlackIwhUrl) {
       slackSetting['slack:incomingWebhookUrl'] = '';
     }
 
@@ -242,51 +228,46 @@ module.exports = function(crowi, app) {
   };
 
   // app.post('/admin/notification/slackSetting' , admin.notification.slackauth);
-  actions.notification.slackSetting = function(req, res) {
+  actions.notification.slackSetting = async function(req, res) {
     const slackSetting = req.form.slackSetting;
 
-    req.session.slackSetting = slackSetting;
     if (req.form.isValid) {
-      Config.updateNamespaceByArray('notification', slackSetting, (err, config) => {
-        Config.updateConfigCache('notification', config);
-        req.flash('successMessage', ['Successfully Updated!']);
-        req.session.slackSetting = null;
-
-        // Re-setup
-        crowi.setupSlack().then(() => {
-          return res.redirect('/admin/notification');
-        });
+      await configManager.updateConfigsInTheSameNamespace('notification', slackSetting);
+      req.flash('successMessage', ['Successfully Updated!']);
+
+      // Re-setup
+      crowi.setupSlack().then(() => {
       });
     }
     else {
       req.flash('errorMessage', req.form.errors);
-      return res.redirect('/admin/notification');
     }
+
+    return res.redirect('/admin/notification');
   };
 
   // app.get('/admin/notification/slackAuth'     , admin.notification.slackauth);
   actions.notification.slackAuth = function(req, res) {
     const code = req.query.code;
 
-    if (!code || !Config.hasSlackConfig(req.config)) {
+    if (!code || !slackNotificationService.hasSlackConfig()) {
       return res.redirect('/admin/notification');
     }
 
     const slack = crowi.slack;
     slack.getOauthAccessToken(code)
-      .then((data) => {
+      .then(async(data) => {
         debug('oauth response', data);
-        Config.updateNamespaceByArray('notification', { 'slack:token': data.access_token }, (err, config) => {
-          if (err) {
-            req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
-          }
-          else {
-            Config.updateConfigCache('notification', config);
-            req.flash('successMessage', ['Successfully Connected!']);
-          }
 
-          return res.redirect('/admin/notification');
-        });
+        try {
+          await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': data.access_token });
+          req.flash('successMessage', ['Successfully Connected!']);
+        }
+        catch (err) {
+          req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
+        }
+
+        return res.redirect('/admin/notification');
       })
       .catch((err) => {
         debug('oauth response ERROR', err);
@@ -296,18 +277,16 @@ module.exports = function(crowi, app) {
   };
 
   // app.post('/admin/notification/slackIwhSetting' , admin.notification.slackIwhSetting);
-  actions.notification.slackIwhSetting = function(req, res) {
+  actions.notification.slackIwhSetting = async function(req, res) {
     const slackIwhSetting = req.form.slackIwhSetting;
 
     if (req.form.isValid) {
-      Config.updateNamespaceByArray('notification', slackIwhSetting, (err, config) => {
-        Config.updateConfigCache('notification', config);
-        req.flash('successMessage', ['Successfully Updated!']);
+      await configManager.updateConfigsInTheSameNamespace('notification', slackIwhSetting);
+      req.flash('successMessage', ['Successfully Updated!']);
 
-        // Re-setup
-        crowi.setupSlack().then(() => {
-          return res.redirect('/admin/notification#slack-incoming-webhooks');
-        });
+      // Re-setup
+      crowi.setupSlack().then(() => {
+        return res.redirect('/admin/notification#slack-incoming-webhooks');
       });
     }
     else {
@@ -317,13 +296,11 @@ module.exports = function(crowi, app) {
   };
 
   // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
-  actions.notification.disconnectFromSlack = function(req, res) {
-    Config.updateNamespaceByArray('notification', { 'slack:token': '' }, (err, config) => {
-      Config.updateConfigCache('notification', config);
-      req.flash('successMessage', ['Successfully Disconnected!']);
+  actions.notification.disconnectFromSlack = async function(req, res) {
+    await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': '' });
+    req.flash('successMessage', ['Successfully Disconnected!']);
 
-      return res.redirect('/admin/notification');
-    });
+    return res.redirect('/admin/notification');
   };
 
   actions.globalNotification = {};
@@ -430,8 +407,7 @@ module.exports = function(crowi, app) {
   actions.user = {};
   actions.user.index = async function(req, res) {
     const activeUsers = await User.countListByStatus(User.STATUS_ACTIVE);
-    const Config = crowi.model('Config');
-    const userUpperLimit = Config.userUpperLimit(crowi);
+    const userUpperLimit = aclService.userUpperLimit();
     const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
 
     const page = parseInt(req.query.page) || 1;
@@ -648,7 +624,7 @@ module.exports = function(crowi, app) {
   actions.userGroup = {};
   actions.userGroup.index = function(req, res) {
     const page = parseInt(req.query.page) || 1;
-    const isAclEnabled = !Config.isPublicWikiOnly(req.config);
+    const isAclEnabled = !aclService.getIsPublicWikiOnly();
     const renderVar = {
       userGroups: [],
       userGroupRelations: new Map(),
@@ -851,7 +827,7 @@ module.exports = function(crowi, app) {
   // Importer management
   actions.importer = {};
   actions.importer.index = function(req, res) {
-    const settingForm = Config.setupConfigFormData('crowi', req.config);
+    const settingForm = configManager.getConfigByPrefix('crowi', 'importer:');
 
     return res.render('admin/importer', {
       settingForm,
@@ -859,7 +835,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.api = {};
-  actions.api.appSetting = function(req, res) {
+  actions.api.appSetting = async function(req, res) {
     const form = req.form.settingForm;
 
     if (req.form.isValid) {
@@ -867,18 +843,20 @@ module.exports = function(crowi, app) {
 
       // mail setting ならここで validation
       if (form['mail:from']) {
-        validateMailSetting(req, form, (err, data) => {
+        validateMailSetting(req, form, async(err, data) => {
           debug('Error validate mail setting: ', err, data);
           if (err) {
             req.form.errors.push('SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。');
             return res.json({ status: false, message: req.form.errors.join('\n') });
           }
 
-          return saveSetting(req, res, form);
+          await configManager.updateConfigsInTheSameNamespace('crowi', form);
+          return res.json({ status: true });
         });
       }
       else {
-        return saveSetting(req, res, form);
+        await configManager.updateConfigsInTheSameNamespace('crowi', form);
+        return res.json({ status: true });
       }
     }
     else {
@@ -896,7 +874,7 @@ module.exports = function(crowi, app) {
     debug('form content', form);
 
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', form);
+      await configManager.updateConfigsInTheSameNamespace('crowi', form);
       return res.json({ status: true });
     }
     catch (err) {
@@ -911,15 +889,7 @@ module.exports = function(crowi, app) {
     }
 
     const form = req.form.settingForm;
-    const config = crowi.getConfig();
-    const isPublicWikiOnly = Config.isPublicWikiOnly(config);
-    if (isPublicWikiOnly) {
-      const basicName = form['security:basicName'];
-      const basicSecret = form['security:basicSecret'];
-      if (basicName !== '' || basicSecret !== '') {
-        req.form.errors.push('Public Wikiのため、Basic認証は利用できません。');
-        return res.json({ status: false, message: req.form.errors.join('\n') });
-      }
+    if (aclService.getIsPublicWikiOnly()) {
       const guestMode = form['security:restrictGuestMode'];
       if (guestMode === 'Deny') {
         req.form.errors.push('Private Wikiへの設定変更はできません。');
@@ -928,7 +898,7 @@ module.exports = function(crowi, app) {
     }
 
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', form);
+      await configManager.updateConfigsInTheSameNamespace('crowi', form);
       return res.json({ status: true });
     }
     catch (err) {
@@ -945,14 +915,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    return saveSettingAsync(form)
+    return configManager.updateConfigsInTheSameNamespace('crowi', form)
       .then(() => {
-        const config = crowi.getConfig();
-
         // reset strategy
         crowi.passportService.resetLdapStrategy();
         // setup strategy
-        if (Config.isEnabledPassportLdap(config)) {
+        if (configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')) {
           crowi.passportService.setupLdapStrategy(true);
         }
         return;
@@ -972,12 +940,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    await crowi.configManager.updateConfigsInTheSameNamespace('crowi', form);
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
 
     // reset strategy
     await crowi.passportService.resetSamlStrategy();
     // setup strategy
-    if (crowi.configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
+    if (configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
       try {
         await crowi.passportService.setupSamlStrategy(true);
       }
@@ -991,6 +959,33 @@ module.exports = function(crowi, app) {
     return res.json({ status: true });
   };
 
+  actions.api.securityPassportBasicSetting = async(req, res) => {
+    const form = req.form.settingForm;
+
+    if (!req.form.isValid) {
+      return res.json({ status: false, message: req.form.errors.join('\n') });
+    }
+
+    debug('form content', form);
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
+
+    // reset strategy
+    await crowi.passportService.resetBasicStrategy();
+    // setup strategy
+    if (configManager.getConfig('crowi', 'security:passport-basic:isEnabled')) {
+      try {
+        await crowi.passportService.setupBasicStrategy(true);
+      }
+      catch (err) {
+        // reset
+        await crowi.passportService.resetBasicStrategy();
+        return res.json({ status: false, message: err.message });
+      }
+    }
+
+    return res.json({ status: true });
+  };
+
   actions.api.securityPassportGoogleSetting = async(req, res) => {
     const form = req.form.settingForm;
 
@@ -999,13 +994,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    await saveSettingAsync(form);
-    const config = await crowi.getConfig();
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
 
     // reset strategy
     await crowi.passportService.resetGoogleStrategy();
     // setup strategy
-    if (Config.isEnabledPassportGoogle(config)) {
+    if (configManager.getConfig('crowi', 'security:passport-google:isEnabled')) {
       try {
         await crowi.passportService.setupGoogleStrategy(true);
       }
@@ -1027,13 +1021,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    await saveSettingAsync(form);
-    const config = await crowi.getConfig();
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
 
     // reset strategy
     await crowi.passportService.resetGitHubStrategy();
     // setup strategy
-    if (Config.isEnabledPassportGitHub(config)) {
+    if (configManager.getConfig('crowi', 'security:passport-github:isEnabled')) {
       try {
         await crowi.passportService.setupGitHubStrategy(true);
       }
@@ -1055,13 +1048,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    await saveSettingAsync(form);
-    const config = await crowi.getConfig();
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
 
     // reset strategy
     await crowi.passportService.resetTwitterStrategy();
     // setup strategy
-    if (Config.isEnabledPassportTwitter(config)) {
+    if (configManager.getConfig('crowi', 'security:passport-twitter:isEnabled')) {
       try {
         await crowi.passportService.setupTwitterStrategy(true);
       }
@@ -1083,14 +1075,12 @@ module.exports = function(crowi, app) {
     }
 
     debug('form content', form);
-    await saveSettingAsync(form);
-    const config = await crowi.getConfig();
-
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
 
     // reset strategy
     await crowi.passportService.resetOidcStrategy();
     // setup strategy
-    if (Config.isEnabledPassportOidc(config)) {
+    if (configManager.getConfig('crowi', 'security:passport-oidc:isEnabled')) {
       try {
         await crowi.passportService.setupOidcStrategy(true);
       }
@@ -1104,23 +1094,16 @@ module.exports = function(crowi, app) {
     return res.json({ status: true });
   };
 
-  actions.api.customizeSetting = function(req, res) {
+  actions.api.customizeSetting = async function(req, res) {
     const form = req.form.settingForm;
 
     if (req.form.isValid) {
       debug('form content', form);
-      return saveSetting(req, res, form);
-    }
+      await configManager.updateConfigsInTheSameNamespace('crowi', form);
+      customizeService.initCustomCss();
+      customizeService.initCustomTitle();
 
-    return res.json({ status: false, message: req.form.errors.join('\n') });
-  };
-
-  actions.api.customizeSetting = function(req, res) {
-    const form = req.form.settingForm;
-
-    if (req.form.isValid) {
-      debug('form content', form);
-      return saveSetting(req, res, form);
+      return res.json({ status: true });
     }
 
     return res.json({ status: false, message: req.form.errors.join('\n') });
@@ -1213,8 +1196,10 @@ module.exports = function(crowi, app) {
       return res.json({ status: false, message: req.form.errors.join('\n') });
     }
 
-    await saveSetting(req, res, form);
-    await importer.initializeEsaClient();
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
+    importer.initializeEsaClient(); // let it run in the back aftert res
+
+    return res.json({ status: true });
   };
 
   /**
@@ -1230,8 +1215,10 @@ module.exports = function(crowi, app) {
       return res.json({ status: false, message: req.form.errors.join('\n') });
     }
 
-    await saveSetting(req, res, form);
-    await importer.initializeQiitaClient();
+    await configManager.updateConfigsInTheSameNamespace('crowi', form);
+    importer.initializeQiitaClient(); // let it run in the back aftert res
+
+    return res.json({ status: true });
   };
 
   /**
@@ -1366,39 +1353,6 @@ module.exports = function(crowi, app) {
     }
   };
 
-  /**
-   * save settings, update config cache, and response json
-   *
-   * @param {any} req
-   * @param {any} res
-   * @param {any} form
-   */
-  function saveSetting(req, res, form) {
-    Config.updateNamespaceByArray('crowi', form, (err, config) => {
-      Config.updateConfigCache('crowi', config);
-      return res.json({ status: true });
-    });
-  }
-
-  /**
-   * save settings, update config cache ONLY. (this method don't response json)
-   *
-   * @param {any} form
-   * @returns
-   */
-  function saveSettingAsync(form) {
-    return new Promise((resolve, reject) => {
-      Config.updateNamespaceByArray('crowi', form, (err, config) => {
-        if (err) {
-          return reject(err);
-        }
-
-        Config.updateConfigCache('crowi', config);
-        return resolve();
-      });
-    });
-  }
-
   function validateMailSetting(req, form, callback) {
     const mailer = crowi.mailer;
     const option = {
@@ -1435,7 +1389,7 @@ module.exports = function(crowi, app) {
   function validateSamlSettingForm(form, t) {
     for (const key of crowi.passportService.mandatoryConfigKeysForSaml) {
       const formValue = form.settingForm[key];
-      if (crowi.configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
+      if (configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
         const formItemName = t(`security_setting.form_item_name.${key}`);
         form.errors.push(t('form_validation.required', formItemName));
       }

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

@@ -40,7 +40,7 @@ module.exports = function(crowi, app) {
       agentScriptContentTpl = swig.compileFile(agentScriptPath);
     }
 
-    const origin = crowi.configManager.getSiteUrl();
+    const origin = crowi.appService.getSiteUrl();
 
     // generate definitions to replace
     const definitions = {

+ 159 - 171
src/server/routes/index.js

@@ -4,7 +4,7 @@ const autoReap = require('multer-autoreap');
 autoReap.options.reapOnError = true; // continue reaping the file even if an error occurs
 
 module.exports = function(crowi, app) {
-  const middleware = require('../util/middlewares');
+  const middlewares = require('../util/middlewares')(crowi, app);
   const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
   const form = require('../form');
   const page = require('./page')(crowi, app);
@@ -22,66 +22,57 @@ module.exports = function(crowi, app) {
   const revision = require('./revision')(crowi, app);
   const search = require('./search')(crowi, app);
   const hackmd = require('./hackmd')(crowi, app);
-  const loginRequired = middleware.loginRequired;
-  const accessTokenParser = middleware.accessTokenParser(crowi, app);
-  const csrf = middleware.csrfVerify(crowi, app);
-  const config = crowi.getConfig();
-  const Config = crowi.model('Config');
+  const {
+    loginRequired,
+    adminRequired,
+    accessTokenParser,
+    csrfVerify: csrf,
+  } = middlewares;
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
-  app.get('/'                        , middleware.applicationInstalled(), loginRequired(crowi, app, false) , page.showTopPage);
+  app.get('/'                        , middlewares.applicationInstalled, loginRequired(false) , page.showTopPage);
 
-  app.get('/installer'               , middleware.applicationNotInstalled() , installer.index);
-  app.post('/installer'              , middleware.applicationNotInstalled() , form.register , csrf, installer.install);
+  app.get('/installer'               , middlewares.applicationNotInstalled , installer.index);
+  app.post('/installer'              , middlewares.applicationNotInstalled , form.register , csrf, installer.install);
 
   app.get('/login/error/:reason'     , login.error);
-  app.get('/login'                   , middleware.applicationInstalled()    , login.login);
+  app.get('/login'                   , middlewares.applicationInstalled    , login.login);
   app.get('/login/invited'           , login.invited);
   app.post('/login/activateInvited'  , form.invited                         , csrf, login.invited);
-
-  // switch POST /login route
-  if (Config.isEnabledPassport(config)) {
-    app.post('/login'                , form.login                           , csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
-    app.post('/_api/login/testLdap'  , loginRequired(crowi, app) , form.login , loginPassport.testLdapCredentials);
-  }
-  else {
-    app.post('/login'                , form.login                           , csrf, login.login);
-  }
+  app.post('/login'                  , form.login                           , csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
+  app.post('/_api/login/testLdap'    , loginRequired() , form.login , loginPassport.testLdapCredentials);
 
   app.post('/register'               , form.register                        , csrf, login.register);
-  app.get('/register'                , middleware.applicationInstalled()    , login.register);
-  app.post('/register/google'        , login.registerGoogle);
-  app.get('/google/callback'         , login.googleCallback);
-  app.get('/login/google'            , login.loginGoogle);
+  app.get('/register'                , middlewares.applicationInstalled    , login.register);
   app.get('/logout'                  , logout.logout);
 
-  app.get('/admin'                          , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
-  app.get('/admin/app'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
-  app.post('/_api/admin/settings/app'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
-  app.post('/_api/admin/settings/siteUrl'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.siteUrl, admin.api.asyncAppSetting);
-  app.post('/_api/admin/settings/mail'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
-  app.post('/_api/admin/settings/aws'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
-  app.post('/_api/admin/settings/plugin'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.plugin, admin.api.appSetting);
+  app.get('/admin'                          , loginRequired() , adminRequired , admin.index);
+  app.get('/admin/app'                      , loginRequired() , adminRequired , admin.app.index);
+  app.post('/_api/admin/settings/app'       , loginRequired() , adminRequired , csrf, form.admin.app, admin.api.appSetting);
+  app.post('/_api/admin/settings/siteUrl'   , loginRequired() , adminRequired , csrf, form.admin.siteUrl, admin.api.asyncAppSetting);
+  app.post('/_api/admin/settings/mail'      , loginRequired() , adminRequired , csrf, form.admin.mail, admin.api.appSetting);
+  app.post('/_api/admin/settings/aws'       , loginRequired() , adminRequired , csrf, form.admin.aws, admin.api.appSetting);
+  app.post('/_api/admin/settings/plugin'    , loginRequired() , adminRequired , csrf, form.admin.plugin, admin.api.appSetting);
 
   // security admin
-  app.get('/admin/security'                     , loginRequired(crowi, app) , middleware.adminRequired() , admin.security.index);
-  app.post('/_api/admin/security/general'       , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.securityGeneral, admin.api.securitySetting);
-  app.post('/_api/admin/security/google'        , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityGoogle, admin.api.securitySetting);
-  app.post('/_api/admin/security/mechanism'     , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityMechanism, admin.api.securitySetting);
-  app.post('/_api/admin/security/passport-ldap' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportLdap, admin.api.securityPassportLdapSetting);
-  app.post('/_api/admin/security/passport-saml' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportSaml, admin.api.securityPassportSamlSetting);
+  app.get('/admin/security'                     , loginRequired() , adminRequired , admin.security.index);
+  app.post('/_api/admin/security/general'       , loginRequired() , adminRequired , form.admin.securityGeneral, admin.api.securitySetting);
+  app.post('/_api/admin/security/passport-ldap' , loginRequired() , adminRequired , csrf, form.admin.securityPassportLdap, admin.api.securityPassportLdapSetting);
+  app.post('/_api/admin/security/passport-saml' , loginRequired() , adminRequired , csrf, form.admin.securityPassportSaml, admin.api.securityPassportSamlSetting);
+  app.post('/_api/admin/security/passport-basic' , loginRequired() , adminRequired , csrf, form.admin.securityPassportBasic, admin.api.securityPassportBasicSetting);
 
   // OAuth
-  app.post('/_api/admin/security/passport-google' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
-  app.post('/_api/admin/security/passport-github' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
-  app.post('/_api/admin/security/passport-twitter', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportTwitter, admin.api.securityPassportTwitterSetting);
-  app.post('/_api/admin/security/passport-oidc'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportOidc, admin.api.securityPassportOidcSetting);
+  app.post('/_api/admin/security/passport-google' , loginRequired() , adminRequired , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
+  app.post('/_api/admin/security/passport-github' , loginRequired() , adminRequired , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
+  app.post('/_api/admin/security/passport-twitter', loginRequired() , adminRequired , csrf, form.admin.securityPassportTwitter, admin.api.securityPassportTwitterSetting);
+  app.post('/_api/admin/security/passport-oidc',    loginRequired() , adminRequired , csrf, form.admin.securityPassportOidc, admin.api.securityPassportOidcSetting);
   app.get('/passport/google'                      , loginPassport.loginWithGoogle);
   app.get('/passport/github'                      , loginPassport.loginWithGitHub);
   app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
   app.get('/passport/oidc'                        , loginPassport.loginWithOidc);
   app.get('/passport/saml'                        , loginPassport.loginWithSaml);
+  app.get('/passport/basic'                       , loginPassport.loginWithBasic);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
   app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback);
@@ -89,160 +80,157 @@ module.exports = function(crowi, app) {
   app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback);
 
   // markdown admin
-  app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);
-  app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting); // change form name
-  app.post('/admin/markdown/xss-setting'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdownXss, admin.markdown.xssSetting);
-  app.post('/admin/markdown/presentationSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdownPresentation, admin.markdown.presentationSetting);
+  app.get('/admin/markdown'                   , loginRequired() , adminRequired , admin.markdown.index);
+  app.post('/admin/markdown/lineBreaksSetting', loginRequired() , adminRequired , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting); // change form name
+  app.post('/admin/markdown/xss-setting'      , loginRequired() , adminRequired , csrf, form.admin.markdownXss, admin.markdown.xssSetting);
+  app.post('/admin/markdown/presentationSetting', loginRequired() , adminRequired , csrf, form.admin.markdownPresentation, admin.markdown.presentationSetting);
 
   // markdown admin
-  app.get('/admin/customize'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.customize.index);
-  app.post('/_api/admin/customize/css'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/script'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customscript, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/header'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customheader, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/theme'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customtheme, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/title'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customtitle, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/behavior' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.custombehavior, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/layout'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customlayout, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/features' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customfeatures, admin.api.customizeSetting);
-  app.post('/_api/admin/customize/highlightJsStyle' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customhighlightJsStyle, admin.api.customizeSetting);
+  app.get('/admin/customize'                , loginRequired() , adminRequired , admin.customize.index);
+  app.post('/_api/admin/customize/css'      , loginRequired() , adminRequired , csrf, form.admin.customcss, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/script'   , loginRequired() , adminRequired , csrf, form.admin.customscript, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/header'   , loginRequired() , adminRequired , csrf, form.admin.customheader, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/theme'    , loginRequired() , adminRequired , csrf, form.admin.customtheme, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/title'    , loginRequired() , adminRequired , csrf, form.admin.customtitle, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/behavior' , loginRequired() , adminRequired , csrf, form.admin.custombehavior, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/layout'   , loginRequired() , adminRequired , csrf, form.admin.customlayout, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/features' , loginRequired() , adminRequired , csrf, form.admin.customfeatures, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/highlightJsStyle' , loginRequired() , adminRequired , csrf, form.admin.customhighlightJsStyle, admin.api.customizeSetting);
 
   // search admin
-  app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
-  app.post('/_api/admin/search/build'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.searchBuildIndex);
+  app.get('/admin/search'              , loginRequired() , adminRequired , admin.search.index);
+  app.post('/_api/admin/search/build'  , loginRequired() , adminRequired , csrf, admin.api.searchBuildIndex);
 
   // notification admin
-  app.get('/admin/notification'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.index);
-  app.post('/admin/notification/slackIwhSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.slackIwhSetting, admin.notification.slackIwhSetting);
-  app.post('/admin/notification/slackSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.slackSetting, admin.notification.slackSetting);
-  app.get('/admin/notification/slackAuth'    , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.slackAuth);
-  app.get('/admin/notification/slackSetting/disconnect', loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.disconnectFromSlack);
-  app.post('/_api/admin/notification.add'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationAdd);
-  app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationRemove);
-  app.get('/_api/admin/users.search'         , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.usersSearch);
-  app.get('/admin/global-notification/new'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.globalNotification.detail);
-  app.get('/admin/global-notification/:id'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.globalNotification.detail);
-  app.post('/admin/global-notification/new'  , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.notificationGlobal, admin.globalNotification.create);
-  app.post('/_api/admin/global-notification/toggleIsEnabled', loginRequired(crowi, app) , middleware.adminRequired() , admin.api.toggleIsEnabledForGlobalNotification);
-  app.post('/admin/global-notification/:id/update', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.notificationGlobal, admin.globalNotification.update);
-  app.post('/admin/global-notification/:id/remove', loginRequired(crowi, app) , middleware.adminRequired() , admin.globalNotification.remove);
-
-  app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
-  app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.invite);
-  app.post('/admin/user/:id/makeAdmin'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.makeAdmin);
-  app.post('/admin/user/:id/removeFromAdmin', loginRequired(crowi, app) , middleware.adminRequired() , admin.user.removeFromAdmin);
-  app.post('/admin/user/:id/activate'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.activate);
-  app.post('/admin/user/:id/suspend'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.suspend);
-  app.post('/admin/user/:id/remove'     , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.remove);
-  app.post('/admin/user/:id/removeCompletely' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.removeCompletely);
+  app.get('/admin/notification'              , loginRequired() , adminRequired , admin.notification.index);
+  app.post('/admin/notification/slackIwhSetting', loginRequired() , adminRequired , csrf, form.admin.slackIwhSetting, admin.notification.slackIwhSetting);
+  app.post('/admin/notification/slackSetting', loginRequired() , adminRequired , csrf, form.admin.slackSetting, admin.notification.slackSetting);
+  app.get('/admin/notification/slackAuth'    , loginRequired() , adminRequired , admin.notification.slackAuth);
+  app.get('/admin/notification/slackSetting/disconnect', loginRequired() , adminRequired , admin.notification.disconnectFromSlack);
+  app.post('/_api/admin/notification.add'    , loginRequired() , adminRequired , csrf, admin.api.notificationAdd);
+  app.post('/_api/admin/notification.remove' , loginRequired() , adminRequired , csrf, admin.api.notificationRemove);
+  app.get('/_api/admin/users.search'         , loginRequired() , adminRequired , admin.api.usersSearch);
+  app.get('/admin/global-notification/new'   , loginRequired() , adminRequired , admin.globalNotification.detail);
+  app.get('/admin/global-notification/:id'   , loginRequired() , adminRequired , admin.globalNotification.detail);
+  app.post('/admin/global-notification/new'  , loginRequired() , adminRequired , form.admin.notificationGlobal, admin.globalNotification.create);
+  app.post('/_api/admin/global-notification/toggleIsEnabled', loginRequired() , adminRequired , admin.api.toggleIsEnabledForGlobalNotification);
+  app.post('/admin/global-notification/:id/update', loginRequired() , adminRequired , form.admin.notificationGlobal, admin.globalNotification.update);
+  app.post('/admin/global-notification/:id/remove', loginRequired() , adminRequired , admin.globalNotification.remove);
+
+  app.get('/admin/users'                , loginRequired() , adminRequired , admin.user.index);
+  app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired() , adminRequired , csrf, admin.user.invite);
+  app.post('/admin/user/:id/makeAdmin'  , loginRequired() , adminRequired , csrf, admin.user.makeAdmin);
+  app.post('/admin/user/:id/removeFromAdmin', loginRequired() , adminRequired , admin.user.removeFromAdmin);
+  app.post('/admin/user/:id/activate'   , loginRequired() , adminRequired , csrf, admin.user.activate);
+  app.post('/admin/user/:id/suspend'    , loginRequired() , adminRequired , csrf, admin.user.suspend);
+  app.post('/admin/user/:id/remove'     , loginRequired() , adminRequired , csrf, admin.user.remove);
+  app.post('/admin/user/:id/removeCompletely' , loginRequired() , adminRequired , csrf, admin.user.removeCompletely);
   // new route patterns from here:
-  app.post('/_api/admin/users.resetPassword'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.resetPassword);
+  app.post('/_api/admin/users.resetPassword'  , loginRequired() , adminRequired , csrf, admin.user.resetPassword);
 
-  app.get('/admin/users/external-accounts'               , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.index);
-  app.post('/admin/users/external-accounts/:id/remove'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.remove);
+  app.get('/admin/users/external-accounts'               , loginRequired() , adminRequired , admin.externalAccount.index);
+  app.post('/admin/users/external-accounts/:id/remove'   , loginRequired() , adminRequired , admin.externalAccount.remove);
 
   // user-groups admin
-  app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
-  app.get('/admin/user-group-detail/:id'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
-  app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
-  app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
-  app.post('/admin/user-group.remove' , loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.removeCompletely);
-  app.get('/_api/admin/user-groups', loginRequired(crowi, app), middleware.adminRequired(), admin.api.userGroups);
+  app.get('/admin/user-groups'             , loginRequired(), adminRequired, admin.userGroup.index);
+  app.get('/admin/user-group-detail/:id'          , loginRequired(), adminRequired, admin.userGroup.detail);
+  app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(), adminRequired, csrf, admin.userGroup.create);
+  app.post('/admin/user-group/:userGroupId/update', loginRequired(), adminRequired, csrf, admin.userGroup.update);
+  app.post('/admin/user-group.remove' , loginRequired(), adminRequired, csrf, admin.userGroup.removeCompletely);
+  app.get('/_api/admin/user-groups', loginRequired(), adminRequired, admin.api.userGroups);
 
   // user-group-relations admin
-  app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
-  app.post('/admin/user-group-relation/:id/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
+  app.post('/admin/user-group-relation/create', loginRequired(), adminRequired, csrf, admin.userGroupRelation.create);
+  app.post('/admin/user-group-relation/:id/remove-relation/:relationId', loginRequired(), adminRequired, csrf, admin.userGroupRelation.remove);
 
   // importer management for admin
-  app.get('/admin/importer'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.importer.index);
-  app.post('/_api/admin/settings/importerEsa' , loginRequired(crowi, app) , middleware.adminRequired() , csrf , form.admin.importerEsa , admin.api.importerSettingEsa);
-  app.post('/_api/admin/settings/importerQiita' , loginRequired(crowi, app) , middleware.adminRequired() , csrf , form.admin.importerQiita , admin.api.importerSettingQiita);
-  app.post('/_api/admin/import/esa'        , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.importDataFromEsa);
-  app.post('/_api/admin/import/testEsaAPI' , loginRequired(crowi, app) , middleware.adminRequired() , csrf , form.admin.importerEsa , admin.api.testEsaAPI);
-  app.post('/_api/admin/import/qiita'        , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.importDataFromQiita);
-  app.post('/_api/admin/import/testQiitaAPI' , loginRequired(crowi, app) , middleware.adminRequired() , csrf , form.admin.importerQiita , admin.api.testQiitaAPI);
-
-  app.get('/me'                       , loginRequired(crowi, app) , me.index);
-  app.get('/me/password'              , loginRequired(crowi, app) , me.password);
-  app.get('/me/apiToken'              , loginRequired(crowi, app) , me.apiToken);
-  app.post('/me'                      , loginRequired(crowi, app) , csrf , form.me.user , me.index);
+  app.get('/admin/importer'                , loginRequired() , adminRequired , admin.importer.index);
+  app.post('/_api/admin/settings/importerEsa' , loginRequired() , adminRequired , csrf , form.admin.importerEsa , admin.api.importerSettingEsa);
+  app.post('/_api/admin/settings/importerQiita' , loginRequired() , adminRequired , csrf , form.admin.importerQiita , admin.api.importerSettingQiita);
+  app.post('/_api/admin/import/esa'        , loginRequired() , adminRequired , admin.api.importDataFromEsa);
+  app.post('/_api/admin/import/testEsaAPI' , loginRequired() , adminRequired , csrf , form.admin.importerEsa , admin.api.testEsaAPI);
+  app.post('/_api/admin/import/qiita'        , loginRequired() , adminRequired , admin.api.importDataFromQiita);
+  app.post('/_api/admin/import/testQiitaAPI' , loginRequired() , adminRequired , csrf , form.admin.importerQiita , admin.api.testQiitaAPI);
+
+  app.get('/me'                       , loginRequired() , me.index);
+  app.get('/me/password'              , loginRequired() , me.password);
+  app.get('/me/apiToken'              , loginRequired() , me.apiToken);
+  app.post('/me'                      , loginRequired() , csrf , form.me.user , me.index);
   // external-accounts
-  if (Config.isEnabledPassport(config)) {
-    app.get('/me/external-accounts'                         , loginRequired(crowi, app) , me.externalAccounts.list);
-    app.post('/me/external-accounts/disassociate'           , loginRequired(crowi, app) , me.externalAccounts.disassociate);
-    app.post('/me/external-accounts/associateLdap'          , loginRequired(crowi, app) , form.login , me.externalAccounts.associateLdap);
-  }
-  app.post('/me/password'             , form.me.password          , loginRequired(crowi, app) , me.password);
-  app.post('/me/imagetype'            , form.me.imagetype         , loginRequired(crowi, app) , me.imagetype);
-  app.post('/me/apiToken'             , form.me.apiToken          , loginRequired(crowi, app) , me.apiToken);
-  app.post('/me/auth/google'          , loginRequired(crowi, app) , me.authGoogle);
-  app.get('/me/auth/google/callback' , loginRequired(crowi, app) , me.authGoogleCallback);
-
-  app.get('/:id([0-9a-z]{24})'       , loginRequired(crowi, app, false) , page.redirector);
-  app.get('/_r/:id([0-9a-z]{24})'    , loginRequired(crowi, app, false) , page.redirector); // alias
-  app.get('/attachment/:pageId/:fileName'  , loginRequired(crowi, app, false), attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
-  app.get('/attachment/:id([0-9a-z]{24})'  , loginRequired(crowi, app, false), attachment.api.get);
-  app.get('/download/:id([0-9a-z]{24})'    , loginRequired(crowi, app, false), attachment.api.download);
-
-  app.get('/_search'                 , loginRequired(crowi, app, false) , search.searchPage);
-  app.get('/_api/search'             , accessTokenParser , loginRequired(crowi, app, false) , search.api.search);
+  app.get('/me/external-accounts'                         , loginRequired() , me.externalAccounts.list);
+  app.post('/me/external-accounts/disassociate'           , loginRequired() , me.externalAccounts.disassociate);
+  app.post('/me/external-accounts/associateLdap'          , loginRequired() , form.login , me.externalAccounts.associateLdap);
+
+  app.post('/me/password'             , form.me.password          , loginRequired() , me.password);
+  app.post('/me/imagetype'            , form.me.imagetype         , loginRequired() , me.imagetype);
+  app.post('/me/apiToken'             , form.me.apiToken          , loginRequired() , me.apiToken);
+
+  app.get('/:id([0-9a-z]{24})'       , loginRequired(false) , page.redirector);
+  app.get('/_r/:id([0-9a-z]{24})'    , loginRequired(false) , page.redirector); // alias
+  app.get('/attachment/:pageId/:fileName'  , loginRequired(false), attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
+  app.get('/attachment/:id([0-9a-z]{24})'  , loginRequired(false), attachment.api.get);
+  app.get('/download/:id([0-9a-z]{24})'    , loginRequired(false), attachment.api.download);
+
+  app.get('/_search'                 , loginRequired(false) , search.searchPage);
+  app.get('/_api/search'             , accessTokenParser , loginRequired(false) , search.api.search);
 
   app.get('/_api/check_username'           , user.api.checkUsername);
-  app.get('/_api/me/user-group-relations'  , accessTokenParser , loginRequired(crowi, app) , me.api.userGroupRelations);
-  app.get('/_api/user/bookmarks'           , loginRequired(crowi, app, false) , user.api.bookmarks);
+  app.get('/_api/me/user-group-relations'  , accessTokenParser , loginRequired() , me.api.userGroupRelations);
+  app.get('/_api/user/bookmarks'           , loginRequired(false) , user.api.bookmarks);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
-  app.get('/_api/users.list'          , accessTokenParser , loginRequired(crowi, app, false) , user.api.list);
-  app.get('/_api/pages.list'          , accessTokenParser , loginRequired(crowi, app, false) , page.api.list);
-  app.get('/_api/pages.recentCreated' , accessTokenParser , loginRequired(crowi, app, false) , page.api.recentCreated);
-  app.post('/_api/pages.create'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.create);
-  app.post('/_api/pages.update'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.update);
-  app.get('/_api/pages.get'           , accessTokenParser , loginRequired(crowi, app, false) , page.api.get);
-  app.get('/_api/pages.exist'         , accessTokenParser , loginRequired(crowi, app, false) , page.api.exist);
-  app.get('/_api/pages.updatePost', accessTokenParser, loginRequired(crowi, app, false), page.api.getUpdatePost);
-  app.get('/_api/pages.getPageTag'    , accessTokenParser , loginRequired(crowi, app, false) , page.api.getPageTag);
+  app.get('/_api/users.list'          , accessTokenParser , loginRequired(false) , user.api.list);
+  app.get('/_api/pages.list'          , accessTokenParser , loginRequired(false) , page.api.list);
+  app.get('/_api/pages.recentCreated' , accessTokenParser , loginRequired(false) , page.api.recentCreated);
+  app.post('/_api/pages.create'       , accessTokenParser , loginRequired() , csrf, page.api.create);
+  app.post('/_api/pages.update'       , accessTokenParser , loginRequired() , csrf, page.api.update);
+  app.get('/_api/pages.get'           , accessTokenParser , loginRequired(false) , page.api.get);
+  app.get('/_api/pages.exist'         , accessTokenParser , loginRequired(false) , page.api.exist);
+  app.get('/_api/pages.updatePost'    , accessTokenParser, loginRequired(false), page.api.getUpdatePost);
+  app.get('/_api/pages.getPageTag'    , accessTokenParser , loginRequired(false) , page.api.getPageTag);
   // allow posting to guests because the client doesn't know whether the user logged in
-  app.post('/_api/pages.seen'         , accessTokenParser , loginRequired(crowi, app, false) , page.api.seen);
-  app.post('/_api/pages.rename'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.rename);
-  app.post('/_api/pages.remove'       , loginRequired(crowi, app) , csrf, page.api.remove); // (Avoid from API Token)
-  app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
-  app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
-  app.post('/_api/pages.duplicate', accessTokenParser, loginRequired(crowi, app), csrf, page.api.duplicate);
-  app.get('/tags'                     , loginRequired(crowi, app, false), tag.showPage);
-  app.get('/_api/tags.list'           , accessTokenParser, loginRequired(crowi, app, false), tag.api.list);
-  app.get('/_api/tags.search'         , accessTokenParser, loginRequired(crowi, app, false), tag.api.search);
-  app.post('/_api/tags.update'         , accessTokenParser, loginRequired(crowi, app, false), tag.api.update);
-  app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
-  app.post('/_api/comments.add'       , comment.api.validators.add(), accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
-  app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);
-  app.get('/_api/bookmarks.get'      , accessTokenParser , loginRequired(crowi, app, false) , bookmark.api.get);
-  app.post('/_api/bookmarks.add'      , accessTokenParser , loginRequired(crowi, app) , csrf, bookmark.api.add);
-  app.post('/_api/bookmarks.remove'   , accessTokenParser , loginRequired(crowi, app) , csrf, bookmark.api.remove);
-  app.post('/_api/likes.add'          , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.like);
-  app.post('/_api/likes.remove'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.unlike);
-  app.get('/_api/attachments.list'   , accessTokenParser , loginRequired(crowi, app, false) , attachment.api.list);
-  app.post('/_api/attachments.add'                  , uploads.single('file'), autoReap, accessTokenParser, loginRequired(crowi, app) ,csrf, attachment.api.add);
-  app.post('/_api/attachments.removeProfileImage', accessTokenParser, loginRequired(crowi, app), csrf, attachment.api.removeProfileImage);
-  app.post('/_api/attachments.uploadProfileImage'   , uploads.single('file'), autoReap, accessTokenParser, loginRequired(crowi, app) ,csrf, attachment.api.uploadProfileImage);
-  app.post('/_api/attachments.remove' , accessTokenParser , loginRequired(crowi, app) , csrf, attachment.api.remove);
-  app.get('/_api/attachments.limit'   , accessTokenParser , loginRequired(crowi, app) , attachment.api.limit);
-
-  app.get('/_api/revisions.get'      , accessTokenParser , loginRequired(crowi, app, false) , revision.api.get);
-  app.get('/_api/revisions.ids'      , accessTokenParser , loginRequired(crowi, app, false) , revision.api.ids);
-  app.get('/_api/revisions.list'     , accessTokenParser , loginRequired(crowi, app, false) , revision.api.list);
-
-  app.get('/trash$'                  , loginRequired(crowi, app, false) , page.trashPageShowWrapper);
-  app.get('/trash/$'                 , loginRequired(crowi, app, false) , page.trashPageListShowWrapper);
-  app.get('/trash/*/$'               , loginRequired(crowi, app, false) , page.deletedPageListShowWrapper);
-
-  app.get('/_hackmd/load-agent'        , hackmd.loadAgent);
-  app.get('/_hackmd/load-styles'       , hackmd.loadStyles);
-  app.post('/_api/hackmd.integrate'    , accessTokenParser , loginRequired(crowi, app) , csrf, hackmd.validateForApi, hackmd.integrate);
-  app.post('/_api/hackmd.saveOnHackmd' , accessTokenParser , loginRequired(crowi, app) , csrf, hackmd.validateForApi, hackmd.saveOnHackmd);
+  app.post('/_api/pages.seen'         , accessTokenParser , loginRequired(false) , page.api.seen);
+  app.post('/_api/pages.rename'       , accessTokenParser , loginRequired() , csrf, page.api.rename);
+  app.post('/_api/pages.remove'       , loginRequired() , csrf, page.api.remove); // (Avoid from API Token)
+  app.post('/_api/pages.revertRemove' , loginRequired() , csrf, page.api.revertRemove); // (Avoid from API Token)
+  app.post('/_api/pages.unlink'       , loginRequired() , csrf, page.api.unlink); // (Avoid from API Token)
+  app.post('/_api/pages.duplicate'    , accessTokenParser, loginRequired(), csrf, page.api.duplicate);
+  app.get('/tags'                     , loginRequired(false), tag.showPage);
+  app.get('/_api/tags.list'           , accessTokenParser, loginRequired(false), tag.api.list);
+  app.get('/_api/tags.search'         , accessTokenParser, loginRequired(false), tag.api.search);
+  app.post('/_api/tags.update'        , accessTokenParser, loginRequired(false), tag.api.update);
+  app.get('/_api/comments.get'        , accessTokenParser , loginRequired(false) , comment.api.get);
+  app.post('/_api/comments.add'       , comment.api.validators.add(), accessTokenParser , loginRequired() , csrf, comment.api.add);
+  app.post('/_api/comments.remove'    , accessTokenParser , loginRequired() , csrf, comment.api.remove);
+  app.get('/_api/bookmarks.get'       , accessTokenParser , loginRequired(false) , bookmark.api.get);
+  app.post('/_api/bookmarks.add'      , accessTokenParser , loginRequired() , csrf, bookmark.api.add);
+  app.post('/_api/bookmarks.remove'   , accessTokenParser , loginRequired() , csrf, bookmark.api.remove);
+  app.post('/_api/likes.add'          , accessTokenParser , loginRequired() , csrf, page.api.like);
+  app.post('/_api/likes.remove'       , accessTokenParser , loginRequired() , csrf, page.api.unlike);
+  app.get('/_api/attachments.list'    , accessTokenParser , loginRequired(false) , attachment.api.list);
+  app.post('/_api/attachments.add'                  , uploads.single('file'), autoReap, accessTokenParser, loginRequired() ,csrf, attachment.api.add);
+  app.post('/_api/attachments.uploadProfileImage'   , uploads.single('file'), autoReap, accessTokenParser, loginRequired() ,csrf, attachment.api.uploadProfileImage);
+  app.post('/_api/attachments.remove'               , accessTokenParser , loginRequired() , csrf, attachment.api.remove);
+  app.post('/_api/attachments.removeProfileImage'   , accessTokenParser , loginRequired() , csrf, attachment.api.removeProfileImage);
+  app.get('/_api/attachments.limit'   , accessTokenParser , loginRequired(), attachment.api.limit);
+
+  app.get('/_api/revisions.get'       , accessTokenParser , loginRequired(false) , revision.api.get);
+  app.get('/_api/revisions.ids'       , accessTokenParser , loginRequired(false) , revision.api.ids);
+  app.get('/_api/revisions.list'      , accessTokenParser , loginRequired(false) , revision.api.list);
+
+  app.get('/trash$'                   , loginRequired(false) , page.trashPageShowWrapper);
+  app.get('/trash/$'                  , loginRequired(false) , page.trashPageListShowWrapper);
+  app.get('/trash/*/$'                , loginRequired(false) , page.deletedPageListShowWrapper);
+
+  app.get('/_hackmd/load-agent'          , hackmd.loadAgent);
+  app.get('/_hackmd/load-styles'         , hackmd.loadStyles);
+  app.post('/_api/hackmd.integrate'      , accessTokenParser , loginRequired() , csrf, hackmd.validateForApi, hackmd.integrate);
+  app.post('/_api/hackmd.saveOnHackmd'   , accessTokenParser , loginRequired() , csrf, hackmd.validateForApi, hackmd.saveOnHackmd);
 
   // API v3
   app.use('/api-docs', require('./apiv3/docs')(crowi));
   app.use('/_api/v3', require('./apiv3')(crowi));
 
-  app.get('/*/$'                   , loginRequired(crowi, app, false) , page.showPageWithEndOfSlash, page.notFound);
-  app.get('/*'                     , loginRequired(crowi, app, false) , page.showPage, page.notFound);
+  app.get('/*/$'                   , loginRequired(false) , page.showPageWithEndOfSlash, page.notFound);
+  app.get('/*'                     , loginRequired(false) , page.showPage, page.notFound);
 };

+ 13 - 21
src/server/routes/installer.js

@@ -2,8 +2,10 @@ module.exports = function(crowi, app) {
   const logger = require('@alias/logger')('growi:routes:installer');
   const path = require('path');
   const fs = require('graceful-fs');
+
   const models = crowi.models;
-  const Config = models.Config;
+  const { appService } = crowi;
+
   const User = models.User;
   const Page = models.Page;
 
@@ -65,6 +67,9 @@ module.exports = function(crowi, app) {
     const password = registerForm.password;
     const language = registerForm['app:globalLang'] || 'en-US';
 
+    await appService.initDB(language);
+
+    // create first admin user
     let adminUser;
     try {
       adminUser = await User.createUser(name, username, email, password, language);
@@ -74,29 +79,16 @@ module.exports = function(crowi, app) {
       req.form.errors.push(`管理ユーザーの作成に失敗しました。${err.message}`);
       return res.render('installer');
     }
+    // create initial pages
+    await createInitialPages(adminUser, language);
 
-    Config.applicationInstall((err, configs) => {
-      if (err) {
-        logger.error(err);
-        return;
-      }
-
-      // save the globalLang config, and update the config cache
-      Config.updateNamespaceByArray('crowi', { 'app:globalLang': language }, (err, config) => {
-        Config.updateConfigCache('crowi', config);
-      });
-
-      // login with passport
-      req.logIn(adminUser, (err) => {
-        if (err) { return next() }
+    // login with passport
+    req.logIn(adminUser, (err) => {
+      if (err) { return next() }
 
-        req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
-        return res.redirect('/admin/app');
-      });
+      req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
+      return res.redirect('/admin/app');
     });
-
-    // create initial pages
-    await createInitialPages(adminUser, language);
   };
 
   return actions;

+ 47 - 1
src/server/routes/login-passport.js

@@ -177,7 +177,7 @@ module.exports = function(crowi, app) {
         if (!isValidLdapUserByGroupFilter(user)) {
           return res.json({
             status: 'warning',
-            message: 'The user is found, but that has no groups.',
+            message: 'This user does not belong to any groups designated by the group search filter.',
             ldapConfiguration: req.ldapConfiguration,
             ldapAccountInfo: req.ldapAccountInfo,
           });
@@ -455,6 +455,51 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * middleware that login with BasicStrategy
+   * @param {*} req
+   * @param {*} res
+   * @param {*} next
+   */
+  const loginWithBasic = async(req, res, next) => {
+    if (!passportService.isBasicStrategySetup) {
+      debug('BasicStrategy has not been set up');
+      req.flash('warningMessage', 'Basic has not been set up');
+      return next();
+    }
+
+    const providerId = 'basic';
+    const strategyName = 'basic';
+    let userId;
+
+    try {
+      userId = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      // display prompt in browser
+      res.setHeader('WWW-Authenticate', 'Basic realm="Users"');
+      res.sendStatus(401).end();
+      return;
+    }
+
+    const userInfo = {
+      id: userId,
+      username: userId,
+      name: userId,
+    };
+
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
+    if (!externalAccount) {
+      return loginFailure(req, res, next);
+    }
+
+    const user = await externalAccount.getPopulatedUser();
+    await req.logIn(user, (err) => {
+      if (err) { return next() }
+      return loginSuccess(req, res, user);
+    });
+  };
+
   const promisifiedPassportAuthentication = (strategyName, req, res) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
@@ -530,6 +575,7 @@ module.exports = function(crowi, app) {
     loginWithTwitter,
     loginWithOidc,
     loginWithSaml,
+    loginWithBasic,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
     loginPassportTwitterCallback,

+ 12 - 124
src/server/routes/login.js

@@ -8,16 +8,12 @@ module.exports = function(crowi, app) {
   const logger = require('@alias/logger')('growi:routes:login');
   const path = require('path');
   const async = require('async');
-  const config = crowi.getConfig();
   const mailer = crowi.getMailer();
   const User = crowi.model('User');
-  const Config = crowi.model('Config');
+  const { configManager, appService, aclService } = crowi;
 
   const actions = {};
 
-  const clearGoogleSession = function(req) {
-    req.session.googleAuthCode = req.session.googleId = req.session.googleEmail = req.session.googleName = req.session.googleImage = null;
-  };
   const loginSuccess = function(req, res, userData) {
     req.user = req.session.user = userData;
 
@@ -32,8 +28,6 @@ module.exports = function(crowi, app) {
       return res.redirect('/me/password');
     }
 
-    clearGoogleSession(req);
-
     const jumpTo = req.session.jumpTo;
     if (jumpTo) {
       req.session.jumpTo = null;
@@ -60,16 +54,6 @@ module.exports = function(crowi, app) {
     return res.redirect('/login');
   };
 
-  actions.googleCallback = function(req, res) {
-    const nextAction = req.session.googleCallbackAction || '/login';
-    debug('googleCallback.nextAction', nextAction);
-    req.session.googleAuthCode = req.query.code || '';
-    debug('google auth code', req.query.code);
-
-
-    return res.redirect(nextAction);
-  };
-
   actions.error = function(req, res) {
     const reason = req.params.reason;
 
@@ -114,43 +98,7 @@ module.exports = function(crowi, app) {
     }
   };
 
-  actions.loginGoogle = function(req, res) {
-    const googleAuth = require('../util/googleAuth')(crowi);
-    const code = req.session.googleAuthCode || null;
-
-    if (!code) {
-      googleAuth.createAuthUrl(req, (err, redirectUrl) => {
-        if (err) {
-          // TODO
-        }
-
-        req.session.googleCallbackAction = '/login/google';
-        return res.redirect(redirectUrl);
-      });
-    }
-    else {
-      googleAuth.handleCallback(req, (err, tokenInfo) => {
-        debug('handleCallback', err, tokenInfo);
-        if (err) {
-          return loginFailure(req, res);
-        }
-
-        const googleId = tokenInfo.user_id;
-        User.findUserByGoogleId(googleId, (err, userData) => {
-          debug('findUserByGoogleId', err, userData);
-          if (!userData) {
-            clearGoogleSession(req);
-            return loginFailure(req, res);
-          }
-          return loginSuccess(req, res, userData);
-        });
-      });
-    }
-  };
-
   actions.register = function(req, res) {
-    const googleAuth = require('../util/googleAuth')(crowi);
-
     // redirect to '/' if both of these are true:
     //  1. user has logged in
     //  2. req.user is not username/email string (which is set by basic-auth-connect)
@@ -159,7 +107,7 @@ module.exports = function(crowi, app) {
     }
 
     // config で closed ならさよなら
-    if (config.crowi['security:registrationMode'] == Config.SECURITY_REGISTRATION_MODE_CLOSED) {
+    if (configManager.getConfig('crowi', 'security:registrationMode') == aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
       return res.redirect('/');
     }
 
@@ -170,8 +118,6 @@ module.exports = function(crowi, app) {
       const username = registerForm.username;
       const email = registerForm.email;
       const password = registerForm.password;
-      var googleId = registerForm.googleId || null;
-      var googleImage = registerForm.googleImage || null;
 
       // email と username の unique チェックする
       User.isRegisterable(email, username, (isRegisterable, errOn) => {
@@ -208,8 +154,8 @@ module.exports = function(crowi, app) {
 
 
           // 作成後、承認が必要なモードなら、管理者に通知する
-          const appTitle = Config.appTitle(config);
-          if (config.crowi['security:registrationMode'] === Config.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+          const appTitle = appService.getAppTitle();
+          if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
             // TODO send mail
             User.findAdmins((err, admins) => {
               async.each(
@@ -222,7 +168,7 @@ module.exports = function(crowi, app) {
                     vars: {
                       createdUser: userData,
                       adminUser,
-                      url: crowi.configManager.getSiteUrl(),
+                      url: appService.getSiteUrl(),
                       appTitle,
                     },
                   },
@@ -238,81 +184,23 @@ module.exports = function(crowi, app) {
             });
           }
 
-          if (googleId) {
-            userData.updateGoogleId(googleId, (err, userData) => {
-              if (err) { // TODO
-              }
-              return loginSuccess(req, res, userData);
-            });
-          }
-          else {
-            // add a flash message to inform the user that processing was successful -- 2017.09.23 Yuki Takei
-            // cz. loginSuccess method doesn't work on it's own when using passport
-            //      because `req.login()` prepared by passport is not called.
-            req.flash('successMessage', `The user '${userData.username}' is successfully created.`);
 
-            return loginSuccess(req, res, userData);
-          }
+          // add a flash message to inform the user that processing was successful -- 2017.09.23 Yuki Takei
+          // cz. loginSuccess method doesn't work on it's own when using passport
+          //      because `req.login()` prepared by passport is not called.
+          req.flash('successMessage', `The user '${userData.username}' is successfully created.`);
+
+          return loginSuccess(req, res, userData);
         });
       });
     }
     else { // method GET of form is not valid
       debug('session is', req.session);
       const isRegistering = true;
-      // google callback を受ける可能性もある
-      const code = req.session.googleAuthCode || null;
-      var googleId = req.session.googleId || null;
-      let googleEmail = req.session.googleEmail || null;
-      let googleName = req.session.googleName || null;
-      var googleImage = req.session.googleImage || null;
-
-      debug('register. if code', code);
-      // callback 経由で reigster にアクセスしてきた時最初だけこの if に入る
-      // code から email などを取得したらそれを session にいれて code は消去
-      if (code) {
-        googleAuth.handleCallback(req, (err, tokenInfo) => {
-          debug('tokenInfo on register GET', tokenInfo);
-          req.session.googleAuthCode = null;
-
-          if (err) {
-            req.flash('registerWarningMessage', 'Error on connectiong Google');
-            return res.redirect('/login?register=1'); // TODO Handling
-          }
-
-          req.session.googleId = googleId = tokenInfo.user_id;
-          req.session.googleEmail = googleEmail = tokenInfo.email;
-          req.session.googleName = googleName = tokenInfo.name;
-          req.session.googleImage = googleImage = tokenInfo.picture;
-
-          if (!User.isEmailValid(googleEmail)) {
-            req.flash('registerWarningMessage', 'このメールアドレスのGoogleアカウントはコネクトできません。');
-            return res.redirect('/login?register=1');
-          }
-          return res.render('login', {
-            isRegistering, googleId, googleEmail, googleName, googleImage,
-          });
-        });
-      }
-      else {
-        return res.render('login', {
-          isRegistering, googleId, googleEmail, googleName, googleImage,
-        });
-      }
+      return res.render('login', { isRegistering });
     }
   };
 
-  actions.registerGoogle = function(req, res) {
-    const googleAuth = require('../util/googleAuth')(crowi);
-    googleAuth.createAuthUrl(req, (err, redirectUrl) => {
-      if (err) {
-        // TODO
-      }
-
-      req.session.googleCallbackAction = '/register';
-      return res.redirect(redirectUrl);
-    });
-  };
-
   actions.invited = async function(req, res) {
     if (!req.user) {
       return res.redirect('/login');

+ 0 - 67
src/server/routes/me.js

@@ -309,72 +309,5 @@ module.exports = function(crowi, app) {
     });
   };
 
-  actions.authGoogle = function(req, res) {
-    const googleAuth = require('../util/googleAuth')(crowi);
-
-    const userData = req.user;
-
-    const toDisconnect = !!req.body.disconnectGoogle;
-    const toConnect = !!req.body.connectGoogle;
-    if (toDisconnect) {
-      userData.deleteGoogleId((err, userData) => {
-        req.flash('successMessage', 'Disconnected from Google account');
-
-        return res.redirect('/me');
-      });
-    }
-    else if (toConnect) {
-      googleAuth.createAuthUrl(req, (err, redirectUrl) => {
-        if (err) {
-          // TODO
-        }
-
-        req.session.googleCallbackAction = '/me/auth/google/callback';
-        return res.redirect(redirectUrl);
-      });
-    }
-    else {
-      return res.redirect('/me');
-    }
-  };
-
-  actions.authGoogleCallback = function(req, res) {
-    const googleAuth = require('../util/googleAuth')(crowi);
-    const userData = req.user;
-
-    googleAuth.handleCallback(req, (err, tokenInfo) => {
-      if (err) {
-        req.flash('warningMessage.auth.google', err.message); // FIXME: show library error message directly
-        return res.redirect('/me'); // TODO Handling
-      }
-
-      const googleId = tokenInfo.user_id;
-      const googleEmail = tokenInfo.email;
-      if (!User.isEmailValid(googleEmail)) {
-        req.flash('warningMessage.auth.google', 'You can\'t connect with this  Google\'s account');
-        return res.redirect('/me');
-      }
-
-      User.findUserByGoogleId(googleId, (err, googleUser) => {
-        if (!err && googleUser) {
-          req.flash('warningMessage.auth.google', 'This Google\'s account is connected by another user');
-          return res.redirect('/me');
-        }
-
-        userData.updateGoogleId(googleId, (err, userData) => {
-          if (err) {
-            debug('Failed to updateGoogleId', err);
-            req.flash('warningMessage.auth.google', 'Failed to connect Google Account');
-            return res.redirect('/me');
-          }
-
-          // TODO if err
-          req.flash('successMessage', 'Connected with Google');
-          return res.redirect('/me');
-        });
-      });
-    });
-  };
-
   return actions;
 };

+ 21 - 15
src/server/routes/page.js

@@ -2,18 +2,21 @@
 module.exports = function(crowi, app) {
   const debug = require('debug')('growi:routes:page');
   const logger = require('@alias/logger')('growi:routes:page');
+  const swig = require('swig-templates');
+
   const pathUtils = require('growi-commons').pathUtils;
+
   const Page = crowi.model('Page');
   const User = crowi.model('User');
-  const Config = crowi.model('Config');
-  const config = crowi.getConfig();
   const Bookmark = crowi.model('Bookmark');
   const PageTagRelation = crowi.model('PageTagRelation');
   const UpdatePost = crowi.model('UpdatePost');
+
   const ApiResponse = require('../util/apiResponse');
-  const interceptorManager = crowi.getInterceptorManager();
-  const swig = require('swig-templates');
   const getToday = require('../util/getToday');
+
+  const { configManager, slackNotificationService } = crowi;
+  const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
 
   const actions = {};
@@ -94,7 +97,7 @@ module.exports = function(crowi, app) {
         logger.error('Error occured in updating slack channels: ', err);
       });
 
-    if (Config.hasSlackConfig(config)) {
+    if (slackNotificationService.hasSlackConfig()) {
       const promises = slackChannels.split(',').map((chan) => {
         return crowi.slack.postPage(page, user, chan, updateOrCreate, previousRevision);
       });
@@ -303,9 +306,9 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.showPageWithEndOfSlash = function(req, res, next) {
-    const behaviorType = Config.behaviorType(config);
+    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
 
-    if (!behaviorType || behaviorType === 'crowi') {
+    if (behaviorType === 'crowi') {
       return showPageListForCrowiBehavior(req, res, next);
     }
     else {
@@ -327,10 +330,10 @@ module.exports = function(crowi, app) {
       return showPageForPresentation(req, res, next);
     }
 
-    const behaviorType = Config.behaviorType(config);
+    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
 
     // check whether this page has portal page
-    if (!behaviorType || behaviorType === 'crowi') {
+    if (behaviorType === 'crowi') {
       const portalPagePath = pathUtils.addTrailingSlash(getPathFromRequest(req));
       const hasPortalPage = await Page.count({ path: portalPagePath }) > 0;
 
@@ -349,9 +352,9 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.trashPageListShowWrapper = function(req, res) {
-    const behaviorType = Config.behaviorType(config);
+    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
 
-    if (!behaviorType || behaviorType === 'crowi') {
+    if (behaviorType === 'crowi') {
       // Crowi behavior for '/trash/*'
       return actions.deletedPageListShow(req, res);
     }
@@ -367,9 +370,9 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.trashPageShowWrapper = function(req, res) {
-    const behaviorType = Config.behaviorType(config);
+    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
 
-    if (!behaviorType || behaviorType === 'crowi') {
+    if (behaviorType === 'crowi') {
       // redirect to '/trash/'
       return res.redirect('/trash/');
     }
@@ -385,9 +388,9 @@ module.exports = function(crowi, app) {
    */
   /* eslint-disable no-else-return */
   actions.deletedPageListShowWrapper = function(req, res) {
-    const behaviorType = Config.behaviorType(config);
+    const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
 
-    if (!behaviorType || behaviorType === 'crowi') {
+    if (behaviorType === 'crowi') {
       // Crowi behavior for '/trash/*'
       return actions.deletedPageListShow(req, res);
     }
@@ -951,6 +954,9 @@ module.exports = function(crowi, app) {
 
     try {
       if (isCompletely) {
+        if (!req.user.canDeleteCompletely(page.creator)) {
+          return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
+        }
         if (isRecursively) {
           page = await Page.completelyDeletePageRecursively(page, req.user, options);
         }

+ 69 - 0
src/server/service/acl.js

@@ -0,0 +1,69 @@
+const logger = require('@alias/logger')('growi:service:AclService'); // eslint-disable-line no-unused-vars
+
+/**
+ * the service class of AclService
+ */
+class AclService {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+    this.labels = {
+      SECURITY_RESTRICT_GUEST_MODE_DENY: 'Deny',
+      SECURITY_RESTRICT_GUEST_MODE_READONLY: 'Readonly',
+      SECURITY_REGISTRATION_MODE_OPEN: 'Open',
+      SECURITY_REGISTRATION_MODE_RESTRICTED: 'Restricted',
+      SECURITY_REGISTRATION_MODE_CLOSED: 'Closed',
+    };
+  }
+
+  getIsPublicWikiOnly() {
+    const publicWikiOnly = process.env.PUBLIC_WIKI_ONLY;
+    return !!publicWikiOnly;
+  }
+
+  getIsGuestAllowedToRead() {
+    // return true if puclic wiki mode
+    if (this.getIsPublicWikiOnly()) {
+      return true;
+    }
+
+    // return false if undefined
+    const isRestrictGuestMode = this.configManager.getConfig('crowi', 'security:restrictGuestMode');
+    if (isRestrictGuestMode) {
+      return false;
+    }
+
+    return this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY === isRestrictGuestMode;
+  }
+
+  getRestrictGuestModeLabels() {
+    const labels = {};
+    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_DENY] = 'security_setting.guest_mode.deny';
+    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_setting.guest_mode.readonly';
+
+    return labels;
+  }
+
+  getRegistrationModeLabels() {
+    const labels = {};
+    labels[this.labels.SECURITY_REGISTRATION_MODE_OPEN] = 'security_setting.registration_mode.open';
+    labels[this.labels.SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_setting.registration_mode.restricted';
+    labels[this.labels.SECURITY_REGISTRATION_MODE_CLOSED] = 'security_setting.registration_mode.closed';
+
+    return labels;
+  }
+
+  userUpperLimit() {
+    // const limit = this.configManager.getConfig('crowi', 'USER_UPPER_LIMIT');
+    const limit = process.env.USER_UPPER_LIMIT;
+
+    if (limit) {
+      return Number(limit);
+    }
+
+    return 0;
+  }
+
+}
+
+module.exports = AclService;

+ 54 - 0
src/server/service/app.js

@@ -0,0 +1,54 @@
+const logger = require('@alias/logger')('growi:service:AppService'); // eslint-disable-line no-unused-vars
+const { pathUtils } = require('growi-commons');
+
+/**
+ * the service class of AppService
+ */
+class AppService {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+  }
+
+  getAppTitle() {
+    return this.configManager.getConfig('crowi', 'app:title') || 'GROWI';
+  }
+
+  /**
+   * get the site url
+   *
+   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
+   *
+   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
+   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
+   * With version 3.3.5 and above, the system use only a value from the config.
+   */
+  /* eslint-disable no-else-return */
+  getSiteUrl() {
+    const siteUrl = this.configManager.getConfig('crowi', 'app:siteUrl');
+    if (siteUrl != null) {
+      return pathUtils.removeTrailingSlash(siteUrl);
+    }
+    else {
+      return '[The site URL is not set. Please set it!]';
+    }
+  }
+  /* eslint-enable no-else-return */
+
+  /**
+   * Execute only once for installing application
+   */
+  async initDB(globalLang) {
+    const initialConfig = this.configManager.configModel.getConfigsObjectForInstalling();
+    initialConfig['app:globalLang'] = globalLang;
+    await this.configManager.updateConfigsInTheSameNamespace('crowi', initialConfig);
+  }
+
+  async isDBInitialized() {
+    const appInstalled = await this.configManager.getConfigFromDB('crowi', 'app:installed');
+    return appInstalled;
+  }
+
+}
+
+module.exports = AppService;

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

@@ -218,7 +218,7 @@ class ConfigLoader {
     // merge defaults
     let mergedConfigFromDB = Object.assign({ crowi: this.configModel.getDefaultCrowiConfigsObject() }, configFromDB);
     mergedConfigFromDB = Object.assign({ markdown: this.configModel.getDefaultMarkdownConfigsObject() }, mergedConfigFromDB);
-
+    mergedConfigFromDB = Object.assign({ notification: this.configModel.getDefaultNotificationConfigsObject() }, mergedConfigFromDB);
 
     // In getConfig API, only null is used as a value to indicate that a config is not set.
     // So, if a value loaded from the database is emtpy,

+ 86 - 28
src/server/service/config-manager.js

@@ -1,5 +1,4 @@
-const debug = require('debug')('growi:service:ConfigManager');
-const pathUtils = require('growi-commons').pathUtils;
+const logger = require('@alias/logger')('growi:service:ConfigManager');
 const ConfigLoader = require('../service/config-loader');
 
 const KEYS_FOR_SAML_USE_ONLY_ENV_OPTION = [
@@ -20,6 +19,9 @@ class ConfigManager {
     this.configModel = configModel;
     this.configLoader = new ConfigLoader(this.configModel);
     this.configObject = null;
+    this.configKeys = [];
+
+    this.getConfig = this.getConfig.bind(this);
   }
 
   /**
@@ -27,8 +29,10 @@ class ConfigManager {
    */
   async loadConfigs() {
     this.configObject = await this.configLoader.load();
+    logger.debug('ConfigManager#loadConfigs', this.configObject);
 
-    debug('ConfigManager#loadConfigs', this.configObject);
+    // cache all config keys
+    this.reloadConfigKeys();
   }
 
   /**
@@ -44,11 +48,77 @@ class ConfigManager {
    * - undefined: a specified config does not exist.
    */
   getConfig(namespace, key) {
+    let value;
+
     if (this.searchOnlyFromEnvVarConfigs('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions')) {
-      return this.searchInSAMLUseOnlyEnvMode(namespace, key);
+      value = this.searchInSAMLUseOnlyEnvMode(namespace, key);
+    }
+
+    value = this.defaultSearch(namespace, key);
+
+    logger.debug(key, value);
+    return value;
+  }
+
+  /**
+   * get a config specified by namespace and regular expresssion
+   */
+  getConfigByRegExp(namespace, regexp) {
+    const result = {};
+
+    for (const key of this.configKeys) {
+      if (regexp.test(key)) {
+        result[key] = this.getConfig(namespace, key);
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * get a config specified by namespace and prefix
+   */
+  getConfigByPrefix(namespace, prefix) {
+    const regexp = new RegExp(`^${prefix}`);
+
+    return this.getConfigByRegExp(namespace, regexp);
+  }
+
+  /**
+   * generate an array of config keys from this.configObject
+   */
+  getConfigKeys() {
+    // type: fromDB, fromEnvVars
+    const types = Object.keys(this.configObject);
+    let namespaces = [];
+    let keys = [];
+
+    for (const type of types) {
+      if (this.configObject[type] != null) {
+        // ns: crowi, markdown, notification
+        namespaces = [...namespaces, ...Object.keys(this.configObject[type])];
+      }
     }
 
-    return this.defaultSearch(namespace, key);
+    // remove duplicates
+    namespaces = [...new Set(namespaces)];
+
+    for (const type of types) {
+      for (const ns of namespaces) {
+        if (this.configObject[type][ns] != null) {
+          keys = [...keys, ...Object.keys(this.configObject[type][ns])];
+        }
+      }
+    }
+
+    // remove duplicates
+    keys = [...new Set(keys)];
+
+    return keys;
+  }
+
+  reloadConfigKeys() {
+    this.configKeys = this.getConfigKeys();
   }
 
   /**
@@ -69,27 +139,6 @@ class ConfigManager {
     return this.searchOnlyFromEnvVarConfigs(namespace, key);
   }
 
-  /**
-   * get the site url
-   *
-   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
-   *
-   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
-   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
-   * With version 3.3.5 and above, the system use only a value from the config.
-   */
-  /* eslint-disable no-else-return */
-  getSiteUrl() {
-    const siteUrl = this.getConfig('crowi', 'app:siteUrl');
-    if (siteUrl != null) {
-      return pathUtils.removeTrailingSlash(siteUrl);
-    }
-    else {
-      return '[The site URL is not set. Please set it!]';
-    }
-  }
-  /* eslint-enable no-else-return */
-
   /**
    * update configs in the same namespace
    *
@@ -122,6 +171,7 @@ class ConfigManager {
     await this.configModel.bulkWrite(queries);
 
     await this.loadConfigs();
+    this.reloadConfigKeys();
   }
 
   /*
@@ -133,27 +183,35 @@ class ConfigManager {
    * and then from configs loaded from the environment variables
    */
   defaultSearch(namespace, key) {
+    // does not exist neither in db nor in env vars
     if (!this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
+      logger.debug(`${namespace}.${key} does not exist neither in db nor in env vars`);
       return undefined;
     }
 
+    // only exists in db
     if (this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
+      logger.debug(`${namespace}.${key} only exists in db`);
       return this.configObject.fromDB[namespace][key];
     }
 
+    // only exists env vars
     if (!this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key)) {
+      logger.debug(`${namespace}.${key} only exists in env vars`);
       return this.configObject.fromEnvVars[namespace][key];
     }
 
+    // exists both in db and in env vars [db > env var]
     if (this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key)) {
-      /* eslint-disable no-else-return */
       if (this.configObject.fromDB[namespace][key] !== null) {
+        logger.debug(`${namespace}.${key} exists both in db and in env vars. loaded from db`);
         return this.configObject.fromDB[namespace][key];
       }
+      /* eslint-disable-next-line no-else-return */
       else {
+        logger.debug(`${namespace}.${key} exists both in db and in env vars. loaded from env vars`);
         return this.configObject.fromEnvVars[namespace][key];
       }
-      /* eslint-enable no-else-return */
     }
   }
 

+ 56 - 0
src/server/service/customize.js

@@ -0,0 +1,56 @@
+const logger = require('@alias/logger')('growi:service:CustomizeService'); // eslint-disable-line no-unused-vars
+
+/**
+ * the service class of CustomizeService
+ */
+class CustomizeService {
+
+  constructor(configManager, appService, xssService) {
+    this.configManager = configManager;
+    this.appService = appService;
+    this.xssService = xssService;
+  }
+
+  /**
+   * initialize custom css strings
+   */
+  initCustomCss() {
+    const uglifycss = require('uglifycss');
+
+    const rawCss = this.configManager.getConfig('crowi', 'customize:css') || '';
+
+    // uglify and store
+    this.customCss = uglifycss.processString(rawCss);
+  }
+
+  getCustomCss() {
+    return this.customCss;
+  }
+
+  getCustomScript() {
+    return this.configManager.getConfig('crowi', 'customize:script') || '';
+  }
+
+  initCustomTitle() {
+    let configValue = this.configManager.getConfig('crowi', 'customize:title');
+
+    if (configValue == null || configValue.trim().length === 0) {
+      configValue = '{{page}} - {{sitename}}';
+    }
+
+    this.customTitleTemplate = configValue;
+  }
+
+  generateCustomTitle(page) {
+    // replace
+    const customTitle = this.customTitleTemplate
+      .replace('{{sitename}}', this.appService.getAppTitle())
+      .replace('{{page}}', page);
+
+    return this.xssService.process(customTitle);
+  }
+
+
+}
+
+module.exports = CustomizeService;

+ 11 - 12
src/server/service/file-uploader/aws.js

@@ -5,24 +5,23 @@ const urljoin = require('url-join');
 const aws = require('aws-sdk');
 
 module.exports = function(crowi) {
-  const lib = {};
+  const Uploader = require('./uploader');
+  const { configManager } = crowi;
+  const lib = new Uploader(configManager);
 
   function getAwsConfig() {
-    const config = crowi.getConfig();
     return {
-      accessKeyId: config.crowi['aws:accessKeyId'],
-      secretAccessKey: config.crowi['aws:secretAccessKey'],
-      region: config.crowi['aws:region'],
-      bucket: config.crowi['aws:bucket'],
+      accessKeyId: configManager.getConfig('crowi', 'aws:accessKeyId'),
+      secretAccessKey: configManager.getConfig('crowi', 'aws:secretAccessKey'),
+      region: configManager.getConfig('crowi', 'aws:region'),
+      bucket: configManager.getConfig('crowi', 'aws:bucket'),
     };
   }
 
-  function S3Factory() {
+  function S3Factory(isUploadable) {
     const awsConfig = getAwsConfig();
-    const Config = crowi.model('Config');
-    const config = crowi.getConfig();
 
-    if (!Config.isUploadable(config)) {
+    if (!isUploadable) {
       throw new Error('AWS is not configured.');
     }
 
@@ -54,7 +53,7 @@ module.exports = function(crowi) {
   };
 
   lib.deleteFileByFilePath = async function(filePath) {
-    const s3 = S3Factory();
+    const s3 = S3Factory(this.getIsUploadable());
     const awsConfig = getAwsConfig();
 
     const params = {
@@ -68,7 +67,7 @@ module.exports = function(crowi) {
   lib.uploadFile = function(fileStream, attachment) {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
-    const s3 = S3Factory();
+    const s3 = S3Factory(this.getIsUploadable());
     const awsConfig = getAwsConfig();
 
     const filePath = getFilePathOnStorage(attachment);

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

@@ -3,7 +3,8 @@ const mongoose = require('mongoose');
 const util = require('util');
 
 module.exports = function(crowi) {
-  const lib = {};
+  const Uploader = require('./uploader');
+  const lib = new Uploader(crowi.configManager);
   const COLLECTION_NAME = 'attachmentFiles';
   const CHUNK_COLLECTION_NAME = `${COLLECTION_NAME}.chunks`;
 

+ 1 - 0
src/server/service/file-uploader/index.js

@@ -22,6 +22,7 @@ class FileUploaderFactory {
 }
 
 const factory = new FileUploaderFactory();
+
 module.exports = (crowi) => {
   return factory.getUploader(crowi);
 };

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

@@ -6,7 +6,8 @@ const mkdir = require('mkdirp');
 const streamToPromise = require('stream-to-promise');
 
 module.exports = function(crowi) {
-  const lib = {};
+  const Uploader = require('./uploader');
+  const lib = new Uploader(crowi.configManager);
   const basePath = path.posix.join(crowi.publicDir, 'uploads');
 
   function getFilePathOnStorage(attachment) {

+ 2 - 2
src/server/service/file-uploader/none.js

@@ -2,8 +2,8 @@
 
 module.exports = function(crowi) {
   const debug = require('debug')('growi:service:fileUploaderNone');
-
-  const lib = {};
+  const Uploader = require('./uploader');
+  const lib = new Uploader(crowi.configManager);
 
   lib.deleteFile = function(filePath) {
     debug(`File deletion: ${filePath}`);

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

@@ -0,0 +1,34 @@
+// file uploader virtual class
+// 各アップローダーで共通のメソッドはここで定義する
+
+class Uploader {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+  }
+
+  getIsUploadable() {
+    const method = process.env.FILE_UPLOAD || 'aws';
+
+    if (method === 'aws' && (
+      !this.configManager.getConfig('crowi', 'aws:accessKeyId')
+        || !this.configManager.getConfig('crowi', 'aws:secretAccessKey')
+        || !this.configManager.getConfig('crowi', 'aws:region')
+        || !this.configManager.getConfig('crowi', 'aws:bucket'))) {
+      return false;
+    }
+
+    return method !== 'none';
+  }
+
+  getFileUploadEnabled() {
+    if (!this.getIsUploadable()) {
+      return false;
+    }
+
+    return !!this.configManager.getConfig('crowi', 'app:fileUpload');
+  }
+
+}
+
+module.exports = Uploader;

+ 1 - 3
src/server/service/global-notification.js

@@ -7,12 +7,10 @@ class GlobalNotificationService {
 
   constructor(crowi) {
     this.crowi = crowi;
-    this.config = crowi.getConfig();
     this.mailer = crowi.getMailer();
     this.GlobalNotification = crowi.model('GlobalNotificationSetting');
     this.User = crowi.model('User');
-    this.Config = crowi.model('Config');
-    this.appTitle = this.Config.appTitle(this.config);
+    this.appTitle = crowi.appService.getAppTitle();
   }
 
   notifyByMail(notification, mailOption) {

+ 0 - 22
src/server/service/notification.js

@@ -1,22 +0,0 @@
-
-
-function Notification(crowi) {
-  this.crowi = crowi;
-  this.config = crowi.getConfig();
-}
-
-Notification.prototype.hasSlackConfig = function() {
-  if (!this.config.notification.slack) {
-    return false;
-  }
-
-  // var config = ;
-};
-
-Notification.prototype.noitfyByEmail = function() {
-};
-
-Notification.prototype.noitfyByChat = function() {
-};
-
-module.exports = Notification;

+ 99 - 50
src/server/service/passport.js

@@ -9,6 +9,7 @@ const TwitterStrategy = require('passport-twitter').Strategy;
 const OidcStrategy = require('openid-client').Strategy;
 const SamlStrategy = require('passport-saml').Strategy;
 const OIDCIssuer = require('openid-client').Issuer;
+const BasicStrategy = require('passport-http').BasicStrategy;
 
 /**
  * the service class of Passport
@@ -58,6 +59,11 @@ class PassportService {
      */
     this.isSamlStrategySetup = false;
 
+    /**
+     * the flag whether BasicStrategy is set up successfully
+     */
+    this.isBasicStrategySetup = false;
+
     /**
      * the flag whether serializer/deserializer are set up successfully
      */
@@ -148,8 +154,9 @@ class PassportService {
     }
 
     const config = this.crowi.config;
-    const Config = this.crowi.model('Config');
-    const isLdapEnabled = Config.isEnabledPassportLdap(config);
+    const { configManager } = this.crowi;
+
+    const isLdapEnabled = configManager.getConfig('crowi', 'security:passport-ldap:isEnabled');
 
     // when disabled
     if (!isLdapEnabled) {
@@ -179,8 +186,7 @@ class PassportService {
    * @memberof PassportService
    */
   getLdapAttrNameMappedToUsername() {
-    const config = this.crowi.config;
-    return config.crowi['security:passport-ldap:attrMapUsername'] || 'uid';
+    return this.crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapUsername') || 'uid';
   }
 
   /**
@@ -190,8 +196,7 @@ class PassportService {
    * @memberof PassportService
    */
   getLdapAttrNameMappedToName() {
-    const config = this.crowi.config;
-    return config.crowi['security:passport-ldap:attrMapName'] || '';
+    return this.crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapName') || '';
   }
 
   /**
@@ -201,8 +206,7 @@ class PassportService {
    * @memberof PassportService
    */
   getLdapAttrNameMappedToMail() {
-    const config = this.crowi.config;
-    return config.crowi['security:passport-ldap:attrMapMail'] || 'mail';
+    return this.crowi.configManager.getConfig('crowi', 'security:passport-ldap:attrMapMail') || 'mail';
   }
 
   /**
@@ -227,16 +231,17 @@ class PassportService {
    */
   getLdapConfigurationFunc(config, opts) {
     /* eslint-disable no-multi-spaces */
+    const { configManager } = this.crowi;
 
     // get configurations
-    const isUserBind          = config.crowi['security:passport-ldap:isUserBind'];
-    const serverUrl           = config.crowi['security:passport-ldap:serverUrl'];
-    const bindDN              = config.crowi['security:passport-ldap:bindDN'];
-    const bindCredentials     = config.crowi['security:passport-ldap:bindDNPassword'];
-    const searchFilter        = config.crowi['security:passport-ldap:searchFilter'] || '(uid={{username}})';
-    const groupSearchBase     = config.crowi['security:passport-ldap:groupSearchBase'];
-    const groupSearchFilter   = config.crowi['security:passport-ldap:groupSearchFilter'];
-    const groupDnProperty     = config.crowi['security:passport-ldap:groupDnProperty'] || 'uid';
+    const isUserBind          = configManager.getConfig('crowi', 'security:passport-ldap:isUserBind');
+    const serverUrl           = configManager.getConfig('crowi', 'security:passport-ldap:serverUrl');
+    const bindDN              = configManager.getConfig('crowi', 'security:passport-ldap:bindDN');
+    const bindCredentials     = configManager.getConfig('crowi', 'security:passport-ldap:bindDNPassword');
+    const searchFilter        = configManager.getConfig('crowi', 'security:passport-ldap:searchFilter') || '(uid={{username}})';
+    const groupSearchBase     = configManager.getConfig('crowi', 'security:passport-ldap:groupSearchBase');
+    const groupSearchFilter   = configManager.getConfig('crowi', 'security:passport-ldap:groupSearchFilter');
+    const groupDnProperty     = configManager.getConfig('crowi', 'security:passport-ldap:groupDnProperty') || 'uid';
     /* eslint-enable no-multi-spaces */
 
     // parse serverUrl
@@ -314,9 +319,8 @@ class PassportService {
       throw new Error('GoogleStrategy has already been set up');
     }
 
-    const config = this.crowi.config;
-    const Config = this.crowi.model('Config');
-    const isGoogleEnabled = Config.isEnabledPassportGoogle(config);
+    const { configManager } = this.crowi;
+    const isGoogleEnabled = configManager.getConfig('crowi', 'security:passport-google:isEnabled');
 
     // when disabled
     if (!isGoogleEnabled) {
@@ -327,11 +331,11 @@ class PassportService {
     passport.use(
       new GoogleStrategy(
         {
-          clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
-          clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
-          callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-            ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
-            : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
+          clientId: configManager.getConfig('crowi', 'security:passport-google:clientId'),
+          clientSecret: configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
+          callbackURL: (this.crowi.appService.getSiteUrl() != null)
+            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
+            : configManager.getConfig('crowi', 'security:passport-google:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           skipUserProfile: false,
         },
         (accessToken, refreshToken, profile, done) => {
@@ -365,9 +369,8 @@ class PassportService {
       throw new Error('GitHubStrategy has already been set up');
     }
 
-    const config = this.crowi.config;
-    const Config = this.crowi.model('Config');
-    const isGitHubEnabled = Config.isEnabledPassportGitHub(config);
+    const { configManager } = this.crowi;
+    const isGitHubEnabled = configManager.getConfig('crowi', 'security:passport-github:isEnabled');
 
     // when disabled
     if (!isGitHubEnabled) {
@@ -378,11 +381,11 @@ class PassportService {
     passport.use(
       new GitHubStrategy(
         {
-          clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
-          clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
-          callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-            ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/github/callback') // auto-generated with v3.2.4 and above
-            : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
+          clientID: configManager.getConfig('crowi', 'security:passport-github:clientId'),
+          clientSecret: configManager.getConfig('crowi', 'security:passport-github:clientSecret'),
+          callbackURL: (this.crowi.appService.getSiteUrl() != null)
+            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/github/callback') // auto-generated with v3.2.4 and above
+            : configManager.getConfig('crowi', 'security:passport-github:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           skipUserProfile: false,
         },
         (accessToken, refreshToken, profile, done) => {
@@ -416,9 +419,8 @@ class PassportService {
       throw new Error('TwitterStrategy has already been set up');
     }
 
-    const config = this.crowi.config;
-    const Config = this.crowi.model('Config');
-    const isTwitterEnabled = Config.isEnabledPassportTwitter(config);
+    const { configManager } = this.crowi;
+    const isTwitterEnabled = configManager.getConfig('crowi', 'security:passport-twitter:isEnabled');
 
     // when disabled
     if (!isTwitterEnabled) {
@@ -429,11 +431,11 @@ class PassportService {
     passport.use(
       new TwitterStrategy(
         {
-          consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
-          consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
-          callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-            ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/twitter/callback') // auto-generated with v3.2.4 and above
-            : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI, // DEPRECATED: backward compatible with v3.2.3 and below
+          consumerKey: configManager.getConfig('crowi', 'security:passport-twitter:consumerKey'),
+          consumerSecret: configManager.getConfig('crowi', 'security:passport-twitter:consumerSecret'),
+          callbackURL: (this.crowi.appService.getSiteUrl() != null)
+            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/twitter/callback') // auto-generated with v3.2.4 and above
+            : configManager.getConfig('crowi', 'security:passport-twitter:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           skipUserProfile: false,
         },
         (accessToken, refreshToken, profile, done) => {
@@ -467,8 +469,7 @@ class PassportService {
       throw new Error('OidcStrategy has already been set up');
     }
 
-    const config = this.crowi.config;
-    const configManager = this.crowi.configManager;
+    const { configManager } = this.crowi;
     const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
 
     // when disabled
@@ -481,12 +482,12 @@ class PassportService {
     // setup client
     // extend oidc request timeouts
     OIDCIssuer.defaultHttpOptions = { timeout: 5000 };
-    const issuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost') || process.env.OAUTH_OIDC_ISSUER_HOST;
-    const clientId = configManager.getConfig('crowi', 'security:passport-oidc:clientId') || process.env.OAUTH_OIDC_CLIENT_ID;
-    const clientSecret = configManager.getConfig('crowi', 'security:passport-oidc:clientSecret') || process.env.OAUTH_OIDC_CLIENT_SECRET;
+    const issuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
+    const clientId = configManager.getConfig('crowi', 'security:passport-oidc:clientId');
+    const clientSecret = configManager.getConfig('crowi', 'security:passport-oidc:clientSecret');
     const redirectUri = (configManager.getConfig('crowi', 'app:siteUrl') != null)
-      ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/oidc/callback')
-      : config.crowi['security:passport-oidc:callbackUrl'] || process.env.OAUTH_OIDC_CALLBACK_URI; // DEPRECATED: backward compatible with v3.2.3 and below
+      ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/oidc/callback')
+      : configManager.getConfig('crowi', 'security:passport-oidc:callbackUrl'); // DEPRECATED: backward compatible with v3.2.3 and below
     const oidcIssuer = await OIDCIssuer.discover(issuerHost);
     debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
@@ -531,7 +532,7 @@ class PassportService {
       throw new Error('SamlStrategy has already been set up');
     }
 
-    const configManager = this.crowi.configManager;
+    const { configManager } = this.crowi;
     const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
 
     // when disabled
@@ -544,8 +545,8 @@ class PassportService {
       new SamlStrategy(
         {
           entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
-          callbackUrl: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-            ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/saml/callback') // auto-generated with v3.2.4 and above
+          callbackUrl: (this.crowi.appService.getSiteUrl() != null)
+            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/saml/callback') // auto-generated with v3.2.4 and above
             : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
           cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),
@@ -588,6 +589,54 @@ class PassportService {
     return missingRequireds;
   }
 
+  /**
+   * reset BasicStrategy
+   *
+   * @memberof PassportService
+   */
+  resetBasicStrategy() {
+    debug('BasicStrategy: reset');
+    passport.unuse('basic');
+    this.isBasicStrategySetup = false;
+  }
+
+  /**
+   * setup BasicStrategy
+   *
+   * @memberof PassportService
+   */
+  setupBasicStrategy() {
+    // check whether the strategy has already been set up
+    if (this.isBasicStrategySetup) {
+      throw new Error('BasicStrategy has already been set up');
+    }
+
+    const configManager = this.crowi.configManager;
+    const isBasicEnabled = configManager.getConfig('crowi', 'security:passport-basic:isEnabled');
+
+    // when disabled
+    if (!isBasicEnabled) {
+      return;
+    }
+
+    debug('BasicStrategy: setting up..');
+
+    const configId = configManager.getConfig('crowi', 'security:passport-basic:id');
+    const configPassword = configManager.getConfig('crowi', 'security:passport-basic:password');
+
+    passport.use(new BasicStrategy(
+      (userId, password, done) => {
+        if (userId !== configId || password !== configPassword) {
+          return done(null, false, { message: 'Incorrect credentials.' });
+        }
+        return done(null, userId);
+      },
+    ));
+
+    this.isBasicStrategySetup = true;
+    debug('BasicStrategy: setup is done');
+  }
+
   /**
    * setup serializer and deserializer
    *

+ 5 - 5
src/server/service/rest-qiita-API.js

@@ -19,9 +19,9 @@ class RestQiitaAPIService {
 
   constructor(crowi) {
     this.crowi = crowi;
-    this.config = crowi.getConfig();
-    this.team = this.config.crowi['importer:qiita:team_name'];
-    this.token = this.config.crowi['importer:qiita:access_token'];
+    this.configManager = crowi.configManager;
+    this.team = this.configManager.getConfig('crowi', 'importer:qiita:team_name');
+    this.token = this.configManager.getConfig('crowi', 'importer:qiita:access_token');
     this.axios = getAxios(this.team, this.token);
   }
 
@@ -31,8 +31,8 @@ class RestQiitaAPIService {
    * @param {string} token
    */
   async reset() {
-    this.team = this.config.crowi['importer:qiita:team_name'];
-    this.token = this.config.crowi['importer:qiita:access_token'];
+    this.team = this.configManager.getConfig('crowi', 'importer:qiita:team_name');
+    this.token = this.configManager.getConfig('crowi', 'importer:qiita:access_token');
     this.axios = getAxios(this.team, this.token);
   }
 

+ 20 - 0
src/server/service/slack-notification.js

@@ -0,0 +1,20 @@
+const logger = require('@alias/logger')('growi:service:SlackNotification'); // eslint-disable-line no-unused-vars
+/**
+ * the service class of SlackNotificationService
+ */
+class SlackNotificationService {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+  }
+
+  hasSlackConfig() {
+    const hasSlackToken = !!this.configManager.getConfig('notification', 'slack:token');
+    const hasSlackIwhUrl = !!this.configManager.getConfig('notification', 'slack:incomingWebhookUrl');
+
+    return hasSlackToken || hasSlackIwhUrl;
+  }
+
+}
+
+module.exports = SlackNotificationService;

+ 23 - 0
src/server/service/user-group.js

@@ -0,0 +1,23 @@
+const logger = require('@alias/logger')('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
+
+const mongoose = require('mongoose');
+
+const UserGroupRelation = mongoose.model('UserGroupRelation');
+
+/**
+ * the service class of UserGroupService
+ */
+class UserGroupService {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+  }
+
+  async init() {
+    logger.debug('removing all invalid relations');
+    return UserGroupRelation.removeAllInvalidRelations();
+  }
+
+}
+
+module.exports = UserGroupService;

+ 71 - 0
src/server/service/xss.js

@@ -0,0 +1,71 @@
+const logger = require('@alias/logger')('growi:service:XssSerivce'); // eslint-disable-line no-unused-vars
+
+const Xss = require('@commons/service/xss');
+const { tags, attrs } = require('@commons/service/xss/recommended-whitelist');
+
+/**
+ * the service class of XssSerivce
+ */
+class XssSerivce {
+
+  constructor(configManager) {
+    this.configManager = configManager;
+
+    this.xss = new Xss();
+  }
+
+  process(value) {
+    return this.xss.process(value);
+  }
+
+  getTagWhiteList() {
+    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
+    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
+
+    if (isEnabledXssPrevention) {
+      switch (xssOpiton) {
+        case 1: // ignore all: use default option
+          return [];
+
+        case 2: // recommended
+          return tags;
+
+        case 3: // custom white list
+          return this.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList');
+
+        default:
+          return [];
+      }
+    }
+    else {
+      return [];
+    }
+  }
+
+  getAttrWhiteList() {
+    const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
+    const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
+
+    if (isEnabledXssPrevention) {
+      switch (xssOpiton) {
+        case 1: // ignore all: use default option
+          return [];
+
+        case 2: // recommended
+          return attrs;
+
+        case 3: // custom white list
+          return this.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList');
+
+        default:
+          return [];
+      }
+    }
+    else {
+      return [];
+    }
+  }
+
+}
+
+module.exports = XssSerivce;

+ 0 - 70
src/server/util/googleAuth.js

@@ -1,70 +0,0 @@
-const debug = require('debug')('growi:lib:googleAuth');
-const urljoin = require('url-join');
-const { GoogleApis } = require('googleapis');
-
-/**
- * googleAuth utility
- */
-
-module.exports = function(crowi) {
-  const google = new GoogleApis();
-  const config = crowi.getConfig();
-
-  const lib = {};
-  function createOauth2Client(url) {
-    return new google.auth.OAuth2(
-      config.crowi['google:clientId'],
-      config.crowi['google:clientSecret'],
-      url,
-    );
-  }
-
-  lib.createAuthUrl = function(req, callback) {
-    const callbackUrl = urljoin(crowi.configManager.getSiteUrl(), '/google/callback');
-    const oauth2Client = createOauth2Client(callbackUrl);
-    google.options({ auth: oauth2Client });
-
-    const redirectUrl = oauth2Client.generateAuthUrl({
-      access_type: 'offline',
-      scope: ['profile', 'email'],
-    });
-
-    callback(null, redirectUrl);
-  };
-
-  lib.handleCallback = function(req, callback) {
-    const callbackUrl = urljoin(crowi.configManager.getSiteUrl(), '/google/callback');
-    const oauth2Client = createOauth2Client(callbackUrl);
-    google.options({ auth: oauth2Client });
-
-    const code = req.session.googleAuthCode || null;
-
-    if (!code) {
-      return callback(new Error('No code exists.'), null);
-    }
-
-    debug('Request googleToken by auth code', code);
-    oauth2Client.getToken(code, (err, tokens) => {
-      debug('Result of google.getToken()', err, tokens);
-      if (err) {
-        return callback(new Error('[googleAuth.handleCallback] Error to get token.'), null);
-      }
-
-      oauth2Client.credentials = tokens;
-
-      const oauth2 = google.oauth2('v2');
-      oauth2.userinfo.get({}, (err, response) => {
-        debug('Response of oauth2.userinfo.get', err, response);
-        if (err) {
-          return callback(new Error('[googleAuth.handleCallback] Error while proceccing userinfo.get.'), null);
-        }
-
-        const data = response.data;
-        data.user_id = data.id; // This is for B.C. (tokeninfo をつかっている前提のコードに対してのもの)
-        return callback(null, data);
-      });
-    });
-  };
-
-  return lib;
-};

+ 5 - 3
src/server/util/importer.js

@@ -7,10 +7,12 @@
 module.exports = (crowi) => {
   const logger = require('@alias/logger')('growi:util:importer');
   const esa = require('esa-nodejs');
-  const config = crowi.getConfig();
   const createGrowiPages = require('./createGrowiPagesFromImports')(crowi);
   const restQiitaAPIService = crowi.getRestQiitaAPIService();
 
+  const configManager = crowi.configManager;
+  const getConfig = configManager.getConfig;
+
   const importer = {};
   let esaClient = {};
 
@@ -19,8 +21,8 @@ module.exports = (crowi) => {
    */
   importer.initializeEsaClient = () => {
     esaClient = esa({
-      team:        config.crowi['importer:esa:team_name'],
-      accessToken: config.crowi['importer:esa:access_token'],
+      team:        getConfig('crowi', 'importer:esa:team_name'),
+      accessToken: getConfig('crowi', 'importer:esa:access_token'),
     });
     logger.debug('initialize esa importer');
   };

+ 15 - 15
src/server/util/mailer.js

@@ -6,24 +6,24 @@ module.exports = function(crowi) {
   const debug = require('debug')('growi:lib:mailer');
   const nodemailer = require('nodemailer');
   const swig = require('swig-templates');
-  const Config = crowi.model('Config');
-  const config = crowi.getConfig();
-  const mailConfig = {};
 
+  const { configManager, appService } = crowi;
+
+  const mailConfig = {};
   let mailer = {};
 
   function createSMTPClient(option) {
     debug('createSMTPClient option', option);
     if (!option) {
       option = { // eslint-disable-line no-param-reassign
-        host: config.crowi['mail:smtpHost'],
-        port: config.crowi['mail:smtpPort'],
+        host: configManager.getConfig('crowi', 'mail:smtpHost'),
+        port: configManager.getConfig('crowi', 'mail:smtpPort'),
       };
 
-      if (config.crowi['mail:smtpUser'] && config.crowi['mail:smtpPassword']) {
+      if (configManager.getConfig('crowi', 'mail:smtpUser') && configManager.getConfig('crowi', 'mail:smtpPassword')) {
         option.auth = {
-          user: config.crowi['mail:smtpUser'],
-          pass: config.crowi['mail:smtpPassword'],
+          user: configManager.getConfig('crowi', 'mail:smtpUser'),
+          pass: configManager.getConfig('crowi', 'mail:smtpPassword'),
         };
       }
       if (option.port === 465) {
@@ -41,8 +41,8 @@ module.exports = function(crowi) {
   function createSESClient(option) {
     if (!option) {
       option = { // eslint-disable-line no-param-reassign
-        accessKeyId: config.crowi['aws:accessKeyId'],
-        secretAccessKey: config.crowi['aws:secretAccessKey'],
+        accessKeyId: configManager.getConfig('crowi', 'aws:accessKeyId'),
+        secretAccessKey: configManager.getConfig('crowi', 'aws:secretAccessKey'),
       };
     }
 
@@ -54,17 +54,17 @@ module.exports = function(crowi) {
   }
 
   function initialize() {
-    if (!config.crowi['mail:from']) {
+    if (!configManager.getConfig('crowi', 'mail:from')) {
       mailer = undefined;
       return;
     }
 
-    if (config.crowi['mail:smtpHost'] && config.crowi['mail:smtpPort']
+    if (configManager.getConfig('crowi', 'mail:smtpHost') && configManager.getConfig('crowi', 'mail:smtpPort')
     ) {
       // SMTP 設定がある場合はそれを優先
       mailer = createSMTPClient();
     }
-    else if (config.crowi['aws:accessKeyId'] && config.crowi['aws:secretAccessKey']) {
+    else if (configManager.getConfig('crowi', 'aws:accessKeyId') && configManager.getConfig('crowi', 'aws:secretAccessKey')) {
       // AWS 設定がある場合はSESを設定
       mailer = createSESClient();
     }
@@ -72,8 +72,8 @@ module.exports = function(crowi) {
       mailer = undefined;
     }
 
-    mailConfig.from = config.crowi['mail:from'];
-    mailConfig.subject = `${Config.appTitle(config)}からのメール`;
+    mailConfig.from = configManager.getConfig('crowi', 'mail:from');
+    mailConfig.subject = `${appService.getAppTitle()}からのメール`;
 
     debug('mailer initialized');
   }

+ 171 - 204
src/server/util/middlewares.js

@@ -4,50 +4,29 @@ const pathUtils = require('growi-commons').pathUtils;
 const md5 = require('md5');
 const entities = require('entities');
 
+module.exports = (crowi, app) => {
+  const { configManager, appService } = crowi;
 
-exports.csrfKeyGenerator = function(crowi, app) {
-  return function(req, res, next) {
-    const csrfKey = (req.session && req.session.id) || 'anon';
-
-    if (req.csrfToken === null) {
-      req.csrfToken = crowi.getTokens().create(csrfKey);
-    }
-
-    next();
-  };
-};
+  const middlewares = {};
 
-exports.loginChecker = function(crowi, app) {
-  const User = crowi.model('User');
-  return async function(req, res, next) {
-    let user = null;
+  middlewares.csrfKeyGenerator = function() {
+    return function(req, res, next) {
+      const csrfKey = (req.session && req.session.id) || 'anon';
 
-    try {
-      // session に user object が入ってる
-      if (req.session.user && '_id' in req.session.user) {
-        user = await User.findById(req.session.user._id).populate(User.IMAGE_POPULATION);
+      if (req.csrfToken === null) {
+        req.csrfToken = crowi.getTokens().create(csrfKey);
       }
 
-      req.user = user;
-      req.session.user = user;
-      res.locals.user = req.user;
       next();
-    }
-    catch (err) {
-      next(err);
-    }
+    };
   };
-};
 
-exports.loginCheckerForPassport = function(crowi, app) {
-  return function(req, res, next) {
+  middlewares.loginCheckerForPassport = function(req, res, next) {
     res.locals.user = req.user;
     next();
   };
-};
 
-exports.csrfVerify = function(crowi, app) {
-  return function(req, res, next) {
+  middlewares.csrfVerify = function(req, res, next) {
     const token = req.body._csrf || req.query._csrf || null;
     const csrfKey = (req.session && req.session.id) || 'anon';
 
@@ -65,134 +44,132 @@ exports.csrfVerify = function(crowi, app) {
     logger.warn('csrf verification failed. return 403', csrfKey, token);
     return res.sendStatus(403);
   };
-};
 
-exports.swigFunctions = function(crowi, app) {
-  return function(req, res, next) {
-    require('../util/swigFunctions')(crowi, app, req, res.locals);
-    next();
+  middlewares.swigFunctions = function() {
+    return function(req, res, next) {
+      require('../util/swigFunctions')(crowi, app, req, res.locals);
+      next();
+    };
   };
-};
 
-exports.swigFilters = function(crowi, app, swig) {
-  // define a function for Gravatar
-  const generateGravatarSrc = function(user) {
-    const email = user.email || '';
-    const hash = md5(email.trim().toLowerCase());
-    return `https://gravatar.com/avatar/${hash}`;
-  };
+  middlewares.swigFilters = function(swig) {
+    // define a function for Gravatar
+    const generateGravatarSrc = function(user) {
+      const email = user.email || '';
+      const hash = md5(email.trim().toLowerCase());
+      return `https://gravatar.com/avatar/${hash}`;
+    };
+
+    // define a function for uploaded picture
+    const getUploadedPictureSrc = function(user) {
+      if (user.image) {
+        return user.image;
+      }
+      if (user.imageAttachment != null) {
+        return user.imageAttachment.filePathProxied;
+      }
 
-  // define a function for uploaded picture
-  const getUploadedPictureSrc = function(user) {
-    if (user.image) {
-      return user.image;
-    }
-    if (user.imageAttachment != null) {
-      return user.imageAttachment.filePathProxied;
-    }
+      return '/images/icons/user.svg';
+    };
 
-    return '/images/icons/user.svg';
-  };
 
+    return function(req, res, next) {
+      swig.setFilter('path2name', (string) => {
+        const name = string.replace(/(\/)$/, '');
 
-  return function(req, res, next) {
-    swig.setFilter('path2name', (string) => {
-      const name = string.replace(/(\/)$/, '');
+        if (name.match(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/)) { // /.../hoge/YYYY/MM/DD 形式のページ
+          return name.replace(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/, '$1');
+        }
+        if (name.match(/.+\/([^/]+\/\d{4}\/\d{2})$/)) { // /.../hoge/YYYY/MM 形式のページ
+          return name.replace(/.+\/([^/]+\/\d{4}\/\d{2})$/, '$1');
+        }
+        if (name.match(/.+\/([^/]+\/\d{4})$/)) { // /.../hoge/YYYY 形式のページ
+          return name.replace(/.+\/([^/]+\/\d{4})$/, '$1');
+        }
 
-      if (name.match(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/)) { // /.../hoge/YYYY/MM/DD 形式のページ
-        return name.replace(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/, '$1');
-      }
-      if (name.match(/.+\/([^/]+\/\d{4}\/\d{2})$/)) { // /.../hoge/YYYY/MM 形式のページ
-        return name.replace(/.+\/([^/]+\/\d{4}\/\d{2})$/, '$1');
-      }
-      if (name.match(/.+\/([^/]+\/\d{4})$/)) { // /.../hoge/YYYY 形式のページ
-        return name.replace(/.+\/([^/]+\/\d{4})$/, '$1');
-      }
+        return name.replace(/.+\/(.+)?$/, '$1'); // ページの末尾を拾う
+      });
 
-      return name.replace(/.+\/(.+)?$/, '$1'); // ページの末尾を拾う
-    });
-
-    swig.setFilter('normalizeDateInPath', (path) => {
-      const patterns = [
-        [/20(\d{2})(\d{2})(\d{2})(.+)/g, '20$1/$2/$3/$4'],
-        [/20(\d{2})(\d{2})(\d{2})/g, '20$1/$2/$3'],
-        [/20(\d{2})(\d{2})(.+)/g, '20$1/$2/$3'],
-        [/20(\d{2})(\d{2})/g, '20$1/$2'],
-        [/20(\d{2})_(\d{1,2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3/$4'],
-        [/20(\d{2})_(\d{1,2})_(\d{1,2})/g, '20$1/$2/$3'],
-        [/20(\d{2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3'],
-        [/20(\d{2})_(\d{1,2})/g, '20$1/$2'],
-      ];
-
-      for (let i = 0; i < patterns.length; i++) {
-        const mat = patterns[i][0];
-        const rep = patterns[i][1];
-        if (path.match(mat)) {
-          return path.replace(mat, rep);
+      swig.setFilter('normalizeDateInPath', (path) => {
+        const patterns = [
+          [/20(\d{2})(\d{2})(\d{2})(.+)/g, '20$1/$2/$3/$4'],
+          [/20(\d{2})(\d{2})(\d{2})/g, '20$1/$2/$3'],
+          [/20(\d{2})(\d{2})(.+)/g, '20$1/$2/$3'],
+          [/20(\d{2})(\d{2})/g, '20$1/$2'],
+          [/20(\d{2})_(\d{1,2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3/$4'],
+          [/20(\d{2})_(\d{1,2})_(\d{1,2})/g, '20$1/$2/$3'],
+          [/20(\d{2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3'],
+          [/20(\d{2})_(\d{1,2})/g, '20$1/$2'],
+        ];
+
+        for (let i = 0; i < patterns.length; i++) {
+          const mat = patterns[i][0];
+          const rep = patterns[i][1];
+          if (path.match(mat)) {
+            return path.replace(mat, rep);
+          }
         }
-      }
 
-      return path;
-    });
+        return path;
+      });
 
-    swig.setFilter('datetz', (input, format) => {
-      // timezone
-      const swigFilters = require('swig-templates/lib/filters');
-      return swigFilters.date(input, format, app.get('tzoffset'));
-    });
+      swig.setFilter('datetz', (input, format) => {
+        // timezone
+        const swigFilters = require('swig-templates/lib/filters');
+        return swigFilters.date(input, format, app.get('tzoffset'));
+      });
 
-    swig.setFilter('nl2br', (string) => {
-      return string
-        .replace(/\n/g, '<br>');
-    });
+      swig.setFilter('nl2br', (string) => {
+        return string
+          .replace(/\n/g, '<br>');
+      });
 
-    swig.setFilter('removeTrailingSlash', (string) => {
-      return pathUtils.removeTrailingSlash(string);
-    });
+      swig.setFilter('removeTrailingSlash', (string) => {
+        return pathUtils.removeTrailingSlash(string);
+      });
 
-    swig.setFilter('addTrailingSlash', (string) => {
-      return pathUtils.addTrailingSlash(string);
-    });
+      swig.setFilter('addTrailingSlash', (string) => {
+        return pathUtils.addTrailingSlash(string);
+      });
 
-    swig.setFilter('presentation', (string) => {
-      // 手抜き
-      return string
-        .replace(/\s(https?.+(jpe?g|png|gif))\s/, '\n\n\n![]($1)\n\n\n');
-    });
+      swig.setFilter('presentation', (string) => {
+        // 手抜き
+        return string
+          .replace(/\s(https?.+(jpe?g|png|gif))\s/, '\n\n\n![]($1)\n\n\n');
+      });
 
-    swig.setFilter('gravatar', generateGravatarSrc);
-    swig.setFilter('uploadedpicture', getUploadedPictureSrc);
+      swig.setFilter('gravatar', generateGravatarSrc);
+      swig.setFilter('uploadedpicture', getUploadedPictureSrc);
 
-    swig.setFilter('picture', (user) => {
-      if (!user) {
-        return '/images/icons/user.svg';
-      }
+      swig.setFilter('picture', (user) => {
+        if (!user) {
+          return '/images/icons/user.svg';
+        }
 
-      if (user.isGravatarEnabled === true) {
-        return generateGravatarSrc(user);
-      }
+        if (user.isGravatarEnabled === true) {
+          return generateGravatarSrc(user);
+        }
 
-      return getUploadedPictureSrc(user);
-    });
+        return getUploadedPictureSrc(user);
+      });
 
-    swig.setFilter('encodeHTML', (string) => {
-      return entities.encodeHTML(string);
-    });
+      swig.setFilter('encodeHTML', (string) => {
+        return entities.encodeHTML(string);
+      });
 
-    swig.setFilter('preventXss', (string) => {
-      return crowi.xss.process(string);
-    });
+      swig.setFilter('preventXss', (string) => {
+        return crowi.xss.process(string);
+      });
 
-    swig.setFilter('slice', (list, start, end) => {
-      return list.slice(start, end);
-    });
+      swig.setFilter('slice', (list, start, end) => {
+        return list.slice(start, end);
+      });
 
-    next();
+      next();
+    };
   };
-};
 
-exports.adminRequired = function() {
-  return function(req, res, next) {
+  middlewares.adminRequired = function(req, res, next) {
     // check the user logged in
     //  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) {
@@ -204,61 +181,54 @@ exports.adminRequired = function() {
     }
     return res.redirect('/login');
   };
-};
 
-/**
- * require login handler
- *
- * @param {any} crowi
- * @param {any} app
- * @param {boolean} isStrictly whethere strictly restricted (default true)
- */
-exports.loginRequired = function(crowi, app, isStrictly = true) {
-  return function(req, res, next) {
-    const User = crowi.model('User');
-
-    // when the route is not strictly restricted
-    if (!isStrictly) {
-      const config = req.config;
-      const Config = crowi.model('Config');
-
-      // when allowed to read
-      if (Config.isGuestAllowedToRead(config)) {
-        return next();
+  /**
+   * require login handler
+   *
+   * @param {boolean} isStrictly whethere strictly restricted (default true)
+   */
+  middlewares.loginRequired = function(isStrictly = true) {
+    return function(req, res, next) {
+      const User = crowi.model('User');
+
+      // when the route is not strictly restricted
+      if (!isStrictly) {
+        // when allowed to read
+        if (crowi.aclService.getIsGuestAllowedToRead()) {
+          return next();
+        }
       }
-    }
 
-    // check the user logged in
-    //  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.status === User.STATUS_ACTIVE) {
-        // Active の人だけ先に進める
-        return next();
-      }
-      if (req.user.status === User.STATUS_REGISTERED) {
-        return res.redirect('/login/error/registered');
-      }
-      if (req.user.status === User.STATUS_SUSPENDED) {
-        return res.redirect('/login/error/suspended');
-      }
-      if (req.user.status === User.STATUS_INVITED) {
-        return res.redirect('/login/invited');
+      // check the user logged in
+      //  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.status === User.STATUS_ACTIVE) {
+          // Active の人だけ先に進める
+          return next();
+        }
+        if (req.user.status === User.STATUS_REGISTERED) {
+          return res.redirect('/login/error/registered');
+        }
+        if (req.user.status === User.STATUS_SUSPENDED) {
+          return res.redirect('/login/error/suspended');
+        }
+        if (req.user.status === User.STATUS_INVITED) {
+          return res.redirect('/login/invited');
+        }
       }
-    }
 
-    // is api path
-    const path = req.path || '';
-    if (path.match(/^\/_api\/.+$/)) {
-      return res.sendStatus(403);
-    }
+      // is api path
+      const path = req.path || '';
+      if (path.match(/^\/_api\/.+$/)) {
+        return res.sendStatus(403);
+      }
 
-    req.session.jumpTo = req.originalUrl;
-    return res.redirect('/login');
+      req.session.jumpTo = req.originalUrl;
+      return res.redirect('/login');
+    };
   };
-};
 
-exports.accessTokenParser = function(crowi, app) {
-  return function(req, res, next) {
+  middlewares.accessTokenParser = function(req, res, next) {
     // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
     const accessToken = req.query.access_token || req.body.access_token || null;
     if (!accessToken) {
@@ -280,45 +250,42 @@ exports.accessTokenParser = function(crowi, app) {
         next();
       });
   };
-};
 
-// this is for Installer
-exports.applicationNotInstalled = function() {
-  return function(req, res, next) {
-    const config = req.config;
+  // this is for Installer
+  middlewares.applicationNotInstalled = async function(req, res, next) {
+    const isInstalled = await appService.isDBInitialized();
 
-    if (Object.keys(config.crowi).length !== 0) {
+    if (isInstalled) {
       req.flash('errorMessage', 'Application already installed.');
       return res.redirect('admin'); // admin以外はadminRequiredで'/'にリダイレクトされる
     }
 
     return next();
   };
-};
 
-exports.applicationInstalled = function() {
-  return function(req, res, next) {
-    const config = req.config;
+  middlewares.applicationInstalled = async function(req, res, next) {
+    const isInstalled = await appService.isDBInitialized();
 
-    if (Object.keys(config.crowi).length === 0) {
+    if (!isInstalled) {
       return res.redirect('/installer');
     }
 
     return next();
   };
-};
 
-exports.awsEnabled = function() {
-  return function(req, res, next) {
-    const config = req.config;
-    if (config.crowi['aws:region'] !== ''
-        && config.crowi['aws:bucket'] !== ''
-        && config.crowi['aws:accessKeyId'] !== ''
-        && config.crowi['aws:secretAccessKey'] !== '') {
-      req.flash('globalError', 'AWS settings required to use this function. Please ask the administrator.');
-      return res.redirect('/');
-    }
+  middlewares.awsEnabled = function() {
+    return function(req, res, next) {
+      if (configManager.getConfig('crowi', 'aws:region') !== ''
+          && configManager.getConfig('crowi', 'aws:bucket') !== ''
+          && configManager.getConfig('crowi', 'aws:accessKeyId') !== ''
+          && configManager.getConfig('crowi', 'aws:secretAccessKey') !== '') {
+        req.flash('globalError', 'AWS settings required to use this function. Please ask the administrator.');
+        return res.redirect('/');
+      }
 
-    return next();
+      return next();
+    };
   };
+
+  return middlewares;
 };

+ 3 - 5
src/server/util/search.js

@@ -19,6 +19,7 @@ function SearchClient(crowi, esUri) {
   this.esUri = esUri;
   this.crowi = crowi;
   this.searchEvent = crowi.event('search');
+  this.configManager = this.crowi.configManager;
 
   // In Elasticsearch RegExp, we don't need to used ^ and $.
   // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-regexp-query.html#_standard_operators
@@ -553,11 +554,8 @@ SearchClient.prototype.appendCriteriaForQueryString = function(query, queryStrin
 };
 
 SearchClient.prototype.filterPagesByViewer = async function(query, user, userGroups) {
-  const Config = this.crowi.model('Config');
-  const config = this.crowi.getConfig();
-
-  const showPagesRestrictedByOwner = !Config.hidePagesRestrictedByOwnerInList(config);
-  const showPagesRestrictedByGroup = !Config.hidePagesRestrictedByGroupInList(config);
+  const showPagesRestrictedByOwner = !this.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+  const showPagesRestrictedByGroup = !this.configManager.getConfig('crowi', 'security:list-policy:hidePagesRestrictedByGroupInList');
 
   query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 

+ 17 - 16
src/server/util/slack.js

@@ -8,16 +8,15 @@ const urljoin = require('url-join');
 /* eslint-disable no-use-before-define */
 
 module.exports = function(crowi) {
-  const config = crowi.getConfig();
-  const Config = crowi.model('Config');
   const Slack = require('slack-node');
+  const { configManager } = crowi;
 
   const slack = {};
 
   const postWithIwh = function(messageObj) {
     return new Promise((resolve, reject) => {
       const client = new Slack();
-      client.setWebhook(config.notification['slack:incomingWebhookUrl']);
+      client.setWebhook(configManager.getConfig('notification', 'slack:incomingWebhookUrl'));
       client.webhook(messageObj, (err, res) => {
         if (err) {
           debug('Post error', err, res);
@@ -31,7 +30,7 @@ module.exports = function(crowi) {
 
   const postWithWebApi = function(messageObj) {
     return new Promise((resolve, reject) => {
-      const client = new Slack(config.notification['slack:token']);
+      const client = new Slack(configManager.getConfig('notification', 'slack:token'));
       // stringify attachments
       if (messageObj.attachments != null) {
         messageObj.attachments = JSON.stringify(messageObj.attachments);
@@ -48,7 +47,7 @@ module.exports = function(crowi) {
   };
 
   const convertMarkdownToMarkdown = function(body) {
-    const url = crowi.configManager.getSiteUrl();
+    const url = crowi.appService.getSiteUrl();
 
     return body
       .replace(/\n\*\s(.+)/g, '\n• $1')
@@ -107,7 +106,8 @@ module.exports = function(crowi) {
   };
 
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
-    const url = crowi.configManager.getSiteUrl();
+    const appTitle = crowi.appService.getAppTitle();
+    const url = crowi.appService.getSiteUrl();
     let body = page.revision.body;
 
     if (updateType === 'create') {
@@ -133,7 +133,7 @@ module.exports = function(crowi) {
 
     const message = {
       channel: `#${channel}`,
-      username: Config.appTitle(config),
+      username: appTitle,
       text: getSlackMessageTextForPage(page.path, page.id, user, updateType),
       attachments: [attachment],
     };
@@ -142,7 +142,8 @@ module.exports = function(crowi) {
   };
 
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
-    const url = crowi.configManager.getSiteUrl();
+    const appTitle = crowi.appService.getAppTitle();
+    const url = crowi.appService.getSiteUrl();
     const body = prepareAttachmentTextForComment(comment);
 
     const attachment = {
@@ -159,7 +160,7 @@ module.exports = function(crowi) {
 
     const message = {
       channel: `#${channel}`,
-      username: Config.appTitle(config),
+      username: appTitle,
       text: getSlackMessageTextForComment(path, String(comment.page), user),
       attachments: [attachment],
     };
@@ -169,7 +170,7 @@ module.exports = function(crowi) {
 
   const getSlackMessageTextForPage = function(path, pageId, user, updateType) {
     let text;
-    const url = crowi.configManager.getSiteUrl();
+    const url = crowi.appService.getSiteUrl();
 
     const pageUrl = `<${urljoin(url, pageId)}|${path}>`;
     if (updateType === 'create') {
@@ -183,7 +184,7 @@ module.exports = function(crowi) {
   };
 
   const getSlackMessageTextForComment = function(path, pageId, user) {
-    const url = crowi.configManager.getSiteUrl();
+    const url = crowi.appService.getSiteUrl();
     const pageUrl = `<${urljoin(url, pageId)}|${path}>`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 
@@ -205,23 +206,23 @@ module.exports = function(crowi) {
 
   const slackPost = (messageObj) => {
     // when incoming Webhooks is prioritized
-    if (Config.isIncomingWebhookPrioritized(config)) {
-      if (Config.hasSlackIwhUrl(config)) {
+    if (configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized')) {
+      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
         debug('posting message with IncomingWebhook');
         return postWithIwh(messageObj);
       }
-      if (Config.hasSlackToken(config)) {
+      if (configManager.getConfig('notification', 'slack:token')) {
         debug('posting message with Web API');
         return postWithWebApi(messageObj);
       }
     }
     // else
     else {
-      if (Config.hasSlackToken(config)) {
+      if (configManager.getConfig('notification', 'slack:token')) {
         debug('posting message with Web API');
         return postWithWebApi(messageObj);
       }
-      if (Config.hasSlackIwhUrl(config)) {
+      if (configManager.getConfig('notification', 'slack:incomingWebhookUrl')) {
         debug('posting message with IncomingWebhook');
         return postWithIwh(messageObj);
       }

+ 26 - 169
src/server/util/swigFunctions.js

@@ -2,10 +2,16 @@ module.exports = function(crowi, app, req, locals) {
   const debug = require('debug')('growi:lib:swigFunctions');
   const stringWidth = require('string-width');
   const Page = crowi.model('Page');
-  const Config = crowi.model('Config');
   const User = crowi.model('User');
-  const passportService = crowi.passportService;
-  const cdnResourcesService = crowi.cdnResourcesService;
+  const {
+    configManager,
+    cdnResourcesService,
+    passportService,
+    appService,
+    aclService,
+    fileUploadService,
+    customizeService,
+  } = crowi;
   debug('initializing swigFunctions');
 
   locals.nodeVersion = function() {
@@ -43,38 +49,25 @@ module.exports = function(crowi, app, req, locals) {
   /**
    * @see ConfigManager#getConfig
    */
-  locals.getConfig = function(namespace, key) {
-    return crowi.configManager.getConfig(namespace, key);
-  };
+  locals.getConfig = configManager.getConfig.bind(configManager);
 
   /**
    * **Do not use this unless absolutely necessary. Use getConfig instead.**
    */
-  locals.getConfigFromDB = function(namespace, key) {
-    return crowi.configManager.getConfigFromDB(namespace, key);
-  };
-  /**
-   * **Do not use this unless absolutely necessary. Use getConfig instead.**
-   */
-  locals.getConfigFromEnvVars = function(namespace, key) {
-    return crowi.configManager.getConfigFromEnvVars(namespace, key);
-  };
+  locals.getConfigFromDB = configManager.getConfigFromDB.bind(configManager);
 
   /**
-   * return app title
+   * **Do not use this unless absolutely necessary. Use getConfig instead.**
    */
-  locals.appTitle = function() {
-    const config = crowi.getConfig();
-    return crowi.xss.process(Config.appTitle(config));
-  };
+  locals.getConfigFromEnvVars = configManager.getConfigFromEnvVars.bind(configManager);
 
   /**
-   * return app-global language
+   * pass service class to swig
    */
-  locals.appGlobalLang = function() {
-    const config = crowi.getConfig();
-    return Config.globalLang(config);
-  };
+  locals.appService = appService;
+  locals.aclService = aclService;
+  locals.fileUploadService = fileUploadService;
+  locals.customizeService = customizeService;
 
   locals.noCdn = function() {
     return !!process.env.NO_CDN;
@@ -101,83 +94,30 @@ module.exports = function(crowi, app, req, locals) {
     return cdnResourcesService.getHighlightJsStyleTag(styleName);
   };
 
-  /**
-   * return true if enabled
-   */
-  locals.isEnabledPassport = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledPassport(config);
-  };
-
-  /**
-   * return true if local strategy has been setup successfully
-   *  used whether restarting the server needed
-   */
-  locals.isPassportLocalStrategySetup = function() {
-    return passportService != null && passportService.isLocalStrategySetup;
-  };
-
   /**
    * return true if enabled and strategy has been setup successfully
    */
   locals.isLdapSetup = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
+    return (
+      configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')
+      && passportService.isLdapStrategySetup
+    );
   };
 
   /**
    * return true if enabled but strategy has some problem
    */
   locals.isLdapSetupFailed = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
-  };
-
-  locals.passportSamlLoginEnabled = function() {
-    return locals.isEnabledPassport() && locals.getConfig('crowi', 'security:passport-saml:isEnabled');
+    return (
+      configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')
+      && !passportService.isLdapStrategySetup
+    );
   };
 
   locals.getSamlMissingMandatoryConfigKeys = function() {
-    // return an empty array if Passport is not enabled
-    // because crowi.passportService is null.
-    if (!locals.isEnabledPassport()) {
-      return [];
-    }
-
     return crowi.passportService.getSamlMissingMandatoryConfigKeys();
   };
 
-  locals.googleLoginEnabled = function() {
-    // return false if Passport is enabled
-    // because official crowi mechanism is not used.
-    if (locals.isEnabledPassport()) {
-      return false;
-    }
-
-    const config = crowi.getConfig();
-    return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
-  };
-
-  locals.passportGoogleLoginEnabled = function() {
-    const config = crowi.getConfig();
-    return locals.isEnabledPassport() && config.crowi['security:passport-google:isEnabled'];
-  };
-
-  locals.passportGitHubLoginEnabled = function() {
-    const config = crowi.getConfig();
-    return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
-  };
-
-  locals.passportTwitterLoginEnabled = function() {
-    const config = crowi.getConfig();
-    return locals.isEnabledPassport() && config.crowi['security:passport-twitter:isEnabled'];
-  };
-
-  locals.passportOidcLoginEnabled = function() {
-    const config = crowi.getConfig();
-    return locals.isEnabledPassport() && config.crowi['security:passport-oidc:isEnabled'];
-  };
-
   locals.searchConfigured = function() {
     if (crowi.getSearcher()) {
       return true;
@@ -189,89 +129,6 @@ module.exports = function(crowi, app, req, locals) {
     return process.env.HACKMD_URI != null;
   };
 
-  locals.isEnabledPlugins = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledPlugins(config);
-  };
-
-  locals.isEnabledLinebreaks = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledLinebreaks(config);
-  };
-
-  locals.isEnabledLinebreaksInComments = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledLinebreaksInComments(config);
-  };
-
-  locals.customCss = function() {
-    return Config.customCss();
-  };
-
-  locals.pageBreakSeparator = function() {
-    const config = crowi.getConfig();
-    return Config.pageBreakSeparator(config);
-  };
-
-  locals.pageBreakCustomSeparator = function() {
-    const config = crowi.getConfig();
-    return Config.pageBreakCustomSeparator(config);
-  };
-
-  locals.customScript = function() {
-    return Config.customScript();
-  };
-
-  locals.customHeader = function() {
-    const config = crowi.getConfig();
-    return Config.customHeader(config);
-  };
-
-  locals.theme = function() {
-    const config = crowi.getConfig();
-    return Config.theme(config);
-  };
-
-  locals.customTitle = function(page) {
-    const config = crowi.getConfig();
-    return Config.customTitle(config, page);
-  };
-
-  locals.behaviorType = function() {
-    const config = crowi.getConfig();
-    return Config.behaviorType(config);
-  };
-
-  locals.layoutType = function() {
-    const config = crowi.getConfig();
-    return Config.layoutType(config);
-  };
-
-  locals.highlightJsStyle = function() {
-    const config = crowi.getConfig();
-    return Config.highlightJsStyle(config);
-  };
-
-  locals.highlightJsStyleBorder = function() {
-    const config = crowi.getConfig();
-    return Config.highlightJsStyleBorder(config);
-  };
-
-  locals.isEnabledTimeline = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledTimeline(config);
-  };
-
-  locals.isUploadable = function() {
-    const config = crowi.getConfig();
-    return Config.isUploadable(config);
-  };
-
-  locals.isEnabledAttachTitleHeader = function() {
-    const config = crowi.getConfig();
-    return Config.isEnabledAttachTitleHeader(config);
-  };
-
   locals.parentPath = function(path) {
     if (path === '/') {
       return path;

+ 21 - 21
src/server/views/admin/app.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('App settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('App settings')) }}{% endblock %}
 
 {% block head_warn_alert_siteurl_undefined %} {# remove including block for './widget/alert_siteurl_undefined.html' #}
 {% endblock %}
@@ -45,7 +45,7 @@
                    id="settingForm[app:title]"
                    type="text"
                    name="settingForm[app:title]"
-                   value="{{ settingForm['app:title'] | default('') }}"
+                   value="{{ getConfig('crowi', 'app:title') | default('') }}"
                    placeholder="GROWI">
             <p class="help-block">{{ t("app_setting.sitename_change") }}</p>
           </div>
@@ -58,8 +58,8 @@
                    id="settingForm[app:confidential]"
                    type="text"
                    name="settingForm[app:confidential]"
-                   value="{{ settingForm['app:confidential'] }}"
-                   placeholder="{{ t('app_setting. ex): internal use only') }}">
+                   value="{{ getConfig('crowi', 'app:confidential') | default('') }}"
+                   placeholder="{{ t('app_setting. ex&rpar;: internal use only') }}">
             <p class="help-block">{{ t("app_setting.header_content") }}</p>
           </div>
         </div>
@@ -72,7 +72,7 @@
                        id="radioLangEn"
                        name="settingForm[app:globalLang]"
                        value="{{ consts.language.LANG_EN_US }}"
-                       {% if appGlobalLang() == consts.language.LANG_EN_US %}checked="checked"{% endif %}>
+                       {% if getConfig('crowi', 'app:globalLang') == consts.language.LANG_EN_US %}checked="checked"{% endif %}>
                 <label for="radioLangEn">{{ t('English') }}</label>
             </div>
             <div class="radio radio-primary radio-inline">
@@ -80,7 +80,7 @@
                        id="radioLangJa"
                        name="settingForm[app:globalLang]"
                        value="{{ consts.language.LANG_JA }}"
-                       {% if appGlobalLang() == consts.language.LANG_JA %}checked="checked"{% endif %}>
+                       {% if getConfig('crowi', 'app:globalLang') == consts.language.LANG_JA %}checked="checked"{% endif %}>
                 <label for="radioLangJa">{{ t('Japanese') }}</label>
             </div>
           </div>
@@ -94,8 +94,8 @@
                      id="cbFileUpload"
                      name="settingForm[app:fileUpload]"
                      value="1"
-                     {% if settingForm['app:fileUpload'] %}checked{% endif %}
-                     {% if not isUploadable() %}disabled="disabled"{% endif %}>
+                     {% if getConfig('crowi', 'app:fileUpload') %}checked{% endif %}
+                     {% if not fileUploadService.getIsUploadable() %}disabled="disabled"{% endif %}>
               <label for="cbFileUpload">
                 {{ t("app_setting.enable_files_except_image") }}
               </label>
@@ -181,7 +181,7 @@
                    type="text"
                    name="settingForm[mail:from]"
                    placeholder="{{ t('eg') }} mail@growi.org"
-                   value="{{ settingForm['mail:from'] }}">
+                   value="{{ getConfig('crowi', 'mail:from') | default('') }}">
           </div>
         </div>
 
@@ -192,14 +192,14 @@
             <input class="form-control"
                    type="text"
                    name="settingForm[mail:smtpHost]"
-                   value="{{ settingForm['mail:smtpHost']|default('') }}">
+                   value="{{ getConfig('crowi', 'mail:smtpHost') | default('') }}">
           </div>
           <div class="col-xs-2">
             <label>{{ t('app_setting.Port') }}</label>
             <input class="form-control"
                    type="text"
                    name="settingForm[mail:smtpPort]"
-                   value="{{ settingForm['mail:smtpPort']|default('') }}">
+                   value="{{ getConfig('crowi', 'mail:smtpPort') | default('') }}">
           </div>
         </div>
 
@@ -209,14 +209,14 @@
             <input class="form-control"
                    type="text"
                    name="settingForm[mail:smtpUser]"
-                   value="{{ settingForm['mail:smtpUser']|default('') }}">
+                   value="{{ getConfig('crowi', 'mail:smtpUser') | default('') }}">
           </div>
           <div class="col-xs-3">
             <label>{{ t('Password') }}</label>
             <input class="form-control"
                    type="password"
                    name="settingForm[mail:smtpPassword]"
-                   value="{{ settingForm['mail:smtpPassword']|default('') }}">
+                   value="{{ getConfig('crowi', 'mail:smtpPassword') | default('') }}">
           </div>
         </div>
 
@@ -248,7 +248,7 @@
                    type="text"
                    name="settingForm[aws:region]"
                    placeholder="例: ap-northeast-1"
-                   value="{{ settingForm['aws:region'] }}">
+                   value="{{ getConfig('crowi', 'aws:region') | default('') }}">
           </div>
         </div>
 
@@ -260,7 +260,7 @@
                    type="text"
                    name="settingForm[aws:bucket]"
                    placeholder="例: crowi"
-                   value="{{ settingForm['aws:bucket'] }}">
+                   value="{{ getConfig('crowi', 'aws:bucket') | default('') }}">
           </div>
         </div>
 
@@ -271,7 +271,7 @@
                    id="settingForm[aws:accessKeyId]"
                    type="text"
                    name="settingForm[aws:accessKeyId]"
-                   value="{{ settingForm['aws:accessKeyId'] }}">
+                   value="{{ getConfig('crowi', 'aws:accessKeyId') | default('') }}">
           </div>
 
         </div>
@@ -283,7 +283,7 @@
                    id="settingForm[aws:secretAccessKey]"
                    type="text"
                    name="settingForm[aws:secretAccessKey]"
-                   value="{{ settingForm['aws:secretAccessKey'] }}">
+                   value="{{ getConfig('crowi', 'aws:secretAccessKey') | default('') }}">
           </div>
         </div>
 
@@ -307,18 +307,18 @@
           <div class="col-xs-6">
 
             <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="primary">
+              <label class="btn btn-default btn-rounded btn-outline {% if getConfig('crowi', 'plugin:isEnabledPlugins') %}active{% endif %}" data-active-class="primary">
                 <input name="settingForm[plugin:isEnabledPlugins]"
                        value="true"
                        type="radio"
-                       {% if true === settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}>
+                       {% if true === getConfig('crowi', 'plugin:isEnabledPlugins') %}checked{% endif %}>
                 ON
               </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['plugin:isEnabledPlugins'] %}active{% endif %}" data-active-class="default">
+              <label class="btn btn-default btn-rounded btn-outline {% if !getConfig('crowi', 'plugin:isEnabledPlugins') %}active{% endif %}" data-active-class="default">
                 <input name="settingForm[plugin:isEnabledPlugins]"
                        value="false"
                        type="radio"
-                       {% if !settingForm['plugin:isEnabledPlugins'] %}checked{% endif %}>
+                       {% if !getConfig('crowi', 'plugin:isEnabledPlugins') %}checked{% endif %}>
                 OFF
               </label>
             </div>

+ 18 - 25
src/server/views/admin/customize.html

@@ -1,20 +1,13 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Customize')) }} {% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }} {% endblock %}
 
 {% block theme_css_block %}
-  {% if 'kibela' === layoutType() %}
-    {% if env === 'development' %}
-      <script src="{{ webpack_asset('styles/theme-kibela.js') }}"></script>
-    {% else %}
-      <link rel="stylesheet" href="{{ webpack_asset('styles/theme-kibela.css') }}">
-    {% endif %}
+  {% set themeName = getConfig('crowi', 'customize:theme') %}
+  {% if env === 'development' %}
+    <script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
   {% else %}
-    {% if env === 'development' %}
-      <script src="{{ webpack_asset('styles/theme-' + theme() + '.js') }}"></script>
-    {% else %}
-    <link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('styles/theme-' + theme() + '.css') }}">
-    {% endif %}
+  <link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}">
   {% endif %}
 {% endblock %}
 
@@ -314,7 +307,7 @@
             <div class="col-xs-9">
               <select class="form-control selectpicker" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)" {% if noCdn() %}disabled{% endif %}>
                 {% for key in Object.keys(highlightJsCssSelectorOptions) %}
-                  <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
+                  <option value={{key}} {% if key == getConfig('crowi', 'customize:highlightJsStyle') %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                 {% endfor %}
               </select>
               <p class="help-block text-warning">{{ t('customize_page.nocdn_desc') }}</p>
@@ -338,7 +331,7 @@
           </div>
 
           <div id="highlightJsCssContainer">
-            {{ cdnHighlightJsStyleTag(highlightJsStyle()) }}
+            {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
           </div>
 
           <p class="help-block">
@@ -388,7 +381,7 @@ export  $initHighlight;</code></pre>
 
           <div class="form-group">
             <div class="col-xs-12">
-              <input class="form-control" name="settingForm[customize:title]" value="{{ settingForm['customize:title'] }}"></input>
+              <input class="form-control" name="settingForm[customize:title]" value="{{ settingForm['customize:title'] | default('') }}"></input>
             </div>
           </div>
 
@@ -418,7 +411,7 @@ export  $initHighlight;</code></pre>
         <div class="form-group">
           <div class="col-xs-12">
             <div id="custom-header-editor"></div>
-            <input type="hidden" id="inputCustomHeader" name="settingForm[customize:header]" value="{{ settingForm['customize:header'] }}">
+            <input type="hidden" id="inputCustomHeader" name="settingForm[customize:header]" value="{{ settingForm['customize:header'] | default('') }}">
           </div>
           <div class="col-xs-12">
             <p class="help-block text-right">
@@ -450,7 +443,7 @@ export  $initHighlight;</code></pre>
         <div class="form-group">
           <div class="col-xs-12">
             <div id="custom-css-editor"></div>
-            <input type="hidden" id="inputCustomCss" name="settingForm[customize:css]" value="{{ settingForm['customize:css'] }}">
+            <input type="hidden" id="inputCustomCss" name="settingForm[customize:css]" value="{{ settingForm['customize:css'] | default('') }}">
           </div>
           <div class="col-xs-12">
             <p class="help-block text-right">
@@ -486,14 +479,14 @@ export  $initHighlight;</code></pre>
           <dl class="dl-horizontal">
             <dt><code>$</code></dt>
             <dd>jQuery instance</dd>
-            <dt><code>crowi</code></dt>
-            <dd>Crowi context instance</dd>
+            <dt><code>appContainer</code></dt>
+            <dd>GROWI App <a href="https://github.com/jamiebuilds/unstated">Unstated Container</a></dd>
+            <dt><code>growiRenderer</code></dt>
+            <dd>GROWI Renderer origin instance</dd>
+            <dt><code>growiPlugin</code></dt>
+            <dd>GROWI Plugin Manager instance</dd>
             <dt><code>Crowi</code></dt>
             <dd>Crowi legacy instance (jQuery based)</dd>
-            <dt><code>crowiRenderer</code></dt>
-            <dd>Crowi Renderer instance</dd>
-            <dt><code>crowiPlugin</code></dt>
-            <dd>GROWI plugin manager instance</dd>
           </dl>
         </p>
         <p class="help-block">
@@ -501,14 +494,14 @@ export  $initHighlight;</code></pre>
 <pre class="hljs"><code>console.log($('.main-container'));
 
 window.addEventListener('load', (event) => {
-  console.log('config: ', crowi.config);
+  console.log('config: ', appContainer.config);
 });</code></pre>
         </p>
 
         <div class="form-group">
           <div class="col-xs-12">
             <div id="custom-script-editor"></div>
-            <input type="hidden" id="inputCustomScript" name="settingForm[customize:script]" value="{{ settingForm['customize:script'] }}">
+            <input type="hidden" id="inputCustomScript" name="settingForm[customize:script]" value="{{ settingForm['customize:script'] | default('') }}">
           </div>
           <div class="col-xs-12">
             <p class="help-block text-right">

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

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('External Account management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('External Account management')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 1 - 1
src/server/views/admin/global-notification-detail.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Notification settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 1 - 1
src/server/views/admin/importer.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Import Data')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Import Data')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 1 - 1
src/server/views/admin/index.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('admin_top.Management Wiki')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('admin_top.Management Wiki')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 1 - 1
src/server/views/admin/markdown.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Markdown settings')) }}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Markdown settings')) }}
  · {{ path }}{% endblock %}
 
 {% block content_header %}

+ 1 - 1
src/server/views/admin/notification.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Notification settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

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

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Full Text Search management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Full Text Search management')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 37 - 172
src/server/views/admin/security.html

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Security settings')) }} · {% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('security_settings')) }} · {% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Security settings') }}</h1>
+    <h1 id="admin-title" class="title">{{ t('security_settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -38,36 +38,14 @@
 
       <form action="/_api/admin/security/general" method="post" class="form-horizontal" id="generalSetting" role="form">
         <fieldset>
-        <legend class="alert-anchor">{{ t('security_setting.Security settings') }}</legend>
-
-          <div class="form-group">
-            <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
-            <div class="col-xs-3">
-              <label for="">ID</label>
-              <input class="form-control" type="text" name="settingForm[security:basicName]" value="{{ settingForm['security:basicName']|default('') }}" autocomplete="nope" {% if not isAclEnabled  %}readonly{% endif%}>
-            </div>
-            <div class="col-xs-3">
-              <label for="">{{ t('Password') }}</label>
-              <input class="form-control" type="text" name="settingForm[security:basicSecret]" value="{{ settingForm['security:basicSecret']|default('') }}" autocomplete="nope" {% if not isAclEnabled  %}readonly{% endif%}>
-            </div>
-            <div class="col-xs-offset-3 col-xs-9">
-              <p class="help-block small">
-                {% if not isAclEnabled %}
-                  {{ t("security_setting.basic_acl_disable") }}<br>
-                {% else %}
-                  {{ t("security_setting.common_authentication") }}<br>
-                  {{ t("security_setting.without_encryption") }}<br>
-                {% endif %}
-              </p>
-            </div>
-          </div>
+        <legend class="alert-anchor">{{ t('security_settings') }}</legend>
 
           <div class="form-group">
             <label for="settingForm[security:restrictGuestMode]" class="col-xs-3 control-label">{{ t('Guest users access') }}</label>
             <div class="col-xs-6">
-              <select class="form-control selectpicker" name="settingForm[security:restrictGuestMode]" value="{{ settingForm['security:restrictGuestMode'] }}">
+              <select class="form-control selectpicker" name="settingForm[security:restrictGuestMode]" value="{{ getConfig('crowi', 'security:restrictGuestMode') }}">
                 {% for modeValue, modeLabel in consts.restrictGuestMode %}
-                <option value="{{ t(modeValue) }}" {% if modeValue == settingForm['security:restrictGuestMode'] %}selected{% endif %} >{{ t(modeLabel) }}</option>
+                <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:restrictGuestMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
                 {% endfor %}
               </select>
             </div>
@@ -76,9 +54,9 @@
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Register limitation') }}</label>
             <div class="col-xs-6">
-              <select class="form-control selectpicker" name="settingForm[security:registrationMode]" value="{{ settingForm['security:registrationMode'] }}">
+              <select class="form-control selectpicker" name="settingForm[security:registrationMode]" value="{{ getConfig('crowi', 'security:registrationMode') }}">
                 {% for modeValue, modeLabel in consts.registrationMode %}
-                <option value="{{ t(modeValue) }}" {% if modeValue == settingForm['security:registrationMode'] %}selected{% endif %} >{{ t(modeLabel) }}</option>
+                <option value="{{ t(modeValue) }}" {% if modeValue == getConfig('crowi', 'security:registrationMode') %}selected{% endif %} >{{ t(modeLabel) }}</option>
                 {% endfor %}
               </select>
               <p class="help-block small">{{ t('The contents entered here will be shown in the header etc') }}</p>
@@ -88,7 +66,7 @@
           <div class="form-group">
             <label for="settingForm[security:registrationWhiteList]" class="col-xs-3 control-label">{{ t('The whitelist of registration permission E-mail address') }}</label>
             <div class="col-xs-8">
-              <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @growi.org">{{ settingForm['security:registrationWhiteList']|join('&#13')|raw }}</textarea>
+              <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @growi.org">{{ getConfig('crowi', 'security:registrationWhiteList') | join('&#13') | raw }}</textarea>
               <p class="help-block small">{{ t("security_setting.restrict_emails") }}{{ t("security_setting.for_instance") }}<code>@growi.org</code>{{ t("security_setting.only_those") }}<br>
               {{ t("security_setting.insert_single") }}</p>
             </div>
@@ -96,7 +74,7 @@
 
           <div class="form-group">
             {% set configName = 'settingForm[security:list-policy:hideRestrictedByOwner]' %}
-            {% set configValue = settingForm['security:list-policy:hideRestrictedByOwner'] %}
+            {% set configValue = getConfig('crowi', 'security:list-policy:hideRestrictedByOwner') %}
             {% set isEnabled = !configValue %}
             <label for="{{configName}}" class="col-xs-3 control-label">{{ t("security_setting.page_listing_1") }}</label>
             <div class="col-xs-9">
@@ -117,7 +95,7 @@
 
           <div class="form-group">
             {% set configName = 'settingForm[security:list-policy:hideRestrictedByGroup]' %}
-            {% set configValue = settingForm['security:list-policy:hideRestrictedByGroup'] %}
+            {% set configValue = getConfig('crowi', 'security:list-policy:hideRestrictedByGroup') %}
             {% set isEnabled = !configValue %}
             <label for="{{configName}}" class="col-xs-3 control-label">{{ t("security_setting.page_listing_2") }}</label>
             <div class="col-xs-9">
@@ -136,6 +114,23 @@
             </div>
           </div>
 
+          <div class="form-group">
+            {% set configName = 'settingForm[security:pageCompleteDeletionAuthority]' %}
+            {% set configValue = getConfig('crowi','security:pageCompleteDeletionAuthority') %}
+            <label for="{{configName}}" class="col-xs-3 control-label">{{ t('security_setting.complete_deletion') }}</label>
+            <div class="col-xs-6">
+              <select class="form-control selectpicker" name="settingForm[security:pageCompleteDeletionAuthority]" value="{{ configValue }}">
+                <option value="anyOne" {% if configValue == "anyOne" %}selected{% endif %}>{{ t('security_setting.anyone') }}</option>
+                <option value="adminOnly" {% if configValue =="adiminOnly" %}selected{% endif %}>{{ t('security_setting.admin_only') }}</option>
+                <option value="adminAndAuthor" {% if configValue == "adminAndAuthor" %}selected{% endif %}>{{ t('security_setting.admin_and_author') }}</option>
+              </select>
+
+              <p class="help-block small">
+                {{ t('security_setting.complete_deletion_explain') }}
+              </p>
+            </div>
+          </div>
+
           <div class="form-group">
             <div class="col-xs-offset-3 col-xs-6">
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
@@ -156,139 +151,16 @@
         </div>
        </div>
 
-      <form action="/_api/admin/security/mechanism" method="post" class="form-horizontal mt-5" id="mechanismSetting" role="form">
-        <fieldset>
-          <legend class="alert-anchor">{{ t('Selecting authentication mechanism') }}</legend>
-          <p class="alert alert-info"><b>{{ t("security_setting.note") }}: </b>{{ t("security_setting.require_server_restart_change_auth") }}</p>
-          <div class="form-group">
-            <div class="col-xs-6">
-              <h4>
-                <div class="radio radio-primary">
-                  <input type="radio" id="radioPassportAuthMech" name="settingForm[security:isEnabledPassport]" value="true"
-                      {% if true === settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
-                  <label for="radioPassportAuthMech">
-                    <a href="http://passportjs.org/" target="_blank">
-                      <img src="/images/admin/security/passport-logo.svg" class="passport-logo"> Passport
-                    </a> {{ t("security_setting.auth_mechanism") }} <small class="text-success">({{ t("security_setting.recommended") }})</small>
-                  </label>
-                </div>
-              </h4>
-              <ul>
-                <li>{{ t("security_setting.username_email_password") }}</li>
-                <li>{{ t("security_setting.ldap_auth") }}</li>
-                <li>{{ t("security_setting.saml_auth") }}</li>
-                <li>{{ t("security_setting.google_auth2") }}</li>
-                <li>{{ t("security_setting.github_auth2") }}</li>
-                <li>{{ t("security_setting.twitter_auth2") }}</li>
-                <li class="text-muted">(TBD) <del>{{ t("security_setting.facebook_auth2") }}</del></li>
-              </ul>
-            </div>
-            <div class="col-xs-6">
-              <h4>
-                <div class="radio radio-primary">
-                  <input type="radio" id="radioCrowiAuthMech" name="settingForm[security:isEnabledPassport]" value="false"
-                      {% if !settingForm['security:isEnabledPassport'] %}checked="checked"{% endif %}>
-                  <label for="radioCrowiAuthMech">
-                    Crowi Classic {{ t("security_setting.auth_mechanism") }}
-                  </label>
-                </div>
-              </h4>
-              <ul>
-                <li>{{ t("security_setting.username_email_password") }}</li>
-                <li class="text-muted">
-                  {{ t("security_setting.google_auth2") }}
-                  <ul><li>{{ t("security_setting.google_auth2_by_crowi_desc") }}</li></ul>
-                </li>
-              </ul>
-            </div>
-          </div>
-
-          <div class="form-group">
-            <div class="col-xs-offset-5 col-xs-6">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-
 
       <div class="auth-mechanism-configurations m-t-10">
 
         <legend>{{ t('security_setting.Authentication mechanism settings') }}</legend>
 
-        {% set isOfficialConfigurationVisible = !isEnabledPassport() %}
-        <div class="official-crowi-auth-settings" {% if !isOfficialConfigurationVisible %}style="display: none;"{% endif %}>
-          {% set isRestartingServerNeeded = isPassportLocalStrategySetup() %}
-          <p class="alert alert-warning"
-              {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
-            <b>
-              <i class="icon-exclamation" aria-hidden="true"></i>
-              {{ t("security_setting.require_server_restart") }}
-            </b>
-            {{ t("security_setting.server_on_passport_auth") }}
-          </p>
-
-          <form action="/_api/admin/security/google" method="post" class="form-horizontal" id="googleSetting" role="form"
-              {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
-
-            <fieldset>
-              <h4>{{ t("security_setting.google_setting") }}</h4>
-              <p class="well alert-anchor">
-                {{ t("security_setting.connect_api_manager") }}
-              </p>
-
-              <ol class="help-block">
-                <li>{{ t("security_setting.access_api_manager", "https://console.cloud.google.com/apis/credentials", "API Manager") }}</li>
-                <li>{{ t("security_setting.create_project") }}</li>
-                <li>{{ t("security_setting.create_auth_to_oauth") }}</li>
-                <ol>
-                  <li>{{ t("security_setting.select_webapp") }}</li>
-                  <li>{{ t("security_setting.change_redirect_url") }}</li>
-                </ol>
-              </ol>
-
-              <div class="form-group">
-                <label for="settingForm[google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
-                <div class="col-xs-6">
-                  <input class="form-control" type="text" name="settingForm[google:clientId]" value="{{ settingForm['google:clientId'] }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <label for="settingForm[google:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
-                <div class="col-xs-6">
-                  <input class="form-control" type="text" name="settingForm[google:clientSecret]" value="{{ settingForm['google:clientSecret'] }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <div class="col-xs-offset-3 col-xs-6">
-                  <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-                </div>
-              </div>
-
-            </fieldset>
-          </form>
-        </div>
-
         {#
          # passport settings nav
          #}
-        {% set isPassportConfigurationVisible = settingForm['security:isEnabledPassport'] %}
-        <div class="passport-settings" {% if !isPassportConfigurationVisible %}style="display: none;"{% endif %}>
-
-          {% set isRestartingServerNeeded = !isPassportLocalStrategySetup() %}
-          <p class="alert alert-warning"
-              {% if !isRestartingServerNeeded %}style="display: none;"{% endif %}>
-            <b>
-              <i class="icon-exclamation" aria-hidden="true"></i>
-              {{ t("security_setting.require_server_restart") }}
-            </b>
-            {{ t("security_setting.server_on_crowi_auth") }}
-          </p>
-          <ul class="nav nav-tabs" role="tablist" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+        <div class="passport-settings">
+          <ul class="nav nav-tabs" role="tablist">
             <li class="active">
               <a href="#passport-ldap" data-toggle="tab" role="tab"><i class="fa fa-sitemap"></i> LDAP</a>
             </li>
@@ -307,12 +179,15 @@
             <li>
               <a href="#passport-oidc" data-toggle="tab" role="tab"><i class="fa fa-openid"></i> OIDC</a>
             </li>
+            <li>
+              <a href="#passport-basic" data-toggle="tab" role="tab"><i class="fa fa-sign-in"></i> Basic</a>
+            </li>
             <li class="tbd">
               <a href="#passport-facebook" data-toggle="tab" role="tab"><i class="fa fa-facebook"></i> (TBD) Facebook</a>
             </li>
           </ul>
 
-          <div class="tab-content p-t-10" {% if isRestartingServerNeeded %}style="opacity: 0.4;"{% endif %}>
+          <div class="tab-content p-t-10">
             <div id="passport-ldap" class="tab-pane active" role="tabpanel" >
               {% include './widget/passport/ldap.html' with { settingForm: settingForm } %}
             </div>
@@ -341,6 +216,10 @@
               {% include './widget/passport/github.html' %}
             </div>
 
+            <div id="passport-basic" class="tab-pane" role="tabpanel">
+              {% include './widget/passport/basic.html' %}
+            </div>
+
           </div><!-- /.tab-content -->
         </div>
 
@@ -397,20 +276,6 @@
         return false;
       });
     });
-
-    // switch display according to on / off of radio buttons
-    $('input[name="settingForm[security:isEnabledPassport]"]:radio').change(function() {
-      const isEnabledPassport = ($(this).val() === "true");
-
-      if (isEnabledPassport) {
-        $('.official-crowi-auth-settings').hide(400);
-        $('.passport-settings').show(400);
-      }
-      else {
-        $('.official-crowi-auth-settings').show(400);
-        $('.passport-settings').hide(400);
-      }
-    });
   </script>
 </div>
 {% endblock content_main %}

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

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('UserGroup Management') + '/' + userGroup.name) | preventXss }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

+ 1 - 1
src/server/views/admin/user-groups.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('UserGroup Management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management')) }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">

Some files were not shown because too many files changed in this diff