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

Merge remote-tracking branch 'origin/imprv/add-option-preventXSS' into imprv/add-option-preventXSS-morita

# Conflicts:
#	lib/views/admin/markdown.html
Yuki Takei 7 лет назад
Родитель
Сommit
dffc00ff17
37 измененных файлов с 803 добавлено и 574 удалено
  1. 11 1
      CHANGES.md
  2. 6 1
      README.md
  3. 12 3
      THIRD-PARTY-NOTICES.md
  4. 0 9
      lib/form/admin/markdownXSS.js
  5. 11 0
      lib/form/admin/markdownXss.js
  6. 1 1
      lib/form/index.js
  7. 18 14
      lib/locales/en-US/translation.json
  8. 4 0
      lib/locales/ja/translation.json
  9. 80 3
      lib/models/config.js
  10. 0 20
      lib/models/page.js
  11. 16 8
      lib/routes/admin.js
  12. 3 4
      lib/routes/index.js
  13. 18 16
      lib/routes/login-passport.js
  14. 0 21
      lib/routes/page.js
  15. 4 4
      lib/service/passport.js
  16. 18 0
      lib/util/recommendedXssWhiteList.js
  17. 20 7
      lib/util/xss.js
  18. 1 1
      lib/views/admin/app.html
  19. 123 129
      lib/views/admin/markdown.html
  20. 224 194
      lib/views/admin/notification.html
  21. 1 1
      lib/views/admin/security.html
  22. 1 1
      lib/views/admin/users.html
  23. 10 0
      lib/views/admin/widget/passport/github.html
  24. 10 0
      lib/views/admin/widget/passport/google-oauth.html
  25. 1 32
      lib/views/modal/create_page.html
  26. 3 1
      lib/views/modal/create_template.html
  27. 23 1
      lib/views/modal/shortcuts.html
  28. 7 0
      lib/views/widget/icon-keyboard-return-enter.html
  29. 2 2
      package.json
  30. 5 1
      resource/js/components/PageComment/CommentForm.js
  31. 2 0
      resource/js/components/PageEditor/AbstractEditor.js
  32. 24 5
      resource/js/components/PageEditor/CodeMirrorEditor.js
  33. 1 1
      resource/js/legacy/crowi.js
  34. 1 2
      resource/js/util/PreProcessor/XssFilter.js
  35. 4 0
      resource/styles/agile-admin/inverse/colors/mono-blue.scss
  36. 4 0
      resource/styles/scss/_shortcuts.scss
  37. 134 91
      yarn.lock

+ 11 - 1
CHANGES.md

@@ -1,20 +1,29 @@
 CHANGES
 ========
 
-## 3.1.8-RC
+## 3.1.10-RC
+
+
+## 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
@@ -57,6 +66,7 @@ CHANGES
 
 ## 3.1.4 (Missing number)
 
+
 ## 3.1.3 (Missing number)
 
 

+ 6 - 1
README.md

@@ -158,13 +158,18 @@ 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_GITHUB_CLIENT_ID: GitHub API client id for OAuth login
+    * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login
 
 
 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

+ 0 - 9
lib/form/admin/markdownXSS.js

@@ -1,9 +0,0 @@
-'use strict';
-
-var form = require('express-form')
-  , field = form.field;
-
-module.exports = form(
-  field('markdownSetting[markdown:isEnabledPreventXSS]').trim().toBooleanStrict()
-);
-

+ 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:isPrevented]').trim().toBooleanStrict(),
+  field('markdownSetting[markdown:xss:option]').trim().toInt(),
+  field('markdownSetting[markdown:xss:tagWhiteList]').trim(),
+  field('markdownSetting[markdown:xss:attrWhiteList]').trim()
+);

+ 1 - 1
lib/form/index.js

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

+ 18 - 14
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": {
@@ -339,6 +342,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",

+ 4 - 0
lib/locales/ja/translation.json

@@ -234,6 +234,9 @@
         "Outdent": "左インデント",
         "Save Page": "保存",
         "Delete Line": "行削除"
