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

Merge pull request #656 from weseek/master

release v3.2.5
Yuki Takei 7 лет назад
Родитель
Сommit
59dc5d5a8d

+ 17 - 1
CHANGES.md

@@ -1,12 +1,28 @@
 CHANGES
 ========
 
-## 3.2.4-RC
+## 3.2.5-RC
+
+* Feature: Add select alignment buttons of Spreadsheet like GUI (Handsontable)
+* Improvement: Expandable Spreadsheet like GUI (Handsontable)
+* Improvement: Move/Resize rows/columns of Spreadsheet like GUI (Handsontable)
+* Improvement: Prevent XSS of New Page modal
+* Fix: Recent Created tab of user home shows wrong page list
+    * Introduced by 3.2.4
+* Support: Upgrade libs
+    * metismenu
+    * sinon
+
+## 3.2.4
 
 * Feature: Edit table with Spreadsheet like GUI (Handsontable)
 * Feature: Paging recent created in users home
 * Improvement: Specify certificate for SAML Authentication
 * Fix: SAML Authentication didn't work
+    * Introduced by 3.2.2
+* Fix: Failed to create new page with title which includes RegEx special characters
+* Fix: Preventing XSS Settings are not applied in default
+    * Introduced by 3.1.12
 * Support: Mongoose migration mechanism
 * Support: Upgrade libs
     * googleapis

+ 1 - 0
config/env.dev.js

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

+ 5 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.2.4-RC",
+  "version": "3.2.5-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -123,7 +123,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.0.16",
-    "@handsontable/react": "^1.1.0",
+    "@handsontable/react": "^2.0.0",
     "autoprefixer": "^9.0.0",
     "babel-core": "^6.25.0",
     "babel-loader": "^7.1.1",
@@ -152,7 +152,7 @@
     "eslint-plugin-react": "^7.7.0",
     "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^2.0.0",
-    "handsontable": "^5.0.1",
+    "handsontable": "^6.0.1",
     "i18next-browser-languagedetector": "^2.2.0",
     "imports-loader": "^0.8.0",
     "jquery-slimscroll": "^1.3.8",
@@ -170,7 +170,7 @@
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
-    "metismenu": "^2.7.4",
+    "metismenu": "^3.0.3",
     "mocha": "^5.2.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
@@ -195,7 +195,7 @@
     "reveal.js": "^3.5.0",
     "sass-loader": "^7.1.0",
     "simple-load-script": "^1.0.2",
-    "sinon": "^6.0.0",
+    "sinon": "^7.0.0",
     "sinon-chai": "^3.2.0",
     "socket.io-client": "^2.0.3",
     "style-loader": "^0.23.0",

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

@@ -298,6 +298,7 @@
 		"Selecting authentication mechanism": "Selecting authentication mechanism",
 		"common_authentication": "If you set the basic authentication, common authentication is applied on the whole page.",
 		"without_encryption": "Please be noted that your ID and Password will be sent wihtout encryption.",
+		"basic_acl_disable": "Because of Public Wiki  setting, basic authentication can not be used.",
 		"users_without_account": "Users without account is not accessible",
     "example": "Example",
     "restrict_emails": "You can restrict registerable e-mail address.",

+ 1 - 0
resource/locales/ja/translation.json

@@ -317,6 +317,7 @@
     "Selecting authentication mechanism": "認証機構選択",
     "common_authentication": "Basic認証を設定すると、ページ全体に共通の認証がかかります。",
     "without_encryption": "IDとパスワードは暗号化されずに送信されるのでご注意下さい。",
+    "basic_acl_disable": "Public Wiki の設定のため、Basic認証は利用できません。",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",

+ 111 - 12
src/client/js/components/PageEditor/HandsontableModal.jsx

@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
 
 import Modal from 'react-bootstrap/es/Modal';
 import Button from 'react-bootstrap/es/Button';
+import Navbar from 'react-bootstrap/es/Navbar';
+import ButtonGroup from 'react-bootstrap/es/ButtonGroup';
+
+import { debounce } from 'throttle-debounce';
 
 import Handsontable from 'handsontable';
 import { HotTable } from '@handsontable/react';
