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

Merge branch 'master' into imprv/refactor-acl

Yuki Takei 7 лет назад
Родитель
Сommit
5dc44b4984

+ 9 - 1
CHANGES.md

@@ -1,13 +1,21 @@
 CHANGES
 ========
 
-## 3.2.7-RC
+## 3.2.8-RC
+
+* 
+
+## 3.2.7
 
 * Feature: Import CSV/TSV/HTML table on Spreadsheet like GUI (Handsontable)
+* Fix: Pasting table data copied from Excel includes unnecessary line breaks
+* Fix: Page break Preset 1 for Presentation mode is broken
+* Fix: Login Form when LDAP login failed caused 500 Internal Server Error
 
 ## 3.2.6
 
 * Feature: Add select alignment buttons of Spreadsheet like GUI (Handsontable)
+* Improvement: Shrink the rows that have no diff of revision history page
 * Fix: Login form rejects weak password
 * Fix: An error occured by uploading attachment file when the page is not exists
     * Introduced by 2.3.5

+ 2 - 4
README.md

@@ -175,10 +175,8 @@ Environment Variables
     * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
     * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
     * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
-    * OAUTH_TWITTER_CLIENT_ID: Twitter API client id for OAuth login.
-    * OAUTH_TWITTER_CLIENT_SECRET: Twitter API client secret for OAuth login.
-    * OAUTH_TWITTER_CLIENT_ID: Twitter API client id for OAuth login.
-    * OAUTH_TWITTER_CLIENT_SECRET: Twitter API client secret for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_KEY: Twitter consumer key(API key) for OAuth login.
+    * OAUTH_TWITTER_CONSUMER_SECRET: Twitter consumer secret(API secret) for OAuth login.
     * SAML_ENTRY_POINT: IdP entry point
     * SAML_ISSUER: Issuer string to supply to IdP
     * SAML_CERT: PEM-encoded X.509 signing certificate string to validate the response from IdP

+ 1 - 1
config/webpack.common.js