+    },
+    "commentform": {
+      "Post": "投稿"
     }
   },
 
@@ -356,6 +359,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 モード",

+ 80 - 3
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,8 +102,12 @@ module.exports = function(crowi) {
 
   function getDefaultMarkdownConfigs() {
     return {
+      'markdown:xss:isPrevented': false,
+      'markdown:xss:option': 2,
+      'markdown:xss:tagWhiteList': [],
+      'markdown:xss:attrWhiteList': [],
       'markdown:isEnabledLinebreaks': false,
-      'markdown:isEnabledPreventXSS': false,
+      'markdown:isEnabledPreventXss': false,
       'markdown:isEnabledLinebreaksInComments': true,
     };
   }
@@ -335,8 +340,8 @@ module.exports = function(crowi) {
     return config.markdown[key];
   };
 
-  configSchema.statics.isEnabledPreventXSS = function(config) {
-    const key = 'markdown:isEnabledPreventXSS';
+  configSchema.statics.isXssPrevented = function(config) {
+    const key = 'markdown:xss:isPrevented';
 
     // return default value if undefined
     if (undefined === config.markdown || undefined === config.markdown[key]) {
@@ -346,6 +351,74 @@ module.exports = function(crowi) {
     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.isXssPrevented(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.isXssPrevented(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
    */
@@ -486,6 +559,10 @@ module.exports = function(crowi) {
       layoutType: Config.layoutType(config),
       isEnabledLinebreaks: Config.isEnabledLinebreaks(config),
       isEnabledLinebreaksInComments: Config.isEnabledLinebreaksInComments(config),
+      isXssPrevented: Config.isXssPrevented(config),
+      xssOption: Config.xssOption(config),
+      tagWhiteList: Config.tagWhiteList(config),
+      attrWhiteList: Config.attrWhiteList(config),
       highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
       env: {

+ 0 - 20
lib/models/page.js

@@ -530,26 +530,6 @@ module.exports = function(crowi) {
     });
   };
 
-  // check if a given page has a children and decendants tempalte
-  pageSchema.statics.checkIfTemplatesExist = function(path) {
-    const Page = this;
-    const pathList = generatePathsOnTree(path, []);
-    const regexpList = pathList.map(path => new RegExp(`${path}/_{1,2}template`));
-    let templateInfo = {
-      childrenTemplateExists: false,
-      decendantsTemplateExists: false,
-    };
-
-    return Page
-      .find({path: {$in: regexpList}})
-      .then(templates => {
-        templateInfo.childrenTemplateExists = (assignTemplateByType(templates, path, '_') ? true : false);
-        templateInfo.decendantsTemplateExists = (assignTemplateByType(templates, path, '__') ? true : false);
-
-        return templateInfo;
-      });
-  };
-
   /**
    * find all templates applicable to the new page
    */

+ 16 - 8
lib/routes/admin.js

@@ -130,15 +130,18 @@ module.exports = function(crowi, app) {
     }
   };
 
-  // app.post('/admin/markdown/XSSSetting' , admin.markdown.XSSSetting);
-  actions.markdown.XSSSetting = function(req, res) {
-    var XSSSetting = req.form.markdownSetting;
+  // app.post('/admin/markdown/xss-setting' , admin.markdown.xssSetting);
+  actions.markdown.xssSetting = function(req, res) {
+    let xssSetting = req.form.markdownSetting;
 
-    req.session.markdownSetting = XSSSetting;
+    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.updateNamespaceByArray('markdown', xssSetting, function(err, config) {
         Config.updateConfigCache('markdown', config);
-        req.session.XSSSetting = null;
+        req.session.xssSetting = null;
         req.flash('successMessage', ['Successfully updated!']);
         return res.redirect('/admin/markdown');
       });
@@ -149,6 +152,11 @@ module.exports = function(crowi, app) {
     }
   };
 
+  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) {
@@ -975,13 +983,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});
       }
     }

+ 3 - 4
lib/routes/index.js

@@ -70,15 +70,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); //change form name
-  app.post('/admin/markdown/XSSSetting'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.markdownXSS, admin.markdown.XSSSetting);
+  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);
@@ -176,7 +176,6 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
   app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
   app.post('/_api/pages.duplicate'    , accessTokenParser, loginRequired(crowi, app), csrf, page.api.duplicate);
-  app.get('/_api/pages.templates'   , accessTokenParser , loginRequired(crowi, app, false) , page.api.templates);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);

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

