Przeglądaj źródła

Merge branch 'master' of https://github.com/crowi/crowi into list_private_page

Takeru Chuganji 9 lat temu
rodzic
commit
06af847551
70 zmienionych plików z 1167 dodań i 713 usunięć
  1. 16 0
      CHANGES.md
  2. 1 2
      README.md
  3. 0 18
      lib/crowi/express-init.js
  4. 0 10
      lib/form/admin/fb.js
  5. 0 1
      lib/form/index.js
  6. 2 2
      lib/form/register.js
  7. 1 1
      lib/models/comment.js
  8. 0 2
      lib/models/config.js
  9. 25 43
      lib/models/user.js
  10. 1 3
      lib/routes/index.js
  11. 58 36
      lib/routes/login.js
  12. 0 4
      lib/routes/logout.js
  13. 17 27
      lib/routes/me.js
  14. 14 1
      lib/routes/page.js
  15. 6 2
      lib/routes/user.js
  16. 22 17
      lib/util/googleAuth.js
  17. 29 3
      lib/util/middlewares.js
  18. 1 1
      lib/util/slack.js
  19. 13 6
      lib/util/swigFunctions.js
  20. 1 1
      lib/views/_form.html
  21. 6 34
      lib/views/admin/app.html
  22. 3 3
      lib/views/admin/notification.html
  23. 1 1
      lib/views/admin/search.html
  24. 8 8
      lib/views/admin/users.html
  25. 1 1
      lib/views/installer.html
  26. 1 1
      lib/views/invited.html
  27. 7 2
      lib/views/layout/2column.html
  28. 8 24
      lib/views/layout/layout.html
  29. 0 1
      lib/views/layout/single.html
  30. 14 30
      lib/views/login.html
  31. 0 72
      lib/views/me/index.html
  32. 52 0
      lib/views/modal/create_page.html
  33. 2 2
      lib/views/modal/delete.html
  34. 1 1
      lib/views/modal/help.html
  35. 28 0
      lib/views/modal/page_name_warning.html
  36. 2 2
      lib/views/modal/rename.html
  37. 2 2
      lib/views/modal/unportalize.html
  38. 1 1
      lib/views/modal/what_is_portal.html
  39. 0 28
      lib/views/modal/widget_today_memo.html
  40. 17 12
      lib/views/page.html
  41. 13 7
      lib/views/page_list.html
  42. 3 3
      lib/views/widget/page_list.html
  43. 1 1
      lib/views/widget/page_side_content.html
  44. 2 2
      lib/views/widget/page_side_header.html
  45. 6 7
      package.json
  46. 1 0
      public/emoji_images
  47. 1 0
      resource/css/_comment.scss
  48. 2 2
      resource/css/_form.scss
  49. 4 7
      resource/css/_layout.scss
  50. 1 1
      resource/css/_page.scss
  51. 36 2
      resource/css/_wiki.scss
  52. 56 6
      resource/css/crowi.scss
  53. 16 0
      resource/js/app.js
  54. 12 19
      resource/js/components/HeaderSearchBox.js
  55. 2 33
      resource/js/components/Page/PageBody.js
  56. 13 20
      resource/js/components/SearchPage.js
  57. 2 2
      resource/js/components/SearchPage/SearchResult.js
  58. 2 1
      resource/js/components/SearchPage/SearchResultList.js
  59. 0 3
      resource/js/components/User/UserPicture.js
  60. 28 3
      resource/js/crowi-form.js
  61. 128 183
      resource/js/crowi.js
  62. 152 0
      resource/js/util/Crowi.js
  63. 122 0
      resource/js/util/CrowiRenderer.js
  64. 64 0
      resource/js/util/LangProcessor/Template.js
  65. 84 0
      resource/js/util/LangProcessor/Tsv2Table.js
  66. 14 0
      resource/js/util/PreProcessor/Emoji.js
  67. 9 0
      resource/js/util/PreProcessor/ImageExpander.js
  68. 10 0
      resource/js/util/PreProcessor/Linker.js
  69. 10 0
      resource/js/util/PreProcessor/MarkdownFixer.js
  70. 12 6
      test/utils.js

+ 16 - 0
CHANGES.md

@@ -1,6 +1,22 @@
 CHANGES
 ========
 
+## 1.5.0
+
+* Feature: Search.
+* Feature: CSRF protection.
+* Feature: Page deletion.
+* Feature: Emoji.
+* Feature: TSV parser for code block.
+* Feature: Page teamplte builder.
+* Feature: Preview scroll sync.
+* Improve: Page header highlighting.
+* Improve: Changed icons and colors of for popular pages on page list.
+* Improve: New page dialog.
+* Fix: Couldn't create some page name like `/meeting` (Thank you @kazsw #100).
+* Removed Feature: Facebook login feature is now removed.
+* And some fixes. (Thank you @suzuki @xcezx)
+
 ## 1.4.0
 
 * Feature: Slack integration.

+ 1 - 2
README.md

@@ -4,7 +4,7 @@ Crowi - The Simple & Powerful Communication Tool Based on Wiki
 ================================================================
 
 
-[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/crowi/crowi/tree/v1.4.0)
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/crowi/crowi/tree/v1.5.0)
 
 [![Circle CI](https://circleci.com/gh/crowi/crowi.svg?style=svg)](https://circleci.com/gh/crowi/crowi)
 [![Join the chat at https://gitter.im/crowi/general](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/crowi/general)
@@ -40,7 +40,6 @@ Dependencies
 * Elasticsearch (optional)
 * Redis (optional)
 * Amazon S3 (optional)
-* Facebook Application (optional)
 * Google Project (optional)
 * Slack App (optional)
 

+ 0 - 18
lib/crowi/express-init.js

@@ -18,7 +18,6 @@ module.exports = function(crowi, app) {
 
   app.use(function(req, res, next) {
     var now = new Date()
-      , fbparams = {}
       , baseUrl
       , config = crowi.getConfig()
       , tzoffset = -(config.crowi['app:timezone'] || 9) * 60 // for datez
@@ -40,7 +39,6 @@ module.exports = function(crowi, app) {
     res.locals.env      = env;
     res.locals.now      = now;
     res.locals.tzoffset = tzoffset;
-    res.locals.facebook = {appId: config.crowi['facebook:appId'] || ''};
     res.locals.consts   = {
         pageGrants: Page.getGrantLabels(),
         userStatus: User.getUserStatusLabels(),
@@ -63,22 +61,6 @@ module.exports = function(crowi, app) {
     }
   });
 
-  // Register Facebook middleware
-  app.use(function(req, res, next) {
-    var config = crowi.getConfig()
-      , facebook = require('facebook-node-sdk')
-      ;
-
-    if (config.crowi['facebook:appId'] && config.crowi['facebook:secret']) {
-      return facebook.middleware({
-        appId: config.crowi['facebook:appId'],
-        secret: config.crowi['facebook:secret']
-      })(req, res, next);
-    } else {
-      return next();
-    }
-  });
-
   app.set('port', crowi.port);
   app.use(express.static(crowi.publicDir));
   app.engine('html', cons.swig);

+ 0 - 10
lib/form/admin/fb.js

@@ -1,10 +0,0 @@
-'use strict';
-
-var form = require('express-form')
-  , field = form.field;
-
-module.exports = form(
-  field('settingForm[facebook:appId]').trim().is(/^\d+$/),
-  field('settingForm[facebook:secret]').trim().is(/^[\da-z]+$/)
-);
-

+ 0 - 1
lib/form/index.js

@@ -14,7 +14,6 @@ exports.admin = {
   mail: require('./admin/mail'),
   aws: require('./admin/aws'),
   google: require('./admin/google'),
-  fb: require('./admin/fb'),
   userInvite: require('./admin/userInvite'),
   slackSetting: require('./admin/slackSetting'),
 };

+ 2 - 2
lib/form/register.js

@@ -8,6 +8,6 @@ module.exports = form(
   field('registerForm.name').required(),
   field('registerForm.email').required(),
   field('registerForm.password').required().is(/^[\x20-\x7F]{6,40}$/),
-  field('registerForm.fbId').isInt(),
-  field('registerForm.googleId').isInt()
+  field('registerForm.googleId'),
+  field('registerForm.googleImage')
 );

+ 1 - 1
lib/models/comment.js

@@ -2,7 +2,7 @@ module.exports = function(crowi) {
   var debug = require('debug')('crowi:models:comment')
     , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
-    , USER_PUBLIC_FIELDS = '_id fbId image googleId name username email status createdAt' // TODO: どこか別の場所へ...
+    , USER_PUBLIC_FIELDS = '_id image googleId name username email status createdAt' // TODO: どこか別の場所へ...
     , commentSchema
   ;
 

+ 0 - 2
lib/models/config.js

@@ -42,8 +42,6 @@ module.exports = function(crowi) {
       'google:clientId'     : '',
       'google:clientSecret' : '',
 
-      'facebook:appId'  : '',
-      'facebook:secret' : '',
     };
   }
 

+ 25 - 43
lib/models/user.js

@@ -11,7 +11,7 @@ module.exports = function(crowi) {
     , STATUS_SUSPENDED  = 3
     , STATUS_DELETED    = 4
     , STATUS_INVITED    = 5
-    , USER_PUBLIC_FIELDS = '_id fbId image googleId name username email status createdAt' // TODO: どこか別の場所へ...
+    , USER_PUBLIC_FIELDS = '_id image googleId name username email status createdAt' // TODO: どこか別の場所へ...
 
     , PAGE_ITEMS        = 20
 
@@ -21,7 +21,6 @@ module.exports = function(crowi) {
 
   userSchema = new mongoose.Schema({
     userId: String,
-    fbId: String, // userId
     image: String,
     googleId: String,
     name: { type: String },
@@ -137,17 +136,6 @@ module.exports = function(crowi) {
     return this.updateImage(null, callback);
   };
 
-  userSchema.methods.updateFacebookId = function(fbId, callback) {
-    this.fbId = this.userId = fbId;
-    this.save(function(err, userData) {
-      return callback(err, userData);
-    });
-  };
-
-  userSchema.methods.deleteFacebookId = function(callback) {
-    return this.updateFacebookId(null, callback);
-  };
-
   userSchema.methods.updateGoogleId = function(googleId, callback) {
     this.googleId = googleId;
     this.save(function(err, userData) {
@@ -218,16 +206,14 @@ module.exports = function(crowi) {
     this.password = '';
     this.email = 'deleted@deleted';
     this.googleId = null;
-    this.fbId = null;
     this.image = null;
     this.save(function(err, userData) {
       return callback(err, userData);
     });
   };
 
-  userSchema.methods.updateGoogleIdAndFacebookId = function(googleId, facebookId, callback) {
+  userSchema.methods.updateGoogleId = function(googleId, callback) {
     this.googleId = googleId;
-    this.fbId = this.userId = facebookId;
     this.save(function(err, userData) {
       return callback(err, userData);
     });
@@ -271,6 +257,29 @@ module.exports = function(crowi) {
 
   };
 
+  userSchema.statics.findAllUsers = function(option) {
+    var User = this;
+    var option = option || {}
+      , sort = option.sort || {createdAt: -1}
+      , status = option.status || STATUS_ACTIVE
+      , fields = option.fields || USER_PUBLIC_FIELDS
+      ;
+
+    return new Promise(function(resolve, reject) {
+      User
+        .find({status: status })
+        .select(fields)
+        .sort(sort)
+        .exec(function (err, userData) {
+          if (err) {
+            return reject(err);
+          }
+
+          return resolve(userData);
+        });
+    });
+  };
+
   userSchema.statics.findUsersByIds = function(ids, option) {
     var User = this;
     var option = option || {}
@@ -344,12 +353,6 @@ module.exports = function(crowi) {
     });
   };
 
-  userSchema.statics.findUserByFacebookId = function(fbId, callback) {
-    this.findOne({userId: fbId}, function (err, userData) {
-      callback(err, userData);
-    });
-  };
-
   userSchema.statics.findUserByGoogleId = function(googleId, callback) {
     this.findOne({googleId: googleId}, function (err, userData) {
       callback(err, userData);
@@ -548,27 +551,6 @@ module.exports = function(crowi) {
     });
   };
 
-  userSchema.statics.createUserByFacebook = function(fbUserInfo, callback) {
-    var User = this
-      , newUser = new User();
-
-    newUser.userId = fbUserInfo.id;
-    newUser.image = '//graph.facebook.com/' + fbUserInfo.id + '/picture?size=square';
-    newUser.name = fbUserInfo.name || '';
-    newUser.username = fbUserInfo.username || '';
-    newUser.email = fbUserInfo.email || '';
-    newUser.createdAt = Date.now();
-    newUser.status = decideUserStatusOnRegistration();
-
-    newUser.save(function(err, userData) {
-      if (userData.status == STATUS_ACTIVE) {
-        userEvent.emit('activated', userData);
-      }
-      return callback(err, userData);
-    });
-  };
-
-
   userSchema.statics.createUserPictureFilePath = function(user, name) {
     var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
 

+ 1 - 3
lib/routes/index.js

@@ -34,7 +34,6 @@ module.exports = function(crowi, app) {
   app.post('/register/google'        , login.registerGoogle);
   app.get('/google/callback'         , login.googleCallback);
   app.get('/login/google'            , login.loginGoogle);
-  app.get('/login/facebook'          , login.loginFacebook);
   app.get('/logout'                  , logout.logout);
 
   app.get('/admin'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
@@ -44,7 +43,6 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
   app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
   app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.google, admin.api.appSetting);
-  app.post('/_api/admin/settings/fb'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.fb , admin.api.appSetting);
 
   // search admin
   app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
@@ -73,7 +71,6 @@ module.exports = function(crowi, app) {
   app.post('/me/password'             , form.me.password          , loginRequired(crowi, app) , me.password);
   app.post('/me/apiToken'             , form.me.apiToken          , loginRequired(crowi, app) , me.apiToken);
   app.post('/me/picture/delete'       , loginRequired(crowi, app) , me.deletePicture);
-  app.post('/me/auth/facebook'        , loginRequired(crowi, app) , me.authFacebook);
   app.post('/me/auth/google'          , loginRequired(crowi, app) , me.authGoogle);
   app.get( '/me/auth/google/callback' , loginRequired(crowi, app) , me.authGoogleCallback);
 
@@ -95,6 +92,7 @@ module.exports = function(crowi, app) {
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/users.list'          , accessTokenParser(crowi, app) , loginRequired(crowi, app) , user.api.list);
+  app.post('/_api/pages.create'        , accessTokenParser(crowi, app) , loginRequired(crowi, app) , csrf, page.api.create);
   app.get('/_api/pages.get'           , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.get);
   app.get('/_api/pages.updatePost'    , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.getUpdatePost);
   app.post('/_api/pages.seen'         , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.seen);

+ 58 - 36
lib/routes/login.js

@@ -12,12 +12,23 @@ module.exports = function(crowi, app) {
     , Revision = crowi.model('Revision')
     , actions = {};
 
+
+  var clearGoogleSession = function(req) {
+      req.session.googleAuthCode
+        = req.session.googleId
+        = req.session.googleEmail
+        = req.session.googleName
+        = req.session.googleImage
+        = null;
+  };
   var loginSuccess = function(req, res, userData) {
     req.user = req.session.user = userData;
     if (!userData.password) {
       return res.redirect('/me/password');
     }
 
+    clearGoogleSession(req);
+
     var jumpTo = req.session.jumpTo;
     if (jumpTo) {
       req.session.jumpTo = null;
@@ -116,26 +127,6 @@ module.exports = function(crowi, app) {
     }
   };
 
-  actions.loginFacebook = function(req, res) {
-    var facebook = req.facebook;
-
-    facebook.getUser(function(err, fbId) {
-      if (err || !fbId) {
-        req.user = req.session.user = false;
-        return res.redirect('/login');
-      }
-
-      User.findUserByFacebookId(fbId, function(err, userData) {
-        debug('on login findUserByFacebookId', err, userData);
-        if (userData) {
-          return loginSuccess(req, res, userData);
-        } else {
-          return loginFailure(req, res);
-        }
-      });
-    });
-  };
-
   actions.register = function(req, res) {
     var googleAuth = require('../util/googleAuth')(config);
 
@@ -156,8 +147,8 @@ module.exports = function(crowi, app) {
       var username = registerForm.username;
       var email = registerForm.email;
       var password = registerForm.password;
-      var facebookId = registerForm.fbId || null;
       var googleId = registerForm.googleId || null;
+      var googleImage = registerForm.googleImage || null;
 
       // email と username の unique チェックする
       User.isRegisterable(email, username, function (isRegisterable, errOn) {
@@ -178,14 +169,14 @@ module.exports = function(crowi, app) {
 
         }
         if (isError) {
-          return res.render('login', {
-          });
+          debug('isError user register error', errOn);
+          return res.redirect('/register');
         }
 
         User.createUserByEmailAndPassword(name, username, email, password, function(err, userData) {
           if (err) {
             req.flash('registerWarningMessage', 'ユーザー登録に失敗しました。');
-            return res.redirect('/login?register=1');
+            return res.redirect('/register');
           } else {
 
             // 作成後、承認が必要なモードなら、管理者に通知する
@@ -219,30 +210,65 @@ module.exports = function(crowi, app) {
               });
             }
 
-            if (facebookId || googleId) {
-              userData.updateGoogleIdAndFacebookId(googleId, facebookId, function(err, userData) {
+            if (googleId) {
+              userData.updateGoogleId(googleId, function(err, userData) {
                 if (err) { // TODO
                 }
                 return loginSuccess(req, res, userData);
               });
+
+              if (googleImage) {
+                var axios = require('axios');
+                var fileUploader = require('../util/fileUploader')(crowi, app);
+                var filePath = User.createUserPictureFilePath(
+                  userData,
+                  googleImage.replace(/^.+\/(.+\..+)$/, '$1')
+                );
+
+                axios.get(googleImage, {responseType: 'stream'})
+                .then(function(response) {
+                  var type = response.headers['content-type'];
+                  var fileStream = response.data;
+                  fileStream.length = parseInt(response.headers['content-length']);
+
+                  fileUploader.uploadFile(filePath, type, fileStream, {})
+                  .then(function(data) {
+                    var imageUrl = fileUploader.generateUrl(filePath);
+                    debug('user picture uploaded', imageUrl);
+                    userData.updateImage(imageUrl, function(err, data) {
+                      if (err) {
+                        debug('Error on update user image', err);
+                      }
+                      // DONE
+                    });
+                  }).catch(function (err) { // ignore
+                    debug('Upload error', err);
+                  });
+                }).catch(function() { // ignore
+                });
+              }
             } else {
               return loginSuccess(req, res, userData);
             }
           }
         });
       });
-    } else { // method GET
+    } else { // method GET of form is not valid
+      debug('session is', req.session);
+      var isRegistering = true;
       // google callback を受ける可能性もある
       var code = req.session.googleAuthCode || null;
       var googleId = req.session.googleId || null;
       var googleEmail = req.session.googleEmail || null;
+      var googleName = req.session.googleName || null;
+      var googleImage = req.session.googleImage || null;
 
       debug('register. if code', code);
       // callback 経由で reigster にアクセスしてきた時最初だけこの if に入る
       // code から email などを取得したらそれを session にいれて code は消去
       if (code) {
         googleAuth.handleCallback(req, function(err, tokenInfo) {
-          debug('tokenInfo', tokenInfo);
+          debug('tokenInfo on register GET', tokenInfo);
           req.session.googleAuthCode = null;
 
           if (err) {
@@ -252,21 +278,17 @@ module.exports = function(crowi, app) {
 
           req.session.googleId = googleId = tokenInfo.user_id;
           req.session.googleEmail = googleEmail = tokenInfo.email;
+          req.session.googleName = googleName = tokenInfo.name;
+          req.session.googleImage = googleImage = tokenInfo.picture;
 
           if (!User.isEmailValid(googleEmail)) {
             req.flash('registerWarningMessage', 'このメールアドレスのGoogleアカウントはコネクトできません。');
             return res.redirect('/login?register=1');
           }
-          return res.render('login', {
-            googleId: googleId,
-            googleEmail: googleEmail,
-          });
+          return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
         });
       } else {
-        return res.render('login', {
-          googleId: googleId,
-          googleEmail: googleEmail,
-        });
+        return res.render('login', { isRegistering, googleId, googleEmail, googleName, googleImage, });
       }
     }
   };

+ 0 - 4
lib/routes/logout.js

@@ -2,10 +2,6 @@ module.exports = function(crowi, app) {
   return {
     logout: function(req, res) {
 
-      if (req.facebook) {
-        req.facebook.destroySession();
-      }
-
       req.session.destroy();
       return res.redirect('/');
     }

+ 17 - 27
lib/routes/me.js

@@ -235,36 +235,26 @@ module.exports = function(crowi, app) {
         req.flash('warningMessage.auth.google', 'このメールアドレスのGoogleアカウントはコネクトできません。');
         return res.redirect('/me');
       }
-      userData.updateGoogleId(googleId, function(err, userData) {
-        // TODO if err
-        req.flash('successMessage', 'Googleコネクトを設定しました。');
-        return res.redirect('/me');
-      });
-    });
-  };
-
-
-  actions.authFacebook = function(req, res) {
-    var userData = req.user;
-
-    var toDisconnect = req.body.disconnectFacebook ? true : false;
-    var fbId = req.body.fbId || 0;
-
-    if (toDisconnect) {
-      userData.deleteFacebookId(function(err, userData) {
-        req.flash('successMessage', 'Facebookコネクトを解除しました。');
 
-        return res.redirect('/me');
-      });
-    } else if (fbId) {
-      userData.updateFacebookId(fbId, function(err, userData) {
-        req.flash('successMessage', 'Facebookコネクトを設定しました。');
+      User.findUserByGoogleId(googleId, function(err, googleUser) {
+        if (!err && googleUser) {
+          req.flash('warningMessage.auth.google', 'このGoogleアカウントは他のユーザーがコネクト済みです。');
+          return res.redirect('/me');
+        } else {
+          userData.updateGoogleId(googleId, function(err, userData) {
+            if (err) {
+              debug('Failed to updateGoogleId', err);
+              req.flash('warningMessage.auth.google', 'Failed to connect Google Account');
+              return res.redirect('/me');
+            }
 
-        return res.redirect('/me');
+            // TODO if err
+            req.flash('successMessage', 'Googleコネクトを設定しました。');
+            return res.redirect('/me');
+          });
+        }
       });
-    } else {
-      return res.redirect('/me');
-    }
+    });
   };
 
   return actions;

+ 14 - 1
lib/routes/page.js

@@ -323,7 +323,8 @@ module.exports = function(crowi, app) {
 
           var fixed = Page.fixToCreatableName(path)
           if (fixed !== path) {
-            res.redirect(fixed);
+            debug('fixed page name', fixed)
+            res.redirect(encodeURI(fixed));
             return ;
           }
 
@@ -518,6 +519,18 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * @api {post} /pages.create Create new page
+   * @apiName CreatePage
+   * @apiGroup Page
+   *
+   * @apiParam {String} body
+   * @apiParam {String} path
+   * @apiParam {String} revision_id
+   */
+  api.create = function(req, res){
+  };
+
   /**
    * @api {get} /pages.get Get page data
    * @apiName GetPage

+ 6 - 2
lib/routes/user.js

@@ -45,11 +45,15 @@ module.exports = function(crowi, app) {
    */
   api.list = function(req, res) {
     var userIds = req.query.user_ids || null; // TODO: handling
+
+    var userFetcher;
     if (!userIds || userIds.split(',').length <= 0) {
-      return res.json(ApiResponse.error('user_ids param is required'));
+      userFetcher = User.findAllUsers()
+    } else {
+      userFetcher = User.findUsersByIds(userIds.split(','))
     }
 
-    User.findUsersByIds(userIds.split(','))
+    userFetcher
     .then(function(userList) {
       var result = {
         users: userList,

+ 22 - 17
lib/util/googleAuth.js

@@ -5,13 +5,13 @@
 module.exports = function(config) {
   'use strict';
 
-  var googleapis = require('googleapis')
+  var google = require('googleapis')
     , debug = require('debug')('crowi:lib:googleAuth')
     , lib = {}
     ;
 
   function createOauth2Client(url) {
-    return new googleapis.auth.OAuth2Client(
+    return new google.auth.OAuth2(
       config.crowi['google:clientId'],
       config.crowi['google:clientSecret'],
       url
@@ -20,11 +20,12 @@ module.exports = function(config) {
 
   lib.createAuthUrl = function(req, callback) {
     var callbackUrl = config.crowi['app:url'] + '/google/callback';
-    var google = createOauth2Client(callbackUrl);
+    var oauth2Client = createOauth2Client(callbackUrl);
+    google.options({auth: oauth2Client});
 
-    var redirectUrl = google.generateAuthUrl({
+    var redirectUrl = oauth2Client.generateAuthUrl({
       access_type: 'offline',
-      scope: 'https://www.googleapis.com/auth/userinfo.email',
+      scope: ['profile', 'email'],
     });
 
     callback(null, redirectUrl);
@@ -32,31 +33,35 @@ module.exports = function(config) {
 
   lib.handleCallback = function(req, callback) {
     var callbackUrl = config.crowi['app:url'] + '/google/callback';
-    var google = createOauth2Client(callbackUrl);
+    var oauth2Client = createOauth2Client(callbackUrl);
+    google.options({auth: oauth2Client});
+
     var code = req.session.googleAuthCode || null;
 
     if (!code) {
       return callback(new Error('No code exists.'), null);
     }
 
-    google.getToken(code, function(err, tokens) {
+    debug('Request googleToken by auth code', code);
+    oauth2Client.getToken(code, function(err, tokens) {
+      debug('Result of google.getToken()', err, tokens);
       if (err) {
         return callback(new Error('[googleAuth.handleCallback] Error to get token.'), null);
       }
 
-      googleapis.discover('oauth2', 'v1').withOpts({cache: { path: __dirname + '/../../tmp/googlecache'}}).execute(function(err, client) {
+      oauth2Client.setCredentials({
+        access_token: tokens.access_token,
+      });
+
+      var oauth2 = google.oauth2('v2');
+      oauth2.userinfo.get({}, function(err, response) {
+        debug('Response of oauth2.userinfo.get', err, response);
         if (err) {
-          return callback(new Error('[googleAuth.handleCallback] Failed to discover oauth2 API endpoint.'), null);
+          return callback(new Error('[googleAuth.handleCallback] Error while proceccing userinfo.get.'), null);
         }
 
-        var tokeninfo = client.oauth2.tokeninfo({id_token: tokens.id_token});
-        tokeninfo.execute(function(err, response) {
-          if (err) {
-            return callback(new Error('[googleAuth.handleCallback] Error while proceccing tokeninfo.'), null);
-          }
-
-          return callback(null, response);
-        });
+        response.user_id = response.id; // This is for B.C. (tokeninfo をつかっている前提のコードに対してのもの)
+        return callback(null, response);
       });
     });
   };

+ 29 - 3
lib/util/middlewares.js

@@ -70,6 +70,29 @@ exports.swigFilters = function(app, swig) {
       return name.replace(/.+\/(.+)?$/, '$1'); // ページの末尾を拾う
     });
 
+    swig.setFilter('normalizeDateInPath', function(path) {
+      var patterns = [
+        [/20(\d{2})(\d{2})(\d{2})(.+)/g, '20$1/$2/$3/$4'],
+        [/20(\d{2})(\d{2})(\d{2})/g, '20$1/$2/$3'],
+        [/20(\d{2})(\d{2})(.+)/g, '20$1/$2/$3'],
+        [/20(\d{2})(\d{2})/g, '20$1/$2'],
+        [/20(\d{2})_(\d{1,2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3/$4'],
+        [/20(\d{2})_(\d{1,2})_(\d{1,2})/g, '20$1/$2/$3'],
+        [/20(\d{2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3'],
+        [/20(\d{2})_(\d{1,2})/g, '20$1/$2'],
+      ];
+
+      for (var i = 0; i < patterns.length ; i++) {
+        var mat = patterns[i][0];
+        var rep = patterns[i][1];
+        if (path.match(mat)) {
+          return path.replace(mat, rep);
+        }
+      }
+
+      return path;
+    });
+
     swig.setFilter('datetz', function(input, format) {
       // timezone
       var swigFilters = require('swig/lib/filters');
@@ -109,11 +132,8 @@ exports.swigFilters = function(app, swig) {
         return '';
       }
 
-      user.fbId = user.userId; // migration
       if (user.image && user.image != '/images/userpicture.png') {
         return user.image;
-      } else if (user.fbId) {
-        return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
       } else {
         return '/images/userpicture.png';
       }
@@ -153,6 +173,12 @@ exports.loginRequired = function(crowi, app) {
       }
     }
 
+    // is api path
+    var path = req.path || '';
+    if (path.match(/^\/_api\/.+$/)) {
+      return res.sendStatus(403);
+    }
+
     req.session.jumpTo = req.originalUrl;
     return res.redirect('/login');
   };

+ 1 - 1
lib/util/slack.js

@@ -160,7 +160,7 @@ module.exports = function(crowi) {
       author_link: url + '/user/' + user.username,
       author_icon: user.image,
       title: page.path,
-      title_link: url + page.path,
+      title_link: url + '/' + page._id,
       text: body,
       mrkdwn_in: ["text"],
     };

+ 13 - 6
lib/util/swigFunctions.js

@@ -6,15 +6,10 @@ module.exports = function(crowi, app, req, locals) {
   ;
 
   // token getter
-  locals._csrf = function() {
+  locals.csrf = function() {
     return req.csrfToken;
   };
 
-  locals.facebookLoginEnabled = function() {
-    var config = crowi.getConfig()
-    return config.crowi['facebook:appId'] && config.crowi['facebook:secret'];
-  };
-
   locals.googleLoginEnabled = function() {
     var config = crowi.getConfig()
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
@@ -40,6 +35,18 @@ module.exports = function(crowi, app, req, locals) {
     return Config.isUploadable(config);
   };
 
+  locals.parentPath = function(path) {
+    if (path == '/') {
+      return path;
+    }
+
+    if (path.match(/.+\/$/)) {
+      return path;
+    }
+
+    return path + '/';
+  };
+
   locals.isUserPageList = function(path) {
     if (path.match(/^\/user\/[^\/]+\/$/)) {
       return true;

+ 1 - 1
lib/views/_form.html

@@ -49,7 +49,7 @@
           {% endfor %}
         </select>
         {% endif %}
-        <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
         <input type="submit" class="btn btn-primary" id="edit-form-submit" value="ページを更新" />
       </div>
     </div>

+ 6 - 34
lib/views/admin/app.html

@@ -54,7 +54,7 @@
 
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">更新</button>
           </div>
         </div>
@@ -106,7 +106,7 @@
 
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">更新</button>
           </div>
         </div>
@@ -151,7 +151,7 @@
 
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">更新</button>
           </div>
         </div>
@@ -200,7 +200,7 @@
 
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">更新</button>
           </div>
         </div>
@@ -229,7 +229,7 @@
 
         <div class="form-group">
           <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
             <button type="submit" class="btn btn-primary">更新</button>
           </div>
         </div>
@@ -237,41 +237,13 @@
       </fieldset>
       </form>
 
-      <form action="/_api/admin/settings/fb" method="post" class="form-horizontal" id="fbSettingForm" role="form">
-      <fieldset>
-      <legend>Facebook 設定</legend>
-        <p class="well">Facebook アプリケーションの設定をすると、Facebook にコネクトして登録やログインが可能になります。</p>
-
-        <div class="form-group">
-          <label for="settingForm[facebook:appId]" class="col-xs-3 control-label">facebook ID</label>
-          <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[facebook:appId]" value="{{ settingForm['facebook:appId'] }}">
-          </div>
-        </div>
-
-        <div class="form-group">
-          <label for="settingForm[facebook:secret]" class="col-xs-3 control-label">Secret</label>
-          <div class="col-xs-6">
-            <input class="form-control" type="text" name="settingForm[facebook:secret]" value="{{ settingForm['facebook:secret'] }}">
-          </div>
-        </div>
-
-        <div class="form-group">
-          <div class="col-xs-offset-3 col-xs-6">
-            <input type="hidden" name="_csrf" value="{{ _csrf() }}">
-            <button type="submit" class="btn btn-primary">更新</button>
-          </div>
-        </div>
-
-      </fieldset>
-      </form>
     </div>
   </div>
 
   <script>
     $(function()
     {
-      $('#appSettingForm, #secSettingForm, #mailSettingForm, #awsSettingForm, #googleSettingForm, #fbSettingForm').each(function() {
+      $('#appSettingForm, #secSettingForm, #mailSettingForm, #awsSettingForm, #googleSettingForm').each(function() {
         $(this).submit(function()
         {
           function showMessage(formId, msg, status) {

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

@@ -65,7 +65,7 @@
           </div>
         </div>
       </fieldset>
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       </form>
 
       {% if hasSlackConfig %}
@@ -109,7 +109,7 @@
               </p>
             </td>
             <td>
-              <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="submit" value="Add" class="btn btn-primary">
             </td>
           </tr>
@@ -126,7 +126,7 @@
             <td>
               <form class="admin-remove-updatepost">
                 <input type="hidden" name="id" value="{{ notif._id.toString() }}">
-                <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
                 <input type="submit" value="Delete" class="btn btn-default">
               </form>
             </td>

+ 1 - 1
lib/views/admin/search.html

@@ -51,7 +51,7 @@
           </div>
         </div>
       </fieldset>
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       </form>
 
     </div>

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

@@ -48,7 +48,7 @@
           </div>
           <button type="submit" class="btn btn-primary">招待する</button>
         </div>
-        <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
       </form>
 
       {% set createdUser = req.flash('createdUser') %}
@@ -127,33 +127,33 @@
                   <li class="dropdown-button">
                   {% if sUser.status == 1 %}
                   <form action="/admin/user/{{ sUser._id.toString() }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-info">承認する</button>
                   </form>
                   {% endif  %}
                   {% if sUser.status == 2 %}
                   <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
-                    <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-warning">アカウント停止</button>
                   </form>
                   {% endif  %}
                   {% if sUser.status == 3 %}
                   <form action="/admin/user/{{ sUser._id.toString() }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-default">元に戻す</button>
                   </form>
                   </li>
                   <li class="dropdown-button">
                   {# label は同じだけど、こっちは論理削除 #}
                   <form action="/admin/user/{{ sUser._id.toString() }}/remove" method="post">
-                    <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-danger">削除する</button>
                   </form>
                   {% endif  %}
                   {% if sUser.status == 5 %}
                   {# label は同じだけど、こっちは物理削除 #}
                   <form action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
-                    <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-danger">削除する</button>
                   </form>
                   {% endif  %}
@@ -167,7 +167,7 @@
                     {% if sUser.admin %}
                       {% if sUser.username != user.username %}
                       <form action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
-                        <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                        <input type="hidden" name="_csrf" value="{{ csrf() }}">
                         <button type="submit" class="btn btn-block btn-danger">管理者からはずす</button>
                       </form>
                       {% else %}
@@ -175,7 +175,7 @@
                       {% endif %}
                     {% else %}
                       <form action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                        <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+                        <input type="hidden" name="_csrf" value="{{ csrf() }}">
                         <button type="submit" class="btn btn-block btn-primary">管理者にする</button>
                       </form>
                     {% endif %}

+ 1 - 1
lib/views/installer.html

@@ -65,7 +65,7 @@
       パスワードは6文字以上の半角英数字または記号
       </p>
 
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       <input type="submit" class="btn btn-primary btn-lg btn-block" value="作成">
     </form>
 

+ 1 - 1
lib/views/invited.html

@@ -80,7 +80,7 @@
       パスワードは6文字以上の半角英数字または記号
       </p>
 
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       <input type="submit" class="btn btn-primary btn-lg btn-block" value="登録を完了">
     </form>
 

+ 7 - 2
lib/views/layout/2column.html

@@ -24,7 +24,6 @@
     </footer>
   </div>
 </aside>
-{% include '../modal/widget_help.html' %}
 
 {% endblock %} {# layout_sidebar #}
 
@@ -32,7 +31,13 @@
 <div id="main" class="main col-md-9 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
   {% if page && page.grant != 1 %}
   <p class="page-grant">
-  <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} (このページの閲覧は制限されています)
+    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} (このページの閲覧は制限されています)
+  </p>
+  {% endif %}
+  {% if page && page.grant == 2 %}
+  <p class="alert alert-info">
+    このページの共有用URL
+    <input type="text" class="copy-link form-control" value="{{ baseUrl }}/{{ page._id.toString() }}" readonly>
   </p>
   {% endif %}
   <article>

+ 8 - 24
lib/views/layout/layout.html

@@ -23,25 +23,6 @@
   data-me="{{ user._id.toString() }}"
  {% block html_base_attr %}{% endblock %}
  >
-<div id="fb-root"></div>
-<script>
-  window.fbAsyncInit = function() {
-    FB.init({
-      appId      : '{{ facebook.appId }}', // App ID
-      //channelUrl : '//WWW.YOUR_DOMAIN.COM/channel.html', // Channel File
-      status     : true, // check login status
-      cookie     : true, // enable cookies to allow the server to access the session
-      xfbml      : true  // parse XFBML
-    });
-  };
-
-  (function(d){
-     var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
-     js = d.createElement('script'); js.id = id; js.async = true;
-     js.src = "//connect.facebook.net/en_US/all.js";
-     d.getElementsByTagName('head')[0].appendChild(js);
-   }(document));
-</script>
 
 {% block layout_head_nav %}
 <nav class="crowi-header navbar navbar-default" role="navigation">
@@ -91,17 +72,19 @@
         </a>
       </li>
       #}
+      <li id="" class="dropdown">
+        <button class="btn btn-default create-page-button" data-target="#create-page" data-toggle="modal">
+          <i class="fa fa-pencil"></i> 作成
+        </button>
+      </li>
       <li id="login-user">
         <a href="/user/{{ user.username }}" id="link-mypage">
           <img src="{{ user|picture }}" class="picture picture-rounded" width="25" /> {{ user.name }}
         </a>
       </li>
-      <li><a href="" title="今日のメモを作成" data-target="#createMemo" data-toggle="modal"><i class="fa fa-pencil"></i></a></li>
       <li class="dropdown">
         <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-bars"></i> <label class="sr-only">メニュー</label></a>
         <ul class="dropdown-menu">
-          <li><a href="" data-target="#createMemo" data-toggle="modal"><i class="fa fa-pencil"></i> 今日のメモを作成</a></li>
-          <li class="divider"></li>
           <li><a href="/me"><i class="fa fa-gears"></i> ユーザー設定</a></li>
           <li class="divider"></li>
           <li><a href="/trash/"><i class="fa fa-trash-o"></i> 削除済みページ</a></li>
@@ -122,7 +105,7 @@
     </ul>
   </div><!-- /.navbar-collapse -->
 </nav>
-{% include '../modal/widget_today_memo.html' %}
+{% include '../modal/create_page.html' %}
 {% endblock  %} {# layout_head_nav #}
 
 <div class="container-fluid">
@@ -143,9 +126,10 @@
 {% block body_end %}
 {% endblock %}
 
+{% include '../modal/help.html' %}
 </body>
 {% endblock %}
 
-<script src="/js/app{% if env  == 'production' %}.min{% endif %}.js"></script>
 <script src="/js/crowi{% if env  == 'production' %}.min{% endif %}.js"></script>
+<script src="/js/app{% if env  == 'production' %}.min{% endif %}.js"></script>
 </html>

+ 0 - 1
lib/views/layout/single.html

@@ -24,5 +24,4 @@
     &copy; {{ now|date('Y') }} {{ config.crowi['app:title'] }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
   </footer>
 </div>
-{% include '../modal/widget_help.html' %}
 {% endblock %}

+ 14 - 30
lib/views/login.html

@@ -15,7 +15,7 @@
 
 <div class="login-dialog-container flip-container col-md-5">
 
-<div class="login-dialog flipper {% if req.query.register or req.body.registerForm or googleId %}to-flip{% endif %}" id="login-dialog">
+<div class="login-dialog flipper {% if req.query.register or req.body.registerForm or isRegistering or googleId %}to-flip{% endif %}" id="login-dialog">
 
   <div class="login-dialog-inner front">
     <h2>ログイン</h2>
@@ -49,7 +49,7 @@
         <input type="password" class="form-control" placeholder="Password" name="loginForm[password]">
       </div>
 
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       <input type="submit" class="btn btn-primary btn-lg btn-block" value="Login">
     </form>
 
@@ -61,16 +61,7 @@
         <p>Google でログイン</p>
         <form role="form" action="/login/google" method="get">
           <button type="submit" class="btn btn-block btn-google"><i class="fa fa-google-plus-square"></i> Login</button>
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
-        </form>
-      </div>
-      {% endif %}
-      {% if facebookLoginEnabled() %}
-      <div class="col-md-6">
-        <p>Facebook でログイン</p>
-        <form role="form">
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
-          <button type="button" id="btn-login-facebook" class="btn btn-block btn-facebook"><i class="fa fa-facebook-square"></i> Login</button>
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
         </form>
       </div>
       {% endif %}
@@ -95,6 +86,11 @@
 
     {% if googleId %}
     <div class="google-info alert alert-info">
+      {% if googleImage %}
+      <p class="text-center">
+        <img src="{{ googleImage }}" class="picture picture-rounded picture-lg">
+      </p>
+      {% endif %}
       <code>{{ googleEmail }}</code> この Google アカウントで登録します<br>
       ユーザーID、名前、パスワードを決めて登録を継続してください。
     </div>
@@ -122,7 +118,6 @@
     </div>
 
     <form role="form" method="post" action="/register" id="register-form">
-      <input type="hidden" class="form-control" name="registerForm[fbId]" value="{{ req.body.registerForm.fbId }}">
       <input type="hidden" class="form-control" name="registerForm[googleId]" value="{{ googleId|default(req.body.registerForm.googleId) }}">
 
       <label>ユーザーID</label>
@@ -138,7 +133,7 @@
       <label>名前</label>
       <div class="input-group">
         <span class="input-group-addon"><i class="fa fa-user"></i></span>
-        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="registerForm[name]" value="{{ req.body.registerForm.name }}" required>
+        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="registerForm[name]" value="{{ googleName|default(req.body.registerForm.name) }}" required>
       </div>
 
       <label >メールアドレス</label>
@@ -166,7 +161,10 @@
       パスワードは6文字以上の半角英数字または記号
       </p>
 
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      {% if googleImage %}
+        <input type="hidden" name="registerForm[googleImage]" value="{{ googleImage }}">
+      {% endif  %}
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       <input type="submit" class="btn btn-primary btn-lg btn-block" value="新規登録">
     </form>
 
@@ -177,19 +175,11 @@
       <div class="col-md-6">
         <p>Google で登録</p>
         <form role="form" method="post" action="/register/google">
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <button type="submit" class="btn btn-block btn-google"><i class="fa fa-google-plus-square"></i> Login</button>
         </form>
       </div>
       {% endif %}
-      {% if facebookLoginEnabled() %}
-      <div class="col-md-6">
-        <p>Facebook で登録</p>
-        <form role="form">
-          <button type="button" id="btn-register-facebook" class="btn btn-block btn-facebook"><i class="fa fa-facebook-square"></i> Login</button>
-        </form>
-      </div>
-      {% endif %}
     </div>
 
     <p class="bottom-text"><a href="#login" id="login"><i class="fa fa-sign-out"></i> ログインはこちら</a></p>
@@ -200,10 +190,4 @@
 
 </div>
 
-{#
-<div class="login-footer">
-  <p>&copy; {{ now|date('Y') }} {{ config.app.title }}. <a href="" data-target="#helpModal" data-toggle="modal"><i class="fa fa-question-circle"></i> ヘルプ</a></p>
-</div>
-#}
-
 {% endblock %}

+ 0 - 72
lib/views/me/index.html

@@ -110,8 +110,6 @@
             <form action="/me/picture/delete" method="post" class="form-horizontal" role="form" onsubmit="return window.confirm('削除してよろしいですか?');">
               <button type="submit" class="btn btn-danger">画像を削除</button>
             </form>
-            {% elseif user.fbId %}
-            プロフィール画像はFacebookから自動的に設定されています。
             {% endif %}
             </p>
           </div>
@@ -173,76 +171,6 @@
     </script>
 
   <div class="row">
-    {% if facebookLoginEnabled() %}
-    <div class="col-sm-6"> {# Facebook Connect #}
-
-      <div class="form-box">
-        <form action="/me/auth/facebook" method="post" class="form-horizontal" role="form" id="auth-connect-facebook">
-          <fieldset>
-            <legend><i class="fa fa-facebook-square"></i> Facebook設定</legend>
-
-          {% if user.userId %}
-
-          <div class="form-group">
-            <div class="col-sm-12">
-              <p>
-                <a href="//www.facebook.com/{{ user.userId }}"><img src="//graph.facebook.com/{{ user.userId }}/picture?size=square" width="32"> </a>
-                <input type="submit" name="disconnectFacebook" class="btn btn-default" value="接続を解除">
-              </p>
-              <p class="help-block">
-              接続を解除すると、Facebookを利用してのログインができなくなります。<br>
-              解除後はメールアドレスとパスワードでログインすることができます。
-              </p>
-            </div>
-          </div>
-
-          {% else %}
-
-          <div class="form-group">
-            <div class="col-sm-12">
-              <div class="text-center">
-                <input type="hidden" class="form-control" name="fbId">
-                <button type="submit" id="btn-connect-facebook" class="btn btn-facebook">Facebookコネクト</button>
-                <script>
-                  $('#btn-connect-facebook').click(function(e)
-                  {
-                    var afterLogin = function(response) {
-                      if (response.status !== 'connected') {
-                        // TODO
-                      } else {
-                        var authR = response.authResponse;
-                        $('#auth-connect-facebook input[name="fbId"]').val(authR.userID);
-                        $('#auth-connect-facebook').submit();
-                      }
-                    };
-                    FB.getLoginStatus(function(response) {
-                      if (response.status === 'connected') {
-                        afterLogin(response);
-                      } else {
-                        FB.login(function(response) {
-                          afterLogin(response);
-                        }, {scope: 'email'});
-                      }
-                    });
-
-                    return false;
-                  });
-                </script>
-              </div>
-              <p class="help-block">
-              Facebookコネクトをすると、Facebookでログイン可能になります。<br>
-              メールアドレスとパスワードでのログインは引き続きご利用いただけます。
-              </p>
-            </div>
-          </div>
-
-          {% endif %}
-          </div>
-        </fieldset>
-        </form>
-    </div> {# /Facebook Connect #}
-
-    {% endif %}
     {% if googleLoginEnabled() %}
 
     <div class="col-sm-6"> {# Google Connect #}

+ 52 - 0
lib/views/modal/create_page.html

@@ -0,0 +1,52 @@
+<div class="modal create-page" id="create-page">
+  <div class="modal-dialog">
+    <div class="modal-content">
+
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h4 class="modal-title">New Page</h4>
+      </div>
+
+      <div class="modal-body">
+
+        <form class="form-horizontal" id="create-page-today" role="form">
+          <fieldset>
+            <div class="col-xs-12">
+              <h4>今日の◯◯を作成</h4>
+            </div>
+            <div class="col-xs-10">
+              <span class="page-today-prefix">{{ userPageRoot(user) }}/</span>
+              <input type="text" data-prefix="{{ userPageRoot(user) }}/" class="page-today-input1 form-control" value="メモ" id="" name="">
+              <span class="page-today-suffix">/{{ now|datetz('Y/m/d') }}/</span>
+              <input type="text" data-prefix="/{{ now|datetz('Y/m/d') }}/" class="page-today-input2 form-control" id="page-today-input2" name="" placeholder="ページ名を入力(空欄OK)">
+            </div>
+            <div class="col-xs-2">
+              <button type="submit" class="btn btn-primary">作成</button>
+            </div>
+          </fieldset>
+        </form>
+        <hr>
+
+        {% if !isTopPage() %}
+        <form class="form-horizontal" id="create-page-under-tree" role="form">
+          <fieldset>
+            <div class="col-xs-12">
+              <h4><code>{{ parentPath(path) }}</code>以下に作成</h4>
+            </div>
+            <div class="col-xs-10">
+              <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="ページ名を入力" required>
+            </div>
+            <div class="col-xs-2">
+              <button type="submit" class="btn btn-primary">作成</button>
+            </div>
+          </fieldset>
+        </form>
+        <hr>
+        {% endif  %}
+
+      </div><!-- /.modal-body -->
+
+    </div><!-- /.modal-content -->
+  </div><!-- /.modal-dialog -->
+</div><!-- /.modal -->
+

+ 2 - 2
lib/views/modal/widget_delete.html → lib/views/modal/delete.html

@@ -1,4 +1,4 @@
-  <div class="modal fade" id="deletePage">
+  <div class="modal" id="deletePage">
     <div class="modal-dialog">
       <div class="modal-content">
 
@@ -19,7 +19,7 @@
         </div>
         <div class="modal-footer">
           <p><small class="pull-left" id="delete-errors"></small></p>
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <input type="hidden" name="path" value="{{ page.path }}">
           <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
           <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">

+ 1 - 1
lib/views/modal/widget_help.html → lib/views/modal/help.html

@@ -1,4 +1,4 @@
-<div class="modal fade" id="help-modal">
+<div class="modal" id="help-modal">
   <div class="modal-dialog">
     <div class="modal-content">
 

+ 28 - 0
lib/views/modal/page_name_warning.html

@@ -0,0 +1,28 @@
+<div class="modal page-warning-modal" id="page-warning-modal">
+  <div class="modal-dialog">
+    <div class="modal-content">
+
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h4 class="modal-title">ページ名に関するヒント</h4>
+      </div>
+      <div class="modal-body alert alert-danger">
+
+        <strong>Warning!</strong><br>
+
+        <p>
+        スラッシュ <code>/</code> で区切られていない、日付の入ったページ名をつけようとしていませんか?<br>
+        ページ名に日付を入れる場合、スラッシュ <code>/</code> で区切ることが推奨されています。<br>
+        <br>
+        推奨されるページ名で作成する場合、<br>
+        <a href="{{ path|normalizeDateInPath }}">{{ path|normalizeDateInPath }}</a> から作成をはじめてください。
+        </p>
+
+      </div>
+
+      <div class="modal-footer">
+        <a href="{{ path|normalizeDateInPath }}" class="btn btn-primary">Rename!</a>
+      </div>
+    </div>
+  </div>
+</div>

+ 2 - 2
lib/views/modal/widget_rename.html → lib/views/modal/rename.html

@@ -1,4 +1,4 @@
-  <div class="modal fade" id="renamePage">
+  <div class="modal" id="renamePage">
     <div class="modal-dialog">
       <div class="modal-content">
 
@@ -40,7 +40,7 @@
         </div>
         <div class="modal-footer">
           <p><small class="pull-left" id="newPageNameCheck"></small></p>
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <input type="hidden" name="path" value="{{ page.path }}">
           <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
           <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">

+ 2 - 2
lib/views/modal/widget_unportalize.html → lib/views/modal/unportalize.html

@@ -3,7 +3,7 @@
 {% else %}
   {% set unportalizedPath = page.path|replace('(\/)$', '') %}
 {% endif %}
-  <div class="modal fade" id="unportalize">
+  <div class="modal" id="unportalize">
     <div class="modal-dialog">
       <div class="modal-content">
 
@@ -33,7 +33,7 @@
         </div>
         <div class="modal-footer">
           <p><small class="pull-left" id="newPageNameCheck"></small></p>
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <input type="hidden" name="path" value="{{ page.path }}">
           <input type="hidden" class="form-control" name="new_path" id="newPageName" value="{{ unportalizedPath }}">
           <input type="hidden" name="page_id" value="{{ page._id.toString() }}">

+ 1 - 1
lib/views/modal/widget_what_is_portal.html → lib/views/modal/what_is_portal.html

@@ -1,4 +1,4 @@
-<div class="modal fade" id="help-portal">
+<div class="modal" id="help-portal">
   <div class="modal-dialog">
     <div class="modal-content">
 

+ 0 - 28
lib/views/modal/widget_today_memo.html

@@ -1,28 +0,0 @@
-
-<div class="modal fade" id="createMemo">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <form role="form" class="form-inline" id="createMemoForm">
-
-      <div class="modal-header">
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        <h4 class="modal-title">新規メモを作成</h4>
-      </div>
-      <div class="modal-body">
-        <p><a href="{{ userPageRoot(user) }}/メモ/"><i class="fa fa-list-ul"></i> 自分のメモ一覧を見る</a></p>
-
-          <div class="input-group">
-            <span class="input-group-addon">{{ userPageRoot(user) }}/メモ/{{ now|datetz('Y/m/d') }}/</span>
-            <input type="text" class="form-control" id="memoName" name="memoName" placeholder="メモ名を入力">
-          </div>
-          <input type="hidden" class="form-control" name="memoNamePrefix" value="{{ userPageRoot(user) }}/メモ/{{ now|datetz('Y/m/d') }}/">
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-default" data-dismiss="modal">キャンセル</button>
-        <input type="submit" class="btn btn-primary" id="createMemoSubmit" value="作成">
-      </div>
-
-      </form>
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->

+ 17 - 12
lib/views/page.html

@@ -14,14 +14,14 @@
 
 
     {% if page %}
-    <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ _csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
+    <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
     {% endif %}
     <h1 class="title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
   </header>
   {% else %}
   {# trash/* #}
   <header id="page-header">
-    <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ _csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
+    <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
     <h1 class="title">{{ path|insertSpaceToEachSlashes }}</h1>
   </header>
   {% endif %}
@@ -42,6 +42,7 @@
   data-path-shortname="{{ path|path2name }}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
+  data-current-username="{% if user %}{{ user.username }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
@@ -51,7 +52,7 @@
   <ul class="nav nav-tabs hidden-print">
     <li><a>Create: {{ path }}</a></li>
     <li class="dropdown pull-right">
-      <a href="/"><i class="fa fa-times"></i> キャンセル</a>
+      <a href="#" onclick="history.back();"><i class="fa fa-times"></i> キャンセル</a>
     </li>
   </ul>
   <div class="tab-content">
@@ -65,7 +66,7 @@
   {% if page.isDeleted() %}
   <div class="alert alert-danger">
     <form role="form" class="pull-right" id="revert-delete-page-form" onsubmit="return false;">
-      <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+      <input type="hidden" name="_csrf" value="{{ csrf() }}">
       <input type="hidden" name="path" value="{{ page.path }}">
       <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
       <input type="submit" class="btn btn-danger btn-inverse btn-sm" value="Put Back!">
@@ -86,7 +87,6 @@
       <a href="#revision-body" data-toggle="tab">
       <i class="fa fa-magic"></i>
       {#
-        <img src="//graph.facebook.com/588883490/picture?size=square" width="16"> <i class="fa fa-arrow-right"></i> <img src="//graph.facebook.com/588883490/picture?size=square" width="16">
         <span class="label label-danger" style=""> 承認待ち</span>
       #}
       </a>
@@ -114,9 +114,6 @@
   </ul>
   {% endif %}
 
-  {% include 'modal/widget_rename.html' %}
-  {% include 'modal/widget_delete.html' %}
-
   <div class="tab-content wiki-content">
   {% if req.query.renamed and not page.isDeleted() %}
   <div class="alert alert-info">
@@ -145,7 +142,7 @@
         <a data-toggle="collapse" data-parent="#revision-toc" href="#revision-toc-content" class="revision-toc-head collapsed">目次</a>
 
       </div>
-      <div class="wiki {{ revision.format }}" id="revision-body-content"></div>
+      <div class="wiki" id="revision-body-content"></div>
     </div>
 
     {# edit form #}
@@ -201,6 +198,7 @@
 {% block content_footer %}
 
 
+{% if page %}
 <div class="page-attachments meta">
   <p>Attachments</p>
   <ul>
@@ -218,6 +216,7 @@
   {# /for BC #}
   Created at {{ page.createdAt|datetz('Y-m-d H:i:s') }} by <img src="{{ page.creator|default(page.creator)|picture }}" class="picture picture-rounded"> {{ page.creator.name }}<br>
 </p>
+{% endif %}
 
 {% endblock %}
 
@@ -239,7 +238,13 @@
 {% block body_end %}
   {% parent %}
 
-  <div id="presentation-layer" class="fullscreen-layer">
-    <div id="presentation-container"></div>
-  </div>
+<div id="presentation-layer" class="fullscreen-layer">
+  <div id="presentation-container"></div>
+</div>
+
+<div id="crowi-modals">
+  {% include 'modal/rename.html' %}
+  {% include 'modal/delete.html' %}
+  {% include 'modal/page_name_warning.html' %}
+</div>
 {% endblock %}

+ 13 - 7
lib/views/page_list.html

@@ -15,7 +15,7 @@
 <div class="header-wrap">
   <header class="portal-header {% if page %}has-page{% endif %}">
     {% if page %}
-      <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ _csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
+      <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
 
     {% endif %}
     <h1 class="title">
@@ -58,6 +58,7 @@
   data-page-portal="{% if page and page.isPortal() %}1{% else %}0{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
+  data-current-username="{% if user %}{{ user.username }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
@@ -69,7 +70,7 @@
    {# portal tab #}
     <li class=" {% if not req.body.pageForm %}active{% endif %}">
       {% if page %}
-      <a href="#revision-body-content" data-toggle="tab">
+      <a href="#revision-body" data-toggle="tab">
         <i class="fa fa-magic"></i>
         PORTAL
       </a>
@@ -97,7 +98,6 @@
     <li class="pull-right"><a href="#revision-history" data-toggle="tab"><i class="fa fa-history"></i> History</a></li>
     {% endif %}
   </ul>
-  {% include 'modal/widget_unportalize.html' %}
 
   <div class="tab-content">
   {% if page and not page.isLatestRevision() %}
@@ -105,7 +105,11 @@
     <strong>注意: </strong> これは現在の版ではありません。 <i class="fa fa-magic"></i> <a href="{{ page.path }}">最新のポータルを表示</a>
   </div>
   {% endif %}
-    <div class="wiki tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body-content">{{ page.revision.body|nl2br|safe }}</div>
+    <div class="tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body">
+      <div class="wiki" id="revision-body-content">
+        <i class="fa fa-spinner fa-pulse fa-fw"></i>
+      </div>
+    </div>
 
     <script type="text/template" id="raw-text-original">{{ page.revision.body }}</script>
 
@@ -183,7 +187,7 @@
     </div>
 
     {# timeline view #}
-    <div class="tab-pane" id="view-timeline">
+    <div class="tab-pane" id="view-timeline" data-shown=0>
       {% for page in pages %}
       <div class="timeline-body" id="id-{{ page.id }}">
         <h3 class="revision-path"><a href="{{ page.path }}">{{ page.path }}</a></h3>
@@ -198,7 +202,6 @@
 
 
 </div> {# /.content-main #}
-{% include 'modal/widget_what_is_portal.html' %}
 
 {% block content_main_after %}
 {% endblock %}
@@ -230,6 +233,10 @@
 {% endblock %} {# side_header #}
 
 {% block body_end %}
+<div id="crowi-modals">
+  {% include 'modal/what_is_portal.html' %}
+  {% include 'modal/unportalize.html' %}
+</div>
 <div class="modal fade portal-warning-modal" id="portal-warning-modal">
   <div class="modal-dialog">
     <div class="modal-content">
@@ -254,5 +261,4 @@
     </div>
   </div>
 </div>
-</div>
 {% endblock %} {# body_end #}

+ 3 - 3
lib/views/widget/page_list.html

@@ -26,14 +26,14 @@
     {% endif  %}
 
     {% if page.liker.length > 0 %}
-    <span>
+    <span class="page-list-liker" data-count="{{ page.liker.length }}">
       <i class="fa fa-thumbs-up"></i>{{ page.liker.length }}
     </span>
     {% endif  %}
 
     {% if viewConfig.seener_threshold and page.seenUsers.length >= viewConfig.seener_threshold %}
-    <span>
-      <i class="fa fa-eye"></i>{{ page.seenUsers.length }}
+    <span class="page-list-seer" data-count="{{ page.seenUsers.length }}">
+      <i class="fa fa-paw"></i>{{ page.seenUsers.length }}
     </span>
     {% endif  %}
 

+ 1 - 1
lib/views/widget/page_side_content.html

@@ -19,7 +19,7 @@
           <textarea class="comment-form-comment form-control" id="comment-form-comment" name="commentForm[comment]"></textarea>
         </div>
         <div class="comment-submit">
-          <input type="hidden" name="_csrf" value="{{ _csrf() }}">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <input type="hidden" name="commentForm[page_id]" value="{{ page._id.toString() }}">
           <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
           <span class="text-danger" id="comment-form-message"></span>

+ 2 - 2
lib/views/widget/page_side_header.html

@@ -33,7 +33,7 @@
         <p class="liker-count">
         <span id="like-count">{{ page.liker.length }}</span>
         <button
-          data-csrftoken="{{ _csrf() }}"
+          data-csrftoken="{{ csrf() }}"
           data-liked="{% if page.isLiked(user) %}1{% else %}0{% endif %}"
           class="btn btn-default btn-sm {% if page.isLiked(user) %}active{% endif %}"
           id="like-button"><i class="fa fa-thumbs-o-up"></i> いいね!</button>
@@ -42,7 +42,7 @@
         </p>
       </dd>
 
-      <dt><i class="fa fa-eye"></i> 見た人</dt>
+      <dt><i class="fa fa-paw"></i> 見た人</dt>
       <dd>
         <p class="seen-user-count">
           {{ page.seenUsers.length }}

+ 6 - 7
package.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi",
-  "version": "1.4.0",
+  "version": "1.5.0",
   "description": "The simple & powerful Wiki",
   "tags": [
     "wiki",
@@ -24,13 +24,13 @@
     "url": "https://github.com/crowi/crowi.git"
   },
   "engines": {
-    "node": "4.2.x",
-    "npm": "3.3.x"
+    "node": "4.x",
+    "npm": "3.x"
   },
   "dependencies": {
     "async": "~1.5.0",
     "aws-sdk": "~2.2.26",
-    "axios": "0.9.x",
+    "axios": "0.13.x",
     "babel-core": "~6.7.6",
     "babel-loader": "~6.2.4",
     "babel-preset-es2015": "~6.6.0",
@@ -40,7 +40,6 @@
     "body-parser": "~1.14.1",
     "bootstrap-sass": "~3.3.6",
     "botkit": "~0.1.1",
-    "browserify": "~12.0.1",
     "cli": "~0.6.0",
     "colors": "^1.1.2",
     "commander": "~2.9.0",
@@ -53,13 +52,13 @@
     "del": "~2.2.0",
     "diff": "~2.2.2",
     "elasticsearch": "~11.0.1",
+    "emojify.js": "^1.1.0",
     "errorhandler": "~1.3.4",
     "express": "~4.13.3",
     "express-form": "~0.12.0",
     "express-session": "~1.12.0",
-    "facebook-node-sdk": "=0.1.10",
     "font-awesome": "~4.5.0",
-    "googleapis": "=0.4.7",
+    "googleapis": "=12.3.0",
     "gulp": "~3.9.0",
     "gulp-concat": "~2.6.0",
     "gulp-cssmin": "~0.1.7",

+ 1 - 0
public/emoji_images

@@ -0,0 +1 @@
+../node_modules/emojify.js/dist/images

+ 1 - 0
resource/css/_comment.scss

@@ -55,6 +55,7 @@
         }
         .page-comment-body {
           padding: 8px 0;
+          word-wrap: break-word;
         }
       }
     }

+ 2 - 2
resource/css/_form.scss

@@ -2,7 +2,7 @@
   padding: 0;
 
   position: fixed;
-  z-index: 1060;
+  z-index: 1050;
   background: #fff;
   top: 0;
   left: 0;
@@ -19,7 +19,7 @@
     bottom: 58px;
     padding: 0 12px;
     position: absolute;
-    z-index: 1061;
+    z-index: 1051;
     left: 0;
     right: 0;
     margin-top: 4px;

+ 4 - 7
resource/css/_layout.scss

@@ -27,6 +27,10 @@
         background: $crowiHeaderBackground;
       }
 
+      > div > .navbar-nav li button {
+        margin: 10px;
+      }
+
       > div > a ,
       > div > ul > li > a {
         color: #ccc;
@@ -148,13 +152,6 @@
 
   .login-dialog-container {
 
-    .facebook-info {
-      border-radius: 4px;
-      border: solid 1px #ccc;
-      padding: 10px;
-      margin-bottom: 15px;
-    }
-
     margin: 40px auto;
     float: none;
 

+ 1 - 1
resource/css/_page.scss

@@ -69,7 +69,7 @@
 
         a:last-child {
           color: #D1E2E4;
-          opacity: .5;
+          opacity: .4;
 
           &:hover {
             color: inherit;

+ 36 - 2
resource/css/_wiki.scss

@@ -100,11 +100,11 @@ div.body {
   pre {
     line-height: 1.4em;
     font-size: .9em;
-    border: solid 1px #333;
+    border: none;
     background: #444;
     color: #f0f0f0;
     font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
-    word-break: break-word;
+    word-wrap: break-word;
   }
 
   img {
@@ -114,6 +114,15 @@ div.body {
     max-width: 100%;
   }
 
+  img.emoji {
+    width: 0.95em;
+    margin: 1px;
+    border: none;
+    box-shadow: none;
+    vertical-align: middle;
+    display: inline;
+  }
+
   ul, ol {
     padding-left: 30px;
 
@@ -131,6 +140,31 @@ div.body {
     }
   }
 
+  .wiki-code {
+    position: relative;
+
+    cite {
+      position: absolute;
+      top: 0;
+      right: 0;
+      padding: 0 4px;
+      background: #ccc;
+      color: #333;
+      font-size: .8em;
+
+    }
+  };
+
+  .page-template-builder {
+    position: relative;
+
+    .template-create-button {
+      position: absolute;
+      top: 8px;
+      right: 8px;
+    }
+  }
+
   .highlighted {
     &em {
       padding: 2px;

+ 56 - 6
resource/css/crowi.scss

@@ -83,7 +83,11 @@ footer {
   }
 }
 
+.modal-backdrop {
+  z-index: 1060;
+}
 .modal {
+  z-index: 1061;
   p {
     font-size: 1em;
   }
@@ -93,6 +97,56 @@ footer {
     margin: 0;
   }
 }
+.modal-body.alert {
+  margin-bottom: 0;
+  border-radius: 0;
+}
+
+.modal.create-page {
+  .modal-body {
+    h3, h4 {
+      margin-bottom: 10px;
+    }
+
+    form {
+      .page-name-addons {
+        position: absolute;
+        top: 7px;
+        left: 27px;
+      }
+      .page-today-prefix {
+        display: inline-block;
+      }
+      .page-today-input1 {
+        width: 60px;
+        padding-left: 2px;
+        padding-right: 2px;
+        display: inline-block;
+      }
+      .page-today-suffix {
+        display: inline-block;
+      }
+      .page-today-input2 {
+        width: 100%;
+        display: inline-block;
+      }
+    }
+  }
+}
+
+.popular-page-high {
+  color: #e80000;
+  font-size: 1.1em;
+  font-weight: bold;
+}
+.popular-page-mid {
+  color: #e47800;
+  font-weight: bold;
+}
+.popular-page-low {
+  color: #ab7c7c;
+}
+
 
 // {{{ add badge variation
 .badge-default {
@@ -152,8 +206,8 @@ footer {
 
   // size list
   &.picture-lg {
-    width: 32px;
-    height: 32px;
+    width: 48px;
+    height: 48px;
   }
   &.picture-sm {
     width: 16px;
@@ -217,10 +271,6 @@ footer {
 // buttons
 .btn-primary {
 }
-$btn-facebook-color: #4c66a4;
-.btn-facebook {
-  @include button-variant(lighten($btn-facebook-color, 50%), $btn-facebook-color, darken($btn-facebook-color, 20%));
-}
 $btn-google-color: rgb(204,89,71);
 .btn-google {
   @include button-variant(lighten($btn-google-color, 50%), $btn-google-color, darken($btn-google-color, 20%));

+ 16 - 0
resource/js/app.js

@@ -1,14 +1,30 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
+import Crowi from './util/Crowi';
+import CrowiRenderer from './util/CrowiRenderer';
+
 import HeaderSearchBox  from './components/HeaderSearchBox';
 import SearchPage  from './components/SearchPage';
 import PageListSearch  from './components/PageListSearch';
+//import PageComment  from './components/PageComment';
+
+if (!window) {
+  window = {};
+}
+// FIXME
+const crowi = new Crowi({me: $('#content-main').data('current-username')}, window);
+window.crowi = crowi;
+crowi.fetchUsers();
+
+const crowiRenderer = new CrowiRenderer();
+window.crowiRenderer = crowiRenderer;
 
 const componentMappings = {
   'search-top': <HeaderSearchBox />,
   'search-page': <SearchPage />,
   'page-list-search': <PageListSearch />,
+  //'page-comment': <PageComment />,
 };
 
 Object.keys(componentMappings).forEach((key) => {

+ 12 - 19
resource/js/components/HeaderSearchBox.js

@@ -11,6 +11,8 @@ export default class SearchBox extends React.Component {
   constructor(props) {
     super(props);
 
+    this.crowi = window.crowi; // FIXME
+
     this.state = {
       searchingKeyword: '',
       searchedPages: [],
@@ -43,26 +45,17 @@ export default class SearchBox extends React.Component {
       searching: true,
     });
 
-    axios.get('/_api/search', {params: {q: keyword}})
-    .then((res) => {
-      if (res.data.ok) {
-        this.setState({
-          searchingKeyword: keyword,
-          searchedPages: res.data.data,
-          searching: false,
-          searchError: null,
-        });
-      } else {
-        this.setState({
-          searchError: res,
-          searching: false,
-        });
-      }
-      // TODO error
-    }).catch((res) => {
-      // TODO error
+    this.crowi.apiGet('/search', {q: keyword})
+    .then(res => {
+      this.setState({
+        searchingKeyword: keyword,
+        searchedPages: res.data,
+        searching: false,
+        searchError: null,
+      });
+    }).catch(err => {
       this.setState({
-        searchError: res,
+        searchError: err,
         searching: false,
       });
     });

+ 2 - 33
resource/js/components/Page/PageBody.js

@@ -1,12 +1,11 @@
 import React from 'react';
-import marked from 'marked';
-import hljs from 'highlight.js';
 
 export default class PageBody extends React.Component {
 
   constructor(props) {
     super(props);
 
+    this.crowiRenderer = window.crowiRenderer; // FIXME
     this.getMarkupHTML = this.getMarkupHTML.bind(this);
   }
 
@@ -16,37 +15,7 @@ export default class PageBody extends React.Component {
       body = this.props.page.revision.body;
     }
 
-    let parsed = '<b>...</b>';
-    try {
-      // TODO
-      marked.setOptions({
-        gfm: true,
-        highlight: function (code, lang) {
-          let result, hl;
-          if (lang) {
-            try {
-              hl = hljs.highlight(lang, code);
-              result = hl.value;
-            } catch (e) {
-              result = code;
-            }
-          } else {
-            result = code;
-          }
-          return result;
-        },
-        tables: true,
-        breaks: true,
-        pedantic: false,
-        sanitize: false,
-        smartLists: true,
-        smartypants: false,
-        langPrefix: 'lang-'
-      });
-      parsed = marked(body);
-    } catch (e) { console.log(e, e.stack); }
-
-    return { __html: parsed };
+    return { __html: this.crowiRenderer.render(body) };
   }
 
   render() {

+ 13 - 20
resource/js/components/SearchPage.js

@@ -1,7 +1,7 @@
 // This is the root component for #search-page
 
 import React from 'react';
-import axios from 'axios'
+import Crowi from '../util/Crowi';
 import SearchForm from './SearchPage/SearchForm';
 import SearchResult from './SearchPage/SearchResult';
 
@@ -10,6 +10,8 @@ export default class SearchPage extends React.Component {
   constructor(props) {
     super(props);
 
+    this.crowi = window.crowi; // FIXME
+
     this.state = {
       location: location,
       searchingKeyword: this.props.query.q || '',
@@ -70,28 +72,19 @@ export default class SearchPage extends React.Component {
       searchingKeyword: keyword,
     });
 
-    axios.get('/_api/search', {params: {q: keyword}})
-    .then((res) => {
-      if (res.data.ok) {
-        this.changeURL(keyword);
-
-        this.setState({
-          searchedKeyword: keyword,
-          searchedPages: res.data.data,
-          searchResultMeta: res.data.meta,
-        });
-      } else {
-        this.setState({
-          searchError: res.data,
-        });
-      }
+    this.crowi.apiGet('/search', {q: keyword})
+    .then(res => {
+      this.changeURL(keyword);
 
-      // TODO error
-    })
-    .catch((res) => {
+      this.setState({
+        searchedKeyword: keyword,
+        searchedPages: res.data,
+        searchResultMeta: res.meta,
+      });
+    }).catch(err => {
       // TODO error
       this.setState({
-        searchError: res.data,
+        searchError: err,
       });
     });
   };

+ 2 - 2
resource/js/components/SearchPage/SearchResult.js

@@ -24,8 +24,8 @@ export default class SearchResult extends React.Component {
   render() {
     const excludePathString = this.props.tree;
 
-    console.log(this.props.searchError);
-    console.log(this.isError());
+    //console.log(this.props.searchError);
+    //console.log(this.isError());
     if (this.isError()) {
       return (
         <div className="content-main">

+ 2 - 1
resource/js/components/SearchPage/SearchResultList.js

@@ -14,7 +14,8 @@ export default class SearchResultList extends React.Component {
     let returnBody = body;
 
     this.props.searchingKeyword.split(' ').forEach((keyword) => {
-      const keywordExp = new RegExp('(' + keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ')', 'ig');
+      const k = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+      const keywordExp = new RegExp(`(${k}(?!(.*?\]|.*?\\)|.*?"|.*?>)))`, 'ig');
       returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
     });
 

+ 0 - 3
resource/js/components/User/UserPicture.js

@@ -6,11 +6,8 @@ export default class UserPicture extends React.Component {
   getUserPicture(user) {
     // from swig.setFilter('picture', function(user)
 
-    user.fbId = user.userId; // migration
     if (user.image && user.image != '/images/userpicture.png') {
       return user.image;
-    } else if (user.fbId) {
-      return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
     } else {
       return '/images/userpicture.png';
     }

+ 28 - 3
resource/js/crowi-form.js

@@ -20,6 +20,10 @@ $(function() {
 
   // for new page
   if (!pageId) {
+    if (!pageId && pagePath.match(/(20\d{4}|20\d{6}|20\d{2}_\d{1,2}|20\d{2}_\d{1,2}_\d{1,2})/)) {
+      $('#page-warning-modal').modal('show');
+    }
+
     if (slackConfigured) {
       FetchPagesUpdatePostAndInsert(pagePath);
     }
@@ -45,13 +49,31 @@ $(function() {
 
   // preview watch
   var originalContent = $('#form-body').val();
-  var prevContent = "";
+
+  // restore draft
+  // とりあえず、originalContent がない場合のみ復元する。(それ以外の場合は後で考える)
+  var draft = crowi.findDraft(pagePath);
+  var originalRevision = $('#page-form [name="pageForm[currentRevision]"]').val();
+  if (!originalRevision && draft) {
+    // TODO
+    $('#form-body').val(draft)
+  }
+
+  var prevContent = originalContent;
+
+  function renderPreview() {
+    var content = $('#form-body').val();
+    var parsedHTML = crowiRenderer.render(content);
+    $('#preview-body').html(parsedHTML);
+  }
+
+  // for initialize preview
+  renderPreview();
   var watchTimer = setInterval(function() {
     var content = $('#form-body').val();
     if (prevContent != content) {
-      var renderer = new Crowi.renderer($('#form-body').val(), $('#preview-body'));
-      renderer.render();
 
+      renderPreview();
       prevContent = content;
     }
   }, 500);
@@ -67,13 +89,16 @@ $(function() {
     var content = $('#form-body').val();
     if (originalContent != content) {
       isFormChanged = true;
+      crowi.saveDraft(pagePath, content);
     } else {
       isFormChanged = false;
+      crowi.clearDraft(pagePath);
     }
   });
   $('#page-form').on('submit', function(e) {
     // avoid message
     isFormChanged = false;
+    crowi.clearDraft(pagePath);
   });
 
   var getCurrentLine = function(event) {

+ 128 - 183
resource/js/crowi.js

@@ -2,9 +2,7 @@
 /* Author: Sotaro KARASAWA <sotarok@crocos.co.jp>
 */
 
-var hljs = require('highlight.js');
 var jsdiff = require('diff');
-var marked = require('marked');
 var io = require('socket.io-client');
 
 //require('bootstrap-sass');
@@ -41,15 +39,15 @@ Crowi.linkPath = function(revisionPath) {
   splittedPath.shift();
   splittedPath.forEach(function(sub) {
     path += '/';
-    pathHtml += ' <a href="' + path + '">/</a> ';
+    pathHtml += ' <a href="' + Crowi.escape(path) + '">/</a> ';
     if (sub) {
       path += sub;
-      pathHtml += '<a href="' + path + '">' + sub + '</a>';
+      pathHtml += '<a href="' + Crowi.escape(path) + '">' + Crowi.escape(sub) + '</a>';
     }
   });
   if (path.substr(-1, 1) != '/') {
     path += '/';
-    pathHtml += ' <a href="' + path + '" class="last-path">/</a>';
+    pathHtml += ' <a href="' + Crowi.escape(path) + '" class="last-path">/</a>';
   }
   $title.html(pathHtml);
 };
@@ -133,89 +131,6 @@ Crowi.unescape = function(s) {
   return s;
 };
 
-Crowi.getRendererType = function() {
-  return new Crowi.rendererType.markdown();
-};
-
-Crowi.rendererType = {};
-Crowi.rendererType.markdown = function(){};
-Crowi.rendererType.markdown.prototype = {
-  render: function(contentText) {
-
-    marked.setOptions({
-      gfm: true,
-      highlight: function (code, lang, callback) {
-        var result, hl;
-        if (lang) {
-          try {
-            hl = hljs.highlight(lang, code);
-            result = hl.value;
-          } catch (e) {
-            result = code;
-          }
-        } else {
-          //result = hljs.highlightAuto(code);
-          //callback(null, result.value);
-          result = code;
-        }
-        return callback(null, result);
-      },
-      tables: true,
-      breaks: true,
-      pedantic: false,
-      sanitize: false,
-      smartLists: true,
-      smartypants: false,
-      langPrefix: 'lang-'
-    });
-
-    var contentHtml = Crowi.unescape(contentText);
-    // TODO 前処理系のプラグイン化
-    contentHtml = this.preFormatMarkdown(contentHtml);
-    contentHtml = this.expandImage(contentHtml);
-    contentHtml = this.link(contentHtml);
-
-    var $body = this.$revisionBody;
-    // Using async version of marked
-    marked(contentHtml, {}, function (err, content) {
-      if (err) {
-        throw err;
-      }
-      $body.html(content);
-    });
-  },
-  preFormatMarkdown: function(content){
-    var x = content
-      .replace(/^(#{1,})([^\s]+)?(.*)$/gm, '$1 $2$3') // spacer for section
-      .replace(/>[\s]*\n>[\s]*\n/g, '> <br>\n> \n');
-    return x;
-  },
-  link: function (content) {
-    return content
-      //.replace(/\s(https?:\/\/[\S]+)/g, ' <a href="$1">$1</a>') // リンク
-      .replace(/\s<((\/[^>]+?){2,})>/g, ' <a href="$1">$1</a>') // ページ間リンク: <> でかこまれてて / から始まり、 / が2個以上
-      ;
-  },
-  expandImage: function (content) {
-    return content.replace(/\s(https?:\/\/[\S]+\.(jpg|jpeg|gif|png))/g, ' <a href="$1"><img src="$1" class="auto-expanded-image"></a>');
-  }
-};
-
-Crowi.renderer = function (contentText, revisionBody) {
-  var $revisionBody = revisionBody || $('#revision-body-content');
-
-  this.contentText = contentText;
-  this.$revisionBody = $revisionBody;
-  this.format = 'markdown'; // とりあえず
-  this.renderer = Crowi.getRendererType();
-  this.renderer.$revisionBody = this.$revisionBody;
-};
-Crowi.renderer.prototype = {
-  render: function() {
-    this.renderer.render(this.contentText);
-  }
-};
-
 // original: middleware.swigFilter
 Crowi.userPicture = function (user) {
   if (!user) {
@@ -224,8 +139,6 @@ Crowi.userPicture = function (user) {
 
   if (user.image && user.image != '/images/userpicture.png') {
     return user.image;
-  } else if (user.fbId) {
-    return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
   } else {
     return '/images/userpicture.png';
   }
@@ -240,6 +153,9 @@ Crowi.modifyScrollTop = function() {
   }
 
   var pageHeader = document.querySelector('#page-header');
+  if (!pageHeader) {
+    return;
+  }
   var pageHeaderRect = pageHeader.getBoundingClientRect();
 
   var sectionHeader = document.querySelector(hash);
@@ -247,32 +163,21 @@ Crowi.modifyScrollTop = function() {
     return;
   }
 
-  var sectionHeaderRect = sectionHeader.getBoundingClientRect();
-  if (sectionHeaderRect.top >= pageHeaderRect.bottom) {
-    return;
+  var timeout = 0;
+  if (window.scrollY === 0) {
+    timeout = 200;
   }
+  setTimeout(function() {
+    var sectionHeaderRect = sectionHeader.getBoundingClientRect();
+    if (sectionHeaderRect.top >= pageHeaderRect.bottom) {
+      return;
+    }
 
-  window.scrollTo(0, (window.scrollY - pageHeaderRect.height - offset));
+    window.scrollTo(0, (window.scrollY - pageHeaderRect.height - offset));
+  }, timeout);
 }
 
 
-//CrowiSearcher = function(path, $el) {
-//  this.$el = $el;
-//  this.path = path;
-//  this.searchResult = {};
-//};
-//CrowiSearcher.prototype.querySearch = function(keyword, option) {
-//};
-//CrowiSearcher.prototype.search = function(keyword) {
-//  var option = {};
-//  this.querySearch(keyword, option);
-//  this.$el.html(this.render());
-//};
-//CrowiSearcher.prototype.render = function() {
-//  return $('<div>');
-//};
-
-
 $(function() {
   var pageId = $('#content-main').data('page-id');
   var revisionId = $('#content-main').data('page-revision-id');
@@ -307,18 +212,44 @@ $(function() {
     $(this).select();
   });
 
-  $('#createMemo').on('shown.bs.modal', function (e) {
-    $('#memoName').focus();
+
+  $('#create-page').on('shown.bs.modal', function (e) {
+
+    var input2Width = $('#create-page-today .page-today-input2').outerWidth();
+    var newWidth = input2Width
+      - $('#create-page-today .page-today-prefix').outerWidth()
+      - $('#create-page-today .page-today-input1').outerWidth()
+      - $('#create-page-today .page-today-suffix').outerWidth()
+      - 10
+      ;
+    $('#create-page-today .form-control.page-today-input2').css({width: newWidth}).focus();
+
   });
-  $('#createMemoForm').submit(function(e)
-  {
-    var prefix = $('[name=memoNamePrefix]', this).val();
-    var name = $('[name=memoName]', this).val();
-    if (name === '') {
-      prefix = prefix.slice(0, -1);
+
+  $('#create-page-today').submit(function(e) {
+    var prefix1 = $('input.page-today-input1', this).data('prefix');
+    var input1 = $('input.page-today-input1', this).val();
+    var prefix2 = $('input.page-today-input2', this).data('prefix');
+    var input2 = $('input.page-today-input2', this).val();
+    if (input1 === '') {
+      prefix1 = 'メモ';
+    }
+    if (input2 === '') {
+      prefix2 = prefix2.slice(0, -1);
     }
-    top.location.href = prefix + name;
+    top.location.href = prefix1 + input1 + prefix2 + input2;
+    return false;
+  });
 
+  $('#create-page-under-tree').submit(function(e) {
+    var name = $('input', this).val();
+    if (!name.match(/^\//)) {
+      name = '/' + name;
+    }
+    if (name.match(/.+\/$/)) {
+      name = name.substr(0, name.length - 1);
+    }
+    top.location.href = name;
     return false;
   });
 
@@ -421,20 +352,39 @@ $(function() {
     var escape = function(s) {
       return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
     };
-    var pattern = escape(shortPath) + '(/)?$';
+    path = Crowi.escape(path);
+    var pattern = escape(Crowi.escape(shortPath)) + '(/)?$';
 
     $link.html(path.replace(new RegExp(pattern), '<strong>' + shortPath + '$1</strong>'));
   });
 
   // for list page
-  $('#view-timeline .timeline-body').each(function()
-  {
-    var id = $(this).attr('id');
-    var contentId = '#' + id + ' > script';
-    var revisionBody = '#' + id + ' .revision-body';
-    var revisionPath = '#' + id + ' .revision-path';
-    var renderer = new Crowi.renderer($(contentId).html(), $(revisionBody));
-    renderer.render();
+  $('a[data-toggle="tab"][href="#view-timeline"]').on('show.bs.tab', function() {
+    var isShown = $('#view-timeline').data('shown');
+    if (isShown == 0) {
+      $('#view-timeline .timeline-body').each(function()
+      {
+        var id = $(this).attr('id');
+        var contentId = '#' + id + ' > script';
+        var revisionBody = '#' + id + ' .revision-body';
+        var revisionPath = '#' + id + ' .revision-path';
+
+        var markdown = Crowi.unescape($(contentId).html());
+        var parsedHTML = crowiRenderer.render(markdown);
+        $(revisionBody).html(parsedHTML);
+
+        $('.template-create-button', revisionBody).on('click', function() {
+          var path = $(this).data('path');
+          var templateId = $(this).data('template');
+          var template = $('#' + templateId).html();
+
+          crowi.saveDraft(path, template);
+          top.location.href = path;
+        });
+      });
+
+      $('#view-timeline').data('shown', 1);
+    }
   });
 
   // login
@@ -446,25 +396,6 @@ $(function() {
     $('#login-dialog').removeClass('to-flip');
     return false;
   });
-  $('#btn-login-facebook').click(function(e)
-  {
-    var afterLogin = function(response) {
-      if (response.status !== 'connected') {
-        $('#login-form-errors').html('<p class="alert alert-danger">Facebookでのログインに失敗しました。</p>');
-      } else {
-        location.href = '/login/facebook';
-      }
-    };
-    FB.getLoginStatus(function(response) {
-      if (response.status === 'connected') {
-        afterLogin(response);
-      } else {
-        FB.login(function(response) {
-          afterLogin(response);
-        }, {scope: 'email'});
-      }
-    });
-  });
 
   $('#register-form input[name="registerForm[username]"]').change(function(e) {
     var username = $(this).val();
@@ -479,43 +410,25 @@ $(function() {
     });
   });
 
-  $('#btn-register-facebook').click(function(e)
-  {
-    var afterLogin = function(response) {
-      if (response.status !== 'connected') {
-        $('#register-form-errors').html('<p class="alert alert-danger">Facebookでのログインに失敗しました。</p>');
-
-      } else {
-        var authR = response.authResponse;
-        $('#register-form input[name="registerForm[fbId]"]').val(authR.userID);
-        FB.api('/me?fields=name,username,email', function(res) {
-          $('#register-form input[name="registerForm[name]"]').val(res.name);
-          $('#register-form input[name="registerForm[username]"]').val(res.username || '');
-          $('#register-form input[name="registerForm[email]"]').val(res.email);
-
-          $('#register-form .facebook-info').remove();
-          $('#register-form').prepend('<div class="facebook-info"><img src="//graph.facebook.com/' + res.id + '/picture?size=square" width="25"> <i class="fa fa-facebook-square"></i> ' + res.name + 'さんとして登録します</div>');
-        });
-      }
-    };
-    FB.getLoginStatus(function(response) {
-      if (response.status === 'connected') {
-        afterLogin(response);
-      } else {
-        FB.login(function(response) {
-          afterLogin(response);
-        }, {scope: 'email'});
-      }
-    });
-  });
-
   if (pageId) {
 
     // if page exists
     var $rawTextOriginal = $('#raw-text-original');
     if ($rawTextOriginal.length > 0) {
-      var renderer = new Crowi.renderer($('#raw-text-original').html());
-      renderer.render();
+      var markdown = Crowi.unescape($('#raw-text-original').html());
+      var parsedHTML = crowiRenderer.render(markdown);
+      $('#revision-body-content').html(parsedHTML);
+
+
+      $('.template-create-button').on('click', function() {
+        var path = $(this).data('path');
+        var templateId = $(this).data('template');
+        var template = $('#' + templateId).html();
+
+        crowi.saveDraft(path, template);
+        top.location.href = path;
+      });
+
       Crowi.correctHeaders('#revision-body-content');
       Crowi.revisionToc('#revision-body-content', '#revision-toc');
     }
@@ -958,9 +871,6 @@ Crowi.highlightSelectedSection = function(hash)
 }
 
 window.addEventListener('load', function(e) {
-  Crowi.highlightSelectedSection(location.hash);
-  Crowi.modifyScrollTop();
-
   // hash on page
   if (location.hash) {
     if (location.hash == '#edit-form') {
@@ -970,6 +880,41 @@ window.addEventListener('load', function(e) {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
   }
+
+  if (crowi.users || crowi.users.length == 0) {
+    var totalUsers = crowi.users.length;
+    var $listLiker = $('.page-list-liker');
+    $listLiker.each(function(i, liker) {
+      var count = $(liker).data('count') || 0;
+      if (count/totalUsers > 0.05) {
+        $(liker).addClass('popular-page-high');
+        // 5%
+      } else if (count/totalUsers > 0.02) {
+        $(liker).addClass('popular-page-mid');
+        // 2%
+      } else if (count/totalUsers > 0.005) {
+        $(liker).addClass('popular-page-low');
+        // 0.5%
+      }
+    });
+    var $listSeer = $('.page-list-seer');
+    $listSeer.each(function(i, seer) {
+      var count = $(seer).data('count') || 0;
+      if (count/totalUsers > 0.10) {
+        // 10%
+        $(seer).addClass('popular-page-high');
+      } else if (count/totalUsers > 0.05) {
+        // 5%
+        $(seer).addClass('popular-page-mid');
+      } else if (count/totalUsers > 0.02) {
+        // 2%
+        $(seer).addClass('popular-page-low');
+      }
+    });
+  }
+
+  Crowi.highlightSelectedSection(location.hash);
+  Crowi.modifyScrollTop();
 });
 
 window.addEventListener('hashchange', function(e) {

+ 152 - 0
resource/js/util/Crowi.js

@@ -0,0 +1,152 @@
+/**
+ * Crowi context class for client
+ */
+
+import axios from 'axios'
+
+export default class Crowi {
+  constructor(context, window) {
+    this.context = context;
+
+    this.location = window.location || {};
+    this.document = window.document || {};
+    this.localStorage = window.localStorage || {};
+
+    this.fetchUsers = this.fetchUsers.bind(this);
+    this.apiGet = this.apiGet.bind(this);
+    this.apiPost = this.apiPost.bind(this);
+    this.apiRequest = this.apiRequest.bind(this);
+
+    // FIXME
+    this.me = context.me;
+
+    this.users = [];
+    this.userByName = {};
+    this.draft = {};
+
+    this.recoverData();
+  }
+
+  getContext() {
+    return context;
+  }
+
+  recoverData() {
+    const keys = [
+      'userByName',
+      'users',
+      'draft',
+    ];
+
+    keys.forEach(key => {
+      if (this.localStorage[key]) {
+        try {
+          this[key] = JSON.parse(this.localStorage[key]);
+        } catch (e) {
+          this.localStorage.removeItem(key);
+        }
+      }
+    });
+  }
+
+  fetchUsers () {
+    const interval = 1000*60*10; // 5min
+    const currentTime = new Date();
+    if (!this.localStorage.lastFetched && interval > currentTime - new Date(this.localStorage.lastFetched)) {
+      return ;
+    }
+
+    this.apiGet('/users.list', {})
+    .then(data => {
+      this.users = data.users;
+      this.localStorage.users = JSON.stringify(data.users);
+
+      let userByName = {};
+      for (let i = 0; i < data.users.length; i++) {
+        const user = data.users[i];
+        userByName[user.username] = user;
+      }
+      this.userByName = userByName;
+      this.localStorage.userByName = JSON.stringify(userByName);
+
+      this.localStorage.lastFetched = new Date();
+    }).catch(err => {
+      this.localStorage.removeItem('lastFetched');
+      // ignore errors
+    });
+  }
+
+  clearDraft(path) {
+    delete this.draft[path];
+    this.localStorage.draft = JSON.stringify(this.draft);
+  }
+
+  saveDraft(path, body) {
+    this.draft[path] = body;
+    this.localStorage.draft = JSON.stringify(this.draft);
+  }
+
+  findDraft(path) {
+    if (this.draft && this.draft[path]) {
+      return this.draft[path];
+    }
+
+    return null;
+  }
+
+  findUser(username) {
+    if (this.userByName && this.userByName[username]) {
+      return this.userByName[username];
+    }
+
+    return null;
+  }
+
+  apiGet(path, params) {
+    return this.apiRequest('get', path, params);
+  }
+
+  apiPost(path, params) {
+    return this.apiRequest('post', path, params);
+  }
+
+  apiRequest(method, path, params) {
+    return new Promise((resolve, reject) => {
+      axios[method](`/_api${path}`, {params})
+      .then(res => {
+        if (res.data.ok) {
+          resolve(res.data);
+        } else {
+          // FIXME?
+          reject(new Error(res.data));
+        }
+      }).catch(res => {
+          // FIXME?
+        reject(new Error('Error'));
+      });
+    });
+  }
+
+  static escape (html, encode) {
+    return html
+      .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#39;');
+  }
+
+  static unescape(html) {
+    return html.replace(/&([#\w]+);/g, function(_, n) {
+      n = n.toLowerCase();
+      if (n === 'colon') return ':';
+      if (n.charAt(0) === '#') {
+        return n.charAt(1) === 'x'
+          ? String.fromCharCode(parseInt(n.substring(2), 16))
+          : String.fromCharCode(+n.substring(1));
+      }
+      return '';
+    });
+  }
+}
+

+ 122 - 0
resource/js/util/CrowiRenderer.js

@@ -0,0 +1,122 @@
+import marked from 'marked';
+import hljs from 'highlight.js';
+
+import MarkdownFixer from './PreProcessor/MarkdownFixer';
+import Linker        from './PreProcessor/Linker';
+import ImageExpander from './PreProcessor/ImageExpander';
+import Emoji         from './PreProcessor/Emoji';
+
+import Tsv2Table from './LangProcessor/Tsv2Table';
+import Template from './LangProcessor/Template';
+
+export default class CrowiRenderer {
+
+
+  constructor(plugins) {
+
+    this.preProcessors = [
+      new MarkdownFixer(),
+      new Linker(),
+      new ImageExpander(),
+      new Emoji(),
+    ];
+
+    this.langProcessors = {
+      'tsv': new Tsv2Table(),
+      'tsv-h': new Tsv2Table({header: true}),
+      'template': new Template(),
+    };
+
+    this.parseMarkdown = this.parseMarkdown.bind(this);
+    this.codeRenderer = this.codeRenderer.bind(this);
+  }
+
+  preProcess(markdown) {
+    for (let i = 0; i < this.preProcessors.length; i++) {
+      if (!this.preProcessors[i].process) {
+        continue;
+      }
+      markdown = this.preProcessors[i].process(markdown);
+    }
+    return markdown;
+  }
+
+  codeRenderer(code, lang, escaped) {
+    let result = '', hl;
+
+    if (lang) {
+      const langAndFn = lang.split(':');
+      const langPattern = langAndFn[0];
+      const langFn = langAndFn[1] || null;
+      if (this.langProcessors[langPattern]) {
+        return this.langProcessors[langPattern].process(code, lang);
+      }
+
+      try {
+        hl = hljs.highlight(langPattern, code);
+        result = hl.value;
+        escaped = true;
+      } catch (e) {
+        result = code;
+      }
+
+      result = (escape ? result : Crowi.escape(result, true));
+
+      let citeTag = '';
+      if (langFn) {
+        citeTag = `<cite>${langFn}</cite>`;
+      }
+      return `<pre class="wiki-code wiki-lang">${citeTag}<code class="lang-${lang}">${result}\n</code></pre>\n`;
+    }
+
+    // no lang specified
+    return `<pre class="wiki-code"><code>${Crowi.escape(code, true)}\n</code></pre>`;
+
+  }
+
+  parseMarkdown(markdown) {
+    let parsed = '';
+
+    const markedRenderer = new marked.Renderer();
+    markedRenderer.code = this.codeRenderer;
+
+    try {
+      // TODO
+      marked.setOptions({
+        gfm: true,
+        tables: true,
+        breaks: true,
+        pedantic: false,
+        sanitize: false,
+        smartLists: true,
+        smartypants: false,
+        renderer: markedRenderer,
+      });
+
+      // override
+      marked.Lexer.lex = function(src, options) {
+        var lexer = new marked.Lexer(options);
+
+        // this is maybe not an official way
+        if (lexer.rules) {
+          lexer.rules.fences = /^ *(`{3,}|~{3,})[ \.]*([^\r\n]+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/;
+        }
+
+        return lexer.lex(src);
+      };
+
+      parsed = marked(markdown);
+    } catch (e) { console.log(e, e.stack); }
+
+    return parsed;
+  }
+
+  render(markdown) {
+    let html = '';
+
+    markdown = this.preProcess(markdown);
+    html = this.parseMarkdown(markdown);
+
+    return html;
+  }
+}

+ 64 - 0
resource/js/util/LangProcessor/Template.js

@@ -0,0 +1,64 @@
+import moment from 'moment';
+
+export default class Template {
+
+  constructor() {
+    this.templatePattern = {
+      'year': this.getYear,
+      'month': this.getMonth,
+      'date': this.getDate,
+      'user': this.getUser,
+    };
+  }
+
+  getYear() {
+    return moment().format('YYYY');
+  }
+
+  getMonth() {
+    return moment().format('YYYY/MM');
+  }
+
+  getDate() {
+    return moment().format('YYYY/MM/DD');
+  }
+
+  getUser() {
+    // FIXME
+    const username = window.crowi.me || null;
+
+    if (!username) {
+      return '';
+    }
+
+    return `/user/${username}`;
+  }
+
+  parseTemplateString(templateString) {
+    let parsed = templateString;
+
+    Object.keys(this.templatePattern).forEach(key => {
+      const k = key .replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+      const matcher = new RegExp(`{${k}}`, 'g');
+      if (parsed.match(matcher)) {
+        const replacer = this.templatePattern[key]();
+        parsed = parsed.replace(matcher, replacer);
+      }
+    });
+
+    return parsed;
+  }
+
+  process(code, lang) {
+    const templateId = new Date().getTime().toString(16) + Math.floor(1000 * Math.random()).toString(16);
+    let pageName = lang;
+    if (lang.match(':')) {
+      pageName = this.parseTemplateString(lang.split(':')[1]);
+    }
+    code = this.parseTemplateString(code);
+    return `
+    <div class="page-template-builder">
+    <button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}"><i class="fa fa-pencil"></i> ${pageName}</button>
+      <pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre></div>\n`;
+  }
+}

+ 84 - 0
resource/js/util/LangProcessor/Tsv2Table.js

@@ -0,0 +1,84 @@
+
+export default class Tsv2Table {
+
+  constructor(option) {
+    if (!option) {
+      option = {};
+    }
+    this.option = option;
+
+    this.option.header = this.option.header || false;
+  }
+  getCols(codeLines) {
+    let max = 0;
+
+    for (let i = 0; i < codeLines ; i++) {
+      if (max < codeLines.length) {
+        max = codeLines.length;
+      }
+    }
+
+    return max;
+  }
+
+  splitColums(line) {
+    // \t is replaced to '    ' by Lexer.lex(), so split by 4 spaces
+    return line.split(/\s{4}/g);
+  }
+
+  getTableHeader(codeLines, option) {
+    let headers = [];
+    let headLine = (codeLines[0] || '');
+
+    //console.log('head', headLine);
+    headers = this.splitColums(headLine).map(col => {
+      return `<th>${Crowi.escape(col)}</th>`;
+    });
+
+    if (headers.length < option.cols) {
+      headers.concat(new Array(option.cols - headers.length));
+    }
+
+    return `<tr>
+      ${headers.join('\n')}
+    </tr>`;
+  }
+
+  getTableBody(codeLines, option) {
+    let rows;
+
+    if (this.option.header) {
+      codeLines.shift();
+    }
+
+    rows = codeLines.map(row => {
+      const cols = this.splitColums(row).map(col => {
+        return `<td>${Crowi.escape(col)}</td>`;
+      }).join('');
+      return `<tr>${cols}</tr>`;
+    });
+
+    return rows.join('\n');
+  }
+
+  process(code) {
+    let option = {};
+    const codeLines = code.split(/\n|\r/);
+
+    option.cols = this.getCols(codeLines);
+
+    let header = '';
+    if (this.option.header) {
+      header = `<thead>
+        ${this.getTableHeader(codeLines, option)}
+      </thead>`;
+    }
+
+    return `<table>
+      ${header}
+      <tbody>
+        ${this.getTableBody(codeLines, option)}
+      </tbody>
+    </table>`;
+  }
+}

+ 14 - 0
resource/js/util/PreProcessor/Emoji.js

@@ -0,0 +1,14 @@
+import emojify from 'emojify.js';
+
+export default class Emoji {
+
+  constructor() {
+    emojify.setConfig({
+      img_dir: '/emoji_images/basic',
+    });
+  }
+
+  process(markdown) {
+    return emojify.replace(markdown);
+  }
+}

+ 9 - 0
resource/js/util/PreProcessor/ImageExpander.js

@@ -0,0 +1,9 @@
+
+export default class ImageExpander {
+
+  process(markdown) {
+
+    return markdown
+      .replace(/\s(https?:\/\/[\S]+\.(jpg|jpeg|gif|png))/g, ' <a href="$1"><img src="$1" class="auto-expanded-image"></a>');
+  }
+}

+ 10 - 0
resource/js/util/PreProcessor/Linker.js

@@ -0,0 +1,10 @@
+
+export default class Linker {
+  process(markdown) {
+
+    return markdown
+      //.replace(/\s(https?:\/\/[\S]+)/g, ' <a href="$1">$1</a>') // リンク
+      .replace(/\s<((\/[^>]+?){2,})>/g, ' <a href="$1">$1</a>') // ページ間リンク: <> でかこまれてて / から始まり、 / が2個以上
+      ;
+  }
+}

+ 10 - 0
resource/js/util/PreProcessor/MarkdownFixer.js

@@ -0,0 +1,10 @@
+
+export default class MarkdownFixer {
+  process(markdown) {
+    var x = markdown
+      .replace(/^(#{1,})((?![\s\#]+).+)$/gm, '$1 $2') // spacer for section
+      .replace(/>[\s]*\n>[\s]*\n/g, '> <br>\n> \n');
+
+    return x;
+  }
+}

+ 12 - 6
test/utils.js

@@ -2,6 +2,7 @@
 
 var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || process.env.MONGO_URI || null
   , mongoose= require('mongoose')
+  , fs = require('fs')
   , models = {}
   , crowi = new (require(ROOT_DIR + '/lib/crowi'))(ROOT_DIR, process.env)
   ;
@@ -42,12 +43,17 @@ after('Close database connection', function (done) {
   return done();
 });
 
-
-models.Page     = require(MODEL_DIR + '/page.js')(crowi);
-models.User     = require(MODEL_DIR + '/user.js')(crowi);
-models.Config   = require(MODEL_DIR + '/config.js')(crowi);
-models.Revision = require(MODEL_DIR + '/revision.js')(crowi);
-models.UpdatePost = require(MODEL_DIR + '/updatePost.js')(crowi);
+// Setup Models
+fs.readdirSync(MODEL_DIR).forEach(function(file) {
+  if (file.match(/^(\w+)\.js$/)) {
+    var name = RegExp.$1;
+    if (name === 'index') {
+      return;
+    }
+    var modelName = name.charAt(0).toUpperCase() + name.slice(1);
+    models[modelName] = require(MODEL_DIR + '/' + file)(crowi);
+  }
+});
 
 crowi.models = models;