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

Merge pull request #684 from weseek/master

release v3.2.7
Yuki Takei 7 лет назад
Родитель
Сommit
cd2f48221a

+ 4 - 1
CHANGES.md

@@ -1,9 +1,12 @@
 CHANGES
 ========
 
-## 3.2.6-RC
+## 3.2.7-RC
 
 * Feature: Import CSV/TSV/HTML table on Spreadsheet like GUI (Handsontable)
+
+## 3.2.6
+
 * Feature: Add select alignment buttons of Spreadsheet like GUI (Handsontable)
 * Fix: Login form rejects weak password
 * Fix: An error occured by uploading attachment file when the page is not exists

+ 17 - 14
README.md

@@ -195,18 +195,6 @@ Documentation
 Contribution
 ============
 
-For development
--------------
-
-### Build and Run the app
-
-1. `clone` this repository
-2. `yarn` to install all dependencies
-    * DO NOT USE `npm install`
-3. `npm run build` to build client app
-4. `npm run server` to start the dev server
-5. Access `http://0.0.0.0:3000`
-
 Found a Bug?
 -------------
 
@@ -224,8 +212,23 @@ Repository. If you would like to *implement* a new feature, firstly please submi
 It also allows us to coordinate better, prevent duplication of work and help you to create the change so it can be successfully accepted into the project.
 * **Small Features** can be created and directly [submitted as a Pull Request][pulls].
 
-Language
----------
+Translation
+--------------
+
+### for GROWI system
+
+We have [the Transifex Project for GROWI](https://www.transifex.com/weseek-inc/growi).  
+Please join to our team!
+
+### for documents
+
+*We have [Gitbook site](https://docs.growi.org), but currently Gitbook doesn't support Multi-langage.*  
+-> https://docs.gitbook.com/v2-changes/important-differences#multi-language-books
+
+*We have to wait until it is implemented.*
+
+Language on GitHub
+------------------
 
 You can write issues and PRs in English or Japanese.
 

+ 1 - 1
package.json

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

+ 12 - 12
resource/locales/en-US/sandbox.md

@@ -19,18 +19,18 @@
 先頭に`#`をレベルの数だけ記述します。
 
 ```
-# 見出し1
-## 見出し2
-### 見出し3
-#### 見出し4
-##### 見出し5
-###### 見出し6
-```
-
-### 見出し3
-#### 見出し4
-##### 見出し5
-###### 見出し6
+# Header 1
+## Header 2
+### Header 3
+#### Header 4
+##### Header 5
+###### Header 6
+```
+
+### Header 3
+#### Header 4
+##### Header 5
+###### Header 6
 
 ## Block 段落
 

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

@@ -442,11 +442,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",

+ 5 - 5
resource/locales/en-US/welcome.md

@@ -6,8 +6,8 @@
 <div class="panel panel-default">
   <div class="panel-heading">Tips</div>
   <div class="panel-body"><ul>
-    <li>Ctrl(⌘)-/ でショートカットヘルプを表示します</li>
-      <li>HTML/CSS の記述時は、<a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> を利用できます</li>
+    <li>Ctrl(⌘)-/ to show quick help</li>
+    <li>You can <a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> to write HTML tags.</li>
   </ul></div>
 </div>
 
@@ -18,12 +18,12 @@ Contents
 
 |All Pages|[/Sandbox]|
 | --- | --- |
-| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [Sandboxをチェック](/Sandbox)</span></div> $lsx(/Sandbox)|
+| $lsx(/) | <div class="alert alert-success"><span style="font-size: x-large;"><i class="icon-check"></i> [Go to Sandbox](/Sandbox)</span></div> $lsx(/Sandbox)|
 
 Slack
 =====
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
-GROWI をより良いものにするために、是非 Slack に参加してください。  
-開発に関する議論を行っている他、導入時の質問等も受け付けています。
+Please join Slack by all means to make GROWI better.
+In addition to discussing development, we also accept questions at the time of introduction.

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

@@ -459,11 +459,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を抑制する",

+ 9 - 25
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);
 
@@ -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 });
 
@@ -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">CSV</option>
-                      <option value="other">TSV</option>
-                      <option value="other">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">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;
+  }
+
+  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
+};

+ 48 - 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,53 @@ export default class MarkdownTable {
     return new MarkdownTable(newTable, this.options);
   }
 
-  static fromTableTag(str) {
-    // TODO impl
-    return new MarkdownTable();
+  /**
+   * 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
    */

+ 8 - 0
src/server/crowi/express-init.js

@@ -109,6 +109,14 @@ module.exports = function(crowi, app) {
       return next();
     }
 
+    // FIXME:
+    //   healthcheck endpoint exclude from basic authentication.
+    //   however, hard coding is not desirable.
+    //   need refactoring (ex. setting basic authentication for each routes)
+    if (req.path === '/_api/v3/healthcheck') {
+      return next();
+    }
+
     if (config.crowi['security:basicName'] && config.crowi['security:basicSecret']) {
       return basicAuth(
         config.crowi['security:basicName'],

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

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

+ 1 - 1
src/server/views/admin/security.html

@@ -259,7 +259,7 @@
             <li>
               <a href="#passport-github" data-toggle="tab" role="tab"><i class="fa fa-github"></i> GitHub</a>
             </li>
-            <li class="tbd">
+            <li>
               <a href="#passport-twitter" data-toggle="tab" role="tab"><i class="fa fa-twitter"></i> Twitter</a>
             </li>
             <li class="tbd">

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