+ 0 - 21
lib/routes/page.js

@@ -281,9 +281,6 @@ module.exports = function(crowi, app) {
         .then(function(tree) {
           renderVars.tree = tree;
         })
-        .then(function() {
-          return Page.checkIfTemplatesExist(path);
-        })
         .then(() => {
           return PageGroupRelation.findByPage(renderVars.page);
         })
@@ -1189,23 +1186,5 @@ module.exports = function(crowi, app) {
     });
   };
 
-  /**
-   * @api {get} /pages.templates Check if templates exist for page
-   * @apiName FindTemplates
-   * @apiGroup Page
-   *
-   * @apiParam {String} path
-   */
-  api.templates = function(req, res) {
-    const pagePath = req.query.path;
-    const templateFinder = Page.checkIfTemplatesExist(pagePath);
-
-    templateFinder.then(function(templateInfo) {
-      return res.json(ApiResponse.success(templateInfo));
-    }).catch(function(err) {
-      return res.json(ApiResponse.error(err));
-    });
-  };
-
   return actions;
 };

+ 4 - 4
lib/service/passport.js

@@ -265,8 +265,8 @@ class PassportService {
 
     debug('GoogleStrategy: setting up..');
     passport.use(new GoogleStrategy({
-      clientId: config.crowi['security:passport-google:clientId'],
-      clientSecret: config.crowi['security:passport-google:clientSecret'],
+      clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
+      clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
       callbackURL: 'http://localhost:3000/passport/google/callback',  //change this
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -311,8 +311,8 @@ class PassportService {
 
     debug('GitHubStrategy: setting up..');
     passport.use(new GitHubStrategy({
-      clientID: config.crowi['security:passport-github:clientId'],
-      clientSecret: config.crowi['security:passport-github:clientSecret'],
+      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: 'http://localhost:3000/passport/github/callback',  //change this
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {

+ 18 - 0
lib/util/recommendedXssWhiteList.js

@@ -0,0 +1,18 @@
+/**
+ * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites
+ * added tags: h4, h5, h6, span, div
+ * 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',
+];
+
+const attrs = ['src', 'width', 'height', 'alt', 'title', 'href', 'class', 'style'];
+
+module.exports = {
+  'tags': tags,
+  'attrs': attrs,
+};

+ 20 - 7
lib/util/xss.js

@@ -1,20 +1,33 @@
 class Xss {
 
-  constructor(isAllowAllAttrs) {
+  constructor(crowi) {
     const xss = require('xss');
 
-    // create the option object
+    const config = crowi.config;
+    const isXssPrevented = config.isXssPrevented;
+    const tagWhiteList = config.tagWhiteList;
+    const attrWhiteList = config.attrWhiteList;
+
+    let whiteListContent = {};
+
+    // default
     let option = {
       stripIgnoreTag: true,
+      stripIgnoreTagBody: false,
       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}"`;
-      };
+
+    if (isXssPrevented) {
+      tagWhiteList.forEach(tag => {
+        whiteListContent[tag] = attrWhiteList;
+      });
     }
+    else {
+      option['stripIgnoreTag'] = false;
+    }
+
     // create the XSS Filter instance
     this.myxss = new xss.FilterXSS(option);
   }

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

+ 123 - 129
lib/views/admin/markdown.html

@@ -80,156 +80,150 @@
           </div>
         </div>
 
+        <div class="form-group my-3">
+          <div class="col-xs-offset-4 col-xs-5">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+            <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
+          </div>
+        </div>
+      </fieldset>
+      </form>
+
+      <form action="/admin/markdown/xss-setting" method="post" class="form-horizontal" id="markdownSettingForm" role="form">
+        <fieldset>
 
+          {% set nameForIsXssEnabled = "settingForm[security:xss-prevent:isEnabled]" %}
 
-        {% set nameForIsXssEnabled = "settingForm[security:xss-prevent:isEnabled]" %}
+          <legend>{{ t('markdown_setting.XSS_setting') }}</legend>
+          <p class="well">{{ t("markdown_setting.XSS_setting_desc") }}</p>
 
-        <legend>{{ t('markdown_setting.XSS_setting') }}</legend>
-        <p class="well">{{ t("markdown_setting.XSS_setting_desc") }}</p>
+          <label for="markdownSetting[markdown:isPreventXss]" class="col-xs-4 control-label">
+            {{ t('markdown_setting.Prevent XSS(Cross Site Scripting)') }}
+          </label>
 
-            <label for="markdownSetting[markdown:isPreventXss]" class="col-xs-4 control-label">
-              {{ t('markdown_setting.Prevent XSS(Cross Site Scripting)') }}
-            </label>
+          <div class="col-xs-5">
+            <div class="form-group">
+              <div class="col-xs-6">
+                <div class="btn-group btn-toggle" data-toggle="buttons">
+                  <label class="btn btn-default btn-rounded btn-outline" data-active-class="primary">
+                    <input name="{{nameForIsXssEnabled}}" value="true" type="radio"
+                        {% if true === isXssEnabled %}checked{% endif %}> ON
+                  </label>
+                  <label class="btn btn-default btn-rounded btn-outline {% if !isGoogleEnabled %}active{% endif %}" data-active-class="default">
+                    <input name="{{nameForIsXssEnabled}}" value="false" type="radio"
+                        {% if !isXssEnabled %}checked{% endif %}> OFF
+                  </label>
+                </div>
+              </div>
+            </div>
+
+            <fieldset id="xss-hide-when-disabled" {%if !isXssEnabled %}style="display: none;"{% endif %}>
+              {% set nameForIsXss2Enabled = "settingForm[security:xss2-prevent:isEnabled]" %}
+              <p class="help-block">{{ t("markdown_setting.Prevent XSS(Cross Site Scripting)desc") }}</p>
 
-            <div class="col-xs-5">
               <div class="form-group">
                 <div class="col-xs-6">
                   <div class="btn-group btn-toggle" data-toggle="buttons">
-                    <label class="btn btn-default btn-rounded btn-outline" data-active-class="primary">
-                      <input name="{{nameForIsXssEnabled}}" value="true" type="radio"
-                          {% if true === isXssEnabled %}checked{% endif %}> ON
-                    </label>
-                    <label class="btn btn-default btn-rounded btn-outline {% if !isGoogleEnabled %}active{% endif %}" data-active-class="default">
-                      <input name="{{nameForIsXssEnabled}}" value="false" type="radio"
-                          {% if !isXssEnabled %}checked{% endif %}> OFF
-                    </label>
+                    <div>
+                      <label data-active-class="primary">
+                        <input name="{{nameForIsXss2Enabled}}" value="1" type="radio"
+                        {% if !isXssEnabled %}checked{% endif %}>
+                        {{ t('markdown_setting.Ignore all tags') }}
+                      </label>
+                    </div>
+                    <div>
+                      <label data-active-class="primary">
+                          <input name="{{nameForIsXss2Enabled}}" value="2" type="radio"
+                          {% if true === isXssEnabled %}checked{% endif %}>
+                        {{ t('markdown_setting.Recommended setting') }}<br>
+                      </label>
+                    </div>
+                    <div>
+                      <label data-active-class="primary">
+                        <input name="{{nameForIsXss2Enabled}}" value="3" type="radio"
+                        {% if true === isXssEnabled %}checked{% endif %}>
+                        {{ t('markdown_setting.Custom Whitelist') }}
+                      </label>
+                    </div>
                   </div>
                 </div>
               </div>
 
-              <fieldset id="xss-hide-when-disabled" {%if !isXssEnabled %}style="display: none;"{% endif %}>
-                <p class="help-block">{{ t("markdown_setting.Prevent XSS(Cross Site Scripting)desc") }}</p>
-
-                {% set nameForIsXss2Enabled = "settingForm[security:xss2-prevent:isEnabled]" %}
-
+              <div id="xss2-hide-when-disabled" {%if !isXssEnabled %}style="display: none;" {% endif %}>
                 <div>
-                  <div class="form-group">
-                    <div class="col-xs-6">
-                      <div class="btn-group btn-toggle" data-toggle="buttons">
-                        <div>
-                          <label data-active-class="primary">
-                            <input name="{{nameForIsXss2Enabled}}" value="1" type="radio"
-                            {% if !isXssEnabled %}checked{% endif %}>
-                            {{ t('markdown_setting.Ignore all tags') }}
-                          </label>
-                        </div>
-                        <div>
-                          <label data-active-class="primary">
-                              <input name="{{nameForIsXss2Enabled}}" value="2" type="radio"
-                              {% if true === isXssEnabled %}checked{% endif %}>
-                            {{ t('markdown_setting.Recommended setting') }}<br>
-                          </label>
-                        </div>
-                        <div>
-                          <label data-active-class="primary">
-                            <input name="{{nameForIsXss2Enabled}}" value="3" type="radio"
-                            {% if true === isXssEnabled %}checked{% endif %}>
-                            {{ t('markdown_setting.Custom Whitelist') }}
-                          </label>
-                        </div>
-                      </div>
-                    </div>
+                  {{ t('markdown_setting.Tag names') }}
+                  <div>
+                    <textarea type="text" name="tag" rows="5" cols="40" readonly>span, iframe, input</textarea>
                   </div>
-
-                  <fieldset id="xss2-hide-when-disabled" {%if !isXssEnabled %}style="display: none;" {% endif %}>
-                    <form>
-                      <div>
-                        {{ t('markdown_setting.Tag names') }}
-                        <div>
-                          <textarea type="text" name="tag" rows="5" cols="40" readonly>span, iframe, input</textarea>
-                        </div>
-                      </div>
-                      <div>
-                        {{ t('markdown_setting.Tag attributes') }}
-                        <div>
-                          <textarea name="tagattribute" rows="5" cols="40" readonly>class, type, placeholder, name, required</textarea>
-                        </div>
-                      </div>
-                    </form>
-                  </fieldset>
-
-                  <fieldset id="xss3-hide-when-disabled" {%if !isXssEnabled %}style="display: none;" {% endif %}>
-                    <form>
-                      <div>
-                        {{ t('markdown_setting.Tag names') }}
-                        <div>
-                          <textarea type="text" name="tag" rows="5" cols="40" value="" placeholder="span, iframe, input"></textarea>
-                          <input type="button" class="btn btn-default" value="おすすめ設定をインポート" />
-                        </div>
-                      </div>
-                      <div>
-                        {{ t('markdown_setting.Tag attributes') }}
-                        <div>
-                          <textarea name="tagattribute" rows="5" cols="40" value="" placeholder="class, type, placeholder, name, required"></textarea>
-                          <input type="button" class="btn btn-default" value="おすすめ設定をインポート" />
-                        </div>
-                      </div>
-                    </form>
-                  </fieldset>
                 </div>
+                <div>
+                  {{ t('markdown_setting.Tag attributes') }}
+                  <div>
+                    <textarea name="tagattribute" rows="5" cols="40" readonly>class, type, placeholder, name, required</textarea>
+                  </div>
+                </div>
+              </div>
 
-              </fieldset>
-            </form>
-
-
-            <script>
-              $('input[name="settingForm[security:xss-prevent:isEnabled]"]').change(function() {
-                const isEnabled = ($(this).val() === "true");
-
-                if (isEnabled) {
-                  $('#xss-hide-when-disabled').show(400);
-                }
-                else {
-                  $('#xss-hide-when-disabled').hide(400);
-                }
-              });
-
-              $('input[name="settingForm[security:xss2-prevent:isEnabled]"]').change(function() {
-                const isEnabled = ($(this).val() === "1");
-                const isEnabled2 = ($(this).val() === "2");
-
-                if (isEnabled) {
-                  $('#xss2-hide-when-disabled').hide(400);
-                  $('#xss3-hide-when-disabled').hide(400);
-                }
-                else if (isEnabled2) {
-                  $('#xss2-hide-when-disabled').show(400);
-                  $('#xss3-hide-when-disabled').hide(400);
-                }
-                else {
-                  $('#xss3-hide-when-disabled').show(400);
-                  $('#xss2-hide-when-disabled').hide(400);
-                }
-              });
-            </script>
-
-
-
+              <div id="xss3-hide-when-disabled" {%if !isXssEnabled %}style="display: none;" {% endif %}>
+                <div>
+                  {{ t('markdown_setting.Tag names') }}
+                  <div>
+                    <textarea type="text" name="tag" rows="5" cols="40" value="" placeholder="span, iframe, input"></textarea>
+                    <input type="button" class="btn btn-default" value="おすすめ設定をインポート" />
+                  </div>
+                </div>
+                <div>
+                  {{ t('markdown_setting.Tag attributes') }}
+                  <div>
+                    <textarea name="tagattribute" rows="5" cols="40" value="" placeholder="class, type, placeholder, name, required"></textarea>
+                    <input type="button" class="btn btn-default" value="おすすめ設定をインポート" />
+                  </div>
+                </div>
+              </div>
+            </fieldset>
+          </div>
 
-        <div class="form-group my-3">
-          <div class="col-xs-offset-4 col-xs-5">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" class="btn btn-primary">{{ t("Update") }}</button>
+          <script>
+            $('input[name="settingForm[security:xss-prevent:isEnabled]"]').change(function() {
+              const isEnabled = ($(this).val() === "true");
+
+              if (isEnabled) {
+                $('#xss-hide-when-disabled').show(400);
+              }
+              else {
+                $('#xss-hide-when-disabled').hide(400);
+              }
+            });
+
+            $('input[name="settingForm[security:xss2-prevent:isEnabled]"]').change(function() {
+              const isEnabled = ($(this).val() === "1");
+              const isEnabled2 = ($(this).val() === "2");
+
+              if (isEnabled) {
+                $('#xss2-hide-when-disabled').hide(400);
+                $('#xss3-hide-when-disabled').hide(400);
+              }
+              else if (isEnabled2) {
+                $('#xss2-hide-when-disabled').show(400);
+                $('#xss3-hide-when-disabled').hide(400);
+              }
+              else {
+                $('#xss3-hide-when-disabled').show(400);
+                $('#xss2-hide-when-disabled').hide(400);
+              }
+            });
+          </script>
+
+          <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>
-        </div>
 
-      </fieldset>
+        </fieldset>
       </form>
 
-
-
-
-
     </div>
   </div>
 

+ 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 %}

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

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

+ 10 - 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,6 +38,11 @@
       <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">

+ 10 - 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,6 +38,11 @@
       <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">

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

@@ -57,7 +57,7 @@
               <div class="create-page-button-container">
                 <a id="link-to-template" href="{{ page.path || path }}" class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b disabled">
                   <i class="icon-fw icon-doc"></i>
-                  <span id="create-template-button-link">{{ t('Create') }}/{{ t('Edit') }}</span>
+                  <span id="create-template-button-link">{{ t('Edit') }}</span>
                 </a>
               </div>
             </div>
@@ -69,34 +69,3 @@
     </div><!-- /.modal-content -->
   </div><!-- /.modal-dialog -->
 </div><!-- /.modal -->
-<script>
-  let buttonTextChildren;
-  let buttonTextDecendants;
-  let pagePath = $("#link-to-template").attr("href");
-
-  if (pagePath.endsWith("/")) {
-      pagePath = pagePath.slice(0, -1);
-  };
-
-  $.get('/_api/pages.templates?path=' + pagePath)   // don't use template literal(`...${}`) for IE11
-    .then(function(templateInfo) {                  // don't use arrow function for IE11
-      buttonTextChildren = templateInfo.childrenTemplateExists ? '{{ t("Edit") }}' : '{{ t("Create") }}';
-      buttonTextDecendants = templateInfo.decendantsTemplateExists ? '{{ t("Edit") }}' : '{{ t("Create") }}';
-    });
-
-  $("#template-type").on("change", function() {
-    // enable button
-    $('#link-to-template').removeClass("disabled");
-
-    if ($("#template-type").val() === "children") {
-      href = pagePath + "/_template#edit-form";
-      $("#link-to-template").attr("href", href);
-      $('#create-template-button-link').text(buttonTextChildren);
-    }
-    else if ($("#template-type").val() === "decentants") {
-      href = pagePath + "/__template#edit-form";
-      $("#link-to-template").attr("href", href);
-      $('#create-template-button-link').text(buttonTextDecendants);
-    };
-  });
-</script>

+ 3 - 1
lib/views/modal/create_template.html

@@ -20,6 +20,7 @@
                 <div class="panel-footer text-center">
                   <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}_template#edit-form"
                       class="btn btn-sm btn-primary" id="template-button-children">
+                      {{ t("Edit") }}
                   </a>
                 </div>
               </div>
@@ -32,8 +33,9 @@
                   <p class="help-block text-center"><small>{{ t('template.decendants.desc') }}</small></p>
                 </div>
                 <div class="panel-footer text-center">
-                  <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}__template#edit-form"
+                  <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path }}/{% endif %}__template#edit-form"
                       class="btn btn-sm btn-primary" id="template-button-decendants">
+                      {{ t("Edit") }}
                   </a>
                 </div>
               </div>

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

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.1.8-RC",
+  "version": "3.1.10-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -135,7 +135,7 @@
     "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",
     "file-loader": "^1.1.0",
     "i18next-browser-languagedetector": "^2.2.0",

+ 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()) },

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

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

+ 1 - 2
resource/js/util/PreProcessor/XssFilter.js

@@ -3,8 +3,7 @@ import Xss from '../../../../lib/util/xss';
 export default class XssFilter {
 
   constructor(crowi) {
-    // TODO read options
-    this.xss = new Xss(true);
+    this.xss = new Xss(crowi);
   }
 
   process(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;
+    }
   }
 }

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

+ 134 - 91
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"
@@ -631,7 +632,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:
@@ -2045,14 +2046,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"
@@ -2806,6 +2799,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"
@@ -2862,59 +2865,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"
@@ -2928,9 +2938,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"
 
@@ -3115,9 +3125,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"
@@ -3166,6 +3176,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"
@@ -3566,9 +3580,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"
@@ -4051,21 +4065,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"
@@ -4333,7 +4346,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"
 
@@ -4449,7 +4462,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:
@@ -4487,6 +4500,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"
@@ -6031,6 +6048,10 @@ parseurl@~1.3.1, parseurl@~1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
 
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
 passport-github@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/passport-github/-/passport-github-1.1.0.tgz#8ce1e3fcd61ad7578eb1df595839e4aea12355d4"
@@ -6044,10 +6065,6 @@ passport-google-auth@^1.0.2:
     googleapis "^16.0.0"
     passport-strategy "1.x"
 
-pascalcase@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-
 passport-ldapauth@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/passport-ldapauth/-/passport-ldapauth-2.0.0.tgz#42dff004417185d0a4d9f776a3eed8d4731fd689"
@@ -6635,6 +6652,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"
@@ -6992,7 +7013,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"
 
@@ -7313,20 +7340,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"
@@ -7895,6 +7918,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"
@@ -7959,7 +7992,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"
 
@@ -8029,12 +8062,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"
@@ -8081,7 +8118,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"
 
@@ -8383,6 +8420,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"