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

Merge remote-tracking branch 'origin/inprv/better-input-on-mobile' into feat/Markdown-editor-for-comment

# Conflicts:
#	lib/routes/comment.js
sou 7 лет назад
Родитель
Сommit
90cfbbcbec
100 измененных файлов с 2104 добавлено и 1791 удалено
  1. 7 0
      .eslintignore
  2. 18 11
      .eslintrc.js
  3. 0 1
      .gitignore
  4. 4 0
      .vscode/settings.json
  5. 18 1
      CHANGES.md
  6. 10 0
      app.js
  7. 4 0
      app.json
  8. 1 1
      bin/generate-plugin-definitions-source.js
  9. 0 73
      bin/migration/0.0.1-0.0.2-convert_embedded_object_to_schema.js
  10. 0 25
      bin/password-hash-generator.js
  11. 0 112
      bin/revision-string-replacer.js
  12. 0 126
      bin/search.js
  13. 1 1
      bin/shrink-emojione-strategy.js
  14. 0 44
      bin/util.js
  15. 1 1
      config/env.dev.js
  16. 0 2
      config/helpers.js
  17. 10 5
      config/webpack.common.js
  18. 1 1
      config/webpack.dev.js
  19. 1 1
      config/webpack.prod.js
  20. 9 8
      lib/crowi/express-init.js
  21. 37 44
      lib/crowi/index.js
  22. 2 5
      lib/events/page.js
  23. 1 1
      lib/form/admin/securityPassportLdap.js
  24. 1 1
      lib/form/revision.js
  25. 6 5
      lib/locales/en-US/translation.json
  26. 6 5
      lib/locales/ja/translation.json
  27. 9 9
      lib/models/attachment.js
  28. 1 1
      lib/models/comment.js
  29. 64 101
      lib/models/config.js
  30. 2 2
      lib/models/external-account.js
  31. 12 9
      lib/models/page-group-relation.js
  32. 155 137
      lib/models/page.js
  33. 9 6
      lib/models/revision.js
  34. 15 21
      lib/models/updatePost.js
  35. 6 6
      lib/models/user-group-relation.js
  36. 8 8
      lib/models/user-group.js
  37. 38 36
      lib/models/user.js
  38. 2 2
      lib/plugins/plugin-utils-v2.js
  39. 1 1
      lib/plugins/plugin-utils.js
  40. 97 80
      lib/routes/admin.js
  41. 9 8
      lib/routes/attachment.js
  42. 4 3
      lib/routes/bookmark.js
  43. 3 3
      lib/routes/comment.js
  44. 4 2
      lib/routes/index.js
  45. 3 2
      lib/routes/installer.js
  46. 22 19
      lib/routes/login-passport.js
  47. 45 39
      lib/routes/login.js
  48. 32 25
      lib/routes/me.js
  49. 75 51
      lib/routes/page.js
  50. 7 4
      lib/routes/revision.js
  51. 3 2
      lib/routes/search.js
  52. 8 6
      lib/routes/user.js
  53. 4 8
      lib/service/notification.js
  54. 12 4
      lib/service/passport.js
  55. 6 5
      lib/util/apiResponse.js
  56. 1 1
      lib/util/formUtil.js
  57. 31 5
      lib/util/interceptor-manager.js
  58. 11 10
      lib/util/mailer.js
  59. 16 11
      lib/util/middlewares.js
  60. 47 60
      lib/util/search.js
  61. 21 17
      lib/util/slack.js
  62. 48 44
      lib/util/swigFunctions.js
  63. 1 1
      lib/util/xss.js
  64. 35 7
      lib/views/_form.html
  65. 12 5
      lib/views/admin/customize.html
  66. 4 4
      lib/views/admin/markdown.html
  67. 9 5
      lib/views/admin/widget/passport/ldap.html
  68. 10 0
      lib/views/layout-growi/not_found.html
  69. 23 0
      lib/views/modal/select_grant_group.html
  70. 1 0
      lib/views/widget/page_modals.html
  71. 10 2
      lib/views/widget/page_tabs.html
  72. 34 3
      lib/views/widget/passport/ldap-association-tester.html
  73. 12 11
      local_modules/crowi-fileupload-aws/index.js
  74. 1 1
      local_modules/crowi-fileupload-local/index.js
  75. 67 63
      package.json
  76. 41 6
      resource/js/app.js
  77. 2 2
      resource/js/components/Admin/CustomCssEditor.js
  78. 2 2
      resource/js/components/Admin/CustomHeaderEditor.js
  79. 2 2
      resource/js/components/Admin/CustomScriptEditor.js
  80. 2 1
      resource/js/components/BookmarkButton.js
  81. 2 2
      resource/js/components/CopyButton.js
  82. 2 3
      resource/js/components/HeaderSearchBox/SearchForm.js
  83. 1 7
      resource/js/components/NewPageNameInputter.js
  84. 6 4
      resource/js/components/Page.js
  85. 2 2
      resource/js/components/Page/RevisionBody.js
  86. 8 8
      resource/js/components/Page/RevisionPath.js
  87. 4 4
      resource/js/components/Page/RevisionUrl.js
  88. 1 1
      resource/js/components/PageAttachment/DeleteAttachmentModal.js
  89. 1 1
      resource/js/components/PageAttachment/PageAttachmentList.js
  90. 1 1
      resource/js/components/PageComment/Comment.js
  91. 2 2
      resource/js/components/PageComment/DeleteCommentModal.js
  92. 3 2
      resource/js/components/PageCommentFormBehavior.js
  93. 1 0
      resource/js/components/PageEditor.js
  94. 104 0
      resource/js/components/PageEditor/AbstractEditor.js
  95. 423 0
      resource/js/components/PageEditor/CodeMirrorEditor.js
  96. 77 348
      resource/js/components/PageEditor/Editor.js
  97. 8 8
      resource/js/components/PageEditor/EmojiAutoCompleteHelper.js
  98. 182 0
      resource/js/components/PageEditor/GrantSelector.js
  99. 26 55
      resource/js/components/PageEditor/MarkdownListUtil.js
  100. 13 6
      resource/js/components/PageEditor/MarkdownTableInterceptor.js

+ 7 - 0
.eslintignore

@@ -0,0 +1,7 @@
+/.github/**
+/.vscode/**
+/node_modules/**
+/public/**
+/resource/js/legacy/thirdparty-js/**
+/test/**
+/tmp/**

+ 18 - 11
.eslintrc.js

@@ -10,8 +10,11 @@ module.exports = {
     "plugin:react/recommended"
     "plugin:react/recommended"
   ],
   ],
   "globals": {
   "globals": {
-    "window": true,
-    "emojione": true
+    "$": true,
+    "jquery": true,
+    "emojione": true,
+    "hljs": true,
+    "window": true
   },
   },
   "parserOptions": {
   "parserOptions": {
     "ecmaFeatures": {
     "ecmaFeatures": {
@@ -41,12 +44,18 @@ module.exports = {
       2,
       2,
       {
       {
         "SwitchCase": 1,
         "SwitchCase": 1,
-        "ignoredNodes": ['JSXElement *', 'JSXElement'],
-        "FunctionExpression": {"parameters": 2},
+        "ignoredNodes": ['JSXElement *', 'JSXElement', "JSXAttribute", "JSXSpreadAttribute"],
+        "FunctionDeclaration": {"body": 1, "parameters": 2},
+        "FunctionExpression": {"body": 1, "parameters": 2},
+        "MemberExpression": "off"
       }
       }
     ],
     ],
     "key-spacing": [
     "key-spacing": [
-      "error", { "beforeColon": false, "afterColon": true }
+      "error", {
+        "beforeColon": false,
+        "afterColon": true,
+        "mode": "minimum"
+      }
     ],
     ],
     "keyword-spacing": [
     "keyword-spacing": [
       "error", {}
       "error", {}
@@ -63,14 +72,12 @@ module.exports = {
       "error",
       "error",
       "single"
       "single"
     ],
     ],
-    "react/jsx-indent-props": [
-      "error",
-      2
-    ],
-    "react/no-string-refs": 'off',
+    "react/jsx-uses-vars": 1,
+    "react/no-string-refs": "off",
     "semi": [
     "semi": [
       "error",
       "error",
-      "always"
+      "always",
+      { "omitLastInOneLineBlock": true }
     ],
     ],
     "space-before-blocks": [
     "space-before-blocks": [
       "error",
       "error",

+ 0 - 1
.gitignore

@@ -35,4 +35,3 @@ package-lock.json
 # IDE #
 # IDE #
 .idea
 .idea
 .vscode
 .vscode
-

+ 4 - 0
.vscode/settings.json

@@ -0,0 +1,4 @@
+{
+  // 既定の改行文字。LF の場合には \n を CRLF の場合には \r\n を使用してください。
+  "files.eol": "\n",
+}

+ 18 - 1
CHANGES.md

@@ -1,16 +1,33 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
-## 3.0.13-RC
+## 3.1.0-RC
+
+* Improvement: Group Access Control List - Select group modal
+* Improvement: Auto-format markdown tables which includes multibyte text
+* Improvement: Enable to switch show/hide border for highlight.js
+* Improvement: BindDN field allows also ActiveDirectory styles 
+* Improvement: Show LDAP logs when testing login
+* Improvement: Detach code blocks correctly
+* Fix: Comment body doesn't break long terms
+* Fix: lsx plugin lists up pages that hit by forward match wrongly
+    * Introduced by 3.0.4
+* Support: Upgrade libs
+    * elasticsearch
+    * googleapis
+
+## 3.0.13
 
 
 * Improvement: Add Vim/Emacs/Sublime-Text icons for keybindings menu
 * Improvement: Add Vim/Emacs/Sublime-Text icons for keybindings menu
 * Improvement: Add 'mono-blue' theme
 * Improvement: Add 'mono-blue' theme
 * Fix: Unportalize process failed silently
 * Fix: Unportalize process failed silently
+* Fix: Sidebar breaks editor layouts
 * Support: Switch the logger from 'pino' to 'bunyan'
 * Support: Switch the logger from 'pino' to 'bunyan'
 * Support: Set the alias for 'debug' to the debug function of 'bunyan'
 * Support: Set the alias for 'debug' to the debug function of 'bunyan'
 * Support: Translate /admin/security
 * Support: Translate /admin/security
 * Support: Optimize bundles
 * Support: Optimize bundles
     * upgrade 'markdown-it-toc-and-anchor-with-slugid' and omit 'uslug'
     * upgrade 'markdown-it-toc-and-anchor-with-slugid' and omit 'uslug'
+* Support: Optimize .eslintrc.js
 
 
 ## 3.0.12
 ## 3.0.12
 
 

+ 10 - 0
app.js

@@ -8,6 +8,7 @@
 require('module-alias/register');
 require('module-alias/register');
 
 
 const logger = require('@alias/logger')('growi');
 const logger = require('@alias/logger')('growi');
+const helpers = require('./config/helpers');
 const growi = new (require('./lib/crowi'))(__dirname, process.env);
 const growi = new (require('./lib/crowi'))(__dirname, process.env);
 
 
 
 
@@ -23,7 +24,16 @@ process.on('unhandledRejection', (reason, p) => {
 });
 });
 
 
 growi.start()
 growi.start()
+  .then(express => {
+    if (helpers.hasProcessFlag('ci')) {
+      logger.info('"--ci" flag is detected. Exit process.');
+      express.close(() => {
+        process.exit();
+      });
+    }
+  })
   .catch(err => {
   .catch(err => {
     logger.error('An error occurred, unable to start the server');
     logger.error('An error occurred, unable to start the server');
     logger.error(err);
     logger.error(err);
+    process.exit(1);
   });
   });

+ 4 - 0
app.json

@@ -8,6 +8,10 @@
   "repository": "https://github.com/weseek/growi",
   "repository": "https://github.com/weseek/growi",
   "success_url": "/",
   "success_url": "/",
   "env": {
   "env": {
+    "NODE_ENV": {
+      "description": "DO NOT CHANGE - 'yarn' needs this to install devDependencies",
+      "value": "development"
+    },
     "SECRET_TOKEN": {
     "SECRET_TOKEN": {
       "description": "A secret key for verifying the integrity of signed cookies.",
       "description": "A secret key for verifying the integrity of signed cookies.",
       "generator": "secret"
       "generator": "secret"

+ 1 - 1
bin/generate-plugin-definitions-source.js

@@ -22,7 +22,7 @@ if (process.env.NODE_ENV === 'development'
     && process.env.PLUGIN_NAMES_TOBE_LOADED !== undefined
     && process.env.PLUGIN_NAMES_TOBE_LOADED !== undefined
     && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
     && process.env.PLUGIN_NAMES_TOBE_LOADED.length > 0) {
 
 
-  pluginNamesDev = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
+  const pluginNamesDev = process.env.PLUGIN_NAMES_TOBE_LOADED.split(',');
 
 
   // merge and remove duplicates
   // merge and remove duplicates
   if (pluginNamesDev.length > 0) {
   if (pluginNamesDev.length > 0) {

+ 0 - 73
bin/migration/0.0.1-0.0.2-convert_embedded_object_to_schema.js

@@ -1,73 +0,0 @@
-/**
- * Fakie::app.js
- *
- * @package Fakie
- * @author  Sotaro KARASAWA <sotarok@crocos.co.jp>
- * @version 0.0.0
- */
-
-var util    = require('util');
-var config  = require('config');
-var mongo   = require('mongoose');
-var async   = require('async');
-
-var user  =     require('../../lib/user');
-var page  =     require('../../lib/page');
-var revision  = require('../../lib/revision');
-
-mongo.connect('mongodb://' + config.mongodb.user + ':' + config.mongodb.password + '@' + config.mongodb.host + '/' + config.mongodb.dbname);
-module.exports = {
-  user: user
-  ,page: page
-  ,revision: revision
-};
-
-
-var options = {
-  offset: 0,
-  limit : 999,
-  revisionSlice: {$slice: 9999}
-};
-
-var q = page.Page.findListByStartWith('/', options, function(err, docs) {
-  var i = 0;
-  async.forEach(docs, function(data, cb) {
-    var ii = 0;
-    var path = data.path;
-    var pageId = data._id;
-    console.log("path: ", data._id, path);
-
-    async.forEachSeries(data.revisions, function(rData, rcb) {
-      var newRevision = new revision.Revision();
-      newRevision.path      = path;
-      newRevision.body      = rData.body;
-      newRevision.format    = rData.format;
-      newRevision.createdAt = rData.createdAt;
-      newRevision.save(function (err, n) {
-        if (!err) {
-          console.log("    ", path, ii++, rData.createdAt, " ... ok", n._id);
-        } else {
-          console.log("    ", path, ii++, rData.createdAt, " ... ERROR!");
-          console.log(err);
-        }
-        rcb();
-      });
-      //rcb();
-    }, function (rErr) {
-      console.log("    ", path, " all revision imported.");
-      revision.Revision.findLatestRevision(path, function(err, fr) {
-        page.Page.updateRevision(pageId, fr._id, function(err, frr) {
-          if (!err) {
-            console.log("        Page revision updated", pageId, path, i++);
-          } else {
-            console.log("        Page revision update ERROR", pageId, path, i++);
-          }
-          cb();
-        });
-      });
-    });
-  }, function(err) {
-    console.log('end');
-    mongo.disconnect();
-  });
-});

+ 0 - 25
bin/password-hash-generator.js

@@ -1,25 +0,0 @@
-var crypto = require('crypto')
-  , cli = require('cli')
-  ;
-
-function generatePassword (seed, password) {
-  var hasher = crypto.createHash('sha256');
-  hasher.update(seed + password);
-
-  cli.debug("seed is: " + seed);
-  cli.debug("password is: " + password);
-  return hasher.digest('hex');
-}
-
-cli.parse({
-    seed:      [false, 'Password seed', 'string', ''],
-    password:  [false, 'Password raw string', 'string'],
-});
-
-cli.main(function(args, options)
-{
-  console.log("args", args);
-  console.log("options", options);
-
-  this.output(generatePassword(options.seed, options.password) + '\n');
-});

+ 0 - 112
bin/revision-string-replacer.js

@@ -1,112 +0,0 @@
-var cli = require('cli')
- , mongo   = require('mongoose')
- , async   = require('async')
- ;
-
-cli.setUsage('MONGO_URI=mongodb://user:password@host/dbnae node bin/revision-string-replacer.js --from=\'aaa\' --to=\'bbb\'\n\n  This means that replace string "aaa" to "bbb" from all revisions.');
-cli.parse({
-    from: [false, 'Specified string is a target to replace.', 'string'],
-    to: [false, 'Replace string which specified by "from" to this string.', 'string'],
-    dry: [false, 'Dry run', 'boolean'],
-});
-
-
-cli.main(function(args, options)
-{
-  var app = {set: function(v) { }}
-    , c = this
-    , from = options.from
-    , to = options.to
-    , dry = options.dry
-    ;
-
-  console.log('This scriprt is not working now. Should be fixed.');
-  cli.exit(1);
-
-  if (!to || !from) {
-    cli.error('"to" and "from" options are required.\n');
-    cli.output(cli.getUsage());
-    cli.exit(1);
-    return ;
-  }
-
-  var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || process.env.MONGO_URI || false;
-  if (!mongoUri) {
-    cli.error('Please set MONGO_URI env.\n');
-    cli.output(cli.getUsage());
-    cli.exit(1);
-    return;
-  }
-
-  mongo.connect(mongoUri);
-
-  // あー config 読み込み&model読み込み周りを app.js から切り離さないといけないにゃぁ
-  configModel = require('../lib/models/config')(app);
-
-  async.series([
-    function (next) {
-      configModel.loadAllConfig(function(err, doc) {
-
-        return next();
-      });
-    }, function (next) {
-      var config = app.set('config');
-
-      models = require('../lib/models')(app);
-      models.Config = configModel;
-
-      return next();
-    }, function (next) {
-      var limit = 100000;
-      c.spinner('Load revisions..');
-      models.Revision.find().limit(limit).exec(function(err, revs) {
-        c.spinner('Load revisions.. done!\n', true);
-        var count = Object.keys(revs).length
-          , i = 0
-          , matched = 0
-          , matchedWords = 0
-          ;
-
-        c.output('Found ' + count + ' revisions.\n');
-        c.output('Start replacing.\n');
-
-        async.each(revs, function(rev, cb) {
-          var regexp = new RegExp(from, 'g');
-          c.progress(++i/count);
-
-          var m = rev.body.match(regexp);
-          if (!m) {
-            return cb();
-          }
-
-          matched++;
-          matchedWords += m.length;
-          if (dry) {
-            return cb();
-          } else {
-            rev.body = rev.body.replace(regexp, to);
-            rev.save(function(err, s) {
-              if (err) {
-                c.error('Error on:' + rev.path);
-              } else {
-              }
-              return cb();
-            });
-          }
-        }, function(err) {
-          if (dry) {
-            cli.info(matchedWords + ' words in (' + matched + ' of ' + count + ') revisions will be replaced!');
-          } else {
-            cli.ok(matchedWords + ' words in (' + matched + ' of ' + count + ') revisions replaced!');
-          }
-          return next();
-        });
-      });
-    }
-  , function (next) {
-      cli.ok('Finished!');
-      mongo.disconnect();
-      return next();
-    }
-  ]);
-});

+ 0 - 126
bin/search.js

@@ -1,126 +0,0 @@
-
-var program = require('commander')
-  , debug = require('debug')('growi:console:search-util')
-  , colors = require('colors')
-  , crowi = new (require('../lib/crowi'))(__dirname + '/../', process.env)
-  ;
-
-crowi.init()
-  .then(function(app) {
-    program
-      .version(crowi.version);
-
-    program
-      .command('create-index')
-      .action(function (cmd, env) {
-        var search = crowi.getSearcher();
-
-        search.buildIndex()
-          .then(function(data) {
-            console.log(data);
-          })
-          .then(function() {
-            process.exit();
-          })
-          .catch(function(err) {
-            console.log("Error", err);
-
-          })
-      });
-
-    program
-      .command('add-pages')
-      .action(function (cmd, env) {
-        var search = crowi.getSearcher();
-
-        search.addAllPages()
-          .then(function(data) {
-            if (data.errors) {
-              console.error(colors.red.underline('Failed to index all pages.'));
-              console.error("");
-              data.items.forEach(function(item, i) {
-                var index = item.index || null;
-                if (index && index.status != 200) {
-                  console.error(colors.red('Error item: id=%s'), index._id)
-                  console.error('error.type=%s, error.reason=%s', index.error.type, index.error.reason);
-                  console.error(index.error.caused_by);
-                }
-                //debug('Item', i, item);
-              });
-            } else {
-              console.log('Data is successfully indexed.');
-            }
-            process.exit(0);
-          })
-          .catch(function(err) {
-            console.log("Error", err);
-          });
-      });
-
-    program
-      .command('rebuild-index')
-      .action(function (cmd, env) {
-        var search = crowi.getSearcher();
-
-        search.deleteIndex()
-          .then(function(data) {
-            if (!data.errors) {
-              console.log('Index deleted.');
-            }
-            return search.buildIndex();
-          })
-          .then(function(data) {
-            if (!data.errors) {
-              console.log('Index created.');
-            }
-            return search.addAllPages();
-          })
-          .then(function(data) {
-            if (!data.errors) {
-              console.log('Data is successfully indexed.');
-            }
-            process.exit(0);
-          })
-          .catch(function(err) {
-            console.error('Error', err);
-          });
-      });
-
-    program
-      .command('search')
-      .action(function (cmd, env) {
-        var Page = crowi.model('Page');
-        var search = crowi.getSearcher();
-        var keyword = cmd;
-
-        search.searchKeyword(keyword, {})
-          .then(function(data) {
-            debug('result is', data);
-            console.log(colors.green('Search result: %d of %d total. (%d ms)'), data.meta.results, data.meta.total, data.meta.took);
-
-            return Page.populatePageListToAnyObjects(data.data);
-          }).then(function(pages) {
-            pages.map(function(page) {
-              console.log(page._score, page._id, page.path);
-            });
-
-            process.exit(0);
-          })
-          .catch(function(err) {
-            console.error('Error', err);
-
-            process.exit(0);
-          });
-      });
-
-
-    program.parse(process.argv);
-
-  });
-
-
-//program
-//  .command('search [query]', 'search with optional query')
-//  .command('list', 'list packages installed', {isDefault: true})
-
-

+ 1 - 1
bin/shrink-emojione-strategy.js

@@ -14,7 +14,7 @@ const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
 let shrinkedMap = {};
 let shrinkedMap = {};
 for (let unicode in emojiStrategy) {
 for (let unicode in emojiStrategy) {
   const data = emojiStrategy[unicode];
   const data = emojiStrategy[unicode];
-  const shortname = data.shortname.replace(/\:/g, '');
+  const shortname = data.shortname.replace(/:/g, '');
 
 
   // ignore if it isn't included in markdownItEmojiFull
   // ignore if it isn't included in markdownItEmojiFull
   if (markdownItEmojiFull[shortname] == null) {
   if (markdownItEmojiFull[shortname] == null) {

+ 0 - 44
bin/util.js

@@ -1,44 +0,0 @@
-var program = require('commander')
-  , debug = require('debug')('growi:console:util')
-  , colors = require('colors')
-  , crowi = new (require('../lib/crowi'))(__dirname + '/../', process.env)
-  ;
-
-crowi.init()
-  .then(function(app) {
-    program
-      .version(crowi.version);
-
-    program
-      .command('count-page-length')
-      .action(function (cmd, env) {
-        var Page = crowi.model('Page');
-        var stream = Page.getStreamOfFindAll();
-        var pages = [];
-
-        stream.on('data', function (doc) {
-          if (!doc.creator || !doc.revision) {
-            return ;
-          }
-
-          pages.push({
-            path: doc.path,
-            body: doc.revision.body,
-            author: doc.creator.username,
-          });
-        }).on('error', function (err) {
-          // TODO: handle err
-          debug('Error stream:', err);
-        }).on('close', function () {
-          // all done
-
-          pages.forEach(function(page, i) {
-            console.log('%d\t%s', page.body.length, page.path);
-          });
-
-          process.exit(0);
-        });
-      });
-
-    program.parse(process.argv);
-  });

+ 1 - 1
config/env.dev.js

@@ -2,7 +2,7 @@ module.exports = {
   NODE_ENV: 'development',
   NODE_ENV: 'development',
   FILE_UPLOAD: 'local',
   FILE_UPLOAD: 'local',
   // MATHJAX: 1,
   // MATHJAX: 1,
-  // ELASTICSEARCH_URI: 'http://localhost:9200/growi',
+  ELASTICSEARCH_URI: 'http://localhost:9200/growi',
   PLUGIN_NAMES_TOBE_LOADED: [
   PLUGIN_NAMES_TOBE_LOADED: [
     // 'growi-plugin-lsx',
     // 'growi-plugin-lsx',
     // 'growi-plugin-pukiwiki-like-linker',
     // 'growi-plugin-pukiwiki-like-linker',

+ 0 - 2
config/helpers.js

@@ -5,8 +5,6 @@
 
 
 var path = require('path');
 var path = require('path');
 
 
-const EVENT = process.env.npm_lifecycle_event || '';
-
 // Helper functions
 // Helper functions
 var ROOT = path.resolve(__dirname, '..');
 var ROOT = path.resolve(__dirname, '..');
 
 

+ 10 - 5
config/webpack.common.js

@@ -3,7 +3,6 @@
  */
  */
 
 
 const webpack = require('webpack');
 const webpack = require('webpack');
-const path = require('path');
 const helpers = require('./helpers');
 const helpers = require('./helpers');
 
 
 /*
 /*
@@ -44,8 +43,8 @@ module.exports = function(options) {
       extensions: ['.js', '.json'],
       extensions: ['.js', '.json'],
       modules: [helpers.root('src'), helpers.root('node_modules')],
       modules: [helpers.root('src'), helpers.root('node_modules')],
       alias: {
       alias: {
-        '@root': path.resolve(__dirname, '../'),
-        '@alias/logger': path.resolve(__dirname, '../lib/service/logger'),
+        '@root': helpers.root('/'),
+        '@alias/logger': helpers.root('lib/service/logger'),
         // replace bunyan
         // replace bunyan
         'bunyan': 'browser-bunyan',
         'bunyan': 'browser-bunyan',
       }
       }
@@ -54,7 +53,13 @@ module.exports = function(options) {
       rules: [
       rules: [
         {
         {
           test: /.jsx?$/,
           test: /.jsx?$/,
-          exclude: /node_modules/,
+          exclude: {
+            test:    helpers.root('node_modules'),
+            exclude: [  // include as a result
+              helpers.root('node_modules/string-width'),
+              helpers.root('node_modules/is-fullwidth-code-point'), // depends from string-width
+            ]
+          },
           use: [{
           use: [{
             loader: 'babel-loader?cacheDirectory',
             loader: 'babel-loader?cacheDirectory',
             options: {
             options: {
@@ -82,7 +87,7 @@ module.exports = function(options) {
         /* File loader for supporting fonts, for example, in CSS files.
         /* File loader for supporting fonts, for example, in CSS files.
         */
         */
         {
         {
-          test: /\.(eot|woff2?|svg|ttf)([\?]?.*)$/,
+          test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/,
           use: 'file-loader',
           use: 'file-loader',
         }
         }
       ]
       ]

+ 1 - 1
config/webpack.dev.js

@@ -99,4 +99,4 @@ module.exports = function(options) {
 
 
     ]
     ]
   });
   });
-}
+};

+ 1 - 1
config/webpack.prod.js

@@ -116,4 +116,4 @@ module.exports = function(env) {
     ],
     ],
 
 
   });
   });
-}
+};

+ 9 - 8
lib/crowi/express-init.js

@@ -70,11 +70,11 @@ module.exports = function(crowi, app) {
     res.locals.now      = now;
     res.locals.now      = now;
     res.locals.tzoffset = tzoffset;
     res.locals.tzoffset = tzoffset;
     res.locals.consts   = {
     res.locals.consts   = {
-        pageGrants: Page.getGrantLabels(),
-        userStatus: User.getUserStatusLabels(),
-        language:   User.getLanguageLabels(),
-        restrictGuestMode: Config.getRestrictGuestModeLabels(),
-        registrationMode: Config.getRegistrationModeLabels(),
+      pageGrants: Page.getGrantLabels(),
+      userStatus: User.getUserStatusLabels(),
+      language:   User.getLanguageLabels(),
+      restrictGuestMode: Config.getRestrictGuestModeLabels(),
+      registrationMode: Config.getRegistrationModeLabels(),
     };
     };
     res.locals.local_config = Config.getLocalconfig(config); // config for browser context
     res.locals.local_config = Config.getLocalconfig(config); // config for browser context
 
 
@@ -82,7 +82,7 @@ module.exports = function(crowi, app) {
   });
   });
 
 
   app.set('port', crowi.port);
   app.set('port', crowi.port);
-  const staticOption = (crowi.node_env === 'production') ? {maxAge:'30d'} : {};
+  const staticOption = (crowi.node_env === 'production') ? {maxAge: '30d'} : {};
   app.use(express.static(crowi.publicDir, staticOption));
   app.use(express.static(crowi.publicDir, staticOption));
   app.engine('html', swig.renderFile);
   app.engine('html', swig.renderFile);
   app.use(webpackAssets(
   app.use(webpackAssets(
@@ -109,14 +109,15 @@ module.exports = function(crowi, app) {
       return basicAuth(
       return basicAuth(
         config.crowi['security:basicName'],
         config.crowi['security:basicName'],
         config.crowi['security:basicSecret'])(req, res, next);
         config.crowi['security:basicSecret'])(req, res, next);
-    } else {
+    }
+    else {
       next();
       next();
     }
     }
   });
   });
 
 
   // passport
   // passport
   if (Config.isEnabledPassport(config)) {
   if (Config.isEnabledPassport(config)) {
-    debug('initialize Passport')
+    debug('initialize Passport');
     app.use(passport.initialize());
     app.use(passport.initialize());
     app.use(passport.session());
     app.use(passport.session());
   }
   }

+ 37 - 44
lib/crowi/index.js

@@ -9,16 +9,11 @@ var debug = require('debug')('growi:crowi')
 
 
   , mongoose    = require('mongoose')
   , mongoose    = require('mongoose')
 
 
-  , eazyLogger = require('eazy-logger')
   , models = require('../models')
   , models = require('../models')
   ;
   ;
 
 
 function Crowi(rootdir, env) {
 function Crowi(rootdir, env) {
   var self = this;
   var self = this;
-  this.logger = eazyLogger.Logger({
-    prefix: '[{green:GROWI}] ',
-    useLevelPrefixes: false,
-  });
 
 
   this.version = pkg.version;
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
@@ -144,13 +139,7 @@ Crowi.prototype.setupDatabase = function() {
 
 
   const mongoUri = getMongoUrl(this.env);
   const mongoUri = getMongoUrl(this.env);
 
 
-  return mongoose.connect(mongoUri).then(
-    () => {},
-    err => {
-      logger.error('DB Connect Error: ', err);
-      logger.error('DB Connect Error: ', mongoUri);
-    }
-  );
+  return mongoose.connect(mongoUri);
 };
 };
 
 
 Crowi.prototype.setupSessionConfig = function() {
 Crowi.prototype.setupSessionConfig = function() {
@@ -346,7 +335,6 @@ Crowi.prototype.getTokens = function() {
 
 
 Crowi.prototype.start = function() {
 Crowi.prototype.start = function() {
   var self = this
   var self = this
-    , http = require('http')
     , server
     , server
     , io;
     , io;
 
 
@@ -364,40 +352,45 @@ Crowi.prototype.start = function() {
     .then(function() {
     .then(function() {
       return self.buildServer();
       return self.buildServer();
     })
     })
-    .then(function(app) {
-      server = http.createServer(app).listen(self.port, function() {
-        logger.info(`[${self.node_env}] Express server listening on port ${self.port}`);
-
-        self.logger.info('{bold:Server URLs:}');
-        self.logger.unprefixed('info', '{grey:=======================================}');
-        self.logger.unprefixed('info', `         APP: {magenta:http://localhost:${self.port}}`);
-        self.logger.unprefixed('info', '{grey:=======================================}');
-
-        // setup for dev
-        if (self.node_env === 'development') {
-          self.crowiDev.setup(server, app);
-        }
-      });
-
-      io = require('socket.io')(server);
-      io.sockets.on('connection', function(socket) {
+    .then(function(express) {
+      return new Promise((resolve) => {
+        server = express.listen(self.port, function() {
+          logger.info(`[${self.node_env}] Express server listening on port ${self.port}`);
+
+          // setup for dev
+          if (self.node_env === 'development') {
+            const eazyLogger = require('eazy-logger').Logger({
+              prefix: '[{green:GROWI}] ',
+              useLevelPrefixes: false,
+            });
+
+            eazyLogger.info('{bold:Server URLs:}');
+            eazyLogger.unprefixed('info', '{grey:=======================================}');
+            eazyLogger.unprefixed('info', `         APP: {magenta:http://localhost:${self.port}}`);
+            eazyLogger.unprefixed('info', '{grey:=======================================}');
+
+            self.crowiDev.setup(server, express);
+          }
+          resolve(server);
+        });
+
+        io = require('socket.io')(server);
+        io.sockets.on('connection', function(socket) {
+        });
+        self.io = io;
+
+        // setup Express Routes
+        self.setupRoutesAtLast(express);
       });
       });
-      self.io = io;
-
-      // setup Express Routes
-      self.setupRoutesAtLast(app);
-
-      return app;
     });
     });
 };
 };
 
 
 Crowi.prototype.buildServer = function() {
 Crowi.prototype.buildServer = function() {
-  var express  = require('express')
-    , app = express()
+  var express = require('express')()
     , env = this.node_env
     , env = this.node_env
     ;
     ;
 
 
-  require('./express-init')(this, app);
+  require('./express-init')(this, express);
 
 
   // import plugins
   // import plugins
   var Config = this.model('Config');
   var Config = this.model('Config');
@@ -405,11 +398,11 @@ Crowi.prototype.buildServer = function() {
   if (isEnabledPlugins) {
   if (isEnabledPlugins) {
     debug('Plugins are enabled');
     debug('Plugins are enabled');
     var PluginService = require('../plugins/plugin.service');
     var PluginService = require('../plugins/plugin.service');
-    var pluginService = new PluginService(this, app);
+    var pluginService = new PluginService(this, express);
     pluginService.autoDetectAndLoadPlugins();
     pluginService.autoDetectAndLoadPlugins();
 
 
     if (env == 'development') {
     if (env == 'development') {
-      this.crowiDev.loadPlugins(app);
+      this.crowiDev.loadPlugins(express);
     }
     }
   }
   }
 
 
@@ -417,7 +410,7 @@ Crowi.prototype.buildServer = function() {
   if (env == 'production') {
   if (env == 'production') {
     const expressBunyanLogger = require('express-bunyan-logger');
     const expressBunyanLogger = require('express-bunyan-logger');
     const logger = require('@alias/logger')('express');
     const logger = require('@alias/logger')('express');
-    app.use(expressBunyanLogger({
+    express.use(expressBunyanLogger({
       logger,
       logger,
       excludes: ['*'],
       excludes: ['*'],
     }));
     }));
@@ -425,10 +418,10 @@ Crowi.prototype.buildServer = function() {
   // use morgan
   // use morgan
   else {
   else {
     const morgan = require('morgan');
     const morgan = require('morgan');
-    app.use(morgan('dev'));
+    express.use(morgan('dev'));
   }
   }
 
 
-  return Promise.resolve(app);
+  return Promise.resolve(express);
 };
 };
 
 
 /**
 /**

+ 2 - 5
lib/events/page.js

@@ -10,13 +10,10 @@ function PageEvent(crowi) {
 util.inherits(PageEvent, events.EventEmitter);
 util.inherits(PageEvent, events.EventEmitter);
 
 
 PageEvent.prototype.onCreate = function(page, user) {
 PageEvent.prototype.onCreate = function(page, user) {
-  var User = this.crowi.model('User');
-  var Page = this.crowi.model('Page');
-
+  debug('onCreate event fired');
 };
 };
 PageEvent.prototype.onUpdate = function(page, user) {
 PageEvent.prototype.onUpdate = function(page, user) {
-  var User = this.crowi.model('User');
-  var Page = this.crowi.model('Page');
+  debug('onUpdate event fired');
 };
 };
 
 
 module.exports = PageEvent;
 module.exports = PageEvent;

+ 1 - 1
lib/form/admin/securityPassportLdap.js

@@ -12,7 +12,7 @@ module.exports = form(
   field('settingForm[security:passport-ldap:isUserBind]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:isUserBind]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:bindDN]').trim()
   field('settingForm[security:passport-ldap:bindDN]').trim()
       // https://regex101.com/r/jK8lpO/1
       // https://regex101.com/r/jK8lpO/1
-      .is(/^(,?[^,=\s]+=[^,=\s]+){1,}$/, 'Bind DN is invalid. <small><a href="https://regex101.com/r/jK8lpO/1">&gt;&gt; Regex</a></small>'),
+      .is(/^(,?[^,=\s]+=[^,=\s]+){1,}$|^[^@\s]+@[^@\s]+$/, 'Bind DN is invalid. <small><a href="https://regex101.com/r/jK8lpO/3">&gt;&gt; Regex</a></small>'),
   field('settingForm[security:passport-ldap:bindDNPassword]'),
   field('settingForm[security:passport-ldap:bindDNPassword]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),

+ 1 - 1
lib/form/revision.js

@@ -5,7 +5,7 @@ var form = require('express-form')
 
 
 module.exports = form(
 module.exports = form(
   field('pageForm.path').required(),
   field('pageForm.path').required(),
-  field('pageForm.body').required().custom(function(value) { return value.replace(/\r/g, '\n'); }),
+  field('pageForm.body').required().custom(function(value) { return value.replace(/\r/g, '\n') }),
   field('pageForm.currentRevision'),
   field('pageForm.currentRevision'),
   field('pageForm.grant').toInt().required(),
   field('pageForm.grant').toInt().required(),
   field('pageForm.notify')
   field('pageForm.notify')

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

@@ -326,7 +326,8 @@
       "search_filter_detail1": "The query used to locate the authenticated user.",
       "search_filter_detail1": "The query used to locate the authenticated user.",
       "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
       "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
       "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
       "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
-      "search_filter_example": "Example to match with 'uid' or 'mail'",
+      "search_filter_example1": "Match with 'uid' or 'mail'",
+      "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
       "username_detail": "Specification of mappings when creating new users",
       "username_detail": "Specification of mappings when creating new users",
       "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
       "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
   		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
   		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
@@ -352,10 +353,10 @@
 
 
   "markdown_setting": {
   "markdown_setting": {
     "markdown_rendering": "You can change Markdown rendering settings.",
     "markdown_rendering": "You can change Markdown rendering settings.",
-    "validate Line Break": "Validate Line Break",
-    "treat_text": "Treat line breaking in the text page as <code>&lt;br&gt;</code> in HTML",
-    "validate_comment": "Validate Line Break in the comment section",
-    "treat_comment": "Treat line breaking in the comment section as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break": "Enable Line Break",
+    "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break for comment": "Enable Line Break in comment",
+    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
     "TBD": "(TBD: Markdown function in the comment section has not been implemented yet)"
     "TBD": "(TBD: Markdown function in the comment section has not been implemented yet)"
   },
   },
 
 

+ 6 - 5
lib/locales/ja/translation.json

@@ -345,7 +345,8 @@
       "search_filter_detail1": "認証されるユーザーを一意に決定するための LDAP フィルタ",
       "search_filter_detail1": "認証されるユーザーを一意に決定するための LDAP フィルタ",
       "search_filter_detail2": "ログイン時のユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
       "search_filter_detail2": "ログイン時のユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
       "search_filter_detail3": "空欄の場合 <code>(uid=&#123;&#123;username&#125;&#125;)</code> が使用されます。",
       "search_filter_detail3": "空欄の場合 <code>(uid=&#123;&#123;username&#125;&#125;)</code> が使用されます。",
-      "search_filter_example": "'uid' または 'mail' に一致させる場合の例",
+      "search_filter_example1": "'uid' または 'mail' に一致",
+      "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
       "username_detail": "新規ユーザーの関連付けを設定",
       "username_detail": "新規ユーザーの関連付けを設定",
       "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
       "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
       "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
       "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
@@ -369,10 +370,10 @@
   },
   },
   "markdown_setting": {
   "markdown_setting": {
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
-    "validate Line Break": "Line Break を有効にする",
-    "treat_text": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
-    "validate_comment": "コメント欄で Line Break を有効にする",
-    "treat_comment": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+    "Enable Line Break": "Line Break を有効にする",
+    "Enable Line Break desc": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+    "Enable Line Break for comment": "コメント欄で Line Break を有効にする",
+    "Enable Line Break for comment desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
     "TBD": "(TBD: コメント欄の Markdown 化は未だ実装されていません)"
     "TBD": "(TBD: コメント欄の Markdown 化は未だ実装されていません)"
 
 
   },
   },

+ 9 - 9
lib/models/attachment.js

@@ -3,9 +3,10 @@ module.exports = function(crowi) {
     , mongoose = require('mongoose')
     , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
     , fileUploader = require('../util/fileUploader')(crowi)
     , fileUploader = require('../util/fileUploader')(crowi)
+    , attachmentSchema
   ;
   ;
 
 
-  function generateFileHash (fileName) {
+  function generateFileHash(fileName) {
     var hasher = require('crypto').createHash('md5');
     var hasher = require('crypto').createHash('md5');
     hasher.update(fileName);
     hasher.update(fileName);
 
 
@@ -105,7 +106,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  attachmentSchema.statics.guessExtByFileType = function (fileType) {
+  attachmentSchema.statics.guessExtByFileType = function(fileType) {
     let ext = '';
     let ext = '';
     const isImage = fileType.match(/^image\/(.+)/i);
     const isImage = fileType.match(/^image\/(.+)/i);
 
 
@@ -116,14 +117,15 @@ module.exports = function(crowi) {
     return ext;
     return ext;
   };
   };
 
 
-  attachmentSchema.statics.createAttachmentFilePath = function (pageId, fileName, fileType) {
+  attachmentSchema.statics.createAttachmentFilePath = function(pageId, fileName, fileType) {
     const Attachment = this;
     const Attachment = this;
     let ext = '';
     let ext = '';
     const fnExt = fileName.match(/(.*)(?:\.([^.]+$))/);
     const fnExt = fileName.match(/(.*)(?:\.([^.]+$))/);
 
 
     if (fnExt) {
     if (fnExt) {
       ext = '.' + fnExt[2];
       ext = '.' + fnExt[2];
-    } else {
+    }
+    else {
       ext = Attachment.guessExtByFileType(fileType);
       ext = Attachment.guessExtByFileType(fileType);
       if (ext !== '') {
       if (ext !== '') {
         ext = '.' + ext;
         ext = '.' + ext;
@@ -139,7 +141,7 @@ module.exports = function(crowi) {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       Attachment.getListByPageId(pageId)
       Attachment.getListByPageId(pageId)
       .then((attachments) => {
       .then((attachments) => {
-        for (attachment of attachments) {
+        for (let attachment of attachments) {
           Attachment.removeAttachment(attachment).then((res) => {
           Attachment.removeAttachment(attachment).then((res) => {
             // do nothing
             // do nothing
           }).catch((err) => {
           }).catch((err) => {
@@ -156,10 +158,8 @@ module.exports = function(crowi) {
   };
   };
 
 
   attachmentSchema.statics.findDeliveryFile = function(attachment, forceUpdate) {
   attachmentSchema.statics.findDeliveryFile = function(attachment, forceUpdate) {
-    var Attachment = this;
-
-    // TODO
-    var forceUpdate = forceUpdate || false;
+    // TODO force update
+    // var forceUpdate = forceUpdate || false;
 
 
     return fileUploader.findDeliveryFile(attachment._id, attachment.filePath);
     return fileUploader.findDeliveryFile(attachment._id, attachment.filePath);
   };
   };

+ 1 - 1
lib/models/comment.js

@@ -112,7 +112,7 @@ module.exports = function(crowi) {
         }
         }
 
 
         resolve(done);
         resolve(done);
-      });;
+      });
     });
     });
   };
   };
 
 

+ 64 - 101
lib/models/config.js

@@ -2,7 +2,6 @@ module.exports = function(crowi) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , debug = require('debug')('growi:models:config')
     , debug = require('debug')('growi:models:config')
     , uglifycss = require('uglifycss')
     , uglifycss = require('uglifycss')
-    , ObjectId = mongoose.Schema.Types.ObjectId
     , configSchema
     , configSchema
     , Config
     , Config
 
 
@@ -39,8 +38,8 @@ module.exports = function(crowi) {
   /**
   /**
    * default values when migrated from Official Crowi
    * default values when migrated from Official Crowi
    */
    */
-  function getDefaultCrowiConfigs()
-  {
+  function getDefaultCrowiConfigs() {
+    /* eslint-disable key-spacing */
     return {
     return {
       //'app:installed'     : "0.0.0",
       //'app:installed'     : "0.0.0",
       'app:confidential'  : '',
       'app:confidential'  : '',
@@ -94,13 +93,14 @@ module.exports = function(crowi) {
       'customize:isSavedStatesOfTabChanges' : true,
       'customize:isSavedStatesOfTabChanges' : true,
       'customize:isEnabledAttachTitleHeader' : false,
       'customize:isEnabledAttachTitleHeader' : false,
     };
     };
+    /* eslint-enable */
   }
   }
 
 
   function getDefaultMarkdownConfigs() {
   function getDefaultMarkdownConfigs() {
     return {
     return {
       'markdown:isEnabledLinebreaks': true,
       'markdown:isEnabledLinebreaks': true,
       'markdown:isEnabledLinebreaksInComments': true,
       'markdown:isEnabledLinebreaksInComments': true,
-    }
+    };
   }
   }
 
 
   function getValueForCrowiNS(config, key) {
   function getValueForCrowiNS(config, key) {
@@ -112,8 +112,7 @@ module.exports = function(crowi) {
     return config.crowi[key];
     return config.crowi[key];
   }
   }
 
 
-  configSchema.statics.getRestrictGuestModeLabels = function()
-  {
+  configSchema.statics.getRestrictGuestModeLabels = function() {
     var labels = {};
     var labels = {};
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'security_setting.guest_mode.deny';
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'security_setting.guest_mode.deny';
     labels[SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_setting.guest_mode.readonly';
     labels[SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_setting.guest_mode.readonly';
@@ -121,8 +120,7 @@ module.exports = function(crowi) {
     return labels;
     return labels;
   };
   };
 
 
-  configSchema.statics.getRegistrationModeLabels = function()
-  {
+  configSchema.statics.getRegistrationModeLabels = function() {
     var labels = {};
     var labels = {};
     labels[SECURITY_REGISTRATION_MODE_OPEN]       = 'security_setting.registration_mode.open';
     labels[SECURITY_REGISTRATION_MODE_OPEN]       = 'security_setting.registration_mode.open';
     labels[SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_setting.registration_mode.restricted';
     labels[SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_setting.registration_mode.restricted';
@@ -131,11 +129,10 @@ module.exports = function(crowi) {
     return labels;
     return labels;
   };
   };
 
 
-  configSchema.statics.updateConfigCache = function(ns, config)
-  {
+  configSchema.statics.updateConfigCache = function(ns, config) {
     var originalConfig = crowi.getConfig();
     var originalConfig = crowi.getConfig();
     var newNSConfig = originalConfig[ns] || {};
     var newNSConfig = originalConfig[ns] || {};
-    Object.keys(config).forEach(function (key) {
+    Object.keys(config).forEach(function(key) {
       if (config[key] || config[key] === '' || config[key] === false) {
       if (config[key] || config[key] === '' || config[key] === false) {
         newNSConfig[key] = config[key];
         newNSConfig[key] = config[key];
       }
       }
@@ -150,10 +147,9 @@ module.exports = function(crowi) {
   };
   };
 
 
   // Execute only once for installing application
   // Execute only once for installing application
-  configSchema.statics.applicationInstall = function(callback)
-  {
+  configSchema.statics.applicationInstall = function(callback) {
     var Config = this;
     var Config = this;
-    Config.count({ ns: 'crowi' }, function (err, count) {
+    Config.count({ ns: 'crowi' }, function(err, count) {
       if (count > 0) {
       if (count > 0) {
         return callback(new Error('Application already installed'), null);
         return callback(new Error('Application already installed'), null);
       }
       }
@@ -165,8 +161,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  configSchema.statics.setupCofigFormData = function(ns, config)
-  {
+  configSchema.statics.setupCofigFormData = function(ns, config) {
     var defaultConfig = {};
     var defaultConfig = {};
 
 
     // set Default Settings
     // set Default Settings
@@ -180,7 +175,7 @@ module.exports = function(crowi) {
     if (!defaultConfig[ns]) {
     if (!defaultConfig[ns]) {
       defaultConfig[ns] = {};
       defaultConfig[ns] = {};
     }
     }
-    Object.keys(config[ns] || {}).forEach(function (key) {
+    Object.keys(config[ns] || {}).forEach(function(key) {
       if (config[ns][key] !== undefined) {
       if (config[ns][key] !== undefined) {
         defaultConfig[key] = config[ns][key];
         defaultConfig[key] = config[ns][key];
       }
       }
@@ -189,47 +184,43 @@ module.exports = function(crowi) {
   };
   };
 
 
 
 
-  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback)
-  {
+  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback) {
     var Config = this;
     var Config = this;
     if (configs.length < 0) {
     if (configs.length < 0) {
       return callback(new Error('Argument #1 is not array.'), null);
       return callback(new Error('Argument #1 is not array.'), null);
     }
     }
 
 
-    Object.keys(configs).forEach(function (key) {
+    Object.keys(configs).forEach(function(key) {
       var value = configs[key];
       var value = configs[key];
 
 
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
         { ns: ns, key: key },
         { ns: ns, key: key },
         { ns: ns, key: key, value: JSON.stringify(value) },
         { ns: ns, key: key, value: JSON.stringify(value) },
         { upsert: true, },
         { upsert: true, },
-        function (err, config) {
+        function(err, config) {
           debug('Config.findAndUpdate', err, config);
           debug('Config.findAndUpdate', err, config);
-      });
+        });
     });
     });
 
 
     return callback(null, configs);
     return callback(null, configs);
   };
   };
 
 
-  configSchema.statics.findAndUpdate = function(ns, key, value, callback)
-  {
+  configSchema.statics.findAndUpdate = function(ns, key, value, callback) {
     var Config = this;
     var Config = this;
     Config.findOneAndUpdate(
     Config.findOneAndUpdate(
       { ns: ns, key: key },
       { ns: ns, key: key },
       { ns: ns, key: key, value: JSON.stringify(value) },
       { ns: ns, key: key, value: JSON.stringify(value) },
       { upsert: true, },
       { upsert: true, },
-      function (err, config) {
+      function(err, config) {
         debug('Config.findAndUpdate', err, config);
         debug('Config.findAndUpdate', err, config);
         callback(err, config);
         callback(err, config);
-    });
+      });
   };
   };
 
 
-  configSchema.statics.getConfig = function(callback)
-  {
+  configSchema.statics.getConfig = function(callback) {
   };
   };
 
 
-  configSchema.statics.loadAllConfig = function(callback)
-  {
+  configSchema.statics.loadAllConfig = function(callback) {
     var Config = this
     var Config = this
       , config = {};
       , config = {};
     config.crowi = {}; // crowi namespace
     config.crowi = {}; // crowi namespace
@@ -255,14 +246,12 @@ module.exports = function(crowi) {
       });
       });
   };
   };
 
 
-  configSchema.statics.appTitle = function(config)
-  {
+  configSchema.statics.appTitle = function(config) {
     const key = 'app:title';
     const key = 'app:title';
     return getValueForCrowiNS(config, key) || 'GROWI';
     return getValueForCrowiNS(config, key) || 'GROWI';
   };
   };
 
 
-  configSchema.statics.isEnabledPassport = function(config)
-  {
+  configSchema.statics.isEnabledPassport = function(config) {
     // always true if growi installed cleanly
     // always true if growi installed cleanly
     if (Object.keys(config.crowi).length == 0) {
     if (Object.keys(config.crowi).length == 0) {
       return true;
       return true;
@@ -272,24 +261,21 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isEnabledPassportLdap = function(config)
-  {
+  configSchema.statics.isEnabledPassportLdap = function(config) {
     const key = 'security:passport-ldap:isEnabled';
     const key = 'security:passport-ldap:isEnabled';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType)
-  {
+  configSchema.statics.isSameUsernameTreatedAsIdenticalUser = function(config, providerType) {
     const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
     const key = `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`;
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isUploadable = function(config)
-  {
+  configSchema.statics.isUploadable = function(config) {
     var method = crowi.env.FILE_UPLOAD || 'aws';
     var method = crowi.env.FILE_UPLOAD || 'aws';
 
 
     if (method == 'aws' && (
     if (method == 'aws' && (
-        !config.crowi['aws:accessKeyId'] ||
+      !config.crowi['aws:accessKeyId'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:region'] ||
         !config.crowi['aws:region'] ||
         !config.crowi['aws:bucket'])) {
         !config.crowi['aws:bucket'])) {
@@ -299,8 +285,7 @@ module.exports = function(crowi) {
     return method != 'none';
     return method != 'none';
   };
   };
 
 
-  configSchema.statics.isGuesstAllowedToRead = function(config)
-  {
+  configSchema.statics.isGuesstAllowedToRead = function(config) {
     // return false if undefined
     // return false if undefined
     if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
     if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
       return false;
       return false;
@@ -309,14 +294,12 @@ module.exports = function(crowi) {
     return SECURITY_RESTRICT_GUEST_MODE_READONLY === config.crowi['security:restrictGuestMode'];
     return SECURITY_RESTRICT_GUEST_MODE_READONLY === config.crowi['security:restrictGuestMode'];
   };
   };
 
 
-  configSchema.statics.isEnabledPlugins = function(config)
-  {
+  configSchema.statics.isEnabledPlugins = function(config) {
     const key = 'plugin:isEnabledPlugins';
     const key = 'plugin:isEnabledPlugins';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isEnabledLinebreaks = function(config)
-  {
+  configSchema.statics.isEnabledLinebreaks = function(config) {
     const key = 'markdown:isEnabledLinebreaks';
     const key = 'markdown:isEnabledLinebreaks';
 
 
     // return default value if undefined
     // return default value if undefined
@@ -327,8 +310,7 @@ module.exports = function(crowi) {
     return config.markdown[key];
     return config.markdown[key];
   };
   };
 
 
-  configSchema.statics.isEnabledLinebreaksInComments = function(config)
-  {
+  configSchema.statics.isEnabledLinebreaksInComments = function(config) {
     const key = 'markdown:isEnabledLinebreaksInComments';
     const key = 'markdown:isEnabledLinebreaksInComments';
 
 
     // return default value if undefined
     // return default value if undefined
@@ -342,46 +324,39 @@ module.exports = function(crowi) {
   /**
   /**
    * initialize custom css strings
    * initialize custom css strings
    */
    */
-  configSchema.statics.initCustomCss = function(config)
-  {
+  configSchema.statics.initCustomCss = function(config) {
     const key = 'customize:css';
     const key = 'customize:css';
     const rawCss = getValueForCrowiNS(config, key);
     const rawCss = getValueForCrowiNS(config, key);
     // uglify and store
     // uglify and store
     this._customCss = uglifycss.processString(rawCss);
     this._customCss = uglifycss.processString(rawCss);
-  }
+  };
 
 
-  configSchema.statics.customCss = function(config)
-  {
+  configSchema.statics.customCss = function(config) {
     return this._customCss;
     return this._customCss;
-  }
+  };
 
 
-  configSchema.statics.initCustomScript = function(config)
-  {
+  configSchema.statics.initCustomScript = function(config) {
     const key = 'customize:script';
     const key = 'customize:script';
     const rawScript = getValueForCrowiNS(config, key);
     const rawScript = getValueForCrowiNS(config, key);
     // store as is
     // store as is
     this._customScript = rawScript;
     this._customScript = rawScript;
-  }
+  };
 
 
-  configSchema.statics.customScript = function(config)
-  {
+  configSchema.statics.customScript = function(config) {
     return this._customScript;
     return this._customScript;
-  }
+  };
 
 
-  configSchema.statics.customHeader = function(config)
-  {
+  configSchema.statics.customHeader = function(config) {
     const key = 'customize:header';
     const key = 'customize:header';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.theme = function(config)
-  {
+  configSchema.statics.theme = function(config) {
     const key = 'customize:theme';
     const key = 'customize:theme';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.customTitle = function(config, page)
-  {
+  configSchema.statics.customTitle = function(config, page) {
     const key = 'customize:title';
     const key = 'customize:title';
     let customTitle = getValueForCrowiNS(config, key);
     let customTitle = getValueForCrowiNS(config, key);
 
 
@@ -392,52 +367,44 @@ module.exports = function(crowi) {
     return customTitle
     return customTitle
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{sitename}}', this.appTitle(config))
       .replace('{{page}}', page);
       .replace('{{page}}', page);
-  }
+  };
 
 
-  configSchema.statics.behaviorType = function(config)
-  {
+  configSchema.statics.behaviorType = function(config) {
     const key = 'customize:behavior';
     const key = 'customize:behavior';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.layoutType = function(config)
-  {
+  configSchema.statics.layoutType = function(config) {
     const key = 'customize:layout';
     const key = 'customize:layout';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.highlightJsStyle = function(config)
-  {
+  configSchema.statics.highlightJsStyle = function(config) {
     const key = 'customize:highlightJsStyle';
     const key = 'customize:highlightJsStyle';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.highlightJsStyleBorder = function(config)
-  {
+  configSchema.statics.highlightJsStyleBorder = function(config) {
     const key = 'customize:highlightJsStyleBorder';
     const key = 'customize:highlightJsStyleBorder';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.isEnabledTimeline = function(config)
-  {
+  configSchema.statics.isEnabledTimeline = function(config) {
     const key = 'customize:isEnabledTimeline';
     const key = 'customize:isEnabledTimeline';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isSavedStatesOfTabChanges = function(config)
-  {
+  configSchema.statics.isSavedStatesOfTabChanges = function(config) {
     const key = 'customize:isSavedStatesOfTabChanges';
     const key = 'customize:isSavedStatesOfTabChanges';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isEnabledAttachTitleHeader = function(config)
-  {
+  configSchema.statics.isEnabledAttachTitleHeader = function(config) {
     const key = 'customize:isEnabledAttachTitleHeader';
     const key = 'customize:isEnabledAttachTitleHeader';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.fileUploadEnabled = function(config)
-  {
+  configSchema.statics.fileUploadEnabled = function(config) {
     const Config = this;
     const Config = this;
 
 
     if (!Config.isUploadable(config)) {
     if (!Config.isUploadable(config)) {
@@ -448,32 +415,28 @@ module.exports = function(crowi) {
     return !!config.crowi['app:fileUpload'];
     return !!config.crowi['app:fileUpload'];
   };
   };
 
 
-  configSchema.statics.hasSlackConfig = function(config)
-  {
+  configSchema.statics.hasSlackConfig = function(config) {
     return Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config);
     return Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config);
   };
   };
 
 
   /**
   /**
    * for Slack Incoming Webhooks
    * for Slack Incoming Webhooks
    */
    */
-  configSchema.statics.hasSlackIwhUrl = function(config)
-  {
+  configSchema.statics.hasSlackIwhUrl = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
     return (config.notification['slack:incomingWebhookUrl'] ? true : false);
     return (config.notification['slack:incomingWebhookUrl'] ? true : false);
   };
   };
 
 
-  configSchema.statics.isIncomingWebhookPrioritized = function(config)
-  {
+  configSchema.statics.isIncomingWebhookPrioritized = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
     return (config.notification['slack:isIncomingWebhookPrioritized'] ? true : false);
     return (config.notification['slack:isIncomingWebhookPrioritized'] ? true : false);
   };
   };
 
 
-  configSchema.statics.hasSlackToken = function(config)
-  {
+  configSchema.statics.hasSlackToken = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
@@ -481,8 +444,7 @@ module.exports = function(crowi) {
     return (config.notification['slack:token'] ? true : false);
     return (config.notification['slack:token'] ? true : false);
   };
   };
 
 
-  configSchema.statics.getLocalconfig = function(config)
-  {
+  configSchema.statics.getLocalconfig = function(config) {
     const Config = this;
     const Config = this;
     const env = crowi.getEnv();
     const env = crowi.getEnv();
 
 
@@ -498,6 +460,7 @@ module.exports = function(crowi) {
       behaviorType: Config.behaviorType(config),
       behaviorType: Config.behaviorType(config),
       layoutType: Config.layoutType(config),
       layoutType: Config.layoutType(config),
       isEnabledLineBreaks: Config.isEnabledLinebreaks(config),
       isEnabledLineBreaks: Config.isEnabledLinebreaks(config),
+      highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
       env: {
       env: {
         PLANTUML_URI: env.PLANTUML_URI || null,
         PLANTUML_URI: env.PLANTUML_URI || null,
@@ -506,7 +469,7 @@ module.exports = function(crowi) {
     };
     };
 
 
     return local_config;
     return local_config;
-  }
+  };
 
 
   /*
   /*
   configSchema.statics.isInstalled = function(config)
   configSchema.statics.isInstalled = function(config)

+ 2 - 2
lib/models/external-account.js

@@ -55,7 +55,7 @@ class ExternalAccount {
     return this.populate('user').execPopulate()
     return this.populate('user').execPopulate()
       .then((account) => {
       .then((account) => {
         return account.user;
         return account.user;
-      })
+      });
   }
   }
 
 
   /**
   /**
@@ -154,4 +154,4 @@ module.exports = function(crowi) {
   ExternalAccount.crowi = crowi;
   ExternalAccount.crowi = crowi;
   schema.loadClass(ExternalAccount);
   schema.loadClass(ExternalAccount);
   return mongoose.model('ExternalAccount', schema);
   return mongoose.model('ExternalAccount', schema);
-}
+};

+ 12 - 9
lib/models/page-group-relation.js

@@ -26,16 +26,16 @@ schema.plugin(mongoosePaginate);
  */
  */
 class PageGroupRelation {
 class PageGroupRelation {
 
 
-    /**
+  /**
    * limit items num for pagination
    * limit items num for pagination
    *
    *
    * @readonly
    * @readonly
    * @static
    * @static
    * @memberof PageGroupRelation
    * @memberof PageGroupRelation
    */
    */
-   static get PAGE_ITEMS() {
-     return 50;
-    }
+  static get PAGE_ITEMS() {
+    return 50;
+  }
 
 
   static set crowi(crowi) {
   static set crowi(crowi) {
     this._crowi = crowi;
     this._crowi = crowi;
@@ -143,6 +143,9 @@ class PageGroupRelation {
    */
    */
   static findByPage(page) {
   static findByPage(page) {
 
 
+    if (page == null) {
+      return null;
+    }
     return this
     return this
       .find({ targetPage: page.id })
       .find({ targetPage: page.id })
       .populate('relatedGroup')
       .populate('relatedGroup')
@@ -164,11 +167,11 @@ class PageGroupRelation {
     return this.findByPage(pageData)
     return this.findByPage(pageData)
       .then((pageRelations) => {
       .then((pageRelations) => {
         return pageRelations.map((pageRelation) => {
         return pageRelations.map((pageRelation) => {
-          return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup)
+          return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup);
         });
         });
       })
       })
       .then((checkPromises) => {
       .then((checkPromises) => {
-        return Promise.all(checkPromises)
+        return Promise.all(checkPromises);
       })
       })
       .then((checkResults) => {
       .then((checkResults) => {
         var checkResult = false;
         var checkResult = false;
@@ -198,7 +201,7 @@ class PageGroupRelation {
       relatedGroup: userGroup.id,
       relatedGroup: userGroup.id,
       targetPage: page.id,
       targetPage: page.id,
     });
     });
-  };
+  }
 
 
   /**
   /**
    * remove all relation for UserGroup
    * remove all relation for UserGroup
@@ -273,8 +276,8 @@ class PageGroupRelation {
   }
   }
 }
 }
 
 
-module.exports = function (crowi) {
+module.exports = function(crowi) {
   PageGroupRelation.crowi = crowi;
   PageGroupRelation.crowi = crowi;
   schema.loadClass(PageGroupRelation);
   schema.loadClass(PageGroupRelation);
   return mongoose.model('PageGroupRelation', schema);
   return mongoose.model('PageGroupRelation', schema);
-}
+};

+ 155 - 137
lib/models/page.js

@@ -48,7 +48,8 @@ module.exports = function(crowi) {
       get: function(data) {
       get: function(data) {
         try {
         try {
           return JSON.parse(data);
           return JSON.parse(data);
-        } catch(e) {
+        }
+        catch (e) {
           return data;
           return data;
         }
         }
       },
       },
@@ -103,8 +104,9 @@ module.exports = function(crowi) {
 
 
     if (this.populated('creator') && this.creator._id.toString() === userData._id.toString()) {
     if (this.populated('creator') && this.creator._id.toString() === userData._id.toString()) {
       return true;
       return true;
-    } else if (this.creator.toString() === userData._id.toString()) {
-      return true
+    }
+    else if (this.creator.toString() === userData._id.toString()) {
+      return true;
     }
     }
 
 
     return false;
     return false;
@@ -149,21 +151,22 @@ module.exports = function(crowi) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-      return new Promise(function(resolve, reject) {
-        var added = self.liker.addToSet(userData._id);
-        if (added.length > 0) {
-          self.save(function(err, data) {
-            if (err) {
-              return reject(err);
-            }
-            debug('liker updated!', added);
-            return resolve(data);
-          });
-        } else {
-          debug('liker not updated');
-          return reject(self);
-        }
-      });
+    return new Promise(function(resolve, reject) {
+      var added = self.liker.addToSet(userData._id);
+      if (added.length > 0) {
+        self.save(function(err, data) {
+          if (err) {
+            return reject(err);
+          }
+          debug('liker updated!', added);
+          return resolve(data);
+        });
+      }
+      else {
+        debug('liker not updated');
+        return reject(self);
+      }
+    });
 
 
   };
   };
 
 
@@ -171,21 +174,22 @@ module.exports = function(crowi) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-      return new Promise(function(resolve, reject) {
-        var beforeCount = self.liker.length;
-        self.liker.pull(userData._id);
-        if (self.liker.length != beforeCount) {
-          self.save(function(err, data) {
-            if (err) {
-              return reject(err);
-            }
-            return resolve(data);
-          });
-        } else {
-          debug('liker not updated');
-          return reject(self);
-        }
-      });
+    return new Promise(function(resolve, reject) {
+      var beforeCount = self.liker.length;
+      self.liker.pull(userData._id);
+      if (self.liker.length != beforeCount) {
+        self.save(function(err, data) {
+          if (err) {
+            return reject(err);
+          }
+          return resolve(data);
+        });
+      }
+      else {
+        debug('liker not updated');
+        return reject(self);
+      }
+    });
 
 
   };
   };
 
 
@@ -193,35 +197,35 @@ module.exports = function(crowi) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-      return this.seenUsers.some(function(seenUser) {
-        return seenUser.equals(userData._id);
-      });
+    return this.seenUsers.some(function(seenUser) {
+      return seenUser.equals(userData._id);
+    });
   };
   };
 
 
   pageSchema.methods.seen = function(userData) {
   pageSchema.methods.seen = function(userData) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-      if (this.isSeenUser(userData)) {
-        debug('seenUsers not updated');
-        return Promise.resolve(this);
+    if (this.isSeenUser(userData)) {
+      debug('seenUsers not updated');
+      return Promise.resolve(this);
+    }
+
+    return new Promise(function(resolve, reject) {
+      if (!userData || !userData._id) {
+        reject(new Error('User data is not valid'));
       }
       }
 
 
-      return new Promise(function(resolve, reject) {
-        if (!userData || !userData._id) {
-          reject(new Error('User data is not valid'));
+      var added = self.seenUsers.addToSet(userData);
+      self.save(function(err, data) {
+        if (err) {
+          return reject(err);
         }
         }
 
 
-        var added = self.seenUsers.addToSet(userData);
-        self.save(function(err, data) {
-          if (err) {
-            return reject(err);
-          }
-
-          debug('seenUsers updated!', added);
-          return resolve(self);
-        });
+        debug('seenUsers updated!', added);
+        return resolve(self);
       });
       });
+    });
   };
   };
 
 
   pageSchema.methods.getSlackChannel = function() {
   pageSchema.methods.getSlackChannel = function() {
@@ -271,7 +275,7 @@ module.exports = function(crowi) {
         {path: 'revision', model: 'Revision'},
         {path: 'revision', model: 'Revision'},
         //{path: 'liker', options: { limit: 11 }},
         //{path: 'liker', options: { limit: 11 }},
         //{path: 'seenUsers', options: { limit: 11 }},
         //{path: 'seenUsers', options: { limit: 11 }},
-      ], function (err, pageData) {
+      ], function(err, pageData) {
         Page.populate(pageData, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, data) {
         Page.populate(pageData, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, data) {
           if (err) {
           if (err) {
             return reject(err);
             return reject(err);
@@ -307,10 +311,9 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.updateCommentCount = function (pageId)
-  {
+  pageSchema.statics.updateCommentCount = function(pageId) {
     var self = this;
     var self = this;
-    var Comment = crowi.model("Comment");
+    var Comment = crowi.model('Comment');
     return Comment.countCommentByPageId(pageId)
     return Comment.countCommentByPageId(pageId)
     .then(function(count) {
     .then(function(count) {
       self.update({_id: pageId}, {commentCount: count}, {}, function(err, data) {
       self.update({_id: pageId}, {commentCount: count}, {}, function(err, data) {
@@ -324,7 +327,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.hasPortalPage = function (path, user, revisionId) {
+  pageSchema.statics.hasPortalPage = function(path, user, revisionId) {
     var self = this;
     var self = this;
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       self.findPage(path, user, revisionId)
       self.findPage(path, user, revisionId)
@@ -341,7 +344,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
-    grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
+    // grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
 
 
     return grantLabels;
     return grantLabels;
@@ -416,7 +419,7 @@ module.exports = function(crowi) {
   pageSchema.statics.fixToCreatableName = function(path) {
   pageSchema.statics.fixToCreatableName = function(path) {
     return path
     return path
       .replace(/\/\//g, '/')
       .replace(/\/\//g, '/')
-      ;
+    ;
   };
   };
 
 
   pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
   pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
@@ -497,22 +500,23 @@ module.exports = function(crowi) {
             return resolve(null);
             return resolve(null);
           }
           }
 
 
-          var pageNotFoundError = new Error('Page Not Found')
+          var pageNotFoundError = new Error('Page Not Found');
           pageNotFoundError.name = 'Crowi:Page:NotFound';
           pageNotFoundError.name = 'Crowi:Page:NotFound';
           return reject(pageNotFoundError);
           return reject(pageNotFoundError);
         }
         }
 
 
         if (!pageData.isGrantedFor(userData)) {
         if (!pageData.isGrantedFor(userData)) {
           PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
           PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
-            .then(function (checkResult) {
+            .then(function(checkResult) {
               if (!checkResult) {
               if (!checkResult) {
                 return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
                 return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
-              } else {
+              }
+              else {
                 // return resolve(pageData);
                 // return resolve(pageData);
                 self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
                 self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
               }
               }
             })
             })
-            .catch(function (err) {
+            .catch(function(err) {
               return reject(err);
               return reject(err);
             });
             });
         }
         }
@@ -625,7 +629,7 @@ module.exports = function(crowi) {
     var Page = this
     var Page = this
       , options = options || {}
       , options = options || {}
       , publicOnly = options.publicOnly || true
       , publicOnly = options.publicOnly || true
-      , criteria = {redirectTo: null,}
+      , criteria = {redirectTo: null, }
       ;
       ;
 
 
     if (publicOnly) {
     if (publicOnly) {
@@ -697,7 +701,7 @@ module.exports = function(crowi) {
           Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS})
           Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS})
           .then(resolve)
           .then(resolve)
           .catch(reject);
           .catch(reject);
-        })
+        });
     });
     });
   };
   };
 
 
@@ -709,6 +713,7 @@ module.exports = function(crowi) {
 
 
     // ignore other pages than descendants
     // ignore other pages than descendants
     path = Page.addSlashOfEnd(path);
     path = Page.addSlashOfEnd(path);
+
     // add option to escape the regex strings
     // add option to escape the regex strings
     const combinedOption = Object.assign({isRegExpEscapedFromPath: true}, option);
     const combinedOption = Object.assign({isRegExpEscapedFromPath: true}, option);
 
 
@@ -718,8 +723,12 @@ module.exports = function(crowi) {
   /**
   /**
    * generate the query to find pages that start with `path`
    * generate the query to find pages that start with `path`
    *
    *
-   * If `path` has `/` at the end, returns '{path}/*' and '{path}' self.
-   * If `path` doesn't have `/` at the end, returns '{path}*'
+   * (GROWI) If 'isRegExpEscapedFromPath' is true, `path` should have `/` at the end
+   *   -> returns '{path}/*' and '{path}' self.
+   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` has `/` at the end
+   *   -> returns '{path}*'
+   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` doesn't have `/` at the end
+   *   -> returns '{path}*'
    *
    *
    * *option*
    * *option*
    *   - includeDeletedPage -- if true, search deleted pages (default: false)
    *   - includeDeletedPage -- if true, search deleted pages (default: false)
@@ -731,17 +740,22 @@ module.exports = function(crowi) {
     var includeDeletedPage = option.includeDeletedPage || false;
     var includeDeletedPage = option.includeDeletedPage || false;
     var isRegExpEscapedFromPath = option.isRegExpEscapedFromPath || false;
     var isRegExpEscapedFromPath = option.isRegExpEscapedFromPath || false;
 
 
+    /*
+     * 1. add condition for finding the page completely match with `path` w/o last slash
+     */
     let pathSlashOmitted = path;
     let pathSlashOmitted = path;
     if (path.match(/\/$/)) {
     if (path.match(/\/$/)) {
-      // add condition for finding the page completely match with `path`
       pathSlashOmitted = path.substr(0, path.length -1);
       pathSlashOmitted = path.substr(0, path.length -1);
       pathCondition.push({path: pathSlashOmitted});
       pathCondition.push({path: pathSlashOmitted});
     }
     }
 
 
-    // create forward match pattern
+    /*
+     * 2. add decendants
+     */
     var pattern = (isRegExpEscapedFromPath)
     var pattern = (isRegExpEscapedFromPath)
-      ? escapeStringRegexp(pathSlashOmitted)  // escape
+      ? escapeStringRegexp(path)  // escape
       : pathSlashOmitted;
       : pathSlashOmitted;
+
     var queryReg = new RegExp('^' + pattern);
     var queryReg = new RegExp('^' + pattern);
     pathCondition.push({path: queryReg});
     pathCondition.push({path: queryReg});
 
 
@@ -753,7 +767,7 @@ module.exports = function(crowi) {
         {grant: GRANT_RESTRICTED, grantedUsers: userData._id},
         {grant: GRANT_RESTRICTED, grantedUsers: userData._id},
         {grant: GRANT_SPECIFIED, grantedUsers: userData._id},
         {grant: GRANT_SPECIFIED, grantedUsers: userData._id},
         {grant: GRANT_OWNER, grantedUsers: userData._id},
         {grant: GRANT_OWNER, grantedUsers: userData._id},
-      ],})
+      ], })
       .and({
       .and({
         $or: pathCondition
         $or: pathCondition
       });
       });
@@ -768,7 +782,7 @@ module.exports = function(crowi) {
     }
     }
 
 
     return q;
     return q;
-  }
+  };
 
 
   pageSchema.statics.updatePageProperty = function(page, updateData) {
   pageSchema.statics.updatePageProperty = function(page, updateData) {
     var Page = this;
     var Page = this;
@@ -784,7 +798,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.updateGrant = function (page, grant, userData, grantUserGroupId) {
+  pageSchema.statics.updateGrant = function(page, grant, userData, grantUserGroupId) {
     var Page = this;
     var Page = this;
 
 
     if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
     if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
@@ -794,7 +808,8 @@ module.exports = function(crowi) {
       page.grant = grant;
       page.grant = grant;
       if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
       if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
         page.grantedUsers = [];
-      } else {
+      }
+      else {
         page.grantedUsers = [];
         page.grantedUsers = [];
         page.grantedUsers.push(userData._id);
         page.grantedUsers.push(userData._id);
       }
       }
@@ -813,7 +828,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.updateGrantUserGroup = function (page, grant, grantUserGroupId, userData) {
+  pageSchema.statics.updateGrantUserGroup = function(page, grant, grantUserGroupId, userData) {
     var UserGroupRelation = crowi.model('UserGroupRelation');
     var UserGroupRelation = crowi.model('UserGroupRelation');
     var PageGroupRelation = crowi.model('PageGroupRelation');
     var PageGroupRelation = crowi.model('PageGroupRelation');
 
 
@@ -897,50 +912,50 @@ module.exports = function(crowi) {
       , grantUserGroupId = options.grantUserGroupId || null;
       , grantUserGroupId = options.grantUserGroupId || null;
 
 
     // force public
     // force public
-      if (isPortalPath(path)) {
-        grant = GRANT_PUBLIC;
-      }
+    if (isPortalPath(path)) {
+      grant = GRANT_PUBLIC;
+    }
 
 
-      return new Promise(function(resolve, reject) {
-        Page.findOne({path: path}, function(err, pageData) {
-          if (pageData) {
-            return reject(new Error('Cannot create new page to existed path'));
-          }
+    return new Promise(function(resolve, reject) {
+      Page.findOne({path: path}, function(err, pageData) {
+        if (pageData) {
+          return reject(new Error('Cannot create new page to existed path'));
+        }
 
 
-          var newPage = new Page();
-          newPage.path = path;
-          newPage.creator = user;
-          newPage.lastUpdateUser = user;
-          newPage.createdAt = Date.now();
-          newPage.updatedAt = Date.now();
-          newPage.redirectTo = redirectTo;
-          newPage.grant = grant;
-          newPage.status = STATUS_PUBLISHED;
-          newPage.grantedUsers = [];
-          newPage.grantedUsers.push(user);
-
-          newPage.save(function (err, newPage) {
-            if (err) {
-              return reject(err);
-            }
+        var newPage = new Page();
+        newPage.path = path;
+        newPage.creator = user;
+        newPage.lastUpdateUser = user;
+        newPage.createdAt = Date.now();
+        newPage.updatedAt = Date.now();
+        newPage.redirectTo = redirectTo;
+        newPage.grant = grant;
+        newPage.status = STATUS_PUBLISHED;
+        newPage.grantedUsers = [];
+        newPage.grantedUsers.push(user);
+
+        newPage.save(function(err, newPage) {
+          if (err) {
+            return reject(err);
+          }
 
 
-            if (newPage.grant == Page.GRANT_USER_GROUP && grantUserGroupId != null) {
-              Page.updateGrantUserGroup(newPage, grant, grantUserGroupId, user)
+          if (newPage.grant == Page.GRANT_USER_GROUP && grantUserGroupId != null) {
+            Page.updateGrantUserGroup(newPage, grant, grantUserGroupId, user)
               .catch((err) => {
               .catch((err) => {
                 return reject(err);
                 return reject(err);
               });
               });
-            }
-            var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
-            Page.pushRevision(newPage, newRevision, user).then(function(data) {
-              resolve(data);
-              pageEvent.emit('create', data, user);
-            }).catch(function(err) {
-              debug('Push Revision Error on create page', err);
-              return reject(err);
-            });
+          }
+          var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
+          Page.pushRevision(newPage, newRevision, user).then(function(data) {
+            resolve(data);
+            pageEvent.emit('create', data, user);
+          }).catch(function(err) {
+            debug('Push Revision Error on create page', err);
+            return reject(err);
           });
           });
         });
         });
       });
       });
+    });
   };
   };
 
 
   pageSchema.statics.updatePage = function(pageData, body, user, options) {
   pageSchema.statics.updatePage = function(pageData, body, user, options) {
@@ -961,7 +976,8 @@ module.exports = function(crowi) {
             resolve(data);
             resolve(data);
             pageEvent.emit('update', data, user);
             pageEvent.emit('update', data, user);
           });
           });
-        } else {
+        }
+        else {
           resolve(pageData);
           resolve(pageData);
           pageEvent.emit('update', pageData, user);
           pageEvent.emit('update', pageData, user);
         }
         }
@@ -986,30 +1002,31 @@ module.exports = function(crowi) {
           // が、 /trash 以下にページが有るのは、個別に作っていたケースのみ。
           // が、 /trash 以下にページが有るのは、個別に作っていたケースのみ。
           // 一応しばらく前から uncreatable pages になっているのでこれでいいことにする
           // 一応しばらく前から uncreatable pages になっているのでこれでいいことにする
           debug('Deleted the page, and rename it', pageData.path, newPath);
           debug('Deleted the page, and rename it', pageData.path, newPath);
-          return Page.rename(pageData, newPath, user, {createRedirectPage: true})
+          return Page.rename(pageData, newPath, user, {createRedirectPage: true});
         }).then(function(pageData) {
         }).then(function(pageData) {
           resolve(pageData);
           resolve(pageData);
         }).catch(reject);
         }).catch(reject);
       });
       });
-    } else {
+    }
+    else {
       return Promise.reject('Page is not deletable.');
       return Promise.reject('Page is not deletable.');
     }
     }
   };
   };
 
 
-  pageSchema.statics.deletePageRecursively = function (pageData, user, options) {
+  pageSchema.statics.deletePageRecursively = function(pageData, user, options) {
     var Page = this
     var Page = this
       , path = pageData.path
       , path = pageData.path
       , options = options || {}
       , options = options || {}
       ;
       ;
 
 
-    return new Promise(function (resolve, reject) {
+    return new Promise(function(resolve, reject) {
       Page
       Page
       .generateQueryToListWithDescendants(path, user, options)
       .generateQueryToListWithDescendants(path, user, options)
-      .then(function (pages) {
-        Promise.all(pages.map(function (page) {
+      .then(function(pages) {
+        Promise.all(pages.map(function(page) {
           return Page.deletePage(page, user, options);
           return Page.deletePage(page, user, options);
         }))
         }))
-        .then(function (data) {
+        .then(function(data) {
           return resolve(pageData);
           return resolve(pageData);
         });
         });
       });
       });
@@ -1034,12 +1051,12 @@ module.exports = function(crowi) {
 
 
         return Page.completelyDeletePage(originPageData);
         return Page.completelyDeletePage(originPageData);
       }).then(function(done) {
       }).then(function(done) {
-        return Page.updatePageProperty(pageData, {status: STATUS_PUBLISHED, lastUpdateUser: user})
+        return Page.updatePageProperty(pageData, {status: STATUS_PUBLISHED, lastUpdateUser: user});
       }).then(function(done) {
       }).then(function(done) {
         pageData.status = STATUS_PUBLISHED;
         pageData.status = STATUS_PUBLISHED;
 
 
         debug('Revert deleted the page, and rename again it', pageData, newPath);
         debug('Revert deleted the page, and rename again it', pageData, newPath);
-        return Page.rename(pageData, newPath, user, {})
+        return Page.rename(pageData, newPath, user, {});
       }).then(function(done) {
       }).then(function(done) {
         pageData.path = newPath;
         pageData.path = newPath;
         resolve(pageData);
         resolve(pageData);
@@ -1047,25 +1064,25 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.revertDeletedPageRecursively = function (pageData, user, options) {
+  pageSchema.statics.revertDeletedPageRecursively = function(pageData, user, options) {
     var Page = this
     var Page = this
       , path = pageData.path
       , path = pageData.path
       , options = options || { includeDeletedPage: true}
       , options = options || { includeDeletedPage: true}
       ;
       ;
 
 
-      return new Promise(function (resolve, reject) {
-        Page
+    return new Promise(function(resolve, reject) {
+      Page
         .generateQueryToListWithDescendants(path, user, options)
         .generateQueryToListWithDescendants(path, user, options)
-        .then(function (pages) {
-          Promise.all(pages.map(function (page) {
+        .then(function(pages) {
+          Promise.all(pages.map(function(page) {
             return Page.revertDeletedPage(page, user, options);
             return Page.revertDeletedPage(page, user, options);
           }))
           }))
-          .then(function (data) {
+          .then(function(data) {
             return resolve(data[0]);
             return resolve(data[0]);
           });
           });
         });
         });
-      });
-    };
+    });
+  };
 
 
   /**
   /**
    * This is danger.
    * This is danger.
@@ -1102,21 +1119,21 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.completelyDeletePageRecursively = function (pageData, user, options) {
+  pageSchema.statics.completelyDeletePageRecursively = function(pageData, user, options) {
     // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
     // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
     var Page = this
     var Page = this
       , path = pageData.path
       , path = pageData.path
       , options = options || { includeDeletedPage: true }
       , options = options || { includeDeletedPage: true }
       ;
       ;
 
 
-    return new Promise(function (resolve, reject) {
+    return new Promise(function(resolve, reject) {
       Page
       Page
       .generateQueryToListWithDescendants(path, user, options)
       .generateQueryToListWithDescendants(path, user, options)
-      .then(function (pages) {
-        Promise.all(pages.map(function (page) {
+      .then(function(pages) {
+        Promise.all(pages.map(function(page) {
           return Page.completelyDeletePage(page, user, options);
           return Page.completelyDeletePage(page, user, options);
         }))
         }))
-        .then(function (data) {
+        .then(function(data) {
           return resolve(data[0]);
           return resolve(data[0]);
         });
         });
       });
       });
@@ -1166,13 +1183,13 @@ module.exports = function(crowi) {
         return Page.removePageById(redirectOriginPageData.id)
         return Page.removePageById(redirectOriginPageData.id)
           // remove recursive
           // remove recursive
           .then(() => {
           .then(() => {
-            return Page.removeRedirectOriginPageByPath(redirectOriginPageData.path)
+            return Page.removeRedirectOriginPageByPath(redirectOriginPageData.path);
           });
           });
       })
       })
       .catch((err) => {
       .catch((err) => {
         // do nothing if origin page doesn't exist
         // do nothing if origin page doesn't exist
         return Promise.resolve();
         return Promise.resolve();
-      })
+      });
   };
   };
 
 
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
@@ -1187,14 +1204,15 @@ module.exports = function(crowi) {
       Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})
       Page.updatePageProperty(pageData, {updatedAt: Date.now(), path: newPagePath, lastUpdateUser: user})
       .then(function(data) {
       .then(function(data) {
         // reivisions の path を変更
         // reivisions の path を変更
-        return Revision.updateRevisionListByPath(path, {path: newPagePath}, {})
+        return Revision.updateRevisionListByPath(path, {path: newPagePath}, {});
       }).then(function(data) {
       }).then(function(data) {
         pageData.path = newPagePath;
         pageData.path = newPagePath;
 
 
         if (createRedirectPage) {
         if (createRedirectPage) {
           var body = 'redirect ' + newPagePath;
           var body = 'redirect ' + newPagePath;
           Page.create(path, body, user, {redirectTo: newPagePath}).then(resolve).catch(reject);
           Page.create(path, body, user, {redirectTo: newPagePath}).then(resolve).catch(reject);
-        } else {
+        }
+        else {
           resolve(data);
           resolve(data);
         }
         }
         pageEvent.emit('update', pageData, user); // update as renamed page
         pageEvent.emit('update', pageData, user); // update as renamed page
@@ -1237,7 +1255,7 @@ module.exports = function(crowi) {
       returnPath += '/';
       returnPath += '/';
     }
     }
     return returnPath;
     return returnPath;
-  }
+  };
 
 
   pageSchema.statics.GRANT_PUBLIC = GRANT_PUBLIC;
   pageSchema.statics.GRANT_PUBLIC = GRANT_PUBLIC;
   pageSchema.statics.GRANT_RESTRICTED = GRANT_RESTRICTED;
   pageSchema.statics.GRANT_RESTRICTED = GRANT_RESTRICTED;

+ 9 - 6
lib/models/revision.js

@@ -1,10 +1,13 @@
 module.exports = function(crowi) {
 module.exports = function(crowi) {
-  var debug = require('debug')('growi:models:revision')
-    , mongoose = require('mongoose')
-    , Xss = require('../util/xss')
+  /* eslint-disable no-unused-vars */
+  const logger = require('@alias/logger')('growi:models:revision');
+  /* eslint-enable */
+
+  var mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
     , revisionSchema;
     , revisionSchema;
 
 
+
   revisionSchema = new mongoose.Schema({
   revisionSchema = new mongoose.Schema({
     path: { type: String, required: true },
     path: { type: String, required: true },
     body: { type: String, required: true },
     body: { type: String, required: true },
@@ -47,12 +50,12 @@ module.exports = function(crowi) {
 
 
           return resolve(data);
           return resolve(data);
         });
         });
-      });
+    });
   };
   };
 
 
   revisionSchema.statics.findRevisions = function(ids) {
   revisionSchema.statics.findRevisions = function(ids) {
     var Revision = this,
     var Revision = this,
-        User = crowi.model('User');
+      User = crowi.model('User');
 
 
     if (!Array.isArray(ids)) {
     if (!Array.isArray(ids)) {
       return Promise.reject('The argument was not Array.');
       return Promise.reject('The argument was not Array.');
@@ -82,7 +85,7 @@ module.exports = function(crowi) {
 
 
   revisionSchema.statics.findRevisionList = function(path, options) {
   revisionSchema.statics.findRevisionList = function(path, options) {
     var Revision = this,
     var Revision = this,
-        User = crowi.model('User');
+      User = crowi.model('User');
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       Revision.find({path: path})
       Revision.find({path: path})

+ 15 - 21
lib/models/updatePost.js

@@ -5,6 +5,7 @@ module.exports = function(crowi) {
   var debug = require('debug')('growi:models:updatePost')
   var debug = require('debug')('growi:models:updatePost')
     , mongoose = require('mongoose')
     , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
+    , updatePostSchema
   ;
   ;
 
 
   // TODO: slack 以外の対応
   // TODO: slack 以外の対応
@@ -18,13 +19,11 @@ module.exports = function(crowi) {
     createdAt: { type: Date, default: Date.now }
     createdAt: { type: Date, default: Date.now }
   });
   });
 
 
-  updatePostSchema.statics.normalizeChannelName = function(channel)
-  {
+  updatePostSchema.statics.normalizeChannelName = function(channel) {
     return channel.replace(/(#|,)/g, '');
     return channel.replace(/(#|,)/g, '');
-  }
+  };
 
 
-  updatePostSchema.statics.createPrefixesByPathPattern = function(pathPattern)
-  {
+  updatePostSchema.statics.createPrefixesByPathPattern = function(pathPattern) {
     var patternPrefix = ['*', '*'];
     var patternPrefix = ['*', '*'];
 
 
     // not begin with slash
     // not begin with slash
@@ -42,10 +41,9 @@ module.exports = function(crowi) {
       patternPrefix[1] = pattern[1];
       patternPrefix[1] = pattern[1];
     }
     }
     return patternPrefix;
     return patternPrefix;
-  }
+  };
 
 
-  updatePostSchema.statics.getRegExpByPattern = function(pattern)
-  {
+  updatePostSchema.statics.getRegExpByPattern = function(pattern) {
     var reg = pattern;
     var reg = pattern;
     if (!reg.match(/^\/.*/)) {
     if (!reg.match(/^\/.*/)) {
       reg = '/*' + reg + '*';
       reg = '/*' + reg + '*';
@@ -55,10 +53,9 @@ module.exports = function(crowi) {
     reg = reg.replace(/(\*)/g, '.*');
     reg = reg.replace(/(\*)/g, '.*');
 
 
     return new RegExp(reg);
     return new RegExp(reg);
-  }
+  };
 
 
-  updatePostSchema.statics.findSettingsByPath = function(path)
-  {
+  updatePostSchema.statics.findSettingsByPath = function(path) {
     var UpdatePost = this;
     var UpdatePost = this;
     var prefixes = UpdatePost.createPrefixesByPathPattern(path);
     var prefixes = UpdatePost.createPrefixesByPathPattern(path);
 
 
@@ -83,8 +80,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  updatePostSchema.statics.findAll = function(offset)
-  {
+  updatePostSchema.statics.findAll = function(offset) {
     var UpdatePost = this;
     var UpdatePost = this;
     var offset = offset || 0;
     var offset = offset || 0;
 
 
@@ -107,8 +103,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  updatePostSchema.statics.create = function(pathPattern, channel, user)
-  {
+  updatePostSchema.statics.create = function(pathPattern, channel, user) {
     var UpdatePost = this;
     var UpdatePost = this;
     var provider = 'slack'; // now slack only
     var provider = 'slack'; // now slack only
 
 
@@ -124,17 +119,16 @@ module.exports = function(crowi) {
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       notif.save(function(err, doc) {
       notif.save(function(err, doc) {
-       if (err) {
-         return reject(err);
-       }
+        if (err) {
+          return reject(err);
+        }
 
 
-       return resolve(doc);
+        return resolve(doc);
       });
       });
     });
     });
   };
   };
 
 
-  updatePostSchema.statics.remove = function(id)
-  {
+  updatePostSchema.statics.remove = function(id) {
     var UpdatePost = this;
     var UpdatePost = this;
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {

+ 6 - 6
lib/models/user-group-relation.js

@@ -54,7 +54,7 @@ class UserGroupRelation {
       .populate('relatedUser')
       .populate('relatedUser')
       .populate('relatedGroup')
       .populate('relatedGroup')
       .exec();
       .exec();
-  };
+  }
 
 
   /**
   /**
    * find all user and group relation of UserGroup
    * find all user and group relation of UserGroup
@@ -146,7 +146,7 @@ class UserGroupRelation {
     const query = {
     const query = {
       relatedGroup: userGroupId,
       relatedGroup: userGroupId,
       relatedUser: userData.id
       relatedUser: userData.id
-    }
+    };
 
 
     return this
     return this
       .findOne(query)
       .findOne(query)
@@ -173,7 +173,7 @@ class UserGroupRelation {
         });
         });
         const query = { _id: { $nin: relatedUserIds }, status: User.STATUS_ACTIVE };
         const query = { _id: { $nin: relatedUserIds }, status: User.STATUS_ACTIVE };
 
 
-        debug("findUserByNotRelatedGroup ", query);
+        debug('findUserByNotRelatedGroup ', query);
         return User.find(query).exec();
         return User.find(query).exec();
       });
       });
   }
   }
@@ -191,7 +191,7 @@ class UserGroupRelation {
     const query = {
     const query = {
       relatedGroup: userGroup.id,
       relatedGroup: userGroup.id,
       relatedUser: userData.id
       relatedUser: userData.id
-    }
+    };
 
 
     return this
     return this
       .count(query)
       .count(query)
@@ -272,8 +272,8 @@ class UserGroupRelation {
 
 
 }
 }
 
 
-module.exports = function (crowi) {
+module.exports = function(crowi) {
   UserGroupRelation.crowi = crowi;
   UserGroupRelation.crowi = crowi;
   schema.loadClass(UserGroupRelation);
   schema.loadClass(UserGroupRelation);
   return mongoose.model('UserGroupRelation', schema);
   return mongoose.model('UserGroupRelation', schema);
-}
+};

+ 8 - 8
lib/models/user-group.js

@@ -43,18 +43,18 @@ class UserGroup {
    * model static methods
    * model static methods
    */
    */
 
 
-   // グループ画像パスの生成
+  // グループ画像パスの生成
   static createUserGroupPictureFilePath(userGroup, name) {
   static createUserGroupPictureFilePath(userGroup, name) {
     var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
     var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
 
 
     return 'userGroup/' + userGroup._id + ext;
     return 'userGroup/' + userGroup._id + ext;
-  };
+  }
 
 
   // すべてのグループを取得(オプション指定可)
   // すべてのグループを取得(オプション指定可)
   static findAllGroups(option) {
   static findAllGroups(option) {
 
 
     return this.find().exec();
     return this.find().exec();
-  };
+  }
 
 
   /**
   /**
    * find all entities with pagination
    * find all entities with pagination
@@ -80,13 +80,13 @@ class UserGroup {
       .catch((err) => {
       .catch((err) => {
         debug('Error on pagination:', err);
         debug('Error on pagination:', err);
       });
       });
-  };
+  }
 
 
   // TBD: グループ名によるグループ検索
   // TBD: グループ名によるグループ検索
   static findUserGroupByName(name) {
   static findUserGroupByName(name) {
     const query = { name: name };
     const query = { name: name };
     return this.findOne(query);
     return this.findOne(query);
-  };
+  }
 
 
   // 登録可能グループ名確認
   // 登録可能グループ名確認
   static isRegisterableName(name) {
   static isRegisterableName(name) {
@@ -96,7 +96,7 @@ class UserGroup {
       .then((userGroupData) => {
       .then((userGroupData) => {
         return (userGroupData == null);
         return (userGroupData == null);
       });
       });
-  };
+  }
 
 
   // グループの完全削除
   // グループの完全削除
   static removeCompletelyById(id) {
   static removeCompletelyById(id) {
@@ -143,9 +143,9 @@ class UserGroup {
 }
 }
 
 
 
 
-module.exports = function (crowi) {
+module.exports = function(crowi) {
   UserGroup.crowi = crowi;
   UserGroup.crowi = crowi;
   schema.loadClass(UserGroup);
   schema.loadClass(UserGroup);
   return mongoose.model('UserGroup', schema);
   return mongoose.model('UserGroup', schema);
-}
+};
 
 

+ 38 - 36
lib/models/user.js

@@ -57,7 +57,7 @@ module.exports = function(crowi) {
 
 
   userEvent.on('activated', userEvent.onActivated);
   userEvent.on('activated', userEvent.onActivated);
 
 
-  function decideUserStatusOnRegistration () {
+  function decideUserStatusOnRegistration() {
     var Config = crowi.model('Config'),
     var Config = crowi.model('Config'),
       config = crowi.getConfig();
       config = crowi.getConfig();
 
 
@@ -79,10 +79,10 @@ module.exports = function(crowi) {
 
 
   function generateRandomEmail() {
   function generateRandomEmail() {
     const randomstr = generateRandomTempPassword();
     const randomstr = generateRandomTempPassword();
-    return `change-it-${randomstr}@example.com`
+    return `change-it-${randomstr}@example.com`;
   }
   }
 
 
-  function generateRandomTempPassword () {
+  function generateRandomTempPassword() {
     var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
     var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
     var password = '';
     var password = '';
     var len = 12;
     var len = 12;
@@ -95,14 +95,14 @@ module.exports = function(crowi) {
     return password;
     return password;
   }
   }
 
 
-  function generatePassword (password) {
+  function generatePassword(password) {
     var hasher = crypto.createHash('sha256');
     var hasher = crypto.createHash('sha256');
     hasher.update(crowi.env.PASSWORD_SEED + password);
     hasher.update(crowi.env.PASSWORD_SEED + password);
 
 
     return hasher.digest('hex');
     return hasher.digest('hex');
   }
   }
 
 
-  function generateApiToken (user) {
+  function generateApiToken(user) {
     var hasher = crypto.createHash('sha256');
     var hasher = crypto.createHash('sha256');
     hasher.update((new Date).getTime() + user._id);
     hasher.update((new Date).getTime() + user._id);
 
 
@@ -179,7 +179,8 @@ module.exports = function(crowi) {
       self.save(function(err, userData) {
       self.save(function(err, userData) {
         if (err) {
         if (err) {
           return reject(err);
           return reject(err);
-        } else {
+        }
+        else {
           return resolve(userData);
           return resolve(userData);
         }
         }
       });
       });
@@ -339,7 +340,7 @@ module.exports = function(crowi) {
       .sort(sort)
       .sort(sort)
       .skip(options.skip || 0)
       .skip(options.skip || 0)
       .limit(options.limit || 21)
       .limit(options.limit || 21)
-      .exec(function (err, userData) {
+      .exec(function(err, userData) {
         callback(err, userData);
         callback(err, userData);
       });
       });
 
 
@@ -360,10 +361,10 @@ module.exports = function(crowi) {
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       User
       User
         .find()
         .find()
-        .or(status.map(s => { return {status: s}; }))
+        .or(status.map(s => { return {status: s} }))
         .select(fields)
         .select(fields)
         .sort(sort)
         .sort(sort)
-        .exec(function (err, userData) {
+        .exec(function(err, userData) {
           if (err) {
           if (err) {
             return reject(err);
             return reject(err);
           }
           }
@@ -387,7 +388,7 @@ module.exports = function(crowi) {
         .find({ _id: { $in: ids }, status: status })
         .find({ _id: { $in: ids }, status: status })
         .select(fields)
         .select(fields)
         .sort(sort)
         .sort(sort)
-        .exec(function (err, userData) {
+        .exec(function(err, userData) {
           if (err) {
           if (err) {
             return reject(err);
             return reject(err);
           }
           }
@@ -416,7 +417,7 @@ module.exports = function(crowi) {
       }
       }
 
 
       return callback(err, result);
       return callback(err, result);
-    }, { sortBy : sort });
+    }, { sortBy: sort });
   };
   };
 
 
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
@@ -462,7 +463,7 @@ module.exports = function(crowi) {
     if (googleId == null) {
     if (googleId == null) {
       callback(null, null);
       callback(null, null);
     }
     }
-    this.findOne({googleId}, function (err, userData) {
+    this.findOne({googleId}, function(err, userData) {
       callback(err, userData);
       callback(err, userData);
     });
     });
   };
   };
@@ -480,7 +481,7 @@ module.exports = function(crowi) {
 
 
   userSchema.statics.findUserByEmailAndPassword = function(email, password, callback) {
   userSchema.statics.findUserByEmailAndPassword = function(email, password, callback) {
     var hashedPassword = generatePassword(password);
     var hashedPassword = generatePassword(password);
-    this.findOne({email: email, password: hashedPassword}, function (err, userData) {
+    this.findOne({email: email, password: hashedPassword}, function(err, userData) {
       callback(err, userData);
       callback(err, userData);
     });
     });
   };
   };
@@ -489,7 +490,7 @@ module.exports = function(crowi) {
     var User = this;
     var User = this;
     var usernameUsable = true;
     var usernameUsable = true;
 
 
-    this.findOne({username: username}, function (err, userData) {
+    this.findOne({username: username}, function(err, userData) {
       if (userData) {
       if (userData) {
         usernameUsable = false;
         usernameUsable = false;
       }
       }
@@ -503,13 +504,13 @@ module.exports = function(crowi) {
     var usernameUsable = true;
     var usernameUsable = true;
 
 
     // username check
     // username check
-    this.findOne({username: username}, function (err, userData) {
+    this.findOne({username: username}, function(err, userData) {
       if (userData) {
       if (userData) {
         usernameUsable = false;
         usernameUsable = false;
       }
       }
 
 
       // email check
       // email check
-      User.findOne({email: email}, function (err, userData) {
+      User.findOne({email: email}, function(err, userData) {
         if (userData) {
         if (userData) {
           emailUsable = false;
           emailUsable = false;
         }
         }
@@ -525,7 +526,7 @@ module.exports = function(crowi) {
 
 
   userSchema.statics.removeCompletelyById = function(id, callback) {
   userSchema.statics.removeCompletelyById = function(id, callback) {
     var User = this;
     var User = this;
-    User.findById(id, function (err, userData) {
+    User.findById(id, function(err, userData) {
       if (!userData) {
       if (!userData) {
         return callback(err, null);
         return callback(err, null);
       }
       }
@@ -551,7 +552,7 @@ module.exports = function(crowi) {
     var User = this;
     var User = this;
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
-      User.findById(id, function (err, userData) {
+      User.findById(id, function(err, userData) {
         if (!userData) {
         if (!userData) {
           return reject(new Error('User not found'));
           return reject(new Error('User not found'));
         }
         }
@@ -587,13 +588,13 @@ module.exports = function(crowi) {
       emailList,
       emailList,
       function(email, next) {
       function(email, next) {
         var newUser = new User()
         var newUser = new User()
-          ,tmpUsername, password;
+          , tmpUsername, password;
 
 
         email = email.trim();
         email = email.trim();
 
 
         // email check
         // email check
         // TODO: 削除済みはチェック対象から外そう〜
         // TODO: 削除済みはチェック対象から外そう〜
-        User.findOne({email: email}, function (err, userData) {
+        User.findOne({email: email}, function(err, userData) {
           // The user is exists
           // The user is exists
           if (userData) {
           if (userData) {
             createdUserList.push({
             createdUserList.push({
@@ -622,7 +623,8 @@ module.exports = function(crowi) {
                 user: null,
                 user: null,
               });
               });
               debug('save failed!! ', err);
               debug('save failed!! ', err);
-            } else {
+            }
+            else {
               createdUserList.push({
               createdUserList.push({
                 email: email,
                 email: email,
                 password: password,
                 password: password,
@@ -650,20 +652,20 @@ module.exports = function(crowi) {
               }
               }
 
 
               mailer.send({
               mailer.send({
-                  to: user.email,
-                  subject: 'Invitation to ' + Config.appTitle(config),
-                  template: 'admin/userInvitation.txt',
-                  vars: {
-                    email: user.email,
-                    password: user.password,
-                    url: config.crowi['app:url'],
-                    appTitle: Config.appTitle(config),
-                  }
-                },
-                function (err, s) {
-                  debug('completed to send email: ', err, s);
-                  next();
+                to: user.email,
+                subject: 'Invitation to ' + Config.appTitle(config),
+                template: 'admin/userInvitation.txt',
+                vars: {
+                  email: user.email,
+                  password: user.password,
+                  url: config.crowi['app:url'],
+                  appTitle: Config.appTitle(config),
                 }
                 }
+              },
+              function(err, s) {
+                debug('completed to send email: ', err, s);
+                next();
+              }
               );
               );
             },
             },
             function(err) {
             function(err) {
@@ -705,7 +707,7 @@ module.exports = function(crowi) {
       }
       }
       return callback(err, userData);
       return callback(err, userData);
     });
     });
-  }
+  };
 
 
   /**
   /**
    * A wrapper function of createUserByEmailAndPasswordAndStatus
    * A wrapper function of createUserByEmailAndPasswordAndStatus
@@ -732,7 +734,7 @@ module.exports = function(crowi) {
         return resolve(userData);
         return resolve(userData);
       });
       });
     });
     });
-  }
+  };
 
 
   userSchema.statics.createUserPictureFilePath = function(user, name) {
   userSchema.statics.createUserPictureFilePath = function(user, name) {
     var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
     var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];

+ 2 - 2
lib/plugins/plugin-utils-v2.js

@@ -32,9 +32,9 @@ class PluginUtilsV2 {
       name,
       name,
       meta,
       meta,
       entries,
       entries,
-    }
+    };
   }
   }
 
 
 }
 }
 
 
-module.exports = PluginUtilsV2
+module.exports = PluginUtilsV2;

+ 1 - 1
lib/plugins/plugin-utils.js

@@ -91,4 +91,4 @@ class PluginUtils {
   }
   }
 }
 }
 
 
-module.exports = PluginUtils
+module.exports = PluginUtils;

+ 97 - 80
lib/routes/admin.js

@@ -43,14 +43,16 @@ module.exports = function(crowi, app) {
     if (pagerMin === 1) {
     if (pagerMin === 1) {
       if (MAX_PAGE_LIST < pagesCount) {
       if (MAX_PAGE_LIST < pagesCount) {
         pagerMax = MAX_PAGE_LIST;
         pagerMax = MAX_PAGE_LIST;
-      } else {
+      }
+      else {
         pagerMax = pagesCount;
         pagerMax = pagesCount;
       }
       }
     }
     }
     if (pagerMax === pagesCount) {
     if (pagerMax === pagesCount) {
       if ((pagerMax - MAX_PAGE_LIST) < 1) {
       if ((pagerMax - MAX_PAGE_LIST) < 1) {
         pagerMin = 1;
         pagerMin = 1;
-      } else {
+      }
+      else {
         pagerMin = pagerMax - MAX_PAGE_LIST;
         pagerMin = pagerMax - MAX_PAGE_LIST;
       }
       }
     }
     }
@@ -121,7 +123,8 @@ module.exports = function(crowi, app) {
         req.flash('successMessage', ['Successfully updated!']);
         req.flash('successMessage', ['Successfully updated!']);
         return res.redirect('/admin/markdown');
         return res.redirect('/admin/markdown');
       });
       });
-    } else {
+    }
+    else {
       req.flash('errorMessage', req.form.errors);
       req.flash('errorMessage', req.form.errors);
       return res.redirect('/admin/markdown');
       return res.redirect('/admin/markdown');
     }
     }
@@ -134,17 +137,17 @@ module.exports = function(crowi, app) {
     settingForm = Config.setupCofigFormData('crowi', req.config);
     settingForm = Config.setupCofigFormData('crowi', req.config);
 
 
     const highlightJsCssSelectorOptions = {
     const highlightJsCssSelectorOptions = {
-      "github":           { name: '[Light] Github',         border: false },
-      "github-gist":      { name: '[Light] Github Gist',    border: true },
-      "atom-one-light":   { name: '[Light] Atom One Light', border: true },
-      "xcode":            { name: '[Light] Xcode',          border: true },
-      "vs":               { name: '[Light] Vs',             border: true },
-      "atom-one-dark":    { name: '[Dark] Atom One Dark',   border: false },
-      "hybrid":           { name: '[Dark] Hybrid',          border: false },
-      "monokai":          { name: '[Dark] Monokai',         border: false },
-      "tomorrow-night":   { name: '[Dark] Tomorrow Night',  border: false },
-      "vs2015":           { name: '[Dark] Vs 2015',         border: false },
-    }
+      'github':           { name: '[Light] Github',         border: false },
+      'github-gist':      { name: '[Light] Github Gist',    border: true },
+      'atom-one-light':   { name: '[Light] Atom One Light', border: true },
+      'xcode':            { name: '[Light] Xcode',          border: true },
+      'vs':               { name: '[Light] Vs',             border: true },
+      'atom-one-dark':    { name: '[Dark] Atom One Dark',   border: false },
+      'hybrid':           { name: '[Dark] Hybrid',          border: false },
+      'monokai':          { name: '[Dark] Monokai',         border: false },
+      'tomorrow-night':   { name: '[Dark] Tomorrow Night',  border: false },
+      'vs2015':           { name: '[Dark] Vs 2015',         border: false },
+    };
 
 
     return res.render('admin/customize', {
     return res.render('admin/customize', {
       settingForm: settingForm,
       settingForm: settingForm,
@@ -198,7 +201,8 @@ module.exports = function(crowi, app) {
           return res.redirect('/admin/notification');
           return res.redirect('/admin/notification');
         });
         });
       });
       });
-    } else {
+    }
+    else {
       req.flash('errorMessage', req.form.errors);
       req.flash('errorMessage', req.form.errors);
       return res.redirect('/admin/notification');
       return res.redirect('/admin/notification');
     }
     }
@@ -217,16 +221,17 @@ module.exports = function(crowi, app) {
     slack.getOauthAccessToken(code)
     slack.getOauthAccessToken(code)
     .then(data => {
     .then(data => {
       debug('oauth response', data);
       debug('oauth response', data);
-        Config.updateNamespaceByArray('notification', {'slack:token': data.access_token}, function(err, config) {
-          if (err) {
-            req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
-          } else {
-            Config.updateConfigCache('notification', config);
-            req.flash('successMessage', ['Successfully Connected!']);
-          }
+      Config.updateNamespaceByArray('notification', {'slack:token': data.access_token}, function(err, config) {
+        if (err) {
+          req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
+        }
+        else {
+          Config.updateConfigCache('notification', config);
+          req.flash('successMessage', ['Successfully Connected!']);
+        }
 
 
-          return res.redirect('/admin/notification');
-        });
+        return res.redirect('/admin/notification');
+      });
     }).catch(err => {
     }).catch(err => {
       debug('oauth response ERROR', err);
       debug('oauth response ERROR', err);
       req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
       req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
@@ -254,7 +259,8 @@ module.exports = function(crowi, app) {
           return res.redirect('/admin/notification#slack-incoming-webhooks');
           return res.redirect('/admin/notification#slack-incoming-webhooks');
         });
         });
       });
       });
-    } else {
+    }
+    else {
       req.flash('errorMessage', req.form.errors);
       req.flash('errorMessage', req.form.errors);
       return res.redirect('/admin/notification#slack-incoming-webhooks');
       return res.redirect('/admin/notification#slack-incoming-webhooks');
     }
     }
@@ -290,7 +296,7 @@ module.exports = function(crowi, app) {
         });
         });
     })
     })
     .then(function() {
     .then(function() {
-      return search.buildIndex()
+      return search.buildIndex();
     })
     })
     .then(function(data) {
     .then(function(data) {
       if (!data.errors) {
       if (!data.errors) {
@@ -302,7 +308,8 @@ module.exports = function(crowi, app) {
       if (!data.errors) {
       if (!data.errors) {
         debug('Data is successfully indexed.');
         debug('Data is successfully indexed.');
         req.flash('successMessage', 'Data is successfully indexed.');
         req.flash('successMessage', 'Data is successfully indexed.');
-      } else {
+      }
+      else {
         debug('Data index error.', data.errors);
         debug('Data index error.', data.errors);
         req.flash('errorMessage', `Data index error: ${data.errors}`);
         req.flash('errorMessage', `Data index error: ${data.errors}`);
       }
       }
@@ -336,12 +343,14 @@ module.exports = function(crowi, app) {
       User.createUsersByInvitation(form.emailList.split('\n'), toSendEmail, function(err, userList) {
       User.createUsersByInvitation(form.emailList.split('\n'), toSendEmail, function(err, userList) {
         if (err) {
         if (err) {
           req.flash('errorMessage', req.form.errors.join('\n'));
           req.flash('errorMessage', req.form.errors.join('\n'));
-        } else {
+        }
+        else {
           req.flash('createdUser', userList);
           req.flash('createdUser', userList);
         }
         }
         return res.redirect('/admin/users');
         return res.redirect('/admin/users');
       });
       });
-    } else {
+    }
+    else {
       req.flash('errorMessage', req.form.errors.join('\n'));
       req.flash('errorMessage', req.form.errors.join('\n'));
       return res.redirect('/admin/users');
       return res.redirect('/admin/users');
     }
     }
@@ -353,7 +362,8 @@ module.exports = function(crowi, app) {
       userData.makeAdmin(function(err, userData) {
       userData.makeAdmin(function(err, userData) {
         if (err === null) {
         if (err === null) {
           req.flash('successMessage', userData.name + 'さんのアカウントを管理者に設定しました。');
           req.flash('successMessage', userData.name + 'さんのアカウントを管理者に設定しました。');
-        } else {
+        }
+        else {
           req.flash('errorMessage', '更新に失敗しました。');
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
           debug(err, userData);
         }
         }
@@ -368,7 +378,8 @@ module.exports = function(crowi, app) {
       userData.removeFromAdmin(function(err, userData) {
       userData.removeFromAdmin(function(err, userData) {
         if (err === null) {
         if (err === null) {
           req.flash('successMessage', userData.name + 'さんのアカウントを管理者から外しました。');
           req.flash('successMessage', userData.name + 'さんのアカウントを管理者から外しました。');
-        } else {
+        }
+        else {
           req.flash('errorMessage', '更新に失敗しました。');
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
           debug(err, userData);
         }
         }
@@ -383,7 +394,8 @@ module.exports = function(crowi, app) {
       userData.statusActivate(function(err, userData) {
       userData.statusActivate(function(err, userData) {
         if (err === null) {
         if (err === null) {
           req.flash('successMessage', userData.name + 'さんのアカウントを有効化しました');
           req.flash('successMessage', userData.name + 'さんのアカウントを有効化しました');
-        } else {
+        }
+        else {
           req.flash('errorMessage', '更新に失敗しました。');
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
           debug(err, userData);
         }
         }
@@ -399,7 +411,8 @@ module.exports = function(crowi, app) {
       userData.statusSuspend(function(err, userData) {
       userData.statusSuspend(function(err, userData) {
         if (err === null) {
         if (err === null) {
           req.flash('successMessage', userData.name + 'さんのアカウントを利用停止にしました');
           req.flash('successMessage', userData.name + 'さんのアカウントを利用停止にしました');
-        } else {
+        }
+        else {
           req.flash('errorMessage', '更新に失敗しました。');
           req.flash('errorMessage', '更新に失敗しました。');
           debug(err, userData);
           debug(err, userData);
         }
         }
@@ -436,7 +449,7 @@ module.exports = function(crowi, app) {
           throw new Error(err.message);
           throw new Error(err.message);
         }
         }
         return userData;
         return userData;
-      })
+      });
     })
     })
     .then((userData) => {
     .then((userData) => {
       return Page.removePageByPath(`/user/${username}`)
       return Page.removePageByPath(`/user/${username}`)
@@ -461,7 +474,8 @@ module.exports = function(crowi, app) {
       if (err) {
       if (err) {
         debug('Error while removing user.', err, id);
         debug('Error while removing user.', err, id);
         req.flash('errorMessage', '完全な削除に失敗しました。');
         req.flash('errorMessage', '完全な削除に失敗しました。');
-      } else {
+      }
+      else {
         req.flash('successMessage', '削除しました');
         req.flash('successMessage', '削除しました');
       }
       }
       return res.redirect('/admin/users');
       return res.redirect('/admin/users');
@@ -481,7 +495,7 @@ module.exports = function(crowi, app) {
       debug('Error on reseting password', err);
       debug('Error on reseting password', err);
       return res.json(ApiResponse.error('Error'));
       return res.json(ApiResponse.error('Error'));
     });
     });
-  }
+  };
 
 
   actions.externalAccount = {};
   actions.externalAccount = {};
   actions.externalAccount.index = function(req, res) {
   actions.externalAccount.index = function(req, res) {
@@ -494,7 +508,7 @@ module.exports = function(crowi, app) {
         return res.render('admin/external-accounts', {
         return res.render('admin/external-accounts', {
           accounts: result.docs,
           accounts: result.docs,
           pager: pager
           pager: pager
-        })
+        });
       });
       });
   };
   };
 
 
@@ -515,13 +529,13 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.userGroup = {};
   actions.userGroup = {};
-  actions.userGroup.index = function (req, res) {
+  actions.userGroup.index = function(req, res) {
     var page = parseInt(req.query.page) || 1;
     var page = parseInt(req.query.page) || 1;
     var renderVar = {
     var renderVar = {
-      userGroups : [],
-      userGroupRelations : new Map(),
-      pager : null,
-    }
+      userGroups: [],
+      userGroupRelations: new Map(),
+      pager: null,
+    };
 
 
     UserGroup.findUserGroupsWithPagination({ page: page })
     UserGroup.findUserGroupsWithPagination({ page: page })
       .then((result) => {
       .then((result) => {
@@ -539,31 +553,31 @@ module.exports = function(crowi, app) {
         });
         });
       })
       })
       .then((allRelationsPromise) => {
       .then((allRelationsPromise) => {
-        return Promise.all(allRelationsPromise)
+        return Promise.all(allRelationsPromise);
       })
       })
       .then((relations) => {
       .then((relations) => {
         renderVar.userGroupRelations = new Map(relations);
         renderVar.userGroupRelations = new Map(relations);
-        debug("in findUserGroupsWithPagination findAllRelationForUserGroupResult", renderVar.userGroupRelations);
+        debug('in findUserGroupsWithPagination findAllRelationForUserGroupResult', renderVar.userGroupRelations);
         return res.render('admin/user-groups', renderVar);
         return res.render('admin/user-groups', renderVar);
       })
       })
       .catch( function(err) {
       .catch( function(err) {
-          debug('Error on find all relations', err);
-          return res.json(ApiResponse.error('Error'));
+        debug('Error on find all relations', err);
+        return res.json(ApiResponse.error('Error'));
       });
       });
   };
   };
 
 
   // グループ詳細
   // グループ詳細
-  actions.userGroup.detail = function (req, res) {
+  actions.userGroup.detail = function(req, res) {
     var name = req.params.name;
     var name = req.params.name;
     var renderVar = {
     var renderVar = {
       userGroup: null,
       userGroup: null,
       userGroupRelations: [],
       userGroupRelations: [],
       pageGroupRelations: [],
       pageGroupRelations: [],
       notRelatedusers: []
       notRelatedusers: []
-    }
+    };
     var targetUserGroup = null;
     var targetUserGroup = null;
     UserGroup.findUserGroupByName(name)
     UserGroup.findUserGroupByName(name)
-      .then(function (userGroup) {
+      .then(function(userGroup) {
         targetUserGroup = userGroup;
         targetUserGroup = userGroup;
         if (targetUserGroup == null) {
         if (targetUserGroup == null) {
           req.flash('errorMessage', 'グループがありません');
           req.flash('errorMessage', 'グループがありません');
@@ -595,15 +609,15 @@ module.exports = function(crowi, app) {
         debug('Error on get userGroupDetail', err);
         debug('Error on get userGroupDetail', err);
         return res.redirect('/admin/user-groups');
         return res.redirect('/admin/user-groups');
       });
       });
-  }
+  };
 
 
   //グループの生成
   //グループの生成
-  actions.userGroup.create = function (req, res) {
+  actions.userGroup.create = function(req, res) {
     var form = req.form.createGroupForm;
     var form = req.form.createGroupForm;
     if (req.form.isValid) {
     if (req.form.isValid) {
       UserGroup.createGroupByName(form.userGroupName)
       UserGroup.createGroupByName(form.userGroupName)
       .then((newUserGroup) => {
       .then((newUserGroup) => {
-        req.flash('successMessage', newUserGroup.name)
+        req.flash('successMessage', newUserGroup.name);
         req.flash('createdUserGroup', newUserGroup);
         req.flash('createdUserGroup', newUserGroup);
         return res.redirect('/admin/user-groups');
         return res.redirect('/admin/user-groups');
       })
       })
@@ -611,14 +625,15 @@ module.exports = function(crowi, app) {
         debug('create userGroup error:', err);
         debug('create userGroup error:', err);
         req.flash('errorMessage', '同じグループ名が既に存在します。');
         req.flash('errorMessage', '同じグループ名が既に存在します。');
       });
       });
-    } else {
+    }
+    else {
       req.flash('errorMessage', req.form.errors.join('\n'));
       req.flash('errorMessage', req.form.errors.join('\n'));
       return res.redirect('/admin/user-groups');
       return res.redirect('/admin/user-groups');
     }
     }
   };
   };
 
 
   //
   //
-  actions.userGroup.update = function (req, res) {
+  actions.userGroup.update = function(req, res) {
 
 
     var userGroupId = req.params.userGroupId;
     var userGroupId = req.params.userGroupId;
     var name = req.body.name;
     var name = req.body.name;
@@ -654,7 +669,7 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
-  actions.userGroup.uploadGroupPicture = function (req, res) {
+  actions.userGroup.uploadGroupPicture = function(req, res) {
     var fileUploader = require('../util/fileUploader')(crowi, app);
     var fileUploader = require('../util/fileUploader')(crowi, app);
     //var storagePlugin = new pluginService('storage');
     //var storagePlugin = new pluginService('storage');
     //var storage = require('../service/storage').StorageService(config);
     //var storage = require('../service/storage').StorageService(config);
@@ -669,7 +684,7 @@ module.exports = function(crowi, app) {
       });
       });
     }
     }
 
 
-    UserGroup.findById(userGroupId, function (err, userGroupData) {
+    UserGroup.findById(userGroupId, function(err, userGroupData) {
       if (!userGroupData) {
       if (!userGroupData) {
         return res.json({
         return res.json({
           'status': false,
           'status': false,
@@ -691,11 +706,11 @@ module.exports = function(crowi, app) {
       var tmpFileStream = fs.createReadStream(tmpPath, { flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true });
       var tmpFileStream = fs.createReadStream(tmpPath, { flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true });
 
 
       fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
       fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
-        .then(function (data) {
+        .then(function(data) {
           var imageUrl = fileUploader.generateUrl(filePath);
           var imageUrl = fileUploader.generateUrl(filePath);
           userGroupData.updateImage(imageUrl)
           userGroupData.updateImage(imageUrl)
           .then(() => {
           .then(() => {
-            fs.unlink(tmpPath, function (err) {
+            fs.unlink(tmpPath, function(err) {
               if (err) {
               if (err) {
                 debug('Error while deleting tmp file.', err);
                 debug('Error while deleting tmp file.', err);
               }
               }
@@ -707,7 +722,7 @@ module.exports = function(crowi, app) {
               });
               });
             });
             });
           });
           });
-        }).catch(function (err) {
+        }).catch(function(err) {
           debug('Uploading error', err);
           debug('Uploading error', err);
 
 
           return res.json({
           return res.json({
@@ -719,7 +734,7 @@ module.exports = function(crowi, app) {
 
 
   };
   };
 
 
-  actions.userGroup.deletePicture = function (req, res) {
+  actions.userGroup.deletePicture = function(req, res) {
 
 
     var userGroupId = req.params.userGroupId;
     var userGroupId = req.params.userGroupId;
     let userGroupName = null;
     let userGroupName = null;
@@ -753,12 +768,12 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
   // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
-  actions.userGroup.removeCompletely = function (req, res) {
+  actions.userGroup.removeCompletely = function(req, res) {
     const id = req.body.user_group_id;
     const id = req.body.user_group_id;
 
 
     UserGroup.removeCompletelyById(id)
     UserGroup.removeCompletelyById(id)
     .then(() => {
     .then(() => {
-        req.flash('successMessage', '削除しました');
+      req.flash('successMessage', '削除しました');
       return res.redirect('/admin/user-groups');
       return res.redirect('/admin/user-groups');
     })
     })
     .catch((err) => {
     .catch((err) => {
@@ -766,12 +781,12 @@ module.exports = function(crowi, app) {
       req.flash('errorMessage', '完全な削除に失敗しました。');
       req.flash('errorMessage', '完全な削除に失敗しました。');
       return res.redirect('/admin/user-groups');
       return res.redirect('/admin/user-groups');
     });
     });
-  }
+  };
 
 
   actions.userGroupRelation = {};
   actions.userGroupRelation = {};
   actions.userGroupRelation.index = function(req, res) {
   actions.userGroupRelation.index = function(req, res) {
 
 
-  }
+  };
 
 
   actions.userGroupRelation.create = function(req, res) {
   actions.userGroupRelation.create = function(req, res) {
     const User = crowi.model('User');
     const User = crowi.model('User');
@@ -795,18 +810,18 @@ module.exports = function(crowi, app) {
       userGroup = resolves[0];
       userGroup = resolves[0];
       user = resolves[1];
       user = resolves[1];
       // Relation を作成
       // Relation を作成
-      UserGroupRelation.createRelation(userGroup, user)
+      UserGroupRelation.createRelation(userGroup, user);
     })
     })
     .then((result) => {
     .then((result) => {
       return res.redirect('/admin/user-group-detail/' + userGroup.name);
       return res.redirect('/admin/user-group-detail/' + userGroup.name);
     }).catch((err) => {
     }).catch((err) => {
       debug('Error on create user-group relation', err);
       debug('Error on create user-group relation', err);
       req.flash('errorMessage', 'Error on create user-group relation');
       req.flash('errorMessage', 'Error on create user-group relation');
-          return res.redirect('/admin/user-group-detail/' + userGroup.name);
+      return res.redirect('/admin/user-group-detail/' + userGroup.name);
     });
     });
-  }
+  };
 
 
-  actions.userGroupRelation.remove = function (req, res) {
+  actions.userGroupRelation.remove = function(req, res) {
     const UserGroupRelation = crowi.model('UserGroupRelation');
     const UserGroupRelation = crowi.model('UserGroupRelation');
     var name = req.params.name;
     var name = req.params.name;
     var relationId = req.params.relationId;
     var relationId = req.params.relationId;
@@ -821,7 +836,7 @@ module.exports = function(crowi, app) {
       req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
       req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
     });
     });
 
 
-  }
+  };
 
 
   actions.api = {};
   actions.api = {};
   actions.api.appSetting = function(req, res) {
   actions.api.appSetting = function(req, res) {
@@ -841,10 +856,12 @@ module.exports = function(crowi, app) {
 
 
           return saveSetting(req, res, form);
           return saveSetting(req, res, form);
         });
         });
-      } else {
+      }
+      else {
         return saveSetting(req, res, form);
         return saveSetting(req, res, form);
       }
       }
-    } else {
+    }
+    else {
       return res.json({status: false, message: req.form.errors.join('\n')});
       return res.json({status: false, message: req.form.errors.join('\n')});
     }
     }
   };
   };
@@ -855,7 +872,8 @@ module.exports = function(crowi, app) {
     if (req.form.isValid) {
     if (req.form.isValid) {
       debug('form content', form);
       debug('form content', form);
       return saveSetting(req, res, form);
       return saveSetting(req, res, form);
-    } else {
+    }
+    else {
       return res.json({status: false, message: req.form.errors.join('\n')});
       return res.json({status: false, message: req.form.errors.join('\n')});
     }
     }
   };
   };
@@ -891,10 +909,11 @@ module.exports = function(crowi, app) {
     if (req.form.isValid) {
     if (req.form.isValid) {
       debug('form content', form);
       debug('form content', form);
       return saveSetting(req, res, form);
       return saveSetting(req, res, form);
-    } else {
+    }
+    else {
       return res.json({status: false, message: req.form.errors.join('\n')});
       return res.json({status: false, message: req.form.errors.join('\n')});
     }
     }
-  }
+  };
 
 
   // app.post('/_api/admin/notifications.add'    , admin.api.notificationAdd);
   // app.post('/_api/admin/notifications.add'    , admin.api.notificationAdd);
   actions.api.notificationAdd = function(req, res) {
   actions.api.notificationAdd = function(req, res) {
@@ -955,8 +974,7 @@ module.exports = function(crowi, app) {
    * @param {any} res
    * @param {any} res
    * @param {any} form
    * @param {any} form
    */
    */
-  function saveSetting(req, res, form)
-  {
+  function saveSetting(req, res, form) {
     Config.updateNamespaceByArray('crowi', form, function(err, config) {
     Config.updateNamespaceByArray('crowi', form, function(err, config) {
       Config.updateConfigCache('crowi', config);
       Config.updateConfigCache('crowi', config);
       return res.json({status: true});
       return res.json({status: true});
@@ -973,8 +991,8 @@ module.exports = function(crowi, app) {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       Config.updateNamespaceByArray('crowi', form, (err, config) => {
       Config.updateNamespaceByArray('crowi', form, (err, config) => {
         if (err) {
         if (err) {
-          return reject(err)
-        };
+          return reject(err);
+        }
 
 
         Config.updateConfigCache('crowi', config);
         Config.updateConfigCache('crowi', config);
         return resolve();
         return resolve();
@@ -982,8 +1000,7 @@ module.exports = function(crowi, app) {
     });
     });
   }
   }
 
 
-  function validateMailSetting(req, form, callback)
-  {
+  function validateMailSetting(req, form, callback) {
     var mailer = crowi.mailer;
     var mailer = crowi.mailer;
     var option = {
     var option = {
       host: form['mail:smtpHost'],
       host: form['mail:smtpHost'],

+ 9 - 8
lib/routes/attachment.js

@@ -54,7 +54,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id
    * @apiParam {String} page_id
    */
    */
-  api.list = function(req, res){
+  api.list = function(req, res) {
     var id = req.query.page_id || null;
     var id = req.query.page_id || null;
     if (!id) {
     if (!id) {
       return res.json(ApiResponse.error('Parameters page_id is required.'));
       return res.json(ApiResponse.error('Parameters page_id is required.'));
@@ -90,7 +90,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id
    * @apiParam {String} page_id
    * @apiParam {File} file
    * @apiParam {File} file
    */
    */
-  api.add = function(req, res){
+  api.add = function(req, res) {
     var id = req.body.page_id || 0,
     var id = req.body.page_id || 0,
       path = decodeURIComponent(req.body.path) || null,
       path = decodeURIComponent(req.body.path) || null,
       pageCreated = false,
       pageCreated = false,
@@ -116,7 +116,8 @@ module.exports = function(crowi, app) {
             resolve(page);
             resolve(page);
           })
           })
           .catch(reject);
           .catch(reject);
-      } else {
+      }
+      else {
         Page.findPageById(id).then(resolve).catch(reject);
         Page.findPageById(id).then(resolve).catch(reject);
       }
       }
     }).then(function(pageData) {
     }).then(function(pageData) {
@@ -152,20 +153,20 @@ module.exports = function(crowi, app) {
           result.attachment.creator = User.filterToPublicFields(result.attachment.creator);
           result.attachment.creator = User.filterToPublicFields(result.attachment.creator);
 
 
           // delete anyway
           // delete anyway
-          fs.unlink(tmpPath, function (err) { if (err) { debug('Error while deleting tmp file.'); } });
+          fs.unlink(tmpPath, function(err) { if (err) { debug('Error while deleting tmp file.') } });
 
 
           return res.json(ApiResponse.success(result));
           return res.json(ApiResponse.success(result));
-        }).catch(function (err) {
+        }).catch(function(err) {
           debug('Error on saving attachment data', err);
           debug('Error on saving attachment data', err);
           // @TODO
           // @TODO
           // Remove from S3
           // Remove from S3
 
 
           // delete anyway
           // delete anyway
-          fs.unlink(tmpPath, function (err) { if (err) { debug('Error while deleting tmp file.'); } });
+          fs.unlink(tmpPath, function(err) { if (err) { debug('Error while deleting tmp file.') } });
 
 
           return res.json(ApiResponse.error('Error while uploading.'));
           return res.json(ApiResponse.error('Error while uploading.'));
         });
         });
-      ;
+      
     }).catch(function(err) {
     }).catch(function(err) {
       debug('Attachement upload error', err);
       debug('Attachement upload error', err);
       return res.json(ApiResponse.error('Error.'));
       return res.json(ApiResponse.error('Error.'));
@@ -179,7 +180,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} attachment_id
    * @apiParam {String} attachment_id
    */
    */
-  api.remove = function(req, res){
+  api.remove = function(req, res) {
     const id = req.body.attachment_id;
     const id = req.body.attachment_id;
 
 
     Attachment.findById(id)
     Attachment.findById(id)

+ 4 - 3
lib/routes/bookmark.js

@@ -19,7 +19,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  actions.api.get = function (req, res) {
+  actions.api.get = function(req, res) {
     var pageId = req.query.page_id;
     var pageId = req.query.page_id;
 
 
     Bookmark.findByPageIdAndUserId(pageId, req.user)
     Bookmark.findByPageIdAndUserId(pageId, req.user)
@@ -50,7 +50,8 @@ module.exports = function(crowi, app) {
     .then(function(pageData) {
     .then(function(pageData) {
       if (pageData) {
       if (pageData) {
         return Bookmark.add(pageData, req.user);
         return Bookmark.add(pageData, req.user);
-      } else {
+      }
+      else {
         return res.json(ApiResponse.success({bookmark: null}));
         return res.json(ApiResponse.success({bookmark: null}));
       }
       }
     }).then(function(data) {
     }).then(function(data) {
@@ -72,7 +73,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  actions.api.remove = function(req, res){
+  actions.api.remove = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
 
 
     Bookmark.removeBookmark(pageId, req.user)
     Bookmark.removeBookmark(pageId, req.user)

+ 3 - 3
lib/routes/comment.js

@@ -83,17 +83,17 @@ module.exports = function(crowi, app) {
   api.remove = function(req, res) {
   api.remove = function(req, res) {
     var commentId = req.body.comment_id;
     var commentId = req.body.comment_id;
     if (!commentId) {
     if (!commentId) {
-      return Promise.resolve(res.json(ApiResponse.error(`'comment_id' is undefined`)));
+      return Promise.resolve(res.json(ApiResponse.error('\'comment_id\' is undefined')));
     }
     }
 
 
     return Comment.findById(commentId).exec()
     return Comment.findById(commentId).exec()
       .then(function(comment) {
       .then(function(comment) {
         return comment.remove()
         return comment.remove()
         .then(function() {
         .then(function() {
-           return Page.updateCommentCount(comment.page);
+          return Page.updateCommentCount(comment.page);
         })
         })
         .then(function() {
         .then(function() {
-           return res.json(ApiResponse.success({}));
+          return res.json(ApiResponse.success({}));
         });
         });
       })
       })
       .catch(function(err) {
       .catch(function(err) {

+ 4 - 2
lib/routes/index.js

@@ -24,6 +24,8 @@ module.exports = function(crowi, app) {
     , Config    = crowi.model('Config')
     , Config    = crowi.model('Config')
     ;
     ;
 
 
+  /* eslint-disable comma-spacing */
+
   app.get('/'                        , middleware.applicationInstalled(), loginRequired(crowi, app, false) , page.pageListShow);
   app.get('/'                        , middleware.applicationInstalled(), loginRequired(crowi, app, false) , page.pageListShow);
 
 
   app.get('/installer'               , middleware.applicationNotInstalled() , middleware.checkSearchIndicesGenerated(crowi, app) , installer.index);
   app.get('/installer'               , middleware.applicationNotInstalled() , middleware.checkSearchIndicesGenerated(crowi, app) , installer.index);
@@ -119,8 +121,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/user-group/:userGroupId/picture/upload', loginRequired(crowi, app), uploads.single('userGroupPicture'), admin.userGroup.uploadGroupPicture);
   app.post('/_api/admin/user-group/:userGroupId/picture/upload', loginRequired(crowi, app), uploads.single('userGroupPicture'), admin.userGroup.uploadGroupPicture);
 
 
   // user-group-relations admin
   // user-group-relations admin
-  app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create)
-  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove)
+  app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
+  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove);
 
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);

+ 3 - 2
lib/routes/installer.js

@@ -58,7 +58,7 @@ module.exports = function(crowi, app) {
 
 
             // login with passport
             // login with passport
             req.logIn(userData, (err) => {
             req.logIn(userData, (err) => {
-              if (err) { return next(); }
+              if (err) { return next() }
               else {
               else {
                 req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
                 req.flash('successMessage', 'GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。');
                 return res.redirect('/admin/app');
                 return res.redirect('/admin/app');
@@ -70,7 +70,8 @@ module.exports = function(crowi, app) {
           createInitialPages(userData, language);
           createInitialPages(userData, language);
         });
         });
       });
       });
-    } else {
+    }
+    else {
       return res.render('installer');
       return res.render('installer');
     }
     }
   };
   };

+ 22 - 19
lib/routes/login-passport.js

@@ -2,6 +2,7 @@ module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
   var debug = require('debug')('growi:routes:login-passport')
   var debug = require('debug')('growi:routes:login-passport')
+    , logger = require('@alias/logger')('growi:routes:login-passport')
     , passport = require('passport')
     , passport = require('passport')
     , config = crowi.getConfig()
     , config = crowi.getConfig()
     , Config = crowi.model('Config')
     , Config = crowi.model('Config')
@@ -18,7 +19,7 @@ module.exports = function(crowi, app) {
     // update lastLoginAt
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
       if (err) {
-        console.log(`updateLastLoginAt dumps error: ${err}`);
+        logger.error(`updateLastLoginAt dumps error: ${err}`);
         debug(`updateLastLoginAt dumps error: ${err}`);
         debug(`updateLastLoginAt dumps error: ${err}`);
       }
       }
     });
     });
@@ -27,7 +28,8 @@ module.exports = function(crowi, app) {
     if (jumpTo) {
     if (jumpTo) {
       req.session.jumpTo = null;
       req.session.jumpTo = null;
       return res.redirect(jumpTo);
       return res.redirect(jumpTo);
-    } else {
+    }
+    else {
       return res.redirect('/');
       return res.redirect('/');
     }
     }
   };
   };
@@ -70,10 +72,8 @@ module.exports = function(crowi, app) {
       return next();
       return next();
     }
     }
 
 
-    const loginForm = req.body.loginForm;
-
     if (!req.form.isValid) {
     if (!req.form.isValid) {
-      debug("invalid form");
+      debug('invalid form');
       return res.render('login', {
       return res.render('login', {
       });
       });
     }
     }
@@ -88,13 +88,13 @@ module.exports = function(crowi, app) {
       debug('info', info);
       debug('info', info);
 
 
       if (err) {  // DB Error
       if (err) {  // DB Error
-        console.log('LDAP Server Error: ', err);
+        logger.error('LDAP Server Error: ', err);
         req.flash('warningMessage', 'LDAP Server Error occured.');
         req.flash('warningMessage', 'LDAP Server Error occured.');
         return next(); // pass and the flash message is displayed when all of authentications are failed.
         return next(); // pass and the flash message is displayed when all of authentications are failed.
       }
       }
 
 
       // authentication failure
       // authentication failure
-      if (!ldapAccountInfo) { return next(); }
+      if (!ldapAccountInfo) { return next() }
       // check groups
       // check groups
       if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
       if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
         return loginFailure(req, res, next);
         return loginFailure(req, res, next);
@@ -130,7 +130,7 @@ module.exports = function(crowi, app) {
         .then((user) => {
         .then((user) => {
           // login
           // login
           req.logIn(user, (err) => {
           req.logIn(user, (err) => {
-            if (err) { return next(); }
+            if (err) { return next() }
             else {
             else {
               return loginSuccess(req, res, user);
               return loginSuccess(req, res, user);
             }
             }
@@ -147,7 +147,7 @@ module.exports = function(crowi, app) {
         });
         });
 
 
     })(req, res, next);
     })(req, res, next);
-  }
+  };
 
 
   /**
   /**
    * middleware that test credentials with LdapStrategy
    * middleware that test credentials with LdapStrategy
@@ -164,24 +164,25 @@ module.exports = function(crowi, app) {
       });
       });
     }
     }
 
 
-    const loginForm = req.body.loginForm;
-
     passport.authenticate('ldapauth', (err, user, info) => {
     passport.authenticate('ldapauth', (err, user, info) => {
       if (res.headersSent) {  // dirty hack -- 2017.09.25
       if (res.headersSent) {  // dirty hack -- 2017.09.25
         return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
         return;               // cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
       }
       }
 
 
       if (err) {  // DB Error
       if (err) {  // DB Error
-        console.log('LDAP Server Error: ', err);
+        logger.error('LDAP Server Error: ', err);
         return res.json({
         return res.json({
           status: 'warning',
           status: 'warning',
           message: 'LDAP Server Error occured.',
           message: 'LDAP Server Error occured.',
+          err
         });
         });
       }
       }
       if (info && info.message) {
       if (info && info.message) {
         return res.json({
         return res.json({
           status: 'warning',
           status: 'warning',
           message: info.message,
           message: info.message,
+          ldapConfiguration: req.ldapConfiguration,
+          ldapAccountInfo: req.ldapAccountInfo,
         });
         });
       }
       }
       if (user) {
       if (user) {
@@ -190,15 +191,19 @@ module.exports = function(crowi, app) {
           return res.json({
           return res.json({
             status: 'warning',
             status: 'warning',
             message: 'The user is found, but that has no groups.',
             message: 'The user is found, but that has no groups.',
+            ldapConfiguration: req.ldapConfiguration,
+            ldapAccountInfo: req.ldapAccountInfo,
           });
           });
         }
         }
         return res.json({
         return res.json({
           status: 'success',
           status: 'success',
           message: 'Successfully authenticated.',
           message: 'Successfully authenticated.',
+          ldapConfiguration: req.ldapConfiguration,
+          ldapAccountInfo: req.ldapAccountInfo,
         });
         });
       }
       }
     })(req, res, () => {});
     })(req, res, () => {});
-  }
+  };
 
 
   /**
   /**
    * middleware that login with LocalStrategy
    * middleware that login with LocalStrategy
@@ -207,8 +212,6 @@ module.exports = function(crowi, app) {
    * @param {*} next
    * @param {*} next
    */
    */
   const loginWithLocal = (req, res, next) => {
   const loginWithLocal = (req, res, next) => {
-    const loginForm = req.body.loginForm;
-
     if (!req.form.isValid) {
     if (!req.form.isValid) {
       return res.render('login', {
       return res.render('login', {
       });
       });
@@ -220,19 +223,19 @@ module.exports = function(crowi, app) {
       debug('info', info);
       debug('info', info);
 
 
       if (err) {  // DB Error
       if (err) {  // DB Error
-        console.log('Database Server Error: ', err);
+        logger.error('Database Server Error: ', err);
         req.flash('warningMessage', 'Database Server Error occured.');
         req.flash('warningMessage', 'Database Server Error occured.');
         return next(); // pass and the flash message is displayed when all of authentications are failed.
         return next(); // pass and the flash message is displayed when all of authentications are failed.
       }
       }
-      if (!user) { return next(); }
+      if (!user) { return next() }
       req.logIn(user, (err) => {
       req.logIn(user, (err) => {
-        if (err) { return next(); }
+        if (err) { return next() }
         else {
         else {
           return loginSuccess(req, res, user);
           return loginSuccess(req, res, user);
         }
         }
       });
       });
     })(req, res, next);
     })(req, res, next);
-  }
+  };
 
 
   return {
   return {
     loginFailure,
     loginFailure,

+ 45 - 39
lib/routes/login.js

@@ -1,26 +1,22 @@
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
-  var googleapis = require('googleapis')
-    , debug = require('debug')('growi:routes:login')
+  var debug = require('debug')('growi:routes:login')
     , async    = require('async')
     , async    = require('async')
-    , passport = require('passport')
     , config = crowi.getConfig()
     , config = crowi.getConfig()
     , mailer = crowi.getMailer()
     , mailer = crowi.getMailer()
-    , Page = crowi.model('Page')
     , User = crowi.model('User')
     , User = crowi.model('User')
     , Config = crowi.model('Config')
     , Config = crowi.model('Config')
-    , Revision = crowi.model('Revision')
     , actions = {};
     , actions = {};
 
 
 
 
   var clearGoogleSession = function(req) {
   var clearGoogleSession = function(req) {
-      req.session.googleAuthCode
-        = req.session.googleId
-        = req.session.googleEmail
-        = req.session.googleName
-        = req.session.googleImage
-        = null;
+    req.session.googleAuthCode
+      = req.session.googleId
+      = req.session.googleEmail
+      = req.session.googleName
+      = req.session.googleImage
+      = null;
   };
   };
   var loginSuccess = function(req, res, userData) {
   var loginSuccess = function(req, res, userData) {
     req.user = req.session.user = userData;
     req.user = req.session.user = userData;
@@ -43,7 +39,8 @@ module.exports = function(crowi, app) {
     if (jumpTo) {
     if (jumpTo) {
       req.session.jumpTo = null;
       req.session.jumpTo = null;
       return res.redirect(jumpTo);
       return res.redirect(jumpTo);
-    } else {
+    }
+    else {
       return res.redirect('/');
       return res.redirect('/');
     }
     }
   };
   };
@@ -70,9 +67,9 @@ module.exports = function(crowi, app) {
 
 
     if (reason === 'suspended') {
     if (reason === 'suspended') {
       reasonMessage = 'This account is suspended.';
       reasonMessage = 'This account is suspended.';
-    } else if (reason === 'registered') {
+    }
+    else if (reason === 'registered') {
       reasonMessage = 'Wait for approved by administrators.';
       reasonMessage = 'Wait for approved by administrators.';
-    } else {
     }
     }
 
 
     return res.render('login/error', {
     return res.render('login/error', {
@@ -90,14 +87,15 @@ module.exports = function(crowi, app) {
 
 
       // find user
       // find user
       User.findUserByUsernameOrEmail(username, password, (err, user) => {
       User.findUserByUsernameOrEmail(username, password, (err, user) => {
-        if (err) { return loginFailure(req, res); }
+        if (err) { return loginFailure(req, res) }
         // check existence and password
         // check existence and password
         if (!user || !user.isPasswordValid(password)) {
         if (!user || !user.isPasswordValid(password)) {
           return loginFailure(req, res);
           return loginFailure(req, res);
         }
         }
         return loginSuccess(req, res, user);
         return loginSuccess(req, res, user);
       });
       });
-    } else { // method GET
+    }
+    else { // method GET
       if (req.form) {
       if (req.form) {
         debug(req.form.errors);
         debug(req.form.errors);
       }
       }
@@ -119,7 +117,8 @@ module.exports = function(crowi, app) {
         req.session.googleCallbackAction = '/login/google';
         req.session.googleCallbackAction = '/login/google';
         return res.redirect(redirectUrl);
         return res.redirect(redirectUrl);
       });
       });
-    } else {
+    }
+    else {
       googleAuth.handleCallback(req, function(err, tokenInfo) {
       googleAuth.handleCallback(req, function(err, tokenInfo) {
         debug('handleCallback', err, tokenInfo);
         debug('handleCallback', err, tokenInfo);
         if (err) {
         if (err) {
@@ -164,7 +163,7 @@ module.exports = function(crowi, app) {
       var googleImage = registerForm.googleImage || null;
       var googleImage = registerForm.googleImage || null;
 
 
       // email と username の unique チェックする
       // email と username の unique チェックする
-      User.isRegisterable(email, username, function (isRegisterable, errOn) {
+      User.isRegisterable(email, username, function(isRegisterable, errOn) {
         var isError = false;
         var isError = false;
         if (!User.isEmailValid(email)) {
         if (!User.isEmailValid(email)) {
           isError = true;
           isError = true;
@@ -190,7 +189,8 @@ module.exports = function(crowi, app) {
           if (err) {
           if (err) {
             req.flash('registerWarningMessage', 'Failed to register.');
             req.flash('registerWarningMessage', 'Failed to register.');
             return res.redirect('/register');
             return res.redirect('/register');
-          } else {
+          }
+          else {
 
 
             // 作成後、承認が必要なモードなら、管理者に通知する
             // 作成後、承認が必要なモードなら、管理者に通知する
             const appTitle = Config.appTitle(config);
             const appTitle = Config.appTitle(config);
@@ -201,20 +201,20 @@ module.exports = function(crowi, app) {
                   admins,
                   admins,
                   function(adminUser, next) {
                   function(adminUser, next) {
                     mailer.send({
                     mailer.send({
-                        to: adminUser.email,
-                        subject: '[' + appTitle + ':admin] A New User Created and Waiting for Activation',
-                        template: 'admin/userWaitingActivation.txt',
-                        vars: {
-                          createdUser: userData,
-                          adminUser: adminUser,
-                          url: config.crowi['app:url'],
-                          appTitle: appTitle,
-                        }
-                      },
-                      function (err, s) {
-                        debug('completed to send email: ', err, s);
-                        next();
+                      to: adminUser.email,
+                      subject: '[' + appTitle + ':admin] A New User Created and Waiting for Activation',
+                      template: 'admin/userWaitingActivation.txt',
+                      vars: {
+                        createdUser: userData,
+                        adminUser: adminUser,
+                        url: config.crowi['app:url'],
+                        appTitle: appTitle,
                       }
                       }
+                    },
+                    function(err, s) {
+                      debug('completed to send email: ', err, s);
+                      next();
+                    }
                     );
                     );
                   },
                   },
                   function(err) {
                   function(err) {
@@ -255,13 +255,14 @@ module.exports = function(crowi, app) {
                       }
                       }
                       // DONE
                       // DONE
                     });
                     });
-                  }).catch(function (err) { // ignore
+                  }).catch(function(err) { // ignore
                     debug('Upload error', err);
                     debug('Upload error', err);
                   });
                   });
                 }).catch(function() { // ignore
                 }).catch(function() { // ignore
                 });
                 });
               }
               }
-            } else {
+            }
+            else {
               // add a flash message to inform the user that processing was successful -- 2017.09.23 Yuki Takei
               // add a flash message to inform the user that processing was successful -- 2017.09.23 Yuki Takei
               // cz. loginSuccess method doesn't work on it's own when using passport
               // cz. loginSuccess method doesn't work on it's own when using passport
               //      because `req.login()` prepared by passport is not called.
               //      because `req.login()` prepared by passport is not called.
@@ -272,7 +273,8 @@ module.exports = function(crowi, app) {
           }
           }
         });
         });
       });
       });
-    } else { // method GET of form is not valid
+    }
+    else { // method GET of form is not valid
       debug('session is', req.session);
       debug('session is', req.session);
       var isRegistering = true;
       var isRegistering = true;
       // google callback を受ける可能性もある
       // google callback を受ける可能性もある
@@ -306,7 +308,8 @@ module.exports = function(crowi, app) {
           }
           }
           return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
           return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
         });
         });
-      } else {
+      }
+      else {
         return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
         return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
       }
       }
     }
     }
@@ -342,17 +345,20 @@ module.exports = function(crowi, app) {
             if (err) {
             if (err) {
               req.flash('warningMessage', 'アクティベートに失敗しました。');
               req.flash('warningMessage', 'アクティベートに失敗しました。');
               return res.render('invited');
               return res.render('invited');
-            } else {
+            }
+            else {
               return res.redirect('/');
               return res.redirect('/');
             }
             }
           });
           });
-        } else {
+        }
+        else {
           req.flash('warningMessage', '利用できないユーザーIDです。');
           req.flash('warningMessage', '利用できないユーザーIDです。');
           debug('username', username);
           debug('username', username);
           return res.render('invited');
           return res.render('invited');
         }
         }
       });
       });
-    } else {
+    }
+    else {
       return res.render('invited', {
       return res.render('invited', {
       });
       });
     }
     }

+ 32 - 25
lib/routes/me.js

@@ -16,7 +16,7 @@ module.exports = function(crowi, app) {
 
 
   actions.api = api;
   actions.api = api;
 
 
-  api.uploadPicture = function (req, res) {
+  api.uploadPicture = function(req, res) {
     var fileUploader = require('../util/fileUploader')(crowi, app);
     var fileUploader = require('../util/fileUploader')(crowi, app);
     //var storagePlugin = new pluginService('storage');
     //var storagePlugin = new pluginService('storage');
     //var storage = require('../service/storage').StorageService(config);
     //var storage = require('../service/storage').StorageService(config);
@@ -53,7 +53,7 @@ module.exports = function(crowi, app) {
     .then(function(data) {
     .then(function(data) {
       var imageUrl = fileUploader.generateUrl(filePath);
       var imageUrl = fileUploader.generateUrl(filePath);
       req.user.updateImage(imageUrl, function(err, data) {
       req.user.updateImage(imageUrl, function(err, data) {
-        fs.unlink(tmpPath, function (err) {
+        fs.unlink(tmpPath, function(err) {
           // エラー自体は無視
           // エラー自体は無視
           if (err) {
           if (err) {
             debug('Error while deleting tmp file.', err);
             debug('Error while deleting tmp file.', err);
@@ -66,7 +66,7 @@ module.exports = function(crowi, app) {
           });
           });
         });
         });
       });
       });
-    }).catch(function (err) {
+    }).catch(function(err) {
       debug('Uploading error', err);
       debug('Uploading error', err);
 
 
       return res.json({
       return res.json({
@@ -99,7 +99,7 @@ module.exports = function(crowi, app) {
         { email: userData.email },                  // query
         { email: userData.email },                  // query
         { name, email, lang, isEmailPublished },                      // updating data
         { name, email, lang, isEmailPublished },                      // updating data
         { runValidators: true, context: 'query' },  // for validation
         { runValidators: true, context: 'query' },  // for validation
-                                                    //   see https://www.npmjs.com/package/mongoose-unique-validator#find--updates -- 2017.09.24 Yuki Takei
+        //   see https://www.npmjs.com/package/mongoose-unique-validator#find--updates -- 2017.09.24 Yuki Takei
         (err) => {
         (err) => {
           if (err) {
           if (err) {
             Object.keys(err.errors).forEach((e) => {
             Object.keys(err.errors).forEach((e) => {
@@ -112,7 +112,8 @@ module.exports = function(crowi, app) {
           return res.redirect('/me');
           return res.redirect('/me');
         });
         });
 
 
-    } else { // method GET
+    }
+    else { // method GET
       /*
       /*
        * disabled because the system no longer allows undefined email -- 2017.10.06 Yuki Takei
        * disabled because the system no longer allows undefined email -- 2017.10.06 Yuki Takei
        *
        *
@@ -127,7 +128,7 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
-  actions.imagetype = function(req,res) {
+  actions.imagetype = function(req, res) {
     if (req.method !== 'POST') {
     if (req.method !== 'POST') {
       // do nothing
       // do nothing
       return;
       return;
@@ -155,7 +156,7 @@ module.exports = function(crowi, app) {
       req.flash('successMessage', req.t('Updated'));
       req.flash('successMessage', req.t('Updated'));
       return res.redirect('/me');
       return res.redirect('/me');
     });
     });
-  }
+  };
 
 
   actions.externalAccounts = {};
   actions.externalAccounts = {};
   actions.externalAccounts.list = function(req, res) {
   actions.externalAccounts.list = function(req, res) {
@@ -176,7 +177,7 @@ module.exports = function(crowi, app) {
           return res.render('me/external-accounts', renderVars);
           return res.render('me/external-accounts', renderVars);
         }
         }
       });
       });
-  }
+  };
 
 
   actions.externalAccounts.disassociate = function(req, res) {
   actions.externalAccounts.disassociate = function(req, res) {
     const userData = req.user;
     const userData = req.user;
@@ -184,7 +185,7 @@ module.exports = function(crowi, app) {
     const redirectWithFlash = (type, msg) => {
     const redirectWithFlash = (type, msg) => {
       req.flash(type, msg);
       req.flash(type, msg);
       return res.redirect('/me/external-accounts');
       return res.redirect('/me/external-accounts');
-    }
+    };
 
 
     if (req.body == null) {
     if (req.body == null) {
       redirectWithFlash('errorMessage', 'Invalid form.');
       redirectWithFlash('errorMessage', 'Invalid form.');
@@ -198,7 +199,7 @@ module.exports = function(crowi, app) {
       else {
       else {
         ExternalAccount.count({user: userData})
         ExternalAccount.count({user: userData})
           .then((count) => {
           .then((count) => {
-            resolve(count > 1)
+            resolve(count > 1);
           });
           });
       }
       }
     })
     })
@@ -233,7 +234,7 @@ module.exports = function(crowi, app) {
       }
       }
     });
     });
 
 
-  }
+  };
 
 
   actions.externalAccounts.associateLdap = function(req, res) {
   actions.externalAccounts.associateLdap = function(req, res) {
     const passport = require('passport');
     const passport = require('passport');
@@ -242,7 +243,7 @@ module.exports = function(crowi, app) {
     const redirectWithFlash = (type, msg) => {
     const redirectWithFlash = (type, msg) => {
       req.flash(type, msg);
       req.flash(type, msg);
       return res.redirect('/me/external-accounts');
       return res.redirect('/me/external-accounts');
-    }
+    };
 
 
     if (!passportService.isLdapStrategySetup) {
     if (!passportService.isLdapStrategySetup) {
       debug('LdapStrategy has not been set up');
       debug('LdapStrategy has not been set up');
@@ -280,7 +281,7 @@ module.exports = function(crowi, app) {
     })(req, res, () => {});
     })(req, res, () => {});
 
 
 
 
-  }
+  };
 
 
   actions.password = function(req, res) {
   actions.password = function(req, res) {
     var passwordForm = req.body.mePassword;
     var passwordForm = req.body.mePassword;
@@ -310,7 +311,8 @@ module.exports = function(crowi, app) {
       // check password confirm
       // check password confirm
       if (newPassword != newPasswordConfirm) {
       if (newPassword != newPasswordConfirm) {
         req.form.errors.push('Failed to verify passwords');
         req.form.errors.push('Failed to verify passwords');
-      } else {
+      }
+      else {
         userData.updatePassword(newPassword, function(err, userData) {
         userData.updatePassword(newPassword, function(err, userData) {
           if (err) {
           if (err) {
             for (var e in err.errors) {
             for (var e in err.errors) {
@@ -325,7 +327,8 @@ module.exports = function(crowi, app) {
           return res.redirect('/me/password');
           return res.redirect('/me/password');
         });
         });
       }
       }
-    } else { // method GET
+    }
+    else { // method GET
       return res.render('me/password', {
       return res.render('me/password', {
       });
       });
     }
     }
@@ -338,16 +341,17 @@ module.exports = function(crowi, app) {
     if (req.method == 'POST' && req.form.isValid) {
     if (req.method == 'POST' && req.form.isValid) {
       userData.updateApiToken()
       userData.updateApiToken()
       .then(function(userData) {
       .then(function(userData) {
-          req.flash('successMessage', 'API Token updated');
-          return res.redirect('/me/apiToken');
+        req.flash('successMessage', 'API Token updated');
+        return res.redirect('/me/apiToken');
       })
       })
       .catch(function(err) {
       .catch(function(err) {
-          //req.flash('successMessage',);
-          req.form.errors.push('Failed to update API Token');
-          return res.render('me/api_token', {
-          });
+        //req.flash('successMessage',);
+        req.form.errors.push('Failed to update API Token');
+        return res.render('me/api_token', {
+        });
       });
       });
-    } else {
+    }
+    else {
       return res.render('me/api_token', {
       return res.render('me/api_token', {
       });
       });
     }
     }
@@ -379,7 +383,8 @@ module.exports = function(crowi, app) {
 
 
         return res.redirect('/me');
         return res.redirect('/me');
       });
       });
-    } else if (toConnect) {
+    }
+    else if (toConnect) {
       googleAuth.createAuthUrl(req, function(err, redirectUrl) {
       googleAuth.createAuthUrl(req, function(err, redirectUrl) {
         if (err) {
         if (err) {
           // TODO
           // TODO
@@ -388,7 +393,8 @@ module.exports = function(crowi, app) {
         req.session.googleCallbackAction = '/me/auth/google/callback';
         req.session.googleCallbackAction = '/me/auth/google/callback';
         return res.redirect(redirectUrl);
         return res.redirect(redirectUrl);
       });
       });
-    } else {
+    }
+    else {
       return res.redirect('/me');
       return res.redirect('/me');
     }
     }
   };
   };
@@ -414,7 +420,8 @@ module.exports = function(crowi, app) {
         if (!err && googleUser) {
         if (!err && googleUser) {
           req.flash('warningMessage.auth.google', 'This Google\'s account is connected by another user');
           req.flash('warningMessage.auth.google', 'This Google\'s account is connected by another user');
           return res.redirect('/me');
           return res.redirect('/me');
-        } else {
+        }
+        else {
           userData.updateGoogleId(googleId, function(err, userData) {
           userData.updateGoogleId(googleId, function(err, userData) {
             if (err) {
             if (err) {
               debug('Failed to updateGoogleId', err);
               debug('Failed to updateGoogleId', err);

+ 75 - 51
lib/routes/page.js

@@ -9,6 +9,7 @@ module.exports = function(crowi, app) {
     , Revision = crowi.model('Revision')
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
     , Bookmark = crowi.model('Bookmark')
     , UserGroupRelation = crowi.model('UserGroupRelation')
     , UserGroupRelation = crowi.model('UserGroupRelation')
+    , PageGroupRelation = crowi.model('PageGroupRelation')
     , ApiResponse = require('../util/apiResponse')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
     , interceptorManager = crowi.getInterceptorManager()
     , pagePathUtil = require('../util/pagePathUtil')
     , pagePathUtil = require('../util/pagePathUtil')
@@ -54,7 +55,8 @@ module.exports = function(crowi, app) {
 
 
     if (length < limit) {
     if (length < limit) {
       next = null;
       next = null;
-    } else {
+    }
+    else {
       next = offset + limit;
       next = offset + limit;
     }
     }
 
 
@@ -77,7 +79,7 @@ module.exports = function(crowi, app) {
     else {
     else {
       return actions.pageListShowForCrowiPlus(req, res);
       return actions.pageListShowForCrowiPlus(req, res);
     }
     }
-  }
+  };
   /**
   /**
    * switch action by behaviorType
    * switch action by behaviorType
    */
    */
@@ -90,7 +92,7 @@ module.exports = function(crowi, app) {
     else {
     else {
       return actions.pageShowForCrowiPlus(req, res);
       return actions.pageShowForCrowiPlus(req, res);
     }
     }
-  }
+  };
   /**
   /**
    * switch action by behaviorType
    * switch action by behaviorType
    */
    */
@@ -105,7 +107,7 @@ module.exports = function(crowi, app) {
       // redirect to '/trash'
       // redirect to '/trash'
       return res.redirect('/trash');
       return res.redirect('/trash');
     }
     }
-  }
+  };
   /**
   /**
    * switch action by behaviorType
    * switch action by behaviorType
    */
    */
@@ -121,7 +123,7 @@ module.exports = function(crowi, app) {
       return actions.deletedPageListShow(req, res);
       return actions.deletedPageListShow(req, res);
     }
     }
 
 
-  }
+  };
   /**
   /**
    * switch action by behaviorType
    * switch action by behaviorType
    */
    */
@@ -136,7 +138,7 @@ module.exports = function(crowi, app) {
       const path = '/trash' + getPathFromRequest(req);
       const path = '/trash' + getPathFromRequest(req);
       return res.redirect(path);
       return res.redirect(path);
     }
     }
-  }
+  };
 
 
 
 
   actions.pageListShow = function(req, res) {
   actions.pageListShow = function(req, res) {
@@ -151,11 +153,11 @@ module.exports = function(crowi, app) {
     // index page
     // index page
     var pagerOptions = {
     var pagerOptions = {
       offset: offset,
       offset: offset,
-      limit : limit
+      limit: limit
     };
     };
     var queryOptions = {
     var queryOptions = {
       offset: offset,
       offset: offset,
-      limit : limit + 1,
+      limit: limit + 1,
       isPopulateRevisionBody: Config.isEnabledTimeline(config),
       isPopulateRevisionBody: Config.isEnabledTimeline(config),
     };
     };
 
 
@@ -175,7 +177,8 @@ module.exports = function(crowi, app) {
       if (portalPage) {
       if (portalPage) {
         renderVars.revision = portalPage.revision;
         renderVars.revision = portalPage.revision;
         return Revision.findRevisionList(portalPage.path, {});
         return Revision.findRevisionList(portalPage.path, {});
-      } else {
+      }
+      else {
         return Promise.resolve([]);
         return Promise.resolve([]);
       }
       }
     }).then(function(tree) {
     }).then(function(tree) {
@@ -207,7 +210,7 @@ module.exports = function(crowi, app) {
     path = path.replace((/\/$/), '');
     path = path.replace((/\/$/), '');
     // redirect
     // redirect
     return res.redirect(path);
     return res.redirect(path);
-  }
+  };
 
 
   actions.pageShowForCrowiPlus = function(req, res) {
   actions.pageShowForCrowiPlus = function(req, res) {
     var path = getPathFromRequest(req);
     var path = getPathFromRequest(req);
@@ -219,11 +222,11 @@ module.exports = function(crowi, app) {
     // index page
     // index page
     var pagerOptions = {
     var pagerOptions = {
       offset: offset,
       offset: offset,
-      limit : limit
+      limit: limit
     };
     };
     var queryOptions = {
     var queryOptions = {
       offset: offset,
       offset: offset,
-      limit : limit + 1,
+      limit: limit + 1,
       isPopulateRevisionBody: Config.isEnabledTimeline(config),
       isPopulateRevisionBody: Config.isEnabledTimeline(config),
       includeDeletedPage: path.startsWith('/trash/'),
       includeDeletedPage: path.startsWith('/trash/'),
     };
     };
@@ -236,6 +239,7 @@ module.exports = function(crowi, app) {
       pages: [],
       pages: [],
       tree: [],
       tree: [],
       userRelatedGroups: [],
       userRelatedGroups: [],
+      pageRelatedGroup: null,
     };
     };
 
 
     var pageTeamplate = 'customlayout-selector/page';
     var pageTeamplate = 'customlayout-selector/page';
@@ -296,7 +300,8 @@ module.exports = function(crowi, app) {
             return Promise.resolve();
             return Promise.resolve();
           }
           }
         });
         });
-      } else {
+      }
+      else {
         return Promise.resolve();
         return Promise.resolve();
       }
       }
     })
     })
@@ -339,14 +344,20 @@ module.exports = function(crowi, app) {
     })
     })
     .then(function() {
     .then(function() {
       return UserGroupRelation.findAllRelationForUser(req.user);
       return UserGroupRelation.findAllRelationForUser(req.user);
-    }).then(function (groupRelations) {
-      debug('findPage : relatedGroups ', groupRelations);
-      renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
-      debug('findPage : groups ', renderVars.userRelatedGroups);
+    }).then(function(groupRelations) {
+      if (groupRelations != null) {
+        renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+      }
+
+      return PageGroupRelation.findByPage(renderVars.page);
+    }).then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
 
 
       return Promise.resolve();
       return Promise.resolve();
     });
     });
-  }
+  };
 
 
   actions.deletedPageListShow = function(req, res) {
   actions.deletedPageListShow = function(req, res) {
     var path = '/trash' + getPathFromRequest(req);
     var path = '/trash' + getPathFromRequest(req);
@@ -356,11 +367,11 @@ module.exports = function(crowi, app) {
     // index page
     // index page
     var pagerOptions = {
     var pagerOptions = {
       offset: offset,
       offset: offset,
-      limit : limit
+      limit: limit
     };
     };
     var queryOptions = {
     var queryOptions = {
       offset: offset,
       offset: offset,
-      limit : limit + 1,
+      limit: limit + 1,
       includeDeletedPage: true,
       includeDeletedPage: true,
     };
     };
 
 
@@ -420,10 +431,19 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
   function renderPage(pageData, req, res) {
     // create page
     // create page
     if (!pageData) {
     if (!pageData) {
-      return res.render('customlayout-selector/not_found', {
-        author: {},
-        page: false,
-      });
+      var userRelatedGroups
+      UserGroupRelation.findAllRelationForUser(req.user)
+        .then((groupRelations) => {
+          userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+          return Promise.resolve();
+        }).then(() => {
+          debug('not found page user group resolver : ', userRelatedGroups);
+          return res.render('customlayout-selector/not_found', {
+            author: {},
+            page: false,
+            userRelatedGroups: userRelatedGroups,
+          });
+        });
     }
     }
 
 
     if (pageData.redirectTo) {
     if (pageData.redirectTo) {
@@ -466,7 +486,8 @@ module.exports = function(crowi, app) {
           debug('Error on finding user related entities', err);
           debug('Error on finding user related entities', err);
           // pass
           // pass
         });
         });
-      } else {
+      }
+      else {
         return Promise.resolve();
         return Promise.resolve();
       }
       }
     }).then(function() {
     }).then(function() {
@@ -531,11 +552,12 @@ module.exports = function(crowi, app) {
       .then(function(page) {
       .then(function(page) {
         if (page) {
         if (page) {
           return res.redirect(pagePathUtil.encodePagePath(path) + '/');
           return res.redirect(pagePathUtil.encodePagePath(path) + '/');
-        } else {
+        }
+        else {
 
 
-          var fixed = Page.fixToCreatableName(path)
+          var fixed = Page.fixToCreatableName(path);
           if (fixed !== path) {
           if (fixed !== path) {
-            debug('fixed page name', fixed)
+            debug('fixed page name', fixed);
             res.redirect(pagePathUtil.encodePagePath(fixed));
             res.redirect(pagePathUtil.encodePagePath(fixed));
             return ;
             return ;
           }
           }
@@ -562,7 +584,7 @@ module.exports = function(crowi, app) {
     var currentRevision = pageForm.currentRevision;
     var currentRevision = pageForm.currentRevision;
     var grant = pageForm.grant;
     var grant = pageForm.grant;
     var path = pageForm.path;
     var path = pageForm.path;
-    var grantUserGroupId = pageForm.grantUserGroupId
+    var grantUserGroupId = pageForm.grantUserGroupId;
 
 
     // TODO: make it pluggable
     // TODO: make it pluggable
     var notify = pageForm.notify || {};
     var notify = pageForm.notify || {};
@@ -602,7 +624,8 @@ module.exports = function(crowi, app) {
       if (data) {
       if (data) {
         previousRevision = data.revision;
         previousRevision = data.revision;
         return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
         return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
-      } else {
+      }
+      else {
         // new page
         // new page
         updateOrCreate = 'create';
         updateOrCreate = 'create';
         return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
         return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
@@ -616,7 +639,7 @@ module.exports = function(crowi, app) {
       // TODO: move to events
       // TODO: move to events
       if (notify.slack) {
       if (notify.slack) {
         if (notify.slack.on && notify.slack.channel) {
         if (notify.slack.on && notify.slack.channel) {
-          data.updateSlackChannel(notify.slack.channel).then(function(){}).catch(function(){});
+          data.updateSlackChannel(notify.slack.channel).then(function() {}).catch(function() {});
 
 
           if (crowi.slack) {
           if (crowi.slack) {
             notify.slack.channel.split(',').map(function(chan) {
             notify.slack.channel.split(',').map(function(chan) {
@@ -647,7 +670,7 @@ module.exports = function(crowi, app) {
   /**
   /**
    * redirector
    * redirector
    */
    */
-  api.redirector = function(req, res){
+  api.redirector = function(req, res) {
     var id = req.params.id;
     var id = req.params.id;
 
 
     Page.findPageById(id)
     Page.findPageById(id)
@@ -680,8 +703,8 @@ module.exports = function(crowi, app) {
     var limit = 50;
     var limit = 50;
     var offset = parseInt(req.query.offset) || 0;
     var offset = parseInt(req.query.offset) || 0;
 
 
-    var pagerOptions = { offset: offset, limit : limit };
-    var queryOptions = { offset: offset, limit : limit + 1};
+    var pagerOptions = { offset: offset, limit: limit };
+    var queryOptions = { offset: offset, limit: limit + 1};
 
 
     // Accepts only one of these
     // Accepts only one of these
     if (username === null && path === null) {
     if (username === null && path === null) {
@@ -700,7 +723,8 @@ module.exports = function(crowi, app) {
         }
         }
         return Page.findListByCreator(user, queryOptions, req.user);
         return Page.findListByCreator(user, queryOptions, req.user);
       });
       });
-    } else {
+    }
+    else {
       pageFetcher = Page.findListByStartWith(path, req.user, queryOptions);
       pageFetcher = Page.findListByStartWith(path, req.user, queryOptions);
     }
     }
 
 
@@ -728,7 +752,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} path
    * @apiParam {String} path
    * @apiParam {String} grant
    * @apiParam {String} grant
    */
    */
-  api.create = function(req, res){
+  api.create = function(req, res) {
     var body = req.body.body || null;
     var body = req.body.body || null;
     var pagePath = req.body.path || null;
     var pagePath = req.body.path || null;
     var grant = req.body.grant || null;
     var grant = req.body.grant || null;
@@ -757,7 +781,7 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.success(result));
       return res.json(ApiResponse.success(result));
     }).catch(function(err) {
     }).catch(function(err) {
       return res.json(ApiResponse.error(err));
       return res.json(ApiResponse.error(err));
-    });;
+    });
 
 
   };
   };
 
 
@@ -775,7 +799,7 @@ module.exports = function(crowi, app) {
    * - If revision_id is specified => update the page,
    * - If revision_id is specified => update the page,
    * - If revision_id is not specified => force update by the new contents.
    * - If revision_id is not specified => force update by the new contents.
    */
    */
-  api.update = function(req, res){
+  api.update = function(req, res) {
     var pageBody = req.body.body || null;
     var pageBody = req.body.body || null;
     var pageId = req.body.page_id || null;
     var pageId = req.body.page_id || null;
     var revisionId = req.body.revision_id || null;
     var revisionId = req.body.revision_id || null;
@@ -790,7 +814,7 @@ module.exports = function(crowi, app) {
     .then(function(pageData) {
     .then(function(pageData) {
       if (pageData && revisionId !== null && !pageData.isUpdatable(revisionId)) {
       if (pageData && revisionId !== null && !pageData.isUpdatable(revisionId)) {
         throw new Error('Revision error.');
         throw new Error('Revision error.');
-      };
+      }
 
 
       var grantOption = {grant: pageData.grant};
       var grantOption = {grant: pageData.grant};
       if (grant !== null) {
       if (grant !== null) {
@@ -821,7 +845,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} path
    * @apiParam {String} path
    * @apiParam {String} revision_id
    * @apiParam {String} revision_id
    */
    */
-  api.get = function(req, res){
+  api.get = function(req, res) {
     const pagePath = req.query.path || null;
     const pagePath = req.query.path || null;
     const pageId = req.query.page_id || null; // TODO: handling
     const pageId = req.query.page_id || null; // TODO: handling
     const revisionId = req.query.revision_id || null;
     const revisionId = req.query.revision_id || null;
@@ -833,7 +857,8 @@ module.exports = function(crowi, app) {
     let pageFinder;
     let pageFinder;
     if (pageId) { // prioritized
     if (pageId) { // prioritized
       pageFinder = Page.findPageByIdAndGrantedUser(pageId, req.user);
       pageFinder = Page.findPageByIdAndGrantedUser(pageId, req.user);
-    } else if (pagePath) {
+    }
+    else if (pagePath) {
       pageFinder = Page.findPage(pagePath, req.user, revisionId);
       pageFinder = Page.findPage(pagePath, req.user, revisionId);
     }
     }
 
 
@@ -854,7 +879,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  api.seen = function(req, res){
+  api.seen = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
     if (!pageId) {
     if (!pageId) {
       return res.json(ApiResponse.error('page_id required'));
       return res.json(ApiResponse.error('page_id required'));
@@ -881,7 +906,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  api.like = function(req, res){
+  api.like = function(req, res) {
     var id = req.body.page_id;
     var id = req.body.page_id;
 
 
     Page.findPageByIdAndGrantedUser(id, req.user)
     Page.findPageByIdAndGrantedUser(id, req.user)
@@ -903,7 +928,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  api.unlike = function(req, res){
+  api.unlike = function(req, res) {
     var id = req.body.page_id;
     var id = req.body.page_id;
 
 
     Page.findPageByIdAndGrantedUser(id, req.user)
     Page.findPageByIdAndGrantedUser(id, req.user)
@@ -955,7 +980,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} revision_id
    * @apiParam {String} revision_id
    */
    */
-  api.remove = function(req, res){
+  api.remove = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
     var previousRevision = req.body.revision_id || null;
     var previousRevision = req.body.revision_id || null;
 
 
@@ -1008,7 +1033,7 @@ module.exports = function(crowi, app) {
    *
    *
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    */
    */
-  api.revertRemove = function(req, res){
+  api.revertRemove = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
 
 
     // get recursively flag
     // get recursively flag
@@ -1046,7 +1071,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} new_path
    * @apiParam {String} new_path
    * @apiParam {Bool} create_redirect
    * @apiParam {Bool} create_redirect
    */
    */
-  api.rename = function(req, res){
+  api.rename = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
     var previousRevision = req.body.revision_id || null;
     var previousRevision = req.body.revision_id || null;
     var newPagePath = Page.normalizePath(req.body.new_path);
     var newPagePath = Page.normalizePath(req.body.new_path);
@@ -1100,13 +1125,12 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} new_path
    * @apiParam {String} new_path
    */
    */
-  api.duplicate = function (req, res) {
+  api.duplicate = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
     var newPagePath = Page.normalizePath(req.body.new_path);
     var newPagePath = Page.normalizePath(req.body.new_path);
-    var page = {};
 
 
     Page.findPageById(pageId)
     Page.findPageById(pageId)
-      .then(function (pageData) {
+      .then(function(pageData) {
         req.body.path = newPagePath;
         req.body.path = newPagePath;
         req.body.body = pageData.revision.body;
         req.body.body = pageData.revision.body;
         req.body.grant = pageData.grant;
         req.body.grant = pageData.grant;
@@ -1123,7 +1147,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} revision_id
    * @apiParam {String} revision_id
    */
    */
-  api.unlink = function(req, res){
+  api.unlink = function(req, res) {
     var pageId = req.body.page_id;
     var pageId = req.body.page_id;
 
 
     Page.findPageByIdAndGrantedUser(pageId, req.user)
     Page.findPageByIdAndGrantedUser(pageId, req.user)

+ 7 - 4
lib/routes/revision.js

@@ -24,7 +24,7 @@ module.exports = function(crowi, app) {
       .then(function(revisionData) {
       .then(function(revisionData) {
         var result = {
         var result = {
           revision: revisionData,
           revision: revisionData,
-        }
+        };
         return res.json(ApiResponse.success(result));
         return res.json(ApiResponse.success(result));
       })
       })
       .catch(function(err) {
       .catch(function(err) {
@@ -53,7 +53,8 @@ module.exports = function(crowi, app) {
       }).catch(function(err) {
       }).catch(function(err) {
         return res.json(ApiResponse.error(err));
         return res.json(ApiResponse.error(err));
       });
       });
-    } else {
+    }
+    else {
       return res.json(ApiResponse.error('Parameter error.'));
       return res.json(ApiResponse.error('Parameter error.'));
     }
     }
   };
   };
@@ -80,14 +81,16 @@ module.exports = function(crowi, app) {
       }).catch(function(err) {
       }).catch(function(err) {
         return res.json(ApiResponse.error(err));
         return res.json(ApiResponse.error(err));
       });
       });
-    } else if (revisionIds.length > 0) {
+    }
+    else if (revisionIds.length > 0) {
       Revision.findRevisions(revisionIds)
       Revision.findRevisions(revisionIds)
       .then(function(revisions) {
       .then(function(revisions) {
         return res.json(ApiResponse.success(revisions));
         return res.json(ApiResponse.success(revisions));
       }).catch(function(err) {
       }).catch(function(err) {
         return res.json(ApiResponse.error(err));
         return res.json(ApiResponse.error(err));
       });
       });
-    } else {
+    }
+    else {
       return res.json(ApiResponse.error('Parameter error.'));
       return res.json(ApiResponse.error('Parameter error.'));
     }
     }
   };
   };

+ 3 - 2
lib/routes/search.js

@@ -29,7 +29,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} q keyword
    * @apiParam {String} q keyword
    * @apiParam {String} path
    * @apiParam {String} path
    */
    */
-  api.search = function(req, res){
+  api.search = function(req, res) {
     var keyword = req.query.q || null;
     var keyword = req.query.q || null;
     var tree = req.query.tree || null;
     var tree = req.query.tree || null;
     if (keyword === null || keyword === '') {
     if (keyword === null || keyword === '') {
@@ -45,7 +45,8 @@ module.exports = function(crowi, app) {
     var doSearch;
     var doSearch;
     if (tree) {
     if (tree) {
       doSearch = search.searchKeywordUnderPath(keyword, tree, {});
       doSearch = search.searchKeywordUnderPath(keyword, tree, {});
-    } else {
+    }
+    else {
       doSearch = search.searchKeyword(keyword, {});
       doSearch = search.searchKeyword(keyword, {});
     }
     }
     var result = {};
     var result = {};

+ 8 - 6
lib/routes/user.js

@@ -1,7 +1,7 @@
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
-   var Page = crowi.model('Page')
+  var Page = crowi.model('Page')
     , User = crowi.model('User')
     , User = crowi.model('User')
     , Revision = crowi.model('Revision')
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
     , Bookmark = crowi.model('Bookmark')
@@ -16,7 +16,7 @@ module.exports = function(crowi, app) {
       skip: req.query.offset || 0,
       skip: req.query.offset || 0,
       limit: req.query.limit || 50,
       limit: req.query.limit || 50,
     };
     };
-    Bookmark.findByUser(req.user, options, function (err, bookmarks) {
+    Bookmark.findByUser(req.user, options, function(err, bookmarks) {
       res.json(bookmarks);
       res.json(bookmarks);
     });
     });
   };
   };
@@ -28,7 +28,8 @@ module.exports = function(crowi, app) {
     .then(function(userData) {
     .then(function(userData) {
       if (userData) {
       if (userData) {
         return res.json({ valid: false });
         return res.json({ valid: false });
-      } else {
+      }
+      else {
         return res.json({ valid: true });
         return res.json({ valid: true });
       }
       }
     }).catch(function(err) {
     }).catch(function(err) {
@@ -48,9 +49,10 @@ module.exports = function(crowi, app) {
 
 
     var userFetcher;
     var userFetcher;
     if (!userIds || userIds.split(',').length <= 0) {
     if (!userIds || userIds.split(',').length <= 0) {
-      userFetcher = User.findAllUsers()
-    } else {
-      userFetcher = User.findUsersByIds(userIds.split(','))
+      userFetcher = User.findAllUsers();
+    }
+    else {
+      userFetcher = User.findUsersByIds(userIds.split(','));
     }
     }
 
 
     userFetcher
     userFetcher

+ 4 - 8
lib/service/notification.js

@@ -1,13 +1,11 @@
 'use strict';
 'use strict';
 
 
-function Notification (crowi)
-{
+function Notification(crowi) {
   this.crowi = crowi;
   this.crowi = crowi;
   this.config = crowi.getConfig();
   this.config = crowi.getConfig();
 }
 }
 
 
-Notification.prototype.hasSlackConfig = function()
-{
+Notification.prototype.hasSlackConfig = function() {
   if (!this.config.notification['slack']) {
   if (!this.config.notification['slack']) {
     return false;
     return false;
   }
   }
@@ -15,12 +13,10 @@ Notification.prototype.hasSlackConfig = function()
   //var config = ;
   //var config = ;
 };
 };
 
 
-Notification.prototype.noitfyByEmail = function()
-{
+Notification.prototype.noitfyByEmail = function() {
 };
 };
 
 
-Notification.prototype.noitfyByChat = function()
-{
+Notification.prototype.noitfyByChat = function() {
 };
 };
 
 
 module.exports = Notification;
 module.exports = Notification;

+ 12 - 4
lib/service/passport.js

@@ -65,7 +65,7 @@ class PassportService {
       (username, password, done) => {
       (username, password, done) => {
         // find user
         // find user
         User.findUserByUsernameOrEmail(username, password, (err, user) => {
         User.findUserByUsernameOrEmail(username, password, (err, user) => {
-          if (err) { return done(err); }
+          if (err) { return done(err) }
           // check existence and password
           // check existence and password
           if (!user || !user.isPasswordValid(password)) {
           if (!user || !user.isPasswordValid(password)) {
             return done(null, false, { message: 'Incorrect credentials.' });
             return done(null, false, { message: 'Incorrect credentials.' });
@@ -114,7 +114,11 @@ class PassportService {
 
 
     passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, {passReqToCallback: true}),
     passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, {passReqToCallback: true}),
       (req, ldapAccountInfo, done) => {
       (req, ldapAccountInfo, done) => {
-        debug("LDAP authentication has succeeded", ldapAccountInfo);
+        debug('LDAP authentication has succeeded', ldapAccountInfo);
+
+        // store ldapAccountInfo to req
+        req.ldapAccountInfo = ldapAccountInfo;
+
         done(null, ldapAccountInfo);
         done(null, ldapAccountInfo);
       }
       }
     ));
     ));
@@ -196,8 +200,8 @@ class PassportService {
 
 
       // user bind
       // user bind
       const fixedBindDN = (isUserBind) ?
       const fixedBindDN = (isUserBind) ?
-          bindDN.replace(/{{username}}/, loginForm.username):
-          bindDN;
+        bindDN.replace(/{{username}}/, loginForm.username):
+        bindDN;
       const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
       const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
       let serverOpt = { url, bindDN: fixedBindDN, bindCredentials: fixedBindCredentials, searchBase, searchFilter };
       let serverOpt = { url, bindDN: fixedBindDN, bindCredentials: fixedBindCredentials, searchBase, searchFilter };
 
 
@@ -212,6 +216,10 @@ class PassportService {
           server: serverOpt,
           server: serverOpt,
         }, opts);
         }, opts);
         debug('ldap configuration: ', mergedOpts);
         debug('ldap configuration: ', mergedOpts);
+
+        // store configuration to req
+        req.ldapConfiguration = mergedOpts;
+
         callback(null, mergedOpts);
         callback(null, mergedOpts);
       });
       });
     };
     };

+ 6 - 5
lib/util/apiResponse.js

@@ -1,9 +1,9 @@
 'use strict';
 'use strict';
 
 
-function ApiResponse () {
-};
+function ApiResponse() {
+}
 
 
-ApiResponse.error = function (err) {
+ApiResponse.error = function(err) {
   var result = {};
   var result = {};
 
 
   result = {
   result = {
@@ -12,14 +12,15 @@ ApiResponse.error = function (err) {
 
 
   if (err instanceof Error) {
   if (err instanceof Error) {
     result.error = err.toString();
     result.error = err.toString();
-  } else {
+  }
+  else {
     result.error = err;
     result.error = err;
   }
   }
 
 
   return result;
   return result;
 };
 };
 
 
-ApiResponse.success = function (data) {
+ApiResponse.success = function(data) {
   var result = data || {};
   var result = data || {};
 
 
   result.ok = true;
   result.ok = true;

+ 1 - 1
lib/util/formUtil.js

@@ -5,7 +5,7 @@ module.exports = {
     return value
     return value
       .replace(/\r\n/g, '\n')
       .replace(/\r\n/g, '\n')
       .replace(/\r/g, '\n')
       .replace(/\r/g, '\n')
-      ;
+    ;
   },
   },
   stringToArrayFilter: function(value) {
   stringToArrayFilter: function(value) {
     if (!value || value === '') {
     if (!value || value === '') {

+ 31 - 5
lib/util/interceptor-manager.js

@@ -6,25 +6,51 @@ const logger = require('@alias/logger')('growi:InterceptorManager');
 class InterceptorManager {
 class InterceptorManager {
 
 
   constructor() {
   constructor() {
+    this.interceptorAndOrders = []; /* [
+                                          {interceptor: instanceA, order: 200 },
+                                          {interceptor: instanceB, order: 100 },
+                                          ...
+                                       ] */
     this.interceptors = [];
     this.interceptors = [];
   }
   }
 
 
   /**
   /**
    * add an Interceptor
    * add an Interceptor
    * @param {BasicInterceptor} interceptor
    * @param {BasicInterceptor} interceptor
+   * @param {number} order
    */
    */
-  addInterceptor(interceptor) {
-    this.addInterceptors([interceptor]);
+  addInterceptor(interceptor, order) {
+    this.addInterceptors([interceptor], order);
   }
   }
 
 
   /**
   /**
    * add Interceptors
    * add Interceptors
    * @param {BasicInterceptor[]} interceptors
    * @param {BasicInterceptor[]} interceptors
+   * @param {number} order
    */
    */
-  addInterceptors(interceptors) {
+  addInterceptors(interceptors, order) {
+    let isDefaultOrder = false;
+    if (order == null) {
+      order = 100;
+      isDefaultOrder = true;
+    }
+
     const interceptorIds = interceptors.map((i) => i.getId());
     const interceptorIds = interceptors.map((i) => i.getId());
-    logger.debug(`adding interceptors '${interceptorIds}'`);
-    this.interceptors = this.interceptors.concat(interceptors);
+    logger.info(`'addInterceptors' invoked. adding interceptors '${interceptorIds}' at order=${order}${isDefaultOrder ? '(default)' : ''}`);
+
+    this.interceptorAndOrders = this.interceptorAndOrders.concat(
+      interceptors.map(interceptor => {
+        return { interceptor, order };
+      })
+    );
+
+    // sort asc
+    this.interceptorAndOrders.sort((a, b) => a.order - b.order);
+    // store sorted list
+    this.interceptors = this.interceptorAndOrders.map(obj => obj.interceptor);
+
+    const thisInterceptorIds = this.interceptors.map((i) => i.getId());
+    logger.info(`interceptors list has initialized: ${thisInterceptorIds}`);
   }
   }
 
 
   /**
   /**

+ 11 - 10
lib/util/mailer.js

@@ -16,8 +16,7 @@ module.exports = function(crowi) {
     ;
     ;
 
 
 
 
-  function createSMTPClient(option)
-  {
+  function createSMTPClient(option) {
     var client;
     var client;
 
 
     debug('createSMTPClient option', option);
     debug('createSMTPClient option', option);
@@ -45,8 +44,7 @@ module.exports = function(crowi) {
     return client;
     return client;
   }
   }
 
 
-  function createSESClient(option)
-  {
+  function createSESClient(option) {
     var client;
     var client;
 
 
     if (!option) {
     if (!option) {
@@ -70,14 +68,16 @@ module.exports = function(crowi) {
     }
     }
 
 
     if (config.crowi['mail:smtpHost'] && config.crowi['mail:smtpPort']
     if (config.crowi['mail:smtpHost'] && config.crowi['mail:smtpPort']
-      ) {
+    ) {
       // SMTP 設定がある場合はそれを優先
       // SMTP 設定がある場合はそれを優先
       mailer = createSMTPClient();
       mailer = createSMTPClient();
 
 
-    } else if (config.crowi['aws:accessKeyId'] && config.crowi['aws:secretAccessKey']) {
+    }
+    else if (config.crowi['aws:accessKeyId'] && config.crowi['aws:secretAccessKey']) {
       // AWS 設定がある場合はSESを設定
       // AWS 設定がある場合はSESを設定
       mailer = createSESClient();
       mailer = createSESClient();
-    } else {
+    }
+    else {
       mailer = undefined;
       mailer = undefined;
     }
     }
 
 
@@ -87,7 +87,7 @@ module.exports = function(crowi) {
     debug('mailer initialized');
     debug('mailer initialized');
   }
   }
 
 
-  function setupMailConfig (overrideConfig) {
+  function setupMailConfig(overrideConfig) {
     var c = overrideConfig
     var c = overrideConfig
       , mc = {}
       , mc = {}
       ;
       ;
@@ -107,7 +107,7 @@ module.exports = function(crowi) {
       return swig.renderFile(
       return swig.renderFile(
         MAIL_TEMPLATE_DIR + config.template,
         MAIL_TEMPLATE_DIR + config.template,
         templateVars,
         templateVars,
-        function (err, output) {
+        function(err, output) {
           if (err) {
           if (err) {
             throw err;
             throw err;
           }
           }
@@ -116,7 +116,8 @@ module.exports = function(crowi) {
           return mailer.sendMail(setupMailConfig(config), callback);
           return mailer.sendMail(setupMailConfig(config), callback);
         }
         }
       );
       );
-    } else {
+    }
+    else {
       debug('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
       debug('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
       return callback(new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.'), null);
       return callback(new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.'), null);
     }
     }

+ 16 - 11
lib/util/middlewares.js

@@ -10,8 +10,8 @@ exports.csrfKeyGenerator = function(crowi, app) {
     }
     }
 
 
     next();
     next();
-  }
-}
+  };
+};
 
 
 exports.loginChecker = function(crowi, app) {
 exports.loginChecker = function(crowi, app) {
   return function(req, res, next) {
   return function(req, res, next) {
@@ -22,13 +22,15 @@ exports.loginChecker = function(crowi, app) {
       User.findById(req.session.user._id, function(err, userData) {
       User.findById(req.session.user._id, function(err, userData) {
         if (err) {
         if (err) {
           next();
           next();
-        } else {
+        }
+        else {
           req.user = req.session.user = userData;
           req.user = req.session.user = userData;
           res.locals.user = req.user;
           res.locals.user = req.user;
           next();
           next();
         }
         }
       });
       });
-    } else {
+    }
+    else {
       req.user = req.session.user = false;
       req.user = req.session.user = false;
       res.locals.user = req.user;
       res.locals.user = req.user;
       next();
       next();
@@ -204,7 +206,7 @@ exports.adminRequired = function() {
  */
  */
 exports.loginRequired = function(crowi, app, isStrictly = true) {
 exports.loginRequired = function(crowi, app, isStrictly = true) {
   return function(req, res, next) {
   return function(req, res, next) {
-    var User = crowi.model('User')
+    var User = crowi.model('User');
 
 
     // when the route is not strictly restricted
     // when the route is not strictly restricted
     if (!isStrictly) {
     if (!isStrictly) {
@@ -223,11 +225,14 @@ exports.loginRequired = function(crowi, app, isStrictly = true) {
       if (req.user.status === User.STATUS_ACTIVE) {
       if (req.user.status === User.STATUS_ACTIVE) {
         // Active の人だけ先に進める
         // Active の人だけ先に進める
         return next();
         return next();
-      } else if (req.user.status === User.STATUS_REGISTERED) {
+      }
+      else if (req.user.status === User.STATUS_REGISTERED) {
         return res.redirect('/login/error/registered');
         return res.redirect('/login/error/registered');
-      } else if (req.user.status === User.STATUS_SUSPENDED) {
+      }
+      else if (req.user.status === User.STATUS_SUSPENDED) {
         return res.redirect('/login/error/suspended');
         return res.redirect('/login/error/suspended');
-      } else if (req.user.status === User.STATUS_INVITED) {
+      }
+      else if (req.user.status === User.STATUS_INVITED) {
         return res.redirect('/login/invited');
         return res.redirect('/login/invited');
       }
       }
     }
     }
@@ -251,7 +256,7 @@ exports.accessTokenParser = function(crowi, app) {
       return next();
       return next();
     }
     }
 
 
-    var User = crowi.model('User')
+    var User = crowi.model('User');
 
 
     debug('accessToken is', accessToken);
     debug('accessToken is', accessToken);
     User.findUserByApiToken(accessToken)
     User.findUserByApiToken(accessToken)
@@ -306,7 +311,7 @@ exports.checkSearchIndicesGenerated = function(crowi, app) {
 
 
     return next();
     return next();
   };
   };
-}
+};
 
 
 exports.applicationInstalled = function() {
 exports.applicationInstalled = function() {
   return function(req, res, next) {
   return function(req, res, next) {
@@ -321,7 +326,7 @@ exports.applicationInstalled = function() {
 };
 };
 
 
 exports.awsEnabled = function() {
 exports.awsEnabled = function() {
-  return function (req, res, next) {
+  return function(req, res, next) {
     var config = req.config;
     var config = req.config;
     if (config.crowi['aws:region'] !== '' && config.crowi['aws:bucket'] !== '' && config.crowi['aws:accessKeyId'] !== '' && config.crowi['aws:secretAccessKey'] !== '') {
     if (config.crowi['aws:region'] !== '' && config.crowi['aws:bucket'] !== '' && config.crowi['aws:accessKeyId'] !== '' && config.crowi['aws:secretAccessKey'] !== '') {
       req.flash('globalError', 'AWS settings required to use this function. Please ask the administrator.');
       req.flash('globalError', 'AWS settings required to use this function. Please ask the administrator.');

+ 47 - 60
lib/util/search.js

@@ -33,9 +33,9 @@ SearchClient.prototype.checkESVersion = function() {
 
 
 SearchClient.prototype.registerUpdateEvent = function() {
 SearchClient.prototype.registerUpdateEvent = function() {
   var pageEvent = this.crowi.event('page');
   var pageEvent = this.crowi.event('page');
-  pageEvent.on('create', this.syncPageCreated.bind(this))
-  pageEvent.on('update', this.syncPageUpdated.bind(this))
-  pageEvent.on('delete', this.syncPageDeleted.bind(this))
+  pageEvent.on('create', this.syncPageCreated.bind(this));
+  pageEvent.on('update', this.syncPageUpdated.bind(this));
+  pageEvent.on('delete', this.syncPageDeleted.bind(this));
 };
 };
 
 
 SearchClient.prototype.shouldIndexed = function(page) {
 SearchClient.prototype.shouldIndexed = function(page) {
@@ -159,8 +159,7 @@ SearchClient.prototype.prepareBodyForDelete = function(body, page) {
 };
 };
 
 
 
 
-SearchClient.prototype.addPages = function(pages)
-{
+SearchClient.prototype.addPages = function(pages) {
   var self = this;
   var self = this;
   var body = [];
   var body = [];
 
 
@@ -174,8 +173,7 @@ SearchClient.prototype.addPages = function(pages)
   });
   });
 };
 };
 
 
-SearchClient.prototype.updatePages = function(pages)
-{
+SearchClient.prototype.updatePages = function(pages) {
   var self = this;
   var self = this;
   var body = [];
   var body = [];
 
 
@@ -189,8 +187,7 @@ SearchClient.prototype.updatePages = function(pages)
   });
   });
 };
 };
 
 
-SearchClient.prototype.deletePages = function(pages)
-{
+SearchClient.prototype.deletePages = function(pages) {
   var self = this;
   var self = this;
   var body = [];
   var body = [];
 
 
@@ -204,8 +201,7 @@ SearchClient.prototype.deletePages = function(pages)
   });
   });
 };
 };
 
 
-SearchClient.prototype.addAllPages = function()
-{
+SearchClient.prototype.addAllPages = function() {
   var self = this;
   var self = this;
   var Page = this.crowi.model('Page');
   var Page = this.crowi.model('Page');
   var cursor = Page.getStreamOfFindAll();
   var cursor = Page.getStreamOfFindAll();
@@ -214,7 +210,7 @@ SearchClient.prototype.addAllPages = function()
   var skipped = 0;
   var skipped = 0;
 
 
   return new Promise(function(resolve, reject) {
   return new Promise(function(resolve, reject) {
-    cursor.on('data', function (doc) {
+    cursor.on('data', function(doc) {
       if (!doc.creator || !doc.revision || !self.shouldIndexed(doc)) {
       if (!doc.creator || !doc.revision || !self.shouldIndexed(doc)) {
         //debug('Skipped', doc.path);
         //debug('Skipped', doc.path);
         skipped++;
         skipped++;
@@ -230,17 +226,17 @@ SearchClient.prototype.addAllPages = function()
           body: body,
           body: body,
           requestTimeout: Infinity,
           requestTimeout: Infinity,
         }).then(res => {
         }).then(res => {
-          debug('addAllPages add anyway (items, errors, took): ', (res.items || []).length, res.errors, res.took)
+          debug('addAllPages add anyway (items, errors, took): ', (res.items || []).length, res.errors, res.took);
         }).catch(err => {
         }).catch(err => {
-          debug('addAllPages error on add anyway: ', err)
+          debug('addAllPages error on add anyway: ', err);
         });
         });
 
 
         body = [];
         body = [];
       }
       }
-    }).on('error', function (err) {
+    }).on('error', function(err) {
       // TODO: handle err
       // TODO: handle err
       debug('Error cursor:', err);
       debug('Error cursor:', err);
-    }).on('close', function () {
+    }).on('close', function() {
       // all done
       // all done
 
 
       // return if body is empty
       // return if body is empty
@@ -272,8 +268,7 @@ SearchClient.prototype.addAllPages = function()
  *   data: [ pages ...],
  *   data: [ pages ...],
  * }
  * }
  */
  */
-SearchClient.prototype.search = function(query)
-{
+SearchClient.prototype.search = function(query) {
   var self = this;
   var self = this;
 
 
   return new Promise(function(resolve, reject) {
   return new Promise(function(resolve, reject) {
@@ -297,8 +292,7 @@ SearchClient.prototype.search = function(query)
   });
   });
 };
 };
 
 
-SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option)
-{
+SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option) {
   // getting path by default is almost for debug
   // getting path by default is almost for debug
   var fields = ['path'];
   var fields = ['path'];
   if (option) {
   if (option) {
@@ -320,8 +314,7 @@ SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option)
   return query;
   return query;
 };
 };
 
 
-SearchClient.prototype.createSearchQuerySortedByScore = function(option)
-{
+SearchClient.prototype.createSearchQuerySortedByScore = function(option) {
   var fields = ['path'];
   var fields = ['path'];
   if (option) {
   if (option) {
     fields = option.fields || fields;
     fields = option.fields || fields;
@@ -342,14 +335,12 @@ SearchClient.prototype.createSearchQuerySortedByScore = function(option)
   return query;
   return query;
 };
 };
 
 
-SearchClient.prototype.appendResultSize = function(query, from, size)
-{
+SearchClient.prototype.appendResultSize = function(query, from, size) {
   query.from = from || this.DEFAULT_OFFSET;
   query.from = from || this.DEFAULT_OFFSET;
   query.size = size || this.DEFAULT_LIMIT;
   query.size = size || this.DEFAULT_LIMIT;
 };
 };
 
 
-SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keyword)
-{
+SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keyword) {
   // query is created by createSearchQuerySortedByScore() or createSearchQuerySortedByUpdatedAt()
   // query is created by createSearchQuerySortedByScore() or createSearchQuerySortedByUpdatedAt()
   if (!query.body.query.bool) {
   if (!query.body.query.bool) {
     query.body.query.bool = {};
     query.body.query.bool = {};
@@ -379,9 +370,9 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
         query: keywords.join(' '),
         query: keywords.join(' '),
         // TODO: By user's i18n setting, change boost or search target fields
         // TODO: By user's i18n setting, change boost or search target fields
         fields: [
         fields: [
-          "path_ja^2",
-          "path_en^2",
-          "body_ja",
+          'path_ja^2',
+          'path_en^2',
+          'body_ja',
           // "path_en",
           // "path_en",
           // "body_en",
           // "body_en",
         ],
         ],
@@ -410,8 +401,8 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
           query: phrase, // each phrase is quoteted words
           query: phrase, // each phrase is quoteted words
           type: 'phrase',
           type: 'phrase',
           fields: [ // Not use "*.ja" fields here, because we want to analyze (parse) search words
           fields: [ // Not use "*.ja" fields here, because we want to analyze (parse) search words
-            "path_raw^2",
-            "body_raw",
+            'path_raw^2',
+            'body_raw',
           ],
           ],
         }
         }
       });
       });
@@ -428,8 +419,8 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
           query: phrase, // each phrase is quoteted words
           query: phrase, // each phrase is quoteted words
           type: 'phrase',
           type: 'phrase',
           fields: [ // Not use "*.ja" fields here, because we want to analyze (parse) search words
           fields: [ // Not use "*.ja" fields here, because we want to analyze (parse) search words
-            "path_raw^2",
-            "body_raw",
+            'path_raw^2',
+            'body_raw',
           ],
           ],
         }
         }
       });
       });
@@ -439,8 +430,7 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
   }
   }
 };
 };
 
 
-SearchClient.prototype.appendCriteriaForPathFilter = function(query, path)
-{
+SearchClient.prototype.appendCriteriaForPathFilter = function(query, path) {
   // query is created by createSearchQuerySortedByScore() or createSearchQuerySortedByUpdatedAt()
   // query is created by createSearchQuerySortedByScore() or createSearchQuerySortedByUpdatedAt()
   if (!query.body.query.bool) {
   if (!query.body.query.bool) {
     query.body.query.bool = {};
     query.body.query.bool = {};
@@ -455,27 +445,26 @@ SearchClient.prototype.appendCriteriaForPathFilter = function(query, path)
   }
   }
   query.body.query.bool.filter.push({
   query.body.query.bool.filter.push({
     wildcard: {
     wildcard: {
-      "path": path + "/*"
+      'path': path + '/*'
     }
     }
   });
   });
 };
 };
 
 
-SearchClient.prototype.searchKeyword = function(keyword, option)
-{
+SearchClient.prototype.searchKeyword = function(keyword, option) {
+  /* eslint-disable no-unused-vars */
   var from = option.offset || null;
   var from = option.offset || null;
+  /* eslint-enable */
   var query = this.createSearchQuerySortedByScore();
   var query = this.createSearchQuerySortedByScore();
   this.appendCriteriaForKeywordContains(query, keyword);
   this.appendCriteriaForKeywordContains(query, keyword);
 
 
   return this.search(query);
   return this.search(query);
 };
 };
 
 
-SearchClient.prototype.searchByPath = function(keyword, prefix)
-{
+SearchClient.prototype.searchByPath = function(keyword, prefix) {
   // TODO path 名だけから検索
   // TODO path 名だけから検索
 };
 };
 
 
-SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, option)
-{
+SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, option) {
   var from = option.offset || null;
   var from = option.offset || null;
   var query = this.createSearchQuerySortedByScore();
   var query = this.createSearchQuerySortedByScore();
   this.appendCriteriaForKeywordContains(query, keyword);
   this.appendCriteriaForKeywordContains(query, keyword);
@@ -488,8 +477,7 @@ SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, option)
   return this.search(query);
   return this.search(query);
 };
 };
 
 
-SearchClient.prototype.getParsedKeywords = function(keyword)
-{
+SearchClient.prototype.getParsedKeywords = function(keyword) {
   var matchWords = [];
   var matchWords = [];
   var notMatchWords = [];
   var notMatchWords = [];
   var phraseWords = [];
   var phraseWords = [];
@@ -507,9 +495,10 @@ SearchClient.prototype.getParsedKeywords = function(keyword)
 
 
     phrases.forEach(function(phrase) {
     phrases.forEach(function(phrase) {
       phrase.trim();
       phrase.trim();
-      if (phrase.match(/^\-/)) {
-        notPhraseWords.push(phrase.replace(/^\-/, ''));
-      } else {
+      if (phrase.match(/^-/)) {
+        notPhraseWords.push(phrase.replace(/^-/, ''));
+      }
+      else {
         phraseWords.push(phrase);
         phraseWords.push(phrase);
       }
       }
     });
     });
@@ -521,9 +510,10 @@ SearchClient.prototype.getParsedKeywords = function(keyword)
       return;
       return;
     }
     }
 
 
-    if (word.match(/^\-(.+)$/)) {
+    if (word.match(/^-(.+)$/)) {
       notMatchWords.push((RegExp.$1));
       notMatchWords.push((RegExp.$1));
-    } else {
+    }
+    else {
       matchWords.push(word);
       matchWords.push(word);
     }
     }
   });
   });
@@ -534,10 +524,9 @@ SearchClient.prototype.getParsedKeywords = function(keyword)
     phrase: phraseWords,
     phrase: phraseWords,
     not_phrase: notPhraseWords,
     not_phrase: notPhraseWords,
   };
   };
-}
+};
 
 
-SearchClient.prototype.syncPageCreated = function(page, user)
-{
+SearchClient.prototype.syncPageCreated = function(page, user) {
   debug('SearchClient.syncPageCreated', page.path);
   debug('SearchClient.syncPageCreated', page.path);
 
 
   if (!this.shouldIndexed(page)) {
   if (!this.shouldIndexed(page)) {
@@ -548,13 +537,12 @@ SearchClient.prototype.syncPageCreated = function(page, user)
   .then(function(res) {
   .then(function(res) {
     debug('ES Response', res);
     debug('ES Response', res);
   })
   })
-  .catch(function(err){
+  .catch(function(err) {
     debug('ES Error', err);
     debug('ES Error', err);
   });
   });
 };
 };
 
 
-SearchClient.prototype.syncPageUpdated = function(page, user)
-{
+SearchClient.prototype.syncPageUpdated = function(page, user) {
   debug('SearchClient.syncPageUpdated', page.path);
   debug('SearchClient.syncPageUpdated', page.path);
   // TODO delete
   // TODO delete
   if (!this.shouldIndexed(page)) {
   if (!this.shouldIndexed(page)) {
@@ -562,7 +550,7 @@ SearchClient.prototype.syncPageUpdated = function(page, user)
     .then(function(res) {
     .then(function(res) {
       debug('deletePages: ES Response', res);
       debug('deletePages: ES Response', res);
     })
     })
-    .catch(function(err){
+    .catch(function(err) {
       debug('deletePages:ES Error', err);
       debug('deletePages:ES Error', err);
     });
     });
 
 
@@ -573,20 +561,19 @@ SearchClient.prototype.syncPageUpdated = function(page, user)
   .then(function(res) {
   .then(function(res) {
     debug('ES Response', res);
     debug('ES Response', res);
   })
   })
-  .catch(function(err){
+  .catch(function(err) {
     debug('ES Error', err);
     debug('ES Error', err);
   });
   });
 };
 };
 
 
-SearchClient.prototype.syncPageDeleted = function(page, user)
-{
+SearchClient.prototype.syncPageDeleted = function(page, user) {
   debug('SearchClient.syncPageDeleted', page.path);
   debug('SearchClient.syncPageDeleted', page.path);
 
 
   this.deletePages([page])
   this.deletePages([page])
   .then(function(res) {
   .then(function(res) {
     debug('deletePages: ES Response', res);
     debug('deletePages: ES Response', res);
   })
   })
-  .catch(function(err){
+  .catch(function(err) {
     debug('deletePages:ES Error', err);
     debug('deletePages:ES Error', err);
   });
   });
 
 

+ 21 - 17
lib/util/slack.js

@@ -5,8 +5,6 @@
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   'use strict';
   'use strict';
 
 
-  const SLACK_URL = 'https://slack.com';
-
   const debug = require('debug')('growi:util:slack'),
   const debug = require('debug')('growi:util:slack'),
     config = crowi.getConfig(),
     config = crowi.getConfig(),
     Config = crowi.model('Config'),
     Config = crowi.model('Config'),
@@ -17,7 +15,7 @@ module.exports = function(crowi) {
     const client = new Slack();
     const client = new Slack();
     client.setWebhook(config.notification['slack:incomingWebhookUrl']);
     client.setWebhook(config.notification['slack:incomingWebhookUrl']);
     client.webhook(messageObj, callback);
     client.webhook(messageObj, callback);
-  }
+  };
 
 
   const postWithWebApi = function(messageObj, callback) {
   const postWithWebApi = function(messageObj, callback) {
     const client = new Slack(config.notification['slack:token']);
     const client = new Slack(config.notification['slack:token']);
@@ -26,7 +24,7 @@ module.exports = function(crowi) {
       messageObj.attachments = JSON.stringify(messageObj.attachments);
       messageObj.attachments = JSON.stringify(messageObj.attachments);
     }
     }
     client.api('chat.postMessage', messageObj, callback);
     client.api('chat.postMessage', messageObj, callback);
-  }
+  };
 
 
   const convertMarkdownToMrkdwn = function(body) {
   const convertMarkdownToMrkdwn = function(body) {
     var url = '';
     var url = '';
@@ -39,7 +37,7 @@ module.exports = function(crowi) {
       .replace(/#{1,}\s?(.+)/g, '\n*$1*')
       .replace(/#{1,}\s?(.+)/g, '\n*$1*')
       .replace(/(\[(.+)\]\((https?:\/\/.+)\))/g, '<$3|$2>')
       .replace(/(\[(.+)\]\((https?:\/\/.+)\))/g, '<$3|$2>')
       .replace(/(\[(.+)\]\((\/.+)\))/g, '<' + url + '$3|$2>')
       .replace(/(\[(.+)\]\((\/.+)\))/g, '<' + url + '$3|$2>')
-      ;
+    ;
 
 
     return body;
     return body;
   };
   };
@@ -55,25 +53,29 @@ module.exports = function(crowi) {
 
 
   const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
   const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
     var diff = require('diff');
     var diff = require('diff');
-    var diffText = ''
+    var diffText = '';
 
 
     diff.diffLines(previousRevision.body, page.revision.body).forEach(function(line) {
     diff.diffLines(previousRevision.body, page.revision.body).forEach(function(line) {
-      debug('diff line', line)
+      debug('diff line', line);
+      /* eslint-disable no-unused-vars */
       var value = line.value.replace(/\r\n|\r/g, '\n');
       var value = line.value.replace(/\r\n|\r/g, '\n');
+      /* eslint-enable */
       if (line.added) {
       if (line.added) {
         diffText += `:pencil2: ...\n${line.value}`;
         diffText += `:pencil2: ...\n${line.value}`;
-      } else if (line.removed) {
+      }
+      else if (line.removed) {
         // diffText += '-' + line.value.replace(/(.+)?\n/g, '- $1\n');
         // diffText += '-' + line.value.replace(/(.+)?\n/g, '- $1\n');
         // 1以下は無視
         // 1以下は無視
         if (line.count > 1) {
         if (line.count > 1) {
           diffText += `:wastebasket: ... ${line.count} lines\n`;
           diffText += `:wastebasket: ... ${line.count} lines\n`;
         }
         }
-      } else {
+      }
+      else {
         //diffText += '...\n';
         //diffText += '...\n';
       }
       }
     });
     });
 
 
-    debug('diff is', diffText)
+    debug('diff is', diffText);
 
 
     return diffText;
     return diffText;
   };
   };
@@ -84,7 +86,8 @@ module.exports = function(crowi) {
 
 
     if (updateType == 'create') {
     if (updateType == 'create') {
       body = prepareAttachmentTextForCreate(page, user);
       body = prepareAttachmentTextForCreate(page, user);
-    } else {
+    }
+    else {
       body = prepareAttachmentTextForUpdate(page, user, previousRevision);
       body = prepareAttachmentTextForUpdate(page, user, previousRevision);
     }
     }
 
 
@@ -96,7 +99,7 @@ module.exports = function(crowi) {
       title: page.path,
       title: page.path,
       title_link: url + '/' + page._id,
       title_link: url + '/' + page._id,
       text: body,
       text: body,
-      mrkdwn_in: ["text"],
+      mrkdwn_in: ['text'],
     };
     };
     if (user.image) {
     if (user.image) {
       attachment.author_icon = user.image;
       attachment.author_icon = user.image;
@@ -119,7 +122,8 @@ module.exports = function(crowi) {
     const pageUrl = `<${url}${path}|${path}>`;
     const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
     if (updateType == 'create') {
       text = `:white_check_mark: ${user.username} created a new page! ${pageUrl}`;
       text = `:white_check_mark: ${user.username} created a new page! ${pageUrl}`;
-    } else {
+    }
+    else {
       text = `:up: ${user.username} updated ${pageUrl}`;
       text = `:up: ${user.username} updated ${pageUrl}`;
     }
     }
 
 
@@ -144,22 +148,22 @@ module.exports = function(crowi) {
       // when incoming Webhooks is prioritized
       // when incoming Webhooks is prioritized
       if (Config.isIncomingWebhookPrioritized(config)) {
       if (Config.isIncomingWebhookPrioritized(config)) {
         if (Config.hasSlackIwhUrl(config)) {
         if (Config.hasSlackIwhUrl(config)) {
-          debug(`posting message with IncomingWebhook`);
+          debug('posting message with IncomingWebhook');
           postWithIwh(messageObj, callback);
           postWithIwh(messageObj, callback);
         }
         }
         else if (Config.hasSlackToken(config)) {
         else if (Config.hasSlackToken(config)) {
-          debug(`posting message with Web API`);
+          debug('posting message with Web API');
           postWithWebApi(messageObj, callback);
           postWithWebApi(messageObj, callback);
         }
         }
       }
       }
       // else
       // else
       else {
       else {
         if (Config.hasSlackToken(config)) {
         if (Config.hasSlackToken(config)) {
-          debug(`posting message with Web API`);
+          debug('posting message with Web API');
           postWithWebApi(messageObj, callback);
           postWithWebApi(messageObj, callback);
         }
         }
         else if (Config.hasSlackIwhUrl(config)) {
         else if (Config.hasSlackIwhUrl(config)) {
-          debug(`posting message with IncomingWebhook`);
+          debug('posting message with IncomingWebhook');
           postWithIwh(messageObj, callback);
           postWithIwh(messageObj, callback);
         }
         }
       }
       }

+ 48 - 44
lib/util/swigFunctions.js

@@ -1,24 +1,27 @@
 module.exports = function(crowi, app, req, locals) {
 module.exports = function(crowi, app, req, locals) {
   var debug = require('debug')('growi:lib:swigFunctions')
   var debug = require('debug')('growi:lib:swigFunctions')
+    , stringWidth = require('string-width')
     , Page = crowi.model('Page')
     , Page = crowi.model('Page')
     , Config = crowi.model('Config')
     , Config = crowi.model('Config')
     , User = crowi.model('User')
     , User = crowi.model('User')
     , passportService = crowi.passportService
     , passportService = crowi.passportService
   ;
   ;
 
 
+  debug('initializing swigFunctions');
+
   locals.nodeVersion = function() {
   locals.nodeVersion = function() {
     return crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version : '-';
     return crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version : '-';
-  }
+  };
   locals.npmVersion = function() {
   locals.npmVersion = function() {
     return crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version : '-';
     return crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version : '-';
-  }
+  };
   locals.yarnVersion = function() {
   locals.yarnVersion = function() {
     return crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version : '-';
     return crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version : '-';
-  }
+  };
 
 
   locals.growiVersion = function() {
   locals.growiVersion = function() {
     return crowi.version;
     return crowi.version;
-  }
+  };
 
 
   // token getter
   // token getter
   locals.csrf = function() {
   locals.csrf = function() {
@@ -26,16 +29,17 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.getAppTitleFontSize = function(appTitle) {
   locals.getAppTitleFontSize = function(appTitle) {
+    let appTitleWidth = stringWidth(appTitle);
     let fontSize = 22;
     let fontSize = 22;
-    if (appTitle.length < 13) { /* do nothing */ }
-    else if (appTitle.length < 21) {
-      fontSize -= 3 * (Math.floor((appTitle.length - 13) / 3) + 1);
+    if (appTitleWidth < 13) { /* do nothing */ }
+    else if (appTitleWidth < 21) {
+      fontSize -= 3 * (Math.floor((appTitleWidth - 13) / 3) + 1);
     }
     }
     else  {
     else  {
       fontSize = 11;
       fontSize = 11;
     }
     }
     return fontSize;
     return fontSize;
-  }
+  };
 
 
   /**
   /**
    * return app title
    * return app title
@@ -43,7 +47,7 @@ module.exports = function(crowi, app, req, locals) {
   locals.appTitle = function() {
   locals.appTitle = function() {
     var config = crowi.getConfig();
     var config = crowi.getConfig();
     return Config.appTitle(config);
     return Config.appTitle(config);
-  }
+  };
 
 
   /**
   /**
    * return true if enabled
    * return true if enabled
@@ -51,7 +55,7 @@ module.exports = function(crowi, app, req, locals) {
   locals.isEnabledPassport = function() {
   locals.isEnabledPassport = function() {
     var config = crowi.getConfig();
     var config = crowi.getConfig();
     return Config.isEnabledPassport(config);
     return Config.isEnabledPassport(config);
-  }
+  };
 
 
   /**
   /**
    * return true if local strategy has been setup successfully
    * return true if local strategy has been setup successfully
@@ -59,23 +63,23 @@ module.exports = function(crowi, app, req, locals) {
    */
    */
   locals.isPassportLocalStrategySetup = function() {
   locals.isPassportLocalStrategySetup = function() {
     return passportService != null && passportService.isLocalStrategySetup;
     return passportService != null && passportService.isLocalStrategySetup;
-  }
+  };
 
 
   /**
   /**
    * return true if enabled and strategy has been setup successfully
    * return true if enabled and strategy has been setup successfully
    */
    */
   locals.isLdapSetup = function() {
   locals.isLdapSetup = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && passportService.isLdapStrategySetup;
-  }
+  };
 
 
   /**
   /**
    * return true if enabled but strategy has some problem
    * return true if enabled but strategy has some problem
    */
    */
   locals.isLdapSetupFailed = function() {
   locals.isLdapSetupFailed = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
     return Config.isEnabledPassport(config) && Config.isEnabledPassportLdap(config) && !passportService.isLdapStrategySetup;
-  }
+  };
 
 
   locals.googleLoginEnabled = function() {
   locals.googleLoginEnabled = function() {
     // return false if Passport is enabled
     // return false if Passport is enabled
@@ -84,7 +88,7 @@ module.exports = function(crowi, app, req, locals) {
       return false;
       return false;
     }
     }
 
 
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
   };
 
 
@@ -96,70 +100,70 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isEnabledPlugins = function() {
   locals.isEnabledPlugins = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledPlugins(config);
     return Config.isEnabledPlugins(config);
-  }
+  };
 
 
   locals.isEnabledLinebreaks = function() {
   locals.isEnabledLinebreaks = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledLinebreaks(config);
     return Config.isEnabledLinebreaks(config);
-  }
+  };
 
 
   locals.isEnabledLinebreaksInComments = function() {
   locals.isEnabledLinebreaksInComments = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledLinebreaksInComments(config);
     return Config.isEnabledLinebreaksInComments(config);
-  }
+  };
 
 
   locals.customCss = function() {
   locals.customCss = function() {
     return Config.customCss();
     return Config.customCss();
-  }
+  };
 
 
   locals.customScript = function() {
   locals.customScript = function() {
     return Config.customScript();
     return Config.customScript();
-  }
+  };
 
 
   locals.customHeader = function() {
   locals.customHeader = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.customHeader(config);
     return Config.customHeader(config);
-  }
+  };
 
 
   locals.theme = function() {
   locals.theme = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.theme(config);
     return Config.theme(config);
-  }
+  };
 
 
   locals.customTitle = function(page) {
   locals.customTitle = function(page) {
     const config = crowi.getConfig();
     const config = crowi.getConfig();
     return Config.customTitle(config, page);
     return Config.customTitle(config, page);
-  }
+  };
 
 
   locals.behaviorType = function() {
   locals.behaviorType = function() {
     var config = crowi.getConfig();
     var config = crowi.getConfig();
     return Config.behaviorType(config);
     return Config.behaviorType(config);
-  }
+  };
 
 
   locals.layoutType = function() {
   locals.layoutType = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.layoutType(config);
     return Config.layoutType(config);
-  }
+  };
 
 
   locals.highlightJsStyle = function() {
   locals.highlightJsStyle = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.highlightJsStyle(config);
     return Config.highlightJsStyle(config);
-  }
+  };
 
 
   locals.highlightJsStyleBorder = function() {
   locals.highlightJsStyleBorder = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.highlightJsStyleBorder(config);
     return Config.highlightJsStyleBorder(config);
-  }
+  };
 
 
   locals.isEnabledTimeline = function() {
   locals.isEnabledTimeline = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledTimeline(config);
     return Config.isEnabledTimeline(config);
-  }
+  };
 
 
   locals.slackConfigured = function() {
   locals.slackConfigured = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
     if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
       return true;
       return true;
     }
     }
@@ -167,12 +171,12 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isUploadable = function() {
   locals.isUploadable = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isUploadable(config);
     return Config.isUploadable(config);
   };
   };
 
 
   locals.isEnabledAttachTitleHeader = function() {
   locals.isEnabledAttachTitleHeader = function() {
-    var config = crowi.getConfig()
+    var config = crowi.getConfig();
     return Config.isEnabledAttachTitleHeader(config);
     return Config.isEnabledAttachTitleHeader(config);
   };
   };
 
 
@@ -189,7 +193,7 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.isUserPageList = function(path) {
   locals.isUserPageList = function(path) {
-    if (path.match(/^\/user\/[^\/]+\/$/)) {
+    if (path.match(/^\/user\/[^/]+\/$/)) {
       return true;
       return true;
     }
     }
 
 
@@ -229,7 +233,7 @@ module.exports = function(crowi, app, req, locals) {
   };
   };
 
 
   locals.css = {
   locals.css = {
-    grant: function (pageData) {
+    grant: function(pageData) {
       if (!pageData) {
       if (!pageData) {
         return '';
         return '';
       }
       }
@@ -249,7 +253,7 @@ module.exports = function(crowi, app, req, locals) {
       }
       }
       return '';
       return '';
     },
     },
-    userStatus: function (user) {
+    userStatus: function(user) {
       //debug('userStatus', user._id, user.usename, user.status);
       //debug('userStatus', user._id, user.usename, user.status);
 
 
       switch (user.status) {
       switch (user.status) {

+ 1 - 1
lib/util/xss.js

@@ -13,7 +13,7 @@ class Xss {
       // allow all attributes
       // allow all attributes
       option.onTagAttr = function(tag, name, value, isWhiteAttr) {
       option.onTagAttr = function(tag, name, value, isWhiteAttr) {
         return `${name}="${value}"`;
         return `${name}="${value}"`;
-      }
+      };
     }
     }
     // create the XSS Filter instance
     // create the XSS Filter instance
     this.myxss = new xss.FilterXSS(option);
     this.myxss = new xss.FilterXSS(option);

+ 35 - 7
lib/views/_form.html

@@ -49,19 +49,47 @@
       {% if forceGrant %}
       {% if forceGrant %}
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       {% else %}
       {% else %}
-      <select name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
+      <div>
+        <div id="page-grant-selector"></div>
+      </div>
+      <input type="hidden" id="page-grant" name="pageForm[grant]" value="{{ pageForm.grant|default(page.grant) }}">
+      <input id="grant-group" type="hidden" value="{% if pageForm.grant %}{{ pageForm.grant }}{% endif %}">
+<!--
+      <select id="select-grant" name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
         {% for grantId, grantLabel in consts.pageGrants %}
         {% for grantId, grantLabel in consts.pageGrants %}
-        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %} {% if grantId == 5 && userRelatedGroups.length == 0 %}disabled{% endif %}>{{ t(grantLabel) }}</option>
+        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %}>{{ t(grantLabel) }}</option>
         {% endfor %}
         {% endfor %}
+        {% if user and user.admin && userRelatedGroups %}
+        <option id="no-group" value="/admin/user-groups">{{ t('Only inside the group') }} you have no groups.</option>
+        {% endif %}
+        <option id="group-grant" value="5">{{ t('Only inside the group') }}</option>
+        {% if pageForm.grant|default(page.grant) == "5" && pageRelatedGroup != null %}
+        <option id="group-grant" value="5" selected>{{pageRelatedGroup}}</option>
+        {% endif %}
       </select>
       </select>
+      <input id="select-grant-pre" type="hidden" value="{{ page.grant }}">
       {% endif %}
       {% endif %}
+      <input id="grant-group" type="hidden" name="pageForm[grantUserGroupId]" value="">
       {% if userRelatedGroups.length != 0 %}
       {% if userRelatedGroups.length != 0 %}
-      <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
-        {% for userGroup in userRelatedGroups %}
-        <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
-        {% endfor %}
-      </select>
+      <div class="collapse width">
+        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
+          {% for userGroup in userRelatedGroups %}
+          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
+          {% endfor %}
+        </select>
+      </div>
+      {% endif %} -->
+      {% if userRelatedGroups.length != 0 %}
+      <div>
+        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
+          {% for userGroup in userRelatedGroups %}
+          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
+          {% endfor %}
+        </select>
+      </div>
       {% endif %}
       {% endif %}
+      <!-- <input type="hidden" id="page-grant" value="{{ page.grant }}"> -->
+      <input type="hidden" id="user-related-group-data" value="{{userRelatedGroups}}">
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">
       <button type="submit" class="btn btn-primary btn-submit" id="edit-form-submit">{{ t('Update') }}</button>
       <button type="submit" class="btn btn-primary btn-submit" id="edit-form-submit">{{ t('Update') }}</button>
     </div>
     </div>

+ 12 - 5
lib/views/admin/customize.html

@@ -281,7 +281,7 @@
       </fieldset>
       </fieldset>
       </form>
       </form>
 
 
-      <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="cutomhighlightJsStyleSettingForm" role="form">
+      <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="customhighlightJsStyleSettingForm" role="form">
         <fieldset>
         <fieldset>
           <legend>{{ t('customize_page.Code Highlight') }}</legend>
           <legend>{{ t('customize_page.Code Highlight') }}</legend>
           <div class="form-group">
           <div class="form-group">
@@ -299,11 +299,11 @@
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <div class="col-xs-9">
             <div class="col-xs-9">
               <div class="btn-group btn-toggle" data-toggle="buttons">
               <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary" onclick="selectBorderOn()">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="true" type="radio"
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="true" type="radio"
                       {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> ON
                       {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> ON
                 </label>
                 </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="default">
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="default" onclick="selectBorderOff()">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="false" type="radio"
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="false" type="radio"
                       {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> OFF
                       {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> OFF
                 </label>
                 </label>
@@ -315,7 +315,7 @@
 
 
           <p class="help-block">
           <p class="help-block">
             Examples:
             Examples:
-            <pre class="hljs"><code class="highlightjs-demo">function $initHighlight(block, cls) {
+            <pre class="hljs {% if !settingForm['customize:highlightJsStyleBorder'] %}hljs-no-border{% endif %}"><code class="highlightjs-demo">function $initHighlight(block, cls) {
   try {
   try {
     if (cls.search(/\bno\-highlight\b/) != -1)
     if (cls.search(/\bno\-highlight\b/) != -1)
       return process(block, true, 0x0F) +
       return process(block, true, 0x0F) +
@@ -507,7 +507,7 @@ window.addEventListener('load', (event) => {
 {% block body_end %}
 {% block body_end %}
   {% parent %}
   {% parent %}
   <script>
   <script>
-    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #cutomhighlightJsStyleSettingForm,
+    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customhighlightJsStyleSettingForm,
        #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomcssSettingForm, #cutomscriptSettingForm, #customtitleSettingForm`
        #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomcssSettingForm, #cutomscriptSettingForm, #customtitleSettingForm`
     ).each(function() {
     ).each(function() {
       $(this).submit(function()
       $(this).submit(function()
@@ -592,6 +592,13 @@ window.addEventListener('load', (event) => {
       $('#themeOptions .active').removeClass('active');
       $('#themeOptions .active').removeClass('active');
       $(`#themeOptions #theme-option-${theme}`).addClass('active');
       $(`#themeOptions #theme-option-${theme}`).addClass('active');
     }
     }
+
+    function selectBorderOn(){
+      $('.hljs-no-border').removeClass('hljs-no-border');
+    }
+    function selectBorderOff(){
+      $('#customhighlightJsStyleSettingForm .hljs').addClass('hljs-no-border')
+    }
   </script>
   </script>
 
 
 </div>
 </div>

+ 4 - 4
lib/views/admin/markdown.html

@@ -44,7 +44,7 @@
 
 
         <div class="form-group">
         <div class="form-group">
           <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
           <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
-            {{ t('markdown_setting.validate Line Break') }}
+            {{ t('markdown_setting.Enable Line Break') }}
           </label>
           </label>
           <div class="col-xs-5">
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <div class="btn-group btn-toggle" data-toggle="buttons">
@@ -57,14 +57,14 @@
                     {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
                     {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
               </label>
               </label>
             </div>
             </div>
-            <p class="help-block">{{ t("markdown_setting.treat_text") }}
+            <p class="help-block">{{ t("markdown_setting.Enable Line Break desc") }}
 </p>
 </p>
           </div>
           </div>
         </div>
         </div>
 
 
         <div class="form-group">
         <div class="form-group">
           <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
           <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
-            (TBD)<br>{{ t("markdown_setting.validate_comment") }}
+            (TBD)<br>{{ t("markdown_setting.Enable Line Break for comment") }}
           </label>
           </label>
           <div class="col-xs-5">
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
             <div class="btn-group btn-toggle" data-toggle="buttons">
@@ -77,7 +77,7 @@
                     {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
                     {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
               </label>
               </label>
             </div>
             </div>
-            <p class="help-block">{{ t("markdown_setting.treat_comment") }}<br>{{ t("markdown_setting.TBD") }}</p>
+            <p class="help-block">{{ t("markdown_setting.Enable Line Break for comment desc") }}<br>{{ t("markdown_setting.TBD") }}</p>
           </div>
           </div>
         </div>
         </div>
 
 

+ 9 - 5
lib/views/admin/widget/passport/ldap.html

@@ -62,14 +62,17 @@
               name="settingForm[security:passport-ldap:bindDN]" value="{{ settingForm['security:passport-ldap:bindDN'] || '' }}">
               name="settingForm[security:passport-ldap:bindDN]" value="{{ settingForm['security:passport-ldap:bindDN'] || '' }}">
           <p class="help-block passport-ldap-managerbind" {% if isUserBind %}style="display: none;"{% endif %}>
           <p class="help-block passport-ldap-managerbind" {% if isUserBind %}style="display: none;"{% endif %}>
             <small>
             <small>
-              {{ t("security_setting.ldap.bind_DN_manager_detail") }}
+              {{ t("security_setting.ldap.bind_DN_manager_detail") }}<br>
+              {{ t("security_setting.example") }}1: <code>uid=admin,dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}2: <code>admin@domain.com</code>
             </small>
             </small>
           </p>
           </p>
           <p class="help-block passport-ldap-userbind" {% if !isUserBind %}style="display: none;"{% endif %}>
           <p class="help-block passport-ldap-userbind" {% if !isUserBind %}style="display: none;"{% endif %}>
             <small>
             <small>
               {{ t("security_setting.ldap.bind_DN_user_detail1") }}<br>
               {{ t("security_setting.ldap.bind_DN_user_detail1") }}<br>
               {{ t("security_setting.ldap.bind_DN_user_detail2") }}<br>
               {{ t("security_setting.ldap.bind_DN_user_detail2") }}<br>
-              {{ t("security_setting.example") }}: <code>uid={% raw %}{{username}}{% endraw %},dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}1: <code>uid={% raw %}{{username}}{% endraw %},dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}2: <code>{% raw %}{{username}}{% endraw %}@domain.com</code>
             </small>
             </small>
           </p>
           </p>
           </div>
           </div>
@@ -105,9 +108,10 @@
               {{ t("security_setting.ldap.search_filter_detail3") }}
               {{ t("security_setting.ldap.search_filter_detail3") }}
             </small>
             </small>
           </p>
           </p>
-          <p>
+          <p class="help-block">
             <small>
             <small>
-              {{ t("security_setting.ldap.search_filter_example") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code>
+              {{ t("security_setting.example") }}1 - {{ t("security_setting.ldap.search_filter_example1") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code><br>
+              {{ t("security_setting.example") }}2 - {{ t("security_setting.ldap.search_filter_example2") }}: <code>(sAMAccountName={% raw %}{{username}}{% endraw %})</code>
             </small>
             </small>
           </p>
           </p>
         </div>
         </div>
@@ -307,7 +311,7 @@
 
 
       <div class="modal-body">
       <div class="modal-body">
 
 
-        {% include '../../../widget/passport/ldap-association-tester.html' %}
+        {% include '../../../widget/passport/ldap-association-tester.html' with { showLog: true } %}
 
 
       </div><!-- /.modal-body -->
       </div><!-- /.modal-body -->
 
 

+ 10 - 0
lib/views/layout-growi/not_found.html

@@ -18,3 +18,13 @@
     </div> {# /.col- #}
     </div> {# /.col- #}
   </div>
   </div>
 {% endblock %}
 {% endblock %}
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+
+  <div id="crowi-modals">
+    {% include '../modal/select_grant_group.html' %}
+  </div>
+{% endblock %}

+ 23 - 0
lib/views/modal/select_grant_group.html

@@ -0,0 +1,23 @@
+<div class="modal select-grant-group" id="select-grant-group">
+  <div class="modal-dialog">
+    <div class="modal-content">
+
+      <div class="modal-header bg-primary">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <div class="modal-title">{{ t('SelectGrantGroup') }}</div>
+      </div>
+
+      <div class="modal-body">
+        <p>グループを下のリストから選択</p>
+
+        <ul class="list-inline">
+          {% for sGroup in userRelatedGroups %}
+          <li><button class="btn btn-xs btn-primary" onclick="$('#grant-group').val('{{sGroup.id}}')" data-dismiss="modal">{{sGroup.name}}</button></li>
+          {% endfor %}
+        </ul>
+
+      </div><!-- /.modal-body -->
+
+    </div><!-- /.modal-content -->
+  </div><!-- /.modal-dialog -->
+</div><!-- /.modal -->

+ 1 - 0
lib/views/widget/page_modals.html

@@ -3,3 +3,4 @@
 {% include '../modal/duplicate.html' %}
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}
 {% include '../modal/put_back.html' %}
 {% include '../modal/page_name_warning.html' %}
 {% include '../modal/page_name_warning.html' %}
+{% include '../modal/select_grant_group.html' %}

+ 10 - 2
lib/views/widget/page_tabs.html

@@ -49,9 +49,17 @@
     {% endif %}
     {% endif %}
   {% endif %}
   {% endif %}
 
 
-  <li class="nav-main-right-tab pull-right"><a href="#revision-history" data-toggle="tab"><i class="icon-layers"></i> History</a></li>
+  <li class="nav-main-right-tab pull-right">
+    <a href="#revision-history" data-toggle="tab">
+      <i class="icon-layers"></i><span class="hidden-xs">  History</span>
+    </a>
+  </li>
   {% if not isPortal %}
   {% if not isPortal %}
-    <li class="nav-main-right-tab pull-right"><a href="?presentation=1" class="toggle-presentation"><i class="icon-film"></i> {{ t('Presentation Mode') }}</a></li>
+    <li class="nav-main-right-tab pull-right">
+      <a href="?presentation=1" class="toggle-presentation">
+        <i class="icon-film"></i><span class="hidden-xs"> {{ t('Presentation Mode') }}</span>
+      </a>
+    </li>
   {% endif %}
   {% endif %}
 
 
 </ul>
 </ul>

+ 34 - 3
lib/views/widget/passport/ldap-association-tester.html

@@ -14,12 +14,20 @@
       </div>
       </div>
     </div>
     </div>
 
 
-    <div class="form-group">
-      <button type="button" class="btn btn-default col-xs-offset-5 col-xs-2" onclick="testLdapCredentials()">{{ t('Test') }}</button>
-    </div>
 
 
   </fieldset>
   </fieldset>
 
 
+  {% if showLog %}
+  <fieldset>
+    <h5>Logs</h5>
+    <textarea id="taLogs" class="col-xs-12" rows="4" readonly></textarea>
+  </fieldset>
+  {% endif %}
+
+  <fieldset class="mt-4">
+    <button type="button" class="btn btn-default col-xs-offset-5 col-xs-2" onclick="testLdapCredentials()">{{ t('Test') }}</button>
+  </fieldset>
+
   <script>
   <script>
     /**
     /**
      * test association (ajax)
      * test association (ajax)
@@ -44,6 +52,14 @@
           }, 5000);
           }, 5000);
         }
         }
       }
       }
+      /**
+       * add logs
+       */
+      function addLogs(formId, log) {
+        const textarea = $(`#${formId} #taLogs`);
+        const newLog = `${new Date()} - ${log}\n\n`;
+        textarea.val(`${newLog}${textarea.val()}`);
+      }
 
 
       var $form = $('#formTestLdapCredentials');
       var $form = $('#formTestLdapCredentials');
       var $action = '/_api/login/testLdap';
       var $action = '/_api/login/testLdap';
@@ -59,6 +75,21 @@
           else {
           else {
             showMessage($id, data.message, data.status);
             showMessage($id, data.message, data.status);
           }
           }
+
+          // add logs
+          if ('true' === '{{showLog}}') {
+            if (data.err) {
+              addLogs($id, data.err);
+            }
+            if (data.ldapConfiguration) {
+              const prettified = JSON.stringify(data.ldapConfiguration.server, undefined, 4);
+              addLogs($id, `LDAP Configuration : ${prettified}`);
+            }
+            if (data.ldapAccountInfo) {
+              const prettified = JSON.stringify(data.ldapAccountInfo, undefined, 4);
+              addLogs($id, `Retrieved LDAP Account : ${prettified}`);
+            }
+          }
         })
         })
         .fail(function() {
         .fail(function() {
           showMessage($id, 'エラーが発生しました', 'danger');
           showMessage($id, 'エラーが発生しました', 'danger');

+ 12 - 11
local_modules/crowi-fileupload-aws/index.js

@@ -9,14 +9,14 @@ module.exports = function(crowi) {
     , debug = require('debug')('growi:lib:fileUploaderAws')
     , debug = require('debug')('growi:lib:fileUploaderAws')
     , lib = {}
     , lib = {}
     , getAwsConfig = function() {
     , getAwsConfig = function() {
-        var config = crowi.getConfig();
-        return {
-          accessKeyId: config.crowi['aws:accessKeyId'],
-          secretAccessKey: config.crowi['aws:secretAccessKey'],
-          region: config.crowi['aws:region'],
-          bucket: config.crowi['aws:bucket']
-        };
+      var config = crowi.getConfig();
+      return {
+        accessKeyId: config.crowi['aws:accessKeyId'],
+        secretAccessKey: config.crowi['aws:secretAccessKey'],
+        region: config.crowi['aws:region'],
+        bucket: config.crowi['aws:bucket']
       };
       };
+    };
 
 
   function S3Factory() {
   function S3Factory() {
     const awsConfig = getAwsConfig();
     const awsConfig = getAwsConfig();
@@ -88,7 +88,7 @@ module.exports = function(crowi) {
     return url;
     return url;
   };
   };
 
 
-  lib.findDeliveryFile = function (fileId, filePath) {
+  lib.findDeliveryFile = function(fileId, filePath) {
     var cacheFile = lib.createCacheFileName(fileId);
     var cacheFile = lib.createCacheFileName(fileId);
 
 
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
@@ -102,7 +102,7 @@ module.exports = function(crowi) {
       var fileStream = fs.createWriteStream(cacheFile);
       var fileStream = fs.createWriteStream(cacheFile);
       var fileUrl = lib.generateUrl(filePath);
       var fileUrl = lib.generateUrl(filePath);
       debug('Load attachement file into local cache file', fileUrl, cacheFile);
       debug('Load attachement file into local cache file', fileUrl, cacheFile);
-      var request = loader.get(fileUrl, function(response) {
+      loader.get(fileUrl, function(response) {
         response.pipe(fileStream, { end: false });
         response.pipe(fileStream, { end: false });
         response.on('end', () => {
         response.on('end', () => {
           fileStream.end();
           fileStream.end();
@@ -130,7 +130,7 @@ module.exports = function(crowi) {
       debug('Failed to delete cache file (file may not exists).', err);
       debug('Failed to delete cache file (file may not exists).', err);
       // through
       // through
     });
     });
-  }
+  };
 
 
   // private
   // private
   lib.createCacheFileName = function(fileId) {
   lib.createCacheFileName = function(fileId) {
@@ -151,7 +151,8 @@ module.exports = function(crowi) {
         debug('Cache file found but the size is 0');
         debug('Cache file found but the size is 0');
         return true;
         return true;
       }
       }
-    } catch (e) {
+    }
+    catch (e) {
       // no such file or directory
       // no such file or directory
       debug('Stats error', e);
       debug('Stats error', e);
       return true;
       return true;

+ 1 - 1
local_modules/crowi-fileupload-local/index.js

@@ -54,7 +54,7 @@ module.exports = function(crowi) {
     return path.posix.join('/uploads', filePath);
     return path.posix.join('/uploads', filePath);
   };
   };
 
 
-  lib.findDeliveryFile = function (fileId, filePath) {
+  lib.findDeliveryFile = function(fileId, filePath) {
     return Promise.resolve(lib.generateUrl(filePath));
     return Promise.resolve(lib.generateUrl(filePath));
   };
   };
 
 

+ 67 - 63
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.0.13-RC",
+  "version": "3.1.0-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -31,6 +31,8 @@
     "clean:report": "rimraf -- report",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "clean": "npm-run-all -p clean:*",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
+    "lint:fix": "eslint . --fix",
+    "lint": "eslint .",
     "mkdirp": "mkdirp",
     "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "prebuild:dev": "env-cmd config/env.dev.js npm run plugin:def",
     "prebuild:dev": "env-cmd config/env.dev.js npm run plugin:def",
@@ -39,6 +41,7 @@
     "postserver:prod:container": "echo ---------------------------------------- && echo [WARNING] && echo   'server:prod:container' is deprecated. && echo   Please use 'sever:prod' && echo ----------------------------------------",
     "postserver:prod:container": "echo ---------------------------------------- && echo [WARNING] && echo   'server:prod:container' is deprecated. && echo   Please use 'sever:prod' && echo ----------------------------------------",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn app.js",
     "server:prod:container": "npm run server:prod",
     "server:prod:container": "npm run server:prod",
+    "server:prod:ci": "npm run server:prod -- --ci",
     "server:prod": "env-cmd config/env.prod.js node app.js",
     "server:prod": "env-cmd config/env.prod.js node app.js",
     "server": "npm run server:dev",
     "server": "npm run server:dev",
     "start": "npm run server:prod",
     "start": "npm run server:prod",
@@ -47,39 +50,21 @@
     "webpack": "webpack"
     "webpack": "webpack"
   },
   },
   "dependencies": {
   "dependencies": {
-    "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
     "async": "^2.3.0",
-    "autoprefixer": "^8.2.0",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",
     "axios": "^0.18.0",
     "axios": "^0.18.0",
-    "babel-core": "^6.25.0",
-    "babel-loader": "^7.1.1",
-    "babel-plugin-lodash": "^3.3.2",
-    "babel-preset-env": "^1.6.0",
-    "babel-preset-react": "^6.24.1",
     "basic-auth-connect": "~1.0.0",
     "basic-auth-connect": "~1.0.0",
     "body-parser": "^1.18.2",
     "body-parser": "^1.18.2",
-    "bootstrap-sass": "~3.3.6",
-    "bootstrap-select": "^1.12.4",
-    "browser-bunyan": "^1.3.0",
     "bunyan": "^1.8.12",
     "bunyan": "^1.8.12",
-    "bunyan-debug": "^2.0.0",
     "bunyan-format": "^0.2.1",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^3.1.1",
     "check-node-version": "^3.1.1",
-    "codemirror": "^5.37.0",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^2.0.1",
     "connect-mongo": "^2.0.1",
     "connect-redis": "^3.3.0",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cookie-parser": "^1.4.3",
     "cross-env": "^5.0.5",
     "cross-env": "^5.0.5",
     "csrf": "~3.0.3",
     "csrf": "~3.0.3",
-    "css-loader": "^0.28.0",
-    "csv-to-markdown-table": "^0.4.0",
-    "date-fns": "^1.29.0",
-    "diff": "^3.3.0",
-    "diff2html": "^2.3.3",
-    "eazy-logger": "^3.0.2",
-    "elasticsearch": "^14.0.0",
+    "elasticsearch": "^15.0.0",
     "entities": "^1.1.1",
     "entities": "^1.1.1",
     "env-cmd": "^8.0.1",
     "env-cmd": "^8.0.1",
     "escape-string-regexp": "^1.0.5",
     "escape-string-regexp": "^1.0.5",
@@ -89,89 +74,108 @@
     "express-sanitizer": "^1.0.4",
     "express-sanitizer": "^1.0.4",
     "express-session": "~1.15.0",
     "express-session": "~1.15.0",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
-    "extract-text-webpack-plugin": "^3.0.2",
-    "file-loader": "^1.1.0",
-    "googleapis": "^29.0.0",
+    "googleapis": "^30.0.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-pluginkit": "^1.1.0",
     "growi-pluginkit": "^1.1.0",
     "i18next": "^11.1.1",
     "i18next": "^11.1.1",
     "i18next-express-middleware": "^1.1.1",
     "i18next-express-middleware": "^1.1.1",
     "i18next-node-fs-backend": "^1.0.0",
     "i18next-node-fs-backend": "^1.0.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
-    "jquery-slimscroll": "^1.3.8",
-    "jquery-ui": "^1.12.1",
-    "jquery.cookie": "~1.4.1",
-    "load-css-file": "^1.0.0",
-    "markdown-it": "^8.4.0",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.0.0",
-    "markdown-it-task-lists": "^2.1.0",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.3",
-    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "method-override": "^2.3.10",
     "method-override": "^2.3.10",
-    "metismenu": "^2.7.4",
     "mkdirp": "~0.5.1",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
     "module-alias": "^2.0.6",
     "mongoose": "^5.0.0",
     "mongoose": "^5.0.0",
     "mongoose-paginate": "^5.0.0",
     "mongoose-paginate": "^5.0.0",
     "mongoose-unique-validator": "^2.0.0",
     "mongoose-unique-validator": "^2.0.0",
     "multer": "~1.3.0",
     "multer": "~1.3.0",
-    "node-sass": "^4.5.0",
     "nodemailer": "^4.0.1",
     "nodemailer": "^4.0.1",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "normalize-path": "^3.0.0",
     "npm-run-all": "^4.1.2",
     "npm-run-all": "^4.1.2",
-    "optimize-js-plugin": "0.0.4",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-ldapauth": "^2.0.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
-    "plantuml-encoder": "^1.2.5",
-    "postcss-loader": "^2.1.3",
     "react": "^16.2.0",
     "react": "^16.2.0",
-    "react-bootstrap": "^0.32.1",
-    "react-bootstrap-typeahead": "^3.0.3",
-    "react-clipboard.js": "^1.1.3",
-    "react-codemirror2": "^5.0.0",
     "react-dom": "^16.2.0",
     "react-dom": "^16.2.0",
-    "react-dropzone": "^4.2.7",
-    "reveal.js": "^3.5.0",
     "rimraf": "^2.6.1",
     "rimraf": "^2.6.1",
-    "sass-loader": "^7.0.1",
-    "simple-load-script": "^1.0.2",
     "slack-node": "^0.1.8",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
     "socket.io": "^2.0.3",
-    "socket.io-client": "^2.0.3",
-    "style-loader": "^0.21.0",
+    "string-width": "^2.1.1",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
-    "throttle-debounce": "^1.0.1",
-    "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
-    "url-join": "^4.0.0",
-    "webpack": "3.11.0",
-    "webpack-bundle-analyzer": "^2.9.0",
-    "webpack-merge": "~4.1.0",
     "xss": "^0.3.5"
     "xss": "^0.3.5"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "assets-webpack-plugin": "~3.5.1",
+    "autoprefixer": "^8.2.0",
+    "babel-core": "^6.25.0",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-lodash": "^3.3.2",
+    "babel-preset-env": "^1.6.0",
+    "babel-preset-react": "^6.24.1",
+    "bootstrap-sass": "~3.3.6",
+    "bootstrap-select": "^1.12.4",
+    "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.23.6",
     "browser-sync": "^2.23.6",
+    "bunyan-debug": "^2.0.0",
     "chai": "^4.1.0",
     "chai": "^4.1.0",
     "cli": "~1.0.1",
     "cli": "~1.0.1",
-    "colors": "^1.1.2",
+    "codemirror": "^5.37.0",
+    "colors": "^1.2.5",
     "commander": "^2.11.0",
     "commander": "^2.11.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
+    "css-loader": "^0.28.0",
+    "csv-to-markdown-table": "^0.4.0",
+    "date-fns": "^1.29.0",
+    "diff": "^3.3.0",
+    "diff2html": "^2.3.3",
+    "eazy-logger": "^3.0.2",
     "eslint": "^4.19.1",
     "eslint": "^4.19.1",
     "eslint-plugin-react": "^7.7.0",
     "eslint-plugin-react": "^7.7.0",
+    "extract-text-webpack-plugin": "^3.0.2",
+    "file-loader": "^1.1.0",
+    "jquery-slimscroll": "^1.3.8",
+    "jquery-ui": "^1.12.1",
+    "jquery.cookie": "~1.4.1",
+    "load-css-file": "^1.0.0",
+    "markdown-it": "^8.4.0",
+    "markdown-it-emoji": "^1.4.0",
+    "markdown-it-footnote": "^3.0.1",
+    "markdown-it-mathjax": "^2.0.0",
+    "markdown-it-named-headers": "^0.0.4",
+    "markdown-it-plantuml": "^1.0.0",
+    "markdown-it-task-lists": "^2.1.0",
+    "markdown-it-toc-and-anchor-with-slugid": "^1.1.3",
+    "markdown-table": "^1.1.1",
+    "metismenu": "^2.7.4",
     "mocha": "^5.0.0",
     "mocha": "^5.0.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
     "node-dev": "^3.1.3",
+    "node-sass": "^4.5.0",
+    "normalize-path": "^3.0.0",
     "on-headers": "^1.0.1",
     "on-headers": "^1.0.1",
+    "optimize-js-plugin": "0.0.4",
+    "plantuml-encoder": "^1.2.5",
+    "postcss-loader": "^2.1.3",
+    "react-bootstrap": "^0.32.1",
+    "react-bootstrap-typeahead": "^3.0.3",
+    "react-clipboard.js": "^1.1.3",
+    "react-codemirror2": "^5.0.0",
+    "react-dropzone": "^4.2.7",
+    "reveal.js": "^3.5.0",
+    "sass-loader": "^7.0.1",
+    "simple-load-script": "^1.0.2",
     "sinon": "^5.0.2",
     "sinon": "^5.0.2",
     "sinon-chai": "^3.0.0",
     "sinon-chai": "^3.0.0",
-    "webpack-dll-bundles-plugin": "^1.0.0-beta.5"
+    "socket.io-client": "^2.0.3",
+    "style-loader": "^0.21.0",
+    "throttle-debounce": "^1.0.1",
+    "toastr": "^2.1.2",
+    "url-join": "^4.0.0",
+    "webpack": "3.11.0",
+    "webpack-bundle-analyzer": "^2.9.0",
+    "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
+    "webpack-merge": "~4.1.0"
   },
   },
   "_moduleAliases": {
   "_moduleAliases": {
     "@root": ".",
     "@root": ".",
@@ -179,9 +183,9 @@
     "debug": "lib/service/logger/alias-for-debug"
     "debug": "lib/service/logger/alias-for-debug"
   },
   },
   "engines": {
   "engines": {
-    "node": ">=6.11 <9",
-    "npm": ">=4",
-    "yarn": "^1.3.1"
+    "node": ">=8.11.1 <9",
+    "npm": ">=5.6.0 <6",
+    "yarn": "^1.5.1"
   },
   },
   "config": {
   "config": {
     "blanket": {
     "blanket": {

+ 41 - 6
resource/js/app.js

@@ -10,6 +10,7 @@ import SearchPage       from './components/SearchPage';
 import PageEditor       from './components/PageEditor';
 import PageEditor       from './components/PageEditor';
 import OptionsSelector  from './components/PageEditor/OptionsSelector';
 import OptionsSelector  from './components/PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
+import GrantSelector, { UserGroup, PageGrant } from './components/PageEditor/GrantSelector';
 import Page             from './components/Page';
 import Page             from './components/Page';
 import PageListSearch   from './components/PageListSearch';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import PageHistory      from './components/PageHistory';
@@ -22,7 +23,6 @@ import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
 import BookmarkButton   from './components/BookmarkButton';
 import NewPageNameInputter from './components/NewPageNameInputter';
 import NewPageNameInputter from './components/NewPageNameInputter';
-import SearchTypeahead  from './components/SearchTypeahead';
 
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -30,7 +30,6 @@ import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 
 
 import * as entities from 'entities';
 import * as entities from 'entities';
 
 
-
 if (!window) {
 if (!window) {
   window = {};
   window = {};
 }
 }
@@ -42,6 +41,7 @@ let pageRevisionCreatedAt = null;
 let pagePath;
 let pagePath;
 let pageContent = '';
 let pageContent = '';
 let markdown = '';
 let markdown = '';
+let pageGrant = null;
 if (mainContent !== null) {
 if (mainContent !== null) {
   pageId = mainContent.getAttribute('data-page-id');
   pageId = mainContent.getAttribute('data-page-id');
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
@@ -159,7 +159,7 @@ if (pageEditorElem) {
     if (componentInstances.page != null) {
     if (componentInstances.page != null) {
       componentInstances.page.setMarkdown(page.revision.body);
       componentInstances.page.setMarkdown(page.revision.body);
     }
     }
-  }
+  };
 
 
   pageEditor = ReactDOM.render(
   pageEditor = ReactDOM.render(
     <PageEditor crowi={crowi} crowiRenderer={crowiRenderer}
     <PageEditor crowi={crowi} crowiRenderer={crowiRenderer}
@@ -189,6 +189,41 @@ if (pageEditorOptionsSelectorElem) {
     pageEditorOptionsSelectorElem
     pageEditorOptionsSelectorElem
   );
   );
 }
 }
+// render GrantSelector
+const userRelatedGroupsElem = document.getElementById('user-related-group-data');
+const pageEditorGrantSelectorElem = document.getElementById('page-grant-selector');
+const pageGrantElem = document.getElementById('page-grant');
+const pageGrantGroupElem = document.getElementById('grant-group');
+function updatePageGrantElems(newPageGrant) {
+  pageGrantElem.value = newPageGrant.grant;
+  pageGrantGroupElem.value = newPageGrant.grantGroup.userGroupId || '';
+}
+if (pageEditorGrantSelectorElem) {
+  let userRelatedGroups;
+  if (userRelatedGroupsElem != null) {
+    let userRelatedGroupsJSONString = userRelatedGroupsElem.textContent;
+    if (userRelatedGroupsJSONString != null && userRelatedGroupsJSONString.length > 0) {
+      userRelatedGroups = JSON.parse(userRelatedGroupsJSONString || '{}', (value) => {
+        return new UserGroup(value);
+      });
+    }
+  }
+  pageGrant = new PageGrant();
+  pageGrant.grant = document.getElementById('page-grant').value;
+  const grantGroupData = JSON.parse(document.getElementById('grant-group').textContent || '{}');
+  if (grantGroupData != null) {
+    const grantGroup = new UserGroup();
+    grantGroup.userGroupId = grantGroupData.id;
+    grantGroup.userGroup = grantGroupData;
+    pageGrant.grantGroup = grantGroup;
+  }
+  ReactDOM.render(
+    <GrantSelector crowi={crowi}
+      userRelatedGroups={userRelatedGroups} pageGrant={pageGrant}
+      onChange={updatePageGrantElems} />,
+    pageEditorGrantSelectorElem
+  );
+}
 
 
 // render for admin
 // render for admin
 const customCssEditorElem = document.getElementById('custom-css-editor');
 const customCssEditorElem = document.getElementById('custom-css-editor');
@@ -199,7 +234,7 @@ if (customCssEditorElem != null) {
   ReactDOM.render(
   ReactDOM.render(
     <CustomCssEditor inputElem={customCssInputElem} />,
     <CustomCssEditor inputElem={customCssInputElem} />,
     customCssEditorElem
     customCssEditorElem
-  )
+  );
 }
 }
 const customScriptEditorElem = document.getElementById('custom-script-editor');
 const customScriptEditorElem = document.getElementById('custom-script-editor');
 if (customScriptEditorElem != null) {
 if (customScriptEditorElem != null) {
@@ -209,7 +244,7 @@ if (customScriptEditorElem != null) {
   ReactDOM.render(
   ReactDOM.render(
     <CustomScriptEditor inputElem={customScriptInputElem} />,
     <CustomScriptEditor inputElem={customScriptInputElem} />,
     customScriptEditorElem
     customScriptEditorElem
-  )
+  );
 }
 }
 const customHeaderEditorElem = document.getElementById('custom-header-editor');
 const customHeaderEditorElem = document.getElementById('custom-header-editor');
 if (customHeaderEditorElem != null) {
 if (customHeaderEditorElem != null) {
@@ -219,7 +254,7 @@ if (customHeaderEditorElem != null) {
   ReactDOM.render(
   ReactDOM.render(
     <CustomHeaderEditor inputElem={customHeaderInputElem} />,
     <CustomHeaderEditor inputElem={customHeaderInputElem} />,
     customHeaderEditorElem
     customHeaderEditorElem
-  )
+  );
 }
 }
 
 
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)

+ 2 - 2
resource/js/components/Admin/CustomCssEditor.js

@@ -35,7 +35,7 @@ export default class CustomCssEditor extends React.Component {
           autoRefresh: true,
           autoRefresh: true,
           matchBrackets: true,
           matchBrackets: true,
           autoCloseBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {"Ctrl-Space": "autocomplete"},
+          extraKeys: {'Ctrl-Space': 'autocomplete'},
         }}
         }}
         editorDidMount={(editor, next) => {
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           // resizable with jquery.ui
@@ -49,7 +49,7 @@ export default class CustomCssEditor extends React.Component {
           this.props.inputElem.value = value;
           this.props.inputElem.value = value;
         }}
         }}
       />
       />
-    )
+    );
   }
   }
 
 
 }
 }

+ 2 - 2
resource/js/components/Admin/CustomHeaderEditor.js

@@ -33,7 +33,7 @@ export default class CustomHeaderEditor extends React.Component {
           autoRefresh: true,
           autoRefresh: true,
           matchBrackets: true,
           matchBrackets: true,
           autoCloseBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {"Ctrl-Space": "autocomplete"},
+          extraKeys: {'Ctrl-Space': 'autocomplete'},
         }}
         }}
         editorDidMount={(editor, next) => {
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           // resizable with jquery.ui
@@ -47,7 +47,7 @@ export default class CustomHeaderEditor extends React.Component {
           this.props.inputElem.value = value;
           this.props.inputElem.value = value;
         }}
         }}
       />
       />
-    )
+    );
   }
   }
 
 
 }
 }

+ 2 - 2
resource/js/components/Admin/CustomScriptEditor.js

@@ -35,7 +35,7 @@ export default class CustomScriptEditor extends React.Component {
           autoRefresh: true,
           autoRefresh: true,
           matchBrackets: true,
           matchBrackets: true,
           autoCloseBrackets: true,
           autoCloseBrackets: true,
-          extraKeys: {"Ctrl-Space": "autocomplete"},
+          extraKeys: {'Ctrl-Space': 'autocomplete'},
         }}
         }}
         editorDidMount={(editor, next) => {
         editorDidMount={(editor, next) => {
           // resizable with jquery.ui
           // resizable with jquery.ui
@@ -49,7 +49,7 @@ export default class CustomScriptEditor extends React.Component {
           this.props.inputElem.value = value;
           this.props.inputElem.value = value;
         }}
         }}
       />
       />
-    )
+    );
   }
   }
 
 
 }
 }

+ 2 - 1
resource/js/components/BookmarkButton.js

@@ -38,7 +38,8 @@ export default class BookmarkButton extends React.Component {
       .then(res => {
       .then(res => {
         this.markBookmarked();
         this.markBookmarked();
       });
       });
-    } else {
+    }
+    else {
       this.props.crowi.apiPost('/bookmarks.remove', {page_id: pageId})
       this.props.crowi.apiPost('/bookmarks.remove', {page_id: pageId})
       .then(res => {
       .then(res => {
         this.markUnBookmarked();
         this.markUnBookmarked();

+ 2 - 2
resource/js/components/CopyButton.js

@@ -21,9 +21,9 @@ export default class CopyButton extends React.Component {
   render() {
   render() {
     const containerStyle = {
     const containerStyle = {
       lineHeight: 0
       lineHeight: 0
-    }
+    };
     const style = Object.assign({
     const style = Object.assign({
-      padding: "0 2px",
+      padding: '0 2px',
       verticalAlign: 'text-top',
       verticalAlign: 'text-top',
     }, this.props.buttonStyle);
     }, this.props.buttonStyle);
 
 

+ 2 - 3
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -1,5 +1,4 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
 import FormGroup from 'react-bootstrap/es/FormGroup';
 import FormGroup from 'react-bootstrap/es/FormGroup';
 import Button from 'react-bootstrap/es/Button';
 import Button from 'react-bootstrap/es/Button';
@@ -47,8 +46,8 @@ export default class SearchForm extends React.Component {
 
 
   render() {
   render() {
     const emptyLabel = (this.state.searchError !== null)
     const emptyLabel = (this.state.searchError !== null)
-        ? 'Error on searching.'
-        : 'No matches found on title... Hit [Enter] key so that search on contents.';
+      ? 'Error on searching.'
+      : 'No matches found on title... Hit [Enter] key so that search on contents.';
 
 
     return (
     return (
       <form
       <form

+ 1 - 7
resource/js/components/NewPageNameInputter.js

@@ -1,12 +1,6 @@
 import React from 'react';
 import React from 'react';
-import { FormGroup } from 'react-bootstrap/es/FormGroup';
-import { Button } from 'react-bootstrap/es/Button';
-import { InputGroup } from 'react-bootstrap/es/InputGroup';
-
-import UserPicture from './User/UserPicture';
-import PageListMeta from './PageList/PageListMeta';
-import PagePath from './PageList/PagePath';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+
 import SearchTypeahead from './SearchTypeahead';
 import SearchTypeahead from './SearchTypeahead';
 
 
 export default class NewPageNameInputter extends React.Component {
 export default class NewPageNameInputter extends React.Component {

+ 6 - 4
resource/js/components/Page.js

@@ -60,7 +60,7 @@ export default class Page extends React.Component {
         return;
         return;
       }
       }
       const k = keyword
       const k = keyword
-            .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
+            .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
             .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
             .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
       const keywordExp = new RegExp(`(${k}(?!(.*?")))`, 'ig');
       const keywordExp = new RegExp(`(${k}(?!(.*?")))`, 'ig');
       returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
       returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
@@ -109,15 +109,17 @@ export default class Page extends React.Component {
 
 
   render() {
   render() {
     const config = this.props.crowi.getConfig();
     const config = this.props.crowi.getConfig();
+    const isMobile = this.props.crowi.isMobile;
     const isMathJaxEnabled = !!config.env.MATHJAX;
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
 
-    return (
-      <RevisionBody html={this.state.html}
+    return <div className={isMobile ? 'page-mobile' : ''}>
+      <RevisionBody
+          html={this.state.html}
           inputRef={el => this.revisionBodyElement = el}
           inputRef={el => this.revisionBodyElement = el}
           isMathJaxEnabled={isMathJaxEnabled}
           isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={true}
           renderMathJaxOnInit={true}
       />
       />
-    )
+    </div>;
   }
   }
 }
 }
 
 

+ 2 - 2
resource/js/components/Page/RevisionBody.js

@@ -35,7 +35,7 @@ export default class RevisionBody extends React.Component {
 
 
   renderMathJax() {
   renderMathJax() {
     const MathJax = window.MathJax;
     const MathJax = window.MathJax;
-    MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.element]);
+    MathJax.Hub.Queue(['Typeset', MathJax.Hub, this.element]);
   }
   }
 
 
   generateInnerHtml(html) {
   generateInnerHtml(html) {
@@ -53,7 +53,7 @@ export default class RevisionBody extends React.Component {
         }}
         }}
         className="wiki" dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}>
         className="wiki" dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}>
       </div>
       </div>
-    )
+    );
   }
   }
 }
 }
 
 

+ 8 - 8
resource/js/components/Page/RevisionPath.js

@@ -60,22 +60,22 @@ export default class RevisionPath extends React.Component {
       return <span>/</span>;
       return <span>/</span>;
     }
     }
     else {
     else {
-      return <span></span>
+      return <span></span>;
     }
     }
   }
   }
 
 
   render() {
   render() {
     // define styles
     // define styles
     const rootStyle = {
     const rootStyle = {
-      marginRight: "0.2em",
-    }
+      marginRight: '0.2em',
+    };
     const separatorStyle = {
     const separatorStyle = {
-      marginLeft: "0.2em",
-      marginRight: "0.2em",
-    }
+      marginLeft: '0.2em',
+      marginRight: '0.2em',
+    };
     const editButtonStyle = {
     const editButtonStyle = {
-      marginLeft: "0.5em",
-      padding: "0 2px",
+      marginLeft: '0.5em',
+      padding: '0 2px',
     };
     };
 
 
     const pageLength = this.state.pages.length;
     const pageLength = this.state.pages.length;

+ 4 - 4
resource/js/components/Page/RevisionUrl.js

@@ -7,12 +7,12 @@ export default class RevisionUrl extends React.Component {
 
 
   render() {
   render() {
     const buttonStyle = {
     const buttonStyle = {
-      fontSize: "1em"
-    }
+      fontSize: '1em'
+    };
 
 
     const url = (this.props.pageId == null)
     const url = (this.props.pageId == null)
-        ? decodeURIComponent(location.href)
-        : `${location.origin}/${this.props.pageId}`;
+      ? decodeURIComponent(location.href)
+      : `${location.origin}/${this.props.pageId}`;
     const copiedText = this.props.pagePath + '\n' + url;
     const copiedText = this.props.pagePath + '\n' + url;
 
 
     return (
     return (

+ 1 - 1
resource/js/components/PageAttachment/DeleteAttachmentModal.js

@@ -57,7 +57,7 @@ export default class DeleteAttachmentModal extends React.Component {
 
 
     let deletingIndicator = '';
     let deletingIndicator = '';
     if (this.props.deleting) {
     if (this.props.deleting) {
-      deletingIndicator = <div class="speeding-wheel-sm"></div>;
+      deletingIndicator = <div className="speeding-wheel-sm"></div>;
     }
     }
     if (this.props.deleteError) {
     if (this.props.deleteError) {
       deletingIndicator = <span>{this.props.deleteError}</span>;
       deletingIndicator = <span>{this.props.deleteError}</span>;

+ 1 - 1
resource/js/components/PageAttachment/PageAttachmentList.js

@@ -12,7 +12,7 @@ export default class PageAttachmentList extends React.Component {
     const attachmentList = this.props.attachments.map((attachment, idx) => {
     const attachmentList = this.props.attachments.map((attachment, idx) => {
       return (
       return (
         <Attachment
         <Attachment
-          key={"page:attachment:" + attachment._id}
+          key={'page:attachment:' + attachment._id}
           attachment={attachment}
           attachment={attachment}
           inUse={this.props.inUse[attachment._id] || false}
           inUse={this.props.inUse[attachment._id] || false}
           onAttachmentDeleteClicked={this.props.onAttachmentDeleteClicked}
           onAttachmentDeleteClicked={this.props.onAttachmentDeleteClicked}

+ 1 - 1
resource/js/components/PageComment/Comment.js

@@ -56,7 +56,7 @@ export default class Comment extends React.Component {
   }
   }
 
 
   getRootClassName() {
   getRootClassName() {
-    return "page-comment "
+    return 'page-comment '
         + (this.isCurrentUserEqualsToAuthor() ? 'page-comment-me ' : '');
         + (this.isCurrentUserEqualsToAuthor() ? 'page-comment-me ' : '');
   }
   }
 
 

+ 2 - 2
resource/js/components/PageComment/DeleteCommentModal.js

@@ -14,7 +14,7 @@ export default class DeleteCommentModal extends React.Component {
   /*
   /*
    * the threshold for omitting body
    * the threshold for omitting body
    */
    */
-  static get OMIT_BODY_THRES() { return 400 };
+  static get OMIT_BODY_THRES() { return 400 }
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -25,7 +25,7 @@ export default class DeleteCommentModal extends React.Component {
 
 
   render() {
   render() {
     if (this.props.comment === undefined) {
     if (this.props.comment === undefined) {
-      return <div></div>
+      return <div></div>;
     }
     }
 
 
     const comment = this.props.comment;
     const comment = this.props.comment;

+ 3 - 2
resource/js/components/PageCommentFormBehavior.js

@@ -40,7 +40,8 @@ export default class PageCommentFormBehavior extends React.Component {
           $('#comment-form-comment').val('');
           $('#comment-form-comment').val('');
           $('#comment-form-is-markdown').prop('checked', false);
           $('#comment-form-is-markdown').prop('checked', false);
           $('#comment-form-message').text('');
           $('#comment-form-message').text('');
-        } else {
+        }
+        else {
           $('#comment-form-message').text(data.error);
           $('#comment-form-message').text(data.error);
         }
         }
       }).fail(function(data) {
       }).fail(function(data) {
@@ -55,7 +56,7 @@ export default class PageCommentFormBehavior extends React.Component {
 
 
   render() {
   render() {
     // render nothing
     // render nothing
-    return <div></div>
+    return <div></div>;
   }
   }
 }
 }
 
 

+ 1 - 0
resource/js/components/PageEditor.js

@@ -368,6 +368,7 @@ export default class PageEditor extends React.Component {
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
           <Editor ref="editor" value={this.state.markdown}
           <Editor ref="editor" value={this.state.markdown}
             editorOptions={this.state.editorOptions}
             editorOptions={this.state.editorOptions}
+            isMobile={this.props.crowi.isMobile}
             isUploadable={this.state.isUploadable}
             isUploadable={this.state.isUploadable}
             isUploadableFile={this.state.isUploadableFile}
             isUploadableFile={this.state.isUploadableFile}
             emojiStrategy={emojiStrategy}
             emojiStrategy={emojiStrategy}

+ 104 - 0
resource/js/components/PageEditor/AbstractEditor.js

@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class AbstractEditor extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.forceToFocus = this.forceToFocus.bind(this);
+    this.setCaretLine = this.setCaretLine.bind(this);
+    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
+
+    this.getStrFromBol = this.getStrFromBol.bind(this);
+    this.getStrToEol = this.getStrToEol.bind(this);
+    this.insertText = this.insertText.bind(this);
+    this.insertLinebreak = this.insertLinebreak.bind(this);
+
+    this.dispatchSave = this.dispatchSave.bind(this);
+  }
+
+  forceToFocus() {
+  }
+
+  /**
+   * set caret position of codemirror
+   * @param {string} number
+   */
+  setCaretLine(line) {
+  }
+
+  /**
+   * scroll
+   * @param {number} line
+   */
+  setScrollTopByLine(line) {
+  }
+
+  /**
+   * return strings from BOL(beginning of line) to current position
+   */
+  getStrFromBol() {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * return strings from current position to EOL(end of line)
+   */
+  getStrToEol() {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * replace Beggining Of Line to current position with param 'text'
+   * @param {string} text
+   */
+  replaceBolToCurrentPos(text) {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * insert text
+   * @param {string} text
+   */
+  insertText(text) {
+  }
+
+  /**
+   * insert line break to the current position
+   */
+  insertLinebreak() {
+    this.insertText('\n');
+  }
+
+  /**
+   * dispatch onSave event
+   */
+  dispatchSave() {
+    if (this.props.onSave != null) {
+      this.props.onSave();
+    }
+  }
+
+  /**
+   * dispatch onPasteFiles event
+   * @param {object} event
+   */
+  dispatchPasteFiles(event) {
+    if (this.props.onPasteFiles != null) {
+      this.props.onPasteFiles(event);
+    }
+  }
+}
+
+AbstractEditor.propTypes = {
+  value: PropTypes.string,
+  editorOptions: PropTypes.object,
+  onChange: PropTypes.func,
+  onScroll: PropTypes.func,
+  onScrollCursorIntoView: PropTypes.func,
+  onSave: PropTypes.func,
+  onPasteFiles: PropTypes.func,
+  onDragEnter: PropTypes.func,
+};
+

+ 423 - 0
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -0,0 +1,423 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AbstractEditor from './AbstractEditor';
+
+import urljoin from 'url-join';
+const loadScript = require('simple-load-script');
+const loadCssSync = require('load-css-file');
+
+import * as codemirror from 'codemirror';
+
+import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
+require('codemirror/addon/display/autorefresh');
+require('codemirror/addon/edit/matchbrackets');
+require('codemirror/addon/edit/matchtags');
+require('codemirror/addon/edit/closetag');
+require('codemirror/addon/edit/continuelist');
+require('codemirror/addon/hint/show-hint');
+require('codemirror/addon/hint/show-hint.css');
+require('codemirror/addon/search/searchcursor');
+require('codemirror/addon/search/match-highlighter');
+require('codemirror/addon/selection/active-line');
+require('codemirror/addon/scroll/annotatescrollbar');
+require('codemirror/addon/fold/foldcode');
+require('codemirror/addon/fold/foldgutter');
+require('codemirror/addon/fold/foldgutter.css');
+require('codemirror/addon/fold/markdown-fold');
+require('codemirror/addon/fold/brace-fold');
+require('codemirror/mode/gfm/gfm');
+
+import pasteHelper from './PasteHelper';
+import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
+
+import InterceptorManager from '../../../../lib/util/interceptor-manager';
+
+import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
+import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+
+export default class CodeMirrorEditor extends AbstractEditor {
+
+  constructor(props) {
+    super(props);
+    this.logger = require('@alias/logger')('growi:PageEditor:CodeMirrorEditor');
+
+    this.state = {
+      value: this.props.value,
+      isEnabledEmojiAutoComplete: false,
+      isLoadingKeymap: false,
+    };
+
+    this.init();
+
+    this.getCodeMirror = this.getCodeMirror.bind(this);
+
+    this.getBol = this.getBol.bind(this);
+    this.getEol = this.getEol.bind(this);
+
+    this.loadTheme = this.loadTheme.bind(this);
+    this.loadKeymapMode = this.loadKeymapMode.bind(this);
+    this.setKeymapMode = this.setKeymapMode.bind(this);
+    this.handleEnterKey = this.handleEnterKey.bind(this);
+
+    this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
+    this.pasteHandler = this.pasteHandler.bind(this);
+
+    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
+  }
+
+  init() {
+    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.37.0';
+
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptors([
+      new PreventMarkdownListInterceptor(),
+      new MarkdownTableInterceptor(),
+    ]);
+
+    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
+    this.loadedKeymapSet = new Set();
+  }
+
+  componentWillMount() {
+    if (this.props.emojiStrategy != null) {
+      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
+      this.setState({isEnabledEmojiAutoComplete: true});
+    }
+  }
+
+  componentDidMount() {
+    // initialize caret line
+    this.setCaretLine(0);
+    // set save handler
+    codemirror.commands.save = this.dispatchSave;
+
+    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
+    window.CodeMirror = require('codemirror');
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // load theme
+    const theme = nextProps.editorOptions.theme;
+    this.loadTheme(theme);
+
+    // set keymap
+    const keymapMode = nextProps.editorOptions.keymapMode;
+    this.setKeymapMode(keymapMode);
+  }
+
+  getCodeMirror() {
+    return this.refs.cm.editor;
+  }
+
+  /**
+   * @inheritDoc
+   */
+  forceToFocus() {
+    const editor = this.getCodeMirror();
+    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
+    const intervalId = setInterval(() => {
+      this.getCodeMirror().focus();
+      if (editor.hasFocus()) {
+        clearInterval(intervalId);
+        // refresh
+        editor.refresh();
+      }
+    }, 100);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setCaretLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    const editor = this.getCodeMirror();
+    const linePosition = Math.max(0, line);
+
+    editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
+    this.setScrollTopByLine(linePosition);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setScrollTopByLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    const editor = this.getCodeMirror();
+    // get top position of the line
+    var top = editor.charCoords({line, ch: 0}, 'local').top;
+    editor.scrollTo(null, top);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrFromBol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(this.getBol(), curPos);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrToEol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(curPos, this.getEol());
+  }
+
+  /**
+   * @inheritDoc
+   */
+  replaceBolToCurrentPos(text) {
+    const editor = this.getCodeMirror();
+    editor.getDoc().replaceRange(text, this.getBol(), editor.getCursor());
+  }
+
+  /**
+   * @inheritDoc
+   */
+  insertText(text) {
+    const editor = this.getCodeMirror();
+    editor.getDoc().replaceSelection(text);
+  }
+
+  /**
+   * return the postion of the BOL(beginning of line)
+   */
+  getBol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return { line: curPos.line, ch: 0 };
+  }
+
+  /**
+   * return the postion of the EOL(end of line)
+   */
+  getEol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    const lineLength = editor.getDoc().getLine(curPos.line).length;
+    return { line: curPos.line, ch: lineLength };
+  }
+
+  loadCss(source) {
+    return new Promise((resolve) => {
+      loadCssSync(source);
+      resolve();
+    });
+  }
+
+  /**
+   * load Theme
+   * @see https://codemirror.net/doc/manual.html#config
+   *
+   * @param {string} theme
+   */
+  loadTheme(theme) {
+    if (!this.loadedThemeSet.has(theme)) {
+      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
+
+      // update Set
+      this.loadedThemeSet.add(theme);
+    }
+  }
+
+  /**
+   * load assets for Key Maps
+   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  loadKeymapMode(keymapMode) {
+    const loadCss = this.loadCss;
+    let scriptList = [];
+    let cssList = [];
+
+    // add dependencies
+    if (this.loadedKeymapSet.size == 0) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
+      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
+    }
+    // load keymap
+    if (!this.loadedKeymapSet.has(keymapMode)) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
+      // update Set
+      this.loadedKeymapSet.add(keymapMode);
+    }
+
+    // set loading state
+    this.setState({ isLoadingKeymap: true });
+
+    return Promise.all(scriptList.concat(cssList))
+      .then(() => {
+        this.setState({ isLoadingKeymap: false });
+      });
+  }
+
+  /**
+   * set Key Maps
+   * @see https://codemirror.net/doc/manual.html#keymaps
+   *
+   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  setKeymapMode(keymapMode) {
+    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
+      // reset
+      this.getCodeMirror().setOption('keyMap', 'default');
+      return;
+    }
+
+    this.loadKeymapMode(keymapMode)
+      .then(() => {
+        this.getCodeMirror().setOption('keyMap', keymapMode);
+      });
+  }
+
+  /**
+   * handle ENTER key
+   */
+  handleEnterKey() {
+    var context = {
+      handlers: [],  // list of handlers which process enter key
+      editor: this,
+    };
+
+    const interceptorManager = this.interceptorManager;
+    interceptorManager.process('preHandleEnter', context)
+      .then(() => {
+        if (context.handlers.length == 0) {
+          codemirror.commands.newlineAndIndentContinueMarkdownList(this.getCodeMirror());
+        }
+      });
+  }
+
+  scrollCursorIntoViewHandler(editor, event) {
+    if (this.props.onScrollCursorIntoView != null) {
+      const line = editor.getCursor().line;
+      this.props.onScrollCursorIntoView(line);
+    }
+  }
+
+  /**
+   * CodeMirror paste event handler
+   * see: https://codemirror.net/doc/manual.html#events
+   * @param {any} editor An editor instance of CodeMirror
+   * @param {any} event
+   */
+  pasteHandler(editor, event) {
+    const types = event.clipboardData.types;
+
+    // text
+    if (types.includes('text/plain')) {
+      pasteHelper.pasteText(this, event);
+    }
+    // files
+    else if (types.includes('Files')) {
+      this.dispatchPasteFiles(event);
+    }
+  }
+
+  getOverlayStyle() {
+    return {
+      position: 'absolute',
+      zIndex: 4,  // forward than .CodeMirror-gutters
+      top: 0,
+      right: 0,
+      bottom: 0,
+      left: 0,
+    };
+  }
+
+  renderLoadingKeymapOverlay() {
+    const overlayStyle = this.getOverlayStyle();
+
+    return this.state.isLoadingKeymap
+      ? <div style={overlayStyle} className="loading-keymap overlay">
+          <span className="overlay-content">
+            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
+          </span>
+        </div>
+      : '';
+  }
+
+  render() {
+    const theme = this.props.editorOptions.theme || 'elegant';
+    const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
+    return <React.Fragment>
+      <ReactCodeMirror
+        ref="cm"
+        editorDidMount={(editor) => {
+          // add event handlers
+          editor.on('paste', this.pasteHandler);
+          editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
+        }}
+        value={this.state.value}
+        options={{
+          mode: 'gfm',
+          theme: theme,
+          styleActiveLine: styleActiveLine,
+          lineNumbers: true,
+          tabSize: 4,
+          indentUnit: 4,
+          lineWrapping: true,
+          autoRefresh: true,
+          autoCloseTags: true,
+          matchBrackets: true,
+          matchTags: {bothTags: true},
+          // folding
+          foldGutter: true,
+          gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+          // match-highlighter, matchesonscrollbar, annotatescrollbar options
+          highlightSelectionMatches: {annotateScrollbar: true},
+          // markdown mode options
+          highlightFormatting: true,
+          // continuelist, indentlist
+          extraKeys: {
+            'Enter': this.handleEnterKey,
+            'Tab': 'indentMore',
+            'Shift-Tab': 'indentLess',
+            'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
+          }
+        }}
+        onScroll={(editor, data) => {
+          if (this.props.onScroll != null) {
+            // add line data
+            const line = editor.lineAtHeight(data.top, 'local');
+            data.line = line;
+            this.props.onScroll(data);
+          }
+        }}
+        onChange={(editor, data, value) => {
+          if (this.props.onChange != null) {
+            this.props.onChange(value);
+          }
+
+          // Emoji AutoComplete
+          if (this.state.isEnabledEmojiAutoComplete) {
+            this.emojiAutoCompleteHelper.showHint(editor);
+          }
+        }}
+        onDragEnter={(editor, event) => {
+          if (this.props.onDragEnter != null) {
+            this.props.onDragEnter(event);
+          }
+        }}
+      />
+
+      { this.renderLoadingKeymapOverlay() }
+
+    </React.Fragment>;
+  }
+
+}
+
+CodeMirrorEditor.propTypes = Object.assign({
+  emojiStrategy: PropTypes.object,
+}, AbstractEditor.propTypes);
+

+ 77 - 348
resource/js/components/PageEditor/Editor.js

@@ -1,232 +1,74 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import urljoin from 'url-join';
-const loadScript = require('simple-load-script');
-const loadCssSync = require('load-css-file');
-
-import * as codemirror from 'codemirror';
-
-import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
-require('codemirror/addon/display/autorefresh');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/matchtags');
-require('codemirror/addon/edit/closetag');
-require('codemirror/addon/edit/continuelist');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/hint/show-hint.css');
-require('codemirror/addon/search/searchcursor');
-require('codemirror/addon/search/match-highlighter');
-require('codemirror/addon/selection/active-line');
-require('codemirror/addon/scroll/annotatescrollbar');
-require('codemirror/addon/fold/foldcode');
-require('codemirror/addon/fold/foldgutter');
-require('codemirror/addon/fold/foldgutter.css');
-require('codemirror/addon/fold/markdown-fold');
-require('codemirror/addon/fold/brace-fold');
-require('codemirror/mode/gfm/gfm');
-
+import AbstractEditor from './AbstractEditor';
+import CodeMirrorEditor from './CodeMirrorEditor';
+import TextAreaEditor from './TextAreaEditor';
 
 
 import Dropzone from 'react-dropzone';
 import Dropzone from 'react-dropzone';
 
 
 import pasteHelper from './PasteHelper';
 import pasteHelper from './PasteHelper';
-import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
-
-import InterceptorManager from '../../../../lib/util/interceptor-manager';
-
-import MarkdownListInterceptor from './MarkdownListInterceptor';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
 
 
-export default class Editor extends React.Component {
+export default class Editor extends AbstractEditor {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.37.0';
-
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new MarkdownListInterceptor(),
-      new MarkdownTableInterceptor(),
-    ]);
-
     this.state = {
     this.state = {
-      value: this.props.value,
       dropzoneActive: false,
       dropzoneActive: false,
-      isEnabledEmojiAutoComplete: false,
       isUploading: false,
       isUploading: false,
-      isLoadingKeymap: false,
     };
     };
 
 
-    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
-    this.loadedKeymapSet = new Set();
-
-    this.getCodeMirror = this.getCodeMirror.bind(this);
-    this.setCaretLine = this.setCaretLine.bind(this);
-    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
-    this.loadTheme = this.loadTheme.bind(this);
-    this.loadKeymapMode = this.loadKeymapMode.bind(this);
-    this.setKeymapMode = this.setKeymapMode.bind(this);
-    this.forceToFocus = this.forceToFocus.bind(this);
-    this.dispatchSave = this.dispatchSave.bind(this);
-    this.handleEnterKey = this.handleEnterKey.bind(this);
+    this.getEditorSubstance = this.getEditorSubstance.bind(this);
 
 
-    this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
-    this.onPaste = this.onPaste.bind(this);
+    this.pasteFilesHandler = this.pasteFilesHandler.bind(this);
 
 
-    this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
-    this.onDragLeave = this.onDragLeave.bind(this);
-    this.onDrop = this.onDrop.bind(this);
+    this.dragEnterHandler = this.dragEnterHandler.bind(this);
+    this.dragLeaveHandler = this.dragLeaveHandler.bind(this);
+    this.dropHandler = this.dropHandler.bind(this);
 
 
     this.getDropzoneAccept = this.getDropzoneAccept.bind(this);
     this.getDropzoneAccept = this.getDropzoneAccept.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
     this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
     this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
-
-    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
-  }
-
-  componentWillMount() {
-    if (this.props.emojiStrategy != null) {
-      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
-      this.setState({isEnabledEmojiAutoComplete: true});
-    }
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
     // initialize caret line
     // initialize caret line
     this.setCaretLine(0);
     this.setCaretLine(0);
-    // set save handler
-    codemirror.commands.save = this.dispatchSave;
-
-    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
-    window.CodeMirror = require('codemirror');
   }
   }
 
 
-  componentWillReceiveProps(nextProps) {
-    // load theme
-    const theme = nextProps.editorOptions.theme;
-    this.loadTheme(theme);
-
-    // set keymap
-    const keymapMode = nextProps.editorOptions.keymapMode;
-    this.setKeymapMode(keymapMode);
-  }
-
-  getCodeMirror() {
-    return this.refs.cm.editor;
-  }
-
-  loadCss(source) {
-    return new Promise((resolve) => {
-      loadCssSync(source);
-      resolve();
-    });
+  getEditorSubstance() {
+    return this.props.isMobile
+      ? this.refs.taEditor
+      : this.refs.cmEditor;
   }
   }
 
 
+  /**
+   * @inheritDoc
+   */
   forceToFocus() {
   forceToFocus() {
-    const editor = this.getCodeMirror();
-    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
-    const intervalId = setInterval(() => {
-      this.getCodeMirror().focus();
-      if (editor.hasFocus()) {
-        clearInterval(intervalId);
-      }
-    }, 100);
+    this.getEditorSubstance().forceToFocus();
   }
   }
 
 
   /**
   /**
-   * set caret position of codemirror
-   * @param {string} number
+   * @inheritDoc
    */
    */
   setCaretLine(line) {
   setCaretLine(line) {
-    if (isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    const linePosition = Math.max(0, line);
-
-    editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
-    this.setScrollTopByLine(linePosition);
+    this.getEditorSubstance().setCaretLine(line);
   }
   }
 
 
   /**
   /**
-   * scroll
-   * @param {number} line
+   * @inheritDoc
    */
    */
   setScrollTopByLine(line) {
   setScrollTopByLine(line) {
-    if (isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    // get top position of the line
-    var top = editor.charCoords({line, ch: 0}, 'local').top;
-    editor.scrollTo(null, top);
-  }
-
-  /**
-   * load Theme
-   * @see https://codemirror.net/doc/manual.html#config
-   *
-   * @param {string} theme
-   */
-  loadTheme(theme) {
-    if (!this.loadedThemeSet.has(theme)) {
-      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
-
-      // update Set
-      this.loadedThemeSet.add(theme);
-    }
-  }
-
-  /**
-   * load assets for Key Maps
-   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  loadKeymapMode(keymapMode) {
-    const loadCss = this.loadCss;
-    let scriptList = [];
-    let cssList = [];
-
-    // add dependencies
-    if (this.loadedKeymapSet.size == 0) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
-      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
-    }
-    // load keymap
-    if (!this.loadedKeymapSet.has(keymapMode)) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
-      // update Set
-      this.loadedKeymapSet.add(keymapMode);
-    }
-
-    // set loading state
-    this.setState({ isLoadingKeymap: true });
-
-    return Promise.all(scriptList.concat(cssList))
-      .then(() => {
-        this.setState({ isLoadingKeymap: false });
-      });
+    this.getEditorSubstance().setScrollTopByLine(line);
   }
   }
 
 
   /**
   /**
-   * set Key Maps
-   * @see https://codemirror.net/doc/manual.html#keymaps
-   *
-   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   * @inheritDoc
    */
    */
-  setKeymapMode(keymapMode) {
-    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
-      // reset
-      this.getCodeMirror().setOption('keyMap', 'default');
-      return;
-    }
-
-    this.loadKeymapMode(keymapMode)
-      .then(() => {
-        this.getCodeMirror().setOption('keyMap', keymapMode);
-      });
+  insertText(text) {
+    this.getEditorSubstance().insertText(text);
   }
   }
 
 
   /**
   /**
@@ -239,24 +81,6 @@ export default class Editor extends React.Component {
     });
     });
   }
   }
 
 
-  /**
-   * insert text
-   * @param {string} text
-   */
-  insertText(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceSelection(text);
-  }
-
-  /**
-   * dispatch onSave event
-   */
-  dispatchSave() {
-    if (this.props.onSave != null) {
-      this.props.onSave();
-    }
-  }
-
   /**
   /**
    * dispatch onUpload event
    * dispatch onUpload event
    */
    */
@@ -266,68 +90,26 @@ export default class Editor extends React.Component {
     }
     }
   }
   }
 
 
-  /**
-   * handle ENTER key
-   */
-  handleEnterKey() {
-
-    const editor = this.getCodeMirror();
-    var context = {
-      handlers: [],  // list of handlers which process enter key
-      editor: editor,
-    };
-
-    const interceptorManager = this.interceptorManager;
-    interceptorManager.process('preHandleEnter', context)
-      .then(() => {
-        if (context.handlers.length == 0) {
-          codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
-        }
-      });
-  }
-
-  onScrollCursorIntoView(editor, event) {
-    if (this.props.onScrollCursorIntoView != null) {
-      const line = editor.getCursor().line;
-      this.props.onScrollCursorIntoView(line);
-    }
-  }
-
-  /**
-   * CodeMirror paste event handler
-   * see: https://codemirror.net/doc/manual.html#events
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   */
-  onPaste(editor, event) {
-    const types = event.clipboardData.types;
+  pasteFilesHandler(event) {
+    const dropzone = this.refs.dropzone;
+    const items = event.clipboardData.items || event.clipboardData.files || [];
 
 
-    // text
-    if (types.includes('text/plain')) {
-      pasteHelper.pasteText(editor, event);
+    // abort if length is not 1
+    if (items.length != 1) {
+      return;
     }
     }
-    // files
-    else if (types.includes('Files')) {
-      const dropzone = this.refs.dropzone;
-      const items = event.clipboardData.items || event.clipboardData.files || [];
-
-      // abort if length is not 1
-      if (items.length != 1) {
-        return;
-      }
 
 
-      const file = items[0].getAsFile();
-      // check type and size
-      if (pasteHelper.fileAccepted(file, dropzone.props.accept) &&
-          pasteHelper.fileMatchSize(file, dropzone.props.maxSize, dropzone.props.minSize)) {
+    const file = items[0].getAsFile();
+    // check type and size
+    if (pasteHelper.fileAccepted(file, dropzone.props.accept) &&
+        pasteHelper.fileMatchSize(file, dropzone.props.maxSize, dropzone.props.minSize)) {
 
 
-        this.dispatchUpload(file);
-        this.setState({ isUploading: true });
-      }
+      this.dispatchUpload(file);
+      this.setState({ isUploading: true });
     }
     }
   }
   }
 
 
-  onDragEnterForCM(editor, event) {
+  dragEnterHandler(event) {
     const dataTransfer = event.dataTransfer;
     const dataTransfer = event.dataTransfer;
 
 
     // do nothing if contents is not files
     // do nothing if contents is not files
@@ -338,11 +120,11 @@ export default class Editor extends React.Component {
     this.setState({ dropzoneActive: true });
     this.setState({ dropzoneActive: true });
   }
   }
 
 
-  onDragLeave() {
+  dragLeaveHandler() {
     this.setState({ dropzoneActive: false });
     this.setState({ dropzoneActive: false });
   }
   }
 
 
-  onDrop(accepted, rejected) {
+  dropHandler(accepted, rejected) {
     // rejected
     // rejected
     if (accepted.length != 1) { // length should be 0 or 1 because `multiple={false}` is set
     if (accepted.length != 1) { // length should be 0 or 1 because `multiple={false}` is set
       this.setState({ dropzoneActive: false });
       this.setState({ dropzoneActive: false });
@@ -416,18 +198,6 @@ export default class Editor extends React.Component {
     );
     );
   }
   }
 
 
-  renderLoadingKeymapOverlay() {
-    const overlayStyle = this.getOverlayStyle();
-
-    return this.state.isLoadingKeymap
-      ? <div style={overlayStyle} className="loading-keymap overlay">
-          <span className="overlay-content">
-            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
-          </span>
-        </div>
-      : '';
-  }
-
   render() {
   render() {
     const flexContainer = {
     const flexContainer = {
       height: '100%',
       height: '100%',
@@ -435,83 +205,49 @@ export default class Editor extends React.Component {
       flexDirection: 'column',
       flexDirection: 'column',
     };
     };
 
 
-    const theme = this.props.editorOptions.theme || 'elegant';
-    const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
+    const isMobile = this.props.isMobile;
+
     return <React.Fragment>
     return <React.Fragment>
       <div style={flexContainer}>
       <div style={flexContainer}>
         <Dropzone
         <Dropzone
-          ref="dropzone"
-          disableClick
-          disablePreview={true}
-          accept={this.getDropzoneAccept()}
-          className={this.getDropzoneClassName()}
-          acceptClassName="dropzone-accepted"
-          rejectClassName="dropzone-rejected"
-          multiple={false}
-          onDragLeave={this.onDragLeave}
-          onDrop={this.onDrop}
-        >
+            ref="dropzone"
+            disableClick
+            disablePreview={true}
+            accept={this.getDropzoneAccept()}
+            className={this.getDropzoneClassName()}
+            acceptClassName="dropzone-accepted"
+            rejectClassName="dropzone-rejected"
+            multiple={false}
+            onDragLeave={this.dragLeaveHandler}
+            onDrop={this.dropHandler}
+          >
+
           { this.state.dropzoneActive && this.renderDropzoneOverlay() }
           { this.state.dropzoneActive && this.renderDropzoneOverlay() }
 
 
-          <ReactCodeMirror
-            ref="cm"
-            editorDidMount={(editor) => {
-              // add event handlers
-              editor.on('paste', this.onPaste);
-              editor.on('scrollCursorIntoView', this.onScrollCursorIntoView);
-            }}
-            value={this.state.value}
-            options={{
-              mode: 'gfm',
-              theme: theme,
-              styleActiveLine: styleActiveLine,
-              lineNumbers: true,
-              tabSize: 4,
-              indentUnit: 4,
-              lineWrapping: true,
-              autoRefresh: true,
-              autoCloseTags: true,
-              matchBrackets: true,
-              matchTags: {bothTags: true},
-              // folding
-              foldGutter: true,
-              gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
-              // match-highlighter, matchesonscrollbar, annotatescrollbar options
-              highlightSelectionMatches: {annotateScrollbar: true},
-              // markdown mode options
-              highlightFormatting: true,
-              // continuelist, indentlist
-              extraKeys: {
-                'Enter': this.handleEnterKey,
-                'Tab': 'indentMore',
-                'Shift-Tab': 'indentLess',
-                'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()); },
-              }
-            }}
-            onScroll={(editor, data) => {
-              if (this.props.onScroll != null) {
-                // add line data
-                const line = editor.lineAtHeight(data.top, 'local');
-                data.line = line;
-                this.props.onScroll(data);
-              }
-            }}
-            onChange={(editor, data, value) => {
-              if (this.props.onChange != null) {
-                this.props.onChange(value);
-              }
-
-              // Emoji AutoComplete
-              if (this.state.isEnabledEmojiAutoComplete) {
-                this.emojiAutoCompleteHelper.showHint(editor);
-              }
-            }}
-            onDragEnter={this.onDragEnterForCM}
-          />
+          {/* for PC */}
+          { !isMobile &&
+            <CodeMirrorEditor
+              ref="cmEditor"
+              onPasteFiles={this.pasteFilesHandler}
+              onDragEnter={this.dragEnterHandler}
+              {...this.props}
+            />
+          }
+
+          {/* for mobile */}
+          { isMobile &&
+            <TextAreaEditor
+              ref="taEditor"
+              onPasteFiles={this.pasteFilesHandler}
+              onDragEnter={this.dragEnterHandler}
+              {...this.props}
+            />
+          }
+
         </Dropzone>
         </Dropzone>
 
 
         <button type="button" className="btn btn-default btn-block btn-open-dropzone"
         <button type="button" className="btn btn-default btn-block btn-open-dropzone"
-          onClick={() => {this.refs.dropzone.open();}}>
+          onClick={() => {this.refs.dropzone.open()}}>
 
 
           <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
           <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
           Attach files by dragging &amp; dropping,&nbsp;
           Attach files by dragging &amp; dropping,&nbsp;
@@ -519,8 +255,6 @@ export default class Editor extends React.Component {
           or pasting from the clipboard.
           or pasting from the clipboard.
         </button>
         </button>
 
 
-        { this.renderLoadingKeymapOverlay() }
-
       </div>
       </div>
 
 
     </React.Fragment>;
     </React.Fragment>;
@@ -528,17 +262,12 @@ export default class Editor extends React.Component {
 
 
 }
 }
 
 
-Editor.propTypes = {
-  value: PropTypes.string,
-  options: PropTypes.object,
-  editorOptions: PropTypes.object,
+Editor.propTypes = Object.assign({
+  isMobile: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   emojiStrategy: PropTypes.object,
   emojiStrategy: PropTypes.object,
   onChange: PropTypes.func,
   onChange: PropTypes.func,
-  onScroll: PropTypes.func,
-  onScrollCursorIntoView: PropTypes.func,
-  onSave: PropTypes.func,
   onUpload: PropTypes.func,
   onUpload: PropTypes.func,
-};
+}, AbstractEditor.propTypes);
 
 

+ 8 - 8
resource/js/components/PageEditor/EmojiAutoCompleteHelper.js

@@ -90,34 +90,34 @@ class EmojiAutoCompleteHelper {
     const maxLength = 12;
     const maxLength = 12;
 
 
     let results1 = [], results2 = [], results3 = [], results4 = [];
     let results1 = [], results2 = [], results3 = [], results4 = [];
-    const countLen1 = () => { results1.length; }
-    const countLen2 = () => { countLen1() + results2.length; }
-    const countLen3 = () => { countLen2() + results3.length; }
-    const countLen4 = () => { countLen3() + results4.length; }
+    const countLen1 = () => { results1.length };
+    const countLen2 = () => { countLen1() + results2.length };
+    const countLen3 = () => { countLen2() + results3.length };
+    const countLen4 = () => { countLen3() + results4.length };
     // TODO performance tune
     // TODO performance tune
     // when total length of all results is less than `maxLength`
     // when total length of all results is less than `maxLength`
     for (let unicode in this.emojiStrategy) {
     for (let unicode in this.emojiStrategy) {
       const data = this.emojiStrategy[unicode];
       const data = this.emojiStrategy[unicode];
 
 
-      if (maxLength <= countLen1()) { break; }
+      if (maxLength <= countLen1()) { break }
       // prefix match to shortname
       // prefix match to shortname
       else if (data.shortname.indexOf(`:${term}`) > -1) {
       else if (data.shortname.indexOf(`:${term}`) > -1) {
         results1.push(data.shortname);
         results1.push(data.shortname);
         continue;
         continue;
       }
       }
-      else if (maxLength <= countLen2()) { continue; }
+      else if (maxLength <= countLen2()) { continue }
       // partial match to shortname
       // partial match to shortname
       else if (data.shortname.indexOf(term) > -1) {
       else if (data.shortname.indexOf(term) > -1) {
         results2.push(data.shortname);
         results2.push(data.shortname);
         continue;
         continue;
       }
       }
-      else if (maxLength <= countLen3()) { continue; }
+      else if (maxLength <= countLen3()) { continue }
       // partial match to elements of aliases
       // partial match to elements of aliases
       else if ((data.aliases != null) && data.aliases.find(elem => elem.indexOf(term) > -1)) {
       else if ((data.aliases != null) && data.aliases.find(elem => elem.indexOf(term) > -1)) {
         results3.push(data.shortname);
         results3.push(data.shortname);
         continue;
         continue;
       }
       }
-      else if (maxLength <= countLen4()) { continue; }
+      else if (maxLength <= countLen4()) { continue }
       // partial match to elements of keywords
       // partial match to elements of keywords
       else if ((data.keywords != null) && data.keywords.find(elem => elem.indexOf(term) > -1)) {
       else if ((data.keywords != null) && data.keywords.find(elem => elem.indexOf(term) > -1)) {
         results4.push(data.shortname);
         results4.push(data.shortname);

+ 182 - 0
resource/js/components/PageEditor/GrantSelector.js

@@ -0,0 +1,182 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import FormControl from 'react-bootstrap/es/FormControl';
+import ControlLabel from 'react-bootstrap/es/ControlLabel';
+// import Button from 'react-bootstrap/es/Button';
+
+// import Modal from 'react-bootstrap/es/Modal';
+
+/**
+ * Page grant select component
+ *
+ * @export
+ * @class GrantSelector
+ * @extends {React.Component}
+ */
+export default class GrantSelector extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pageGrant: this.props.pageGrant,
+      isGroupModalShown: false,
+    };
+
+    this.availableGrants = [1, 2, /*3, */4, 5];
+
+    this.availableGrantLabels = {
+      1: 'Public',
+      2: 'Anyone with the linc',
+      // 3:'Specified users only',
+      4: 'Just me',
+      5: 'Only inside the group',
+    };
+
+    this.onChangeGrant = this.onChangeGrant.bind(this);
+  }
+
+  // Init component when the component did mount.
+  componentDidMount() {
+    this.init();
+  }
+
+  // Initialize the component.
+  init() {
+    this.grantSelectorInputEl.value = this.state.pageGrant.grant;
+  }
+
+  /**
+   * On change event handler for pagegrant.
+   * @param {any} grant page grant
+   * @memberof GrantSelector
+   */
+  onChangeGrant(grant) {
+    const newValue = this.grantSelectorInputEl.value;
+    const newGrant = Object.assign(this.state.pageGrant, {grant: newValue});
+    this.setState({ pageGrant: newGrant });
+
+    // dispatch event
+    this.dispatchOnChange();
+  }
+
+  // (TBD)
+  // /**
+  //  * On click event handler for grant usergroup.
+  //  *
+  //  * @memberof GrantSelector
+  //  */
+  // onClickGrantGroup() {
+  //   const newValue = this.groupSelectorInputEl.value;
+  //   const newGrant = Object.assign(this.state.pageGrant, { grantGroup: newValue });
+  //   this.setState({ pageGrant: newGrant });
+
+  //   // dispatch event
+  //   this.dispatchOnChange();
+  //   // close group select modal
+  //   if (this.state.isModalShown) {
+  //     this.setState({ isGroupModalShown: false });
+  //   }
+  // }
+
+  /**
+   * dispatch onChange event
+   * @memberof GrantSelector
+   */
+  dispatchOnChange() {
+    if (this.props.onChange != null) {
+      this.props.onChange(this.state.pageGrant);
+    }
+  }
+
+  /**
+   * Render grant selector DOM.
+   * @returns
+   * @memberof GrantSelector
+   */
+  renderGrantSelector() {
+    const grantElems = this.availableGrants.map((grant) => {
+      return <option key={grant} value={grant}>{this.availableGrantLabels[grant]}</option>;
+    });
+
+    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
+
+    return (
+      <FormGroup controlId="formControlsSelect">
+        <ControlLabel>Grant:</ControlLabel>
+        <FormControl componentClass="select" placeholder="select" defaultValue={this.state.pageGrant.grant} bsClass={bsClassName} className="btn-group-sm selectpicker"
+          onChange={this.onChangeGrant}
+          inputRef={ el => this.grantSelectorInputEl=el }>
+
+          {grantElems}
+
+        </FormControl>
+      </FormGroup>
+    );
+  }
+
+  // (TBD)
+  // /**
+  //  * Render select grantgroup modal.
+  //  *
+  //  * @returns
+  //  * @memberof GrantSelector
+  //  */
+  // renderSelectGroupModal() {
+  //   // const userRelatedGroups = this.props.userRelatedGroups;
+  //   const groupList = this.userRelatedGroups.map((group) => {
+  //     return <li>
+  //         <Button onClick={this.onClickGrantGroup(group)} bsClass="btn btn-sm btn-primary">{group.name}</Button>
+  //       </li>;
+  //   });
+  //   return (
+  //     <Modal show={this.props.isGroupModalShown} className="select-grant-group">
+  //       <Modal.Header closeButton>
+  //         <Modal.Title>
+  //           Select a Group
+  //         </Modal.Title>
+  //       </Modal.Header>
+  //       <Modal.Body>
+
+  //         <ul className="list-inline">
+  //           {groupList}
+  //         </ul>
+  //       </Modal.Body>
+  //     </Modal>
+  //   );
+  // }
+
+  render() {
+    return <span>
+      <span className="m-l-5">{this.renderGrantSelector()}</span>
+    </span>;
+  }
+}
+
+export class PageGrant {
+  constructor(props) {
+    this.grant = '';
+    this.grantGroup = null;
+
+    Object.assign(this, props);
+  }
+}
+
+export class UserGroup {
+  constructor(props) {
+    this.userGroupId = '';
+    this.userGroup;
+
+    Object.assign(this, props);
+  }
+}
+
+GrantSelector.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  isGroupModalShown: PropTypes.bool,
+  userRelatedGroups: PropTypes.object,
+  pageGrant: PropTypes.instanceOf(PageGrant),
+  onChange: PropTypes.func,
+};

+ 26 - 55
resource/js/components/PageEditor/MarkdownListUtil.js

@@ -1,5 +1,3 @@
-import * as codemirror from 'codemirror';
-
 /**
 /**
  * Utility for markdown list
  * Utility for markdown list
  */
  */
@@ -11,26 +9,41 @@ class MarkdownListUtil {
     this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
     this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
 
+    this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this);
     this.pasteText = this.pasteText.bind(this);
     this.pasteText = this.pasteText.bind(this);
+  }
+
+  /**
+   * Self Implementation with AbstractEditor interface
+   * @param {AbstractEditor} editor An instance of AbstractEditor
+   */
+  newlineAndIndentContinueMarkdownList(editor) {
+    const strFromBol = editor.getStrFromBol();
 
 
-    this.getBol = this.getBol.bind(this);
-    this.getEol = this.getEol.bind(this);
-    this.getStrFromBol = this.getStrFromBol.bind(this);
-    this.getStrToEol = this.getStrToEol.bind(this);
-    this.newlineWithoutIndent = this.newlineWithoutIndent.bind(this);
+    if (this.indentAndMarkOnlyRE.test(strFromBol)) {
+      // clear current line and end list
+      editor.replaceBolToCurrentPos('\n');
+    }
+    else if (this.indentAndMarkRE.test(strFromBol)) {
+      // continue list
+      const indentAndMark = strFromBol.match(this.indentAndMarkRE)[0];
+      editor.insertText(`\n${indentAndMark}`);
+    }
+    else {
+      editor.insertLinebreak();
+    }
   }
   }
 
 
   /**
   /**
    * paste text
    * paste text
-   * @param {any} editor An editor instance of CodeMirror
+   * @param {AbstractEditor} editor An instance of AbstractEditor
    * @param {any} event
    * @param {any} event
    * @param {string} text
    * @param {string} text
    */
    */
   pasteText(editor, event, text) {
   pasteText(editor, event, text) {
     // get strings from BOL(beginning of line) to current position
     // get strings from BOL(beginning of line) to current position
-    const strFromBol = this.getStrFromBol(editor);
+    const strFromBol = editor.getStrFromBol();
 
 
-    const matched = strFromBol.match(this.indentAndMarkRE);
     // when match indentAndMarkOnlyRE
     // when match indentAndMarkOnlyRE
     // (this means the current position is the beginning of the list item)
     // (this means the current position is the beginning of the list item)
     if (this.indentAndMarkOnlyRE.test(strFromBol)) {
     if (this.indentAndMarkOnlyRE.test(strFromBol)) {
@@ -39,7 +52,7 @@ class MarkdownListUtil {
       // replace
       // replace
       if (adjusted != null) {
       if (adjusted != null) {
         event.preventDefault();
         event.preventDefault();
-        editor.getDoc().replaceRange(adjusted, this.getBol(editor), editor.getCursor());
+        editor.replaceBolToCurrentPos(adjusted);
       }
       }
     }
     }
   }
   }
@@ -64,7 +77,7 @@ class MarkdownListUtil {
       // indent
       // indent
       const replacedLines = lines.map((line) => {
       const replacedLines = lines.map((line) => {
         return indent + line;
         return indent + line;
-      })
+      });
 
 
       adjusted = replacedLines.join('\n');
       adjusted = replacedLines.join('\n');
     }
     }
@@ -75,7 +88,7 @@ class MarkdownListUtil {
     // not listful data
     // not listful data
     else {
     else {
       // append `indentAndMark` at the beginning of all lines (except the first line)
       // append `indentAndMark` at the beginning of all lines (except the first line)
-      const replacedText = text.replace(/(\r\n|\r|\n)/g, "$1" + indentAndMark);
+      const replacedText = text.replace(/(\r\n|\r|\n)/g, '$1' + indentAndMark);
       // append `indentAndMark` to the first line
       // append `indentAndMark` to the first line
       adjusted = indentAndMark + replacedText;
       adjusted = indentAndMark + replacedText;
     }
     }
@@ -112,48 +125,6 @@ class MarkdownListUtil {
     return isListful;
     return isListful;
   }
   }
 
 
-  /**
-   * return the postion of the BOL(beginning of line)
-   */
-  getBol(editor) {
-    const curPos = editor.getCursor();
-    return { line: curPos.line, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOL(end of line)
-   */
-  getEol(editor) {
-    const curPos = editor.getCursor();
-    const lineLength = editor.getDoc().getLine(curPos.line).length;
-    return { line: curPos.line, ch: lineLength };
-  }
-
-  /**
-   * return strings from BOL(beginning of line) to current position
-   */
-  getStrFromBol(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBol(editor), curPos);
-  }
-
-  /**
-   * return strings from current position to EOL(end of line)
-   */
-  getStrToEol(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(curPos, this.getEol(editor));
-  }
-
-  /**
-   * insert newline without indent
-   */
-  newlineWithoutIndent(editor, strToEol) {
-    codemirror.commands.newlineAndIndent(editor);
-
-    // replace the line with strToEol (abort auto indent)
-    editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
-  }
 }
 }
 
 
 // singleton pattern
 // singleton pattern

+ 13 - 6
resource/js/components/PageEditor/MarkdownTableInterceptor.js

@@ -32,25 +32,32 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
    */
    */
   process(contextName, ...args) {
   process(contextName, ...args) {
     const context = Object.assign(args[0]);   // clone
     const context = Object.assign(args[0]);   // clone
-    const editor = context.editor;
+    const editor = context.editor;            // AbstractEditor instance
+
+    // do nothing if editor is not a CodeMirrorEditor
+    if (editor == null || editor.getCodeMirror() == null) {
+      return Promise.resolve(context);
+    }
+
+    const cm = editor.getCodeMirror();
 
 
     // get strings from BOL(beginning of line) to current position
     // get strings from BOL(beginning of line) to current position
-    const strFromBol = mtu.getStrFromBol(editor);
+    const strFromBol = editor.getStrFromBol();
 
 
-    if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
+    if (mtu.isEndOfLine(cm) && mtu.linePartOfTableRE.test(strFromBol)) {
       // get lines all of table from current position to beginning of table
       // get lines all of table from current position to beginning of table
-      const strFromBot = mtu.getStrFromBot(editor);
+      const strFromBot = mtu.getStrFromBot(cm);
       let table = mtu.parseFromTableStringToMarkdownTable(strFromBot);
       let table = mtu.parseFromTableStringToMarkdownTable(strFromBot);
 
 
       mtu.addRowToMarkdownTable(table);
       mtu.addRowToMarkdownTable(table);
 
 
-      const strToEot = mtu.getStrToEot(editor);
+      const strToEot = mtu.getStrToEot(cm);
       const tableBottom = mtu.parseFromTableStringToMarkdownTable(strToEot);
       const tableBottom = mtu.parseFromTableStringToMarkdownTable(strToEot);
       if (tableBottom.table.length > 0) {
       if (tableBottom.table.length > 0) {
         table = mtu.mergeMarkdownTable([table, tableBottom]);
         table = mtu.mergeMarkdownTable([table, tableBottom]);
       }
       }
 
 
-      mtu.replaceMarkdownTableWithReformed(editor, table);
+      mtu.replaceMarkdownTableWithReformed(cm, table);
 
 
       // report to manager that handling was done
       // report to manager that handling was done
       context.handlers.push(this.className);
       context.handlers.push(this.className);

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