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

Merge branch 'imprv/growi_renderer-in-reveal' into imprv/add-reveal-growi_renderer-plugin

Yuto Iwata пре 7 година
родитељ
комит
bfbe7a58c0
39 измењених фајлова са 421 додато и 268 уклоњено
  1. 16 0
      CHANGES.md
  2. 5 3
      README.md
  3. 8 8
      package.json
  4. 6 5
      resource/locales/en-US/translation.json
  5. 6 4
      resource/locales/ja/translation.json
  6. 1 1
      src/client/styles/scss/_admin.scss
  7. 1 5
      src/server/crowi/express-init.js
  8. 1 1
      src/server/events/user.js
  9. 0 1
      src/server/form/admin/app.js
  10. 8 0
      src/server/form/admin/siteUrl.js
  11. 1 0
      src/server/form/index.js
  12. 1 1
      src/server/models/config.js
  13. 49 18
      src/server/models/page.js
  14. 1 1
      src/server/models/user.js
  15. 44 36
      src/server/routes/admin.js
  16. 1 2
      src/server/routes/attachment.js
  17. 1 7
      src/server/routes/hackmd.js
  18. 7 6
      src/server/routes/index.js
  19. 4 4
      src/server/routes/login.js
  20. 2 2
      src/server/routes/me.js
  21. 1 4
      src/server/routes/page.js
  22. 6 0
      src/server/service/config-loader.js
  23. 19 0
      src/server/service/config-manager.js
  24. 1 1
      src/server/service/file-uploader/gridfs.js
  25. 9 10
      src/server/service/passport.js
  26. 4 3
      src/server/util/googleAuth.js
  27. 6 7
      src/server/util/search.js
  28. 12 15
      src/server/util/slack.js
  29. 78 14
      src/server/views/admin/app.html
  30. 4 2
      src/server/views/admin/user-group-detail.html
  31. 2 2
      src/server/views/admin/widget/passport/github.html
  32. 2 2
      src/server/views/admin/widget/passport/google-oauth.html
  33. 5 5
      src/server/views/admin/widget/passport/saml.html
  34. 2 2
      src/server/views/admin/widget/passport/twitter.html
  35. 8 1
      src/server/views/layout/layout.html
  36. 1 1
      src/server/views/modal/duplicate.html
  37. 1 1
      src/server/views/modal/rename.html
  38. 25 7
      src/test/models/page.test.js
  39. 72 86
      yarn.lock

+ 16 - 0
CHANGES.md

@@ -3,9 +3,25 @@ CHANGES
 
 ## 3.3.5-RC
 
+* Improvement: Site URL settings must be set
+* Improvement: Site URL settings can be set with environment variable
+* Fix: "Anyone with the link" ACL doesn't work correctly
+    * Introduced 3.3.0
+* Fix: Related pages list of /admin/user-group-detail/xxx doesn't show anything
+    * Introduced 3.3.0
+* Fix: Diff of revision contents doesn't appeared when notifing with slack
 * Fix: NPE occured on /admin/security when Crowi Classic Auth Mechanism is set
+* Fix: Coudn't render Timing Diagram with PlantUML
 * I18n: Cheatsheet for editor
 * I18n: Some admin pages
+* Support: Upgrade libs
+    * diff
+    * markdown-it-plantuml
+    * mongoose
+    * nodemailer
+    * mongoose-gridfs
+    * sinon
+    * sinon-chai
 
 ## 3.3.4
 

+ 5 - 3
README.md

@@ -42,8 +42,9 @@ Features
 * **Features**
   * Create hierarchical pages with markdown
   * Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
-  * Support Authentication with LDAP / Active Directory
-  * Slack Incoming Webhooks Integration
+  * Support Authentication with LDAP / Active Directory, OAuth
+  * SSO(Single Sign On) with SAML
+  * Slack/Mattermost, IFTTT Integration
   * [Miscellaneous features](https://github.com/weseek/growi/wiki/Additional-Features)
 * **[Docker Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
@@ -58,7 +59,7 @@ Using Heroku
 ------------
 
 1. Go to https://heroku.com/deploy
-1. (Optional) Input INSTALL_PLUGINS to install plugins
+2. (Optional) Input INSTALL_PLUGINS to install plugins
 
 Using docker-compose
 ---------------------
@@ -175,6 +176,7 @@ Environment Variables
     * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
     * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.
 * **Option (Overwritable in admin page)**
+    * APP_SITE_URL: Site URL. e.g. `https://example.com`, `https://example.com:8080`
     * 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.

+ 8 - 8
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.3.5-RC",
+  "version": "3.3.6-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -71,7 +71,7 @@
     "cookie-parser": "^1.4.3",
     "cross-env": "^5.0.5",
     "csrf": "~3.0.3",
-    "diff": "^3.5.0",
+    "diff": "^4.0.1",
     "elasticsearch": "^15.0.0",
     "entities": "^1.1.1",
     "env-cmd": "^8.0.1",
@@ -96,12 +96,12 @@
     "migrate-mongo": "^5.0.1",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
-    "mongoose": "^5.3.1",
-    "mongoose-gridfs": "^0.5.0",
+    "mongoose": "^5.4.4",
+    "mongoose-gridfs": "^0.6.2",
     "mongoose-paginate": "^5.0.3",
     "mongoose-unique-validator": "^2.0.2",
     "multer": "~1.4.0",
-    "nodemailer": "^4.0.1",
+    "nodemailer": "^5.1.1",
     "nodemailer-ses-transport": "~1.5.0",
     "npm-run-all": "^4.1.2",
     "passport": "^0.4.0",
@@ -165,7 +165,7 @@
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.0.0",
+    "markdown-it-plantuml": "^1.3.0",
     "markdown-it-task-checkbox": "^1.0.6",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
@@ -196,8 +196,8 @@
     "reveal.js": "^3.5.0",
     "sass-loader": "^7.1.0",
     "simple-load-script": "^1.0.2",
-    "sinon": "^7.0.0",
-    "sinon-chai": "^3.2.0",
+    "sinon": "^7.2.2",
+    "sinon-chai": "^3.3.0",
     "socket.io-client": "^2.0.3",
     "stream-to-promise": "^2.2.0",
     "style-loader": "^0.23.0",

+ 6 - 5
resource/locales/en-US/translation.json

@@ -321,7 +321,8 @@
     "Site Name": "Site name",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
-    "Site URL": "Site URL",
+    "Site URL desc": "This is for the site URL setting.",
+    "Site URL warn": "Some features don't work because the site URL is not set.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "Confidential name": "Confidential name",
     "Default Language for new users": "Default Language for new users",
@@ -340,7 +341,7 @@
     "Port": "Port",
     "User": "User",
     "AWS settings": "AWS settings",
-    "AWS_access": "This is for AWS settings. If you complete AWS settings, file upload function, progile picture function etc will be enabled.",
+    "AWS_access": "This is for AWS settings. If you complete AWS settings, file upload function, profile picture function etc will be enabled.",
     "No_SMTP_setting": "If you do not have SMTP settings, e-mails will be sent via SES. You need to verify from e-mail address and production settings.",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
     "region": "Region",
@@ -349,8 +350,8 @@
     "Enable plugin loading": "Enable plugin loading",
     "Load plugins": "Load plugins",
     "Enable": "Enable",
-    "Disable": "Disable"
-
+    "Disable": "Disable",
+    "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>%s</code> is used."
   },
   "security_setting": {
 		"Basic authentication": "Basic authentication",
@@ -379,7 +380,7 @@
     "auth_mechanism": "authentication mechanism",
     "recommended": "Recommended",
     "username_email_password": "Username, Email and Password authentication",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Define it from %s",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
     "ldap_auth": "LDAP authentication",
     "saml_auth": "SAML authentication",
     "google_auth2": "Google OAuth authentication",

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

@@ -88,6 +88,7 @@
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
+  "Site URL settings": "サイトURL設定",
   "Markdown settings": "マークダウン設定",
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
@@ -334,8 +335,9 @@
     "Site Name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
-    "Site URL": "サイトURL",
-    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL。",
+    "Site URL desc": "サイトURLを設定します。",
+    "Site URL warn": "サイトURLが設定されていないため、一部機能が動作しない状態になっています。",
+    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
     "Confidential name": "コンフィデンシャル表示",
     "Default Language for new users": "新規ユーザーのデフォルト設定言語",
     "ex): internal use only": "例: 社外秘",
@@ -362,8 +364,8 @@
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Load plugins": "プラグインを読み込む",
     "Enable": "有効",
-    "Disable": "無効"
-
+    "Disable": "無効",
+    "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>%s</code> の値を利用します"
    },
 
   "security_setting": {

+ 1 - 1
src/client/styles/scss/_admin.scss

@@ -126,7 +126,7 @@
     }
   }
 