@@ -58,7 +58,7 @@ module.exports = (options) => {
     },
     resolve: {
       extensions: ['.js', '.jsx', '.json'],
-      modules: [helpers.root('node_modules')],
+      modules: ((options.resolve && options.resolve.modules) || []).concat([helpers.root('node_modules')]),
       alias: {
         '@root': helpers.root('/'),
         '@commons': helpers.root('src/lib'),

+ 1 - 2
config/webpack.dev.js

@@ -24,8 +24,7 @@ module.exports = require('./webpack.common')({
     'js/dev': './src/client/js/dev',
   },
   resolve: {
-    // TODO merge in webpack.common.js
-    modules: [path.join(process.env.HOME, '.node_modules')],
+    modules: ['../node_modules'],
   },
   module: {
     rules: [

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.2.7-RC",
+  "version": "3.2.8-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 4 - 2
resource/locales/en-US/translation.json

@@ -255,7 +255,6 @@
     "Package name": "Package name",
     "Specified version": "Specified version",
     "Installed version": "Installed version"
-
   },
 
   "app_setting": {
@@ -265,7 +264,9 @@
     "Site URL": "Site URL",
     "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",
     "ex): internal use only":"ex): internal use only",
+    "File Uploading": "File Uploading",
     "enable_files_except_image": "Enable file upload other than image files.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "Update": "Update",
@@ -442,11 +443,12 @@
     "Page break setting": "Page break Setting",
     "Preset one separator": "Preset 1",
     "Preset one separator desc": "3 Blank lines",
+    "Preset one separator value": "\\n\\n\\n",
     "Preset two separator": "Preset 2",
     "Preset two separator desc": "5 Hyphens",
     "Preset two separator value": "-----",
     "Custom separator": "Custom",
-    "Custom separator desc": "Any character",
+    "Custom separator desc": "Regular Expression",
     "XSS_setting": "Prevent XSS(Cross Site Scripting) Setting",
     "XSS_setting_desc": "You can change the handling of HTML tags in markdown text.",
     "Enable XSS prevention": "Enable XSS Prevention",

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

@@ -271,9 +271,6 @@
     "Package name": "パッケージ名",
     "Specified version": "指定バージョン",
     "Installed version": "インストールされているバージョン"
-
-
-
   },
 
   "app_setting": {
@@ -283,7 +280,9 @@
     "Site URL": "サイトURL",
     "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL。",
     "Confidential name": "コンフィデンシャル表示",
+    "Default Language for new users": "新規ユーザーのデフォルト設定言語",
     "ex): internal use only": "例: 社外秘",
+    "File Uploading": "ファイルアップロード",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
     "Update": "更新",
@@ -459,11 +458,12 @@
     "Page break setting": "改頁を設定する",
     "Preset one separator": "プリセット 1",
     "Preset one separator desc": "連続した空行3行で改頁します",
+    "Preset one separator value": "\\n\\n\\n",
     "Preset two separator": "プリセット 2",
     "Preset two separator desc": "連続したハイフン5つで改頁します",
     "Preset two separator value": "-----",
     "Custom separator": "カスタム",
-    "Custom separator desc": "任意の文字で改頁します",
+    "Custom separator desc": "正規表現を設定できます",
     "XSS_setting": "XSS(Cross Site Scripting)対策設定",
     "XSS_setting_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "Enable XSS prevention": "XSSを抑制する",

+ 11 - 27
src/client/js/components/PageEditor/HandsontableModal.jsx

@@ -1,19 +1,14 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-
 import Modal from 'react-bootstrap/es/Modal';
 import Button from 'react-bootstrap/es/Button';
 import ButtonGroup from 'react-bootstrap/es/ButtonGroup';
-
-import { debounce } from 'throttle-debounce';
 import Collapse from 'react-bootstrap/es/Collapse';
-import FormGroup from 'react-bootstrap/es/FormGroup';
-import ControlLabel from 'react-bootstrap/es/ControlLabel';
-import FormControl from 'react-bootstrap/es/FormControl';
-
 import Handsontable from 'handsontable';
 import { HotTable } from '@handsontable/react';
+import { debounce } from 'throttle-debounce';
 
+import MarkdownTableDataImportForm from './MarkdownTableDataImportForm';
 import MarkdownTable from '../../models/MarkdownTable';
 
 const DEFAULT_HOT_HEIGHT = 300;
@@ -51,6 +46,7 @@ export default class HandsontableModal extends React.PureComponent {
     this.synchronizeAlignment = this.synchronizeAlignment.bind(this);
     this.alignButtonHandler = this.alignButtonHandler.bind(this);
     this.toggleDataImportArea = this.toggleDataImportArea.bind(this);
+    this.importData = this.importData.bind(this);
     this.expandWindow = this.expandWindow.bind(this);
     this.contractWindow = this.contractWindow.bind(this);
 
@@ -134,7 +130,7 @@ export default class HandsontableModal extends React.PureComponent {
 
   save() {
     if (this.props.onSave != null) {
-      this.props.onSave(this.state.markdownTable);
+      this.props.onSave(this.state.markdownTable.clone().normalizeCells());
     }
 
     this.hide();
@@ -248,6 +244,11 @@ export default class HandsontableModal extends React.PureComponent {
     this.setState({ isDataImportAreaExpanded: !this.state.isDataImportAreaExpanded });
   }
 
+  importData(markdownTable) {
+    this.init(markdownTable);
+    this.toggleDataImportArea();
+  }
+
   expandWindow() {
     this.setState({ isWindowExpanded: true });
 
@@ -298,7 +299,7 @@ export default class HandsontableModal extends React.PureComponent {
         <Modal.Body className="p-0 d-flex flex-column">
           <div className="px-4 py-3 modal-navbar">
             <Button className="m-r-20 data-import-button" onClick={this.toggleDataImportArea}>
-              (TBD) Data Import<i className={this.state.isDataImportAreaExpanded ? 'fa fa-angle-up' : 'fa fa-angle-down' }></i>
+              Data Import<i className={this.state.isDataImportAreaExpanded ? 'fa fa-angle-up' : 'fa fa-angle-down' }></i>
             </Button>
             <ButtonGroup>
               <Button onClick={() => { this.alignButtonHandler('l') }}><i className="ti-align-left"></i></Button>
@@ -307,24 +308,7 @@ export default class HandsontableModal extends React.PureComponent {
             </ButtonGroup>
             <Collapse in={this.state.isDataImportAreaExpanded}>
               <div> {/* This div is necessary for smoothing animations. (https://react-bootstrap.github.io/utilities/transitions/#transitions-collapse) */}
-                <form action="" className="data-import-form pt-5">
-                  <FormGroup>
-                    <ControlLabel>Select Data Format</ControlLabel>
-                    <FormControl componentClass="select" placeholder="select">
-                      <option value="select">(TBD) CSV</option>
-                      <option value="other">(TBD) TSV</option>
-                      <option value="other">(TBD) HTML</option>
-                    </FormControl>
-                  </FormGroup>
-                  <FormGroup>
-                    <ControlLabel>Import Data</ControlLabel>
-                    <FormControl componentClass="textarea" placeholder="Paste table data" style={{ height: 200 }}  />
-                  </FormGroup>
-                  <div className="d-flex justify-content-end">
-                    <Button bsStyle="default" onClick={this.toggleDataImportArea}>Cancel</Button>
-                    <Button bsStyle="primary">(TBD) Import</Button>
-                  </div>
-                </form>
+                <MarkdownTableDataImportForm onCancel={this.toggleDataImportArea} onImport={this.importData}/>
               </div>
             </Collapse>
           </div>

+ 86 - 0
src/client/js/components/PageEditor/MarkdownTableDataImportForm.jsx

@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import ControlLabel from 'react-bootstrap/es/ControlLabel';
+import FormControl from 'react-bootstrap/es/FormControl';
+import Button from 'react-bootstrap/es/Button';
+import MarkdownTable from '../../models/MarkdownTable';
+import Collapse from 'react-bootstrap/es/Collapse';
+
+export default class MarkdownTableDataImportForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      dataFormat: 'csv',
+      data: '',
+      parserErrorMessage: null
+    };
+
+    this.importButtonHandler = this.importButtonHandler.bind(this);
+  }
+
+  importButtonHandler() {
+    try {
+      const markdownTable = this.convertFormDataToMarkdownTable();
+      this.props.onImport(markdownTable);
+      this.setState({parserErrorMessage: null});
+    }
+    catch (e) {
+      this.setState({parserErrorMessage: e.message});
+    }
+  }
+
+  convertFormDataToMarkdownTable() {
+    let result;
+    switch (this.state.dataFormat) {
+      case 'csv':
+        result = MarkdownTable.fromDSV(this.state.data, ',');
+        break;
+      case 'tsv':
+        result = MarkdownTable.fromDSV(this.state.data, '\t');
+        break;
+      case 'html':
+        result = MarkdownTable.fromHTMLTableTag(this.state.data);
+        break;
+    }
+    return result.normalizeCells();
+  }
+
+  render() {
+    return (
+      <form action="" className="data-import-form pt-5">
+        <FormGroup>
+          <ControlLabel>Select Data Format</ControlLabel>
+          <FormControl componentClass="select" placeholder="select"
+                       value={this.state.dataFormat} onChange={e => this.setState({dataFormat: e.target.value})}>
+            <option value="csv">CSV</option>
+            <option value="tsv">TSV</option>
+            <option value="html">HTML</option>
+          </FormControl>
+        </FormGroup>
+        <FormGroup>
+          <ControlLabel>Import Data</ControlLabel>
+          <FormControl componentClass="textarea" placeholder="Paste table data" style={{ height: 200 }}
+                       onChange={e => this.setState({data: e.target.value})}/>
+        </FormGroup>
+        <Collapse in={this.state.parserErrorMessage != null}>
+          <FormGroup>
+            <ControlLabel>Parse Error</ControlLabel>
+            <FormControl componentClass="textarea" style={{ height: 100 }}  value={this.state.parserErrorMessage} readOnly/>
+          </FormGroup>
+        </Collapse>
+        <div className="d-flex justify-content-end">
+          <Button bsStyle="default" onClick={this.props.onCancel}>Cancel</Button>
+          <Button bsStyle="primary" onClick={this.importButtonHandler}>Import</Button>
+        </div>
+      </form>
+    );
+  }
+}
+
+MarkdownTableDataImportForm.propTypes = {
+  onCancel: PropTypes.func,
+  onImport: PropTypes.func
+};

+ 61 - 4
src/client/js/models/MarkdownTable.js

@@ -1,5 +1,6 @@
 import markdownTable from 'markdown-table';
 import stringWidth from 'string-width';
+import csvToMarkdown from 'csv-to-markdown-table';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
 // https://regex101.com/r/7BN2fR/7
@@ -7,6 +8,9 @@ const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
 const tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
+// set up DOMParser
+const domParser = new (window.DOMParser)();
+
 /**
  * markdown table class for markdown-table module
  *   ref. https://github.com/wooorm/markdown-table
@@ -36,13 +40,66 @@ export default class MarkdownTable {
     return new MarkdownTable(newTable, this.options);
   }
 
-  static fromTableTag(str) {
-    // TODO impl
-    return new MarkdownTable();
+  /**
+   * normalize all cell data(trim & convert the newline character to space)
+   */
+  normalizeCells() {
+    for (let i = 0; i < this.table.length; i++) {
+      for (let j = 0; j < this.table[i].length; j++) {
+        this.table[i][j] = this.table[i][j].trim().replace(/\r?\n/g, ' ');
+      }
+    }
+
+    return this;
+  }
+
+  /**
+   * return a MarkdownTable instance made from a string of HTML table tag
+   *
+   * If a parser error occurs, an error object with an error message is thrown.
+   * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
+   */
+  static fromHTMLTableTag(str) {
+    // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
+    const dom = domParser.parseFromString(str, 'application/xml');
+
+    if (dom.querySelector('parsererror')) {
+      throw new Error(dom.documentElement.innerHTML);
+    }
+
+    const tableElement = dom.querySelector('table');
+    const trElements = tableElement.querySelectorAll('tr');
+
+    let table = [];
+    let maxRowSize = 0;
+    for (let i = 0; i < trElements.length; i++) {
+      let row = [];
+      let cellElements = trElements[i].querySelectorAll('th,td');
+      for (let j = 0; j < cellElements.length; j++) {
+        row.push(cellElements[j].innerHTML);
+      }
+      table.push(row);
+
+      if (maxRowSize < row.length) maxRowSize = row.length;
+    }
+
+    let align = [];
+    for (let i = 0; i < maxRowSize; i++) {
+      align.push('');
+    }
+
+    return new MarkdownTable(table, {align: align});
+  }
+
+  /**
+   * return a MarkdownTable instance made from a string of delimiter-separated values
+   */
+  static fromDSV(str, delimiter) {
+    return MarkdownTable.fromMarkdownString(csvToMarkdown(str, delimiter, true));
   }
 
   /**
-   * returns MarkdownTable instance
+   * return a MarkdownTable instance
    *   ref. https://github.com/wooorm/markdown-table
    * @param {string} str markdown string
    */

+ 5 - 2
src/client/styles/agile-admin/inverse/colors/_apply-colors.scss

@@ -34,11 +34,14 @@ body{
 }
 
 .navbar-header{
-     background:$topbar;
+  background:$topbar;
 }
-.navbar-top-links > li > a{
+.navbar-top-links > li > a {
     color:$white;
 }
+.navbar-top-links .confidential {
+  background-color: darken($topbar, 10%);
+}
 /*
 .notify .heartbit{
     border-color:$white;

+ 2 - 8
src/client/styles/scss/_layout.scss

@@ -6,14 +6,8 @@
   .navbar-top-links { // {{{
     .confidential {
       a {
-        // border: solid 2px #f00;
-        // background: #fff;
-        // color: #f00;
-        // font-weight: bold;
-        // height: 42px;
-        // margin-top: 5px;
-        // padding: 10px;
-        // margin-right: 5px;
+        cursor: default;
+        font-weight: bold;
       }
     }
 

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

@@ -7,6 +7,7 @@ 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()
 );
 

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

@@ -53,6 +53,7 @@ module.exports = function(crowi) {
       'app:confidential'  : '',
 
       'app:fileUpload'    : false,
+      'app:globalLang'    : 'en',
 
       'security:restrictGuestMode'      : 'Deny',
 
@@ -283,6 +284,11 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key) || 'GROWI';
   };
 
+  configSchema.statics.globalLang = function(config) {
+    const key = 'app:globalLang';
+    return getValueForCrowiNS(config, key);
+  };
+
   configSchema.statics.isEnabledPassport = function(config) {
     // always true if growi installed cleanly
     if (Object.keys(config.crowi).length == 0) {
@@ -605,6 +611,7 @@ module.exports = function(crowi) {
       },
       recentCreatedLimit: Config.showRecentCreatedNumber(config),
       isAclEnabled: !Config.isPublicWikiOnly(config),
+      globalLang: Config.globalLang(config),
     };
 
     return local_config;

+ 58 - 21
src/server/routes/login-passport.js

@@ -80,11 +80,18 @@ module.exports = function(crowi, app) {
 
     const providerId = 'ldap';
     const strategyName = 'ldapauth';
-    const ldapAccountInfo = await promisifiedPassportAuthentication(req, res, next, strategyName);
+    let ldapAccountInfo;
+
+    try {
+      ldapAccountInfo = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      return next(err);
+    }
 
     // check groups for LDAP
     if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
-      return loginFailure(req, res, next);
+      return next();
     }
 
     /*
@@ -106,9 +113,9 @@ module.exports = function(crowi, app) {
       'email': mailToBeRegistered,
     };
 
-    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res, next);
+      return next();
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -223,13 +230,21 @@ module.exports = function(crowi, app) {
   const loginPassportGoogleCallback = async(req, res, next) => {
     const providerId = 'google';
     const strategyName = 'google';
-    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+
+    let response;
+    try {
+      response = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      return loginFailure(req, res, next);
+    }
+
     const userInfo = {
       'id': response.id,
       'username': response.displayName,
       'name': `${response.name.givenName} ${response.name.familyName}`
     };
-    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
       return loginFailure(req, res, next);
     }
@@ -256,14 +271,22 @@ module.exports = function(crowi, app) {
   const loginPassportGitHubCallback = async(req, res, next) => {
     const providerId = 'github';
     const strategyName = 'github';
-    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+
+    let response;
+    try {
+      response = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      return loginFailure(req, res, next);
+    }
+
     const userInfo = {
       'id': response.id,
       'username': response.username,
       'name': response.displayName
     };
 
-    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
       return loginFailure(req, res, next);
     }
@@ -290,14 +313,22 @@ module.exports = function(crowi, app) {
   const loginPassportTwitterCallback = async(req, res, next) => {
     const providerId = 'twitter';
     const strategyName = 'twitter';
-    const response = await promisifiedPassportAuthentication(req, res, next, strategyName);
+
+    let response;
+    try {
+      response = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      return loginFailure(req, res, next);
+    }
+
     const userInfo = {
       'id': response.id,
       'username': response.username,
       'name': response.displayName
     };
 
-    const externalAccount = await getOrCreateUser(req, res, next, userInfo, providerId);
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
       return loginFailure(req, res, next);
     }
@@ -330,7 +361,14 @@ module.exports = function(crowi, app) {
     const attrMapFirstName = config.crowi['security:passport-saml:attrMapFirstName'] || 'firstName';
     const attrMapLastName = config.crowi['security:passport-saml:attrMapLastName'] || 'lastName';
 
-    const response = await promisifiedPassportAuthentication(req, res, loginFailure, strategyName);
+    let response;
+    try {
+      response = await promisifiedPassportAuthentication(strategyName, req, res);
+    }
+    catch (err) {
+      return loginFailure(req, res);
+    }
+
     const userInfo = {
       'id': response[attrMapId],
       'username': response[attrMapUsername],
@@ -344,7 +382,7 @@ module.exports = function(crowi, app) {
       userInfo['name'] = `${response[attrMapFirstName]} ${response[attrMapLastName]}`.trim();
     }
 
-    const externalAccount = await getOrCreateUser(req, res, loginFailure, userInfo, providerId);
+    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
       return loginFailure(req, res);
     }
@@ -361,7 +399,7 @@ module.exports = function(crowi, app) {
     });
   };
 
-  const promisifiedPassportAuthentication = (req, res, next, strategyName) => {
+  const promisifiedPassportAuthentication = (strategyName, req, res) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
         if (res.headersSent) {  // dirty hack -- 2017.09.25
@@ -372,24 +410,23 @@ module.exports = function(crowi, app) {
 
         if (err) {
           logger.error(`'${strategyName}' passport authentication error: `, err);
-          req.flash('warningMessage', `Error occured in '${strategyName}' passport authentication`);  // pass and the flash message is displayed when all of authentications are failed.
-          return next(req, res);
+          reject(err);
         }
 
+        logger.debug('response', response);
+        logger.debug('info', info);
+
         // authentication failure
         if (!response) {
-          return next(req, res);
+          reject(response);
         }
 
-        logger.debug('response', response);
-        logger.debug('info', info);
-
         resolve(response);
-      })(req, res, next);
+      })(req, res);
     });
   };
 
-  const getOrCreateUser = async(req, res, next, userInfo, providerId) => {
+  const getOrCreateUser = async(req, res, userInfo, providerId) => {
     try {
       // find or register(create) user
       const externalAccount = await ExternalAccount.findOrRegister(

+ 1 - 0
src/server/routes/me.js

@@ -117,6 +117,7 @@ module.exports = function(crowi, app) {
             Object.keys(err.errors).forEach((e) => {
               req.form.errors.push(err.errors[e].message);
             });
+
             return res.render('me/index', {});
           }
           req.i18n.changeLanguage(lang);

+ 8 - 0
src/server/util/swigFunctions.js

@@ -49,6 +49,14 @@ module.exports = function(crowi, app, req, locals) {
     return crowi.xss.process(Config.appTitle(config));
   };
 
+  /**
+   * return app-global language
+   */
+  locals.appGlobalLang = function() {
+    const config = crowi.getConfig();
+    return Config.globalLang(config);
+  };
+
   /**
    * return true if enabled
    */

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

@@ -60,7 +60,22 @@
         </div>
 
         <div class="form-group">
-          <div class="col-xs-offset-3 col-xs-6">
+          <label class="col-xs-3 control-label tbd">(TBD) {{ t('app_setting.Default Language for new users') }}</label>
+          <div class="col-xs-6">
+            <div class="radio radio-primary radio-inline">
+                <input type="radio" id="radioLangEn" name="userForm[globalLang]" value="{{ consts.language.LANG_EN_US }}" {% if user.lang == consts.language.LANG_EN_US %}checked="checked"{% endif %}>
+                <label for="radioLangEn">{{ t('English') }}</label>
+            </div>
+            <div class="radio radio-primary radio-inline">
+                <input type="radio" id="radioLangJa" name="userForm[globalLang]" value="{{ consts.language.LANG_JA }}" {% if user.lang == consts.language.LANG_JA %}checked="checked"{% endif %}>
+                <label for="radioLangJa">{{ t('Japanese') }}</label>
+            </div>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label class="col-xs-3 control-label">{{ t('app_setting.File Uploading') }}</label>
+          <div class="col-xs-6">
             <div class="checkbox checkbox-info">
               <input type="checkbox" id="cbFileUpload" name="settingForm[app:fileUpload]" value="1"
                 {% if settingForm['app:fileUpload'] %}

+ 5 - 6
src/server/views/admin/customize.html

@@ -65,12 +65,6 @@
       <form action="/_api/admin/customize/layout" method="post" class="form-horizontal" id="customlayoutSettingForm" role="form">
       <fieldset>
         <legend>{{ t('customize_page.Layout') }}</legend>
-        {% if env === 'development' %}
-          <br>
-          <div class="alert alert-warning">
-            <strong>DEBUG MESSAGE:</strong> development build では、リアルタイムプレビューが無効になります
-          </div>
-        {% endif %}
         <div class="form-group">
           <div class="col-sm-4">
             <h4>
@@ -134,6 +128,11 @@
           </div>
         </div>
         <h2>{{ t('customize_page.Theme') }}</h2>
+        {% if env === 'development' %}
+          <div class="alert alert-warning">
+            <strong>DEBUG MESSAGE:</strong> development build では、リアルタイムプレビューが無効になります
+          </div>
+        {% endif %}
         <div id="themeOptions" {% if 'kibela' == settingForm['customize:layout'] %}class="disabled"{% endif %}>
           {# Light Themes #}
           <div class="d-flex">

+ 2 - 2
src/server/views/admin/external-accounts.html

@@ -100,11 +100,11 @@
                 </button>
                 <ul class="dropdown-menu" role="menu">
                   <li class="dropdown-header">{{ t('user_management.Edit menu') }}</li>
-                  <form id="form_remove_{{ account.accountId }}" action="/admin/users/external-accounts/{{ account.accountId }}/remove" method="post">
+                  <form id="form_remove_{{ loop.index }}" action="/admin/users/external-accounts/{{ account.accountId }}/remove" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                   </form>
                   <li>
-                    <a href="javascript:form_remove_{{ account.accountId }}.submit()">
+                    <a href="javascript:form_remove_{{ loop.index }}.submit()">
                       <i class="icon-fw icon-fire text-danger"></i>
                       削除する
                     </a>

+ 24 - 21
src/server/views/admin/markdown.html

@@ -94,42 +94,45 @@
         <p class="well">{{ t("markdown_setting.presentation_setting_desc") }}</p>
 
         <fieldset class="form-group row my-2">
-            {% set nameForPageBreakOption = "markdownSetting[markdown:presentation:pageBreakSeparator]" %}
-            {% set pageBreakSeparator = markdownSetting['markdown:presentation:pageBreakSeparator'] %}
+          {% set nameForPageBreakOption = "markdownSetting[markdown:presentation:pageBreakSeparator]" %}
+          {% set pageBreakSeparator = markdownSetting['markdown:presentation:pageBreakSeparator'] %}
 
           <label class="col-xs-3 control-label">
             {{ t('markdown_setting.Page break setting') }}
           </label>
 
           <div class="col-xs-3 radio radio-primary">
-              <input type="radio" id="option1" name="{{nameForPageBreakOption}}" value="1" {% if pageBreakSeparator === 1 %}checked{% endif %}>
-              <label for="option1">
-                <p class="font-weight-bold">{{ t('markdown_setting.Preset one separator') }}</p>
-                <div class="m-t-15">
-                    {{ t('markdown_setting.Preset one separator desc') }}
-                </div>
-              </label>
+            <input type="radio" id="option1" name="{{nameForPageBreakOption}}" value="1" {% if pageBreakSeparator === 1 %}checked{% endif %}>
+            <label for="option1">
+              <p class="font-weight-bold">{{ t('markdown_setting.Preset one separator') }}</p>
+              <p class="mt-3">
+                {{ t('markdown_setting.Preset one separator desc') }}
+                <pre><code>{{ t('markdown_setting.Preset one separator value') }}</code></pre>
+              </p>
+            </label>
           </div>
 
           <div class="col-xs-3 radio radio-primary">
-              <input type="radio" id="option2" name="{{nameForPageBreakOption}}" value="2" {% if pageBreakSeparator === 2 %}checked{% endif %}>
-              <label for="option2">
-                <p class="font-weight-bold">{{ t('markdown_setting.Preset two separator') }}</p>
-                <div class="m-t-15">
-                    {{ t('markdown_setting.Preset two separator desc') }}
-                    <input class="form-control" type="text" name="presetTwoSeparator" value="{{ t('markdown_setting.Preset two separator value') }}" readonly>
-                </div>
-              </label>
+            <input type="radio" id="option2" name="{{nameForPageBreakOption}}" value="2" {% if pageBreakSeparator === 2 %}checked{% endif %}>
+            <label for="option2">
+              <p class="font-weight-bold">{{ t('markdown_setting.Preset two separator') }}</p>
+              <p class="mt-3">
+                {{ t('markdown_setting.Preset two separator desc') }}
+                <pre><code>{{ t('markdown_setting.Preset two separator value') }}</code></pre>
+              </p>
+            </label>
           </div>
 
           <div class="col-xs-3 radio radio-primary">
             <input type="radio" id="option3" name="{{nameForPageBreakOption}}" value="3" {% if pageBreakSeparator === 3 %}checked{% endif %}>
             <label for="option3">
               <p class="font-weight-bold">{{ t('markdown_setting.Custom separator') }}</p>
-              <div class="m-t-15">
-                  {{ t('markdown_setting.Custom separator desc') }}
-                  <input class="form-control" type="text" name="markdownSetting[markdown:presentation:pageBreakCustomSeparator]" value="{{markdownSetting['markdown:presentation:pageBreakCustomSeparator']|default('') }}">
-              </div>
+              <p class="mt-3">
+                {{ t('markdown_setting.Custom separator desc') }}
+                <div>
+                  <input class="form-control" name="markdownSetting[markdown:presentation:pageBreakCustomSeparator]" value="{{markdownSetting['markdown:presentation:pageBreakCustomSeparator']|default('') }}">
+                </div>
+              </p>
             </label>
           </div>
 

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

@@ -199,11 +199,11 @@
                   <i class="icon-settings"></i> <span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu" role="menu">
-                  <form id="form_removeFromGroup_{{ sUser.id }}" action="/admin/user-group-relation/{{userGroup._id.toString()}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                  <form id="form_removeFromGroup_{{ loop.index }}" action="/admin/user-group-relation/{{userGroup._id.toString()}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                   </form>
                   <li>
-                    <a href="javascript:form_removeFromGroup_{{ sUser.id }}.submit()">
+                    <a href="javascript:form_removeFromGroup_{{ loop.index }}.submit()">
                       <i class="icon-fw icon-user-unfollow"></i> グループから外す
                     </a>
                   </li>

+ 4 - 0
src/server/views/installer.html

@@ -49,6 +49,10 @@
         <small>初めに作成するアカウントは、自動的に管理者権限が付与されます</small>
       </p>
 
+      <p class="alert alert-warning p-b-10 p-t-10">
+        <small>現在の言語設定: {{ appGlobalLang() }}</small>
+      </p>
+
       <form role="form" action="/installer/createAdmin" method="post" id="register-form">
 
         <div class="input-group" id="input-group-username">

+ 4 - 8
src/server/views/page_presentation.html

@@ -44,8 +44,6 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
     <title>{{ path|path2name }} | {{ path }}</title>
 
     <!-- styles -->
-    <link rel="stylesheet" href="{{ webpack_asset('styles/style.css') }}">
-    <link rel="stylesheet" href="{{ webpack_asset('styles/theme-default.css') }}">
     <link rel="stylesheet" href="{{ webpack_asset('styles/style-presentation.css') }}">
 
     <!-- Google Fonts -->
@@ -61,17 +59,15 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
         {% set pageBreakSeparator = pageBreakSeparator()|default(1) %}
         {% set pageBreakCustomSeparator = pageBreakCustomSeparator()|default('') %}
 
-        {% if 1 === pageBreakSeparator %}
-          {% set dataSeparator = "^\n\n\n" %}
+        {% if 3 === pageBreakSeparator %}
+          {% set dataSeparator = pageBreakCustomSeparator %}
         {% elseif 2 === pageBreakSeparator %}
           {% set dataSeparator = "^-----$" %}
-        {% elseif 3 === pageBreakSeparator %}
-          {% set dataSeparator = "^" + pageBreakCustomSeparator + "$" %}
         {% else %}
-          {% set dataSeparator = "^\n\n\n" %}
+          {% set dataSeparator = "\n\n\n" %}
         {% endif %}
 
-        <section data-markdown data-separator={{dataSeparator}}>
+        <section data-markdown data-separator="{{dataSeparator}}">
           <script type="text/template">
 {{ revision.body|presentation|safe }}
           </script>

+ 5 - 5
yarn.lock

@@ -4060,8 +4060,8 @@ gzip-size@^5.0.0:
     pify "^3.0.0"
 
 handsontable@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-6.0.1.tgz#93f07d895b42335e2882044a79bca96003a2cab2"
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/handsontable/-/handsontable-6.1.1.tgz#4be8fbe25efd3f0b85b494967475a687007e288d"
   dependencies:
     moment "2.20.1"
     numbro "^2.0.6"
@@ -6336,8 +6336,8 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
 
 numbro@^2.0.6:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.0.tgz#618ac6e4b2f32f2e623190ce4b05f4c8b09c3207"
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.1.tgz#b977fc6a769163f90e2e2d7623ff9db4d66bc661"
   dependencies:
     bignumber.js "^4.0.4"
 
@@ -6830,7 +6830,7 @@ pify@^3.0.0:
 
 pikaday@1.5.1:
   version "1.5.1"
-  resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
+  resolved "http://registry.npmjs.org/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
   optionalDependencies:
     moment "2.x"