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

Merge pull request #626 from weseek/fix/saml

Fix/saml
Yuki Takei 7 лет назад
Родитель
Сommit
4e8465e96f
41 измененных файлов с 710 добавлено и 296 удалено
  1. 8 1
      CHANGES.md
  2. 7 2
      README.md
  3. 1 0
      config/env.dev.js
  4. 29 0
      config/migrate.js
  5. 12 5
      package.json
  6. 13 0
      resource/certs/localhost/cert.pem
  7. 11 0
      resource/certs/localhost/csr.pem
  8. 15 0
      resource/certs/localhost/key.pem
  9. 11 1
      resource/locales/en-US/translation.json
  10. 14 4
      resource/locales/ja/translation.json
  11. 47 0
      src/migrations/20180926134048-make-email-unique.js
  12. 94 0
      src/migrations/20180927102719-init-serverurl.js
  13. 3 1
      src/server/.node-dev.json
  14. 3 3
      src/server/app.js
  15. 47 11
      src/server/crowi/dev.js
  16. 4 3
      src/server/crowi/express-init.js
  17. 33 52
      src/server/crowi/index.js
  18. 2 1
      src/server/form/admin/app.js
  19. 0 1
      src/server/form/admin/securityPassportGitHub.js
  20. 0 1
      src/server/form/admin/securityPassportGoogle.js
  21. 8 8
      src/server/form/admin/securityPassportSaml.js
  22. 0 1
      src/server/form/admin/securityPassportTwitter.js
  23. 19 13
      src/server/models/config.js
  24. 21 4
      src/server/models/user.js
  25. 3 3
      src/server/routes/attachment.js
  26. 3 3
      src/server/routes/hackmd.js
  27. 28 11
      src/server/routes/login-passport.js
  28. 1 1
      src/server/routes/login.js
  29. 13 4
      src/server/service/passport.js
  30. 2 2
      src/server/util/googleAuth.js
  31. 6 6
      src/server/util/slack.js
  32. 9 1
      src/server/views/admin/app.html
  33. 10 10
      src/server/views/admin/widget/passport/github.html
  34. 10 10
      src/server/views/admin/widget/passport/google-oauth.html
  35. 57 18
      src/server/views/admin/widget/passport/saml.html
  36. 10 8
      src/server/views/admin/widget/passport/twitter.html
  37. 1 1
      src/server/views/modal/duplicate.html
  38. 1 1
      src/server/views/modal/rename.html
  39. 5 5
      src/test/crowi/crowi.test.js
  40. 2 2
      src/test/utils.js
  41. 147 98
      yarn.lock

+ 8 - 1
CHANGES.md

@@ -3,7 +3,14 @@ CHANGES
 
 
 ## 3.2.4-RC
 ## 3.2.4-RC
 
 
-* 
+* Improvement: Specify certificate for SAML Authentication
+* Fix: SAML Authentication didn't work
+* Support: Mongoose migration mechanism
+* Support: Upgrade libs
+    * mocha
+    * mongoose
+    * mongoose-paginate
+    * mongoose-unique-validator
 
 
 ## 3.2.3
 ## 3.2.3
 
 

+ 7 - 2
README.md

@@ -173,10 +173,15 @@ Environment Variables
 * **Option (Overwritable in admin page)**
 * **Option (Overwritable in admin page)**
     * OAUTH_GOOGLE_CLIENT_ID: Google API client id for OAuth login.
     * OAUTH_GOOGLE_CLIENT_ID: Google API client id for OAuth login.
     * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
     * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
-    * OAUTH_GOOGLE_CALLBACK_URI: Google API callback URI for OAuth login (Set `https://${growi.host}/passport/google/callback`).
     * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
     * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
     * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
     * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
-    * OAUTH_GITHUB_CALLBACK_URI: GitHub API callback URI for OAuth login (Set `https://${growi.host}/passport/github/callback`).
+    * OAUTH_TWITTER_CLIENT_ID: Twitter API client id for OAuth login.
+    * OAUTH_TWITTER_CLIENT_SECRET: Twitter API client secret for OAuth login.
+    * OAUTH_TWITTER_CLIENT_ID: Twitter API client id for OAuth login.
+    * OAUTH_TWITTER_CLIENT_SECRET: Twitter API client secret for OAuth login.
+    * SAML_ENTRY_POINT: IdP entry point
+    * SAML_ISSUER: Issuer string to supply to IdP
+    * SAML_CERT: PEM-encoded X.509 signing certificate string to validate the response from IdP
 
 
 
 
 Documentation
 Documentation

+ 1 - 0
config/env.dev.js

@@ -8,4 +8,5 @@ module.exports = {
     // 'growi-plugin-lsx',
     // 'growi-plugin-lsx',
     // 'growi-plugin-pukiwiki-like-linker',
     // 'growi-plugin-pukiwiki-like-linker',
   ],
   ],
+  // DEV_HTTPS: true,
 };
 };

+ 29 - 0
config/migrate.js

@@ -0,0 +1,29 @@
+/**
+ * Configuration file for migrate-mongo
+ * @see https://github.com/seppevs/migrate-mongo
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ */
+
+function getMongoUri(env) {
+  return env.MONGOLAB_URI || // for B.C.
+    env.MONGODB_URI || // MONGOLAB changes their env name
+    env.MONGOHQ_URL ||
+    env.MONGO_URI ||
+    ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
+}
+
+const mongoUri = getMongoUri(process.env);
+const match = mongoUri.match(/^(.+)\/([^/]+)$/);
+module.exports = {
+  mongoUri,
+  mongodb: {
+    url: match[1],
+    databaseName: match[2],
+    options: {
+      useNewUrlParser: true, // removes a deprecation warning when connecting
+    },
+  },
+  migrationsDir: 'src/migrations/',
+  changelogCollectionName: 'migrations'
+};

+ 12 - 5
package.json

@@ -37,10 +37,16 @@
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "lint:fix": "eslint . --fix",
     "lint:fix": "eslint . --fix",
     "lint": "eslint .",
     "lint": "eslint .",
+    "migrate": "npm run migrate:up",
+    "migrate:create": "migrate-mongo create -f config/migrate.js -- ",
+    "migrate:status": "migrate-mongo status -f config/migrate.js",
+    "migrate:up": "migrate-mongo up -f config/migrate.js",
+    "migrate:down": "migrate-mongo down -f config/migrate.js",
     "mkdirp": "mkdirp",
     "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "prebuild:dev:app": "env-cmd config/env.dev.js npm run plugin:def",
     "prebuild:dev:app": "env-cmd config/env.dev.js npm run plugin:def",
     "prebuild:prod": "npm run plugin:def",
     "prebuild:prod": "npm run plugin:def",
+    "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "prestart": "npm run build:prod",
     "server:debug": "env-cmd config/env.dev.js node-dev --inspect src/server/app.js",
     "server:debug": "env-cmd config/env.dev.js node-dev --inspect src/server/app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn src/server/app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn src/server/app.js",
@@ -48,7 +54,7 @@
     "server:prod": "env-cmd config/env.prod.js node src/server/app.js",
     "server:prod": "env-cmd config/env.prod.js node src/server/app.js",
     "server": "npm run server:dev",
     "server": "npm run server:dev",
     "start": "npm run server:prod",
     "start": "npm run server:prod",
-    "test": "mocha --timeout 10000 -r src/test/bootstrap.js src/test/**/*.js",
+    "test": "mocha --timeout 10000 --exit -r src/test/bootstrap.js src/test/**/*.js",
     "version": "node -p \"require('./package.json').version\"",
     "version": "node -p \"require('./package.json').version\"",
     "webpack": "webpack"
     "webpack": "webpack"
   },
   },
@@ -90,11 +96,12 @@
     "markdown-it-blockdiag": "^1.0.2",
     "markdown-it-blockdiag": "^1.0.2",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
+    "migrate-mongo": "^3.0.5",
     "mkdirp": "~0.5.1",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
     "module-alias": "^2.0.6",
-    "mongoose": "^5.0.0",
-    "mongoose-paginate": "^5.0.0",
-    "mongoose-unique-validator": "^2.0.0",
+    "mongoose": "^5.2.0",
+    "mongoose-paginate": "^5.0.3",
+    "mongoose-unique-validator": "^2.0.2",
     "multer": "~1.3.0",
     "multer": "~1.3.0",
     "nodemailer": "^4.0.1",
     "nodemailer": "^4.0.1",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
@@ -164,7 +171,7 @@
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "markdown-table": "^1.1.1",
     "metismenu": "^2.7.4",
     "metismenu": "^2.7.4",
-    "mocha": "^5.0.0",
+    "mocha": "^5.2.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
     "node-dev": "^3.1.3",
     "node-sass": "^4.5.0",
     "node-sass": "^4.5.0",

+ 13 - 0
resource/certs/localhost/cert.pem

@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
+UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
+AwwJbG9jYWxob3N0MB4XDTE4MDkxMjEwMjIzNFoXDTE4MTAxMjEwMjIzNFowSDEL
+MAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMRUwEwYDVQQKDAxXRVNFRUssIElu
+Yy4xEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
+gYEAy0FKOlCQynbo5wYyMCL5LW27Re9dB14wXPDT+zd7wDdYrCbAKKZ1r+7Sj1e7
+638lnn7n4WkhkgsQi/mTxF7W9PHYF00Dh2X0qGf9t+LocNeLVQBHMGNi7HXh8X3j
+iM7w9FffdlfBvuYxPIdDXP12x9JmRhr59Tpv1aaMcRxAY1cCAwEAATANBgkqhkiG
+9w0BAQsFAAOBgQBa/PwnEeFCQ5G4SS6IcL6QVh3KLfeVMCfYVk1o0iJVmJTvfdrq
+crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
+pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
+-----END CERTIFICATE-----

+ 11 - 0
resource/certs/localhost/csr.pem

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBhzCB8QIBADBIMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xFTATBgNV
+BAoMDFdFU0VFSywgSW5jLjESMBAGA1UEAwwJbG9jYWxob3N0MIGfMA0GCSqGSIb3
+DQEBAQUAA4GNADCBiQKBgQDLQUo6UJDKdujnBjIwIvktbbtF710HXjBc8NP7N3vA
+N1isJsAopnWv7tKPV7vrfyWefufhaSGSCxCL+ZPEXtb08dgXTQOHZfSoZ/234uhw
+14tVAEcwY2LsdeHxfeOIzvD0V992V8G+5jE8h0Nc/XbH0mZGGvn1Om/VpoxxHEBj
+VwIDAQABoAAwDQYJKoZIhvcNAQELBQADgYEAd49hz4IoQO55tr62OFlZr254ZPBX
+SXxCtSWawBWLFij8QLl1B8JkHARrMKdM1jBCy5UXcH05DrxGwIOXIcRW7mfrIQDH
+pGs+BQCHMHuYnssg/z2aDhafkmPaLBwh0KWPypVIStxUwLcKxA1xk5VBoP/q+Lgk
+h/mCVJ7JY40BlLA=
+-----END CERTIFICATE REQUEST-----