-  .authentication-settings-table {
+  .settings-table {
     table-layout: fixed;
 
     .item-name {

+ 1 - 5
src/server/crowi/express-init.js

@@ -66,12 +66,8 @@ module.exports = function(crowi, app) {
     req.config = config;
     req.csrfToken = null;
 
-    config.crowi['app:siteUrl:fixed'] = (config.crowi['app:siteUrl'] != null)
-      ? config.crowi['app:siteUrl']                                                                         // prioritized with v3.2.4 and above
-      : (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');   // auto generate (default with v3.2.3 and below)
-
     res.locals.req      = req;
-    res.locals.baseUrl  = config.crowi['app:siteUrl:fixed'];
+    res.locals.baseUrl  = crowi.configManager.getSiteUrl();
     res.locals.config   = config;
     res.locals.env      = env;
     res.locals.now      = now;

+ 1 - 1
src/server/events/user.js

@@ -14,7 +14,7 @@ UserEvent.prototype.onActivated = async function(user) {
 
   const userPagePath = Page.getUserPagePath(user);
 
-  const page = await Page.findByPathAndViewer(userPagePath, user);
+  const page = await Page.findByPath(userPagePath, user);
 
   if (page == null) {
     const body = `# ${user.username}\nThis is ${user.username}'s page`;

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

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

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

@@ -0,0 +1,8 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[app:siteUrl]').trim().isUrl()
+);

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

@@ -12,6 +12,7 @@ module.exports = {
   },
   admin: {
     app: require('./admin/app'),
+    siteUrl: require('./admin/siteUrl'),
     mail: require('./admin/mail'),
     aws: require('./admin/aws'),
     importerEsa: require('./admin/importerEsa'),

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

@@ -604,7 +604,7 @@ module.exports = function(crowi) {
     const local_config = {
       crowi: {
         title: Config.appTitle(crowi),
-        url: config.crowi['app:siteUrl:fixed'] || '',
+        url: crowi.configManager.getSiteUrl(),
       },
       upload: {
         image: Config.isUploadable(config),

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

@@ -62,6 +62,26 @@ const pageSchema = new mongoose.Schema({
 pageSchema.plugin(uniqueValidator);
 
 
+/**
+ * return an array of ancestors paths that is extracted from specified pagePath
+ * e.g.
+ *  when `pagePath` is `/foo/bar/baz`,
+ *  this method returns [`/foo/bar/baz`, `/foo/bar`, `/foo`, `/`]
+ *
+ * @param {string} pagePath
+ * @return {string[]} ancestors paths
+ */
+const extractToAncestorsPaths = (pagePath) => {
+  const ancestorsPaths = [];
+
+  let parentPath;
+  while (parentPath !== '/') {
+    parentPath = nodePath.dirname(parentPath || pagePath);
+    ancestorsPaths.push(parentPath);
+  }
+
+  return ancestorsPaths;
+};
 
 const addSlashOfEnd = (path) => {
   let returnPath = path;
@@ -168,22 +188,24 @@ class PageQueryBuilder {
     return this;
   }
 
-  addConditionToFilteringByViewer(user, userGroups, showPagesRestrictedByOwner, showPagesRestrictedByGroup) {
+  addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup) {
     const grantConditions = [
       {grant: null},
       {grant: GRANT_PUBLIC},
     ];
 
+    if (showAnyoneKnowsLink) {
+      grantConditions.push({grant: GRANT_RESTRICTED});
+    }
+
     if (showPagesRestrictedByOwner) {
       grantConditions.push(
-        {grant: GRANT_RESTRICTED},
         {grant: GRANT_SPECIFIED},
         {grant: GRANT_OWNER},
       );
     }
     else if (user != null) {
       grantConditions.push(
-        {grant: GRANT_RESTRICTED, grantedUsers: user._id},
         {grant: GRANT_SPECIFIED, grantedUsers: user._id},
         {grant: GRANT_OWNER, grantedUsers: user._id},
       );
@@ -549,7 +571,7 @@ module.exports = function(crowi) {
     }
 
     const queryBuilder = new PageQueryBuilder(baseQuery);
-    queryBuilder.addConditionToFilteringByViewer(user, userGroups);
+    queryBuilder.addConditionToFilteringByViewer(user, userGroups, true);
 
     const count = await queryBuilder.query.exec();
     return count > 0;
@@ -571,7 +593,7 @@ module.exports = function(crowi) {
     }
 
     const queryBuilder = new PageQueryBuilder(baseQuery);
-    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
+    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
 
     return await queryBuilder.query.exec();
   };
@@ -604,7 +626,7 @@ module.exports = function(crowi) {
     }
 
     const queryBuilder = new PageQueryBuilder(baseQuery);
-    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
+    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
 
     return await queryBuilder.query.exec();
   };
@@ -623,7 +645,10 @@ module.exports = function(crowi) {
       return null;
     }
 
-    const parentPath = nodePath.dirname(path);
+    const ancestorsPaths = extractToAncestorsPaths(path);
+
+    // pick the longest one
+    const baseQuery = this.findOne({path: { $in: ancestorsPaths }}).sort({path: -1});
 
     let relatedUserGroups = userGroups;
     if (user != null && relatedUserGroups == null) {
@@ -632,11 +657,10 @@ module.exports = function(crowi) {
       relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
-    const page = await this.findByPathAndViewer(parentPath, user, relatedUserGroups);
+    const queryBuilder = new PageQueryBuilder(baseQuery);
+    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
 
-    return (page != null)
-      ? page
-      : this.findAncestorByPathAndViewer(parentPath, user, relatedUserGroups);
+    return await queryBuilder.query.exec();
   };
 
   pageSchema.statics.findByRedirectTo = function(path) {
@@ -650,7 +674,7 @@ module.exports = function(crowi) {
     const builder = new PageQueryBuilder(this.find());
     builder.addConditionToListWithDescendants(path, option);
 
-    return await findListFromBuilderAndViewer(builder, user, option);
+    return await findListFromBuilderAndViewer(builder, user, false, option);
   };
 
   /**
@@ -660,7 +684,7 @@ module.exports = function(crowi) {
     const builder = new PageQueryBuilder(this.find());
     builder.addConditionToListByStartWith(path, option);
 
-    return await findListFromBuilderAndViewer(builder, user, option);
+    return await findListFromBuilderAndViewer(builder, user, false, option);
   };
 
   /**
@@ -674,7 +698,12 @@ module.exports = function(crowi) {
     const opt = Object.assign({sort: 'createdAt', desc: -1}, option);
     const builder = new PageQueryBuilder(this.find({ creator: targetUser._id }));
 
-    return await findListFromBuilderAndViewer(builder, currentUser, opt);
+    let showAnyoneKnowsLink = null;
+    if (targetUser != null && currentUser != null) {
+      showAnyoneKnowsLink = targetUser._id.equals(currentUser._id);
+    }
+
+    return await findListFromBuilderAndViewer(builder, currentUser, showAnyoneKnowsLink, opt);
   };
 
   pageSchema.statics.findListByPageIds = async function(ids, option) {
@@ -700,9 +729,10 @@ module.exports = function(crowi) {
    * find pages by PageQueryBuilder
    * @param {PageQueryBuilder} builder
    * @param {User} user
+   * @param {boolean} showAnyoneKnowsLink
    * @param {any} option
    */
-  async function findListFromBuilderAndViewer(builder, user, option) {
+  async function findListFromBuilderAndViewer(builder, user, showAnyoneKnowsLink, option) {
     validateCrowi();
 
     const User = crowi.model('User');
@@ -721,7 +751,7 @@ module.exports = function(crowi) {
     }
 
     // add grant conditions
-    await addConditionToFilteringByViewerForList(builder, user);
+    await addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink);
 
     builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
 
@@ -740,8 +770,9 @@ module.exports = function(crowi) {
    *
    * @param {PageQueryBuilder} builder
    * @param {User} user
+   * @param {boolean} showAnyoneKnowsLink
    */
-  async function addConditionToFilteringByViewerForList(builder, user) {
+  async function addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink) {
     validateCrowi();
 
     const Config = crowi.model('Config');
@@ -758,7 +789,7 @@ module.exports = function(crowi) {
       userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
-    return builder.addConditionToFilteringByViewer(user, userGroups, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
+    return builder.addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
   }
 
   /**

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

@@ -707,7 +707,7 @@ module.exports = function(crowi) {
                 vars: {
                   email: user.email,
                   password: user.password,
-                  url: config.crowi['app:siteUrl:fixed'],
+                  url: crowi.configManager.getSiteUrl(),
                   appTitle: Config.appTitle(config),
                 }
               },

+ 44 - 36
src/server/routes/admin.js

@@ -692,48 +692,37 @@ module.exports = function(crowi, app) {
   };
 
   // グループ詳細
-  actions.userGroup.detail = function(req, res) {
+  actions.userGroup.detail = async function(req, res) {
     const userGroupId = req.params.id;
     const renderVar = {
       userGroup: null,
       userGroupRelations: [],
-      pageGroupRelations: [],
-      notRelatedusers: []
+      notRelatedusers: [],
+      relatedPages: [],
     };
-    let targetUserGroup = null;
-    UserGroup.findOne({ _id: userGroupId})
-      .then(function(userGroup) {
-        targetUserGroup = userGroup;
-        if (targetUserGroup == null) {
-          req.flash('errorMessage', 'グループがありません');
-          throw new Error('no userGroup is exists. ', name);
-        }
-        else {
-          renderVar.userGroup = targetUserGroup;
-
-          return Promise.all([
-            // get all user and group relations
-            UserGroupRelation.findAllRelationForUserGroup(targetUserGroup),
-            // get all page and group relations
-            PageGroupRelation.findAllRelationForUserGroup(targetUserGroup),
-            // get all not related users for group
-            UserGroupRelation.findUserByNotRelatedGroup(targetUserGroup),
-          ]);
-        }
-      })
-      .then((resolves) => {
-        renderVar.userGroupRelations = resolves[0];
-        renderVar.pageGroupRelations = resolves[1];
-        renderVar.notRelatedusers = resolves[2];
-        debug('notRelatedusers', renderVar.notRelatedusers);
 
-        return res.render('admin/user-group-detail', renderVar);
-      })
-      .catch((err) => {
-        req.flash('errorMessage', 'ユーザグループの検索に失敗しました');
-        debug('Error on get userGroupDetail', err);
-        return res.redirect('/admin/user-groups');
-      });
+    const userGroup = await UserGroup.findOne({ _id: userGroupId});
+
+    if (userGroup == null) {
+      logger.error('no userGroup is exists. ', userGroupId);
+      req.flash('errorMessage', 'グループがありません');
+      return res.redirect('/admin/user-groups');
+    }
+    renderVar.userGroup = userGroup;
+
+    const resolves = await Promise.all([
+      // get all user and group relations
+      UserGroupRelation.findAllRelationForUserGroup(userGroup),
+      // get all not related users for group
+      UserGroupRelation.findUserByNotRelatedGroup(userGroup),
+      // get all related pages
+      Page.find({grant: Page.GRANT_USER_GROUP, grantedGroup: { $in: [userGroup] }}),
+    ]);
+    renderVar.userGroupRelations = resolves[0];
+    renderVar.notRelatedusers = resolves[1];
+    renderVar.relatedPages = resolves[2];
+
+    return res.render('admin/user-group-detail', renderVar);
   };
 
   //グループの生成
@@ -1015,6 +1004,25 @@ module.exports = function(crowi, app) {
     }
   };
 
+  actions.api.asyncAppSetting = async(req, res) => {
+    const form = req.form.settingForm;
+
+    if (!req.form.isValid) {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+
+    debug('form content', form);
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', form);
+      return res.json({status: true});
+    }
+    catch (err) {
+      logger.error(err);
+      return res.json({status: false});
+    }
+  };
+
   actions.api.securitySetting = function(req, res) {
     const form = req.form.settingForm;
     const config = crowi.getConfig();

+ 1 - 2
src/server/routes/attachment.js

@@ -92,8 +92,7 @@ module.exports = function(crowi, app) {
       //   1. this is buggy (doesn't work on Win)
       //   2. ensure backward compatibility of data
 
-      // var config = crowi.getConfig();
-      // var baseUrl = (config.crowi['app:siteUrl:fixed'] || '');
+      // var baseUrl = crowi.configManager.getSiteUrl();
       return res.json(ApiResponse.success({
         attachments: attachments.map(at => {
           const fileUrl = at.fileUrl;

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

@@ -39,13 +39,7 @@ module.exports = function(crowi, app) {
       agentScriptContentTpl = swig.compileFile(agentScriptPath);
     }
 
-    let origin = `${req.protocol}://${req.get('host')}`;
-
-    // use config.crowi['app:siteUrl:fixed'] when exist req.headers['x-forwarded-proto'].
-    // refs: lib/crowi/express-init.js
-    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
-      origin = config.crowi['app:siteUrl:fixed'];
-    }
+    const origin = crowi.configManager.getSiteUrl();
 
     // generate definitions to replace
     const definitions = {

+ 7 - 6
src/server/routes/index.js

@@ -54,12 +54,13 @@ module.exports = function(crowi, app) {
   app.get('/login/google'            , login.loginGoogle);
   app.get('/logout'                  , logout.logout);
 
-  app.get('/admin'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
-  app.get('/admin/app'                  , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
-  app.post('/_api/admin/settings/app'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
-  app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
-  app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
-  app.post('/_api/admin/settings/plugin', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.plugin, admin.api.appSetting);
+  app.get('/admin'                          , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
+  app.get('/admin/app'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
+  app.post('/_api/admin/settings/app'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
+  app.post('/_api/admin/settings/siteUrl'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.siteUrl, admin.api.asyncAppSetting);
+  app.post('/_api/admin/settings/mail'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
+  app.post('/_api/admin/settings/aws'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
+  app.post('/_api/admin/settings/plugin'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.plugin, admin.api.appSetting);
 
   // security admin
   app.get('/admin/security'                     , loginRequired(crowi, app) , middleware.adminRequired() , admin.security.index);

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

@@ -106,7 +106,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.loginGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     var code = req.session.googleAuthCode || null;
 
     if (!code) {
@@ -140,7 +140,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.register = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
 
     // ログイン済みならさようなら
     if (req.user) {
@@ -212,7 +212,7 @@ module.exports = function(crowi, app) {
                       vars: {
                         createdUser: userData,
                         adminUser: adminUser,
-                        url: config.crowi['app:siteUrl:fixed'],
+                        url: crowi.configManager.getSiteUrl(),
                         appTitle: appTitle,
                       }
                     },
@@ -321,7 +321,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.registerGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     googleAuth.createAuthUrl(req, function(err, redirectUrl) {
       if (err) {
         // TODO

+ 2 - 2
src/server/routes/me.js

@@ -384,7 +384,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.authGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
 
     var userData = req.user;
 
@@ -413,7 +413,7 @@ module.exports = function(crowi, app) {
   };
 
   actions.authGoogleCallback = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     var userData = req.user;
 
     googleAuth.handleCallback(req, function(err, tokenInfo) {

+ 1 - 4
src/server/routes/page.js

@@ -120,7 +120,6 @@ module.exports = function(crowi, app) {
     if (userData != null) {
       renderVars.pageUser = userData;
       renderVars.bookmarkList = await Bookmark.findByUser(userData, {limit: 10, populatePage: true, requestUser: requestUser});
-      renderVars.createdList = await Page.findListByCreator(userData, {limit: 10}, requestUser);
     }
   }
 
@@ -622,9 +621,9 @@ module.exports = function(crowi, app) {
       options.grantUserGroupId = grantUserGroupId;
     }
 
-    try {
       const Revision = crowi.model('Revision');
       const previousRevision = await Revision.findById(revisionId);
+    try {
       page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
     }
     catch (err) {
@@ -651,8 +650,6 @@ module.exports = function(crowi, app) {
 
     // user notification
     if (isSlackEnabled && slackChannels != null) {
-      const Revision = crowi.model('Revision');
-      const previousRevision = await Revision.findById(page.revision);
       await notifyToSlackByUser(page, req.user, slackChannels, 'update', previousRevision);
     }
   };

+ 6 - 0
src/server/service/config-loader.js

@@ -110,6 +110,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   //   type:    ,
   //   default:
   // },
+  APP_SITE_URL: {
+    ns:      'crowi',
+    key:     'app:siteUrl',
+    type:    TYPES.STRING,
+    default: null
+  },
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',

+ 19 - 0
src/server/service/config-manager.js

@@ -68,6 +68,25 @@ class ConfigManager {
     return this.searchOnlyFromEnvVarConfigs(namespace, key);
   }
 
+  /**
+   * get the site url
+   *
+   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
+   *
+   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
+   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
+   * With version 3.3.5 and above, the system use only a value from the config.
+   */
+  getSiteUrl() {
+    const siteUrl = this.getConfig('crowi', 'app:siteUrl');
+    if (siteUrl != null) {
+      return siteUrl;
+    }
+    else {
+      return '[The site URL is not set. Please set it!]';
+    }
+  }
+
   /**
    * update configs in the same namespace
    *

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

@@ -97,7 +97,7 @@ module.exports = function(crowi) {
 
   lib.getFileData = async function(filePath) {
     const file = await getFile(filePath);
-    const id = file.id;
+    const id = file._id;
     const contentType = file.contentType;
     const data = await readFileData(id);
     return {

+ 9 - 10
src/server/service/passport.js

@@ -311,8 +311,8 @@ class PassportService {
     passport.use(new GoogleStrategy({
       clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
       clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/google/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/google/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -358,8 +358,8 @@ class PassportService {
     passport.use(new GitHubStrategy({
       clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
       clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/github/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/github/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -405,8 +405,8 @@ class PassportService {
     passport.use(new TwitterStrategy({
       consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
       consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/twitter/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/twitter/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,   // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -451,10 +451,9 @@ class PassportService {
     debug('SamlStrategy: setting up..');
     passport.use(new SamlStrategy({
       entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
-      callbackUrl:
-        (config.crowi['app:siteUrl'] != null)
-          ? `${config.crowi['app:siteUrl']}/passport/saml/callback`                 // auto-generated with v3.2.4 and above
-          : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'),    // DEPRECATED: backward compatible with v3.2.3 and below
+      callbackUrl: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/saml/callback`          // auto-generated with v3.2.4 and above
+        : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'),    // DEPRECATED: backward compatible with v3.2.3 and below
       issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
       cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),
     }, function(profile, done) {

+ 4 - 3
src/server/util/googleAuth.js

@@ -2,12 +2,13 @@
  * googleAuth utility
  */
 
-module.exports = function(config) {
+module.exports = function(crowi) {
   'use strict';
 
   const { GoogleApis } = require('googleapis');
   var google = new GoogleApis()
     , debug = require('debug')('growi:lib:googleAuth')
+    , config = crowi.getConfig()
     , lib = {}
     ;
 
@@ -20,7 +21,7 @@ module.exports = function(config) {
   }
 
   lib.createAuthUrl = function(req, callback) {
-    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
+    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
 
@@ -33,7 +34,7 @@ module.exports = function(config) {
   };
 
   lib.handleCallback = function(req, callback) {
-    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
+    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
 

+ 6 - 7
src/server/util/search.js

@@ -541,23 +541,22 @@ SearchClient.prototype.filterPagesByViewer = async function(query, user, userGro
 
   const grantConditions = [
     { term: { grant: GRANT_PUBLIC } },
+    { bool: {
+      must: [
+        { term: { grant: GRANT_RESTRICTED } },
+        { term: { granted_users: user._id.toString() } }
+      ]
+    } },
   ];
 
   if (showPagesRestrictedByOwner) {
     grantConditions.push(
-      { term: { grant: GRANT_RESTRICTED } },
       { term: { grant: GRANT_SPECIFIED } },
       { term: { grant: GRANT_OWNER } },
     );
   }
   else if (user != null) {
     grantConditions.push(
-      { bool: {
-        must: [
-          { term: { grant: GRANT_RESTRICTED } },
-          { term: { granted_users: user._id.toString() } }
-        ]
-      } },
       { bool: {
         must: [
           { term: { grant: GRANT_SPECIFIED } },

+ 12 - 15
src/server/util/slack.js

@@ -44,11 +44,8 @@ module.exports = function(crowi) {
     });
   };
 
-  const convertMarkdownToMrkdwn = function(body) {
-    var url = '';
-    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
-      url = config.crowi['app:siteUrl:fixed'];
-    }
+  const convertMarkdownToMarkdown = function(body) {
+    const url = crowi.configManager.getSiteUrl();
 
     body = body
       .replace(/\n\*\s(.+)/g, '\n• $1')
@@ -61,22 +58,22 @@ module.exports = function(crowi) {
   };
 
   const prepareAttachmentTextForCreate = function(page, user) {
-    var body = page.revision.body;
+    let body = page.revision.body;
     if (body.length > 2000) {
       body = body.substr(0, 2000) + '...';
     }
 
-    return convertMarkdownToMrkdwn(body);
+    return convertMarkdownToMarkdown(body);
   };
 
   const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
-    var diff = require('diff');
-    var diffText = '';
+    const diff = require('diff');
+    let diffText = '';
 
     diff.diffLines(previousRevision.body, page.revision.body).forEach(function(line) {
       debug('diff line', line);
       /* eslint-disable no-unused-vars */
-      var value = line.value.replace(/\r\n|\r/g, '\n');
+      const value = line.value.replace(/\r\n|\r/g, '\n');
       /* eslint-enable */
       if (line.added) {
         diffText += `${line.value} ... :lower_left_fountain_pen:`;
@@ -105,7 +102,7 @@ module.exports = function(crowi) {
     }
 
     if (comment.isMarkdown) {
-      return convertMarkdownToMrkdwn(body);
+      return convertMarkdownToMarkdown(body);
     }
     else {
       return body;
@@ -113,7 +110,7 @@ module.exports = function(crowi) {
   };
 
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     let body = page.revision.body;
 
     if (updateType == 'create') {
@@ -148,7 +145,7 @@ module.exports = function(crowi) {
   };
 
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     const body = prepareAttachmentTextForComment(comment);
 
     const attachment = {
@@ -175,7 +172,7 @@ module.exports = function(crowi) {
 
   const getSlackMessageTextForPage = function(path, user, updateType) {
     let text;
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
 
     const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
@@ -189,7 +186,7 @@ module.exports = function(crowi) {
   };
 
   const getSlackMessageTextForComment = function(path, user) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     const pageUrl = `<${url}${path}|${path}>`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 

+ 78 - 14
src/server/views/admin/app.html

@@ -48,19 +48,6 @@
           </div>
         </div>
 
-        <div class="form-group">
-          <label for="settingForm[app:siteUrl]" class="col-xs-3 control-label">{{ t('app_setting.Site URL') }}</label>
-          <div class="col-xs-6">
-            <input class="form-control"
-                   id="settingForm[app:siteUrl]"
-                   type="text"
-                   name="settingForm[app:siteUrl]"
-                   value="{{ settingForm['app:siteUrl'] | default('') }}"
-                   placeholder="{{ t('eg') }} https://my.growi.org">
-            <p class="help-block">{{ t("app_setting.siteurl_help") }}</p>
-          </div>
-        </div>
-
         <div class="form-group">
           <label for="settingForm[app:confidential]" class="col-xs-3 control-label">{{ t('app_setting.Confidential name') }}</label>
           <div class="col-xs-6">
@@ -127,6 +114,57 @@
       </fieldset>
       </form>
 
+      <form action="/_api/admin/settings/siteUrl" method="post" class="form-horizontal" id="siteUrlSettingForm" role="form">
+        <fieldset>
+          <legend>{{ t('Site URL settings') }}</legend>
+          <p class="well">{{ t('app_setting.Site URL desc') }}</p>
+          {% if !getConfig('crowi', 'app:siteUrl') %}
+            <p class="alert alert-danger"><i class="icon-exclamation"></i> {{ t('app_setting.Site URL warn') }}</p>
+          {% endif %}
+
+          <div class="col-xs-offset-3">
+            <table class="table settings-table">
+              <colgroup>
+                <col class="from-db">
+                <col class="from-env-vars">
+              </colgroup>
+              <thead>
+              <tr><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td>
+                    <input class="form-control"
+                           type="text"
+                           name="settingForm[app:siteUrl]"
+                           value="{{ getConfigFromDB('crowi', 'app:siteUrl') | default('') }}"
+                           placeholder="e.g. https://my.growi.org">
+                    <p class="help-block">{{ t("app_setting.siteurl_help") }}</p>
+                  </td>
+                  <td>
+                    <input class="form-control"
+                           type="text"
+                           value="{{ getConfigFromEnvVars('crowi', 'app:siteUrl') | default('') }}"
+                           readonly>
+                    <p class="help-block">
+                      {{ t("app_setting.Use env var if empty", "APP_SITE_URL") }}
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+          <div class="form-group">
+            <div class="col-xs-offset-3 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t('app_setting.Update') }}</button>
+            </div>
+          </div>
+        </fieldset>
+      </form>
+
+
       <form action="/_api/admin/settings/mail" method="post" class="form-horizontal" id="mailSettingForm" role="form">
       <fieldset>
       <legend>{{ t('app_setting.Mail settings') }}</legend>
@@ -298,7 +336,7 @@
   </div>
 
   <script>
-    $('#appSettingForm, #mailSettingForm, #awsSettingForm, #pluginSettingForm').each(function() {
+    $('#appSettingForm, #siteUrlSettingForm, #mailSettingForm, #awsSettingForm, #pluginSettingForm').each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {
@@ -346,6 +384,32 @@
       });
     });
 
+    /**
+     * The following script sets the class name 'unused' to the cell in from-env-vars column
+     * when the value of the corresponding cell from the database is not empty.
+     * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
+     *
+     * TODO The following script is duplicated from saml.html. It is desirable to integrate those in the future.
+     */
+    $('.settings-table tbody tr').each(function(_, element) {
+      const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
+      const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
+
+      // initialize
+      addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars);
+
+      // set keyup event handler
+      inputElemFromDB.keyup(function () { addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) });
+    });
+
+    function addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) {
+      if (inputElemFromDB.val() === '') {
+        inputElemFromEnvVars.parent().removeClass('unused');
+      }
+      else {
+        inputElemFromEnvVars.parent().addClass('unused');
+      }
+    };
   </script>
 
 </div>

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

@@ -234,8 +234,10 @@
 
       <legend class="m-t-20">ページ一覧</legend>
 
-      {% if pageGroupRelations.length == 0 %}<p>グループが閲覧権限を保有するページはありません</p>{% endif %}
-      {% include '../widget/page_list.html' with { pages: pageGroupRelations, pagePropertyName: 'targetPage' } %}
+      <div class="page-list">
+        {% if relatedPages.length == 0 %}<p>グループが閲覧権限を保有するページはありません</p>{% endif %}
+        {% include '../widget/page_list.html' with { pages: relatedPages } %}
+      </div>
 
     </div>
   </div>

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

@@ -4,7 +4,7 @@
 
   {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
   {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/github/callback' %}
 
   <div class="form-group">
@@ -53,7 +53,7 @@
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>

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

@@ -4,7 +4,7 @@
 
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/google/callback' %}
 
   <div class="form-group">
@@ -53,7 +53,7 @@
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>

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

@@ -5,7 +5,7 @@
   {% set nameForIsSamlEnabled = "settingForm[security:passport-saml:isEnabled]" %}
   {% set isSamlEnabled  = getConfig('crowi', 'security:passport-saml:isEnabled') %}
   {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions') %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/saml/callback' %}
 
   {% if useOnlyEnvVars %}
@@ -44,7 +44,7 @@
              value="{{ callbackUrl }}"
              readonly>
       <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'SAML Identity') }}</p>
-      {% if !settingForm['app:siteUrl'] %}
+      {% if !getConfig('crowi', 'app:siteUrl') %}
       <div class="alert alert-danger">
         <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
       </div>
@@ -67,7 +67,7 @@
     {% endif %}
 
     <h4>Basic Settings</h4>
-    <table class="table authentication-settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
       <colgroup>
         <col class="item-name">
         <col class="from-db">
@@ -164,7 +164,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
 
     <h4>Attribute Mapping</h4>
 
-    <table class="table authentication-settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
       <colgroup>
         <col class="item-name">
         <col class="from-db">
@@ -384,7 +384,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
    * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
    * This behavior is disabled when the system is in the use-only-env-vars mode.
    */
-  $('.authentication-settings-table:not(.use-only-env-vars) tbody tr').each(function(_, element) {
+  $('.settings-table:not(.use-only-env-vars) tbody tr').each(function(_, element) {
     const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
     const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
 

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

@@ -4,7 +4,7 @@
 
   {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
   {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/twitter/callback' %}
 
   <div class="form-group">
@@ -55,7 +55,7 @@
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>

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

@@ -117,7 +117,7 @@
 <div id="wrapper">
   <!-- Navigation -->
   {% block layout_head_nav %}
-  <nav class="navbar navbar-default navbar-static-top m-b-0">
+  <nav class="navbar navbar-default navbar-static-top mb-0">
     <div class="navbar-header">
       <a class="navbar-toggle hidden-sm hidden-md hidden-lg " href="javascript:void(0)" data-toggle="collapse" data-target=".navbar-collapse">
         <i class="ti-menu"></i>
@@ -195,6 +195,13 @@
   {% include '../modal/create_page.html' %}
   {% endblock  %} {# layout_head_nav #}
 
+  {% if !getConfig('crowi', 'app:siteUrl') %}
+  <div class="alert alert-danger mb-0">
+    <i class="icon-exclamation"></i>
+    {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+  </div>
+  {% endif %}
+
   {% block sidebar %}
   <!-- Left navbar-header -->
   <div class="navbar-default sidebar hidden-print" role="navigation">

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

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

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

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

+ 25 - 7
src/test/models/page.test.js

@@ -76,7 +76,7 @@ describe('Page', () => {
         path: '/grant/specified',
         grant: Page.GRANT_SPECIFIED,
         grantedUsers: [testUser0],
-        creator: testUser0
+        creator: testUser0,
       },
       {
         path: '/grant/owner',
@@ -92,7 +92,7 @@ describe('Page', () => {
       },
       {
         path: '/grant/groupacl',
-        grant: 5,
+        grant: Page.GRANT_USER_GROUP,
         grantedUsers: [],
         grantedGroup: testGroup0,
         creator: testUser1,
@@ -233,7 +233,7 @@ describe('Page', () => {
     context('with a restricted page and an user who has no grant', () => {
       it('should return false', async() => {
         const user = await User.findOne({email: 'anonymous1@example.com'});
-        const page = await Page.findOne({path: '/grant/restricted'});
+        const page = await Page.findOne({path: '/grant/owner'});
 
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         expect(bool).to.be.equal(false);
@@ -283,8 +283,8 @@ describe('Page', () => {
 
   describe('.findPage', () => {
     context('findByIdAndViewer', () => {
-      it('should find page', async() => {
-        const pageToFind = createdPages[0];
+      it('should find page (public)', async() => {
+        const pageToFind = createdPages[1];
         const grantedUser = createdUsers[0];
 
         const page = await Page.findByIdAndViewer(pageToFind._id, grantedUser);
@@ -292,8 +292,26 @@ describe('Page', () => {
         expect(page.path).to.equal(pageToFind.path);
       });
 
-      it('should not be found by grant', async() => {
-        const pageToFind = createdPages[0];
+      it('should find page (anyone knows link)', async() => {
+        const pageToFind = createdPages[2];
+        const grantedUser = createdUsers[1];
+
+        const page = await Page.findByIdAndViewer(pageToFind._id, grantedUser);
+        expect(page).to.be.not.null;
+        expect(page.path).to.equal(pageToFind.path);
+      });
+
+      it('should find page (just me)', async() => {
+        const pageToFind = createdPages[4];
+        const grantedUser = createdUsers[0];
+
+        const page = await Page.findByIdAndViewer(pageToFind._id, grantedUser);
+        expect(page).to.be.not.null;
+        expect(page.path).to.equal(pageToFind.path);
+      });
+
+      it('should not be found by grant (just me)', async() => {
+        const pageToFind = createdPages[4];
         const grantedUser = createdUsers[1];
 
         const page = await Page.findByIdAndViewer(pageToFind._id, grantedUser);

+ 72 - 86
yarn.lock

@@ -31,33 +31,34 @@
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/@handsontable/react/-/react-2.0.0.tgz#30d9c2bd05421588a6ed1b3050b1f7dc476b35d3"
 
-"@lykmapipo/gridfs-stream@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@lykmapipo/gridfs-stream/-/gridfs-stream-1.2.0.tgz#0f74826816b4f7414ae36862d67ce4849a224d91"
-  dependencies:
-    flushwritable "^1.0.0"
-
 "@sinonjs/commons@^1.0.2":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.0.2.tgz#3e0ac737781627b8844257fadc3d803997d0526e"
   dependencies:
     type-detect "4.0.8"
 
-"@sinonjs/formatio@3.0.0", "@sinonjs/formatio@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.0.0.tgz#9d282d81030a03a03fa0c5ce31fd8786a4da311a"
+"@sinonjs/commons@^1.2.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.3.0.tgz#50a2754016b6f30a994ceda6d9a0a8c36adda849"
+  integrity sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==
   dependencies:
-    "@sinonjs/samsam" "2.1.0"
+    type-detect "4.0.8"
 
-"@sinonjs/samsam@2.1.0":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.0.tgz#b8b8f5b819605bd63601a6ede459156880f38ea3"
+"@sinonjs/formatio@^3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.1.0.tgz#6ac9d1eb1821984d84c4996726e45d1646d8cce5"
+  integrity sha512-ZAR2bPHOl4Xg6eklUGpsdiIJ4+J1SNag1DHHrG/73Uz/nVwXqjgUtRPLoS+aVyieN9cSbc0E4LsU984tWcDyNg==
   dependencies:
-    array-from "^2.1.1"
+    "@sinonjs/samsam" "^2 || ^3"
 
-"@sinonjs/samsam@^2.1.2":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.2.tgz#16947fce5f57258d01f1688fdc32723093c55d3f"
+"@sinonjs/samsam@^2 || ^3", "@sinonjs/samsam@^3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.0.2.tgz#304fb33bd5585a0b2df8a4c801fcb47fa84d8e43"
+  integrity sha512-m08g4CS3J6lwRQk1pj1EO+KEVWbrbXsmi9Pw0ySmrIbcVxVaedoFgLvFsV8wHLwh01EpROVz3KvVcD1Jmks9FQ==
+  dependencies:
+    "@sinonjs/commons" "^1.0.2"
+    array-from "^2.1.1"
+    lodash.get "^4.4.2"
 
 "@types/body-parser@*":
   version "1.16.8"
@@ -1570,7 +1571,7 @@ bs-recipes@1.3.4:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.3.4.tgz#0d2d4d48a718c8c044769fdc4f89592dc8b69585"
 
-bson@^1.1.0:
+bson@^1.1.0, bson@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0"
 
@@ -1578,10 +1579,6 @@ bson@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c"
 
-bson@~1.0.5:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.9.tgz#12319f8323b1254739b7c6bef8d3e89ae05a2f57"
-
 buffer-equal-constant-time@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@@ -2762,6 +2759,11 @@ diff@^3.3.1:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
 
+diff@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff"
+  integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==
+
 diffie-hellman@^5.0.0:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -3661,10 +3663,6 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.1"
     readable-stream "^2.0.4"
 
-flushwritable@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/flushwritable/-/flushwritable-1.0.0.tgz#3e328d8fde412ad47e738e3be750b4d290043498"
-
 fn-args@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/fn-args/-/fn-args-3.0.0.tgz#df5c3805ed41ec3b38a72aabe390cf9493ec084c"
@@ -5068,9 +5066,10 @@ jsx-ast-utils@^2.0.1:
   dependencies:
     array-includes "^3.0.3"
 
-just-extend@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288"
+just-extend@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
+  integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==
 
 jwa@^1.1.4:
   version "1.1.5"
@@ -5335,7 +5334,7 @@ lodash.foreach@^4.1.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
 
-lodash.get@4.4.2, lodash.get@^4.0, lodash.get@^4.0.2, lodash.get@^4.4.2:
+lodash.get@^4.0, lodash.get@^4.0.2, lodash.get@^4.4.2:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
 
@@ -5395,7 +5394,7 @@ lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
-lodash@4.17.11, lodash@^4.17.11:
+lodash@4.17.11, lodash@>=4.17.11, lodash@^4.17.11:
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
 
@@ -5522,9 +5521,10 @@ markdown-it-named-headers@^0.0.4:
   dependencies:
     string "^3.0.1"
 
-markdown-it-plantuml@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-1.0.0.tgz#7b6a351a1d9275705c09626b02d873301e5899c2"
+markdown-it-plantuml@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-1.3.0.tgz#a4eb7bcdc718f6b3b889840d974e5f9184a90102"
+  integrity sha512-BW4/dzaIFX3D3dMnifmYko2o2+gajkkt1VQpEuicC6E7sCtKgjOAI4qpYmJKH70guGH0qoK0sNMHXJfi3noTbQ==
 
 markdown-it-task-checkbox@^1.0.6:
   version "1.0.6"
@@ -5877,16 +5877,6 @@ mongodb-core@2.1.19:
     bson "~1.0.4"
     require_optional "~1.0.0"
 
-mongodb-core@3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.1.5.tgz#59ca67d7f6cea570d5437624a7afec8d752d477a"
-  dependencies:
-    bson "^1.1.0"
-    require_optional "^1.0.1"
-    safe-buffer "^5.1.2"
-  optionalDependencies:
-    saslprep "^1.0.0"
-
 mongodb-core@3.1.9:
   version "3.1.9"
   resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.1.9.tgz#c31ee407bf932b0149eaed775c17ee09974e4ca3"
@@ -5904,13 +5894,6 @@ mongodb@3.1.10:
     mongodb-core "3.1.9"
     safe-buffer "^5.1.2"
 
-mongodb@3.1.6:
-  version "3.1.6"
-  resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.1.6.tgz#6054641973b5bf5b5ae1c67dcbcf8fa88280273d"
-  dependencies:
-    mongodb-core "3.1.5"
-    safe-buffer "^5.1.2"
-
 mongodb@^2.0.36:
   version "2.2.35"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.35.tgz#cd1b5af8a9463e3f9a787fa5b3d05565579730f9"
@@ -5919,13 +5902,13 @@ mongodb@^2.0.36:
     mongodb-core "2.1.19"
     readable-stream "2.2.7"
 
-mongoose-gridfs@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/mongoose-gridfs/-/mongoose-gridfs-0.5.0.tgz#626e12ab605c2ed2a205a5953cd5aa8615f44feb"
+mongoose-gridfs@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/mongoose-gridfs/-/mongoose-gridfs-0.6.2.tgz#b144219af01c51b14c58e5cdf7f58293d2ce1d67"
+  integrity sha512-fW6+D1Pn+qtpuYK3lYl1OMd3wenNIJ0/KhpOAoiRZWdzHjABiUQrtZSCyLEjmVgsxuTqIsUqvS1KeMUvUmtO2A==
   dependencies:
-    "@lykmapipo/gridfs-stream" "^1.2.0"
-    lodash "^4.17.10"
-    stream-read "^1.1.2"
+    lodash ">=4.17.11"
+    stream-read ">=1.1.2"
 
 mongoose-legacy-pluralize@1.0.2:
   version "1.0.2"
@@ -5944,16 +5927,16 @@ mongoose-unique-validator@^2.0.2:
     lodash.foreach "^4.1.0"
     lodash.get "^4.0.2"
 
-mongoose@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.3.1.tgz#52d5bfb67788a2194e5f7a2a5c0d597e4b86fd7a"
+mongoose@^5.4.4:
+  version "5.4.4"
+  resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.4.4.tgz#923923cd39a03b970c269f828322990ea163f9a8"
+  integrity sha512-KoYUFtgrXQ9Sxuf5IEE0Y2LswWGhHiN27bxtObANxMgXbuNLTtLB3xaep8jITUDitG4kkI+FKElUJH4uY8G2Bw==
   dependencies:
     async "2.6.1"
-    bson "~1.0.5"
+    bson "~1.1.0"
     kareem "2.3.0"
-    lodash.get "4.4.2"
-    mongodb "3.1.6"
-    mongodb-core "3.1.5"
+    mongodb "3.1.10"
+    mongodb-core "3.1.9"
     mongoose-legacy-pluralize "1.0.2"
     mpath "0.5.1"
     mquery "3.2.0"
@@ -6095,12 +6078,13 @@ nice-try@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
 
-nise@^1.4.5:
-  version "1.4.6"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.6.tgz#76cc3915925056ae6c405dd8ad5d12bde570c19f"
+nise@^1.4.7:
+  version "1.4.8"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.8.tgz#ce91c31e86cf9b2c4cac49d7fcd7f56779bfd6b0"
+  integrity sha512-kGASVhuL4tlAV0tvA34yJYZIVihrUt/5bDwpp4tTluigxUr2bBlJeDXmivb6NuEdFkqvdv/Ybb9dm16PSKUhtw==
   dependencies:
-    "@sinonjs/formatio" "3.0.0"
-    just-extend "^3.0.0"
+    "@sinonjs/formatio" "^3.1.0"
+    just-extend "^4.0.2"
     lolex "^2.3.2"
     path-to-regexp "^1.7.0"
     text-encoding "^0.6.4"
@@ -6270,9 +6254,10 @@ nodemailer-ses-transport@~1.5.0:
   dependencies:
     aws-sdk "^2.2.36"
 
-nodemailer@^4.0.1:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.4.1.tgz#ce480eb3db7b949b3366e301b8f0af1c1248025e"
+nodemailer@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.1.1.tgz#0c48d1ecab02e86d9ff6c620ee75ed944b763505"
+  integrity sha512-hKGCoeNdFL2W7S76J/Oucbw0/qRlfG815tENdhzcqTpSjKgAN91mFOqU2lQUflRRxFM7iZvCyaFcAR9noc/CqQ==
 
 nopt@1.0.10:
   version "1.0.10"
@@ -8419,23 +8404,23 @@ simple-swizzle@^0.2.2:
   dependencies:
     is-arrayish "^0.3.1"
 
-sinon-chai@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.2.0.tgz#ed995e13a8a3cfccec18f218d9b767edc47e0715"
+sinon-chai@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.3.0.tgz#8084ff99451064910fbe2c2cb8ab540c00b740ea"
+  integrity sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==
 
-sinon@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.0.0.tgz#99f2e5198d90a01ccbcebd4dc181a24827cb90dd"
+sinon@^7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.2.2.tgz#388ecabd42fa93c592bfc71d35a70894d5a0ca07"
+  integrity sha512-WLagdMHiEsrRmee3jr6IIDntOF4kbI6N2pfbi8wkv50qaUQcBglkzkjtoOEbeJ2vf1EsrHhLI+5Ny8//WHdMoA==
   dependencies:
-    "@sinonjs/commons" "^1.0.2"
-    "@sinonjs/formatio" "^3.0.0"
-    "@sinonjs/samsam" "^2.1.2"
+    "@sinonjs/commons" "^1.2.0"
+    "@sinonjs/formatio" "^3.1.0"
+    "@sinonjs/samsam" "^3.0.2"
     diff "^3.5.0"
-    lodash.get "^4.4.2"
     lolex "^3.0.0"
-    nise "^1.4.5"
+    nise "^1.4.7"
     supports-color "^5.5.0"
-    type-detect "^4.0.8"
 
 slack-node@^0.1.8:
   version "0.1.8"
@@ -8696,9 +8681,10 @@ stream-http@^2.7.2:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
-stream-read@^1.1.2:
+stream-read@>=1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/stream-read/-/stream-read-1.1.2.tgz#3137110d7aa80ba54e4b829c4cd33ca106b9564d"
+  integrity sha1-MTcRDXqoC6VOS4KcTNM8oQa5Vk0=
   dependencies:
     dezalgo "^1.0.1"
 
@@ -9135,7 +9121,7 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-detect@4.0.8, type-detect@^4.0.8:
+type-detect@4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"