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

Merge commit 'a7af2784e2cb7d3a84ac558837b7fe07cf83c14d' into feat/enable-to-tag-page

yusuketk 7 лет назад
Родитель
Сommit
e6574abab0
35 измененных файлов с 614 добавлено и 604 удалено
  1. 29 1
      CHANGES.md
  2. 1 0
      config/logger/config.dev.js
  3. 6 6
      package.json
  4. 1 1
      resource/cdn-manifests.js
  5. 10 18
      resource/search/mappings.json
  6. 1 1
      src/client/js/app.js
  7. 1 1
      src/client/js/components/InstallerForm.jsx
  8. 3 1
      src/client/js/components/PageAttachment.js
  9. 26 8
      src/client/js/components/PageComment/CommentForm.jsx
  10. 2 7
      src/client/js/components/PageComments.js
  11. 13 11
      src/client/js/components/PageEditor/Editor.jsx
  12. 5 5
      src/client/js/components/PagePathAutoComplete.jsx
  13. 8 6
      src/client/js/legacy/crowi.js
  14. 13 8
      src/client/styles/scss/_layout_crowi_sidebar.scss
  15. 0 57
      src/lib/util/page-path-utils.js
  16. 84 0
      src/lib/util/path-utils.js
  17. 0 10
      src/server/models/page.js
  18. 5 0
      src/server/models/user.js
  19. 2 2
      src/server/routes/index.js
  20. 55 33
      src/server/routes/installer.js
  21. 16 15
      src/server/routes/page.js
  22. 4 3
      src/server/service/config-manager.js
  23. 7 1
      src/server/service/file-uploader/gridfs.js
  24. 5 4
      src/server/service/passport.js
  25. 12 10
      src/server/util/googleAuth.js
  26. 5 34
      src/server/util/middlewares.js
  27. 15 5
      src/server/util/search.js
  28. 9 7
      src/server/util/slack.js
  29. 6 27
      src/server/views/layout-crowi/widget/page_side_content.html
  30. 1 13
      src/server/views/layout-growi/widget/comments.html
  31. 1 13
      src/server/views/layout-kibela/widget/comments.html
  32. 4 4
      src/server/views/modal/what_is_portal.html
  33. 0 14
      src/test/models/page.test.js
  34. 32 0
      src/test/util/path-utils.test.js
  35. 232 278
      yarn.lock

+ 29 - 1
CHANGES.md

@@ -1,14 +1,42 @@
 CHANGES
 ========
 
-## 3.4.0-RC
+## 3.4.2-RC
+
+* 
+
+## 3.4.1
+
+* Fix: "Cannot find module 'stream-to-promise'" occured when build client with `FILE_UPLOAD=local`
+
+## 3.4.0
+
+### BREAKING CHANGES
+
+None.
+
+Upgrading Guide: https://docs.growi.org/guide/upgrading/34x.html
+
+### Updates
 
 * Improvement: Restrict to access attachments when the user is not allowed to see page
 * Improvement: Show fans and visitors of page
+* Improvement: Full text search tokenizing
+* Improvement: Markdown comment on Crowi Classic Layout
 * Fix: Profile image is not displayed when `FILE_UPLOAD=mongodb`
+* Fix: Posting comment doesn't work under Crowi Classic Layout
+    * Introduced by 3.1.5
+* Fix: HackMD doesn't work when `siteUrl` ends with slash
+* Fix: Ensure not to be able to move/duplicate page to the path which has trailing slash
 * Support: Launch with Node.js v10
 * Support: Launch with MongoDB 3.6
 * Support: Launch with Elasticsearch 6.6
+* Support: Upgrade libs
+    * bootstrap-sass
+    * browser-sync
+    * react
+    * react-dom
+
 
 ## 3.3.10
 

+ 1 - 0
config/logger/config.dev.js

@@ -11,6 +11,7 @@ module.exports = {
   // 'growi:routes:login': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:service:PassportService': 'debug',
+  'growi:lib:search': 'debug',
   // 'growi:service:GlobalNotification': 'debug',
   // 'growi:lib:importer': 'debug',
   // 'growi:routes:page': 'debug',

+ 6 - 6
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.4.0-RC",
+  "version": "3.4.2-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -115,6 +115,7 @@
     "rimraf": "^2.6.1",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
+    "stream-to-promise": "^2.2.0",
     "string-width": "^3.0.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
@@ -132,10 +133,10 @@
     "babel-polyfill": "^6.26.0",
     "babel-preset-env": "^1.6.0",
     "babel-preset-react": "^6.24.1",
-    "bootstrap-sass": "~3.4.0",
+    "bootstrap-sass": "^3.4.1",
     "bootstrap-select": "^1.12.4",
     "browser-bunyan": "^1.3.0",
-    "browser-sync": "^2.23.6",
+    "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "chai": "^4.1.0",
     "cli": "~1.0.1",
@@ -183,12 +184,12 @@
     "penpal": "^3.0.3",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
-    "react": "^16.7.0",
+    "react": "^16.8.3",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.3.4",
     "react-clipboard.js": "^2.0.0",
     "react-codemirror2": "^5.1.0",
-    "react-dom": "^16.4.1",
+    "react-dom": "^16.8.3",
     "react-dropzone": "=7.0.1",
     "react-frame-component": "^4.0.0",
     "react-i18next": "=7.13.0",
@@ -200,7 +201,6 @@
     "sinon": "^7.2.2",
     "sinon-chai": "^3.3.0",
     "socket.io-client": "^2.0.3",
-    "stream-to-promise": "^2.2.0",
     "style-loader": "^0.23.0",
     "terser-webpack-plugin": "^1.2.2",
     "throttle-debounce": "^2.0.0",

+ 1 - 1
resource/cdn-manifests.js

@@ -2,7 +2,7 @@ module.exports = {
   js: [
     {
       name: 'basis',
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1,npm/bootstrap@3.3.7/dist/js/bootstrap.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1,npm/bootstrap@3.4.1/dist/js/bootstrap.min.js',
       groups: ['basis'],
       args: {
         integrity: '',

+ 10 - 18
resource/search/mappings.json

@@ -5,21 +5,13 @@
         "english_stop": {
           "type":       "stop",
           "stopwords":  "_english_"
-        },
-        "english_stemmer": {
-          "type":       "stemmer",
-          "language":   "english"
-        },
-        "english_possessive_stemmer": {
-          "type":       "stemmer",
-          "language":   "possessive_english"
         }
       },
       "tokenizer": {
-        "ngram_tokenizer": {
-          "type": "ngram",
+        "edge_ngram_tokenizer": {
+          "type": "edge_ngram",
           "min_gram": 2,
-          "max_gram": 3,
+          "max_gram": 20,
           "token_chars": ["letter", "digit"]
         }
       },
@@ -28,13 +20,11 @@
           "tokenizer": "kuromoji_tokenizer",
           "char_filter" : ["icu_normalizer"]
         },
-        "english": {
-          "tokenizer": "ngram_tokenizer",
+        "english_edge_ngram": {
+          "tokenizer": "edge_ngram_tokenizer",
           "filter": [
-            "english_possessive_stemmer",
             "lowercase",
-            "english_stop",
-            "english_stemmer"
+            "english_stop"
           ]
         }
       }
@@ -56,7 +46,8 @@
             },
             "en": {
               "type": "text",
-              "analyzer": "english"
+              "analyzer": "english_edge_ngram",
+              "search_analyzer": "standard"
             }
           }
         },
@@ -69,7 +60,8 @@
             },
             "en": {
               "type": "text",
-              "analyzer": "english"
+              "analyzer": "english_edge_ngram",
+              "search_analyzer": "standard"
             }
           }
         },