+ 15 - 0
resource/certs/localhost/key.pem

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDLQUo6UJDKdujnBjIwIvktbbtF710HXjBc8NP7N3vAN1isJsAo
+pnWv7tKPV7vrfyWefufhaSGSCxCL+ZPEXtb08dgXTQOHZfSoZ/234uhw14tVAEcw
+Y2LsdeHxfeOIzvD0V992V8G+5jE8h0Nc/XbH0mZGGvn1Om/VpoxxHEBjVwIDAQAB
+AoGALsf8OafJa5Aq0uGOM54ZE+eprtME6mk3YGzdnXiLtxYGBrl0iOanN7MUK4HZ
+8r30/qHe5Pa5j0+Uo2LyK8RYXOcT77CeSSQiSGlBgGj7US7ZmyTqsOwaUKVnbFcy
+Bf/bTJl4EjZREy7kdfCWVO1yY98tV4XrZ2CBDSEDyI8UiiECQQDw47YBMy2duuS6
+z5Ui0xRr8MYwRlCNOt6xKQxlKEhnnRA0vdNB6VMiSzvZ9Bt5nPWI/B3ugUupo6Hn
+FXi1tOgTAkEA2AE7RTICcGPpogOtq/5g7pPNofH524hN26qtUT2kKjUy316JYjrU
+t+N6Ck867w4juVbDcVOTB2Nbj+2+t2EILQJAfo1CyvKWHm1XSQVRNlBqRCLkG+x0
+2R16bNxB1MsK7tRG9U5ctB3ePQAFW4WxAX0CSYsaNnjaxS5gGkTfe6ak3QJAWVlh
+EAVYtu7NRKQq4btOk0F2TOfQB7xBIH1gRfuufXsV+Qmc4JIfTZV99OfDJAGAS3kV
+TTpZ1jOGO2oHeslbXQJBAM4xX8hUueQMIllpBNjlAx1xTqptOHa4elaaPZi7HcDj
+olRU0OP/wPOoEJRvHGP8+LAerx5CEYbadnukQAnNPLA=
+-----END RSA PRIVATE KEY-----

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

@@ -260,6 +260,8 @@
     "Site Name": "Site name",
     "Site Name": "Site name",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "header_content": "The contents entered here will be shown in the header etc.",
+    "Site URL": "Site URL",
+    "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "Confidential name": "Confidential name",
     "Confidential name": "Confidential name",
     "ex): internal use only":"ex): internal use only",
     "ex): internal use only":"ex): internal use only",
     "enable_files_except_image": "Enable file upload other than image files.",
     "enable_files_except_image": "Enable file upload other than image files.",
@@ -308,6 +310,7 @@
     "auth_mechanism": "authentication mechanism",
     "auth_mechanism": "authentication mechanism",
     "recommended": "Recommended",
     "recommended": "Recommended",
     "username_email_password": "Username, Email and Password authentication",
     "username_email_password": "Username, Email and Password authentication",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Define it from %s",
     "ldap_auth": "LDAP authentication",
     "ldap_auth": "LDAP authentication",
     "saml_auth": "SAML authentication",
     "saml_auth": "SAML authentication",
     "google_auth2": "Google OAuth authentication",
     "google_auth2": "Google OAuth authentication",
@@ -331,6 +334,7 @@
     "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link":"Go to Markdown settings",
     "xss_prevent_setting_link":"Go to Markdown settings",
     "callback_URL": "Callback URL",
     "callback_URL": "Callback URL",
