Преглед изворни кода

Merge branch 'master' into feat/integrate-with-hackmd

# Conflicts:
#	CHANGES.md
#	resource/styles/scss/_on-edit.scss
Yuki Takei пре 7 година
родитељ
комит
75cd0a4e4a
65 измењених фајлова са 1316 додато и 575 уклоњено
  1. 29 3
      CHANGES.md
  2. 8 1
      README.md
  3. 12 3
      THIRD-PARTY-NOTICES.md
  4. 8 7
      config/webpack.dev.js
  5. 0 1
      config/webpack.dll.js
  6. 14 13
      config/webpack.prod.js
  7. 1 1
      lib/crowi/express-init.js
  8. 5 2
      lib/crowi/index.js
  9. 11 0
      lib/form/admin/markdownXss.js
  10. 2 1
      lib/form/admin/securityPassportGitHub.js
  11. 2 1
      lib/form/admin/securityPassportGoogle.js
  12. 1 1
      lib/form/admin/userGroupCreate.js
  13. 1 0
      lib/form/index.js
  14. 34 16
      lib/locales/en-US/translation.json
  15. 20 3
      lib/locales/ja/translation.json
  16. 92 1
      lib/models/config.js
  17. 17 7
      lib/models/page.js
  18. 0 6
      lib/models/user-group.js
  19. 66 41
      lib/routes/admin.js
  20. 6 5
      lib/routes/index.js
  21. 18 16
      lib/routes/login-passport.js
  22. 12 7
      lib/service/passport.js
  23. 7 3
      lib/util/middlewares.js
  24. 19 0
      lib/util/recommendedXssWhiteList.js
  25. 26 26
      lib/util/swigFunctions.js
  26. 18 10
      lib/util/xss.js
  27. 13 0
      lib/util/xssOption.js
  28. 1 1
      lib/views/_form.html
  29. 1 1
      lib/views/admin/app.html
  30. 162 40
      lib/views/admin/markdown.html
  31. 224 194
      lib/views/admin/notification.html
  32. 12 3
      lib/views/admin/security.html
  33. 3 3
      lib/views/admin/user-group-detail.html
  34. 3 3
      lib/views/admin/user-groups.html
  35. 1 1
      lib/views/admin/users.html
  36. 25 0
      lib/views/admin/widget/passport/github.html
  37. 25 0
      lib/views/admin/widget/passport/google-oauth.html
  38. 0 1
      lib/views/layout/layout.html
  39. 1 1
      lib/views/modal/create_page.html
  40. 23 1
      lib/views/modal/shortcuts.html
  41. 7 0
      lib/views/widget/icon-keyboard-return-enter.html
  42. 2 2
      lib/views/widget/not_found_content.html
  43. 1 1
      lib/views/widget/page_alerts.html
  44. 1 1
      lib/views/widget/page_content.html
  45. 1 1
      lib/views/widget/page_list_and_timeline.html
  46. 4 4
      package.json
  47. 8 2
      resource/js/app.js
  48. 6 1
      resource/js/components/CopyButton.js
  49. 3 3
      resource/js/components/NewPageNameInput.js
  50. 4 1
      resource/js/components/Page/RevisionPath.js
  51. 10 1
      resource/js/components/Page/RevisionUrl.js
  52. 5 1
      resource/js/components/PageComment/CommentForm.js
  53. 2 0
      resource/js/components/PageEditor/AbstractEditor.js
  54. 24 5
      resource/js/components/PageEditor/CodeMirrorEditor.js
  55. 4 18
      resource/js/components/PageEditor/Editor.js
  56. 4 1
      resource/js/components/PageEditor/GrantSelector.js
  57. 2 0
      resource/js/legacy/crowi-admin.js
  58. 1 1
      resource/js/legacy/crowi.js
  59. 13 3
      resource/js/util/PreProcessor/XssFilter.js
  60. 4 0
      resource/styles/agile-admin/inverse/colors/mono-blue.scss
  61. 3 2
      resource/styles/scss/_admin.scss
  62. 125 0
      resource/styles/scss/_editor-overlay.scss
  63. 4 0
      resource/styles/scss/_shortcuts.scss
  64. 1 0
      resource/styles/scss/style.scss
  65. 154 103
      yarn.lock

+ 29 - 3
CHANGES.md

@@ -7,23 +7,48 @@ CHANGES
     * react
     * react-dom
 
-## 3.1.8-RC
+## 3.1.12-RC
+
+* Feature: Add XSS Settings
+* Improvement: Prevent XSS in various situations
+* Improvement: Add overlay styles for pasting file to comment form
+* Fix: Omit unnecessary css link
+    * Introduced by 3.1.10
+* Fix: Invitation mail do not be sent
+
+## 3.1.11
+
+* Fix: OAuth doesn't work in production because callback URL field cannot be specified
+    * Introduced by 3.1.9
+
+## 3.1.10
+
+* Fix: Enter key on react-bootstrap-typeahead doesn't submit
+    * Introduced by 3.1.9
+* Fix: CodeMirror of `/admin/customize` is broken
+    * Introduced by 3.1.9
+
+## 3.1.9
 
 * Feature: Login with Google Account
 * Feature: Login with GitHub Account
 * Feature: Attach files in Comment
 * Improvement: Write comment with CodeMirror Editor
+* Improvement: Post comment with `Ctrl-Enter`
 * Improvement: Place the commented page at the beginning of the list
 * Improvement: Resolve errors on IE11 (Experimental)
 * Support: Migrate to webpack 4 
 * Support: Upgrade libs
+    * eslint
     * react-bootstrap-typeahead
     * react-codemirror2
     * webpack
 
+## 3.1.8 (Missing number)
+
 ## 3.1.7
 
-* Fix: Update hidden input 'pageForm[grant]' when save with Ctrl-S
+* Fix: Update hidden input 'pageForm[grant]' when save with `Ctrl-S`
 * Fix: Show alert message when conflict
 * Fix: `BLOCKDIAG_URI` environment variable doesn't work
 * Fix: Paste in markdown list doesn't work correctly
@@ -63,6 +88,7 @@ CHANGES
 
 ## 3.1.4 (Missing number)
 
+
 ## 3.1.3 (Missing number)
 
 
@@ -125,7 +151,7 @@ CHANGES
 * Fix: Sidebar breaks editor layouts
 * Support: Switch the logger from 'pino' to 'bunyan'
 * Support: Set the alias for 'debug' to the debug function of 'bunyan'
-* Support: Translate /admin/security
+* Support: Translate `/admin/security`
 * Support: Optimize bundles
     * upgrade 'markdown-it-toc-and-anchor-with-slugid' and omit 'uslug'
 * Support: Optimize .eslintrc.js

+ 8 - 1
README.md

@@ -158,13 +158,20 @@ Environment Variables
     * NODE_ENV: `production` OR `development`.
     * PORT: Server port. default: `3000`
     * ELASTICSEARCH_URI: URI to connect to Elasticearch.
-    * REDIS_URI: URI to connect to Redis (to session store).
+    * REDIS_URI: URI to connect to Redis (use it as a session store instead of MongoDB).
     * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
     * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.
     * PASSWORD_SEED: A password seed used by password hash generator.
     * SECRET_TOKEN: A secret key for verifying the integrity of signed cookies.
     * SESSION_NAME: The name of the session ID cookie to set in the response by Express. default: `connect.sid`
     * FILE_UPLOAD: `aws` (default), `local`, `none`