+ 1 - 1
src/client/js/app.js

@@ -294,7 +294,7 @@ const componentMappings = {
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
-  'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addSlashToTheEnd={true} />,
+  'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addTrailingSlash={true} />,
   'rename-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
   'duplicate-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
 

+ 1 - 1
src/client/js/components/InstallerForm.jsx

@@ -43,7 +43,7 @@ class InstallerForm extends React.Component {
           <small>{ this.props.t('installer.initial_account_will_be_administrator_automatically') }</small>
         </p>
 
-        <form role="form" action="/installer/createAdmin" method="post" id="register-form">
+        <form role="form" action="/installer" method="post" id="register-form">
           <div className="input-group m-t-20 m-b-20 mx-auto">
             <div className="radio radio-primary radio-inline">
               <input type="radio" id="radioLangEn" name="registerForm[app:globalLang]" value="en-US"

+ 3 - 1
src/client/js/components/PageAttachment.js

@@ -87,7 +87,9 @@ export default class PageAttachment extends React.Component {
     let deleteAttachmentModal = '';
     if (this.isUserLoggedIn()) {
       const attachmentToDelete = this.state.attachmentToDelete;
-      let deleteModalClose = () => this.setState({ attachmentToDelete: null });
+      let deleteModalClose = () => {
+        this.setState({ attachmentToDelete: null, deleteError: '' });
+      };
       let showModal = attachmentToDelete !== null;
 
       let deleteInUse = null;

+ 26 - 8
src/client/js/components/PageComment/CommentForm.jsx

@@ -33,6 +33,7 @@ export default class CommentForm extends React.Component {
     const isUploadableFile = config.upload.file;
 
     this.state = {
+      isLayoutTypeGrowi: false,
       isFormShown: false,
       comment: '',
       isMarkdown: true,
@@ -60,6 +61,19 @@ export default class CommentForm extends React.Component {
     this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
   }
 
+  componentWillMount() {
+    this.init();
+  }
+
+  init() {
+    if (!this.props.pageId) {
+      return;
+    }
+
+    const layoutType = this.props.crowi.getConfig()['layoutType'];
+    this.setState({isLayoutTypeGrowi: 'crowi-plus' === layoutType || 'growi' === layoutType});
+  }
+
   updateState(value) {
     this.setState({comment: value});
   }
@@ -227,6 +241,8 @@ export default class CommentForm extends React.Component {
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml(): ReactUtils.nl2br(comment);
     const emojiStrategy = this.props.crowi.getEmojiStrategy();
 
+    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
+
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const submitButton = (
       <Button type="submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
@@ -240,15 +256,17 @@ export default class CommentForm extends React.Component {
         <form className="form page-comment-form" id="page-comment-form" onSubmit={this.postComment}>
           { username &&
             <div className="comment-form">
-              <div className="comment-form-user">
-                <a href={creatorsPage}>
-                  <UserPicture user={user} />
-                </a>
-              </div>
+              { isLayoutTypeGrowi &&
+                <div className="comment-form-user">
+                  <a href={creatorsPage}>
+                    <UserPicture user={user} />
+                  </a>
+                </div>
+              }
               <div className="comment-form-main">
                 {/* Add Comment Button */}
                 { !this.state.isFormShown &&
-                  <button className="btn btn-lg btn-link center-block" onClick={this.showCommentFormBtnClickHandler}>
+                  <button className={`btn btn-lg ${isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`} onClick={this.showCommentFormBtnClickHandler}>
                     <i className="icon-bubble"></i> Add Comment
                   </button>
                 }
@@ -263,7 +281,7 @@ export default class CommentForm extends React.Component {
                           editorOptions={this.props.editorOptions}
                           lineNumbers={false}
                           isMobile={this.props.crowi.isMobile}
-                          isUploadable={this.state.isUploadable}
+                          isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi}  // enable only when GROWI layout
                           isUploadableFile={this.state.isUploadableFile}
                           emojiStrategy={emojiStrategy}
                           onChange={this.updateState}
@@ -283,7 +301,7 @@ export default class CommentForm extends React.Component {
                   <div className="comment-submit">
                     <div className="d-flex">
                       <label style={{flex: 1}}>
-                      { this.state.key == 1 &&
+                      { isLayoutTypeGrowi && this.state.key == 1 &&
                         <span>
                           <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateStateCheckbox} /> Markdown
                         </span>

+ 2 - 7
src/client/js/components/PageComments.js

@@ -42,18 +42,13 @@ export default class PageComments extends React.Component {
   }
 
   componentWillMount() {
-    const pageId = this.props.pageId;
-
-    if (pageId) {
-      this.init();
-    }
-
+    this.init();
     this.retrieveData = this.retrieveData.bind(this);
   }
 
   init() {
     if (!this.props.pageId) {
-      return ;
+      return;
     }
 
     const layoutType = this.props.crowi.getConfig()['layoutType'];

+ 13 - 11
src/client/js/components/PageEditor/Editor.jsx

@@ -276,17 +276,19 @@ export default class Editor extends AbstractEditor {
 
         </Dropzone>
 
-        <button type="button" className="btn btn-default btn-block btn-open-dropzone"
-          onClick={() => {this.refs.dropzone.open()}}>
-
-          <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
-          Attach files
-          <span className="desc-long">
-            &nbsp;by dragging &amp; dropping,&nbsp;
-            <span className="btn-link">selecting them</span>,&nbsp;
-            or pasting from the clipboard.
-          </span>
-        </button>
+        { this.props.isUploadable &&
+          <button type="button" className="btn btn-default btn-block btn-open-dropzone"
+            onClick={() => {this.refs.dropzone.open()}}>
+
+            <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
+            Attach files
+            <span className="desc-long">
+              &nbsp;by dragging &amp; dropping,&nbsp;
+              <span className="btn-link">selecting them</span>,&nbsp;
+              or pasting from the clipboard.
+            </span>
+          </button>
+        }
       </div>
     );
   }

+ 5 - 5
src/client/js/components/PagePathAutoComplete.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import * as pagePathUtils from '@commons/util/page-path-utils';
+import * as pathUtils from '@commons/util/path-utils';
 import SearchTypeahead from './SearchTypeahead';
 
 export default class PagePathAutoComplete extends React.Component {
@@ -34,9 +34,9 @@ export default class PagePathAutoComplete extends React.Component {
   }
 
   getKeywordOnInit(path) {
-    return this.props.addSlashToTheEnd
-      ? pagePathUtils.addSlashToTheEnd(path)
-      : pagePathUtils.removeLastSlash(path);
+    return this.props.addTrailingSlash
+      ? pathUtils.addTrailingSlash(path)
+      : pathUtils.removeTrailingSlash(path);
   }
 
   render() {
@@ -59,7 +59,7 @@ export default class PagePathAutoComplete extends React.Component {
 PagePathAutoComplete.propTypes = {
   crowi:            PropTypes.object.isRequired,
   initializedPath:  PropTypes.string,
-  addSlashToTheEnd: PropTypes.bool,
+  addTrailingSlash: PropTypes.bool,
 };
 
 PagePathAutoComplete.defaultProps = {

+ 8 - 6
src/client/js/legacy/crowi.js

@@ -7,12 +7,12 @@ import ReactDOM from 'react-dom';
 
 import { debounce } from 'throttle-debounce';
 
-const pagePathUtils = require('@commons/util/page-path-utils');
 const entities = require('entities');
 const escapeStringRegexp = require('escape-string-regexp');
 require('jquery.cookie');
 require('bootstrap-select');
 
+import * as pathUtils from '@commons/util/path-utils';
 import GrowiRenderer from '../util/GrowiRenderer';
 import RevisionLoader from '../components/Page/RevisionLoader';
 
@@ -343,7 +343,7 @@ $(function() {
     if (name.match(/.+\/$/)) {
       name = name.substr(0, name.length - 1);
     }
-    top.location.href = pagePathUtils.encodePagePath(name) + '#edit';
+    top.location.href = pathUtils.encodePagePath(name) + '#edit';
     return false;
   });
 
@@ -356,7 +356,7 @@ $(function() {
     // create name-value map
     let nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap['q'] is renamed page path
+      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is renamed page path
     });
 
     const data = $(this).serialize() + `&socketClientId=${crowi.getSocketClientId()}`;
@@ -370,10 +370,11 @@ $(function() {
     .done(function(res) {
       // error
       if (!res.ok) {
+        const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
         $('#renamePage .msg, #unportalize .msg').hide();
         $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
         $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
-          <a href="${nameValueMap.new_path}">${nameValueMap.new_path} <i class="icon-login"></i></a>
+          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
         `);
       }
       else {
@@ -394,7 +395,7 @@ $(function() {
     // create name-value map
     let nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap['q'] is duplicated page path
+      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is duplicated page path
     });
 
     $.ajax({
@@ -405,10 +406,11 @@ $(function() {
     }).done(function(res) {
       // error
       if (!res.ok) {
+        const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
         $('#duplicatePage .msg').hide();
         $(`#duplicatePage .msg-${res.code}`).show();
         $('#duplicatePage #linkToNewPage').html(`
-          <a href="${nameValueMap.q}">${nameValueMap.q} <i class="icon-login"></i></a>
+          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
         `);
       }
       else {

+ 13 - 8
src/client/styles/scss/_layout_crowi_sidebar.scss

@@ -88,22 +88,27 @@
       .page-comment-form {
         margin-top: 16px;
 
-        .comment-form {
-        }
-
         .comment-form-main {
-
-          .comment-form-comment {
-            height: 60px;
+          .navbar-editor button {
+            padding: 5px;
+            font-size: 12px;
+          }
+          .overlay-gfm-cheatsheet {
+            display: none;  // hide cheatsheet
+          }
+          .CodeMirror {
+            height: 150px;
           }
-
           .comment-submit {
             margin-top: 8px;
-            text-align: right;
           }
         }
       }
 
+      hr {
+        border-color: #ccc;
+      }
+
       .page-comments-list {
         .page-comment {
           margin-top: 8px;

+ 0 - 57
src/lib/util/page-path-utils.js

@@ -1,57 +0,0 @@
-'use strict';
-
-function encodePagesPath(pages) {
-  pages.forEach(function(page) {
-    if (!page.path) {
-      return;
-    }
-    page.path = encodePagePath(page.path);
-  });
-  return pages;
-}
-
-function encodePagePath(path) {
-  const paths = path.split('/');
-  paths.forEach(function(item, index) {
-    paths[index] = encodeURIComponent(item);
-  });
-  return paths.join('/');
-}
-
-function matchEndWithSlash(path) {
-  // https://regex101.com/r/Z21fEd/1
-  return path.match(/(.+?)(\/)?$/);
-}
-
-function isEndWithSlash(path) {
-  const match = matchEndWithSlash(path);
-  return (match[2] != null);
-}
-
-function addSlashToTheEnd(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  if (!isEndWithSlash(path)) {
-    return `${path}/`;
-  }
-  return path;
-}
-
-function removeLastSlash(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  const match = matchEndWithSlash(path);
-  return match[1];
-}
-
-module.exports = {
-  encodePagePath,
-  encodePagesPath,
-  isEndWithSlash,
-  addSlashToTheEnd,
-  removeLastSlash,
-};

+ 84 - 0
src/lib/util/path-utils.js

@@ -0,0 +1,84 @@
+'use strict';
+
+function encodePagesPath(pages) {
+  pages.forEach(function(page) {
+    if (!page.path) {
+      return;
+    }
+    page.path = encodePagePath(page.path);
+  });
+  return pages;
+}
+
+function encodePagePath(path) {
+  const paths = path.split('/');
+  paths.forEach(function(item, index) {
+    paths[index] = encodeURIComponent(item);
+  });
+  return paths.join('/');
+}
+
+function matchSlashes(path) {
+  // https://regex101.com/r/Z21fEd/5
+  return path.match(/^((\/+)?(.+?))(\/+)?$/);
+}
+
+function hasHeadingSlash(path) {
+  const match = matchSlashes(path);
+  return (match[2] != null);
+}
+
+function hasTrailingSlash(path) {
+  const match = matchSlashes(path);
+  return (match[4] != null);
+}
+
+function addHeadingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  if (!hasHeadingSlash(path)) {
+    return `/${path}`;
+  }
+  return path;
+}
+
+function addTrailingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  if (!hasTrailingSlash(path)) {
+    return `${path}/`;
+  }
+  return path;
+}
+
+function removeTrailingSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  const match = matchSlashes(path);
+  return match[1];
+}
+
+function normalizePath(path) {
+  const match = matchSlashes(path);
+  if (match == null) {
+    return '/';
+  }
+  return `/${match[3]}`;
+}
+
+module.exports = {
+  encodePagePath,
+  encodePagesPath,
+  hasHeadingSlash,
+  hasTrailingSlash,
+  addHeadingSlash,
+  addTrailingSlash,
+  removeTrailingSlash,
+  normalizePath,
+};

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

@@ -549,16 +549,6 @@ module.exports = function(crowi) {
     return grantLabels;
   };
 
-  pageSchema.statics.normalizePath = function(path) {
-    if (!path.match(/^\//)) {
-      path = '/' + path;
-    }
-
-    path = path.replace(/\/\s+?/g, '/').replace(/\s+\//g, '/');
-
-    return path;
-  };
-
   pageSchema.statics.getUserPagePath = function(user) {
     return '/user/' + user.username;
   };

+ 5 - 0
src/server/models/user.js

@@ -270,6 +270,11 @@ module.exports = function(crowi) {
     });
   };
 
+  userSchema.methods.asyncMakeAdmin = async function(callback) {
+    this.admin = 1;
+    return this.save();
+  };
+
   userSchema.methods.statusActivate = function(callback) {
     debug('Activate User', this);
     this.status = STATUS_ACTIVE;

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

@@ -32,8 +32,8 @@ module.exports = function(crowi, app) {
 
   app.get('/'                        , middleware.applicationInstalled(), loginRequired(crowi, app, false) , page.showTopPage);
 
-  app.get('/installer'               , middleware.applicationNotInstalled() , middleware.checkSearchIndicesGenerated(crowi, app) , installer.index);
-  app.post('/installer/createAdmin'  , middleware.applicationNotInstalled() , form.register , csrf, installer.createAdmin);
+  app.get('/installer'               , middleware.applicationNotInstalled() , installer.index);
+  app.post('/installer'              , middleware.applicationNotInstalled() , form.register , csrf, installer.install);
   //app.post('/installer/user'         , middleware.applicationNotInstalled() , installer.createFirstUser);
 
   app.get('/login/error/:reason'     , login.error);

+ 55 - 33
src/server/routes/installer.js

@@ -11,28 +11,50 @@ module.exports = function(crowi, app) {
 
   const actions = {};
 
-  function createInitialPages(owner, lang) {
+  async function initSearchIndex() {
+    const search = crowi.getSearcher();
+    if (search == null) {
+      return;
+    }
+
+    await search.deleteIndex();
+    await search.buildIndex();
+    await search.addAllPages();
+  }
+
+  async function createInitialPages(owner, lang) {
+    const promises = [];
+
     // create portal page for '/'
     const welcomeMarkdownPath = path.join(crowi.localeDir, lang, 'welcome.md');
     const welcomeMarkdown = fs.readFileSync(welcomeMarkdownPath);
-    Page.create('/', welcomeMarkdown, owner, {});
+    promises.push(Page.create('/', welcomeMarkdown, owner, {}));
 
     // create /Sandbox
     const sandboxMarkdownPath = path.join(crowi.localeDir, lang, 'sandbox.md');
     const sandboxMarkdown = fs.readFileSync(sandboxMarkdownPath);
-    Page.create('/Sandbox', sandboxMarkdown, owner, {});
+    promises.push(Page.create('/Sandbox', sandboxMarkdown, owner, {}));
 
     // create /Sandbox/Bootstrap3
     const bs3MarkdownPath = path.join(crowi.localeDir, 'en-US', 'sandbox-bootstrap3.md');
     const bs3Markdown = fs.readFileSync(bs3MarkdownPath);
-    Page.create('/Sandbox/Bootstrap3', bs3Markdown, owner, {});
+    promises.push(Page.create('/Sandbox/Bootstrap3', bs3Markdown, owner, {}));
+
+    await Promise.all(promises);
+
+    try {
+      await initSearchIndex();
+    }
+    catch (err) {
+      logger.error('Failed to build Elasticsearch Indices', err);
+    }
   }
 
   actions.index = function(req, res) {
     return res.render('installer');
   };
 
-  actions.createAdmin = function(req, res, next) {
+  actions.install = async function(req, res, next) {
     const registerForm = req.body.registerForm || {};
 
     if (!req.form.isValid) {
@@ -45,39 +67,39 @@ module.exports = function(crowi, app) {
     const password = registerForm.password;
     const language = registerForm['app:globalLang'] || 'en-US';
 
-    User.createUserByEmailAndPassword(name, username, email, password, language, function(err, userData) {
+    let adminUser;
+    try {
+      adminUser = await User.createUser(name, username, email, password, language);
+      await adminUser.asyncMakeAdmin();
+    }
+    catch (err) {
+      req.form.errors.push('管理ユーザーの作成に失敗しました。' + err.message);
+      return res.render('installer');
+    }
+
+    Config.applicationInstall(function(err, configs) {
       if (err) {
-        req.form.errors.push('管理ユーザーの作成に失敗しました。' + err.message);
-        // TODO
-        return res.render('installer');
+        logger.error(err);
+        return;
       }
 
-      userData.makeAdmin(function(err, userData) {
-        Config.applicationInstall(function(err, configs) {
-          if (err) {
-            logger.error(err);
-            return;
-          }
-
-          // save the globalLang config, and update the config cache
-          Config.updateNamespaceByArray('crowi', {'app:globalLang': language}, function(err, config) {
-            Config.updateConfigCache('crowi', config);
-          });
-
-          // login with passport
-          req.logIn(userData, (err) => {
-            if (err) { return next() }
-            else {
-              req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
-              return res.redirect('/admin/app');
-            }
-          });
-        });
-
-        // create initial pages
-        createInitialPages(userData, language);
+      // save the globalLang config, and update the config cache
+      Config.updateNamespaceByArray('crowi', {'app:globalLang': language}, function(err, config) {
+        Config.updateConfigCache('crowi', config);
+      });
+
+      // login with passport
+      req.logIn(adminUser, (err) => {
+        if (err) { return next() }
+        else {
+          req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
+          return res.redirect('/admin/app');
+        }
       });
     });
+
+    // create initial pages
+    await createInitialPages(adminUser, language);
   };
 
   return actions;

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

@@ -3,7 +3,7 @@ module.exports = function(crowi, app) {
 
   const debug = require('debug')('growi:routes:page')
     , logger = require('@alias/logger')('growi:routes:page')
-    , pagePathUtils = require('@commons/util/page-path-utils')
+    , pathUtils = require('@commons/util/path-utils')
     , Page = crowi.model('Page')
     , User = crowi.model('User')
     , Config   = crowi.model('Config')
@@ -48,8 +48,7 @@ module.exports = function(crowi, app) {
   }
 
   function getPathFromRequest(req) {
-    const path = '/' + (req.params[0] || '');
-    return path.replace(/\.md$/, '');
+    return pathUtils.normalizePath(req.params[0] || '');
   }
 
   function isUserPage(path) {
@@ -153,7 +152,7 @@ module.exports = function(crowi, app) {
       seener_threshold: SEENER_THRESHOLD,
     };
     renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
-    renderVars.pages = pagePathUtils.encodePagesPath(result.pages);
+    renderVars.pages = pathUtils.encodePagesPath(result.pages);
   }
 
   function replacePlaceholdersOfTemplate(template, req) {
@@ -186,7 +185,7 @@ module.exports = function(crowi, app) {
   }
 
   async function showPageListForCrowiBehavior(req, res, next) {
-    const portalPath = Page.addSlashOfEnd(getPathFromRequest(req));
+    const portalPath = pathUtils.addTrailingSlash(getPathFromRequest(req));
     const revisionId = req.query.revision;
 
     // check whether this page has portal page
@@ -233,7 +232,7 @@ module.exports = function(crowi, app) {
     }
     else if (page.redirectTo) {
       debug(`Redirect to '${page.redirectTo}'`);
-      return res.redirect(encodeURI(page.redirectTo + '?redirectFrom=' + pagePathUtils.encodePagePath(path)));
+      return res.redirect(encodeURI(page.redirectTo + '?redirectFrom=' + pathUtils.encodePagePath(path)));
     }
 
     logger.debug('Page is found when processing pageShowForGrowiBehavior', page._id, page.path);
@@ -329,12 +328,12 @@ module.exports = function(crowi, app) {
 
     // check whether this page has portal page
     if (!behaviorType || 'crowi' === behaviorType) {
-      const portalPagePath = Page.addSlashOfEnd(getPathFromRequest(req));
+      const portalPagePath = pathUtils.addTrailingSlash(getPathFromRequest(req));
       let hasPortalPage = await Page.count({ path: portalPagePath }) > 0;
 
       if (hasPortalPage) {
         logger.debug('The portal page is found', portalPagePath);
-        return res.redirect(encodeURI(portalPagePath + '?redirectFrom=' + pagePathUtils.encodePagePath(req.path)));
+        return res.redirect(encodeURI(portalPagePath + '?redirectFrom=' + pathUtils.encodePagePath(req.path)));
       }
     }
 
@@ -391,10 +390,12 @@ module.exports = function(crowi, app) {
   actions.notFound = async function(req, res) {
     const path = getPathFromRequest(req);
 
+    const isCreatable = Page.isCreatableName(path);
+
     let view;
     const renderVars = { path };
 
-    if (req.isForbidden) {
+    if (!isCreatable || req.isForbidden) {
       view = 'customlayout-selector/forbidden';
     }
     else {
@@ -446,7 +447,7 @@ module.exports = function(crowi, app) {
     }
 
     renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
-    renderVars.pages = pagePathUtils.encodePagesPath(result.pages);
+    renderVars.pages = pathUtils.encodePagesPath(result.pages);
     res.render('customlayout-selector/page_list', renderVars);
 
   };
@@ -460,7 +461,7 @@ module.exports = function(crowi, app) {
     const page = await Page.findByIdAndViewer(id, req.user);
 
     if (page != null) {
-      return res.redirect(pagePathUtils.encodePagePath(page.path));
+      return res.redirect(pathUtils.encodePagePath(page.path));
     }
 
     return res.redirect('/');
@@ -510,7 +511,7 @@ module.exports = function(crowi, app) {
         result.pages.pop();
       }
 
-      result.pages = pagePathUtils.encodePagesPath(result.pages);
+      result.pages = pathUtils.encodePagesPath(result.pages);
       return res.json(ApiResponse.success(result));
     }
     catch (err) {
@@ -964,7 +965,7 @@ module.exports = function(crowi, app) {
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
-    const newPagePath = Page.normalizePath(req.body.new_path);
+    const newPagePath = pathUtils.normalizePath(req.body.new_path);
     const options = {
       createRedirectPage: req.body.create_redirect || 0,
       moveUnderTrees: req.body.move_trees || 0,
@@ -1028,7 +1029,7 @@ module.exports = function(crowi, app) {
    */
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
-    const newPagePath = Page.normalizePath(req.body.new_path);
+    const newPagePath = pathUtils.normalizePath(req.body.new_path);
 
     const page = await Page.findByIdAndViewer(pageId, req.user);
 
@@ -1090,7 +1091,7 @@ module.exports = function(crowi, app) {
 
     try {
       let result = await Page.findListByCreator(page.creator, req.user, queryOptions);
-      result.pages = pagePathUtils.encodePagesPath(result.pages);
+      result.pages = pathUtils.encodePagesPath(result.pages);
 
       return res.json(ApiResponse.success(result));
     }

+ 4 - 3
src/server/service/config-manager.js

@@ -1,5 +1,6 @@
-const ConfigLoader = require('../service/config-loader')
-  , debug = require('debug')('growi:service:ConfigManager');
+const debug = require('debug')('growi:service:ConfigManager');
+const pathUtils = require('@commons/util/path-utils');
+const ConfigLoader = require('../service/config-loader');
 
 const KEYS_FOR_SAML_USE_ONLY_ENV_OPTION = [
   'security:passport-saml:isEnabled',
@@ -80,7 +81,7 @@ class ConfigManager {
   getSiteUrl() {
     const siteUrl = this.getConfig('crowi', 'app:siteUrl');
     if (siteUrl != null) {
-      return siteUrl;
+      return pathUtils.removeTrailingSlash(siteUrl);
     }
     else {
       return '[The site URL is not set. Please set it!]';

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

@@ -22,7 +22,13 @@ module.exports = function(crowi) {
   AttachmentFile.promisifiedWrite = util.promisify(AttachmentFile.write).bind(AttachmentFile);
 
   lib.deleteFile = async function(attachment) {
-    const attachmentFile = await AttachmentFile.findOne({ filename: attachment.fileName });
+    let filenameValue = attachment.fileName;
+
+    if (attachment.filePath != null) {  // backward compatibility for v3.3.x or below
+      filenameValue = attachment.filePath;
+    }
+
+    const attachmentFile = await AttachmentFile.findOne({ filename: filenameValue });
 
     AttachmentFile.unlinkById(attachmentFile._id, function(error, unlinkedFile) {
       if (error) {

+ 5 - 4
src/server/service/passport.js

@@ -1,4 +1,5 @@
 const debug = require('debug')('growi:service:PassportService');
+const urljoin = require('url-join');
 const passport = require('passport');
 const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
@@ -312,7 +313,7 @@ class PassportService {
       clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
       clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
       callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-        ? `${this.crowi.configManager.getSiteUrl()}/passport/google/callback`                               // auto-generated with v3.2.4 and above
+        ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/google/callback')                       // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -359,7 +360,7 @@ class PassportService {
       clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
       clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
       callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-        ? `${this.crowi.configManager.getSiteUrl()}/passport/github/callback`                               // auto-generated with v3.2.4 and above
+        ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/github/callback')                       // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -406,7 +407,7 @@ class PassportService {
       consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
       consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
       callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-        ? `${this.crowi.configManager.getSiteUrl()}/passport/twitter/callback`                               // auto-generated with v3.2.4 and above
+        ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/twitter/callback')                       // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,   // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
@@ -452,7 +453,7 @@ class PassportService {
     passport.use(new SamlStrategy({
       entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
       callbackUrl: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
-        ? `${this.crowi.configManager.getSiteUrl()}/passport/saml/callback`          // auto-generated with v3.2.4 and above
+        ? urljoin(this.crowi.configManager.getSiteUrl(), '/passport/saml/callback')  // auto-generated with v3.2.4 and above
         : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'),    // DEPRECATED: backward compatible with v3.2.3 and below
       issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
       cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),

+ 12 - 10
src/server/util/googleAuth.js

@@ -1,3 +1,7 @@
+const debug = require('debug')('growi:lib:googleAuth');
+const urljoin = require('url-join');
+const { GoogleApis } = require('googleapis');
+
 /**
  * googleAuth utility
  */
@@ -5,9 +9,7 @@
 module.exports = function(crowi) {
   'use strict';
 
-  const { GoogleApis } = require('googleapis');
-  var google = new GoogleApis()
-    , debug = require('debug')('growi:lib:googleAuth')
+  const google = new GoogleApis()
     , config = crowi.getConfig()
     , lib = {}
     ;
@@ -21,11 +23,11 @@ module.exports = function(crowi) {
   }
 
   lib.createAuthUrl = function(req, callback) {
-    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
-    var oauth2Client = createOauth2Client(callbackUrl);
+    const callbackUrl = urljoin(crowi.configManager.getSiteUrl(), '/google/callback');
+    const oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
 
-    var redirectUrl = oauth2Client.generateAuthUrl({
+    const redirectUrl = oauth2Client.generateAuthUrl({
       access_type: 'offline',
       scope: ['profile', 'email'],
     });
@@ -34,11 +36,11 @@ module.exports = function(crowi) {
   };
 
   lib.handleCallback = function(req, callback) {
-    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
-    var oauth2Client = createOauth2Client(callbackUrl);
+    const callbackUrl = urljoin(crowi.configManager.getSiteUrl(), '/google/callback');
+    const oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
 
-    var code = req.session.googleAuthCode || null;
+    const code = req.session.googleAuthCode || null;
 
     if (!code) {
       return callback(new Error('No code exists.'), null);
@@ -53,7 +55,7 @@ module.exports = function(crowi) {
 
       oauth2Client.credentials = tokens;
 
-      var oauth2 = google.oauth2('v2');
+      const oauth2 = google.oauth2('v2');
       oauth2.userinfo.get({}, function(err, response) {
         debug('Response of oauth2.userinfo.get', err, response);
         if (err) {

+ 5 - 34
src/server/util/middlewares.js

@@ -1,8 +1,10 @@
 const debug = require('debug')('growi:lib:middlewares');
 const logger = require('@alias/logger')('growi:lib:middlewares');
+const pathUtils = require('@commons/util/path-utils');
 const md5 = require('md5');
 const entities = require('entities');
 
+
 exports.csrfKeyGenerator = function(crowi, app) {
   return function(req, res, next) {
     var csrfKey = (req.session && req.session.id) || 'anon';
@@ -145,12 +147,8 @@ exports.swigFilters = function(crowi, app, swig) {
         .replace(/\n/g, '<br>');
     });
 
-    swig.setFilter('removeLastSlash', function(string) {
-      if (string == '/') {
-        return string;
-      }
-
-      return string.substr(0, string.length - 1);
+    swig.setFilter('removeTrailingSlash', function(string) {
+      return pathUtils.removeTrailingSlash(string);
     });
 
     swig.setFilter('presentation', function(string) {
@@ -284,7 +282,7 @@ exports.accessTokenParser = function(crowi, app) {
 // this is for Installer
 exports.applicationNotInstalled = function() {
   return function(req, res, next) {
-    var config = req.config;
+    const config = req.config;
 
     if (Object.keys(config.crowi).length !== 0) {
       req.flash('errorMessage', 'Application already installed.');
@@ -295,33 +293,6 @@ exports.applicationNotInstalled = function() {
   };
 };
 
-exports.checkSearchIndicesGenerated = function(crowi, app) {
-  return function(req, res, next) {
-    const searcher = crowi.getSearcher();
-
-    // build index
-    if (searcher) {
-      searcher.buildIndex()
-        .then((data) => {
-          if (!data.errors) {
-            debug('Index created.');
-          }
-          return searcher.addAllPages();
-        })
-        .catch((error) => {
-          if (error.message != null && error.message.match(/index_already_exists_exception/)) {
-            debug('Creating index is failed: ', error.message);
-          }
-          else {
-            console.log(`Error while building index of Elasticsearch': ${error.message}`);
-          }
-        });
-    }
-
-    return next();
-  };
-};
-
 exports.applicationInstalled = function() {
   return function(req, res, next) {
     var config = req.config;

+ 15 - 5
src/server/util/search.js

@@ -351,11 +351,14 @@ SearchClient.prototype.search = async function(query) {
         query: query.body.query
       },
     });
-    logger.info('ES returns explanations: ', result.explanations);
+    logger.debug('ES returns explanations: ', result.explanations);
   }
 
   const result = await this.client.search(query);
 
+  // for debug
+  logger.debug('ES result: ', result);
+
   return {
     meta: {
       took: result.took,
@@ -447,6 +450,7 @@ SearchClient.prototype.appendCriteriaForQueryString = function(query, queryStrin
     const q = {
       multi_match: {
         query: parsedKeywords.match.join(' '),
+        type: 'most_fields',
         fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en'],
       },
     };
@@ -457,7 +461,7 @@ SearchClient.prototype.appendCriteriaForQueryString = function(query, queryStrin
     const q = {
       multi_match: {
         query: parsedKeywords.not_match.join(' '),
-        fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en'],
+        fields: ['path.ja', 'path.en', 'body.ja', 'body.en'],
         operator: 'or'
       },
     };
@@ -624,13 +628,19 @@ SearchClient.prototype.filterPagesByType = function(query, type) {
   }
 };
 
-SearchClient.prototype.appendFunctionScore = function(query) {
+SearchClient.prototype.appendFunctionScore = function(query, queryString) {
   const User = this.crowi.model('User');
   const count = User.count({}) || 1;
-  // newScore = oldScore + log(1 + factor * 'bookmark_count')
+
+  const minScore = queryString.length * 0.1 - 1;    // increase with length
+  logger.debug('min_score: ', minScore);
+
   query.body.query = {
     function_score: {
       query: { ...query.body.query },
+      //// disable min_score -- 2019.02.28 Yuki Takei
+      //// more precise adjustment is needed...
+      // min_score: minScore,
       field_value_factor: {
         field: 'bookmark_count',
         modifier: 'log1p',
@@ -654,7 +664,7 @@ SearchClient.prototype.searchKeyword = async function(queryString, user, userGro
 
   this.appendResultSize(query, from, size);
 
-  this.appendFunctionScore(query);
+  this.appendFunctionScore(query, queryString);
 
   return this.search(query);
 };

+ 9 - 7
src/server/util/slack.js

@@ -1,3 +1,6 @@
+const debug = require('debug')('growi:util:slack');
+const urljoin = require('url-join');
+
 /**
  * slack
  */
@@ -5,8 +8,7 @@
 module.exports = function(crowi) {
   'use strict';
 
-  const debug = require('debug')('growi:util:slack'),
-    config = crowi.getConfig(),
+  const config = crowi.getConfig(),
     Config = crowi.model('Config'),
     Slack = require('slack-node'),
     slack = {};
@@ -123,10 +125,10 @@ module.exports = function(crowi) {
     const attachment = {
       color: '#263a3c',
       author_name: '@' + user.username,
-      author_link: url + '/user/' + user.username,
+      author_link: urljoin(url, 'user', user.username),
       author_icon: user.image,
       title: page.path,
-      title_link: url + '/' + page._id,
+      title_link: urljoin(url, page._id),
       text: body,
       mrkdwn_in: ['text'],
     };
@@ -151,7 +153,7 @@ module.exports = function(crowi) {
     const attachment = {
       color: '#263a3c',
       author_name: '@' + user.username,
-      author_link: url + '/user/' + user.username,
+      author_link: urljoin(url, 'user', user.username),
       author_icon: user.image,
       text: body,
       mrkdwn_in: ['text'],
@@ -174,7 +176,7 @@ module.exports = function(crowi) {
     let text;
     const url = crowi.configManager.getSiteUrl();
 
-    const pageUrl = `<${url}${path}|${path}>`;
+    const pageUrl = `<${urljoin(url, path)}|${path}>`;
     if (updateType == 'create') {
       text = `:rocket: ${user.username} created a new page! ${pageUrl}`;
     }
@@ -187,7 +189,7 @@ module.exports = function(crowi) {
 
   const getSlackMessageTextForComment = function(path, user) {
     const url = crowi.configManager.getSiteUrl();
-    const pageUrl = `<${url}${path}|${path}>`;
+    const pageUrl = `<${urljoin(url, path)}|${path}>`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 
     return text;

+ 6 - 27
src/server/views/layout-crowi/widget/page_side_content.html

@@ -12,33 +12,12 @@
 
 <h3><i class="icon-bubble"></i> Comments</h3>
 <div class="page-comments">
-  <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
-    <div class="comment-form">
-      <div class="comment-form-main">
-        <div class="comment-write" id="comment-write">
-          <textarea class="comment-form-comment form-control" id="comment-form-comment" name="commentForm[comment]" {% if not user %}disabled{% endif %}></textarea>
-        </div>
-        <div class="comment-submit">
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <input type="hidden" name="commentForm[page_id]" value="{{ page._id.toString() }}">
-          <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
-          <span class="text-danger" id="comment-form-message"></span>
-          <input type="submit" id="comment-form-button" value="Comment" class="btn btn-primary btn-sm form-inline" {% if not user %}disabled{% endif %}>
-        </div>
-      </div>
-    </div>
-  </form>
-  <div id="page-comment-form-behavior"></div>
-
-  <div class="page-comments-list" id="page-comments-list">
-    <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
-
-    <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="ti-angle-double-up"></i> Comments for Newer Revision <i class="ti-angle-double-up"></i></a>
+  {% if page and not page.isDeleted() %}
+  <div id="page-comment-write"></div>
+  <hr>
+  {% endif %}
 
-    <div class="page-comments-list-current" id="page-comments-list-current"></div>
-
-    <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="ti-angle-double-down"></i> Comments for Older Revision <i class="ti-angle-double-down"></i></a>
+  <div id="page-comment-form-behavior"></div>
 
-    <div class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
-  </div>
+  <div class="page-comments-list" id="page-comments-list"></div>
 </div>

+ 1 - 13
src/server/views/layout-growi/widget/comments.html

@@ -4,19 +4,7 @@
 
     <h4><i class="icon-fw icon-bubbles"></i> Comments</h4>
 
-    <div class="page-comments-list" id="page-comments-list">
-      {# transplanted to PageComments React component -- 2017.06.02 Yuki Takei
-      <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
-
-      <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="ti-angle-double-up"></i> Comments for Newer Revision <i class="ti-angle-double-up"></i></a>
-
-      <div class="page-comments-list-current" id="page-comments-list-current"></div>
-
-      <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="ti-angle-double-down"></i> Comments for Older Revision <i class="ti-angle-double-down"></i></a>
-
-      <div class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
-      #}
-    </div>
+    <div class="page-comments-list" id="page-comments-list"></div>
 
     {% if page and not page.isDeleted() %}
     <div id="page-comment-write"></div>

+ 1 - 13
src/server/views/layout-kibela/widget/comments.html

@@ -4,19 +4,7 @@
 
       <h4><i class="icon-fw icon-bubbles"></i> Comments</h4>
 
-      <div class="page-comments-list" id="page-comments-list">
-        {# transplanted to PageComments React component -- 2017.06.02 Yuki Takei
-        <div class="page-comments-list-newer collapse" id="page-comments-list-newer"></div>
-
-        <a class="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer"><i class="ti-angle-double-up"></i> Comments for Newer Revision <i class="ti-angle-double-up"></i></a>
-
-        <div class="page-comments-list-current" id="page-comments-list-current"></div>
-
-        <a class="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older"><i class="ti-angle-double-down"></i> Comments for Older Revision <i class="ti-angle-double-down"></i></a>
-
-        <div class="page-comments-list-older collapse in" id="page-comments-list-older"></div>
-        #}
-      </div>
+      <div class="page-comments-list" id="page-comments-list"></div>
 
       {% if page and not page.isDeleted() %}
       <div id="page-comment-write"></div>

+ 4 - 4
src/server/views/modal/what_is_portal.html

@@ -67,12 +67,12 @@
 
         <strong>Warning!</strong><br>
 
-        <p>既に <strong><a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a></strong> のページが存在します。</p>
+        <p>既に <strong><a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a></strong> のページが存在します。</p>
 
         <p>
-          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> をポータル化するには、
-          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> に移動し、「ページを移動」させてください。<br>
-          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> とは別に、このページ(<code>{{ path }}</code>)にポータルを作成する場合、このまま編集を続けて作成してください。
+          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> をポータル化するには、
+          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> に移動し、「ページを移動」させてください。<br>
+          <a href="{{ path|removeTrailingSlash }}">{{ path|removeTrailingSlash }}</a> とは別に、このページ(<code>{{ path }}</code>)にポータルを作成する場合、このまま編集を続けて作成してください。
         </p>
 
       </div>

+ 0 - 14
src/test/models/page.test.js

@@ -267,20 +267,6 @@ describe('Page', () => {
     });
   });
 
-  describe('Normalize path', () => {
-    context('Normalize', () => {
-      it('should start with slash', done => {
-        expect(Page.normalizePath('hoge/fuga')).to.equal('/hoge/fuga');
-        done();
-      });
-
-      it('should trim spaces of slash', done => {
-        expect(Page.normalizePath('/ hoge / fuga')).to.equal('/hoge/fuga');
-        done();
-      });
-    });
-  });
-
   describe('.findPage', () => {
     context('findByIdAndViewer', () => {
       it('should find page (public)', async() => {

+ 32 - 0
src/test/util/path-utils.test.js

@@ -0,0 +1,32 @@
+const chai = require('chai')
+  , expect = chai.expect
+  , sinonChai = require('sinon-chai')
+  ;
+chai.use(sinonChai);
+
+const pathUtils = require('@commons/util/path-utils');
+
+describe('page-utils', () => {
+
+  describe('.normalizePath', () => {
+    it('should rurn root path with empty string', done => {
+      expect(pathUtils.normalizePath('')).to.equal('/');
+      done();
+    });
+
+    it('should add heading slash', done => {
+      expect(pathUtils.normalizePath('hoge/fuga')).to.equal('/hoge/fuga');
+      done();
+    });
+
+    it('should remove trailing slash', done => {
+      expect(pathUtils.normalizePath('/hoge/fuga/')).to.equal('/hoge/fuga');
+      done();
+    });
+
+    it('should remove unnecessary slashes', done => {
+      expect(pathUtils.normalizePath('//hoge/fuga//')).to.equal('/hoge/fuga');
+      done();
+    });
+  });
+});

Разница между файлами не показана из-за своего большого размера
+ 232 - 278
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов