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

Merge branch 'master' into feat/ldap-auth

# Conflicts:
#	yarn.lock
Yuki Takei 8 лет назад
Родитель
Сommit
4b715fee30

+ 1 - 0
.gitignore

@@ -17,6 +17,7 @@ npm-debug.log
 /npm-debug.log.*
 
 # Dist #
+/report/
 /public/
 /src/*/__build__/
 /__build__/**

+ 14 - 0
CHANGES.md

@@ -1,6 +1,20 @@
 CHANGES
 ========
 
+## 2.2.1-RC
+
+* 
+
+## 2.2.0
+
+* Support: Merge official Crowi v1.6.3
+
+## 2.1.2
+
+* Improvement: Ensure to prevent suspending own account
+* Fix: Ensure to be able to use `.` for username when invited
+* Fix: monospace font for `<code></code>`
+
 ## 2.1.1
 
 * Fix: The problem that React Modal doesn't work

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

@@ -20,6 +20,9 @@ cli.main(function(args, options)
     , 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());

+ 6 - 0
config/webpack.dev.js

@@ -14,10 +14,12 @@ const commonConfig = require('./webpack.common.js');
  */
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 /*
  * Webpack Constants
  */
+const ANALYZE = process.env.ANALYZE;
 const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
 const HOST = process.env.HOST || '0.0.0.0';
 const PORT = process.env.PORT || 3000;
@@ -65,6 +67,10 @@ module.exports = function (options) {
 
       new webpack.NoEmitOnErrorsPlugin(),
 
+      new BundleAnalyzerPlugin({
+        analyzerMode: ANALYZE ? 'server' : 'disabled',
+      }),
+
     ]
   });
 }

+ 8 - 0
config/webpack.prod.js

@@ -13,10 +13,12 @@ const commonConfig = require('./webpack.common.js'); // the settings that are co
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
 const OptimizeJsPlugin = require('optimize-js-plugin');
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 /**
  * Webpack Constants
  */
+const ANALYZE = process.env.ANALYZE;
 const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
 const HOST = process.env.HOST || 'localhost';
 const PORT = process.env.PORT || 3000;
@@ -85,6 +87,12 @@ module.exports = function (env) {
         },
       }),
 
+      new BundleAnalyzerPlugin({
+        analyzerMode: ANALYZE ? 'static' : 'disabled',
+        reportFilename: helpers.root('report/bundle-analyzer.html'),
+        openAnalyzer: false,
+      }),
+
     ],
 
   });

+ 6 - 0
lib/crowi/dev.js

@@ -20,10 +20,16 @@ class CrowiDev {
   }
 
   init() {
+    this.initPromiseRejectionWarningHandler();
     this.initSwig();
     this.hackLRWebSocketServer();
   }
 
+  initPromiseRejectionWarningHandler() {
+    // https://qiita.com/syuilo/items/0800d7e44e93203c7285
+    process.on('unhandledRejection', console.dir);
+  }
+
   initSwig() {
     swig.setDefaults({ cache: false });
   }

+ 1 - 1
lib/form/invited.js

@@ -4,7 +4,7 @@ var form = require('express-form')
   , field = form.field;
 
 module.exports = form(
-  field('invitedForm.username').required().is(/^[\da-zA-Z\-_]+$/),
+  field('invitedForm.username').required().is(/^[\da-zA-Z\-_\.]+$/),
   field('invitedForm.name').required(),
   field('invitedForm.password').required().is(/^[\x20-\x7F]{6,}$/)
 );

+ 3 - 3
lib/models/config.js