+* **Option (Overwritable in admin page)**
+    * OAUTH_GOOGLE_CLIENT_ID: Google API client id 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_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`)
 
 
 Documentation

+ 12 - 3
THIRD-PARTY-NOTICES.md

@@ -12,9 +12,18 @@ For any licenses that require disclosure of source, sources are available at
 https://github.com/weseek/growi.
 
 
-1. crowi/crowi (https://github.com/crowi/crowi)
-2. Microsoft/vscode (https://github.com/Microsoft/vscode)
-3. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
+1. Apache License, Version 2.0 Derivative Works
+2. crowi/crowi (https://github.com/crowi/crowi)
+3. Microsoft/vscode (https://github.com/Microsoft/vscode)
+4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
+
+
+License Notice for Apache License, Version 2.0 Derivative Works
+--------------------------------------------------------
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+This software includes works that is distributed in the Apache License 2.0
 
 
 License Notice for Crowi

+ 8 - 7
config/webpack.dev.js

@@ -9,7 +9,7 @@ const helpers = require('./helpers');
 /*
  * Webpack Plugins
  */
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 /**
@@ -40,18 +40,19 @@ module.exports = require('./webpack.common')({
       },
       { // Dump CSS for HackMD
         test: /\.scss$/,
-        use: [
-          MiniCssExtractPlugin.loader,
-          'css-loader',
-          'scss-loader',
-        ],
+        use: ExtractTextPlugin.extract({
+          use: [
+            'css-loader',
+            'sass-loader'
+          ]
+        }),
         include: [helpers.root('resource/styles/hackmd')]
       },
     ],
   },
   plugins: [
 
-    new MiniCssExtractPlugin({
+    new ExtractTextPlugin({
       filename: '[name].bundle.css',
     }),
 

+ 0 - 1
config/webpack.dll.js

@@ -12,7 +12,6 @@ module.exports = {
       // Libraries
       'axios',
       'babel-polyfill',
-      'bootstrap-select',
       'browser-bunyan', 'bunyan-format',
       'codemirror', 'react-codemirror2',
       'clipboard',

+ 14 - 13
config/webpack.prod.js

@@ -8,7 +8,7 @@ const helpers = require('./helpers');
  * Webpack Plugins
  */
 const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
 const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
@@ -28,24 +28,25 @@ module.exports = require('./webpack.common')({
     rules: [
       {
         test: /\.scss$/,
-        use: [
-          MiniCssExtractPlugin.loader,
-          'css-loader',
-          { loader: 'postcss-loader', options: {
-            sourceMap: false,
-            plugins: (loader) => [
-              require('autoprefixer')()
-            ]
-          } },
-          'sass-loader'
-        ],
+        use: ExtractTextPlugin.extract({
+          use: [
+            'css-loader',
+            { loader: 'postcss-loader', options: {
+              sourceMap: false,
+              plugins: (loader) => [
+                require('autoprefixer')()
+              ]
+            } },
+            'sass-loader'
+          ]
+        }),
         include: [helpers.root('resource/styles/scss')]
       }
     ]
   },
   plugins: [
 
-    new MiniCssExtractPlugin({
+    new ExtractTextPlugin({
       filename: '[name].[hash].css',
     }),
 

+ 1 - 1
lib/crowi/express-init.js

@@ -127,7 +127,7 @@ module.exports = function(crowi, app) {
 
   app.use(flash());
 
-  app.use(middleware.swigFilters(app, swig));
+  app.use(middleware.swigFilters(crowi, app, swig));
   app.use(middleware.swigFunctions(crowi, app));
 
   app.use(middleware.csrfKeyGenerator(crowi, app));

+ 5 - 2
lib/crowi/index.js

@@ -1,7 +1,7 @@
 'use strict';
 
 
-var debug = require('debug')('growi:crowi')
+const debug = require('debug')('growi:crowi')
   , logger = require('@alias/logger')('growi:crowi')
   , pkg = require('@root/package.json')
   , path = require('path')
@@ -10,10 +10,12 @@ var debug = require('debug')('growi:crowi')
   , mongoose    = require('mongoose')
 
   , models = require('../models')
+
+  , Xss = require('../util/xss')
   ;
 
 function Crowi(rootdir, env) {
-  var self = this;
+  const self = this;
 
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
@@ -35,6 +37,7 @@ function Crowi(rootdir, env) {
   this.mailer = {};
   this.interceptorManager = {};
   this.passportService = null;
+  this.xss = new Xss();
 
   this.tokens = null;
 

+ 11 - 0
lib/form/admin/markdownXss.js

@@ -0,0 +1,11 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('markdownSetting[markdown:xss:isEnabledPrevention]').trim().toBooleanStrict(),
+  field('markdownSetting[markdown:xss:option]').trim().toInt(),
+  field('markdownSetting[markdown:xss:tagWhiteList]').trim(),
+  field('markdownSetting[markdown:xss:attrWhiteList]').trim()
+);

+ 2 - 1
lib/form/admin/securityPassportGitHub.js

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

+ 2 - 1
lib/form/admin/securityPassportGoogle.js

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

+ 1 - 1
lib/form/admin/userGroupCreate.js

@@ -1,6 +1,6 @@
 'use strict';
 
-var form = require('express-form')
+const form = require('express-form')
   , field = form.field;
 
 module.exports = form(

+ 1 - 0
lib/form/index.js

@@ -22,6 +22,7 @@ module.exports = {
     securityPassportGoogle: require('./admin/securityPassportGoogle'),
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     markdown: require('./admin/markdown'),
+    markdownXss: require('./admin/markdownXss'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customheader: require('./admin/customheader'),

+ 34 - 16
lib/locales/en-US/translation.json

@@ -206,20 +206,23 @@
   },
 
   "modal_shortcuts": {
-      "global": {
-          "title": "Global shortcuts",
-          "Open/Close shortcut help": "Open/Close shortcut help",
-          "Edit Page": "Edit Page",
-          "Create Page": "Create Page"
-      },
-      "editor": {
-          "title": "Editor shortcuts",
-          "Indent": "Indent",
-          "Outdent": "Outdent",
-          "Save Page": "Save Page",
-          "Delete Line": "Delete Line"
-
-              }
+    "global": {
+      "title": "Global shortcuts",
+      "Open/Close shortcut help": "Open/Close shortcut help",
+      "Edit Page": "Edit Page",
+      "Create Page": "Create Page"
+    },
+    "editor": {
+      "title": "Editor shortcuts",
+      "Indent": "Indent",
+      "Outdent": "Outdent",
+      "Save Page": "Save Page",
+      "Delete Line": "Delete Line"
+    },
+    "commentform": {
+      "title": "Comment Form shortcuts",
+      "Post": "Post"
+    }
   },
 
   "template": {
@@ -324,6 +327,9 @@
     "change_redirect_url": "Enter <code>https://${crowi.host}/google/callback</code> <br>(where <code>${crowi.host}</code> is your host name) for \"Authorized redirect URIs\".",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
+    "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
+    "xss_prevent_setting_link":"Go to Markdown settings",
+    "callback_URL": "Callback URL",
     "guest_mode": {
       "deny": "Deny Unregistered Users",
       "readonly": "View Only"
@@ -337,6 +343,7 @@
     "optional": "Optional",
     "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
     "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
+    "Use env var if empty": "Use env var <code>%s</code> if empty",
     "ldap": {
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
@@ -393,11 +400,22 @@
 	},
 
   "markdown_setting": {
-    "markdown_rendering": "You can change Markdown rendering settings.",
+    "line_break_setting": "Line Break Setting",
+    "line_break_setting_desc": "You can change line break settings.",
     "Enable Line Break": "Enable Line Break",
     "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
     "Enable Line Break for comment": "Enable Line Break in comment",
-    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML"
+    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
+    "XSS_setting": "Prevent XSS(Cross Site Scripting) Setting",
+    "XSS_setting_desc": "You can change the handling of HTML tags in markdown text.",
+    "Enable XSS prevention": "Enable XSS Prevention",
+    "Ignore all tags": "Ignore All Tags",
+    "Ignore all tags desc": "Stripe all HTML tags and attributes",
+    "Recommended setting": "Recommended Setting",
+    "Custom Whitelist": "Custom Whitelist",
+    "Tag names":"Tag names",
+    "Tag attributes":"Tag attributes",
+    "import_recommended": "Import recommended %s"
   },
 
   "customize_page": {

+ 20 - 3
lib/locales/ja/translation.json

@@ -234,6 +234,9 @@
         "Outdent": "左インデント",
         "Save Page": "保存",
         "Delete Line": "行削除"
+    },
+    "commentform": {
+      "Post": "投稿"
     }
   },
 
@@ -341,6 +344,9 @@
     "change_redirect_url": "承認済みのリダイレクトURLに、 <code>https://${crowi.host}/google/callback</code> を入力<br>(<code>${crowi.host}</code>は環境に合わせて変更してください)",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
+    "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
+    "xss_prevent_setting_link":"マークダウン設定ページに移動",
+    "callback_URL": "コールバックURL",
     "guest_mode": {
       "deny": "アカウントを持たないユーザーはアクセス不可",
       "readonly": "閲覧のみ許可"
@@ -354,6 +360,7 @@
     "optional": "オプション",
     "Treat username matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
     "Treat username matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
+    "Use env var if empty": "空の場合、環境変数 <code>%s</code> を利用します",
     "ldap": {
       "server_url_detail": "LDAP URLを <code>ldap://host:port/DN</code> または <code>ldaps://host:port/DN</code> の形式で入力してください。",
       "bind_mode": "Bind モード",
@@ -409,12 +416,22 @@
     }
   },
   "markdown_setting": {
-    "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
+    "line_break_setting": "Line Break設定",
+    "line_break_setting_desc": "Line Breakの設定を変更できます。",
     "Enable Line Break": "Line Break を有効にする",
     "Enable Line Break desc": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
     "Enable Line Break for comment": "コメント欄で Line Break を有効にする",
-    "Enable Line Break for comment desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
-
+    "Enable Line Break for comment desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+    "XSS_setting": "XSS(Cross Site Scripting)対策設定",
+    "XSS_setting_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
+    "Enable XSS prevention": "XSSを抑制する",
+    "Ignore all tags": "すべてのタグを抑制する",
+    "Ignore all tags desc": "すべてのHTMLタグと属性を使用不可にします",
+    "Recommended setting": "おすすめ設定",
+    "Custom Whitelist": "カスタムホワイトリスト",
+    "Tag names": "タグ名のホワイトリスト",
+    "Tag attributes": "タグ属性のホワイトリスト",
+    "import_recommended": "おすすめをインポート"
   },
 
   "customize_page": {

+ 92 - 1
lib/models/config.js

@@ -2,6 +2,7 @@ module.exports = function(crowi) {
   var mongoose = require('mongoose')
     , debug = require('debug')('growi:models:config')
     , uglifycss = require('uglifycss')
+    , recommendedXssWhiteList = require('../util/recommendedXssWhiteList')
     , configSchema
     , Config
 
@@ -101,6 +102,10 @@ module.exports = function(crowi) {
 
   function getDefaultMarkdownConfigs() {
     return {
+      'markdown:xss:isEnabledPrevention': true,
+      'markdown:xss:option': 2,
+      'markdown:xss:tagWhiteList': [],
+      'markdown:xss:attrWhiteList': [],
       'markdown:isEnabledLinebreaks': false,
       'markdown:isEnabledLinebreaksInComments': true,
     };
@@ -334,6 +339,85 @@ module.exports = function(crowi) {
     return config.markdown[key];
   };
 
+  configSchema.statics.isEnabledXssPrevention = function(config) {
+    const key = 'markdown:xss:isEnabledPrevention';
+
+    // return default value if undefined
+    if (undefined === config.markdown || undefined === config.markdown[key]) {
+      return getDefaultMarkdownConfigs[key];
+    }
+
+    return config.markdown[key];
+  };
+
+  configSchema.statics.xssOption = function(config) {
+    const key = 'markdown:xss:option';
+
+    // return default value if undefined
+    if (undefined === config.markdown || undefined === config.markdown[key]) {
+      return getDefaultMarkdownConfigs[key];
+    }
+
+    return config.markdown[key];
+  };
+
+  configSchema.statics.tagWhiteList = function(config) {
+    const key = 'markdown:xss:tagWhiteList';
+
+    // return default value if undefined
+    if (undefined === config.markdown || undefined === config.markdown[key]) {
+      return getDefaultMarkdownConfigs[key];
+    }
+
+    if (this.isEnabledXssPrevention(config)) {
+      switch (this.xssOption(config)) {
+        case 1: // ignore all: use default option
+          return [];
+
+        case 2: // recommended
+          return recommendedXssWhiteList.tags;
+
+        case 3: // custom white list
+          return config.markdown[key];
+
+        default:
+          return [];
+      }
+    }
+    else {
+      return [];
+    }
+
+  };
+
+  configSchema.statics.attrWhiteList = function(config) {
+    const key = 'markdown:xss:attrWhiteList';
+
+    // return default value if undefined
+    if (undefined === config.markdown || undefined === config.markdown[key]) {
+      return getDefaultMarkdownConfigs[key];
+    }
+
+    if (this.isEnabledXssPrevention(config)) {
+      switch (this.xssOption(config)) {
+        case 1: // ignore all: use default option
+          return [];
+
+        case 2: // recommended
+          return recommendedXssWhiteList.attrs;
+
+        case 3: // custom white list
+          return config.markdown[key];
+
+        default:
+          return [];
+      }
+    }
+    else {
+      return [];
+    }
+  };
+
   /**
    * initialize custom css strings
    */
@@ -377,9 +461,12 @@ module.exports = function(crowi) {
       customTitle = '{{page}} - {{sitename}}';
     }
 
-    return customTitle
+    // replace
+    customTitle = customTitle
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{page}}', page);
+
+    return crowi.xss.process(customTitle);
   };
 
   configSchema.statics.behaviorType = function(config) {
@@ -474,6 +561,10 @@ module.exports = function(crowi) {
       layoutType: Config.layoutType(config),
       isEnabledLinebreaks: Config.isEnabledLinebreaks(config),
       isEnabledLinebreaksInComments: Config.isEnabledLinebreaksInComments(config),
+      isEnabledXssPrevention: Config.isEnabledXssPrevention(config),
+      xssOption: Config.xssOption(config),
+      tagWhiteList: Config.tagWhiteList(config),
+      attrWhiteList: Config.attrWhiteList(config),
       highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
       env: {

+ 17 - 7
lib/models/page.js

@@ -984,13 +984,17 @@ module.exports = function(crowi) {
   };
 
   pageSchema.statics.create = function(path, body, user, options) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
-      , grant = options.grant || GRANT_PUBLIC
       , redirectTo = options.redirectTo || null
       , grantUserGroupId = options.grantUserGroupId || null;
 
+    let grant = options.grant || GRANT_PUBLIC;
+
+    // sanitize path
+    path = crowi.xss.process(path);
+
     // force public
     if (isPortalPath(path)) {
       grant = GRANT_PUBLIC;
@@ -1003,7 +1007,7 @@ module.exports = function(crowi) {
           throw new Error('Cannot create new page to existed path');
         }
 
-        var newPage = new Page();
+        const newPage = new Page();
         newPage.path = path;
         newPage.creator = user;
         newPage.lastUpdateUser = user;
@@ -1251,11 +1255,14 @@ module.exports = function(crowi) {
   };
 
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
-    var Page = this
+    const Page = this
       , Revision = crowi.model('Revision')
       , path = pageData.path
       , createRedirectPage = options.createRedirectPage || 0
-      , moveUnderTrees     = options.moveUnderTrees || 0;
+      ;
+
+    // sanitize path
+    newPagePath = crowi.xss.process(newPagePath);
 
     return Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})  // pageData の path を変更
       .then((data) => {
@@ -1266,7 +1273,7 @@ module.exports = function(crowi) {
         pageData.path = newPagePath;
 
         if (createRedirectPage) {
-          var body = 'redirect ' + newPagePath;
+          const body = 'redirect ' + newPagePath;
           Page.create(path, body, user, {redirectTo: newPagePath});
         }
         pageEvent.emit('update', pageData, user); // update as renamed page
@@ -1276,10 +1283,13 @@ module.exports = function(crowi) {
   };
 
   pageSchema.statics.renameRecursively = function(pageData, newPagePathPrefix, user, options) {
-    var Page = this
+    const Page = this
       , path = pageData.path
       , pathRegExp = new RegExp('^' + escapeStringRegexp(path), 'i');
 
+    // sanitize path
+    newPagePathPrefix = crowi.xss.process(newPagePathPrefix);
+
     return Page.generateQueryToListWithDescendants(path, user, options)
       .then(function(pages) {
         return Promise.all(pages.map(function(page) {

+ 0 - 6
lib/models/user-group.js

@@ -82,12 +82,6 @@ class UserGroup {
       });
   }
 
-  // TBD: グループ名によるグループ検索
-  static findUserGroupByName(name) {
-    const query = { name: name };
-    return this.findOne(query);
-  }
-
   // 登録可能グループ名確認
   static isRegisterableName(name) {
     const query = { name: name };

+ 66 - 41
lib/routes/admin.js

@@ -14,6 +14,7 @@ module.exports = function(crowi, app) {
     , PluginUtils = require('../plugins/plugin-utils')
     , pluginUtils = new PluginUtils()
     , ApiResponse = require('../util/apiResponse')
+    , recommendedXssWhiteList = require('../util/recommendedXssWhiteList')
 
     , MAX_PAGE_LIST = 50
     , actions = {};
@@ -104,10 +105,12 @@ module.exports = function(crowi, app) {
   // app.get('/admin/markdown'                  , admin.markdown.index);
   actions.markdown = {};
   actions.markdown.index = function(req, res) {
-    var config = crowi.getConfig();
-    var markdownSetting = Config.setupCofigFormData('markdown', config);
+    const config = crowi.getConfig();
+    const markdownSetting = Config.setupCofigFormData('markdown', config);
+
     return res.render('admin/markdown', {
       markdownSetting: markdownSetting,
+      recommendedXssWhiteList: recommendedXssWhiteList,
     });
   };
 
@@ -130,6 +133,33 @@ module.exports = function(crowi, app) {
     }
   };
 
+  // app.post('/admin/markdown/xss-setting' , admin.markdown.xssSetting);
+  actions.markdown.xssSetting = function(req, res) {
+    let xssSetting = req.form.markdownSetting;
+
+    xssSetting['markdown:xss:tagWhiteList'] = stringToArray(xssSetting['markdown:xss:tagWhiteList']);
+    xssSetting['markdown:xss:attrWhiteList'] = stringToArray(xssSetting['markdown:xss:attrWhiteList']);
+
+    req.session.markdownSetting = xssSetting;
+    if (req.form.isValid) {
+      Config.updateNamespaceByArray('markdown', xssSetting, function(err, config) {
+        Config.updateConfigCache('markdown', config);
+        req.session.xssSetting = null;
+        req.flash('successMessage', ['Successfully updated!']);
+        return res.redirect('/admin/markdown');
+      });
+    }
+    else {
+      req.flash('errorMessage', req.form.errors);
+      return res.redirect('/admin/markdown');
+    }
+  };
+
+  const stringToArray = (string) => {
+    const array = string.split(',');
+    return array.map(item => item.trim());
+  };
+
   // app.get('/admin/customize' , admin.customize.index);
   actions.customize = {};
   actions.customize.index = function(req, res) {
@@ -422,7 +452,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.user.remove = function(req, res) {
-    var id = req.params.id;
+    const id = req.params.id;
     let username = '';
 
     return new Promise((resolve, reject) => {
@@ -443,17 +473,10 @@ module.exports = function(crowi, app) {
     })
     .then((userData) => {
       // remove all External Accounts
-      ExternalAccount.remove({user: userData})
-      .then((err) => {
-        if (err) {
-          throw new Error(err.message);
-        }
-        return userData;
-      });
+      return ExternalAccount.remove({user: userData}).then(() => userData);
     })
     .then((userData) => {
-      return Page.removePageByPath(`/user/${username}`)
-        .then(() => userData);
+      return Page.removePageByPath(`/user/${username}`).then(() => userData);
     })
     .then((userData) => {
       req.flash('successMessage', `${username} さんのアカウントを削除しました`);
@@ -568,15 +591,15 @@ module.exports = function(crowi, app) {
 
   // グループ詳細
   actions.userGroup.detail = function(req, res) {
-    var name = req.params.name;
-    var renderVar = {
+    const userGroupId = req.params.id;
+    const renderVar = {
       userGroup: null,
       userGroupRelations: [],
       pageGroupRelations: [],
       notRelatedusers: []
     };
-    var targetUserGroup = null;
-    UserGroup.findUserGroupByName(name)
+    let targetUserGroup = null;
+    UserGroup.findOne({ _id: userGroupId})
       .then(function(userGroup) {
         targetUserGroup = userGroup;
         if (targetUserGroup == null) {
@@ -613,18 +636,20 @@ module.exports = function(crowi, app) {
 
   //グループの生成
   actions.userGroup.create = function(req, res) {
-    var form = req.form.createGroupForm;
+    const form = req.form.createGroupForm;
     if (req.form.isValid) {
-      UserGroup.createGroupByName(form.userGroupName)
-      .then((newUserGroup) => {
-        req.flash('successMessage', newUserGroup.name);
-        req.flash('createdUserGroup', newUserGroup);
-        return res.redirect('/admin/user-groups');
-      })
-      .catch((err) => {
-        debug('create userGroup error:', err);
-        req.flash('errorMessage', '同じグループ名が既に存在します。');
-      });
+      const userGroupName = crowi.xss.process(form.userGroupName);
+
+      UserGroup.createGroupByName(userGroupName)
+        .then((newUserGroup) => {
+          req.flash('successMessage', newUserGroup.name);
+          req.flash('createdUserGroup', newUserGroup);
+          return res.redirect('/admin/user-groups');
+        })
+        .catch((err) => {
+          debug('create userGroup error:', err);
+          req.flash('errorMessage', '同じグループ名が既に存在します。');
+        });
     }
     else {
       req.flash('errorMessage', req.form.errors.join('\n'));
@@ -635,8 +660,8 @@ module.exports = function(crowi, app) {
   //
   actions.userGroup.update = function(req, res) {
 
-    var userGroupId = req.params.userGroupId;
-    var name = req.body.name;
+    const userGroupId = req.params.userGroupId;
+    const name = crowi.xss.process(req.body.name);
 
     UserGroup.findById(userGroupId)
     .then((userGroupData) => {
@@ -665,7 +690,7 @@ module.exports = function(crowi, app) {
       }
     })
     .then(() => {
-      return res.redirect('/admin/user-group-detail/' + name);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     });
   };
 
@@ -736,7 +761,7 @@ module.exports = function(crowi, app) {
 
   actions.userGroup.deletePicture = function(req, res) {
 
-    var userGroupId = req.params.userGroupId;
+    const userGroupId = req.params.userGroupId;
     let userGroupName = null;
 
     UserGroup.findById(userGroupId)
@@ -752,7 +777,7 @@ module.exports = function(crowi, app) {
     .then((updated) => {
       req.flash('successMessage', 'Deleted group picture');
 
-      return res.redirect('/admin/user-group-detail/' + userGroupName);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     .catch((err) => {
       debug('An error occured.', err);
@@ -762,7 +787,7 @@ module.exports = function(crowi, app) {
         return res.redirect('/admin/user-groups/');
       }
       else {
-        return res.redirect('/admin/user-group-detail/' + userGroupName);
+        return res.redirect('/admin/user-group-detail/' + userGroupId);
       }
     });
   };
@@ -824,23 +849,22 @@ module.exports = function(crowi, app) {
       UserGroupRelation.createRelation(userGroup, user);
     })
     .then((result) => {
-      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.id);
     }).catch((err) => {
       debug('Error on create user-group relation', err);
       req.flash('errorMessage', 'Error on create user-group relation');
-      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.id);
     });
   };
 
   actions.userGroupRelation.remove = function(req, res) {
     const UserGroupRelation = crowi.model('UserGroupRelation');
-    var name = req.params.name;
-    var relationId = req.params.relationId;
+    const userGroupId = req.params.id;
+    const relationId = req.params.relationId;
 
-    debug(name, relationId);
     UserGroupRelation.removeById(relationId)
     .then(() =>{
-      return res.redirect('/admin/user-group-detail/' + name);
+      return res.redirect('/admin/user-group-detail/' + userGroupId);
     })
     .catch((err) => {
       debug('Error on remove user-group-relation', err);
@@ -956,13 +980,13 @@ module.exports = function(crowi, app) {
     // reset strategy
     await crowi.passportService.resetGitHubStrategy();
     // setup strategy
-    if (Config.isEnabledPassportGoogle(config)) {
+    if (Config.isEnabledPassportGitHub(config)) {
       try {
         await crowi.passportService.setupGitHubStrategy(true);
       }
       catch (err) {
         // reset
-        await crowi.passportService.resetGoogleStrategy();
+        await crowi.passportService.resetGitHubStrategy();
         return res.json({status: false, message: err.message});
       }
     }
@@ -1099,6 +1123,7 @@ module.exports = function(crowi, app) {
     debug('mailer setup for validate SMTP setting', smtpClient);
 
     smtpClient.sendMail({
+      from: form['mail:from'],
       to: req.user.email,
       subject: 'Wiki管理設定のアップデートによるメール通知',
       text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。'

+ 6 - 5
lib/routes/index.js

@@ -71,14 +71,15 @@ module.exports = function(crowi, app) {
   // OAuth
   app.post('/_api/admin/security/passport-google' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGoogle, admin.api.securityPassportGoogleSetting);
   app.post('/_api/admin/security/passport-github' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.securityPassportGitHub, admin.api.securityPassportGitHubSetting);
-  app.get('/passport/google'                      , loginPassport.loginPassportGoogle);
-  app.get('/passport/github'                      , loginPassport.loginPassportGitHub);
+  app.get('/passport/google'                      , loginPassport.loginWithGoogle);
+  app.get('/passport/github'                      , loginPassport.loginWithGitHub);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
 
   // markdown admin
   app.get('/admin/markdown'                   , loginRequired(crowi, app) , middleware.adminRequired() , admin.markdown.index);
-  app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting);
+  app.post('/admin/markdown/lineBreaksSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting); //change form name
+  app.post('/admin/markdown/xss-setting'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdownXss, admin.markdown.xssSetting);
 
   // markdown admin
   app.get('/admin/customize'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.customize.index);
@@ -122,7 +123,7 @@ module.exports = function(crowi, app) {
 
   // user-groups admin
   app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
-  app.get('/admin/user-group-detail/:name'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
+  app.get('/admin/user-group-detail/:id'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
   app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
   app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
   app.post('/admin/user-group/:userGroupId/picture/delete', loginRequired(crowi, app), admin.userGroup.deletePicture);
@@ -131,7 +132,7 @@ module.exports = function(crowi, app) {
 
   // user-group-relations admin
   app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
-  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
+  app.post('/admin/user-group-relation/:id/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);

+ 18 - 16
lib/routes/login-passport.js

@@ -1,7 +1,7 @@
 module.exports = function(crowi, app) {
   'use strict';
 
-  var debug = require('debug')('growi:routes:login-passport')
+  const debug = require('debug')('growi:routes:login-passport')
     , logger = require('@alias/logger')('growi:routes:login-passport')
     , passport = require('passport')
     , config = crowi.getConfig()
@@ -24,7 +24,7 @@ module.exports = function(crowi, app) {
       }
     });
 
-    var jumpTo = req.session.jumpTo;
+    const jumpTo = req.session.jumpTo;
     if (jumpTo) {
       req.session.jumpTo = null;
       return res.redirect(jumpTo);
@@ -101,7 +101,7 @@ module.exports = function(crowi, app) {
       'id': ldapAccountId,
       'username': usernameToBeRegistered,
       'name': nameToBeRegistered
-    }
+    };
 
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
     if (!externalAccount) {
@@ -112,7 +112,7 @@ module.exports = function(crowi, app) {
 
     // login
     await req.logIn(user, err => {
-      if (err) { return next(err) };
+      if (err) { return next(err) }
       return loginSuccess(req, res, user);
     });
   };
@@ -205,10 +205,11 @@ module.exports = function(crowi, app) {
     })(req, res, next);
   };
 
-  const loginPassportGoogle = function(req, res) {
+  const loginWithGoogle = function(req, res, next) {
     if (!passportService.isGoogleStrategySetup) {
       debug('GoogleStrategy has not been set up');
-      return;
+      req.flash('warningMessage', 'GoogleStrategy has not been set up');
+      return next();
     }
 
     passport.authenticate('google', {
@@ -224,7 +225,7 @@ module.exports = function(crowi, app) {
       'id': response.id,
       'username': response.displayName,
       'name': `${response.name.givenName} ${response.name.familyName}`
-    }
+    };
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
     if (!externalAccount) {
       return loginFailure(req, res, next);
@@ -234,15 +235,16 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, err => {
-      if (err) { return next(err) };
+      if (err) { return next(err) }
       return loginSuccess(req, res, user);
     });
   };
 
-  const loginPassportGitHub = function(req, res) {
+  const loginWithGitHub = function(req, res, next) {
     if (!passportService.isGitHubStrategySetup) {
       debug('GitHubStrategy has not been set up');
-      return;
+      req.flash('warningMessage', 'GitHubStrategy has not been set up');
+      return next();
     }
 
     passport.authenticate('github')(req, res);
@@ -256,7 +258,7 @@ module.exports = function(crowi, app) {
       'id': response.id,
       'username': response.username,
       'name': response.displayName
-    }
+    };
 
     const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
     if (!externalAccount) {
@@ -267,7 +269,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, err => {
-      if (err) { return next(err) };
+      if (err) { return next(err) }
       return loginSuccess(req, res, user);
     });
   };
@@ -290,7 +292,7 @@ module.exports = function(crowi, app) {
           return next();
         }
 
-        resolve(response)
+        resolve(response);
       })(req, res, next);
     });
   };
@@ -321,15 +323,15 @@ module.exports = function(crowi, app) {
         }
       }
     }
-  }
+  };
 
   return {
     loginFailure,
     loginWithLdap,
     testLdapCredentials,
     loginWithLocal,
-    loginPassportGoogle,
-    loginPassportGitHub,
+    loginWithGoogle,
+    loginWithGitHub,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
   };

+ 12 - 7
lib/service/passport.js

@@ -220,7 +220,12 @@ class PassportService {
         bindDN.replace(/{{username}}/, loginForm.username):
         bindDN;
       const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
-      let serverOpt = { url, bindDN: fixedBindDN, bindCredentials: fixedBindCredentials, searchBase, searchFilter };
+      let serverOpt = {
+        url, bindDN: fixedBindDN, bindCredentials: fixedBindCredentials,
+        searchBase, searchFilter,
+        attrMapUsername: this.getLdapAttrNameMappedToUsername(),
+        attrMapName: this.getLdapAttrNameMappedToName(),
+      };
 
       if (groupSearchBase && groupSearchFilter) {
         serverOpt = Object.assign(serverOpt, { groupSearchBase, groupSearchFilter, groupDnProperty });
@@ -265,9 +270,9 @@ class PassportService {
 
     debug('GoogleStrategy: setting up..');
     passport.use(new GoogleStrategy({
-      clientId: config.crowi['security:passport-google:clientId'],
-      clientSecret: config.crowi['security:passport-google:clientSecret'],
-      callbackURL: 'http://localhost:3000/passport/google/callback',  //change this
+      clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
+      clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
+      callbackURL: config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
       if (profile) {
@@ -311,9 +316,9 @@ class PassportService {
 
     debug('GitHubStrategy: setting up..');
     passport.use(new GitHubStrategy({
-      clientID: config.crowi['security:passport-github:clientId'],
-      clientSecret: config.crowi['security:passport-github:clientSecret'],
-      callbackURL: 'http://localhost:3000/passport/github/callback',  //change this
+      clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
+      clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
+      callbackURL: config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
       if (profile) {

+ 7 - 3
lib/util/middlewares.js

@@ -77,7 +77,7 @@ exports.swigFunctions = function(crowi, app) {
   };
 };
 
-exports.swigFilters = function(app, swig) {
+exports.swigFilters = function(crowi, app, swig) {
 
   // define a function for Gravatar
   const generateGravatarSrc = function(user) {
@@ -139,7 +139,7 @@ exports.swigFilters = function(app, swig) {
 
     swig.setFilter('datetz', function(input, format) {
       // timezone
-      var swigFilters = require('swig-templates/lib/filters');
+      const swigFilters = require('swig-templates/lib/filters');
       return swigFilters.date(input, format, app.get('tzoffset'));
     });
 
@@ -179,10 +179,14 @@ exports.swigFilters = function(app, swig) {
       }
     });
 
-    swig.setFilter('sanitize', function(string) {
+    swig.setFilter('encodeHTML', function(string) {
       return entities.encodeHTML(string);
     });
 
+    swig.setFilter('preventXss', function(string) {
+      return crowi.xss.process(string);
+    });
+
     next();
   };
 };

+ 19 - 0
lib/util/recommendedXssWhiteList.js

@@ -0,0 +1,19 @@
+/**
+ * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites
+ * added tags: h4, h5, h6, span, div, iframe, table, thead, tbody, tfoot, th, td, tr, colgroup, col
+ * added attributes: class, style
+ */
+
+const tags = [
+  'a', 'b', 'blockquote', 'blockquote', 'code', 'del', 'dd', 'dl', 'dt', 'em',
+  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'img', 'kbd', 'li', 'ol', 'p', 'pre',
+  's', 'sup', 'sub', 'strong', 'strike', 'ul', 'br', 'hr', 'span', 'div', 'iframe',
+  'table', 'thead', 'tbody', 'tfoot', 'th', 'td', 'tr', 'colgroup', 'col',
+];
+
+const attrs = ['src', 'href', 'class', 'id', 'width', 'height', 'alt', 'title', 'style'];
+
+module.exports = {
+  'tags': tags,
+  'attrs': attrs,
+};

+ 26 - 26
lib/util/swigFunctions.js

@@ -1,5 +1,5 @@
 module.exports = function(crowi, app, req, locals) {
-  var debug = require('debug')('growi:lib:swigFunctions')
+  const debug = require('debug')('growi:lib:swigFunctions')
     , stringWidth = require('string-width')
     , Page = crowi.model('Page')
     , Config = crowi.model('Config')
@@ -45,15 +45,15 @@ module.exports = function(crowi, app, req, locals) {
    * return app title
    */
   locals.appTitle = function() {
-    var config = crowi.getConfig();
-    return Config.appTitle(config);
+    const config = crowi.getConfig();
+    return crowi.xss.process(Config.appTitle(config));
   };
 
   /**
    * return true if enabled
    */
   locals.isEnabledPassport = function() {
-    var config = crowi.getConfig();
+    const config = crowi.getConfig();
     return Config.isEnabledPassport(config);
   };
 
@@ -69,7 +69,7 @@ module.exports = function(crowi, app, req, locals) {
    * return true if enabled and strategy has been setup successfully
    */
   locals.isLdapSetup = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
   };
 
@@ -77,7 +77,7 @@ module.exports = function(crowi, app, req, locals) {
    * return true if enabled but strategy has some problem
    */
   locals.isLdapSetupFailed = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
   };
 
@@ -88,17 +88,17 @@ module.exports = function(crowi, app, req, locals) {
       return false;
     }
 
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
 
   locals.passportGoogleLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-google:isEnabled'];
   };
 
   locals.passportGitHubLoginEnabled = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return locals.isEnabledPassport() && config.crowi['security:passport-github:isEnabled'];
   };
 
@@ -110,17 +110,17 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isEnabledPlugins = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledPlugins(config);
   };
 
   locals.isEnabledLinebreaks = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaks(config);
   };
 
   locals.isEnabledLinebreaksInComments = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledLinebreaksInComments(config);
   };
 
@@ -133,12 +133,12 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.customHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.customHeader(config);
   };
 
   locals.theme = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.theme(config);
   };
 
@@ -148,32 +148,32 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.behaviorType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.behaviorType(config);
   };
 
   locals.layoutType = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.layoutType(config);
   };
 
   locals.highlightJsStyle = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyle(config);
   };
 
   locals.highlightJsStyleBorder = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.highlightJsStyleBorder(config);
   };
 
   locals.isEnabledTimeline = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledTimeline(config);
   };
 
   locals.slackConfigured = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
       return true;
     }
@@ -181,12 +181,12 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isUploadable = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isUploadable(config);
   };
 
   locals.isEnabledAttachTitleHeader = function() {
-    var config = crowi.getConfig();
+    let config = crowi.getConfig();
     return Config.isEnabledAttachTitleHeader(config);
   };
 
@@ -211,7 +211,7 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isTopPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path === '/') {
       return true;
     }
@@ -220,7 +220,7 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isTrashPage = function() {
-    var path = req.path || '';
+    let path = req.path || '';
     if (path.match(/^\/trash\/.*/)) {
       return true;
     }
@@ -229,8 +229,8 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.isDeletablePage = function() {
-    var Page = crowi.model('Page');
-    var path = req.path || '';
+    let Page = crowi.model('Page');
+    let path = req.path || '';
 
     return Page.isDeletableName(path);
   };

+ 18 - 10
lib/util/xss.js

@@ -1,26 +1,34 @@
 class Xss {
 
-  constructor(isAllowAllAttrs) {
+  constructor(xssOption) {
     const xss = require('xss');
 
-    // create the option object
+    xssOption = xssOption || {};
+
+    const tagWhiteList = xssOption.tagWhiteList || [];
+    const attrWhiteList = xssOption.attrWhiteList || [];
+
+    let whiteListContent = {};
+
+    // default
     let option = {
       stripIgnoreTag: true,
+      stripIgnoreTagBody: false,    // see https://github.com/weseek/growi/pull/505
       css: false,
+      whiteList: whiteListContent,
       escapeHtml: (html) => html,   // resolve https://github.com/weseek/growi/issues/221
     };
-    if (isAllowAllAttrs) {
-      // allow all attributes
-      option.onTagAttr = function(tag, name, value, isWhiteAttr) {
-        return `${name}="${value}"`;
-      };
-    }
+
+    tagWhiteList.forEach(tag => {
+      whiteListContent[tag] = attrWhiteList;
+    });
+
     // create the XSS Filter instance
     this.myxss = new xss.FilterXSS(option);
   }
 
-  process(markdown) {
-    return this.myxss.process(markdown);
+  process(document) {
+    return this.myxss.process(document);
   }
 
 }

+ 13 - 0
lib/util/xssOption.js

@@ -0,0 +1,13 @@
+class XssOption {
+
+  constructor(config) {
+    const recommendedXssWhiteList = require('../util/recommendedXssWhiteList');
+    const initializedConfig = (config != null) ? config : {};
+
+    this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
+    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedXssWhiteList.tags;
+    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedXssWhiteList.attrs;
+  }
+
+}
+module.exports = XssOption;

+ 1 - 1
lib/views/_form.html

@@ -17,7 +17,7 @@
 <form action="/_/edit" id="page-form" method="post" class="{% if isUploadable() %}uploadable{% endif %} page-form">
 
   <input type="hidden" id="form-body" name="pageForm[body]" value="{% if pageForm.body %}{{ pageForm.body }}{% endif %}">
-  <input type="hidden" name="pageForm[path]" value="{{ path }}">
+  <input type="hidden" name="pageForm[path]" value="{{ path | preventXss }}">
   <input type="hidden" name="pageForm[currentRevision]" value="{{ pageForm.currentRevision|default(page.revision._id.toString()) }}">
   <div class="page-editor-footer d-flex flex-row align-items-center justify-content-between">
     <div>

+ 1 - 1
lib/views/admin/app.html

@@ -92,7 +92,7 @@
         <div class="form-group">
           <label for="settingForm[mail.from]" class="col-xs-3 control-label">{{ t('app_setting.From e-mail address') }}</label>
           <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[mail:from]" placeholder="例: mail@crowi.wiki" value="{{ settingForm['mail:from'] }}">
+            <input class="form-control" type="text" name="settingForm[mail:from]" placeholder="例: mail@growi.org" value="{{ settingForm['mail:from'] }}">
           </div>
         </div>
 

+ 162 - 40
lib/views/admin/markdown.html

@@ -38,61 +38,179 @@
       {% endif %}
 
       <form action="/admin/markdown/lineBreaksSetting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
-      <fieldset>
-        <legend>{{ t('Markdown settings') }}</legend>
-        <p class="well">{{ t("markdown_setting.markdown_rendering") }}</p>
-
-        <div class="form-group">
-          <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
-            {{ t('markdown_setting.Enable Line Break') }}
-          </label>
-          <div class="col-xs-5">
-            <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
-                <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="true" type="radio"
-                    {% if true === markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> ON
-              </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="default">
-                <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="false" type="radio"
-                    {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
-              </label>
+        <fieldset>
+          <legend>{{ t('markdown_setting.line_break_setting') }}</legend>
+          <p class="well">{{ t("markdown_setting.line_break_setting_desc") }}</p>
+
+          <div class="form-group">
+            <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
+              {{ t('markdown_setting.Enable Line Break') }}
+            </label>
+            <div class="col-xs-5">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="primary">
+                  <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="true" type="radio"
+                      {% if true === markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}active{% endif %}" data-active-class="default">
+                  <input name="markdownSetting[markdown:isEnabledLinebreaks]" value="false" type="radio"
+                      {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
+                </label>
+              </div>
+              <p class="help-block">{{ t("markdown_setting.Enable Line Break desc") }}</p>
             </div>
-            <p class="help-block">{{ t("markdown_setting.Enable Line Break desc") }}</p>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
-            {{ t("markdown_setting.Enable Line Break for comment") }}
-          </label>
-          <div class="col-xs-5">
-            <div class="btn-group btn-toggle" data-toggle="buttons">
-              <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
-                <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="true" type="radio"
-                    {% if true === markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> ON
-              </label>
-              <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="default">
-                <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="false" type="radio"
-                    {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
-              </label>
+          <div class="form-group">
+            <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
+              {{ t("markdown_setting.Enable Line Break for comment") }}
+            </label>
+            <div class="col-xs-5">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="primary">
+                  <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="true" type="radio"
+                      {% if true === markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}active{% endif %}" data-active-class="default">
+                  <input name="markdownSetting[markdown:isEnabledLinebreaksInComments]" value="false" type="radio"
+                      {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
+                </label>
+              </div>
+              <p class="help-block">{{ t("markdown_setting.Enable Line Break for comment desc") }}</p>
             </div>
-            <p class="help-block">{{ t("markdown_setting.Enable Line Break for comment desc") }}</p>
           </div>
-        </div>
 
-        <div class="form-group">
-          <div class="col-xs-offset-4 col-xs-5">
+          <div class="form-group my-3">
+            <div class="col-xs-offset-4 col-xs-5">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
+            </div>
+          </div>
+        </fieldset>
+      </form>
+
+      <form action="/admin/markdown/xss-setting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
+        {% set nameForIsXssEnabled = "markdownSetting[markdown:xss:isEnabledPrevention]" %}
+        {% set isXssEnabled = markdownSetting['markdown:xss:isEnabledPrevention'] %}
+
+        <legend>{{ t('markdown_setting.XSS_setting') }}</legend>
+        <p class="well">{{ t("markdown_setting.XSS_setting_desc") }}</p>
+
+        <fieldset class="row">
+          <div class="form-group">
+            <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
+              {{ t('markdown_setting.Enable XSS prevention') }}
+            </label>
+            <div class="col-xs-5">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default btn-rounded btn-outline {% if isXssEnabled %}active{% endif %}" data-active-class="primary">
+                  <input name="{{nameForIsXssEnabled}}" value="true" type="radio"
+                      {% if isXssEnabled %}checked{% endif %}> ON
+                </label>
+                <label class="btn btn-default btn-rounded btn-outline {% if !isXssEnabled %}active{% endif %}" data-active-class="default">
+                  <input name="{{nameForIsXssEnabled}}" value="false" type="radio"
+                      {% if !isXssEnabled %}checked{% endif %}> OFF
+                </label>
+              </div>
+            </div>
+          </div>
+        </fieldset>
+
+        <fieldset class="form-group row my-3" id="xss-hide-when-disabled" {% if !isXssEnabled %}style="display: none;"{% endif %}>
+          {% set nameForXssOption = "markdownSetting[markdown:xss:option]" %}
+          {% set xssOption = markdownSetting['markdown:xss:option'] %}
+
+          <div class="col-xs-4 radio radio-primary">
+            <input type="radio" id="option1" name="{{nameForXssOption}}" value="1" {% if xssOption === 1 %}checked{% endif %}>
+            <label for="option1">
+              <p class="font-weight-bold">{{ t('markdown_setting.Ignore all tags') }}</p>
+              <div class="m-t-15">
+                  {{ t('markdown_setting.Ignore all tags desc') }}
+              </div>
+            </label>
+          </div>
+
+          <div class="col-xs-4 radio radio-primary">
+            <input type="radio" id="option2" name="{{nameForXssOption}}" value="2" {% if xssOption === 2 %}checked{% endif %}>
+            <label for="option2">
+              <p class="font-weight-bold">{{ t('markdown_setting.Recommended setting') }}</p>
+              <div class="m-t-15">
+                {{ t('markdown_setting.Tag names') }}
+                <textarea class="form-control xss-list" name="recommendedTags" rows="6" cols="40" readonly>{{ recommendedXssWhiteList.tags }}</textarea>
+              </div>
+              <div class="m-t-15">
+                {{ t('markdown_setting.Tag attributes') }}
+                <textarea class="form-control xss-list" name="recommendedAttrs" rows="6" cols="40" readonly>{{ recommendedXssWhiteList.attrs }}</textarea>
+              </div>
+            </label>
+          </div>
+
+          <div class="col-xs-4 radio radio-primary">
+            <input type="radio" id="option3" name="{{nameForXssOption}}" value="3" {% if xssOption === 3 %}checked{% endif %}>
+            <label for="option3">
+              <p class="font-weight-bold">{{ t('markdown_setting.Custom Whitelist') }}</p>
+              <div class="m-t-15">
+                <div class="d-flex justify-content-between">
+                  {{ t('markdown_setting.Tag names') }}
+                  <p id="btn-import-tags" class="btn btn-xs btn-primary">
+                    {{ t('markdown_setting.import_recommended', 'tags') }}
+                  </p>
+                </div>
+                <textarea class="form-control xss-list" type="text" name="markdownSetting[markdown:xss:tagWhiteList]" rows="6" cols="40" placeholder="e.g. iframe, script, video...">{{ markdownSetting['markdown:xss:tagWhiteList'] }}</textarea>
+              </div>
+              <div class="m-t-15">
+                <div class="d-flex justify-content-between">
+                  {{ t('markdown_setting.Tag attributes') }}
+                  <p id="btn-import-attrs" class="btn btn-xs btn-primary">
+                    {{ t('markdown_setting.import_recommended', 'attributes') }}
+                  </p>
+                </div>
+                <textarea class="form-control xss-list" name="markdownSetting[markdown:xss:attrWhiteList]" rows="6" cols="40" placeholder="e.g. src, id, name...">{{ markdownSetting['markdown:xss:attrWhiteList'] }}</textarea>
+              </div>
+            </label>
+          </div>
+
+        </fieldset>
+
+        <div class="form-group row">
+          <div class="col-xs-12 d-flex justify-content-center">
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
           </div>
         </div>
-      </fieldset>
-      </form>
 
+      </form>
     </div>
   </div>
 
 </div>
+
+<script>
+  // give a space between items in textarea(',' => ', ')
+  for (var i = 0; i < $('textarea.xss-list').length; i++) {
+    $($('textarea.xss-list')[i]).val($($('textarea.xss-list')[i]).val().replace(/,/g, ', '));
+  };
+
+  $('input[name="markdownSetting[markdown:xss:isEnabledPrevention]"]').change(function() {
+    if ($(this).val() === 'true') {
+      $('#xss-hide-when-disabled').slideDown();
+    }
+    else {
+      $('#xss-hide-when-disabled').slideUp();
+    }
+  });
+
+  $('#btn-import-tags').on('click', () => {
+    var $tagWhiteList = $('textarea[name="markdownSetting[markdown:xss:tagWhiteList]"]');
+    var $recommendedTagList = $('textarea[name="recommendedTags"]');
+    $tagWhiteList.val($recommendedTagList.val());
+  });
+  $('#btn-import-attrs').on('click', () => {
+    var $attrWhiteList = $('textarea[name="markdownSetting[markdown:xss:attrWhiteList]"]');
+    var $recommendedAttrList = $('textarea[name="recommendedAttrs"]');
+    $attrWhiteList.val($recommendedAttrList.val());
+  });
+</script>
 {% endblock content_main %}
 
 {% block content_footer %}
@@ -100,3 +218,7 @@
 
 
 
+
+
+
+

+ 224 - 194
lib/views/admin/notification.html

@@ -38,231 +38,261 @@
 
       <ul class="nav nav-tabs" role="tablist">
         <li class="active">
-          <a href="#slack-incoming-webhooks" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Incoming Webhooks</a>
+          <a href="#user-trigger-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> User Trigger Notification</a>
         </li>
         <li role="tab">
-          <a href="#slack-app" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack App</a>
+          <a href="#global-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> Global Notification</a>
         </li>
       </ul>
-
       <div class="tab-content m-t-15">
-        <div id="slack-incoming-webhooks" class="tab-pane active" role="tabpanel">
-
-          <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-            <fieldset>
-              <legend>Slack Incoming Webhooks Configuration</legend>
-
-              <div class="form-group">
-                <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
-                <div class="col-xs-9">
-                  <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
-                <div class="col-xs-9">
-                  <div class="checkbox checkbox-info">
-                    <input type="checkbox" id ="cbPrioritizeIWH" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
-                      {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
-                    <label for="cbPrioritizeIWH">
-                      Prioritize Incoming Webhook than Slack App
-                    </label>
+        <div id="user-trigger-notification" class="tab-pane active" role="tabpanel">
+
+          <select class="selectpicker" id="selectSlackOption" data-width="auto">
+            <option value="1">Slack Incoming Webhooks</option>
+            <option value="2">Slack App</option>
+          </select><!-- /.select-tab -->
+
+          <div class="tab-content m-t-15">
+            <div id="slack-incoming-webhooks" class="tab-pane active" role="tabpanel">
+
+              <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
+                <fieldset>
+                  <legend>Slack Incoming Webhooks Configuration</legend>
+
+                  <div class="form-group">
+                    <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
+                    <div class="col-xs-9">
+                      <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
+                    </div>
                   </div>
-                  <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
-                </div>
-              </div>
-
-              <div class="form-group">
-                <div class="col-xs-offset-3 col-xs-6">
-                  <button type="submit" class="btn btn-primary">Save</button>
-                </div>
-              </div>
-            </fieldset>
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          </form>
 
-          <hr>
-          <h3>
-            <i class="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
-          </h3>
-
-          <ol id="collapseHelpForIwh" class="collapse">
-            <li>
-              (At Workspace) Add a hook
-              <ol>
-                <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
-                <li>Choose the default channel to post.</li>
-                <li>Add.</li>
-              </ol>
-            </li>
-            <li>
-              (At GROWI admin page) Set Webhook URL
-              <ol>
-                <li>Input "Webhook URL" and submit on this page.</li>
-              </ol>
-            </li>
-          </ol>
-
-        </div><!-- /#slack-incoming-webhooks -->
-
-        <div id="slack-app" class="tab-pane" role="tabpanel" >
-
-          <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-            <fieldset>
-              <legend>Slack App Configuration</legend>
-
-              <p class="well">
-                <i class="icon-fw icon-exclamation text-danger"></i><span class="text-danger">NOT RECOMMENDED</span>
-                <br><br>
-                This is the way that compatible with Crowi,<br>
-                but not recommended in GROWI because it is <strong>too complex</strong>.
-                <br><br>
-                Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
-              </p>
-
-              <div class="form-group">
-                <label for="slackSetting[slack:token]" class="col-xs-3 control-label">OAuth Access Token</label>
-                <div class="col-xs-6">
-                  <input class="form-control" type="text" name="slackSetting[slack:token]" value="{{ slackSetting['slack:token'] || '' }}">
-                </div>
-              </div>
-
-              <div class="form-group">
-                <div class="col-xs-offset-3 col-xs-6">
-                  <button type="submit" class="btn btn-primary">Save</button>
-                </div>
-              </div>
-            </fieldset>
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          </form>
+                  <div class="form-group">
+                    <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
+                    <div class="col-xs-9">
+                      <div class="checkbox checkbox-info">
+                        <input type="checkbox" id ="cbPrioritizeIWH" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
+                         {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
+                        <label for="cbPrioritizeIWH">
+                         Prioritize Incoming Webhook than Slack App
+                        </label>
+                      </div>
+                      <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
+                    </div>
+                  </div>
 
-          <hr>
-          <h3>
-            <i class="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
-          </h3>
-
-          <ol id="collapseHelpForApp" class="collapse">
-            <li>
-              Register Slack App
-              <ol>
+                  <div class="form-group">
+                    <div class="col-xs-offset-3 col-xs-6">
+                      <button type="submit" class="btn btn-primary">Save</button>
+                    </div>
+                  </div>
+                </fieldset>
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              </form>
+
+              <hr>
+              <h3>
+                <i class="icon-question" aria-hidden="true"></i>
+                <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
+              </h3>
+
+              <ol id="collapseHelpForIwh" class="collapse">
                 <li>
-                  Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
-                  <dl class="dl-horizontal">
-                    <dt>App Name</dt> <dd><code>growi</code> </dd>
-                    <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
-                  </dl>
+                 (At Workspace) Add a hook
+                  <ol>
+                    <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
+                    <li>Choose the default channel to post.</li>
+                    <li>Add.</li>
+                  </ol>
+                </li>
+                <li>
+                (At GROWI admin page) Set Webhook URL
+                  <ol>
+                    <li>Input "Webhook URL" and submit on this page.</li>
+                  </ol>
                 </li>
-                <li><strong>Save</strong> it.</li>
-              </ol>
-            </li>
-            <li>
-              Set Permission Scopes to the App
-              <ol>
-                <li>Go to "OAuth &amp; Permissions" page.</li>
-                <li>Add "Send messages as GROWI"(<code>chat:write:bot</code>).</li>
-                <li>Don't forget to <strong>save</strong>.</li>
-              </ol>
-            </li>
-            <li>
-              Create a bot user
-              <ol>
-                <li>Go to "Bot Users" page and add.</li>
-              </ol>
-            </li>
-            <li>
-              Install the app
-              <ol>
-                <li>Go to "Install App to Your Workspace" page and install.</li>
-                <li>Go to "OAuth &amp; Permissions" page and copy <code>OAuth Access Token</code>.</li>
-              </ol>
-            </li>
-            <li>
-              (At this page) Set OAuth Access Token
-              <ol>
-                <li>Input "OAuth Access Token".</li>
-                <li>Don't forget to <strong>save</strong>.</li>
               </ol>
-            </li>
-          </ol>
 
-        </div><!-- /#slack-app -->
+            </div><!-- /#slack-incoming-webhooks -->
 
+            <div id="slack-app" class="tab-pane" role="tabpanel" >
 
+              <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
+                <fieldset>
+                  <legend>Slack App Configuration</legend>
 
-      </div><!-- /.tab-content -->
+                  <p class="well">
+                    <i class="icon-fw icon-exclamation text-danger"></i><span class="text-danger">NOT RECOMMENDED</span>
+                    <br><br>
+                    This is the way that compatible with Crowi,<br>
+                    but not recommended in GROWI because it is <strong>too complex</strong>.
+                    <br><br>
+                    Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateSlackIwh()">Slack incomming webhooks Configuration</a> instead.
+                  </p>
+
+                  <div class="form-group">
+                    <label for="slackSetting[slack:token]" class="col-xs-3 control-label">OAuth Access Token</label>
+                    <div class="col-xs-6">
+                      <input class="form-control" type="text" name="slackSetting[slack:token]" value="{{ slackSetting['slack:token'] || '' }}">
+                    </div>
+                  </div>
 
-      <hr>
-
-      <h4>Default Notification Settings for Patterns</h4>
-
-      <table class="table table-bordered">
-        <thead>
-          <th>Pattern</th>
-          <th>Channel</th>
-          <th>Operation</th>
-        </thead>
-        <tbody class="admin-notif-list">
-          <form id="slackNotificationForm">
-          <tr>
-            <td>
-              <input class="form-control" type="text" name="pathPattern" value="" placeholder="e.g. /projects/xxx/MTG/*">
-              <p class="help-block">
-                Path name of wiki. Pattern expression with <code>*</code> can be used.
-              </p>
-            </td>
-            <td>
-              <input class="form-control form-inline" type="text" name="channel" value="" placeholder="e.g. project-xxx">
-              <p class="help-block">
-                Slack channel name. Without <code>#</code>.
-              </p>
-            </td>
-            <td>
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <input type="submit" value="Add" class="btn btn-primary">
-            </td>
-          </tr>
-          </form>
-
-          {% for notif in settings %}
-          <tr class="admin-notif-row" data-updatepost-id="{{ notif._id.toString() }}">
-            <td>
-              {{ notif.pathPattern }}
-            </td>
-            <td>
-              {{ notif.channel }}
-            </td>
-            <td>
-              <form class="admin-remove-updatepost">
-                <input type="hidden" name="id" value="{{ notif._id.toString() }}">
+                  <div class="form-group">
+                    <div class="col-xs-offset-3 col-xs-6">
+                      <button type="submit" class="btn btn-primary">Save</button>
+                    </div>
+                  </div>
+                </fieldset>
                 <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <input type="submit" value="Delete" class="btn btn-default">
               </form>
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
 
+              <hr>
+              <h3>
+                <i class="icon-question" aria-hidden="true"></i>
+                <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
+              </h3>
+
+              <ol id="collapseHelpForApp" class="collapse">
+                <li>
+                  Register Slack App
+                  <ol>
+                    <li>
+                     Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
+                      <dl class="dl-horizontal">
+                        <dt>App Name</dt> <dd><code>growi</code> </dd>
+                        <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
+                      </dl>
+                    </li>
+                    <li><strong>Save</strong> it.</li>
+                  </ol>
+                </li>
+                <li>
+                  Set Permission Scopes to the App
+                  <ol>
+                    <li>Go to "OAuth &amp; Permissions" page.</li>
+                    <li>Add "Send messages as GROWI"(<code>chat:write:bot</code>).</li>
+                    <li>Don't forget to <strong>save</strong>.</li>
+                  </ol>
+                </li>
+                <li>
+                  Create a bot user
+                  <ol>
+                    <li>Go to "Bot Users" page and add.</li>
+                  </ol>
+                </li>
+                <li>
+                  Install the app
+                  <ol>
+                    <li>Go to "Install App to Your Workspace" page and install.</li>
+                    <li>Go to "OAuth &amp; Permissions" page and copy <code>OAuth Access Token</code>.</li>
+                  </ol>
+                </li>
+                <li>
+                  (At this page) Set OAuth Access Token
+                  <ol>
+                    <li>Input "OAuth Access Token".</li>
+                    <li>Don't forget to <strong>save</strong>.</li>
+                  </ol>
+                </li>
+              </ol>
+
+            </div><!-- /#slack-app -->
+
+          </div><!-- /.tab-content -->
+          <hr>
+          <h4>Default Notification Settings for Patterns</h4>
+
+          <table class="table table-bordered">
+            <thead>
+              <th>Pattern</th>
+              <th>Channel</th>
+              <th>Operation</th>
+            </thead>
+            <tbody class="admin-notif-list">
+              <form id="slackNotificationForm">
+              <tr>
+                <td>
+                  <input class="form-control" type="text" name="pathPattern" value="" placeholder="e.g. /projects/xxx/MTG/*">
+                  <p class="help-block">
+                    Path name of wiki. Pattern expression with <code>*</code> can be used.
+                  </p>
+                </td>
+                <td>
+                  <input class="form-control form-inline" type="text" name="channel" value="" placeholder="e.g. project-xxx">
+                  <p class="help-block">
+                    Slack channel name. Without <code>#</code>.
+                  </p>
+                </td>
+                <td>
+                  <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  <input type="submit" value="Add" class="btn btn-primary">
+                </td>
+              </tr>
+              </form>
+
+              {% for notif in settings %}
+              <tr class="admin-notif-row" data-updatepost-id="{{ notif._id.toString() }}">
+                <td>
+                  {{ notif.pathPattern }}
+                </td>
+                <td>
+                  {{ notif.channel }}
+                </td>
+                <td>
+                  <form class="admin-remove-updatepost">
+                    <input type="hidden" name="id" value="{{ notif._id.toString() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                    <input type="submit" value="Delete" class="btn btn-default">
+                  </form>
+                </td>
+              </tr>
+              {% endfor %}
+            </tbody>
+          </table>
+        </div><!-- /#user-trigger-notification -->
+
+        <div id="global-notification" class="tab-pane" role="tabpanel" >
+          <p class="alert alert-info">not implemented now</p>
+        </div><!-- /#global-notification -->
+
+      </div><!-- /.tab-content -->
 
     </div>
   </div>
 
   <script>
+    function activateTab(tab){
+      $('.nav-tabs a[href="#' + tab + '"]').tab('show');
+    };
+
+    function activateSlackIwh() {
+      $("#selectSlackOption").selectpicker('val', '1');
+      $("#slack-app").removeClass('active');
+      $("#slack-incoming-webhooks").addClass('active');
+    }
+
+    function activateSlackApp() {
+      $("#selectSlackOption").selectpicker('val', '2');
+      $("#slack-incoming-webhooks").removeClass('active');
+      $("#slack-app").addClass('active');
+    }
+
     window.addEventListener('load', function(e) {
       // hash on page
       if (location.hash) {
-        if (location.hash == '#slack-app') {
-          activateTab('slack-app');
+        if (location.hash == '#global-notification') {
+          activateTab('global-notification');
         }
       }
     });
 
-    function activateTab(tab){
-      $('.nav-tabs a[href="#' + tab + '"]').tab('show');
-    };
+    $("#selectSlackOption").on('change', function() {
+      if (this.value === "1") {
+        activateSlackIwh();
+      }
+      else if (this.value === "2") {
+        activateSlackApp();
+      }
+    });
   </script>
 </div>
 {% endblock content_main %}

+ 12 - 3
lib/views/admin/security.html

@@ -84,7 +84,7 @@
           <div class="form-group">
             <label for="settingForm[security:registrationWhiteList]" class="col-xs-3 control-label">{{ t('The whitelist of registration permission E-mail address') }}</label>
             <div class="col-xs-8">
-              <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @crowi.wiki">{{ settingForm['security:registrationWhiteList']|join('&#13')|raw }}</textarea>
+              <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="{{ t('security_setting.example') }}: @growi.org">{{ settingForm['security:registrationWhiteList']|join('&#13')|raw }}</textarea>
               <p class="help-block">{{ t("security_setting.restrict_emails") }}{{ t("security_setting.for_instance") }}<code>@growi.org</code>{{ t("security_setting.only_those") }}<br>
               {{ t("security_setting.insert_single") }}</p>
             </div>
@@ -100,7 +100,17 @@
         </fieldset>
       </form>
 
-      <form action="/_api/admin/security/mechanism" method="post" class="form-horizontal m-t-30" id="mechanismSetting" role="form">
+      <!-- prevent XSS link -->
+      <div class="mt-5">
+        <legend>{{ t('security_setting.xss_prevent_setting') }}</legend>
+        <div class="text-center">
+          <a class="flexbox" style="font-size: large;" href="/admin/markdown/#preventXSS">
+            <i class="fa-fw icon-login"></i> {{ t('security_setting.xss_prevent_setting_link') }}
+          </a>
+        </div>
+       </div>
+
+      <form action="/_api/admin/security/mechanism" method="post" class="form-horizontal mt-5" id="mechanismSetting" role="form">
         <fieldset>
           <legend class="alert-anchor">{{ t('Selecting authentication mechanism') }}</legend>
           <p class="alert alert-info"><b>{{ t("security_setting.note") }}: </b>{{ t("security_setting.require_server_restart_change_auth") }}</p>
@@ -274,7 +284,6 @@
         </div>
 
       </div><!-- /.auth-mechanism-configurations -->
-
     </div>
   </div>
 

+ 3 - 3
lib/views/admin/user-group-detail.html

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) }}{% endblock %}
+{% block html_title %}{{ customTitle(t('UserGroup management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 class="title" id="">{{ t('UserGroup management') + '/' + userGroup.name }}</h1>
+    <h1 class="title" id="">{{ t('UserGroup management') + '/' + userGroup.name | preventXss }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -199,7 +199,7 @@
                   <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu" role="menu">
-                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup.name}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup._id.toString()}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                   </form>
                   <li>

+ 3 - 3
lib/views/admin/user-groups.html

@@ -119,12 +119,12 @@
         </thead>
         <tbody>
           {% for sGroup in userGroups %}
-          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup.name %}
+          {% set sGroupDetailPageUrl = '/admin/user-group-detail/' + sGroup._id.toString() %}
           <tr>
             <td>
               <img src="{{ sGroup|picture }}" class="picture img-circle" />
             </td>
-            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name }}</a></td>
+            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name | preventXss }}</a></td>
             <td><ul class="list-inline">
               {% for relation in userGroupRelations.get(sGroup) %}
               <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
@@ -146,7 +146,7 @@
                   <li>
                     <a href="#"
                         data-user-group-id="{{ sGroup._id.toString() }}"
-                        data-user-group-name="{{ sGroup.name.toString() }}"
+                        data-user-group-name="{{ sGroup.name.toString() | encodeHTML }}"
                         data-target="#admin-delete-user-group-modal"
                         data-toggle="modal">
                       <i class="icon-fw icon-fire text-danger"></i> 削除する

+ 1 - 1
lib/views/admin/users.html

@@ -45,7 +45,7 @@
         <div id="inviteUserForm" class="collapse">
           <div class="form-group">
             <label for="inviteForm[emailList]">メールアドレス (複数行入力で複数人招待可能)</label>
-            <textarea class="form-control" name="inviteForm[emailList]" placeholder="例: user@crowi.wiki"></textarea>
+            <textarea class="form-control" name="inviteForm[emailList]" placeholder="例: user@growi.org"></textarea>
           </div>
           <div class="checkbox checkbox-info">
             <input type="checkbox" id="inviteWithEmail" name="inviteForm[sendEmail]" checked>

+ 25 - 0
lib/views/admin/widget/passport/github.html

@@ -26,6 +26,11 @@
       <label for="settingForm[security:passport-github:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
       <div class="col-xs-6">
         <input class="form-control" type="text" name="settingForm[security:passport-github:clientId]" value="{{ settingForm['security:passport-github:clientId'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_GITHUB_CLIENT_SECRET") }}
+          </small>
+        </p>
       </div>
     </div>
 
@@ -33,8 +38,28 @@
       <label for="settingForm[security:passport-github:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
       <div class="col-xs-6">
         <input class="form-control" type="text" name="settingForm[security:passport-github:clientSecret]" value="{{ settingForm['security:passport-github:clientSecret'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_GITHUB_CLIENT_SECRET") }}
+          </small>
+        </p>
       </div>
     </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-github:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <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>
+      </div>
+    </div>
+
     <div class="form-group">
       <div class="col-xs-6 col-xs-offset-3">
         <div class="checkbox checkbox-info">

+ 25 - 0
lib/views/admin/widget/passport/google-oauth.html

@@ -26,6 +26,11 @@
       <label for="settingForm[security:passport-google:clientId]" class="col-xs-3 control-label">{{ t("security_setting.clientID") }}</label>
       <div class="col-xs-6">
         <input class="form-control" type="text" name="settingForm[security:passport-google:clientId]" value="{{ settingForm['security:passport-google:clientId'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_GOOGLE_CLIENT_ID") }}
+          </small>
+        </p>
       </div>
     </div>
 
@@ -33,8 +38,28 @@
       <label for="settingForm[security:passport-google:clientSecret]" class="col-xs-3 control-label">{{ t("security_setting.client_secret") }}</label>
       <div class="col-xs-6">
         <input class="form-control" type="text" name="settingForm[security:passport-google:clientSecret]" value="{{ settingForm['security:passport-google:clientSecret'] || '' }}">
+        <p class="help-block">
+          <small>
+            {{ t("security_setting.Use env var if empty", "OAUTH_GOOGLE_CLIENT_SECRET") }}
+          </small>
+        </p>
       </div>
     </div>
+
+    <div class="form-group">
+      <label for="settingForm[security:passport-google:callbackUrl]" class="col-xs-3 control-label">{{ t("security_setting.callback_URL") }}</label>
+      <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>
+      </div>
+    </div>
+
     <div class="form-group">
       <div class="col-xs-6 col-xs-offset-3">
         <div class="checkbox checkbox-info">

+ 0 - 1
lib/views/layout/layout.html

@@ -88,7 +88,6 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
       <script src="{{ webpack_asset('styles/style.js') }}"></script>
       <script src="{{ webpack_asset('styles/theme-' + theme() + '.js') }}"></script>
     {% else %}
-      <link rel="stylesheet" href="{{ webpack_asset('js/vendors.css') }}">
       <link rel="stylesheet" href="{{ webpack_asset('styles/style.css') }}">
       <link rel="stylesheet" href="{{ webpack_asset('styles/theme-' + theme() + '.css') }}">
     {% endif %}

+ 1 - 1
lib/views/modal/create_page.html

@@ -46,7 +46,7 @@
 
         <div id="template-form" class="row form-horizontal m-t-15">
           <fieldset class="col-xs-12">
-            <legend>{{ t('template.modal_label.Create template under', parentPath(path)) }}</legend>
+            <legend>{{ t('template.modal_label.Create template under', parentPath(path)) | preventXss }}</legend>
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
                 <select id="template-type" class="form-control selectpicker" title="{{ t('template.option_label.select') }}">

+ 23 - 1
lib/views/modal/shortcuts.html

@@ -52,7 +52,29 @@
             </table>
           </div><!-- /.col-sm-6 -->
 
-        </div>
+        </div><!-- /.row -->
+
+        <div class="row">
+          <div class="col-sm-6">
+            <h3><strong></strong></h3>
+          </div><!-- /.col-sm-6 -->
+
+          <div class="col-sm-6">
+            <h3><strong>{{ t('modal_shortcuts.commentform.title') }}</strong></h3>
+
+            <table class="table">
+              <tr>
+                <th>{{ t('modal_shortcuts.commentform.Post') }}:</th>
+                <td><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
+              </tr>
+              <tr>
+                <th>{{ t('modal_shortcuts.editor.Delete Line') }}:</th>
+                <td><span class="key cmd-key"></span> + <span class="key">D</span></td>
+              </tr>
+            </table>
+          </div><!-- /.col-sm-6 -->
+
+        </div><!-- /.row -->
 
       </div>
 

+ 7 - 0
lib/views/widget/icon-keyboard-return-enter.html

@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20px" viewBox="0 0 34 21">
+  <g id="ba5f4106-f870-416b-bb0c-2580c9a76268">
+    <g id="1def15e1-5198-4ca2-9457-3b509e83053f">
+      <polygon points="31 0 31 9 5 9 11.8 1.8 10 0 0 10.5 10 21 11.8 19.2 5 12 34 12 34 0 31 0" />
+    </g>
+  </g>
+</svg>

+ 2 - 2
lib/views/widget/not_found_content.html

@@ -8,8 +8,8 @@
 </div>
 
 <div id="content-main" class="content-main content-main-not-found page-list"
-  data-path="{{ path }}"
-  data-path-shortname="{{ path|path2name }}"
+  data-path="{{ path | preventXss }}"
+  data-path-shortname="{{ path|path2name | preventXss }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
 

+ 1 - 1
lib/views/widget/page_alerts.html

@@ -7,7 +7,7 @@
       {% elseif page.grant == 4 %}
         <i class="icon-fw icon-lock"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
       {% elseif page.grant == 5 %}
-        <i class="icon-fw icon-organization"></i><strong>'{{ pageRelatedGroup.name }}' only</strong> ({{ t('Browsing of this page is restricted') }})
+        <i class="icon-fw icon-organization"></i><strong>'{{ pageRelatedGroup.name | preventXss }}' only</strong> ({{ t('Browsing of this page is restricted') }})
       {% endif %}
       </p>
     {% endif %}

+ 1 - 1
lib/views/widget/page_content.html

@@ -17,7 +17,7 @@
   <div class="tab-content">
 
     {% if page %}
-      <script type="text/template" id="raw-text-original">{{ revision.body.toString() | sanitize }}</script>
+      <script type="text/template" id="raw-text-original">{{ revision.body.toString() | encodeHTML }}</script>
 
       {# formatted text #}
       <div class="tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body">

+ 1 - 1
lib/views/widget/page_list_and_timeline.html

@@ -33,7 +33,7 @@
             <div class="revision-body wiki"></div>
           </div>
         </div>
-        <script type="text/template">{{ page.revision.body.toString() | sanitize }}</script>
+        <script type="text/template">{{ page.revision.body.toString() | encodeHTML }}</script>
       </div>
       <hr>
       {% endfor %}

+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.1.8-RC",
+  "version": "3.1.12-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -136,8 +136,9 @@
     "date-fns": "^1.29.0",
     "diff2html": "^2.3.3",
     "eazy-logger": "^3.0.2",
-    "eslint": "^4.19.1",
+    "eslint": "^5.0.0",
     "eslint-plugin-react": "^7.7.0",
+    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.0",
     "i18next-browser-languagedetector": "^2.2.0",
     "jquery-slimscroll": "^1.3.8",
@@ -156,7 +157,6 @@
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "metismenu": "^2.7.4",
-    "mini-css-extract-plugin": "^0.4.0",
     "mocha": "^5.0.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
@@ -170,7 +170,7 @@
     "postcss-loader": "^2.1.3",
     "react": "^16.4.1",
     "react-bootstrap": "^0.32.1",
-    "react-bootstrap-typeahead": "^3.1.4",
+    "react-bootstrap-typeahead": "=3.0.4",
     "react-clipboard.js": "^2.0.0",
     "react-codemirror2": "^5.0.4",
     "react-dom": "^16.4.1",

+ 8 - 2
resource/js/app.js

@@ -4,6 +4,8 @@ import { I18nextProvider } from 'react-i18next';
 
 import i18nFactory from './i18n';
 
+import Xss from '../../lib/util/xss';
+
 import Crowi from './util/Crowi';
 // import CrowiRenderer from './util/CrowiRenderer';
 import GrowiRenderer from './util/GrowiRenderer';
@@ -25,7 +27,7 @@ import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
-import NewPageNameInputter from './components/NewPageNameInputter';
+import NewPageNameInput from './components/NewPageNameInput';
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -40,6 +42,10 @@ if (!window) {
 const userlang = $('body').data('userlang');
 const i18n = i18nFactory(userlang);
 
+// setup xss library
+const xss = new Xss();
+window.xss = xss;
+
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
 let pageRevisionId = null;
@@ -116,7 +122,7 @@ const componentMappings = {
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
-  'page-name-inputter': <NewPageNameInputter crowi={crowi} parentPageName={pagePath} />,
+  'page-name-inputter': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
 
 };
 // additional definitions if data exists

+ 6 - 1
resource/js/components/CopyButton.js

@@ -8,6 +8,9 @@ export default class CopyButton extends React.Component {
     super(props);
 
     this.showToolTip = this.showToolTip.bind(this);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
 
   showToolTip() {
@@ -27,12 +30,14 @@ export default class CopyButton extends React.Component {
       verticalAlign: 'text-top',
     }, this.props.buttonStyle);
 
+    const text = this.xss.process(this.props.text);
+
     return (
       <span className="btn-copy-container" style={containerStyle}>
         <ClipboardButton className={this.props.buttonClassName}
             button-id={this.props.buttonId} button-data-toggle="tooltip" button-data-container="body" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
             button-style={style}
-            data-clipboard-text={this.props.text} onSuccess={this.showToolTip}>
+            data-clipboard-text={text} onSuccess={this.showToolTip}>
 
           <i className={this.props.iconClassName}></i>
         </ClipboardButton>

+ 3 - 3
resource/js/components/NewPageNameInputter.js → resource/js/components/NewPageNameInput.js

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import SearchTypeahead from './SearchTypeahead';
 
-export default class NewPageNameInputter extends React.Component {
+export default class NewPageNameInput extends React.Component {
 
   constructor(props) {
 
@@ -59,11 +59,11 @@ export default class NewPageNameInputter extends React.Component {
   }
 }
 
-NewPageNameInputter.propTypes = {
+NewPageNameInput.propTypes = {
   crowi:          PropTypes.object.isRequired,
   parentPageName: PropTypes.string,
 };
 
-NewPageNameInputter.defaultProps = {
+NewPageNameInput.defaultProps = {
   parentPageName: '',
 };

+ 4 - 1
resource/js/components/Page/RevisionPath.js

@@ -13,6 +13,9 @@ export default class RevisionPath extends React.Component {
       isListPage: false,
       isLinkToListPage: true,
     };
+
+    // retrieve xss library from window
+    this.xss = window.xss;
   }
 
   componentWillMount() {
@@ -37,7 +40,7 @@ export default class RevisionPath extends React.Component {
     splitted.forEach((pageName) => {
       pages.push({
         pagePath: parentPath + encodeURIComponent(pageName),
-        pageName: pageName,
+        pageName: this.xss.process(pageName),
       });
       parentPath += pageName + '/';
     });

+ 10 - 1
resource/js/components/Page/RevisionUrl.js

@@ -5,15 +5,24 @@ import CopyButton from '../CopyButton';
 
 export default class RevisionUrl extends React.Component {
 
+  constructor(props) {
+    super(props);
+
+    // retrieve xss library from window
+    this.xss = window.xss;
+  }
+
   render() {
     const buttonStyle = {
       fontSize: '1em'
     };
 
+    const pagePath = this.xss.process(this.props.pagePath);
+
     const url = (this.props.pageId == null)
       ? decodeURIComponent(location.href)
       : `${location.origin}/${this.props.pageId}`;
-    const copiedText = this.props.pagePath + '\n' + url;
+    const copiedText = pagePath + '\n' + url;
 
     return (
       <span>

+ 5 - 1
resource/js/components/PageComment/CommentForm.js

@@ -72,7 +72,10 @@ export default class CommentForm extends React.Component {
    * Load data of comments and rerender <PageComments />
    */
   postComment(event) {
-    event.preventDefault();
+    if (event != null) {
+      event.preventDefault();
+    }
+
     this.props.crowi.apiPost('/comments.add', {
       commentForm: {
         comment: this.state.comment,
@@ -222,6 +225,7 @@ export default class CommentForm extends React.Component {
                         emojiStrategy={emojiStrategy}
                         onChange={this.updateState}
                         onUpload={this.onUpload}
+                        onCtrlEnter={this.postComment}
                       />
                     </Tab>
                     { this.state.isMarkdown == true &&

+ 2 - 0
resource/js/components/PageEditor/AbstractEditor.js

@@ -109,6 +109,7 @@ export default class AbstractEditor extends React.Component {
       this.props.onPasteFiles(event);
     }
   }
+
 }
 
 AbstractEditor.propTypes = {
@@ -121,6 +122,7 @@ AbstractEditor.propTypes = {
   onSave: PropTypes.func,
   onPasteFiles: PropTypes.func,
   onDragEnter: PropTypes.func,
+  onCtrlEnter: PropTypes.func,
 };
 AbstractEditor.defaultProps = {
   isGfmMode: true,

+ 24 - 5
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -8,6 +8,15 @@ const loadScript = require('simple-load-script');
 const loadCssSync = require('load-css-file');
 
 import * as codemirror from 'codemirror';
+// set save handler
+codemirror.commands.save = (instance) => {
+  if (instance.codeMirrorEditor != null) {
+    instance.codeMirrorEditor.dispatchSave();
+  }
+};
+// set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
+window.CodeMirror = require('codemirror');
+
 
 import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
 require('codemirror/addon/edit/matchbrackets');
@@ -62,6 +71,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.loadKeymapMode = this.loadKeymapMode.bind(this);
     this.setKeymapMode = this.setKeymapMode.bind(this);
     this.handleEnterKey = this.handleEnterKey.bind(this);
+    this.handleCtrlEnterKey = this.handleCtrlEnterKey.bind(this);
 
     this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
     this.pasteHandler = this.pasteHandler.bind(this);
@@ -91,13 +101,11 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   componentDidMount() {
+    // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
+    this.getCodeMirror().codeMirrorEditor = this;
+
     // initialize caret line
     this.setCaretLine(0);
-    // set save handler
-    codemirror.commands.save = this.dispatchSave;
-
-    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
-    window.CodeMirror = require('codemirror');
   }
 
   componentWillReceiveProps(nextProps) {
@@ -362,6 +370,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
       });
   }
 
+  /**
+   * handle Ctrl+ENTER key
+   */
+  handleCtrlEnterKey() {
+    if (this.props.onCtrlEnter != null) {
+      this.props.onCtrlEnter();
+    }
+  }
+
   scrollCursorIntoViewHandler(editor, event) {
     if (this.props.onScrollCursorIntoView != null) {
       const line = editor.getCursor().line;
@@ -461,6 +478,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
           // continuelist, indentlist
           extraKeys: {
             'Enter': this.handleEnterKey,
+            'Ctrl-Enter': this.handleCtrlEnterKey,
+            'Cmd-Enter': this.handleCtrlEnterKey,
             'Tab': 'indentMore',
             'Shift-Tab': 'indentLess',
             'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },

+ 4 - 18
resource/js/components/PageEditor/Editor.js

@@ -185,22 +185,9 @@ export default class Editor extends AbstractEditor {
     return className;
   }
 
-  getOverlayStyle() {
-    return {
-      position: 'absolute',
-      zIndex: 4,  // forward than .CodeMirror-gutters
-      top: 0,
-      right: 0,
-      bottom: 0,
-      left: 0,
-    };
-  }
-
   renderDropzoneOverlay() {
-    const overlayStyle = this.getOverlayStyle();
-
     return (
-      <div style={overlayStyle} className="overlay">
+      <div className="overlay">
         {this.state.isUploading &&
           <span className="overlay-content">
             <div className="speeding-wheel d-inline-block"></div>
@@ -221,8 +208,8 @@ export default class Editor extends AbstractEditor {
 
     const isMobile = this.props.isMobile;
 
-    return <React.Fragment>
-      <div style={flexContainer}>
+    return (
+      <div style={flexContainer} className="editor-container">
         <Dropzone
             ref="dropzone"
             disableClick
@@ -270,8 +257,7 @@ export default class Editor extends AbstractEditor {
         </button>
 
       </div>
-
-    </React.Fragment>;
+    );
   }
 
 }

+ 4 - 1
resource/js/components/PageEditor/GrantSelector.js

@@ -43,6 +43,9 @@ class GrantSelector extends React.Component {
       };
     }
 
+    // retrieve xss library from window
+    this.xss = window.xss;
+
     this.showSelectGroupModal = this.showSelectGroupModal.bind(this);
     this.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
 
@@ -81,7 +84,7 @@ class GrantSelector extends React.Component {
 
   getGroupName() {
     const pageGrantGroup = this.state.pageGrantGroup;
-    return pageGrantGroup ? pageGrantGroup.name : '';
+    return pageGrantGroup ? this.xss.process(pageGrantGroup.name) : '';
   }
 
   /**

+ 2 - 0
resource/js/legacy/crowi-admin.js

@@ -106,3 +106,5 @@ $(function() {
   // style switcher
   $('#styleOptions').styleSwitcher();
 });
+
+

+ 1 - 1
resource/js/legacy/crowi.js

@@ -1000,7 +1000,7 @@ window.addEventListener('keydown', (event) => {
 
   // ignore when target dom is input
   const inputPattern = /^input|textinput|textarea$/i;
-  if (target.tagName.match(inputPattern) || target.isContentEditable) {
+  if (inputPattern.test(target.tagName) || target.isContentEditable) {
     return;
   }
 

+ 13 - 3
resource/js/util/PreProcessor/XssFilter.js

@@ -1,14 +1,24 @@
 import Xss from '../../../../lib/util/xss';
+import xssOption from '../../../../lib/util/xssOption';
 
 export default class XssFilter {
 
   constructor(crowi) {
-    // TODO read options
-    this.xss = new Xss(true);
+    this.crowi = crowi;
+
+    if (crowi.config.isEnabledXssPrevention) {
+      this.xssOption = new xssOption(crowi.config);
+      this.xss = new Xss(this.xssOption);
+    }
   }
 
   process(markdown) {
-    return this.xss.process(markdown);
+    if (this.crowi.config.isEnabledXssPrevention) {
+      return this.xss.process(markdown);
+    }
+    else {
+      return markdown;
+    }
   }
 
 }

+ 4 - 0
resource/styles/agile-admin/inverse/colors/mono-blue.scss

@@ -33,5 +33,9 @@ $inline-code-bg: lighten($subthemecolor,70%);
   .code-line.revision-head.highlighted {
     background-color: lighten($themecolor,20%);
     color: $themelight;
+
+    .icon-note, .icon-link {
+      color: $themelight;
+    }
   }
 }

+ 3 - 2
resource/styles/scss/_admin.scss

@@ -1,4 +1,5 @@
 .admin-page {
+    //security XSS prevent
 
   .admin-user-menu {
     .dropdown-menu {
@@ -6,7 +7,7 @@
       right: 0;
       width: 300px;
     }
-  }
+   }
 
   .admin-group-menu {
     .dropdown-menu {
@@ -37,7 +38,7 @@
     }
 
     .auth-mechanism-configurations {
-      min-height: 800px;
+      min-height: 300px;
     }
   }
 

+ 125 - 0
resource/styles/scss/_editor-overlay.scss

@@ -0,0 +1,125 @@
+.editor-container {
+  .overlay {
+    // layout
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    position: absolute;
+    z-index: 7;  // forward than .CodeMirror-vscrollbar
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+  }
+
+  .overlay-content {
+    padding: 0.5em;
+  }
+
+  .page-editor-editor-container {
+    .overlay-content {
+      font-size: 2.5em;
+    }
+  }
+
+  @mixin overlay-processing-style() {
+    .overlay {
+      background: rgba(255,255,255,0.5);
+    }
+    .overlay-content {
+      padding: 0.3em;
+      background: rgba(200,200,200,0.5);
+      color: #444;
+    }
+  }
+  // add icon on cursor
+  .autoformat-markdown-table-activated .CodeMirror-cursor {
+    &:after {
+      font-family: 'FontAwesome';
+      content: '\f0ce';
+    }
+  }
+
+  // for Dropzone
+  .dropzone {
+    @mixin insertSimpleLineIcons($code) {
+      &:before {
+        margin-right: 0.2em;
+        font-family: 'simple-line-icons';
+        content: $code;
+      }
+    }
+
+    position: relative;   // against .overlay position: absolute
+
+    // unuploadable or rejected
+    &.dropzone-unuploadable, &.dropzone-rejected {
+      .overlay {
+        background: rgba(200,200,200,0.8);
+      }
+      .overlay-content {
+        color: #444;
+      }
+    }
+    // uploading
+    &.dropzone-uploading {
+      @include overlay-processing-style();
+    }
+
+    // unuploadable
+    &.dropzone-unuploadable {
+      .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e617");  // icon-exclamation
+        &:after {
+          content: "File uploading is disabled";
+        }
+      }
+    }
+    // uploadable
+    &.dropzone-uploadable {
+      // accepted
+      &.dropzone-accepted:not(.dropzone-rejected) {
+        .overlay {
+          border: 4px dashed #ccc;
+        }
+        .overlay-content {
+          // insert content
+          @include insertSimpleLineIcons("\e084");  // icon-cloud-upload
+          &:after {
+            content: "Drop here to upload";
+          }
+          // style
+          color: #666;
+          background: rgba(200,200,200,0.8);
+        }
+      }
+      // file type mismatch
+      &.dropzone-rejected:not(.dropzone-uploadablefile) .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e032");  // icon-picture
+        &:after {
+          content: "Only an image file is allowed";
+        }
+      }
+      // multiple files
+      &.dropzone-accepted.dropzone-rejected .overlay-content {
+        // insert content
+        @include insertSimpleLineIcons("\e617");  // icon-exclamation
+        &:after {
+          content: "Only 1 file is allowed";
+        }
+      }
+    }
+  } // end of.dropzone
+
+  .textarea-editor {
+    border: none;
+    font-family: monospace;
+  }
+
+  .loading-keymap {
+    @include overlay-processing-style();
+  }
+}

+ 4 - 0
resource/styles/scss/_shortcuts.scss

@@ -38,6 +38,10 @@
     text-transform: uppercase;
     text-align: center;
     color: #666;
+    /* SVG Properties*/
+    polygon {
+      fill: #666;
+    }
 
     &.key-longer {
       width: 64px;

+ 1 - 0
resource/styles/scss/style.scss

@@ -21,6 +21,7 @@
 @import 'comment_growi';
 @import 'create-page';
 @import 'create-template';
+@import 'editor-overlay';
 @import 'layout';
 @import 'layout_crowi';
 @import 'layout_crowi_sidebar';

+ 154 - 103
yarn.lock

@@ -252,25 +252,17 @@ acorn-dynamic-import@^3.0.0:
   dependencies:
     acorn "^5.0.0"
 
-acorn-jsx@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+acorn-jsx@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e"
   dependencies:
-    acorn "^3.0.4"
-
-acorn@^3.0.4:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+    acorn "^5.0.3"
 
 acorn@^5.0.0, acorn@^5.1.1:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
 
-acorn@^5.5.0:
-  version "5.5.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
-
-acorn@^5.6.2:
+acorn@^5.0.3, acorn@^5.6.0, acorn@^5.6.2:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
 
@@ -284,9 +276,9 @@ agentkeepalive@^3.4.1:
   dependencies:
     humanize-ms "^1.2.1"
 
-ajv-keywords@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
+ajv-keywords@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
 
 ajv-keywords@^3.1.0:
   version "3.1.0"
@@ -299,7 +291,7 @@ ajv@^4.9.1:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^5.0.0, ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
+ajv@^5.0.0, ajv@^5.1.0:
   version "5.5.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
   dependencies:
@@ -308,6 +300,15 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
+ajv@^6.0.1, ajv@^6.5.0:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.1.tgz#88ebc1263c7133937d108b80c5572e64e1d9322d"
+  dependencies:
+    fast-deep-equal "^2.0.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.1"
+
 ajv@^6.1.0:
   version "6.2.1"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671"
@@ -552,6 +553,12 @@ async@^2.3.0:
   dependencies:
     lodash "^4.14.0"
 
+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@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -631,7 +638,7 @@ axios@^0.18.0:
     follow-redirects "^1.3.0"
     is-buffer "^1.1.5"
 
-babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+babel-code-frame@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
   dependencies:
@@ -1947,8 +1954,8 @@ code-point-at@^1.0.0:
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
 codemirror@^5.37.0:
-  version "5.37.0"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.37.0.tgz#c349b584e158f590277f26d37c2469a6bc538036"
+  version "5.39.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.39.0.tgz#4654f7d2f7e525e04a62e72d9482348ccb37dce5"
 
 collection-visit@^1.0.0:
   version "1.0.0"
@@ -2051,14 +2058,6 @@ concat-stream@^1.5.0:
     readable-stream "^2.2.2"
     typedarray "^0.0.6"
 
-concat-stream@^1.6.0:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26"
-  dependencies:
-    inherits "^2.0.3"
-    readable-stream "^2.2.2"
-    typedarray "^0.0.6"
-
 connect-browser-sync@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/connect-browser-sync/-/connect-browser-sync-2.1.0.tgz#1248da281a439fe99b023270d18555c1f046c229"
@@ -2812,6 +2811,16 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
+es-abstract@^1.10.0:
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
+  dependencies:
+    es-to-primitive "^1.1.1"
+    function-bind "^1.1.1"
+    has "^1.0.1"
+    is-callable "^1.1.3"
+    is-regex "^1.0.4"
+
 es-abstract@^1.4.3:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
@@ -2868,59 +2877,66 @@ eslint-scope@^3.7.1:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
+eslint-scope@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
 eslint-visitor-keys@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
 
-eslint@^4.19.1:
-  version "4.19.1"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
+eslint@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.0.0.tgz#3576704f7377aca072da69c00862277c5fe57153"
   dependencies:
-    ajv "^5.3.0"
-    babel-code-frame "^6.22.0"
+    ajv "^6.5.0"
+    babel-code-frame "^6.26.0"
     chalk "^2.1.0"
-    concat-stream "^1.6.0"
-    cross-spawn "^5.1.0"
+    cross-spawn "^6.0.5"
     debug "^3.1.0"
     doctrine "^2.1.0"
-    eslint-scope "^3.7.1"
+    eslint-scope "^4.0.0"
     eslint-visitor-keys "^1.0.0"
-    espree "^3.5.4"
-    esquery "^1.0.0"
+    espree "^4.0.0"
+    esquery "^1.0.1"
     esutils "^2.0.2"
     file-entry-cache "^2.0.0"
     functional-red-black-tree "^1.0.1"
     glob "^7.1.2"
-    globals "^11.0.1"
+    globals "^11.5.0"
     ignore "^3.3.3"
     imurmurhash "^0.1.4"
-    inquirer "^3.0.6"
-    is-resolvable "^1.0.0"
-    js-yaml "^3.9.1"
+    inquirer "^5.2.0"
+    is-resolvable "^1.1.0"
+    js-yaml "^3.11.0"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.3.0"
-    lodash "^4.17.4"
-    minimatch "^3.0.2"
+    lodash "^4.17.5"
+    minimatch "^3.0.4"
     mkdirp "^0.5.1"
     natural-compare "^1.4.0"
     optionator "^0.8.2"
     path-is-inside "^1.0.2"
     pluralize "^7.0.0"
     progress "^2.0.0"
-    regexpp "^1.0.1"
+    regexpp "^1.1.0"
     require-uncached "^1.0.3"
-    semver "^5.3.0"
+    semver "^5.5.0"
+    string.prototype.matchall "^2.0.0"
     strip-ansi "^4.0.0"
-    strip-json-comments "~2.0.1"
-    table "4.0.2"
-    text-table "~0.2.0"
+    strip-json-comments "^2.0.1"
+    table "^4.0.3"
+    text-table "^0.2.0"
 
-espree@^3.5.4:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
+espree@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634"
   dependencies:
-    acorn "^5.5.0"
-    acorn-jsx "^3.0.0"
+    acorn "^5.6.0"
+    acorn-jsx "^4.1.1"
 
 esprima@^2.6.0:
   version "2.7.3"
@@ -2934,9 +2950,9 @@ esprima@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9"
 
-esquery@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+esquery@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
   dependencies:
     estraverse "^4.0.0"
 
@@ -3121,9 +3137,9 @@ extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
-external-editor@^2.0.4:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
+external-editor@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
   dependencies:
     chardet "^0.4.0"
     iconv-lite "^0.4.17"
@@ -3156,6 +3172,15 @@ extglob@^2.0.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+extract-text-webpack-plugin@^4.0.0-beta.0:
+  version "4.0.0-beta.0"
+  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-4.0.0-beta.0.tgz#f7361d7ff430b42961f8d1321ba8c1757b5d4c42"
+  dependencies:
+    async "^2.4.1"
+    loader-utils "^1.1.0"
+    schema-utils "^0.4.5"
+    webpack-sources "^1.1.0"
+
 extsprintf@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.2.0.tgz#5ad946c22f5b32ba7f8cd7426711c6e8a3fc2529"
@@ -3172,6 +3197,10 @@ fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
 
+fast-deep-equal@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+
 fast-json-stable-stringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -3572,9 +3601,9 @@ global-modules-path@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.1.0.tgz#923ec524e8726bb0c1a4ed4b8e21e1ff80c88bbb"
 
-globals@^11.0.1:
-  version "11.3.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
+globals@^11.5.0:
+  version "11.7.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
 
 globals@^9.18.0:
   version "9.18.0"
@@ -4057,21 +4086,20 @@ ini@~1.3.0:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
-inquirer@^3.0.6:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+inquirer@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
   dependencies:
     ansi-escapes "^3.0.0"
     chalk "^2.0.0"
     cli-cursor "^2.1.0"
     cli-width "^2.0.0"
-    external-editor "^2.0.4"
+    external-editor "^2.1.0"
     figures "^2.0.0"
     lodash "^4.3.0"
     mute-stream "0.0.7"
     run-async "^2.2.0"
-    rx-lite "^4.0.8"
-    rx-lite-aggregates "^4.0.8"
+    rxjs "^5.5.2"
     string-width "^2.1.0"
     strip-ansi "^4.0.0"
     through "^2.3.6"
@@ -4339,7 +4367,7 @@ is-regex@^1.0.4:
   dependencies:
     has "^1.0.1"
 
-is-resolvable@^1.0.0:
+is-resolvable@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
 
@@ -4455,7 +4483,7 @@ js-yaml@3.5.4:
     argparse "^1.0.2"
     esprima "^2.6.0"
 
-js-yaml@^3.11.0, js-yaml@^3.4.3, js-yaml@^3.9.1:
+js-yaml@^3.11.0, js-yaml@^3.4.3:
   version "3.11.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
   dependencies:
@@ -4493,6 +4521,10 @@ json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
 
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
 json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -5201,13 +5233,6 @@ mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
 
-mini-css-extract-plugin@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.0.tgz#ff3bf08bee96e618e177c16ca6131bfecef707f9"
-  dependencies:
-    loader-utils "^1.1.0"
-    webpack-sources "^1.1.0"
-
 minimalistic-assert@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
@@ -6641,6 +6666,10 @@ punycode@^1.2.4, punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
 
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
 q@^1.0.1, q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@@ -6726,9 +6755,9 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-react-bootstrap-typeahead@^3.1.4:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.1.4.tgz#970106959e0491f29fdeeb5dd7f51ca87b83c2e2"
+react-bootstrap-typeahead@=3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.0.4.tgz#1ebfb259f986c86ff03beef1091cab4614d6ea1d"
   dependencies:
     classnames "^2.2.0"
     escape-string-regexp "^1.0.5"
@@ -6738,7 +6767,7 @@ react-bootstrap-typeahead@^3.1.4:
     prop-types-extra "^1.0.1"
     react-onclickoutside "^6.1.1"
     react-overlays "^0.8.1"
-    react-popper "^0.10.4"
+    react-popper "^0.9.0"
     warning "^3.0.0"
 
 react-bootstrap@^0.32.1:
@@ -6812,9 +6841,9 @@ react-overlays@^0.8.0, react-overlays@^0.8.1:
     react-transition-group "^2.2.0"
     warning "^3.0.0"
 
-react-popper@^0.10.4:
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.4.tgz#af2a415ea22291edd504678d7afda8a6ee3295aa"
+react-popper@^0.9.0:
+  version "0.9.5"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.9.5.tgz#02a24ef3eec33af9e54e8358ab70eb0e331edd05"
   dependencies:
     popper.js "^1.14.1"
     prop-types "^15.6.1"
@@ -7002,7 +7031,13 @@ regexp-clone@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589"
 
-regexpp@^1.0.1:
+regexp.prototype.flags@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c"
+  dependencies:
+    define-properties "^1.1.2"
+
+regexpp@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
 
@@ -7323,20 +7358,16 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
-rx-lite-aggregates@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
-  dependencies:
-    rx-lite "*"
-
-rx-lite@*, rx-lite@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
-
 rx@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
 
+rxjs@^5.5.2:
+  version "5.5.11"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87"
+  dependencies:
+    symbol-observable "1.0.1"
+
 rxjs@^6.1.0:
   version "6.2.1"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.1.tgz#246cebec189a6cbc143a3ef9f62d6f4c91813ca1"
@@ -7905,6 +7936,16 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
+string.prototype.matchall@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-2.0.0.tgz#2af8fe3d2d6dc53ca2a59bd376b089c3c152b3c8"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.10.0"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    regexp.prototype.flags "^1.2.0"
+
 string.prototype.padend@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
@@ -7969,7 +8010,7 @@ strip-indent@^1.0.1:
   dependencies:
     get-stdin "^4.0.1"
 
-strip-json-comments@~2.0.1:
+strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
@@ -8039,12 +8080,16 @@ swig-templates@^2.0.2:
     optimist "~0.6"
     uglify-js "2.6.0"
 
-table@4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+symbol-observable@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
+table@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
   dependencies:
-    ajv "^5.2.3"
-    ajv-keywords "^2.1.0"
+    ajv "^6.0.1"
+    ajv-keywords "^3.0.0"
     chalk "^2.1.0"
     lodash "^4.17.4"
     slice-ansi "1.0.0"
@@ -8091,7 +8136,7 @@ text-encoding@^0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
 
-text-table@~0.2.0:
+text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
 
@@ -8393,6 +8438,12 @@ upath@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
 
+uri-js@^4.2.1:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  dependencies:
+    punycode "^2.1.0"
+
 urix@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"