@@ -10,21 +14,32 @@ import { HotTable } from '@handsontable/react';
 import MarkdownTable from '../../models/MarkdownTable';
 import HandsontableUtil from './HandsontableUtil';
 
+const DEFAULT_HOT_HEIGHT = 300;
+
 export default class HandsontableModal extends React.Component {
+
+
   constructor(props) {
     super(props);
 
     this.state = {
       show: false,
+      isWindowExpanded: false,
       markdownTableOnInit: HandsontableModal.getDefaultMarkdownTable(),
       markdownTable: HandsontableModal.getDefaultMarkdownTable(),
-      handsontableSetting: HandsontableModal.getDefaultHandsotableSetting()
+      handsontableHeight: DEFAULT_HOT_HEIGHT,
+      handsontableSetting: HandsontableModal.getDefaultHandsontableSetting()
     };
 
     this.init = this.init.bind(this);
     this.reset = this.reset.bind(this);
     this.cancel = this.cancel.bind(this);
     this.save = this.save.bind(this);
+    this.expandWindow = this.expandWindow.bind(this);
+    this.contractWindow = this.contractWindow.bind(this);
+
+    // create debounced method for expanding HotTable
+    this.expandHotTableHeightWithDebounce = debounce(100, this.expandHotTableHeight);
   }
 
   init(markdownTable) {
@@ -34,8 +49,14 @@ export default class HandsontableModal extends React.Component {
         markdownTableOnInit: initMarkdownTable,
         markdownTable: initMarkdownTable.clone(),
         handsontableSetting: Object.assign({}, this.state.handsontableSetting, {
-          afterUpdateSettings: HandsontableUtil.createHandlerToSynchronizeHandontableAlignWith(initMarkdownTable.options.align),
-          loadData: HandsontableUtil.createHandlerToSynchronizeHandontableAlignWith(initMarkdownTable.options.align)
+          /*
+           * The afterUpdateSettings hook is called when this component state changes.
+           *
+           * In detail, when this component state changes, React will re-render HotTable because it is passed some state values of this component.
+           * HotTable#shouldComponentUpdate is called in this process and it call the updateSettings method for the Handsontable instance.
+           * After updateSetting is executed, Handsontable calls a AfterUpdateSetting hook.
+           */
+          afterUpdateSettings: HandsontableUtil.createHandlerToSynchronizeHandontableAlignWith(initMarkdownTable.options.align)
         })
       }
     );
@@ -65,15 +86,84 @@ export default class HandsontableModal extends React.Component {
     this.setState({ show: false });
   }
 
+  setClassNameToColumns(className) {
+    const selectedRange = this.refs.hotTable.hotInstance.getSelectedRange();
+    if (selectedRange == null) return;
+
+    let startCol;
+    let endCol;
+
+    if (selectedRange[0].from.col < selectedRange[0].to.col) {
+      startCol = selectedRange[0].from.col;
+      endCol = selectedRange[0].to.col;
+    }
+    else {
+      startCol = selectedRange[0].to.col;
+      endCol = selectedRange[0].from.col;
+    }
+
+    HandsontableUtil.setClassNameToColumns(this.refs.hotTable.hotInstance, startCol, endCol, className);
+  }
+
+  expandWindow() {
+    this.setState({ isWindowExpanded: true });
+
+    // invoke updateHotTableHeight method with delay
+    // cz. Resizing this.refs.hotTableContainer is completeted after a little delay after 'isWindowExpanded' set with 'true'
+    this.expandHotTableHeightWithDebounce();
+  }
+
+  contractWindow() {
+    this.setState({ isWindowExpanded: false, handsontableHeight: DEFAULT_HOT_HEIGHT });
+  }
+
+  /**
+   * Expand the height of the Handsontable
+   *  by updating 'handsontableHeight' state
+   *  according to the height of this.refs.hotTableContainer
+   */
+  expandHotTableHeight() {
+    if (this.state.isWindowExpanded && this.refs.hotTableContainer != null) {
+      const height = this.refs.hotTableContainer.getBoundingClientRect().height;
+      this.setState({ handsontableHeight: height });
+    }
+  }
+
+  renderExpandOrContractButton() {
+    const iconClassName = this.state.isWindowExpanded ? 'icon-size-actual' : 'icon-size-fullscreen';
+    return (
+      <button className="close mr-3" onClick={this.state.isWindowExpanded ? this.contractWindow : this.expandWindow}>
+        <i className={iconClassName} style={{ fontSize: '0.8em' }} aria-hidden="true"></i>
+      </button>
+    );
+  }
+
   render() {
+    const dialogClassNames = ['handsontable-modal'];
+    if (this.state.isWindowExpanded) {
+      dialogClassNames.push('handsontable-modal-expanded');
+    }
+
+    const dialogClassName = dialogClassNames.join(' ');
+
     return (
-      <Modal show={this.state.show} onHide={this.cancel} bsSize="large">
+      <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName}>
         <Modal.Header closeButton>
+          { this.renderExpandOrContractButton() }
           <Modal.Title>Edit Table</Modal.Title>
         </Modal.Header>
-        <Modal.Body className="p-0">
-          <div className="p-4">
-            <HotTable ref='hotTable' data={this.state.markdownTable.table} settings={this.state.handsontableSetting} />
+        <Modal.Body className="p-0 d-flex flex-column">
+          <Navbar>
+            <Navbar.Form>
+              <ButtonGroup>
+                <Button onClick={() => { this.setClassNameToColumns('htLeft') }}><i className="ti-align-left"></i></Button>
+                <Button onClick={() => { this.setClassNameToColumns('htCenter') }}><i className="ti-align-center"></i></Button>
+                <Button onClick={() => { this.setClassNameToColumns('htRight') }}><i className="ti-align-right"></i></Button>
+              </ButtonGroup>
+            </Navbar.Form>
+          </Navbar>
+          <div ref="hotTableContainer" className="m-4 hot-table-container">
+            <HotTable ref='hotTable' data={this.state.markdownTable.table} settings={this.state.handsontableSetting} height={this.state.handsontableHeight} />
           </div>
         </Modal.Body>
         <Modal.Footer>
@@ -102,11 +192,21 @@ export default class HandsontableModal extends React.Component {
     );
   }
 
-  static getDefaultHandsotableSetting() {
+  static getDefaultHandsontableSetting() {
     return {
-      height: 300,
       rowHeaders: true,
       colHeaders: true,
+      manualRowMove: true,
+      manualRowResize: true,
+      manualColumnMove: true,
+      manualColumnResize: true,
+      selectionMode: 'multiple',
+      outsideClickDeselects: false,
+
+      modifyColWidth: function(width) {
+        return Math.max(80, Math.min(400, width));
+      },
+
       contextMenu: {
         items: {
           'row_above': {}, 'row_below': {}, 'col_left': {}, 'col_right': {},
@@ -137,9 +237,8 @@ export default class HandsontableModal extends React.Component {
             }
           }
         }
-      },
-      stretchH: 'all',
-      selectionMode: 'multiple'
+      }
+
     };
   }
 }