@@ -378,10 +378,10 @@ module.exports = function(crowi) {
 
   configSchema.statics.hasSlackConfig = function(config)
   {
-    return Config.hasSlackAppConfig(config) || Config.hasSlackIwhUrl(config);
+    return Config.hasSlackWebClientConfig(config) || Config.hasSlackIwhUrl(config);
   };
 
-  configSchema.statics.hasSlackAppConfig = function(config)
+  configSchema.statics.hasSlackWebClientConfig = function(config)
   {
     if (!config.notification) {
       return false;
@@ -415,7 +415,7 @@ module.exports = function(crowi) {
 
   configSchema.statics.hasSlackToken = function(config)
   {
-    if (!this.hasSlackAppConfig(config)) {
+    if (!this.hasSlackWebClientConfig(config)) {
       return false;
     }
 

+ 27 - 35
lib/routes/admin.js

@@ -140,13 +140,13 @@ module.exports = function(crowi, app) {
     var config = crowi.getConfig();
     var UpdatePost = crowi.model('UpdatePost');
     var slackSetting = Config.setupCofigFormData('notification', config);
-    var hasSlackAppConfig = Config.hasSlackAppConfig(config);
+    var hasSlackWebClientConfig = Config.hasSlackWebClientConfig(config);
     var hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
     var hasSlackToken = Config.hasSlackToken(config);
     var slack = crowi.slack;
     var slackAuthUrl = '';
 
-    if (!Config.hasSlackAppConfig(req.config)) {
+    if (!Config.hasSlackWebClientConfig(req.config)) {
       slackSetting['slack:clientId'] = '';
       slackSetting['slack:clientSecret'] = '';
     }
@@ -167,7 +167,7 @@ module.exports = function(crowi, app) {
       return res.render('admin/notification', {
         settings,
         slackSetting,
-        hasSlackAppConfig,
+        hasSlackWebClientConfig,
         hasSlackIwhUrl,
         hasSlackToken,
         slackAuthUrl
@@ -175,7 +175,7 @@ module.exports = function(crowi, app) {
     });
   };
 
-  // app.post('/admin/notification/slackSetting' , admin.notification.slackSetting);
+  // app.post('/admin/notification/slackSetting' , admin.notification.slackauth);
   actions.notification.slackSetting = function(req, res) {
     var slackSetting = req.form.slackSetting;
 
@@ -199,26 +199,17 @@ module.exports = function(crowi, app) {
 
   // app.get('/admin/notification/slackAuth'     , admin.notification.slackauth);
   actions.notification.slackAuth = function(req, res) {
-    var code = req.query.code;
-    var config = crowi.getConfig();
+    const code = req.query.code;
+    const config = crowi.getConfig();
 
-    if (!code || !Config.hasSlackAppConfig(req.config)) {
+    if (!code || !Config.hasSlackConfig(req.config)) {
       return res.redirect('/admin/notification');
     }
 
-    var slack = crowi.slack;
-    var bot = slack.initAppBot(true);
-    var args = {
-      code,
-      client_id: config.notification['slack:clientId'],
-      client_secret: config.notification['slack:clientSecret'],
-    }
-    bot.api.oauth.access(args, function(err, data) {
-      debug('oauth response', err, data);
-      if (!data.ok || !data.access_token) {
-        req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
-        return res.redirect('/admin/notification');
-      } else {
+    const slack = crowi.slack;
+    slack.getOauthAccessToken(code)
+    .then(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.']);
@@ -227,23 +218,11 @@ module.exports = function(crowi, app) {
             req.flash('successMessage', ['Successfully Connected!']);
           }
 
-          slack.initAppBot();
           return res.redirect('/admin/notification');
         });
-      }
-    });
-  };
-
-  // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
-  actions.notification.disconnectFromSlack = function(req, res) {
-    const config = crowi.getConfig();
-    const slack = crowi.slack;
-
-    Config.updateNamespaceByArray('notification', {'slack:token': ''}, function(err, config) {
-      Config.updateConfigCache('notification', config);
-      req.flash('successMessage', ['Successfully Disconnected!']);
-
-      slack.initAppBot();
+    }).catch(err => {
+      debug('oauth response ERROR', err);
+      req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
       return res.redirect('/admin/notification');
     });
   };
@@ -279,6 +258,19 @@ module.exports = function(crowi, app) {
     }
   };
 
+  // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
+  actions.notification.disconnectFromSlack = function(req, res) {
+    const config = crowi.getConfig();
+    const slack = crowi.slack;
+
+    Config.updateNamespaceByArray('notification', {'slack:token': ''}, function(err, config) {
+      Config.updateConfigCache('notification', config);
+      req.flash('successMessage', ['Successfully Disconnected!']);
+
+      return res.redirect('/admin/notification');
+    });
+  };
+
   actions.search.buildIndex = function(req, res) {
     var search = crowi.getSearcher();
     if (!search) {

+ 2 - 0
lib/routes/attachment.js

@@ -23,12 +23,14 @@ module.exports = function(crowi, app) {
       // TODO: file delivery plugin for cdn
       Attachment.findDeliveryFile(data)
       .then(fileName => {
+        const encodedFileName = encodeURIComponent(data.originalName);
 
         var deliveryFile = {
           fileName: fileName,
           options: {
             headers: {
               'Content-Type': data.fileFormat,
+              'Content-Disposition': `inline;filename*=UTF-8''${encodedFileName}`,
             },
           },
         };

+ 16 - 6
lib/routes/page.js

@@ -605,7 +605,7 @@ module.exports = function(crowi, app) {
           if (crowi.slack) {
             notify.slack.channel.split(',').map(function(chan) {
               var message = crowi.slack.prepareSlackMessage(pageData, req.user, chan, updateOrCreate, previousRevision);
-              crowi.slack.post(message).then(function(){}).catch(function(){});
+              crowi.slack.post(message.channel, message.text, message).then(function(){}).catch(function(){});
             });
           }
         }
@@ -872,12 +872,22 @@ module.exports = function(crowi, app) {
    * @apiParam {String} revision_id
    */
   api.get = function(req, res){
-    var pagePath = req.query.path || null;
-    var pageId = req.query.page_id || null; // TODO: handling
-    var revisionId = req.query.revision_id || null;
+    const pagePath = req.query.path || null;
+    const pageId = req.query.page_id || null; // TODO: handling
+    const revisionId = req.query.revision_id || null;
 
-    Page.findPage(pagePath, req.user, revisionId)
-    .then(function(pageData) {
+    if (!pageId && !pagePath) {
+      return res.json(ApiResponse.error(new Error('Parameter path or page_id is required.')));
+    }
+
+    let pageFinder;
+    if (pageId) { // prioritized
+      pageFinder = Page.findPageByIdAndGrantedUser(pageId, req.user);
+    } else if (pagePath) {
+      pageFinder = Page.findPage(pagePath, req.user, revisionId);
+    }
+
+    pageFinder.then(function(pageData) {
       var result = {};
       result.page = pageData;
 

+ 90 - 125
lib/util/slack.js

@@ -5,114 +5,35 @@
 module.exports = function(crowi) {
   'use strict';
 
-  var debug = require('debug')('crowi:util:slack'),
+  const SLACK_URL = 'https://slack.com';
+
+  const debug = require('debug')('crowi:util:slack'),
     Config = crowi.model('Config'),
-    Botkit = require('botkit'),
-    isDebugSlackbot = false,
-    appBot = null,                  // for Slack App
-    iwhBot = null,                  // for Slack Incoming Webhooks
+    SlackWebClient = require('@slack/client').WebClient,
+    SlackIncomingWebhook = require('@slack/client').IncomingWebhook,
     slack = {};
-  slack.appController = undefined;  // for Slack App
-  slack.iwhController = undefined;  // for Slack Incoming Webhooks
-
-  // isDebugSlackbot = true;           // for debug
 
-  slack.getBot = function() {
-    var config = crowi.getConfig();
+  slack.client = undefined;
+  slack.incomingWebhook = undefined;
 
-    // when incoming Webhooks is prioritized
-    if (Config.isIncomingWebhookPrioritized(config)) {
-      if (Config.hasSlackIwhUrl(config)) {
-        return iwhBot || slack.initIwhBot();
-      }
-      else if (Config.hasSlackToken(config)) {
-        return appBot || slack.initAppBot();
-      }
-    }
-    // else
-    else {
-      if (Config.hasSlackToken(config)) {
-        return appBot || slack.initAppBot();
-      }
-      else if (Config.hasSlackIwhUrl(config)) {
-        return iwhBot || slack.initIwhBot();
-      }
+  slack.getClient = function() {
+    // alreay created
+    if (slack.client) {
+      return slack.client;
     }
 
-    return false;
-  };
+    const config = crowi.getConfig();
 
-  slack.initAppBot = function(isClearToken) {
-    var config = crowi.getConfig();
-
-    if (!slack.appController) {
-      slack.configureSlackApp();
-    }
-
-    if (!slack.appController) {
-      return false;
+    let client;
+    if (Config.hasSlackToken(config)) {
+      client = new SlackWebClient(config.notification['slack:token']);
+      slack.client = client;
     }
 
-    if (!isClearToken && Config.hasSlackToken(config)) {
-      appBot = slack.appController.spawn({token: config.notification['slack:token']});
-    } else {
-      appBot = slack.appController.spawn();
-    }
-    return appBot;
+    return slack.client;
   };
 
-  slack.initIwhBot = function() {
-    var config = crowi.getConfig();
-
-    if (!slack.iwhController) {
-      slack.configureSlackIwh();
-    }
-
-    if (!slack.iwhController) {
-      return false;
-    }
-
-    iwhBot = slack.iwhController.spawn({
-      incoming_webhook: {
-        url: config.notification['slack:incomingWebhookUrl']
-      }
-    });
-
-    return iwhBot;
-  }
-
-  slack.configureSlackApp = function ()
-  {
-    var config = crowi.getConfig();
-
-    if (Config.hasSlackAppConfig(config)) {
-      slack.appController = Botkit.slackbot({debug: isDebugSlackbot});
-      slack.appController.configureSlackApp({
-        clientId: config.notification['slack:clientId'],
-        clientSecret: config.notification['slack:clientSecret'],
-        redirectUri: slack.getSlackAuthCallbackUrl(),
-        scopes: ['chat:write:bot']
-      });
-
-      return true;
-    }
-
-    return false;
-  }
-
-  slack.configureSlackIwh = function ()
-  {
-    var config = crowi.getConfig();
-
-    if (Config.hasSlackIwhUrl(config)) {
-      slack.iwhController = Botkit.slackbot({debug: isDebugSlackbot});
-      return true;
-    }
-
-    return false;
-  }
-
-  // hmmm
+  // this is called to generate redirect_uri
   slack.getSlackAuthCallbackUrl = function()
   {
     var config = crowi.getConfig();
@@ -121,48 +42,89 @@ module.exports = function(crowi) {
     return (config.crowi['app:url'] || '') + '/admin/notification/slackAuth';
   }
 
+  // this is called to get the url for oauth screen
   slack.getAuthorizeURL = function () {
-    if (!slack.appController) {
-      slack.configureSlackApp();
-    }
+    const config = crowi.getConfig();
+    if (Config.hasSlackWebClientConfig(config)) {
+      const slackClientId = config.notification['slack:clientId'];
+      const redirectUri = slack.getSlackAuthCallbackUrl();
+
+      return `${SLACK_URL}/oauth/authorize?client_id=${slackClientId}&redirect_uri=${redirectUri}&scope=chat:write:bot`;
+    } else {
 
-    if (!slack.appController) {
       return '';
     }
-
-    return slack.appController.getAuthorizeURL();
   }
 
-  slack.post = function (message) {
-    var bot = slack.getBot();
-    let sendMethod = undefined;
+  // this is called to get access token with code (oauth process)
+  slack.getOauthAccessToken = function(code) {
 
-    // use Slack App
-    if (bot === appBot) {
-      debug(`sendMethod: bot.api.chat.postMessage`);
-      sendMethod = bot.api.chat.postMessage;
-    }
-    // use Slack Incoming Webhooks
-    else if (bot === iwhBot) {
-      debug(`sendMethod: bot.sendWebhook`);
-      sendMethod = bot.sendWebhook;
+    const client = new SlackWebClient();
+
+    const config = crowi.getConfig();
+    const clientId = config.notification['slack:clientId'];
+    const clientSecret = config.notification['slack:clientSecret'];
+    const redirectUri = slack.getSlackAuthCallbackUrl();
+
+    return client.oauth.access(clientId, clientSecret, code, {redirect_uri: redirectUri});
+  }
+
+  slack.getIncomingWebhook = function() {
+    // alreay created
+    if (slack.incomingWebhook) {
+      return slack.incomingWebhook;
     }
 
-    if (sendMethod === undefined) {
-      debug(`sendMethod is undefined`);
-      return Promise.resolve();
+    const config = crowi.getConfig();
+
+    let incomingWebhook;
+    if (Config.hasSlackIwhUrl(config)) {
+      incomingWebhook = new SlackIncomingWebhook(config.notification['slack:incomingWebhookUrl']);
+      slack.incomingWebhook = incomingWebhook;
     }
 
+    return slack.incomingWebhook;
+  };
+
+  slack.post = function (channel, message, opts) {
+    const config = crowi.getConfig();
+
     return new Promise(function(resolve, reject) {
-      sendMethod(message, function(err, res) {
+
+      // define callback function
+      const callback = function(err, res) {
         if (err) {
           debug('Post error', err, res);
           debug('Sent data to slack is:', message);
           return reject(err);
         }
-
         resolve(res);
-      });
+      };
+
+      // when incoming Webhooks is prioritized
+      if (Config.isIncomingWebhookPrioritized(config)) {
+        if (Config.hasSlackIwhUrl(config)) {
+          debug(`posting message with IncomingWebhook`);
+          slack.getIncomingWebhook().send(opts, callback);
+        }
+        else if (Config.hasSlackToken(config)) {
+          debug(`posting message with WebClient`);
+          slack.getClient().chat.postMessage(channel, message, opts, callback);
+        }
+      }
+      // else
+      else {
+        if (Config.hasSlackToken(config)) {
+          debug(`posting message with WebClient`);
+          slack.getClient().chat.postMessage(channel, message, opts, callback);
+        }
+        else if (Config.hasSlackIwhUrl(config)) {
+          debug(`posting message with IncomingWebhook`);
+          slack.getIncomingWebhook().send(opts, callback);
+        }
+      }
+
+      resolve();
     });
   };
 
@@ -244,7 +206,7 @@ module.exports = function(crowi) {
 
     var message = {
       channel: '#' + channel,
-      username: 'Crowi',
+      username: config.crowi['app:title'],
       text: this.getSlackMessageText(page.path, user, updateType),
       attachments: [attachment],
     };
@@ -253,12 +215,15 @@ module.exports = function(crowi) {
   };
 
   slack.getSlackMessageText = function(path, user, updateType) {
-    var text;
+    let text;
+    const config = crowi.getConfig();
+    const url = config.crowi['app:url'] || '';
 
+    const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
-      text = `:white_check_mark: ${user.username} created a new page! ${path}`;
+      text = `:white_check_mark: ${user.username} created a new page! ${pageUrl}`;
     } else {
-      text = `:up: ${user.username} updated ${path}`;
+      text = `:up: ${user.username} updated ${pageUrl}`;
     }
 
     return text;

+ 8 - 8
lib/views/admin/notification.html

@@ -84,7 +84,7 @@
             <input type="hidden" name="_csrf" value="{{ csrf() }}">
           </form>
 
-          {% if hasSlackAppConfig %}
+          {% if hasSlackWebClientConfig %}
           <div class="text-center">
             {% if hasSlackToken %}
             <p>Crowi and Slack is already <strong>connected</strong>. You can re-connect to refresh and overwirte the token with your Slack account.</p>
@@ -104,7 +104,7 @@
 
           {% endif %}
 
-          {# {% if not hasSlackAppConfig %} #}
+          {# {% if not hasSlackWebClientConfig %} #}
           <hr>
           <h3>
             <i class="fa fa-question-circle" aria-hidden="true"></i>
@@ -119,7 +119,7 @@
                   Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
                   <dl class="dl-horizontal">
                     <dt>App Name</dt> <dd><code>crowi-plus</code> </dd>
-                    <dt>Development Slack Team</dt> <dd>Select the team you want to notify to.</dd>
+                    <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
                   </dl>
                 </li>
                 <li><strong>Save</strong> it.</li>
@@ -156,17 +156,17 @@
             <li>
               Install the app
               <ol>
-                <li>Go to "Install App to Your Team" page and install.</li>
+                <li>Go to "Install App to Your Workspace" page and install.</li>
               </ol>
             </li>
             <li>
-              (At Team) Approve the app
+              (At Workspace) Approve the app
               <ol>
-                <li>Go to the management Apps page for the team you installed the app and approve crowi-plus.</li>
+                <li>Go to the management Apps page for the workspace you installed the app and approve crowi-plus.</li>
               </ol>
             </li>
             <li>
-              (At Team) Invite the bot to your team
+              (At Workspace) Invite the bot to your workspace
               <ol>
                 <li>Invite the user you created in <code>4. Add a bot user</code> to the channel you notify to.</li>
               </ol>
@@ -223,7 +223,7 @@
 
           <ol id="collapseHelpForIwh" class="collapse">
             <li>
-              (At Team) Add a hook
+              (At Workspace) Add a hook
               <ol>
                 <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
                 <li>Choose the default channel to post.</li>

+ 11 - 6
lib/views/admin/users.html

@@ -205,12 +205,17 @@
                   </form>
                   {% endif  %}
                   {% if sUser.status == 2 %}
-
-                  <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-block btn-warning">アカウント停止</button>
-                  </form>
-                  {% endif  %}
+                    {% if sUser.username != user.username %}
+                    <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
+                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                      <button type="submit" class="btn btn-block btn-warning">アカウント停止</button>
+                    </form>
+                    {% else %}
+                    <button class="btn btn-block btn-warning" disabled>アカウント停止</button>
+                    <br>
+                    <p class="alert alert-danger">自分自身のアカウントを停止することはできません</p>
+                    {% endif %}
+                  {% endif %}
                   {% if sUser.status == 3 %}
                   <form action="/admin/user/{{ sUser._id.toString() }}/activate" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">

+ 1 - 1
lib/views/mail/admin/userWaitingActivation.txt

@@ -12,7 +12,7 @@ Email: {{ createdUser.email }}
 ====
 
 Please do some action with following URL:
-{{ url }}/admin/user
+{{ url }}/admin/users
 
 
 --

+ 23 - 18
package.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi-plus",
-  "version": "2.1.2-RC",
+  "version": "2.2.1-RC",
   "description": "Enhanced Crowi",
   "tags": [
     "wiki",
@@ -19,13 +19,16 @@
     "url": "https://github.com/weseek/crowi-plus/issues"
   },
   "scripts": {
+    "build:dev:analyze": "cross-env ANALYZE=1 npm run build:dev",
     "build:dev:watch": "npm run build:dev -- --watch",
     "build:dev": "npm run clean:js && webpack --config config/webpack.dev.js  --progress --profile",
+    "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod": "npm run clean && webpack --config config/webpack.prod.js  --progress --profile --bail",
     "build": "npm run build:dev",
     "clean:js": "rimraf -- public/js",
     "clean:dll": "rimraf -- public/dll",
-    "clean": "npm run clean:js && npm run clean:dll",
+    "clean:report": "rimraf -- report",
+    "clean": "npm run clean:js && npm run clean:dll && npm run clean:report",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
@@ -45,27 +48,28 @@
   },
   "dependencies": {
     "8fold-marked": "^0.3.7",
+    "@slack/client": "^3.14.0",
     "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
-    "aws-sdk": "^2.80.0",
-    "axios": "^0.16.1",
-    "babel-core": "^6.24.0",
-    "babel-loader": "^7.0.0",
-    "babel-preset-env": "^1.4.0",
-    "babel-preset-react": "^6.23.0",
+    "aws-sdk": "^2.88.0",
+    "axios": "^0.16.2",
+    "babel-core": "^6.25.0",
+    "babel-loader": "^7.1.1",
+    "babel-preset-env": "^1.6.0",
+    "babel-preset-react": "^6.24.1",
     "basic-auth-connect": "~1.0.0",
-    "body-parser": "^1.17.1",
+    "body-parser": "^1.18.2",
     "bootstrap-sass": "~3.3.6",
-    "botkit": "^0.6.0",
     "check-node-version": "^2.0.1",
     "connect-flash": "~0.1.1",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
+    "cross-env": "^5.0.5",
     "crowi-pluginkit": "^1.1.0",
     "csrf": "~3.0.3",
     "css-loader": "^0.28.0",
     "debug": "^3.1.0",
-    "diff": "^3.2.0",
+    "diff": "^3.3.0",
     "diff2html": "^2.3.0",
     "elasticsearch": "^13.2.0",
     "emojify.js": "^1.1.0",
@@ -80,7 +84,7 @@
     "googleapis": "^22.0.0",
     "graceful-fs": "^4.1.11",
     "highlight.js": "^9.10.0",
-    "i18next": "^9.0.0",
+    "i18next": "^10.0.1",
     "i18next-express-middleware": "^1.0.5",
     "i18next-node-fs-backend": "^1.0.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
@@ -89,7 +93,7 @@
     "md5": "^2.2.1",
     "method-override": "^2.3.10",
     "mkdirp": "~0.5.1",
-    "moment": "^2.18.0",
+    "moment": "2.18.1",
     "mongoose": "4.11.14",
     "mongoose-paginate": "5.0.x",
     "mongoose-unique-validator": "^1.0.6",
@@ -120,7 +124,7 @@
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.27",
     "webpack": "^3.1.0",
-    "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
+    "webpack-bundle-analyzer": "^2.9.0",
     "webpack-merge": "~4.1.0"
   },
   "devDependencies": {
@@ -134,12 +138,13 @@
     "node-dev": "^3.1.3",
     "on-headers": "^1.0.1",
     "sinon": "^4.0.0",
-    "sinon-chai": "^2.13.0"
+    "sinon-chai": "^2.13.0",
+    "webpack-dll-bundles-plugin": "^1.0.0-beta.5"
   },
   "engines": {
-    "node": "6.11.3",
-    "npm": "4.6.1",
-    "yarn": "1.1.0"
+    "node": ">=6.11 <7",
+    "npm": ">=4.6 <5",
+    "yarn": "~1.1.0"
   },
   "config": {
     "blanket": {

+ 4 - 1
resource/css/_form.scss

@@ -1,3 +1,6 @@
+// import crowi variable
+@import 'utilities';
+
 .crowi.main-container .main .content-main.on-edit { // {{{ Edit Form of Page
   padding: 0;
 
@@ -137,7 +140,7 @@
 } // }}}
 
 textarea {
-  font-family: menlo, monaco, consolas, monospace;
+  font-family: $font-family-monospace-not-strictly;
   line-height: 1.1em;
 }
 

+ 2 - 1
resource/css/_utilities.scss

@@ -58,7 +58,8 @@ $link-hover-color:      darken($link-color, 15%);
 //
 $font-family-sans-serif:  Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
 // $font-family-serif:       Georgia, "Times New Roman", Times, serif;
-$font-family-monospace:   Monaco, Menlo, Consolas, "Courier New", MeiryoKe_Console, monospace;
+$font-family-monospace:   Osaka-Mono, "MS Gothic", Monaco, Menlo, Consolas, "Courier New", monospace;
+$font-family-monospace-not-strictly:   Monaco, Menlo, Consolas, "Courier New", MeiryoKe_Console, "M+ 1m", monospace;
 // $font-family-base:        $font-family-sans-serif;
 //
 // $font-size-base:          14px;

+ 8 - 1
resource/css/_wiki.scss

@@ -1,3 +1,6 @@
+// import crowi variable
+@import 'utilities';
+
 div.body {
   padding: 10px;
 }
@@ -142,7 +145,7 @@ div.body {
   }
 
   img.emoji {
-    width: 0.95em;
+    width: 1.1em;
     margin: 1px;
     border: none;
     box-shadow: none;
@@ -181,6 +184,10 @@ div.body {
   };
 */
 
+  p code {  // only inline code blocks
+    font-family: $font-family-monospace-not-strictly;
+  }
+
   .page-template-builder {
     position: relative;
 

+ 0 - 1
resource/js/legacy/crowi.js

@@ -3,7 +3,6 @@
 */
 
 var io = require('socket.io-client');
-var moment = require("moment");
 require('bootstrap-sass');
 require('jquery.cookie');
 

+ 97 - 56
test/models/page.test.js

@@ -6,20 +6,23 @@ var chai = require('chai')
   ;
 chai.use(sinonChai);
 
-describe('Page', function () {
+describe('Page', () => {
   var Page = utils.models.Page,
     User   = utils.models.User,
-    conn   = utils.mongoose.connection;
+    conn   = utils.mongoose.connection,
+    createdPages,
+    createdUsers;
 
-  before(function (done) {
-    Promise.resolve().then(function() {
+  before(done => {
+    Promise.resolve().then(() => {
       var userFixture = [
         {name: 'Anon 0', username: 'anonymous0', email: 'anonymous0@example.com'},
         {name: 'Anon 1', username: 'anonymous1', email: 'anonymous1@example.com'}
       ];
 
       return testDBUtil.generateFixture(conn, 'User', userFixture);
-    }).then(function(testUsers) {
+    }).then(testUsers => {
+      createdUsers = testUsers;
       var testUser0 = testUsers[0];
 
       var fixture = [
@@ -62,16 +65,17 @@ describe('Page', function () {
       ];
 
       return testDBUtil.generateFixture(conn, 'Page', fixture)
-      .then(function(pages) {
+      .then(pages => {
+        createdPages = pages;
         done();
       });
     });
   });
 
-  describe('.isPublic', function () {
-    context('with a public page', function() {
-      it('should return true', function(done) {
-        Page.findOne({path: '/grant/public'}, function(err, page) {
+  describe('.isPublic', () => {
+    context('with a public page', () => {
+      it('should return true', done => {
+        Page.findOne({path: '/grant/public'}, (err, page) => {
           expect(err).to.be.null;
           expect(page.isPublic()).to.be.equal(true);
           done();
@@ -79,10 +83,10 @@ describe('Page', function () {
       });
     });
 
-    ['restricted', 'specified', 'owner'].forEach(function(grant) {
-      context('with a ' + grant + ' page', function() {
-        it('should return false', function(done) {
-          Page.findOne({path: '/grant/' + grant}, function(err, page) {
+    ['restricted', 'specified', 'owner'].forEach(grant => {
+      context('with a ' + grant + ' page', () => {
+        it('should return false', done => {
+          Page.findOne({path: '/grant/' + grant}, (err, page) => {
             expect(err).to.be.null;
             expect(page.isPublic()).to.be.equal(false);
             done();
@@ -92,22 +96,22 @@ describe('Page', function () {
     });
   });
 
-  describe('.getDeletedPageName', function() {
-    it('should return trash page name', function() {
+  describe('.getDeletedPageName', () => {
+    it('should return trash page name', () => {
       expect(Page.getDeletedPageName('/hoge')).to.be.equal('/trash/hoge');
       expect(Page.getDeletedPageName('hoge')).to.be.equal('/trash/hoge');
     });
   });
-  describe('.getRevertDeletedPageName', function() {
-    it('should return reverted trash page name', function() {
+  describe('.getRevertDeletedPageName', () => {
+    it('should return reverted trash page name', () => {
       expect(Page.getRevertDeletedPageName('/hoge')).to.be.equal('/hoge');
       expect(Page.getRevertDeletedPageName('/trash/hoge')).to.be.equal('/hoge');
       expect(Page.getRevertDeletedPageName('/trash/hoge/trash')).to.be.equal('/hoge/trash');
     });
   });
 
-  describe('.isDeletableName', function() {
-    it('should decide deletable or not', function() {
+  describe('.isDeletableName', () => {
+    it('should decide deletable or not', () => {
       expect(Page.isDeletableName('/hoge')).to.be.true;
       expect(Page.isDeletableName('/user/xxx')).to.be.false;
       expect(Page.isDeletableName('/user/xxx123')).to.be.false;
@@ -116,8 +120,8 @@ describe('Page', function () {
     });
   });
 
-  describe('.isCreatableName', function() {
-    it('should decide creatable or not', function() {
+  describe('.isCreatableName', () => {
+    it('should decide creatable or not', () => {
       expect(Page.isCreatableName('/hoge')).to.be.true;
 
       // edge cases
@@ -164,13 +168,13 @@ describe('Page', function () {
     });
   });
 
-  describe('.isCreator', function() {
-    context('with creator', function() {
-      it('should return true', function(done) {
-        User.findOne({email: 'anonymous0@example.com'}, function(err, user) {
+  describe('.isCreator', () => {
+    context('with creator', () => {
+      it('should return true', done => {
+        User.findOne({email: 'anonymous0@example.com'}, (err, user) => {
           if (err) { done(err); }
 
-          Page.findOne({path: '/user/anonymous/memo'}, function(err, page) {
+          Page.findOne({path: '/user/anonymous/memo'}, (err, page) => {
             expect(page.isCreator(user)).to.be.equal(true);
             done();
           })
@@ -178,12 +182,12 @@ describe('Page', function () {
       });
     });
 
-    context('with non-creator', function() {
-      it('should return false', function(done) {
-        User.findOne({email: 'anonymous1@example.com'}, function(err, user) {
+    context('with non-creator', () => {
+      it('should return false', done => {
+        User.findOne({email: 'anonymous1@example.com'}, (err, user) => {
           if (err) { done(err); }
 
-          Page.findOne({path: '/user/anonymous/memo'}, function(err, page) {
+          Page.findOne({path: '/user/anonymous/memo'}, (err, page) => {
             expect(page.isCreator(user)).to.be.equal(false);
             done();
           })
@@ -192,13 +196,13 @@ describe('Page', function () {
     });
   });
 
-  describe('.isGrantedFor', function() {
-    context('with a granted user', function() {
-      it('should return true', function(done) {
-        User.findOne({email: 'anonymous0@example.com'}, function(err, user) {
+  describe('.isGrantedFor', () => {
+    context('with a granted user', () => {
+      it('should return true', done => {
+        User.findOne({email: 'anonymous0@example.com'}, (err, user) => {
           if (err) { done(err); }
 
-          Page.findOne({path: '/user/anonymous/memo'}, function(err, page) {
+          Page.findOne({path: '/user/anonymous/memo'}, (err, page) => {
             if (err) { done(err); }
 
             expect(page.isGrantedFor(user)).to.be.equal(true);
@@ -208,12 +212,12 @@ describe('Page', function () {
       });
     });
 
-    context('with a public page', function() {
-      it('should return true', function(done) {
-        User.findOne({email: 'anonymous1@example.com'}, function(err, user) {
+    context('with a public page', () => {
+      it('should return true', done => {
+        User.findOne({email: 'anonymous1@example.com'}, (err, user) => {
           if (err) { done(err); }
 
-          Page.findOne({path: '/grant/public'}, function(err, page) {
+          Page.findOne({path: '/grant/public'}, (err, page) => {
             if (err) { done(err); }
 
             expect(page.isGrantedFor(user)).to.be.equal(true);
@@ -223,12 +227,12 @@ describe('Page', function () {
       });
     });
 
-    context('with a restricted page and an user who has no grant', function() {
-      it('should return false', function(done) {
-        User.findOne({email: 'anonymous1@example.com'}, function(err, user) {
+    context('with a restricted page and an user who has no grant', () => {
+      it('should return false', done => {
+        User.findOne({email: 'anonymous1@example.com'}, (err, user) => {
           if (err) { done(err); }
 
-          Page.findOne({path: '/grant/restricted'}, function(err, page) {
+          Page.findOne({path: '/grant/restricted'}, (err, page) => {
             if (err) { done(err); }
 
             expect(page.isGrantedFor(user)).to.be.equal(false);
@@ -239,21 +243,21 @@ describe('Page', function () {
     });
   });
 
-  describe('Extended field', function () {
-    context('Slack Channel.', function() {
-      it('should be empty', function(done) {
-        Page.findOne({path: '/page/for/extended'}, function(err, page) {
+  describe('Extended field', () => {
+    context('Slack Channel.', () => {
+      it('should be empty', done => {
+        Page.findOne({path: '/page/for/extended'}, (err, page) => {
           expect(page.extended.hoge).to.be.equal(1);
           expect(page.getSlackChannel()).to.be.equal('');
           done();
         })
       });
 
-      it('set slack channel and should get it and should keep hoge ', function(done) {
-        Page.findOne({path: '/page/for/extended'}, function(err, page) {
+      it('set slack channel and should get it and should keep hoge ', done => {
+        Page.findOne({path: '/page/for/extended'}, (err, page) => {
           page.updateSlackChannel('slack-channel1')
-          .then(function(data) {
-            Page.findOne({path: '/page/for/extended'}, function(err, page) {
+          .then(data => {
+            Page.findOne({path: '/page/for/extended'}, (err, page) => {
               expect(page.extended.hoge).to.be.equal(1);
               expect(page.getSlackChannel()).to.be.equal('slack-channel1');
               done();
@@ -265,18 +269,55 @@ describe('Page', function () {
     });
   });
 
-  describe('Normalize path', function () {
-    context('Normalize', function() {
-      it('should start with slash', function(done) {
+  describe('Normalize path', () => {
+    context('Normalize', () => {
+      it('should start with slash', done => {
         expect(Page.normalizePath('hoge/fuga')).to.equal('/hoge/fuga');
         done();
       });
 
-      it('should trim spaces of slash', function(done) {
+      it('should trim spaces of slash', done => {
         expect(Page.normalizePath('/ hoge / fuga')).to.equal('/hoge/fuga');
         done();
       });
     });
   });
 
+  describe('.findPage', () => {
+    context('findPageById', () => {
+      it('should find page', done => {
+        const pageToFind = createdPages[0];
+        Page.findPageById(pageToFind._id)
+        .then(pageData => {
+          expect(pageData.path).to.equal(pageToFind.path);
+          done();
+        });
+      });
+    });
+
+    context('findPageByIdAndGrantedUser', () => {
+      it('should find page', done => {
+        const pageToFind = createdPages[0];
+        const grantedUser = createdUsers[0];
+        Page.findPageByIdAndGrantedUser(pageToFind._id, grantedUser)
+        .then(pageData => {
+          expect(pageData.path).to.equal(pageToFind.path);
+          done();
+        });
+      });
+
+      it('should error by grant', done => {
+        const pageToFind = createdPages[0];
+        const grantedUser = createdUsers[1];
+        Page.findPageByIdAndGrantedUser(pageToFind._id, grantedUser)
+        .then(pageData => {
+          done(new Error());
+        }).catch(err => {
+          expect(err).to.instanceof(Error);
+          done();
+        });
+      });
+    });
+  });
+
 });

+ 1 - 1
test/models/user.test.js

@@ -33,7 +33,7 @@ describe('User', function () {
 
         User.findUsersByPartOfEmail('ao', {})
         .then(function(userData) {
-          expect(userData).to.be.a('array');
+          expect(userData).to.instanceof(Array);
           expect(userData[0]).to.instanceof(User);
           expect(userData[0].email).to.equal('aoi@example.com');
           done();

+ 7 - 2
wercker.yml

@@ -58,11 +58,16 @@ build-prod:
       code: yarn list --depth=0
 
     - script:
-      name: npm run build:prod
+      name: npm run build:prod:analyze
       code: |
-        npm run build:prod
+        npm run build:prod:analyze
 
   after-steps:
+    - script:
+      name: copy report to artifacts
+      code: |
+        cp -r report $WERCKER_REPORT_ARTIFACTS_DIR
+
     - slack-notifier:
       url: $SLACK_WEBHOOK_URL
       channel: ci

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


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