+    "desc_of_callback_URL": "Use it in the setting of the %s provider",
     "guest_mode": {
     "guest_mode": {
       "deny": "Deny Unregistered Users",
       "deny": "Deny Unregistered Users",
       "readonly": "View Only"
       "readonly": "View Only"
@@ -379,7 +383,13 @@
       "name": "SAML",
       "name": "SAML",
       "entry_point": "Entry Point",
       "entry_point": "Entry Point",
       "issuer": "Issuer",
       "issuer": "Issuer",
-      "mapping_detail": "Specification of mappings for %s when creating new users"
+      "First Name": "First Name",
+      "Last Name": "Last Name",
+      "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "mapping_detail": "Specification of mappings for %s when creating new users",
+      "cert_detail1": "PEM-encoded X.509 signing certificate to validate the response from IdP",
+      "cert_detail2": "Use env var <code>SAML_CERT</code> if empty, and no validation is processed if the variable is also undefined"
     },
     },
     "OAuth": {
     "OAuth": {
       "register": "Register for %s",
       "register": "Register for %s",

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

@@ -278,6 +278,8 @@
     "Site Name": "サイト名",
     "Site Name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
+    "Site URL": "サイトURL",
+    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL。",
     "Confidential name": "コンフィデンシャル表示",
     "Confidential name": "コンフィデンシャル表示",
     "ex): internal use only": "例: 社外秘",
     "ex): internal use only": "例: 社外秘",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
@@ -327,6 +329,7 @@
     "auth_mechanism": "認証機構",
     "auth_mechanism": "認証機構",
     "recommended": "推奨",
     "recommended": "推奨",
     "username_email_password": "ユーザー名、Eメール、パスワードでの認証",
     "username_email_password": "ユーザー名、Eメール、パスワードでの認証",
+    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
     "ldap_auth": "LDAP 認証",
     "ldap_auth": "LDAP 認証",
     "saml_auth": "SAML 認証",
     "saml_auth": "SAML 認証",
     "google_auth2": "Google OAuth 認証",
     "google_auth2": "Google OAuth 認証",
@@ -349,6 +352,7 @@
     "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link":"マークダウン設定ページに移動",
     "xss_prevent_setting_link":"マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "callback_URL": "コールバックURL",
+    "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
     "guest_mode": {
     "guest_mode": {
       "deny": "アカウントを持たないユーザーはアクセス不可",
       "deny": "アカウントを持たないユーザーはアクセス不可",
       "readonly": "閲覧のみ許可"
       "readonly": "閲覧のみ許可"
@@ -397,17 +401,23 @@
       "name": "SAML",
       "name": "SAML",
       "entry_point": "エントリーポイント",
       "entry_point": "エントリーポイント",
       "issuer": "発行者",
       "issuer": "発行者",
-      "mapping_detail": "新規ユーザーの%sに関連付ける属性"
+      "First Name": "姓",
+      "Last Name": "名",
+      "id_detail": "SAML Identity プロバイダ内で一意に識別可能な値を格納している属性",
+      "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
+      "mapping_detail": "新規ユーザーの%sに関連付ける属性",
+      "cert_detail1": "IdP からのレスポンスの validation を行うための、PEMエンコードされた X.509 証明書",
+      "cert_detail2": "空の場合は環境変数 <code>SAML_CERT</code> を利用し、そちらも存在しない場合は validation 自体を行いません"
     },
     },
     "OAuth": {
     "OAuth": {
       "register": "%sに登録",
       "register": "%sに登録",
-      "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力<br>(<code>%s</code>は環境に合わせて変更してください)",
+      "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力",
       "Google": {
       "Google": {
         "name": "Google OAuth",
         "name": "Google OAuth",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
         "register_2": "プロジェクトがない場合はプロジェクトを作成",
         "register_2": "プロジェクトがない場合はプロジェクトを作成",
         "register_3": "認証情報を作成 &rightarrow; OAuthクライアントID &rightarrow; ウェブアプリケーションを選択",
         "register_3": "認証情報を作成 &rightarrow; OAuthクライアントID &rightarrow; ウェブアプリケーションを選択",
-        "register_4": "承認済みのリダイレクトURIを<code>%s</code>としてGrowiを登録 (<code>%s</code>は環境に合わせて変更してください)",
+        "register_4": "承認済みのリダイレクトURIを<code>%s</code>としてGrowiを登録",
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力"
       },
       },
       "Facebook": {
       "Facebook": {
@@ -424,7 +434,7 @@
       "GitHub": {
       "GitHub": {
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
         "register_1": "<a href=\"%s\" target=\"_blank\">%s</a>へアクセス",
-        "register_2": "\"Authorization callback URL\"を<code>%s</code>としてGrowiを登録 (<code>%s</code>は環境に合わせて変更してください)",
+        "register_2": "\"Authorization callback URL\"を<code>%s</code>としてGrowiを登録",
         "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力"
         "register_3": "上記フォームにクライアントIDとクライアントシークレットを入力"
       },
       },
       "how_to": {
       "how_to": {

+ 47 - 0
src/migrations/20180926134048-make-email-unique.js

@@ -0,0 +1,47 @@
+'use strict';
+
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:make-email-unique');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+module.exports = {
+
+  async up(db, next) {
+    logger.info('Start migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const User = require('@server/models/user')();
+
+    // get all users who has 'deleted@deleted' email
+    const users = await User.find({email: 'deleted@deleted'});
+    if (users.length > 0) {
+      logger.info(`${users.length} users found. Replace email...`, users);
+    }
+
+    // make email unique
+    const promises = users.map(user => {
+      const now = new Date();
+      const deletedLabel = `deleted_at_${now.getTime()}`;
+      user.email = `${deletedLabel}@deleted`;
+      return user.save();
+    });
+    await Promise.all(promises);
+
+    // sync index
+    logger.info('Invoking syncIndexes');
+    await User.syncIndexes();
+
+    await mongoose.disconnect();
+
+    logger.info('Migration has successfully terminated');
+    next();
+  },
+
+  down(db, next) {
+    // do not rollback
+    next();
+  }
+
+};

+ 94 - 0
src/migrations/20180927102719-init-serverurl.js

@@ -0,0 +1,94 @@
+'use strict';
+
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:init-serverurl');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const queryToFindSiteUrl = {
+  ns: 'crowi',
+  key: 'app:siteUrl',
+};
+
+/**
+ * check all values of the array are equal
+ * @see https://stackoverflow.com/a/35568895
+ */
+function isAllValuesSame(array) {
+  return !!array.reduce((a, b) => {
+    return (a === b) ? a : NaN;
+  });
+}
+
+module.exports = {
+
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = require('@server/models/config')();
+
+    // find 'app:siteUrl'
+    const siteUrlConfig = await Config.findOne({
+      ns: 'crowi',
+      key: 'app:siteUrl',
+    });
+    // exit if exists
+    if (siteUrlConfig != null) {
+      logger.info('\'app:siteUrl\' is already exists. This migration terminates without any changes.');
+      return;
+    }
+
+    // find all callbackUrls
+    const configs = await Config.find({
+      ns: 'crowi',
+      $or: [
+        { key: 'security:passport-github:callbackUrl' },
+        { key: 'security:passport-google:callbackUrl' },
+        { key: 'security:passport-twitter:callbackUrl' },
+        { key: 'security:passport-saml:callbackUrl' },
+      ]
+    });
+
+    // determine serverUrl
+    let siteUrl;
+    if (configs.length > 0) {
+      logger.info(`${configs.length} configs which has callbackUrl found: `);
+      logger.info(configs);
+
+      // extract domain
+      const siteUrls = configs.map(config => {
+        // see https://regex101.com/r/Q0Isjo/2
+        const match = config.value.match(/^"(https?:\/\/[^/]+).*"$/);
+        return (match != null) ? match[1] : null;
+      }).filter(value => value != null);
+
+      // determine serverUrl if all values are same
+      if (siteUrls.length > 0 && isAllValuesSame(siteUrls)) {
+        siteUrl = siteUrls[0];
+      }
+    }
+
+    if (siteUrl != null) {
+      await Config.findOneAndUpdateByNsAndKey('crowi', 'app:siteUrl', siteUrl);
+      logger.info('Migration has successfully applied');
+    }
+  },
+
+  async down(db) {
+    logger.info('Undo migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = require('@server/models/config')();
+
+    // remote 'app:siteUrl'
+    await Config.findOneAndDelete({
+      ns: 'crowi',
+      key: 'app:siteUrl',
+    });
+
+    logger.info('Migration has successfully undoed');
+  }
+
+};

+ 3 - 1
src/server/.node-dev.json

@@ -1,6 +1,8 @@
 {
 {
   "ignore": [
   "ignore": [
     "package.json",
     "package.json",
-    "public/manifest.json"
+    "public/manifest.json",
+    "config/env.",
+    "config/webpack."
   ]
   ]
 }
 }

+ 3 - 3
src/server/app.js

@@ -9,7 +9,7 @@ require('module-alias/register');
 
 
 const logger = require('@alias/logger')('growi');
 const logger = require('@alias/logger')('growi');
 const helpers = require('@commons/util/helpers');
 const helpers = require('@commons/util/helpers');
-const growi = new (require('./crowi'))(helpers.root(), process.env);
+const growi = new (require('./crowi'))(helpers.root());
 
 
 
 
 /************************************
 /************************************
@@ -24,10 +24,10 @@ process.on('unhandledRejection', (reason, p) => {
 });
 });
 
 
 growi.start()
 growi.start()
-  .then(express => {
+  .then(server => {
     if (helpers.hasProcessFlag('ci')) {
     if (helpers.hasProcessFlag('ci')) {
       logger.info('"--ci" flag is detected. Exit process.');
       logger.info('"--ci" flag is detected. Exit process.');
-      express.close(() => {
+      server.close(() => {
         process.exit();
         process.exit();
       });
       });
     }
     }

+ 47 - 11
src/server/crowi/dev.js

@@ -1,4 +1,4 @@
-const debug = require('debug')('growi:crowi:dev');
+const logger = require('@alias/logger')('growi:crowi:dev');
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 
 
@@ -50,30 +50,66 @@ class CrowiDev {
 
 
   /**
   /**
    *
    *
-   *
-   * @param {any} server http server
    * @param {any} app express
    * @param {any} app express
+   */
+  setupServer(app) {
+    const port = this.crowi.port;
+    let server = app;
+
+    // for log
+    let serverUrl = `http://localhost:${port}}`;
+
+    if (this.crowi.env.DEV_HTTPS) {
+      logger.info(`[${this.crowi.node_env}] Express server will start with HTTPS Self-Signed Certification`);
+
+      serverUrl = `https://localhost:${port}}`;
+
+      const fs = require('graceful-fs');
+      const https = require('https');
+
+      const options = {
+        key: fs.readFileSync(path.join(this.crowi.rootDir, './resource/certs/localhost/key.pem')),
+        cert: fs.readFileSync(path.join(this.crowi.rootDir, './resource/certs/localhost/cert.pem')),
+      };
+
+      server = https.createServer(options, app);
+    }
+
+    const eazyLogger = require('eazy-logger').Logger({
+      prefix: '[{green:GROWI}] ',
+      useLevelPrefixes: false,
+    });
+
+    eazyLogger.info('{bold:Server URLs:}');
+    eazyLogger.unprefixed('info', '{grey:=======================================}');
+    eazyLogger.unprefixed('info', `         APP: {magenta:${serverUrl}}`);
+    eazyLogger.unprefixed('info', '{grey:=======================================}');
+
+    return server;
+  }
+
+  /**
    *
    *
-   * @memberOf CrowiDev
+   * @param {any} app express
    */
    */
-  setup(server, app) {
+  setupExpressAfterListening(app) {
     this.setupHeaderDebugger(app);
     this.setupHeaderDebugger(app);
     this.setupBrowserSync(app);
     this.setupBrowserSync(app);
   }
   }
 
 
   setupHeaderDebugger(app) {
   setupHeaderDebugger(app) {
-    debug('setupHeaderDebugger');
+    logger.debug('setupHeaderDebugger');
 
 
     app.use((req, res, next) => {
     app.use((req, res, next) => {
       onHeaders(res, () => {
       onHeaders(res, () => {
-        debug('HEADERS GOING TO BE WRITTEN');
+        logger.debug('HEADERS GOING TO BE WRITTEN');
       });
       });
       next();
       next();
     });
     });
   }
   }
 
 
   setupBrowserSync(app) {
   setupBrowserSync(app) {
-    debug('setupBrowserSync');
+    logger.debug('setupBrowserSync');
 
 
     const browserSync = require('browser-sync');
     const browserSync = require('browser-sync');
     const bs = browserSync.create().init({
     const bs = browserSync.create().init({
@@ -93,12 +129,12 @@ class CrowiDev {
         && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
         && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
 
 
       const pluginNames = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
       const pluginNames = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
-      debug('loading Plugins for development', pluginNames);
+      logger.debug('[development] loading Plugins', pluginNames);
 
 
       // merge and remove duplicates
       // merge and remove duplicates
       if (pluginNames.length > 0) {
       if (pluginNames.length > 0) {
-        var PluginService = require('../plugins/plugin.service');
-        var pluginService = new PluginService(this.crowi, app);
+        const PluginService = require('../plugins/plugin.service');
+        const pluginService = new PluginService(this.crowi, app);
         pluginService.loadPlugins(pluginNames);
         pluginService.loadPlugins(pluginNames);
       }
       }
     }
     }

+ 4 - 3
src/server/crowi/express-init.js

@@ -57,17 +57,18 @@ module.exports = function(crowi, app) {
       , User = crowi.model('User')
       , User = crowi.model('User')
       , Config = crowi.model('Config')
       , Config = crowi.model('Config')
       ;
       ;
-    let baseUrl;
 
 
     app.set('tzoffset', tzoffset);
     app.set('tzoffset', tzoffset);
 
 
     req.config = config;
     req.config = config;
     req.csrfToken = null;
     req.csrfToken = null;
 
 
-    config.crowi['app:url'] = baseUrl = (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');
+    config.crowi['app:siteUrl:fixed'] = (config.crowi['app:siteUrl'] != null)
+      ? config.crowi['app:siteUrl']                                                                         // prioritized with v3.2.4 and above
+      : (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');   // auto generate (default with v3.2.3 and below)
 
 
     res.locals.req      = req;
     res.locals.req      = req;
-    res.locals.baseUrl  = baseUrl;
+    res.locals.baseUrl  = config.crowi['app:siteUrl:fixed'];
     res.locals.config   = config;
     res.locals.config   = config;
     res.locals.env      = env;
     res.locals.env      = env;
     res.locals.now      = now;
     res.locals.now      = now;

+ 33 - 52
src/server/crowi/index.js

@@ -15,7 +15,7 @@ const debug = require('debug')('growi:crowi')
 
 
   ;
   ;
 
 
-function Crowi(rootdir, env) {
+function Crowi(rootdir) {
   const self = this;
   const self = this;
 
 
   this.version = pkg.version;
   this.version = pkg.version;
@@ -45,7 +45,7 @@ function Crowi(rootdir, env) {
 
 
   this.models = {};
   this.models = {};
 
 
-  this.env = env;
+  this.env = process.env;
   this.node_env = this.env.NODE_ENV || 'development';
   this.node_env = this.env.NODE_ENV || 'development';
 
 
   this.port = this.env.PORT || 3000;
   this.port = this.env.PORT || 3000;
@@ -124,8 +124,8 @@ Crowi.prototype.getEnv = function() {
 // getter/setter of model instance
 // getter/setter of model instance
 //
 //
 Crowi.prototype.model = function(name, model) {
 Crowi.prototype.model = function(name, model) {
-  if (model) {
-    return this.models[name] = model;
+  if (model != null) {
+    this.models[name] = model;
   }
   }
 
 
   return this.models[name];
   return this.models[name];
@@ -146,17 +146,17 @@ Crowi.prototype.setupDatabase = function() {
 
 
   const mongoUri = getMongoUrl(this.env);
   const mongoUri = getMongoUrl(this.env);
 
 
-  return mongoose.connect(mongoUri);
+  return mongoose.connect(mongoUri, { useNewUrlParser: true });
 };
 };
 
 
 Crowi.prototype.setupSessionConfig = function() {
 Crowi.prototype.setupSessionConfig = function() {
-  var self = this
+  const self = this
     , session  = require('express-session')
     , session  = require('express-session')
-    , sessionConfig
     , sessionAge = (1000*3600*24*30)
     , sessionAge = (1000*3600*24*30)
     , redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null
     , redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null
     , mongoUrl = getMongoUrl(this.env)
     , mongoUrl = getMongoUrl(this.env)
     ;
     ;
+  let sessionConfig;
 
 
   return new Promise(function(resolve, reject) {
   return new Promise(function(resolve, reject) {
     sessionConfig = {
     sessionConfig = {
@@ -347,56 +347,37 @@ Crowi.prototype.getTokens = function() {
   return this.tokens;
   return this.tokens;
 };
 };
 
 
-Crowi.prototype.start = function() {
-  var self = this
-    , server
-    , io;
-
+Crowi.prototype.start = async function() {
   // init CrowiDev
   // init CrowiDev
-  if (self.node_env === 'development') {
+  if (this.node_env === 'development') {
     const CrowiDev = require('./dev');
     const CrowiDev = require('./dev');
-    this.crowiDev = new CrowiDev(self);
+    this.crowiDev = new CrowiDev(this);
     this.crowiDev.init();
     this.crowiDev.init();
   }
   }
 
 
-  return Promise.resolve()
-    .then(function() {
-      return self.init();
-    })
-    .then(function() {
-      return self.buildServer();
-    })
-    .then(function(express) {
-      return new Promise((resolve) => {
-        server = express.listen(self.port, function() {
-          logger.info(`[${self.node_env}] Express server listening on port ${self.port}`);
-
-          // setup for dev
-          if (self.node_env === 'development') {
-            const eazyLogger = require('eazy-logger').Logger({
-              prefix: '[{green:GROWI}] ',
-              useLevelPrefixes: false,
-            });
-
-            eazyLogger.info('{bold:Server URLs:}');
-            eazyLogger.unprefixed('info', '{grey:=======================================}');
-            eazyLogger.unprefixed('info', `         APP: {magenta:http://localhost:${self.port}}`);
-            eazyLogger.unprefixed('info', '{grey:=======================================}');
-
-            self.crowiDev.setup(server, express);
-          }
-          resolve(server);
-        });
-
-        io = require('socket.io')(server);
-        io.sockets.on('connection', function(socket) {
-        });
-        self.io = io;
-
-        // setup Express Routes
-        self.setupRoutesAtLast(express);
-      });
-    });
+  await this.init();
+  const express = await this.buildServer();
+
+  const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
+
+  // listen
+  const serverListening = server.listen(this.port, () => {
+    logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
+    if (this.node_env === 'development') {
+      this.crowiDev.setupExpressAfterListening(express);
+    }
+  });
+
+  // setup WebSocket
+  const io = require('socket.io')(serverListening);
+  io.sockets.on('connection', function(socket) {
+  });
+  this.io = io;
+
+  // setup Express Routes
+  this.setupRoutesAtLast(express);
+
+  return serverListening;
 };
 };
 
 
 Crowi.prototype.buildServer = function() {
 Crowi.prototype.buildServer = function() {

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

@@ -4,7 +4,8 @@ var form = require('express-form')
   , field = form.field;
   , field = form.field;
 
 
 module.exports = form(
 module.exports = form(
-  field('settingForm[app:title]').required(),
+  field('settingForm[app:title]').trim(),
+  field('settingForm[app:siteUrl]').trim().required().isUrl(),
   field('settingForm[app:confidential]'),
   field('settingForm[app:confidential]'),
   field('settingForm[app:fileUpload]').trim().toBooleanStrict()
   field('settingForm[app:fileUpload]').trim().toBooleanStrict()
 );
 );

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

@@ -8,6 +8,5 @@ module.exports = form(
   field('settingForm[security:passport-github:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-github:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-github:clientId]').trim(),
   field('settingForm[security:passport-github:clientId]').trim(),
   field('settingForm[security:passport-github:clientSecret]').trim(),
   field('settingForm[security:passport-github:clientSecret]').trim(),
-  field('settingForm[security:passport-github:callbackUrl]').trim(),
   field('settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-github:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
 );
 );

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

@@ -8,6 +8,5 @@ module.exports = form(
   field('settingForm[security:passport-google:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-google:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-google:clientId]').trim(),
   field('settingForm[security:passport-google:clientId]').trim(),
   field('settingForm[security:passport-google:clientSecret]').trim(),
   field('settingForm[security:passport-google:clientSecret]').trim(),
-  field('settingForm[security:passport-google:callbackUrl]').trim(),
   field('settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-google:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
 );
 );

+ 8 - 8
src/server/form/admin/securityPassportSaml.js

@@ -5,13 +5,13 @@ const field = form.field;
 
 
 module.exports = form(
 module.exports = form(
   field('settingForm[security:passport-saml:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-saml:isEnabled]').trim().toBooleanStrict().required(),
-  field('settingForm[security:passport-saml:entryPoint]').trim(),
-  field('settingForm[security:passport-saml:callbackUrl]').trim(),
-  field('settingForm[security:passport-saml:issuer]').trim(),
-  field('settingForm[security:passport-saml:attrMapId]'),
-  field('settingForm[security:passport-saml:attrMapUsername]'),
-  field('settingForm[security:passport-saml:attrMapMail]'),
-  field('settingForm[security:passport-saml:attrMapFirstName]'),
-  field('settingForm[security:passport-saml:attrMapLastName]'),
+  field('settingForm[security:passport-saml:entryPoint]').trim().required().isUrl(),
+  field('settingForm[security:passport-saml:issuer]').trim().required(),
+  field('settingForm[security:passport-saml:attrMapId]').trim().required(),
+  field('settingForm[security:passport-saml:attrMapUsername]').trim().required(),
+  field('settingForm[security:passport-saml:attrMapMail]').trim().required(),
+  field('settingForm[security:passport-saml:attrMapFirstName]').trim(),
+  field('settingForm[security:passport-saml:attrMapLastName]').trim(),
+  field('settingForm[security:passport-saml:cert]').trim(),
   field('settingForm[security:passport-saml:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-saml:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
 );
 );

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

@@ -8,6 +8,5 @@ module.exports = form(
   field('settingForm[security:passport-twitter:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-twitter:isEnabled]').trim().toBooleanStrict().required(),
   field('settingForm[security:passport-twitter:consumerKey]').trim(),
   field('settingForm[security:passport-twitter:consumerKey]').trim(),
   field('settingForm[security:passport-twitter:consumerSecret]').trim(),
   field('settingForm[security:passport-twitter:consumerSecret]').trim(),
-  field('settingForm[security:passport-twitter:callbackUrl]').trim(),
   field('settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-twitter:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
 );
 );

+ 19 - 13
src/server/models/config.js

@@ -21,6 +21,12 @@ module.exports = function(crowi) {
     value: { type: String, required: true }
     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
    * default values when GROWI is cleanly installed
    */
    */
@@ -67,6 +73,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
       'security:passport-ldap:isSameUsernameTreatedAsIdenticalUser': false,
+      'security:passport-saml:isEnabled' : false,
       'security:passport-google:isEnabled' : false,
       'security:passport-google:isEnabled' : false,
       'security:passport-github:isEnabled' : false,
       'security:passport-github:isEnabled' : false,
       'security:passport-twitter:isEnabled' : false,
       'security:passport-twitter:isEnabled' : false,
@@ -148,8 +155,10 @@ module.exports = function(crowi) {
   };
   };
 
 
   configSchema.statics.updateConfigCache = function(ns, config) {
   configSchema.statics.updateConfigCache = function(ns, config) {
-    var originalConfig = crowi.getConfig();
-    var newNSConfig = originalConfig[ns] || {};
+    validateCrowi();
+
+    const originalConfig = crowi.getConfig();
+    const newNSConfig = originalConfig[ns] || {};
     Object.keys(config).forEach(function(key) {
     Object.keys(config).forEach(function(key) {
       if (config[key] || config[key] === '' || config[key] === false) {
       if (config[key] || config[key] === '' || config[key] === false) {
         newNSConfig[key] = config[key];
         newNSConfig[key] = config[key];
@@ -223,16 +232,11 @@ module.exports = function(crowi) {
     return callback(null, configs);
     return callback(null, configs);
   };
   };
 
 
-  configSchema.statics.findAndUpdate = function(ns, key, value, callback) {
-    var Config = this;
-    Config.findOneAndUpdate(
+  configSchema.statics.findOneAndUpdateByNsAndKey = async function(ns, key, value) {
+    return this.findOneAndUpdate(
       { ns: ns, key: key },
       { ns: ns, key: key },
       { ns: ns, key: key, value: JSON.stringify(value) },
       { ns: ns, key: key, value: JSON.stringify(value) },
-      { upsert: true, },
-      function(err, config) {
-        debug('Config.findAndUpdate', err, config);
-        callback(err, config);
-      });
+      { upsert: true, });
   };
   };
 
 
   configSchema.statics.getConfig = function(callback) {
   configSchema.statics.getConfig = function(callback) {
@@ -310,7 +314,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   configSchema.statics.isUploadable = function(config) {
   configSchema.statics.isUploadable = function(config) {
-    var method = crowi.env.FILE_UPLOAD || 'aws';
+    const method = process.env.FILE_UPLOAD || 'aws';
 
 
     if (method == 'aws' && (
     if (method == 'aws' && (
       !config.crowi['aws:accessKeyId'] ||
       !config.crowi['aws:accessKeyId'] ||
@@ -496,6 +500,8 @@ module.exports = function(crowi) {
   };
   };
 
 
   configSchema.statics.customTitle = function(config, page) {
   configSchema.statics.customTitle = function(config, page) {
+    validateCrowi();
+
     const key = 'customize:title';
     const key = 'customize:title';
     let customTitle = getValueForCrowiNS(config, key);
     let customTitle = getValueForCrowiNS(config, key);
 
 
@@ -588,12 +594,12 @@ module.exports = function(crowi) {
 
 
   configSchema.statics.getLocalconfig = function(config) {
   configSchema.statics.getLocalconfig = function(config) {
     const Config = this;
     const Config = this;
-    const env = crowi.getEnv();
+    const env = process.env;
 
 
     const local_config = {
     const local_config = {
       crowi: {
       crowi: {
         title: Config.appTitle(crowi),
         title: Config.appTitle(crowi),
-        url: config.crowi['app:url'] || '',
+        url: config.crowi['app:siteUrl:fixed'] || '',
       },
       },
       upload: {
       upload: {
         image: Config.isUploadable(config),
         image: Config.isUploadable(config),

+ 21 - 4
src/server/models/user.js

@@ -21,9 +21,14 @@ module.exports = function(crowi) {
 
 
     , PAGE_ITEMS        = 50
     , PAGE_ITEMS        = 50
 
 
-    , userEvent = crowi.event('user')
-
   let userSchema;
   let userSchema;
+  let userEvent;
+
+  // init event
+  if (crowi != null) {
+    userEvent = crowi.event('user');
+    userEvent.on('activated', userEvent.onActivated);
+  }
 
 
   userSchema = new mongoose.Schema({
   userSchema = new mongoose.Schema({
     userId: String,
     userId: String,
@@ -55,9 +60,15 @@ module.exports = function(crowi) {
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(uniqueValidator);
   userSchema.plugin(uniqueValidator);
 
 
-  userEvent.on('activated', userEvent.onActivated);
+  function validateCrowi() {
+    if (crowi == null) {
+      throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
+    }
+  }
 
 
   function decideUserStatusOnRegistration() {
   function decideUserStatusOnRegistration() {
+    validateCrowi();
+
     var Config = crowi.model('Config'),
     var Config = crowi.model('Config'),
       config = crowi.getConfig();
       config = crowi.getConfig();
 
 
@@ -96,6 +107,8 @@ module.exports = function(crowi) {
   }
   }
 
 
   function generatePassword(password) {
   function generatePassword(password) {
+    validateCrowi();
+
     var hasher = crypto.createHash('sha256');
     var hasher = crypto.createHash('sha256');
     hasher.update(crowi.env.PASSWORD_SEED + password);
     hasher.update(crowi.env.PASSWORD_SEED + password);
 
 
@@ -302,6 +315,8 @@ module.exports = function(crowi) {
   };
   };
 
 
   userSchema.statics.isEmailValid = function(email, callback) {
   userSchema.statics.isEmailValid = function(email, callback) {
+    validateCrowi();
+
     var config = crowi.getConfig()
     var config = crowi.getConfig()
       , whitelist = config.crowi['security:registrationWhiteList'];
       , whitelist = config.crowi['security:registrationWhiteList'];
 
 
@@ -573,6 +588,8 @@ module.exports = function(crowi) {
   };
   };
 
 
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
+    validateCrowi();
+
     var User = this
     var User = this
       , createdUserList = []
       , createdUserList = []
       , Config = crowi.model('Config')
       , Config = crowi.model('Config')
@@ -658,7 +675,7 @@ module.exports = function(crowi) {
                 vars: {
                 vars: {
                   email: user.email,
                   email: user.email,
                   password: user.password,
                   password: user.password,
-                  url: config.crowi['app:url'],
+                  url: config.crowi['app:siteUrl:fixed'],
                   appTitle: Config.appTitle(config),
                   appTitle: Config.appTitle(config),
                 }
                 }
               },
               },

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

@@ -55,7 +55,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id
    * @apiParam {String} page_id
    */
    */
   api.list = function(req, res) {
   api.list = function(req, res) {
-    var id = req.query.page_id || null;
+    const id = req.query.page_id || null;
     if (!id) {
     if (!id) {
       return res.json(ApiResponse.error('Parameters page_id is required.'));
       return res.json(ApiResponse.error('Parameters page_id is required.'));
     }
     }
@@ -69,10 +69,10 @@ module.exports = function(crowi, app) {
       //   2. ensure backward compatibility of data
       //   2. ensure backward compatibility of data
 
 
       // var config = crowi.getConfig();
       // var config = crowi.getConfig();
-      // var baseUrl = (config.crowi['app:url'] || '');
+      // var baseUrl = (config.crowi['app:siteUrl:fixed'] || '');
       return res.json(ApiResponse.success({
       return res.json(ApiResponse.success({
         attachments: attachments.map(at => {
         attachments: attachments.map(at => {
-          var fileUrl = at.fileUrl;
+          const fileUrl = at.fileUrl;
           at = at.toObject();
           at = at.toObject();
           // at.url = baseUrl + fileUrl;
           // at.url = baseUrl + fileUrl;
           at.url = fileUrl;
           at.url = fileUrl;

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

@@ -41,10 +41,10 @@ module.exports = function(crowi, app) {
 
 
     let origin = `${req.protocol}://${req.get('host')}`;
     let origin = `${req.protocol}://${req.get('host')}`;
 
 
-    // use config.crowi['app:url'] when exist req.headers['x-forwarded-proto'].
+    // use config.crowi['app:siteUrl:fixed'] when exist req.headers['x-forwarded-proto'].
     // refs: lib/crowi/express-init.js
     // refs: lib/crowi/express-init.js
-    if (config.crowi && config.crowi['app:url']) {
-      origin = config.crowi['app:url'];
+    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
+      origin = config.crowi['app:siteUrl:fixed'];
     }
     }
 
 
     // generate definitions to replace
     // generate definitions to replace

+ 28 - 11
src/server/routes/login-passport.js

@@ -321,30 +321,42 @@ module.exports = function(crowi, app) {
     passport.authenticate('saml')(req, res);
     passport.authenticate('saml')(req, res);
   };
   };
 
 
-  const loginPassportSamlCallback = async(req, res, next) => {
+  const loginPassportSamlCallback = async(req, res) => {
     const providerId = 'saml';
     const providerId = 'saml';
     const strategyName = 'saml';
     const strategyName = 'saml';
-    const attrMapId = config.crowi['security:passport-saml:attrMapId'] || 'id';
-    const attrMapUsername = config.crowi['security:passport-saml:attrMapUsername'] || 'userName';
+    const attrMapId = config.crowi['security:passport-saml:attrMapId'];
+    const attrMapUsername = config.crowi['security:passport-saml:attrMapUsername'];
+    const attrMapMail = config.crowi['security:passport-saml:attrMapMail'];
     const attrMapFirstName = config.crowi['security:passport-saml:attrMapFirstName'] || 'firstName';
     const attrMapFirstName = config.crowi['security:passport-saml:attrMapFirstName'] || 'firstName';
     const attrMapLastName = config.crowi['security:passport-saml:attrMapLastName'] || 'lastName';
     const attrMapLastName = config.crowi['security:passport-saml:attrMapLastName'] || 'lastName';
-    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+
+    const response = await promisifiedPassportAuthentication(req, res, loginFailure, strategyName);
     const userInfo = {
     const userInfo = {
       'id': response[attrMapId],
       'id': response[attrMapId],
       'username': response[attrMapUsername],
       'username': response[attrMapUsername],
-      'name': `${response[attrMapFirstName]} ${response[attrMapLastName]}`,
+      'email': response[attrMapMail]
     };
     };
 
 
-    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    // determine name
+    const firstName = response[attrMapFirstName];
+    const lastName = response[attrMapLastName];
+    if (firstName != null || lastName != null) {
+      userInfo['name'] = `${response[attrMapFirstName]} ${response[attrMapLastName]}`.trim();
+    }
+
+    const externalAccount = await getOrCreateUser(req, res, loginFailure, userInfo, providerId);
     if (!externalAccount) {
     if (!externalAccount) {
-      return loginFailure(req, res, next);
+      return loginFailure(req, res);
     }
     }
 
 
     const user = await externalAccount.getPopulatedUser();
     const user = await externalAccount.getPopulatedUser();
 
 
     // login
     // login
     req.logIn(user, err => {
     req.logIn(user, err => {
-      if (err) { return next(err) }
+      if (err != null) {
+        logger.error(err);
+        return loginFailure(req, res);
+      }
       return loginSuccess(req, res, user);
       return loginSuccess(req, res, user);
     });
     });
   };
   };
@@ -356,17 +368,22 @@ module.exports = function(crowi, app) {
           return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
           return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
         }
         }
 
 
+        logger.debug(`--- authenticate with ${strategyName} strategy ---`);
+
         if (err) {
         if (err) {
           logger.error(`'${strategyName}' passport authentication error: `, err);
           logger.error(`'${strategyName}' passport authentication error: `, err);
-          req.flash('warningMessage', `Error occured in '${strategyName}' passport authentication`);
-          return next(); // pass and the flash message is displayed when all of authentications are failed.
+          req.flash('warningMessage', `Error occured in '${strategyName}' passport authentication`);  // pass and the flash message is displayed when all of authentications are failed.
+          return next(req, res);
         }
         }
 
 
         // authentication failure
         // authentication failure
         if (!response) {
         if (!response) {
-          return next();
+          return next(req, res);
         }
         }
 
 
+        logger.debug('response', response);
+        logger.debug('info', info);
+
         resolve(response);
         resolve(response);
       })(req, res, next);
       })(req, res, next);
     });
     });

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

@@ -208,7 +208,7 @@ module.exports = function(crowi, app) {
                       vars: {
                       vars: {
                         createdUser: userData,
                         createdUser: userData,
                         adminUser: adminUser,
                         adminUser: adminUser,
-                        url: config.crowi['app:url'],
+                        url: config.crowi['app:siteUrl:fixed'],
                         appTitle: appTitle,
                         appTitle: appTitle,
                       }
                       }
                     },
                     },

+ 13 - 4
src/server/service/passport.js

@@ -298,7 +298,9 @@ class PassportService {
     passport.use(new GoogleStrategy({
     passport.use(new GoogleStrategy({
       clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
       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,
       clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
-      callbackURL: config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,
+      callbackURL: (config.crowi['app:siteUrl'] != null)
+        ? `${config.crowi['app:siteUrl']}/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
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
       if (profile) {
       if (profile) {
@@ -343,7 +345,9 @@ class PassportService {
     passport.use(new GitHubStrategy({
     passport.use(new GitHubStrategy({
       clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
       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,
       clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
-      callbackURL: config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,
+      callbackURL: (config.crowi['app:siteUrl'] != null)
+        ? `${config.crowi['app:siteUrl']}/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
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
       if (profile) {
       if (profile) {
@@ -388,7 +392,9 @@ class PassportService {
     passport.use(new TwitterStrategy({
     passport.use(new TwitterStrategy({
       consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
       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,
       consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
-      callbackURL: config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,
+      callbackURL: (config.crowi['app:siteUrl'] != null)
+        ? `${config.crowi['app:siteUrl']}/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
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
       if (profile) {
       if (profile) {
@@ -431,9 +437,12 @@ class PassportService {
 
 
     debug('SamlStrategy: setting up..');
     debug('SamlStrategy: setting up..');
     passport.use(new SamlStrategy({
     passport.use(new SamlStrategy({
-      path: config.crowi['security:passport-saml:callbackUrl'] || process.env.SAML_CALLBACK_URI,
       entryPoint: config.crowi['security:passport-saml:entryPoint'] || process.env.SAML_ENTRY_POINT,
       entryPoint: config.crowi['security:passport-saml:entryPoint'] || process.env.SAML_ENTRY_POINT,
+      callbackUrl: (config.crowi['app:siteUrl'] != null)
+        ? `${config.crowi['app:siteUrl']}/passport/saml/callback`                                 // auto-generated with v3.2.4 and above
+        : config.crowi['security:passport-saml:callbackUrl'] || process.env.SAML_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       issuer: config.crowi['security:passport-saml:issuer'] || process.env.SAML_ISSUER,
       issuer: config.crowi['security:passport-saml:issuer'] || process.env.SAML_ISSUER,
+      cert: config.crowi['security:passport-saml:cert'] || process.env.SAML_CERT,
     }, function(profile, done) {
     }, function(profile, done) {
       if (profile) {
       if (profile) {
         return done(null, profile);
         return done(null, profile);

+ 2 - 2
src/server/util/googleAuth.js

@@ -20,7 +20,7 @@ module.exports = function(config) {
   }
   }
 
 
   lib.createAuthUrl = function(req, callback) {
   lib.createAuthUrl = function(req, callback) {
-    var callbackUrl = config.crowi['app:url'] + '/google/callback';
+    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
     google.options({auth: oauth2Client});
 
 
@@ -33,7 +33,7 @@ module.exports = function(config) {
   };
   };
 
 
   lib.handleCallback = function(req, callback) {
   lib.handleCallback = function(req, callback) {
-    var callbackUrl = config.crowi['app:url'] + '/google/callback';
+    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
     google.options({auth: oauth2Client});
 
 

+ 6 - 6
src/server/util/slack.js

@@ -46,8 +46,8 @@ module.exports = function(crowi) {
 
 
   const convertMarkdownToMrkdwn = function(body) {
   const convertMarkdownToMrkdwn = function(body) {
     var url = '';
     var url = '';
-    if (config.crowi && config.crowi['app:url']) {
-      url = config.crowi['app:url'];
+    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
+      url = config.crowi['app:siteUrl:fixed'];
     }
     }
 
 
     body = body
     body = body
@@ -113,7 +113,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
-    const url = config.crowi['app:url'] || '';
+    const url = config.crowi['app:siteUrl:fixed'] || '';
     let body = page.revision.body;
     let body = page.revision.body;
 
 
     if (updateType == 'create') {
     if (updateType == 'create') {
@@ -148,7 +148,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
-    const url = config.crowi['app:url'] || '';
+    const url = config.crowi['app:siteUrl:fixed'] || '';
     const body = prepareAttachmentTextForComment(comment);
     const body = prepareAttachmentTextForComment(comment);
 
 
     const attachment = {
     const attachment = {
@@ -175,7 +175,7 @@ module.exports = function(crowi) {
 
 
   const getSlackMessageTextForPage = function(path, user, updateType) {
   const getSlackMessageTextForPage = function(path, user, updateType) {
     let text;
     let text;
-    const url = config.crowi['app:url'] || '';
+    const url = config.crowi['app:siteUrl:fixed'] || '';
 
 
     const pageUrl = `<${url}${path}|${path}>`;
     const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
     if (updateType == 'create') {
@@ -189,7 +189,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const getSlackMessageTextForComment = function(path, user) {
   const getSlackMessageTextForComment = function(path, user) {
-    const url = config.crowi['app:url'] || '';
+    const url = config.crowi['app:siteUrl:fixed'] || '';
     const pageUrl = `<${url}${path}|${path}>`;
     const pageUrl = `<${url}${path}|${path}>`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 
 

+ 9 - 1
src/server/views/admin/app.html

@@ -38,11 +38,19 @@
         <div class="form-group">
         <div class="form-group">
           <label for="settingForm[app:title]" class="col-xs-3 control-label">{{ t('app_setting.Site Name') }}</label>
           <label for="settingForm[app:title]" class="col-xs-3 control-label">{{ t('app_setting.Site Name') }}</label>
           <div class="col-xs-6">
           <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] | default('') }}">
+            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] | default('') }}" placeholder="GROWI">
             <p class="help-block">{{ t("app_setting.sitename_change") }}</p>
             <p class="help-block">{{ t("app_setting.sitename_change") }}</p>
           </div>
           </div>
         </div>
         </div>
 
 
+        <div class="form-group">
+          <label for="settingForm[app:siteUrl]" class="col-xs-3 control-label">{{ t('app_setting.Site URL') }}</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[app:siteUrl]" value="{{ settingForm['app:siteUrl'] | default('') }}" placeholder="e.g. https://my.growi.org">
+            <p class="help-block">{{ t("app_setting.siteurl_help") }}</p>
+          </div>
+        </div>
+
         <div class="form-group">
         <div class="form-group">
           <label for="settingForm[app:confidential]" class="col-xs-3 control-label">{{ t('app_setting.Confidential name') }}</label>
           <label for="settingForm[app:confidential]" class="col-xs-3 control-label">{{ t('app_setting.Confidential name') }}</label>
           <div class="col-xs-6">
           <div class="col-xs-6">

+ 10 - 10
src/server/views/admin/widget/passport/github.html

@@ -4,6 +4,7 @@
 
 
   {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
   {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
   {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
   {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
+  {% set callbackUrl = settingForm['app:siteUrl'] || '[INVALID]' + '/passport/github/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
     <label for="{{nameForIsGitHubEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.GitHub.name") }}</label>
     <label for="{{nameForIsGitHubEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.GitHub.name") }}</label>
@@ -47,16 +48,15 @@
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-github:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-github:callbackUrl]" value="{{ settingForm['security:passport-github:callbackUrl'] || '' }}"
-            placeholder="http(s)://${growi.host}/passport/github/callback">
-        <p class="help-block">
-          Input <code>http(s)://${growi.host}/passport/github/callback</code><br>
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GITHUB_CALLBACK_URI") }}
-          </small>
-        </p>
+          <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
+        <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
+        {% if !settingForm['app:siteUrl'] %}
+        <div class="alert alert-danger">
+          <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+        </div>
+        {% endif %}
       </div>
       </div>
     </div>
     </div>
 
 
@@ -98,7 +98,7 @@
   </h4>
   </h4>
   <ol id="collapseHelpForGithubOauth" class="collapse">
   <ol id="collapseHelpForGithubOauth" class="collapse">
     <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>
     <li>{{ t("security_setting.OAuth.GitHub.register_1", "https://github.com/settings/developers", "GitHub Developer Settings") }}</li>
-    <li>{{ t("security_setting.OAuth.GitHub.register_2", "https://${growi.host}/passport/github/callback", "${growi.host}") }}</li>
+    <li>{{ t("security_setting.OAuth.GitHub.register_2", callbackUrl) }}</li>
     <li>{{ t("security_setting.OAuth.GitHub.register_3") }}</li>
     <li>{{ t("security_setting.OAuth.GitHub.register_3") }}</li>
   </ol>
   </ol>
 </div>
 </div>

+ 10 - 10
src/server/views/admin/widget/passport/google-oauth.html

@@ -4,6 +4,7 @@
 
 
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
+  {% set callbackUrl = settingForm['app:siteUrl'] || '[INVALID]' + '/passport/google/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
     <label for="{{nameForIsGoogleEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Google.name") }}</label>
     <label for="{{nameForIsGoogleEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Google.name") }}</label>
@@ -47,16 +48,15 @@
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-google:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-google:callbackUrl]" value="{{ settingForm['security:passport-google:callbackUrl'] || '' }}"
-            placeholder="http(s)://${growi.host}/passport/google/callback">
-        <p class="help-block">
-          Input <code>http(s)://${growi.host}/passport/google/callback</code><br>
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_GOOGLE_CALLBACK_URI") }}
-          </small>
-        </p>
+          <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
+        <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
+        {% if !settingForm['app:siteUrl'] %}
+        <div class="alert alert-danger">
+          <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+        </div>
+        {% endif %}
       </div>
       </div>
     </div>
     </div>
 
 
@@ -100,7 +100,7 @@
     <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_1", "https://console.cloud.google.com/apis/credentials", "Google Cloud Platform API Manager") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_2") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_2") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_3") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_3") }}</li>
-    <li>{{ t("security_setting.OAuth.Google.register_4", "https://${growi.host}/passport/google/callback", "${growi.host}") }}</li>
+    <li>{{ t("security_setting.OAuth.Google.register_4", callbackUrl) }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_5") }}</li>
     <li>{{ t("security_setting.OAuth.Google.register_5") }}</li>
   </ol>
   </ol>
 </div>
 </div>

+ 57 - 18
src/server/views/admin/widget/passport/saml.html

@@ -35,16 +35,15 @@
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-saml:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-saml:callbackUrl]" value="{{ settingForm['security:passport-saml:callbackUrl'] || '' }}"
-            placeholder="http(s)://${growi.host}/passport/saml/callback">
-        <p class="help-block">
-          Input <code>http(s)://${growi.host}/passport/saml/callback</code><br>
-          <small>
-            {{ t("security_setting.Use env var if empty", "SAML_ISSUER") }}
-          </small>
-        </p>
+        <input class="form-control" type="text" value="{% if settingForm['app:siteUrl'] %}{{ settingForm['app:siteUrl'] }}{% else %}[INVALID] {% endif %}/passport/saml/callback" readonly>
+        <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'SAML Identity') }}</p>
+        {% if !settingForm['app:siteUrl'] %}
+        <div class="alert alert-danger">
+          <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+        </div>
+        {% endif %}
       </div>
       </div>
     </div>
     </div>
 
 
@@ -63,13 +62,13 @@
     <h4>Attribute Mapping</h4>
     <h4>Attribute Mapping</h4>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-saml:attrMapId]" class="col-xs-3 control-label">User ID</label>
+      <label for="settingForm[security:passport-saml:attrMapId]" class="col-xs-3 control-label">Identifier</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" placeholder="Default: id"
+        <input class="form-control" type="text"
             name="settingForm[security:passport-saml:attrMapId]" value="{{ settingForm['security:passport-saml:attrMapId'] || '' }}">
             name="settingForm[security:passport-saml:attrMapId]" value="{{ settingForm['security:passport-saml:attrMapId'] || '' }}">
         <p class="help-block">
         <p class="help-block">
           <small>
           <small>
-            {{ t("security_setting.SAML.mapping_detail", "User ID") }}
+            {{ t("security_setting.SAML.id_detail") }}
           </small>
           </small>
         </p>
         </p>
       </div>
       </div>
@@ -78,11 +77,11 @@
     <div class="form-group">
     <div class="form-group">
       <label for="settingForm[security:passport-saml:attrMapUsername]" class="col-xs-3 control-label">Username</label>
       <label for="settingForm[security:passport-saml:attrMapUsername]" class="col-xs-3 control-label">Username</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" placeholder="Default: username"
+        <input class="form-control" type="text"
             name="settingForm[security:passport-saml:attrMapUsername]" value="{{ settingForm['security:passport-saml:attrMapUsername'] || '' }}">
             name="settingForm[security:passport-saml:attrMapUsername]" value="{{ settingForm['security:passport-saml:attrMapUsername'] || '' }}">
         <p class="help-block">
         <p class="help-block">
           <small>
           <small>
-            {{ t("security_setting.SAML.mapping_detail", "Username") }}
+            {{ t("security_setting.SAML.username_detail") }}
           </small>
           </small>
         </p>
         </p>
       </div>
       </div>
@@ -106,26 +105,66 @@
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-saml:attrMapFirstName]" class="col-xs-3 control-label">First Name</label>
+      <label for="settingForm[security:passport-saml:attrMapMail]" class="col-xs-3 control-label">Mail</label>
+      <div class="col-xs-6">
+        <input class="form-control" type="text"
+            name="settingForm[security:passport-saml:attrMapMail]" value="{{ settingForm['security:passport-saml:attrMapMail'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.SAML.mapping_detail", t("Email")) }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-saml:attrMapFirstName]" class="col-xs-3 control-label">{{ t("security_setting.SAML.First Name") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
         <input class="form-control" type="text" placeholder="Default: firstName"
         <input class="form-control" type="text" placeholder="Default: firstName"
             name="settingForm[security:passport-saml:attrMapFirstName]" value="{{ settingForm['security:passport-saml:attrMapFirstName'] || '' }}">
             name="settingForm[security:passport-saml:attrMapFirstName]" value="{{ settingForm['security:passport-saml:attrMapFirstName'] || '' }}">
         <p class="help-block">
         <p class="help-block">
           <small>
           <small>
-            {{ t("security_setting.SAML.mapping_detail", "First Name") }}
+            {{ t("security_setting.SAML.mapping_detail", t("security_setting.SAML.First Name")) }}
           </small>
           </small>
         </p>
         </p>
       </div>
       </div>
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-saml:attrMapLastName]" class="col-xs-3 control-label">Last Name</label>
+      <label for="settingForm[security:passport-saml:attrMapLastName]" class="col-xs-3 control-label">{{ t("security_setting.SAML.Last Name") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
         <input class="form-control" type="text" placeholder="Default: lastName"
         <input class="form-control" type="text" placeholder="Default: lastName"
             name="settingForm[security:passport-saml:attrMapLastName]" value="{{ settingForm['security:passport-saml:attrMapLastName'] || '' }}">
             name="settingForm[security:passport-saml:attrMapLastName]" value="{{ settingForm['security:passport-saml:attrMapLastName'] || '' }}">
         <p class="help-block">
         <p class="help-block">
           <small>
           <small>
-            {{ t("security_setting.SAML.mapping_detail", "Last Name") }}
+            {{ t("security_setting.SAML.mapping_detail", t("security_setting.SAML.Last Name")) }}
+          </small>
+        </p>
+      </div>
+    </div>
+
+    <h4>Options</h4>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-saml:cert]" class="col-xs-3 control-label">Certificate</label>
+      <div class="col-xs-6">
+        <textarea class="form-control input-sm" type="text" rows="5" name="settingForm[security:passport-saml:cert]">{{ settingForm['security:passport-saml:cert'] || '' }}</textarea>
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.SAML.cert_detail1") }}<br>
+            {{ t("security_setting.SAML.cert_detail2") }}
+          </small>
+        </p>
+        <p>
+          <small>
+            e.g.
+            <pre>-----BEGIN CERTIFICATE-----
+MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
+UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
+...
+crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
+pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
+-----END CERTIFICATE-----</pre>
           </small>
           </small>
         </p>
         </p>
       </div>
       </div>

+ 10 - 8
src/server/views/admin/widget/passport/twitter.html

@@ -4,6 +4,7 @@
 
 
   {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
   {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
   {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
   {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
+  {% set callbackUrl = settingForm['app:siteUrl'] || '[INVALID]' + '/passport/twitter/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
     <label for="{{nameForIsTwitterEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Twitter.name") }}</label>
     <label for="{{nameForIsTwitterEnabled}}" class="col-xs-3 control-label">{{ t("security_setting.OAuth.Twitter.name") }}</label>
@@ -49,14 +50,15 @@
     </div>
     </div>
 
 
     <div class="form-group">
     <div class="form-group">
-      <label for="settingForm[security:passport-twitter:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <label class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
       <div class="col-xs-6">
       <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-twitter:callbackUrl]" value="{{ settingForm['security:passport-twitter:callbackUrl'] || '' }}">
-        <p class="help-block">
-          <small>
-            {{ t("security_setting.Use env var if empty", "OAUTH_TWITTER_CALLBACK_URL") }}
-          </small>
-        </p>
+          <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
+        <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
+        {% if !settingForm['app:siteUrl'] %}
+        <div class="alert alert-danger">
+          <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+        </div>
+        {% endif %}
       </div>
       </div>
     </div>
     </div>
 
 
@@ -101,7 +103,7 @@
     <li>{{ t("security_setting.OAuth.Twitter.register_1", "https://apps.twitter.com/", "Twitter Application Management") }}</li>
     <li>{{ t("security_setting.OAuth.Twitter.register_1", "https://apps.twitter.com/", "Twitter Application Management") }}</li>
     <li>{{ t("security_setting.OAuth.Twitter.register_2") }}</li>
     <li>{{ t("security_setting.OAuth.Twitter.register_2") }}</li>
     <li>{{ t("security_setting.OAuth.Twitter.register_3") }}</li>
     <li>{{ t("security_setting.OAuth.Twitter.register_3") }}</li>
-    <li>{{ t("security_setting.OAuth.Twitter.register_4", "https://${growi.host}/passport/twitter/callback", "${growi.host}") }}</li>
+    <li>{{ t("security_setting.OAuth.Twitter.register_4", callbackUrl) }}</li>
   </ol>
   </ol>
 </div>
 </div>
 
 

+ 1 - 1
src/server/views/modal/duplicate.html

@@ -16,7 +16,7 @@
             <div class="form-group">
             <div class="form-group">
               <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
               <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
               <div class="input-group">
               <div class="input-group">
-                <span class="input-group-addon">{{ config.crowi['app:url'] }}</span>
+                <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
                 <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
                 <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
               </div>
               </div>
             </div>
             </div>

+ 1 - 1
src/server/views/modal/rename.html

@@ -16,7 +16,7 @@
             <div class="form-group">
             <div class="form-group">
               <label for="newPageName">{{ t('New page name') }}</label><br>
               <label for="newPageName">{{ t('New page name') }}</label><br>
               <div class="input-group">
               <div class="input-group">
-                <span class="input-group-addon">{{ config.crowi['app:url'] }}</span>
+                <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
                 <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
                 <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
               </div>
               </div>
             </div>
             </div>

+ 5 - 5
src/test/crowi/crowi.test.js

@@ -13,21 +13,21 @@ describe('Test for Crowi application context', function() {
 
 
   describe('construction', function() {
   describe('construction', function() {
     it('initialize crowi context', function() {
     it('initialize crowi context', function() {
-      const crowi = new Crowi(helpers.root(), process.env);
+      const crowi = new Crowi(helpers.root());
       expect(crowi).to.be.instanceof(Crowi);
       expect(crowi).to.be.instanceof(Crowi);
       expect(crowi.version).to.equal(require('../../../package.json').version);
       expect(crowi.version).to.equal(require('../../../package.json').version);
       expect(crowi.env).to.be.an('Object');
       expect(crowi.env).to.be.an('Object');
     });
     });
 
 
     it('config getter, setter', function() {
     it('config getter, setter', function() {
-      const crowi = new Crowi(helpers.root(), process.env);
+      const crowi = new Crowi(helpers.root());
       expect(crowi.getConfig()).to.deep.equals({});
       expect(crowi.getConfig()).to.deep.equals({});
       crowi.setConfig({test: 1});
       crowi.setConfig({test: 1});
       expect(crowi.getConfig()).to.deep.equals({test: 1});
       expect(crowi.getConfig()).to.deep.equals({test: 1});
     });
     });
 
 
     it('model getter, setter', function() {
     it('model getter, setter', function() {
-      const crowi = new Crowi(helpers.root(), process.env);
+      const crowi = new Crowi(helpers.root());
       // set
       // set
       crowi.model('hoge', { fuga: 1 });
       crowi.model('hoge', { fuga: 1 });
       expect(crowi.model('hoge')).to.deep.equals({ fuga: 1 });
       expect(crowi.model('hoge')).to.deep.equals({ fuga: 1 });
@@ -39,9 +39,9 @@ describe('Test for Crowi application context', function() {
       mongoose.disconnect(); // avoid error of Trying to open unclosed connection
       mongoose.disconnect(); // avoid error of Trying to open unclosed connection
     });
     });
     it('setup completed', function(done) {
     it('setup completed', function(done) {
-      const crowi = new Crowi(helpers.root(), process.env);
+      const crowi = new Crowi(helpers.root());
       // set
       // set
-      const p = crowi.setupDatabase()
+      const p = crowi.setupDatabase();
       expect(p).to.instanceof(Promise);
       expect(p).to.instanceof(Promise);
       p.then(function() {
       p.then(function() {
         expect(mongoose.connection.readyState).to.equals(1);
         expect(mongoose.connection.readyState).to.equals(1);

+ 2 - 2
src/test/utils.js

@@ -16,7 +16,7 @@ before('Create database connection and clean up', function (done) {
     return done();
     return done();
   }
   }
 
 
-  mongoose.connect(mongoUri);
+  mongoose.connect(mongoUri, { useNewUrlParser: true });
 
 
   function clearDB() {
   function clearDB() {
     for (var i in mongoose.connection.collections) {
     for (var i in mongoose.connection.collections) {
@@ -26,7 +26,7 @@ before('Create database connection and clean up', function (done) {
   }
   }
 
 
   if (mongoose.connection.readyState === 0) {
   if (mongoose.connection.readyState === 0) {
-    mongoose.connect(mongoUri, function (err) {
+    mongoose.connect(mongoUri, { useNewUrlParser: true }, function (err) {
       if (err) {
       if (err) {
         throw err;
         throw err;
       }
       }

+ 147 - 98
yarn.lock

@@ -377,6 +377,10 @@ ansistyles@~0.1.1:
   version "0.1.3"
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
 
 
+any-promise@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+
 anymatch@^1.3.0:
 anymatch@^1.3.0:
   version "1.3.2"
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -545,22 +549,16 @@ async@1.5.2, async@^1.4.0:
   version "1.5.2"
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
 
-async@2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+async@2.6.1, async@^2.1.5, async@^2.4.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
   dependencies:
   dependencies:
-    lodash "^4.14.0"
+    lodash "^4.17.10"
 
 
 async@^0.9.0:
 async@^0.9.0:
   version "0.9.2"
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
 
-async@^2.1.5, async@^2.4.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
-  dependencies:
-    lodash "^4.17.10"
-
 async@^2.3.0:
 async@^2.3.0:
   version "2.6.0"
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
@@ -1308,11 +1306,7 @@ bluebird@3.0.5:
   version "3.0.5"
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.0.5.tgz#2ff9d07c9b3edb29d6d280fe07528365e7ecd392"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.0.5.tgz#2ff9d07c9b3edb29d6d280fe07528365e7ecd392"
 
 
-bluebird@3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
-
-bluebird@^3.5.1:
+bluebird@3.5.1, bluebird@^3.5.1:
   version "3.5.1"
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
 
 
@@ -1409,9 +1403,9 @@ browser-bunyan@^1.3.0:
     "@browser-bunyan/console-raw-stream" "^1.3.0"
     "@browser-bunyan/console-raw-stream" "^1.3.0"
     "@browser-bunyan/levels" "^1.3.0"
     "@browser-bunyan/levels" "^1.3.0"
 
 
-browser-stdout@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
+browser-stdout@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
 
 
 browser-sync-ui@v1.0.1:
 browser-sync-ui@v1.0.1:
   version "1.0.1"
   version "1.0.1"
@@ -1527,10 +1521,18 @@ bs-recipes@1.3.4:
   version "1.3.4"
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.3.4.tgz#0d2d4d48a718c8c044769fdc4f89592dc8b69585"
   resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.3.4.tgz#0d2d4d48a718c8c044769fdc4f89592dc8b69585"
 
 
+bson@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0"
+
 bson@~1.0.4:
 bson@~1.0.4:
   version "1.0.4"
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c"
 
 
+bson@~1.0.5:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.9.tgz#12319f8323b1254739b7c6bef8d3e89ae05a2f57"
+
 buffer-equal-constant-time@1.0.1:
 buffer-equal-constant-time@1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@@ -1857,7 +1859,7 @@ cli-cursor@^2.1.0:
   dependencies:
   dependencies:
     restore-cursor "^2.0.0"
     restore-cursor "^2.0.0"
 
 
-cli-table@^0.3.1:
+cli-table@0.3.1, cli-table@^0.3.1:
   version "0.3.1"
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
   dependencies:
   dependencies:
@@ -2003,18 +2005,18 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
   dependencies:
     delayed-stream "~1.0.0"
     delayed-stream "~1.0.0"
 
 
-commander@2.11.0:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+commander@2.15.1, commander@^2.2.0:
+  version "2.15.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
+commander@2.18.0:
+  version "2.18.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970"
 
 
 commander@^2.11.0, commander@^2.9.0:
 commander@^2.11.0, commander@^2.9.0:
   version "2.12.2"
   version "2.12.2"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
 
 
-commander@^2.2.0:
-  version "2.15.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
-
 commander@~2.13.0:
 commander@~2.13.0:
   version "2.13.0"
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
@@ -2644,18 +2646,14 @@ diff2html@^2.3.3:
     lodash "^4.17.4"
     lodash "^4.17.4"
     whatwg-fetch "^2.0.3"
     whatwg-fetch "^2.0.3"
 
 
-diff@3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
+diff@3.5.0, diff@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
 
 
 diff@^3.3.1:
 diff@^3.3.1:
   version "3.4.0"
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
 
 
-diff@^3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
-
 diffie-hellman@^5.0.0:
 diffie-hellman@^5.0.0:
   version "5.0.2"
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -3565,6 +3563,14 @@ fs-extra@3.0.1:
     jsonfile "^3.0.0"
     jsonfile "^3.0.0"
     universalify "^0.1.0"
     universalify "^0.1.0"
 
 
+fs-extra@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6"
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
 fs-minipass@^1.2.5:
 fs-minipass@^1.2.5:
   version "1.2.5"
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@@ -3851,9 +3857,9 @@ growi-pluginkit@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/growi-pluginkit/-/growi-pluginkit-1.1.0.tgz#8e837cd0ba50e6a13eacc75882e09c96eda10679"
   resolved "https://registry.yarnpkg.com/growi-pluginkit/-/growi-pluginkit-1.1.0.tgz#8e837cd0ba50e6a13eacc75882e09c96eda10679"
 
 
-growl@1.10.3:
-  version "1.10.3"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
+growl@1.10.5:
+  version "1.10.5"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
 
 
 growly@^1.2.0:
 growly@^1.2.0:
   version "1.3.0"
   version "1.3.0"
@@ -4819,6 +4825,12 @@ jsonfile@^3.0.0:
   optionalDependencies:
   optionalDependencies:
     graceful-fs "^4.1.6"
     graceful-fs "^4.1.6"
 
 
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
 jsonify@~0.0.0:
 jsonify@~0.0.0:
   version "0.0.0"
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -4878,9 +4890,9 @@ jws@^3.1.4:
     jwa "^1.1.4"
     jwa "^1.1.4"
     safe-buffer "^5.0.1"
     safe-buffer "^5.0.1"
 
 
-kareem@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.0.1.tgz#f17f77e9032f64aa402b334f91fb4407fe4c042c"
+kareem@2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.2.1.tgz#9950809415aa3cde62ab43b4f7b919d99816e015"
 
 
 keycode@^2.1.2:
 keycode@^2.1.2:
   version "2.1.9"
   version "2.1.9"
@@ -5166,6 +5178,10 @@ lodash.uniq@^4.5.0:
   version "4.5.0"
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
 
+lodash@4.17.11:
+  version "4.17.11"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
 lodash@^3.10.1:
 lodash@^3.10.1:
   version "3.10.1"
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -5368,6 +5384,10 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
     errno "^0.1.3"
     errno "^0.1.3"
     readable-stream "^2.0.1"
     readable-stream "^2.0.1"
 
 
+memory-pager@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.1.0.tgz#9308915e0e972849fefbae6f8bc95d6b350e7344"
+
 memorystream@^0.3.1:
 memorystream@^0.3.1:
   version "0.3.1"
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
   resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
@@ -5444,6 +5464,19 @@ micromatch@^3.1.4, micromatch@^3.1.8:
     snapdragon "^0.8.1"
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
     to-regex "^3.0.2"
 
 
+migrate-mongo@^3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/migrate-mongo/-/migrate-mongo-3.0.5.tgz#c4d83ca285361a1005f147f22e3ba2d344800e16"
+  dependencies:
+    async "2.6.1"
+    cli-table "0.3.1"
+    commander "2.18.0"
+    fs-extra "7.0.0"
+    lodash "4.17.11"
+    moment "2.22.2"
+    mongodb "3.1.6"
+    shifting "1.2.5"
+
 miller-rabin@^4.0.0:
 miller-rabin@^4.0.0:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
   resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5499,7 +5532,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
 
 
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
   dependencies:
@@ -5573,20 +5606,21 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdir
   dependencies:
   dependencies:
     minimist "0.0.8"
     minimist "0.0.8"
 
 
-mocha@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.0.tgz#cccac988b0bc5477119cba0e43de7af6d6ad8f4e"
+mocha@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
   dependencies:
   dependencies:
-    browser-stdout "1.3.0"
-    commander "2.11.0"
+    browser-stdout "1.3.1"
+    commander "2.15.1"
     debug "3.1.0"
     debug "3.1.0"
-    diff "3.3.1"
+    diff "3.5.0"
     escape-string-regexp "1.0.5"
     escape-string-regexp "1.0.5"
     glob "7.1.2"
     glob "7.1.2"
-    growl "1.10.3"
+    growl "1.10.5"
     he "1.1.1"
     he "1.1.1"
+    minimatch "3.0.4"
     mkdirp "0.5.1"
     mkdirp "0.5.1"
-    supports-color "4.4.0"
+    supports-color "5.4.0"
 
 
 module-alias@^2.0.6:
 module-alias@^2.0.6:
   version "2.0.6"
   version "2.0.6"
@@ -5596,7 +5630,7 @@ moment@2.20.1, moment@^2.10.6:
   version "2.20.1"
   version "2.20.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
 
 
-moment@2.x:
+moment@2.22.2, moment@2.x:
   version "2.22.2"
   version "2.22.2"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
 
 
@@ -5607,18 +5641,22 @@ mongodb-core@2.1.19:
     bson "~1.0.4"
     bson "~1.0.4"
     require_optional "~1.0.0"
     require_optional "~1.0.0"
 
 
-mongodb-core@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.0.1.tgz#ff6dc36ee96ff596953d80a6840d6731bc92efed"
+mongodb-core@3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.1.5.tgz#59ca67d7f6cea570d5437624a7afec8d752d477a"
   dependencies:
   dependencies:
-    bson "~1.0.4"
+    bson "^1.1.0"
     require_optional "^1.0.1"
     require_optional "^1.0.1"
+    safe-buffer "^5.1.2"
+  optionalDependencies:
+    saslprep "^1.0.0"
 
 
-mongodb@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.0.1.tgz#278ee8006257ec22798594a6259546825d6de1b2"
+mongodb@3.1.6:
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.1.6.tgz#6054641973b5bf5b5ae1c67dcbcf8fa88280273d"
   dependencies:
   dependencies:
-    mongodb-core "3.0.1"
+    mongodb-core "3.1.5"
+    safe-buffer "^5.1.2"
 
 
 mongodb@^2.0.36:
 mongodb@^2.0.36:
   version "2.2.35"
   version "2.2.35"
@@ -5628,37 +5666,39 @@ mongodb@^2.0.36:
     mongodb-core "2.1.19"
     mongodb-core "2.1.19"
     readable-stream "2.2.7"
     readable-stream "2.2.7"
 
 
-mongoose-legacy-pluralize@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.1.tgz#31ae25db45c30f1448c0f93f52769e903367c701"
+mongoose-legacy-pluralize@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4"
 
 
-mongoose-paginate@^5.0.0:
+mongoose-paginate@^5.0.3:
   version "5.0.3"
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/mongoose-paginate/-/mongoose-paginate-5.0.3.tgz#d7ae49ed5bf64f1f7af7620ea865b67058c55371"
   resolved "https://registry.yarnpkg.com/mongoose-paginate/-/mongoose-paginate-5.0.3.tgz#d7ae49ed5bf64f1f7af7620ea865b67058c55371"
   dependencies:
   dependencies:
     bluebird "3.0.5"
     bluebird "3.0.5"
 
 
-mongoose-unique-validator@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/mongoose-unique-validator/-/mongoose-unique-validator-2.0.0.tgz#76e406cd0b322bbbb79496e2b7425d885ce661c4"
+mongoose-unique-validator@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/mongoose-unique-validator/-/mongoose-unique-validator-2.0.2.tgz#a980e68638157cd8fcea86d754eeb2cf67e6bb76"
   dependencies:
   dependencies:
     lodash.foreach "^4.1.0"
     lodash.foreach "^4.1.0"
     lodash.get "^4.0.2"
     lodash.get "^4.0.2"
 
 
-mongoose@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.0.0.tgz#85e92fd72d718b21b3d55a62b09434da37354b5a"
+mongoose@^5.2.0:
+  version "5.2.17"
+  resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.2.17.tgz#8baeb60a675d00da03633d679a72457dbb5b2285"
   dependencies:
   dependencies:
-    async "2.1.4"
-    bson "~1.0.4"
-    kareem "2.0.1"
+    async "2.6.1"
+    bson "~1.0.5"
+    kareem "2.2.1"
     lodash.get "4.4.2"
     lodash.get "4.4.2"
-    mongodb "3.0.1"
-    mongoose-legacy-pluralize "1.0.1"
-    mpath "0.3.0"
-    mquery "3.0.0-rc0"
+    mongodb "3.1.6"
+    mongodb-core "3.1.5"
+    mongoose-legacy-pluralize "1.0.2"
+    mpath "0.5.1"
+    mquery "3.2.0"
     ms "2.0.0"
     ms "2.0.0"
     regexp-clone "0.0.1"
     regexp-clone "0.0.1"
+    safe-buffer "5.1.2"
     sliced "1.0.1"
     sliced "1.0.1"
 
 
 morgan@^1.9.0:
 morgan@^1.9.0:
@@ -5682,18 +5722,19 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     rimraf "^2.5.4"
     run-queue "^1.0.3"
     run-queue "^1.0.3"
 
 
-mpath@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.3.0.tgz#7a58f789e9b5fd3c94520634157960f26bd5ef44"
+mpath@0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.5.1.tgz#17131501f1ff9e6e4fbc8ffa875aa7065b5775ab"
 
 
-mquery@3.0.0-rc0:
-  version "3.0.0-rc0"
-  resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.0.0-rc0.tgz#05ec656e92f079828bedf4202e60fb8eaacb9f47"
+mquery@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.0.tgz#e276472abd5109686a15eb2a8e0761db813c81cc"
   dependencies:
   dependencies:
-    bluebird "3.5.0"
-    debug "2.6.9"
+    bluebird "3.5.1"
+    debug "3.1.0"
     regexp-clone "0.0.1"
     regexp-clone "0.0.1"
-    sliced "0.0.5"
+    safe-buffer "5.1.2"
+    sliced "1.0.1"
 
 
 ms@0.7.1:
 ms@0.7.1:
   version "0.7.1"
   version "0.7.1"
@@ -7757,7 +7798,7 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
   version "5.1.1"
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
 
 
-safe-buffer@^5.1.2:
+safe-buffer@5.1.2, safe-buffer@^5.1.2:
   version "5.1.2"
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 
 
@@ -7783,6 +7824,12 @@ sanitizer@0.1.3:
   version "0.1.3"
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
   resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
 
 
+saslprep@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.2.tgz#da5ab936e6ea0bbae911ffec77534be370c9f52d"
+  dependencies:
+    sparse-bitfield "^3.0.3"
+
 sass-graph@^2.2.4:
 sass-graph@^2.2.4:
   version "2.2.4"
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
   resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
@@ -8003,6 +8050,12 @@ shellwords@^0.1.0:
   version "0.1.1"
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
 
 
+shifting@1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/shifting/-/shifting-1.2.5.tgz#24b8b370586bb661a353c10a3e18e4a85abf5053"
+  dependencies:
+    any-promise "^1.3.0"
+
 signal-exit@^3.0.0, signal-exit@^3.0.2:
 signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -8049,10 +8102,6 @@ slice-ansi@1.0.0:
   dependencies:
   dependencies:
     is-fullwidth-code-point "^2.0.0"
     is-fullwidth-code-point "^2.0.0"
 
 
-sliced@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f"
-
 sliced@1.0.1:
 sliced@1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
   resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
@@ -8175,6 +8224,12 @@ source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
 
 
+sparse-bitfield@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11"
+  dependencies:
+    memory-pager "^1.0.2"
+
 spdx-correct@~1.0.0:
 spdx-correct@~1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -8433,11 +8488,11 @@ superagent@^1.2.0:
     readable-stream "1.0.27-1"
     readable-stream "1.0.27-1"
     reduce-component "1.0.1"
     reduce-component "1.0.1"
 
 
-supports-color@4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+supports-color@5.4.0, supports-color@^5.4.0:
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
   dependencies:
   dependencies:
-    has-flag "^2.0.0"
+    has-flag "^3.0.0"
 
 
 supports-color@^2.0.0:
 supports-color@^2.0.0:
   version "2.0.0"
   version "2.0.0"
@@ -8461,12 +8516,6 @@ supports-color@^5.3.0:
   dependencies:
   dependencies:
     has-flag "^3.0.0"
     has-flag "^3.0.0"
 
 
-supports-color@^5.4.0:
-  version "5.4.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
-  dependencies:
-    has-flag "^3.0.0"
-
 svgo@^1.0.0:
 svgo@^1.0.0:
   version "1.0.5"
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.0.5.tgz#7040364c062a0538abacff4401cea6a26a7a389a"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.0.5.tgz#7040364c062a0538abacff4401cea6a26a7a389a"