+ 16 - 11
src/client/js/components/SavePageControls.jsx

@@ -42,6 +42,8 @@ class SavePageControls extends React.PureComponent {
   render() {
     const { t } = this.props;
 
+    const config = this.props.crowi.getConfig();
+    const isAclEnabled = config.isAclEnabled;
     const label = this.state.pageId == null ? t('Create') : t('Update');
 
     return (
@@ -56,17 +58,20 @@ class SavePageControls extends React.PureComponent {
               slackChannels={this.props.slackChannels} />
         </div>
 
-        <div className="mr-2">
-          <GrantSelector crowi={this.props.crowi}
-              ref={(elem) => {
-                if (this.refs.grantSelector == null) {
-                  this.refs.grantSelector = elem.getWrappedInstance();
-                }
-              }}
-              grant={this.props.grant}
-              grantGroupId={this.props.grantGroupId}
-              grantGroupName={this.props.grantGroupName} />
-        </div>
+
+        {isAclEnabled &&
+          <div className="mr-2">
+            <GrantSelector crowi={this.props.crowi}
+                ref={(elem) => {
+                  if (this.refs.grantSelector == null) {
+                    this.refs.grantSelector = elem.getWrappedInstance();
+                  }
+                }}
+                grant={this.props.grant}
+                grantGroupId={this.props.grantGroupId}
+                grantGroupName={this.props.grantGroupName} />
+          </div>
+        }
 
         <button className="btn btn-primary btn-submit" onClick={this.submit}>{label}</button>
       </div>

+ 38 - 0
src/client/styles/scss/_handsontable.scss

@@ -0,0 +1,38 @@
+.handsontable {
+  .handsontableInput {
+    max-width: 290px !important;
+  }
+
+  td {
+    word-break: break-all;
+  }
+}
+
+// expanded window layout
+.handsontable-modal.handsontable-modal-expanded {
+  // full-screen modal
+  width: 97%;
+  height: 95%;
+  .modal-content {
+    height: 95%;
+  }
+
+  // expand .modal-body (with calculating height)
+  .modal-body {
+    $modal-header: 54px;
+    $modal-footer: 46px;
+    $margin: $modal-header + $modal-footer;
+    height: calc(100% - #{$margin});
+
+    // expand .hot-table-container (with flexbox)
+    .hot-table-container {
+      flex: 1;
+    }
+  }
+}
+
+// Prevent handsontable/handsontable #2937 (Manual column resize does not work when handsontable is loaded inside Bootstrap 3.0 Modal)
+// see https://github.com/handsontable/handsontable/issues/2937#issuecomment-287390111
+.modal.in .modal-dialog.handsontable-modal {
+  transform: none;
+}

+ 0 - 3
src/client/styles/scss/_override-handsontable.scss

@@ -1,3 +0,0 @@
-.modal .handsontable .wtBorder {
-  z-index: 110;
-}

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

@@ -14,9 +14,6 @@
 // override react-bootstrap-typeahead styles
 @import 'override-rbt';
 
-// override Handsontable styles
-@import 'override-handsontable';
-
 // crowi component
 @import 'admin';
 @import 'attachments';
@@ -42,6 +39,7 @@
 @import 'shortcuts';
 @import 'user';
 @import 'user_growi';
+@import 'handsontable';
 @import 'wiki';
 
 /*

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

@@ -338,6 +338,11 @@ module.exports = function(crowi) {
   };
 
   configSchema.statics.isGuesstAllowedToRead = function(config) {
+    // return true if puclic wiki mode
+    if (Config.isPublicWikiOnly(config)) {
+      return true;
+    }
+
     // return false if undefined
     if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
       return false;
@@ -360,6 +365,13 @@ module.exports = function(crowi) {
     const key = 'markdown:isEnabledLinebreaksInComments';
     return getValueForMarkdownNS(config, key);
   };
+  configSchema.statics.isPublicWikiOnly = function(config) {
+    const publicWikiOnly = process.env.PUBLIC_WIKI_ONLY;
+    if ( publicWikiOnly === 'true' || publicWikiOnly == 1) {
+      return true;
+    }
+    return false;
+  };
 
   configSchema.statics.pageBreakSeparator = function(config) {
     const key = 'markdown:presentation:pageBreakSeparator';
@@ -592,6 +604,7 @@ module.exports = function(crowi) {
         MATHJAX: env.MATHJAX || null,
       },
       recentCreatedLimit: Config.showRecentCreatedNumber(config),
+      isAclEnabled: !Config.isPublicWikiOnly(config),
     };
 
     return local_config;

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

@@ -491,6 +491,7 @@ module.exports = function(crowi) {
           return true;
         }
       }).then((checkResult) => {
+        console.log(checkResult);
         if (checkResult) {
           return resolve(pageData);
         }

+ 19 - 1
src/server/routes/admin.js

@@ -104,7 +104,8 @@ module.exports = function(crowi, app) {
   actions.security = {};
   actions.security.index = function(req, res) {
     const settingForm = Config.setupCofigFormData('crowi', req.config);
-    return res.render('admin/security', { settingForm });
+    const isAclEnabled = !Config.isPublicWikiOnly(req.config);
+    return res.render('admin/security', { settingForm, isAclEnabled });
   };
 
   // app.get('/admin/markdown'                  , admin.markdown.index);
@@ -669,10 +670,12 @@ module.exports = function(crowi, app) {
   actions.userGroup = {};
   actions.userGroup.index = function(req, res) {
     var page = parseInt(req.query.page) || 1;
+    const isAclEnabled = !Config.isPublicWikiOnly(req.config);
     var renderVar = {
       userGroups: [],
       userGroupRelations: new Map(),
       pager: null,
+      isAclEnabled,
     };
 
     UserGroup.findUserGroupsWithPagination({ page: page })
@@ -1030,6 +1033,21 @@ module.exports = function(crowi, app) {
 
   actions.api.securitySetting = function(req, res) {
     const form = req.form.settingForm;
+    const config = crowi.getConfig();
+    const isPublicWikiOnly = Config.isPublicWikiOnly(config);
+    if (isPublicWikiOnly) {
+      const basicName = form['security:basicName'];
+      const basicSecret = form['security:basicSecret'];
+      if (basicName != '' || basicSecret != '') {
+        req.form.errors.push('Public Wikiのため、Basic認証は利用できません。');
+        return res.json({status: false, message: req.form.errors.join('\n')});
+      }
+      const guestMode = form['security:restrictGuestMode'];
+      if ( guestMode == 'Deny' ) {
+        req.form.errors.push('Private Wikiへの設定変更はできません。');
+        return res.json({status: false, message: req.form.errors.join('\n')});
+      }
+    }
 
     if (req.form.isValid) {
       debug('form content', form);

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

@@ -1263,22 +1263,26 @@ module.exports = function(crowi, app) {
   };
 
   api.recentCreated = async function(req, res) {
-    const username = req.query.user || null;
-    const limit = + req.query.limit || 50;
-    const offset = + req.query.offset || 0;
+    const pageId = req.query.page_id;
 
-    const queryOptions = { offset: offset, limit: limit };
+    if (pageId == null) {
+      return res.json(ApiResponse.error('param \'pageId\' must not be null'));
+    }
 
-    if (username == null ) {
-      return res.json(ApiResponse.error('Parameter user is required.'));
+    const page = await Page.findPageById(pageId);
+    if (page == null) {
+      return res.json(ApiResponse.error(`Page (id='${pageId}') does not exist`));
+    }
+    if (!isUserPage(page.path)) {
+      return res.json(ApiResponse.error(`Page (id='${pageId}') is not a user home`));
     }
 
+    const limit = + req.query.limit || 50;
+    const offset = + req.query.offset || 0;
+    const queryOptions = { offset: offset, limit: limit };
+
     try {
-      let user = await User.findUserByUsername(username);
-      if (user == null) {
-        throw new Error('The user not found.');
-      }
-      let pages = await Page.findListByCreator(user, queryOptions, req.user);
+      let pages = await Page.findListByCreator(page.creator, queryOptions, req.user);
 
       const result = {};
       result.pages = pagePathUtils.encodePagesPath(pages);

+ 9 - 4
src/server/views/admin/security.html

@@ -44,16 +44,20 @@
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
             <div class="col-xs-3">
               <label for="">ID</label>
-              <input class="form-control" type="text" name="settingForm[security:basicName]"   value="{{ settingForm['security:basicName']|default('') }}">
+              <input class="form-control" type="text" name="settingForm[security:basicName]"   value="{{ settingForm['security:basicName']|default('') }}" {% if not isAclEnabled  %}readonly{% endif%}>
             </div>
             <div class="col-xs-3">
               <label for="">{{ t('Password') }}</label>
-              <input class="form-control" type="text" name="settingForm[security:basicSecret]" value="{{ settingForm['security:basicSecret']|default('') }}">
+              <input class="form-control" type="text" name="settingForm[security:basicSecret]" value="{{ settingForm['security:basicSecret']|default('') }}" {% if not isAclEnabled  %}readonly{% endif%}>
             </div>
             <div class="col-xs-offset-3 col-xs-9">
               <p class="help-block">
-                {{ t("security_setting.common_authentication") }}<br>
-                {{ t("security_setting.without_encryption") }}<br>
+                {% if not isAclEnabled %}
+                  {{ t("security_setting.basic_acl_disable") }}<br>
+                {% else %}
+                  {{ t("security_setting.common_authentication") }}<br>
+                  {{ t("security_setting.without_encryption") }}<br>
+                {% endif %}
               </p>
             </div>
           </div>
@@ -301,6 +305,7 @@
       {
         function showMessage(formId, msg, status) {
           $('#' + formId + ' > .alert').remove();
+          $('#' + formId ).find('.alert').remove();
 
           if (!status) {
             status = 'success';

+ 14 - 2
src/server/views/admin/user-groups.html

@@ -33,7 +33,11 @@
 
     <div class="col-md-9">
       <p>
-        <button  data-toggle="collapse" class="btn btn-default" href="#createGroupForm">新規グループの作成</button>
+        {% if isAclEnabled %}
+          <button  data-toggle="collapse" class="btn btn-default" href="#createGroupForm">新規グループの作成</button>
+        {% else %}
+          現在の設定では新規グループの作成はできません。
+        {% endif %}
       </p>
       <form role="form" action="/admin/user-group/create" method="post">
         <div id="createGroupForm" class="collapse">
@@ -124,13 +128,18 @@
             <td>
               <img src="{{ sGroup|picture }}" class="picture img-circle" />
             </td>
-            <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name | preventXss }}</a></td>
+            {% if isAclEnabled %}
+              <td><a href="{{ sGroupDetailPageUrl }}">{{ sGroup.name | preventXss }}</a></td>
+            {% else %}
+              <td>{{ sGroup.name | preventXss }}</td>
+            {% endif %}
             <td><ul class="list-inline">
               {% for relation in userGroupRelations.get(sGroup) %}
               <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
               {% endfor %}
             </ul></td>
             <td>{{ sGroup.createdAt|date('Y-m-d', sGroup.createdAt.getTimezoneOffset()) }}</td>
+            {% if isAclEnabled %}
             <td>
               <div class="btn-group admin-group-menu">
                 <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
@@ -156,6 +165,9 @@
                 </ul>
               </div>
             </td>
+            {% else %}
+              <td></td>
+            {% endif %}
           </tr>
           {% endfor %}
         </tbody>

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

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

+ 65 - 37
yarn.lock

@@ -27,15 +27,31 @@
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.3.0.tgz#a052303ae5d1a1f9b63eeb3a94495a2f429f4831"
 
-"@handsontable/react@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@handsontable/react/-/react-1.1.0.tgz#7f7cc822bc4cfab26f843792982ef81838e82d07"
-
-"@sinonjs/formatio@^2.0.0":
+"@handsontable/react@^2.0.0":
   version "2.0.0"
-  resolved "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
+  resolved "https://registry.yarnpkg.com/@handsontable/react/-/react-2.0.0.tgz#30d9c2bd05421588a6ed1b3050b1f7dc476b35d3"
+
+"@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"
+  dependencies:
+    "@sinonjs/samsam" "2.1.0"
+
+"@sinonjs/samsam@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.0.tgz#b8b8f5b819605bd63601a6ede459156880f38ea3"
   dependencies:
-    samsam "1.3.0"
+    array-from "^2.1.1"
+
+"@sinonjs/samsam@^2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.2.tgz#16947fce5f57258d01f1688fdc32723093c55d3f"
 
 "@types/body-parser@*":
   version "1.16.8"
@@ -452,6 +468,10 @@ array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
 
+array-from@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
+
 array-includes@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
@@ -4022,9 +4042,9 @@ gzip-size@^5.0.0:
     duplexer "^0.1.1"
     pify "^3.0.0"
 
-handsontable@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-5.0.1.tgz#4aadbaf1a468d8c7b3cdbf8a5f49c4110879c373"
+handsontable@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-6.0.1.tgz#93f07d895b42335e2882044a79bca96003a2cab2"
   dependencies:
     moment "2.20.1"
     numbro "^2.0.6"
@@ -5001,9 +5021,9 @@ jsx-ast-utils@^2.0.1:
   dependencies:
     array-includes "^3.0.3"
 
-just-extend@^1.1.27:
-  version "1.1.27"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905"
+just-extend@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288"
 
 jwa@^1.1.4:
   version "1.1.5"
@@ -5352,10 +5372,14 @@ lodash@^4.17.10, lodash@^4.17.5:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
-lolex@^2.3.2, lolex@^2.4.2:
+lolex@^2.3.2:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.0.tgz#9c087a69ec440e39d3f796767cf1b2cdc43d5ea5"
 
+lolex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.0.0.tgz#f04ee1a8aa13f60f1abd7b0e8f4213ec72ec193e"
+
 long@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
@@ -5578,9 +5602,9 @@ methods@~1.1.1, methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
 
-metismenu@^2.7.4:
-  version "2.7.4"
-  resolved "https://registry.yarnpkg.com/metismenu/-/metismenu-2.7.4.tgz#06e75a4dc0150ad5f60ebb0c7cd4e569bf52f519"
+metismenu@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/metismenu/-/metismenu-3.0.3.tgz#961e4c9469144d5078f6228b6e049e58f3137140"
 
 micromatch@2.3.11, micromatch@^2.1.5:
   version "2.3.11"
@@ -6003,12 +6027,12 @@ 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.3.3:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.1.tgz#78bc2b343d5ff1031ea9d1bb2c87a94c26db7250"
+nise@^1.4.5:
+  version "1.4.6"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.6.tgz#76cc3915925056ae6c405dd8ad5d12bde570c19f"
   dependencies:
-    "@sinonjs/formatio" "^2.0.0"
-    just-extend "^1.1.27"
+    "@sinonjs/formatio" "3.0.0"
+    just-extend "^3.0.0"
     lolex "^2.3.2"
     path-to-regexp "^1.7.0"
     text-encoding "^0.6.4"
@@ -8034,10 +8058,6 @@ safe-regex@^1.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
 
-samsam@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
-
 sanitizer@0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
@@ -8313,16 +8333,18 @@ sinon-chai@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.2.0.tgz#ed995e13a8a3cfccec18f218d9b767edc47e0715"
 
-sinon@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.0.0.tgz#f26627e4830dc34279661474da2c9e784f166215"
+sinon@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.0.0.tgz#99f2e5198d90a01ccbcebd4dc181a24827cb90dd"
   dependencies:
-    "@sinonjs/formatio" "^2.0.0"
+    "@sinonjs/commons" "^1.0.2"
+    "@sinonjs/formatio" "^3.0.0"
+    "@sinonjs/samsam" "^2.1.2"
     diff "^3.5.0"
     lodash.get "^4.4.2"
-    lolex "^2.4.2"
-    nise "^1.3.3"
-    supports-color "^5.4.0"
+    lolex "^3.0.0"
+    nise "^1.4.5"
+    supports-color "^5.5.0"
     type-detect "^4.0.8"
 
 slack-node@^0.1.8:
@@ -8761,6 +8783,12 @@ supports-color@^5.3.0:
   dependencies:
     has-flag "^3.0.0"
 
+supports-color@^5.5.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  dependencies:
+    has-flag "^3.0.0"
+
 svgo@^1.0.0:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.0.5.tgz#7040364c062a0538abacff4401cea6a26a7a389a"
@@ -8989,14 +9017,14 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
+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"
+
 type-detect@^4.0.0:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2"
 
-type-detect@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
-
 type-is@^1.6.4, type-is@~1.6.15:
   version "1.6.15"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"