Browse Source

Merge pull request #36 from crowi/v1.3.0-wip

v1.3.0
Sotaro KARASAWA 10 years ago
parent
commit
bf7f6ec467

+ 10 - 0
CHANGES.md

@@ -1,6 +1,16 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
+## 1.3.0
+
+* Feature: Image uploader.
+* Feature: Textarea editor (Thank you: @suzuki #38).
+* Feature: Register API Token for user and added `pages.get` api (Experimental).
+* Improve: Design on full-screen editor.
+* Fix: Library version (mongoose-paginator is now fixed its version).
+* Add unit test for user model.
+* Library Update: node.js 4.2.x, npm 3.3.x and so far.
+
 ## 1.2.0
 ## 1.2.0
 
 
 * Re-writed application structure.
 * Re-writed application structure.

+ 2 - 1
README.md

@@ -5,6 +5,7 @@ Crowi - The Simple & Powerful Communication Tool Based on Wiki
 [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
 [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
 
 
 [![Circle CI](https://circleci.com/gh/crowi/crowi.svg?style=svg)](https://circleci.com/gh/crowi/crowi)
 [![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)
 
 
 
 
 Crowi is:
 Crowi is:
@@ -28,7 +29,7 @@ More info are [here](https://github.com/crowi/crowi/wiki/Install-and-Configurati
 Dependencies
 Dependencies
 -------------
 -------------
 
 
-* Node.js (0.10.x)
+* Node.js (4.2.x)
 * MongoDB
 * MongoDB
 * Redis (optional)
 * Redis (optional)
 * Amazon S3
 * Amazon S3

+ 0 - 3
app.json

@@ -8,9 +8,6 @@
   "website": "https://crowi.wiki/",
   "website": "https://crowi.wiki/",
   "repository": "https://github.com/crowi/crowi",
   "repository": "https://github.com/crowi/crowi",
   "success_url": "/",
   "success_url": "/",
-  "scripts": {
-    "postdeploy": "grunt"
-  },
   "env": {
   "env": {
     "NODE_ENV": "production",
     "NODE_ENV": "production",
     "SECRET_TOKEN": {
     "SECRET_TOKEN": {

+ 2 - 1
bower.json

@@ -26,6 +26,7 @@
     "reveal.js": "~3.0.0",
     "reveal.js": "~3.0.0",
     "jquery": "~2.1.3",
     "jquery": "~2.1.3",
     "highlightjs": "~8.4.0",
     "highlightjs": "~8.4.0",
-    "inline-attachment": "~2.0.1"
+    "inline-attachment": "~2.0.1",
+    "jquery-selection": "~1.0.1"
   }
   }
 }
 }

+ 4 - 8
circle.yml

@@ -5,15 +5,11 @@ machine:
   environment:
   environment:
     MONGO_URI: mongodb://127.0.0.1/crowi_test
     MONGO_URI: mongodb://127.0.0.1/crowi_test
 deployment:
 deployment:
-  demo:
-    branch: master
-    heroku:
-      appname: crowi-demo
   dev:
   dev:
     branch: /(master|wip-v.*)/
     branch: /(master|wip-v.*)/
     heroku:
     heroku:
       appname: crowi-dev
       appname: crowi-dev
-  strk:
-    branch: master
-    heroku:
-      appname: strk-wiki
+notify:
+  webhooks:
+    - url: https://webhooks.gitter.im/e/5a035388e3274b621d20
+

+ 14 - 1
gulpfile.js

@@ -41,6 +41,7 @@ var js = {
     'node_modules/socket.io-client/socket.io.js',
     'node_modules/socket.io-client/socket.io.js',
     'bower_components/marked/lib/marked.js',
     'bower_components/marked/lib/marked.js',
     'bower_components/jquery.cookie/jquery.cookie.js',
     'bower_components/jquery.cookie/jquery.cookie.js',
+    'bower_components/jquery-selection/src/jquery.selection.js',
     'bower_components/highlightjs/highlight.pack.js',
     'bower_components/highlightjs/highlight.pack.js',
     'resource/js/crowi.js'
     'resource/js/crowi.js'
   ],
   ],
@@ -51,6 +52,10 @@ var js = {
     'bower_components/reveal.js/js/reveal.js'
     'bower_components/reveal.js/js/reveal.js'
   ],
   ],
   revealDist: dirs.jsDist + '/crowi-reveal.js',
   revealDist: dirs.jsDist + '/crowi-reveal.js',
+  formSrc: [
+    'resource/js/crowi-form.js'
+  ],
+  formDist: dirs.jsDist + '/crowi-form.js',
   clientWatch: ['resource/js/**/*.js'],
   clientWatch: ['resource/js/**/*.js'],
   watch: ['test/**/*.test.js', 'app.js', 'lib/**/*.js'],
   watch: ['test/**/*.test.js', 'app.js', 'lib/**/*.js'],
   lint: ['app.js', 'lib/**/*.js'],
   lint: ['app.js', 'lib/**/*.js'],
@@ -68,6 +73,10 @@ gulp.task('js:concat', function() {
     .pipe(concat('crowi-reveal.js'))
     .pipe(concat('crowi-reveal.js'))
     .pipe(gulp.dest(dirs.jsDist));
     .pipe(gulp.dest(dirs.jsDist));
 
 
+  gulp.src(js.formSrc)
+    .pipe(concat('crowi-form.js'))
+    .pipe(gulp.dest(dirs.jsDist));
+
   return gulp.src(js.src)
   return gulp.src(js.src)
     .pipe(concat('crowi.js'))
     .pipe(concat('crowi.js'))
     .pipe(gulp.dest(dirs.jsDist));
     .pipe(gulp.dest(dirs.jsDist));
@@ -79,6 +88,11 @@ gulp.task('js:min', ['js:concat'], function() {
     .pipe(rename({suffix: '.min'}))
     .pipe(rename({suffix: '.min'}))
     .pipe(gulp.dest(dirs.jsDist));
     .pipe(gulp.dest(dirs.jsDist));
 
 
+  gulp.src(js.formDist)
+    .pipe(uglify())
+    .pipe(rename({suffix: '.min'}))
+    .pipe(gulp.dest(dirs.jsDist));
+
   return gulp.src(js.dist)
   return gulp.src(js.dist)
     .pipe(uglify())
     .pipe(uglify())
     .pipe(rename({suffix: '.min'}))
     .pipe(rename({suffix: '.min'}))
@@ -140,4 +154,3 @@ gulp.task('watch', function() {
 gulp.task('css', ['css:sass', 'css:concat',]);
 gulp.task('css', ['css:sass', 'css:concat',]);
 gulp.task('default', ['css:min', 'js:min',]);
 gulp.task('default', ['css:min', 'js:min',]);
 gulp.task('dev', ['css:concat', 'js:concat','jshint', 'test']);
 gulp.task('dev', ['css:concat', 'js:concat','jshint', 'test']);
-

+ 2 - 0
lib/crowi/index.js

@@ -63,7 +63,9 @@ Crowi.prototype.init = function() {
         if (err) {
         if (err) {
           return reject();
           return reject();
         }
         }
+
         self.setConfig(doc);
         self.setConfig(doc);
+
         return resolve();
         return resolve();
       });
       });
     });
     });

+ 2 - 1
lib/form/index.js

@@ -4,7 +4,8 @@ exports.invited = require('./invited');
 exports.revision = require('./revision');
 exports.revision = require('./revision');
 exports.me = {
 exports.me = {
   user: require('./me/user'),
   user: require('./me/user'),
-  password: require('./me/password')
+  password: require('./me/password'),
+  apiToken: require('./me/apiToken'),
 };
 };
 exports.admin = {
 exports.admin = {
   app: require('./admin/app'),
   app: require('./admin/app'),

+ 9 - 0
lib/form/me/apiToken.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('apiTokenForm.confirm').required()
+);
+

+ 15 - 0
lib/models/config.js

@@ -21,6 +21,7 @@ module.exports = function(crowi) {
     return {
     return {
       'app:title'         : 'Crowi',
       'app:title'         : 'Crowi',
       'app:confidential'  : '',
       'app:confidential'  : '',
+      'app:uploadable'  : false,
 
 
       'security:registrationMode'      : 'Open',
       'security:registrationMode'      : 'Open',
       'security:registrationWhiteList' : [],
       'security:registrationWhiteList' : [],
@@ -158,11 +159,25 @@ module.exports = function(crowi) {
           config[el.ns][el.key] = JSON.parse(el.value);
           config[el.ns][el.key] = JSON.parse(el.value);
         });
         });
 
 
+        // temporary ...
+        config.crowi['app:uploadable'] = Config.isUploadable(config);
+
         debug('Config loaded', config);
         debug('Config loaded', config);
         return callback(null, config);
         return callback(null, config);
       });
       });
   };
   };
 
 
+  configSchema.statics.isUploadable = function(config)
+  {
+    if (!config.crowi['aws:accessKeyId'] ||
+        !config.crowi['aws:secretAccessKey'] ||
+        !config.crowi['aws:region'] ||
+        !config.crowi['aws:bucket']) {
+      return false;
+    }
+
+    return true;
+  };
 
 
   Config = mongoose.model('Config', configSchema);
   Config = mongoose.model('Config', configSchema);
   Config.SECURITY_REGISTRATION_MODE_OPEN       = SECURITY_REGISTRATION_MODE_OPEN;
   Config.SECURITY_REGISTRATION_MODE_OPEN       = SECURITY_REGISTRATION_MODE_OPEN;

+ 40 - 60
lib/models/page.js

@@ -91,13 +91,13 @@ module.exports = function(crowi) {
 
 
   pageSchema.methods.isLiked = function(userData) {
   pageSchema.methods.isLiked = function(userData) {
     if (undefined === this.populated('liker')) {
     if (undefined === this.populated('liker')) {
-      if (this.liker.indexOf(userData._id) != -1) {
+      if (this.liker.indexOf(userData._id.toString()) != -1) {
         return true;
         return true;
       }
       }
       return true;
       return true;
     } else {
     } else {
       return this.liker.some(function(likedUser) {
       return this.liker.some(function(likedUser) {
-        return likedUser._id.toString() == userData._id.toString();
+        return likedUser._id.equals(userData._id);
       });
       });
     }
     }
   };
   };
@@ -106,26 +106,15 @@ module.exports = function(crowi) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-    if (undefined === this.populated('liker')) {
-      var added = this.liker.addToSet(userData._id);
-      if (added.length > 0) {
-        this.save(function(err, data) {
-          debug('liker updated!', added);
-          return callback(err, data);
-        });
-      } else {
-        debug('liker not updated');
-        return callback(null, this);
-      }
+    var added = this.liker.addToSet(userData._id);
+    if (added.length > 0) {
+      this.save(function(err, data) {
+        debug('liker updated!', added);
+        return callback(err, data);
+      });
     } else {
     } else {
-      Page.update(
-        {_id: self._id},
-        { $addToSet: { liker:  userData._id }},
-        function(err, numAffected, raw) {
-          debug('Updated liker,', err, numAffected, raw);
-          callback(null, self);
-        }
-      );
+      debug('liker not updated');
+      return callback(null, this);
     }
     }
   };
   };
 
 
@@ -133,54 +122,45 @@ module.exports = function(crowi) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-    if (undefined === this.populated('liker')) {
-      var removed = this.liker.pull(userData._id);
-      if (removed.length > 0) {
-        this.save(function(err, data) {
-          debug('unlike updated!', removed);
-          return callback(err, data);
-        });
-      } else {
-        debug('unlike not updated');
-        callback(null, this);
-      }
+    var removed = this.liker.pull(userData._id);
+    if (removed.length > 0) {
+      this.save(function(err, data) {
+        debug('unlike updated!', removed);
+        return callback(err, data);
+      });
     } else {
     } else {
-      Page.update(
-        {_id: self._id},
-        { $pull: { liker:  userData._id }},
-        function(err, numAffected, raw) {
-          debug('Updated liker (unlike)', err, numAffected, raw);
-          callback(null, self);
-        }
-      );
+      debug('unlike not updated');
+      callback(null, this);
     }
     }
   };
   };
 
 
+  pageSchema.methods.isSeenUser = function(userData) {
+    var self = this,
+      Page = self;
+
+    return this.seenUsers.some(function(seenUser) {
+      return seenUser._id.equals(userData._id);
+    });
+  };
+
   pageSchema.methods.seen = function(userData, callback) {
   pageSchema.methods.seen = function(userData, callback) {
     var self = this,
     var self = this,
       Page = self;
       Page = self;
 
 
-    if (undefined === this.populated('seenUsers')) {
-      var added = this.seenUsers.addToSet(userData._id);
-      if (added.length > 0) {
-        this.save(function(err, data) {
-          debug('seenUsers updated!', added);
-          return callback(err, data);
-        });
-      } else {
-        debug('seenUsers not updated');
-        return callback(null, this);
-      }
-    } else {
-      Page.update(
-        {_id: self._id},
-        { $addToSet: { seenUsers:  userData._id }},
-        function(err, numAffected, raw) {
-          debug('Updated seenUsers,', err, numAffected, raw);
-          callback(null, self);
-        }
-      );
+    if (!userData || !userData._id) {
+      callback(new Error('User data is not valid'), null);
+    }
+
+    if (this.isSeenUser(userData)) {
+      debug('seenUsers not updated');
+      return callback(null, self);
     }
     }
+
+    var added = self.seenUsers.addToSet(userData);
+    this.save(function(err, data) {
+      debug('seenUsers updated!', added);
+      return callback(err, self);
+    });
   };
   };
 
 
   pageSchema.statics.getGrantLabels = function() {
   pageSchema.statics.getGrantLabels = function() {

+ 39 - 5
lib/models/user.js

@@ -25,6 +25,7 @@ module.exports = function(crowi) {
     username: { type: String },
     username: { type: String },
     email: { type: String, required: true },
     email: { type: String, required: true },
     password: String,
     password: String,
+    apiToken: String,
     status: { type: Number, required: true, default: STATUS_ACTIVE },
     status: { type: Number, required: true, default: STATUS_ACTIVE },
     createdAt: { type: Date, default: Date.now },
     createdAt: { type: Date, default: Date.now },
     admin: { type: Boolean, default: 0 }
     admin: { type: Boolean, default: 0 }
@@ -58,6 +59,13 @@ module.exports = function(crowi) {
     return hasher.digest('hex');
     return hasher.digest('hex');
   }
   }
 
 
+  function generateApiToken (user) {
+    var hasher = crypto.createHash('sha256');
+    hasher.update((new Date).getTime() + user._id);
+
+    return hasher.digest('base64');
+  }
+
   userSchema.methods.isPasswordSet = function() {
   userSchema.methods.isPasswordSet = function() {
     if (this.password) {
     if (this.password) {
       return true;
       return true;
@@ -69,10 +77,6 @@ module.exports = function(crowi) {
     return this.password == generatePassword(password);
     return this.password == generatePassword(password);
   };
   };
 
 
-  userSchema.methods.setPassword = function(password) {
-    return this.password == generatePassword(password);
-  };
-
   userSchema.methods.setPassword = function(password) {
   userSchema.methods.setPassword = function(password) {
     this.password = generatePassword(password);
     this.password = generatePassword(password);
     return this;
     return this;
@@ -100,6 +104,22 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  userSchema.methods.updateApiToken = function(callback) {
+    var self = this;
+
+    self.apiToken = generateApiToken(this);
+    return new Promise(function(resolve, reject) {
+      self.save(function(err, userData) {
+        if (err) {
+          return reject(err);
+        } else {
+          return resolve(userData);
+        }
+      });
+
+    });
+  };
+
   userSchema.methods.updateImage = function(image, callback) {
   userSchema.methods.updateImage = function(image, callback) {
     this.image = image;
     this.image = image;
     this.save(function(err, userData) {
     this.save(function(err, userData) {
@@ -254,7 +274,7 @@ module.exports = function(crowi) {
   userSchema.statics.findUsersWithPagination = function(options, callback) {
   userSchema.statics.findUsersWithPagination = function(options, callback) {
     var sort = options.sort || {status: 1, username: 1, createdAt: 1};
     var sort = options.sort || {status: 1, username: 1, createdAt: 1};
 
 
-    this.paginate({}, options.page || 1, PAGE_ITEMS, function(err, pageCount, paginatedResults, itemCount) {
+    this.paginate({}, { page: options.page || 1, limit: PAGE_ITEMS }, function(err, paginatedResults, pageCount, itemCount) {
       if (err) {
       if (err) {
         debug('Error on pagination:', err);
         debug('Error on pagination:', err);
         return callback(err, null);
         return callback(err, null);
@@ -270,6 +290,20 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  userSchema.statics.findUserByApiToken = function(apiToken) {
+    var self = this;
+
+    return new Promise(function(resolve, reject) {
+      self.findOne({apiToken: apiToken}, function (err, userData) {
+        if (err) {
+          return reject(err);
+        } else {
+          return resolve(userData);
+        }
+      });
+    });
+  };
+
   userSchema.statics.findUserByFacebookId = function(fbId, callback) {
   userSchema.statics.findUserByFacebookId = function(fbId, callback) {
     this.findOne({userId: fbId}, function (err, userData) {
     this.findOne({userId: fbId}, function (err, userData) {
       callback(err, userData);
       callback(err, userData);

+ 7 - 3
lib/routes/index.js

@@ -10,6 +10,7 @@ module.exports = function(crowi, app) {
     , user      = require('./user')(crowi, app)
     , user      = require('./user')(crowi, app)
     , attachment= require('./attachment')(crowi, app)
     , attachment= require('./attachment')(crowi, app)
     , loginRequired = middleware.loginRequired
     , loginRequired = middleware.loginRequired
+    , accessTokenParser = middleware.accessTokenParser
     ;
     ;
 
 
   app.get('/'                        , loginRequired(crowi, app) , page.pageListShow);
   app.get('/'                        , loginRequired(crowi, app) , page.pageListShow);
@@ -38,8 +39,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.mail, admin.api.appSetting);
   app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.mail, admin.api.appSetting);
   app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
   app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
   app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
   app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
-  app.post('/_api/admin/settings/fb'    , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.fb
-  , admin.api.appSetting);
+  app.post('/_api/admin/settings/fb'    , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.fb , admin.api.appSetting);
 
 
   app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
   app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
   app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , admin.user.invite);
   app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , admin.user.invite);
@@ -52,8 +52,10 @@ module.exports = function(crowi, app) {
 
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);
+  app.get('/me/apiToken'              , loginRequired(crowi, app) , me.apiToken);
   app.post('/me'                      , form.me.user              , loginRequired(crowi, app) , me.index);
   app.post('/me'                      , form.me.user              , loginRequired(crowi, app) , me.index);
   app.post('/me/password'             , form.me.password          , loginRequired(crowi, app) , me.password);
   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/picture/delete'       , loginRequired(crowi, app) , me.deletePicture);
   app.post('/me/auth/facebook'        , loginRequired(crowi, app) , me.authFacebook);
   app.post('/me/auth/facebook'        , loginRequired(crowi, app) , me.authFacebook);
   app.post('/me/auth/google'          , loginRequired(crowi, app) , me.authGoogle);
   app.post('/me/auth/google'          , loginRequired(crowi, app) , me.authGoogle);
@@ -72,7 +74,9 @@ module.exports = function(crowi, app) {
   app.post('/_api/page/:id/unlike'    , loginRequired(crowi, app) , page.api.unlike);
   app.post('/_api/page/:id/unlike'    , loginRequired(crowi, app) , page.api.unlike);
   app.get( '/_api/page/:id/bookmark'  , loginRequired(crowi, app) , page.api.isBookmarked);
   app.get( '/_api/page/:id/bookmark'  , loginRequired(crowi, app) , page.api.isBookmarked);
   app.post('/_api/page/:id/bookmark'  , loginRequired(crowi, app) , page.api.bookmark);
   app.post('/_api/page/:id/bookmark'  , loginRequired(crowi, app) , page.api.bookmark);
-  //app.get('/_api/page/*'           , user.useUserData()         , page.api.get);
+
+  // HTTP RPC Styled API (に徐々に移行していいこうと思う)
+  app.get('/_api/pages.get'           , accessTokenParser(crowi, app) , loginRequired(crowi, app) , page.api.get);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
 
 

+ 22 - 0
lib/routes/me.js

@@ -165,6 +165,28 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
+  actions.apiToken = function(req, res) {
+    var apiTokenForm = req.body.apiTokenForm;
+    var userData = req.user;
+
+    if (req.method == 'POST' && req.form.isValid) {
+      userData.updateApiToken()
+      .then(function(userData) {
+          req.flash('successMessage', 'API Token を更新しました');
+          return res.redirect('/me/apiToken');
+      })
+      .catch(function(err) {
+          //req.flash('successMessage',);
+          req.form.errors.push('API Token の更新に失敗しました');
+          return res.render('me/api_token', {
+          });
+      });
+    } else {
+      return res.render('me/api_token', {
+      });
+    }
+  };
+
   actions.updates = function(req, res) {
   actions.updates = function(req, res) {
     res.render('me/update', {
     res.render('me/update', {
     });
     });

+ 33 - 3
lib/routes/page.js

@@ -73,10 +73,11 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     Revision.findRevisionList(pageData.path, {}, function(err, tree) {
     Revision.findRevisionList(pageData.path, {}, function(err, tree) {
+      var revision = pageData.revision || {};
       res.render(req.query.presentation ? 'page_presentation' : 'page', {
       res.render(req.query.presentation ? 'page_presentation' : 'page', {
         path: pageData.path,
         path: pageData.path,
-        revision: pageData.revision || {},
-        author: pageData.revision.author || false,
+        revision: revision,
+        author: revision.author || false,
         page: pageData,
         page: pageData,
         tree: tree || [],
         tree: tree || [],
       });
       });
@@ -107,7 +108,6 @@ module.exports = function(crowi, app) {
         res.redirect(encodeURI(path));
         res.redirect(encodeURI(path));
         return ;
         return ;
       }
       }
-      debug('Page found', pageData);
 
 
       if (err == Page.PAGE_GRANT_ERROR) {
       if (err == Page.PAGE_GRANT_ERROR) {
         debug('PAGE_GRANT_ERROR');
         debug('PAGE_GRANT_ERROR');
@@ -115,6 +115,7 @@ module.exports = function(crowi, app) {
       }
       }
 
 
       if (pageData) {
       if (pageData) {
+        debug('Page found', pageData._id, pageData.path);
         pageData.seen(req.user, function(err, data) {
         pageData.seen(req.user, function(err, data) {
           return renderPage(data, req, res);
           return renderPage(data, req, res);
         });
         });
@@ -203,6 +204,35 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
+  /**
+   * @api pages.get
+   * @param page /page/path
+   * @param page_id XXXXX
+   */
+  api.get = function(req, res){
+    var pagePath = req.query.page;
+    var revision = req.query.revision;
+    var options = {};
+
+    Page.findPage(pagePath, req.user, revision, options, function(err, pageData) {
+      var result = {};
+      if (err) {
+        result = {
+          ok: false,
+          message: err.toString()
+        };
+      }
+      if (pageData) {
+        result = {
+          ok: true,
+          page: pageData
+        };
+      }
+
+      return res.json(result);
+    });
+  };
+
   /**
   /**
    * page bookmark
    * page bookmark
    */
    */

+ 7 - 17
lib/util/fileUploader.js

@@ -3,17 +3,18 @@
  */
  */
 
 
 
 
-module.exports = function(crowi, app) {
+module.exports = function(crowi) {
   'use strict';
   'use strict';
 
 
   var aws = require('aws-sdk')
   var aws = require('aws-sdk')
     , debug = require('debug')('crowi:lib:fileUploader')
     , debug = require('debug')('crowi:lib:fileUploader')
     , Promise = require('bluebird')
     , Promise = require('bluebird')
+    , Config = crowi.model('Config')
     , config = crowi.getConfig()
     , config = crowi.getConfig()
     , lib = {}
     , lib = {}
     ;
     ;
 
 
-  function getAwsConfig ()
+  lib.getAwsConfig = function()
   {
   {
     return {
     return {
       accessKeyId: config.crowi['aws:accessKeyId'],
       accessKeyId: config.crowi['aws:accessKeyId'],
@@ -21,18 +22,7 @@ module.exports = function(crowi, app) {
       region: config.crowi['aws:region'],
       region: config.crowi['aws:region'],
       bucket: config.crowi['aws:bucket']
       bucket: config.crowi['aws:bucket']
     };
     };
-  }
-
-  function isUploadable(awsConfig) {
-    if (!awsConfig.accessKeyId ||
-        !awsConfig.secretAccessKey ||
-        !awsConfig.region ||
-        !awsConfig.bucket) {
-      return false;
-    }
-
-    return true;
-  }
+  };
 
 
   // lib.deleteFile = function(filePath, callback) {
   // lib.deleteFile = function(filePath, callback) {
   //   // TODO 実装する
   //   // TODO 実装する
@@ -40,8 +30,8 @@ module.exports = function(crowi, app) {
   //
   //
 
 
   lib.uploadFile = function(filePath, contentType, fileStream, options) {
   lib.uploadFile = function(filePath, contentType, fileStream, options) {
-    var awsConfig = getAwsConfig();
-    if (!isUploadable(awsConfig)) {
+    var awsConfig = lib.getAwsConfig();
+    if (!Config.isUploadable(config)) {
       return new Promise.reject(new Error('AWS is not configured.'));
       return new Promise.reject(new Error('AWS is not configured.'));
     }
     }
 
 
@@ -70,7 +60,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   lib.generateS3FileUrl = function(filePath) {
   lib.generateS3FileUrl = function(filePath) {
-    var awsConfig = getAwsConfig();
+    var awsConfig = lib.getAwsConfig();
     var url = 'https://' + awsConfig.bucket +'.s3.amazonaws.com/' + filePath;
     var url = 'https://' + awsConfig.bucket +'.s3.amazonaws.com/' + filePath;
 
 
     return url;
     return url;

+ 19 - 0
lib/util/middlewares.js

@@ -104,6 +104,25 @@ exports.loginRequired = function(crowi, app) {
   };
   };
 };
 };
 
 
+exports.accessTokenParser = function(crowi, app) {
+  return function(req, res, next) {
+    var accessToken = req.query.access_token;
+    if (!accessToken) {
+      return next();
+    }
+
+    var User = crowi.model('User')
+
+    User.findUserByApiToken(accessToken)
+    .then(function(userData) {
+      req.user = userData;
+      next();
+    }).catch(function(err) {
+      next();
+    });
+  };
+};
+
 // this is for Installer
 // this is for Installer
 exports.applicationNotInstalled = function() {
 exports.applicationNotInstalled = function() {
   return function(req, res, next) {
   return function(req, res, next) {

+ 2 - 120
lib/views/_form.html

@@ -9,7 +9,7 @@
 </div>
 </div>
 {% endif %}
 {% endif %}
 <div id="form-box" class="row">
 <div id="form-box" class="row">
-  <form action="{{ path }}/edit" id="page-form" method="post" class="col-md-6">
+  <form action="{{ path }}/edit" id="page-form" method="post" class="col-md-6 {% if config.crowi['app:uploadable'] %}uploadable{% endif %}">
     <textarea name="pageForm[body]" class="form-control form-body-height" id="form-body">{% if pageForm.body %}{{ pageForm.body }}{% elseif not revision.body %}# {{ path|path2name }}{% else %}{{ revision.body }}{% endif %}</textarea>
     <textarea name="pageForm[body]" class="form-control form-body-height" id="form-body">{% if pageForm.body %}{{ pageForm.body }}{% elseif not revision.body %}# {{ path|path2name }}{% else %}{{ revision.body }}{% endif %}</textarea>
 
 
     <input type="hidden" name="pageForm[format]" value="markdown" id="form-format">
     <input type="hidden" name="pageForm[format]" value="markdown" id="form-format">
@@ -36,123 +36,5 @@
   </div>
   </div>
   <div class="file-module hidden">
   <div class="file-module hidden">
   </div>
   </div>
-  <script type="text/javascript">
-  $(function() {
-    // preview watch
-    var originalContent = $('#form-body').val();
-    var prevContent = "";
-    var watchTimer = setInterval(function() {
-      var content = $('#form-body').val();
-      if (prevContent != content) {
-        var renderer = new Crowi.renderer($('#form-body').val(), $('#preview-body'));
-        renderer.render();
-
-        prevContent = content;
-      }
-    }, 500);
-
-    // tabs handle
-    $('textarea#form-body').on('keydown', function(event){
-      var self  = $(this)
-          start = this.selectionStart,
-          end   = this.selectionEnd
-          val   = self.val();
-
-      if (event.keyCode === 9) {
-        // tab
-        event.preventDefault();
-        self.val(
-          val.substring(0, start)
-          + '    '
-          + val.substring(end, val.length)
-        );
-        this.selectionStart = start + 4;
-        this.selectionEnd   = start + 4;
-      } else if (event.keyCode === 27) {
-        // escape
-        self.blur();
-      }
-    });
-
-    var unbindInlineAttachment = function($form) {
-      $form.unbind('.inlineattach');
-    };
-    var bindInlineAttachment = function($form, attachmentOption) {
-      var $this = $form;
-      var editor = createEditorInstance($form);
-      var inlineattach = new inlineAttachment(attachmentOption, editor);
-      $form.bind({
-        'paste.inlineattach': function(e) {
-          inlineattach.onPaste(e.originalEvent);
-        },
-        'drop.inlineattach': function(e) {
-          e.stopPropagation();
-          e.preventDefault();
-          inlineattach.onDrop(e.originalEvent);
-        },
-        'dragenter.inlineattach dragover.inlineattach': function(e) {
-          e.stopPropagation();
-          e.preventDefault();
-        }
-      });
-    };
-    var createEditorInstance = function($form) {
-      var $this = $form;
-
-      return {
-        getValue: function() {
-          return $this.val();
-        },
-        insertValue: function(val) {
-          inlineAttachment.util.insertTextAtCursor($this[0], val);
-        },
-        setValue: function(val) {
-          $this.val(val);
-        }
-      };
-    };
-
-    var $inputForm = $('textarea#form-body');
-    if ($inputForm.length > 0) {
-      var pageId = $('#content-main').data('page-id') || 0;
-      var attachmentOption = {
-        uploadUrl: '/_api/attachment/page/' + pageId,
-        extraParams: {
-          path: location.pathname
-        },
-        progressText: '(Uploading file...)',
-        urlText: "\n![file]({filename})\n"
-      };
-
-      attachmentOption.onFileUploadResponse = function(res) {
-        var result = JSON.parse(res.response);
-
-        if (result.status && result.pageCreated) {
-          var page = result.page,
-            pageId = page._id;
-
-          $('#content-main').data('page-id', page._id);
-          $('#page-form [name="pageForm[currentRevision]"]').val(page.revision)
-
-          unbindInlineAttachment($inputForm);
-
-          attachmentOption.uploadUrl = '/_api/attachment/page/' + pageId,
-          bindInlineAttachment($inputForm, attachmentOption);
-        }
-        return true;
-      };
-
-      bindInlineAttachment($inputForm, attachmentOption);
-    }
-
-    $('textarea#form-body').on('dragenter dragover', function() {
-        $(this).addClass('dragover');
-      });
-    $('textarea#form-body').on('drop dragleave dragend', function() {
-        $(this).removeClass('dragover');
-      });
-  });
-
-
-  </script>
+  <script src="/js/crowi-form{% if env  == 'production' %}.min{% endif %}.js"></script>
 </div>
 </div>

+ 83 - 0
lib/views/me/api_token.html

@@ -0,0 +1,83 @@
+{% extends '../layout/2column.html' %}
+
+
+{% block html_title %}APIの設定 · {{ path }}{% endblock %}
+
+{% block content_head %}
+<header  id="page-header">
+  <h1 class="title" id="">ユーザー設定</h1>
+</header>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+
+  <ul class="nav nav-tabs">
+    <li><a href="/me"><i class="fa fa-gears"></i> ユーザー情報</a></li>
+    <li><a href="/me/password"><i class="fa fa-key"></i> パスワード設定</a></li>
+    <li class="active"><a href="/me/apiToken"><i class="fa fa-rocket"></i> API設定</a></li>
+  </ul>
+
+  <div class="tab-content">
+
+  {% set message = req.flash('successMessage') %}
+  {% if message.length %}
+  <div class="alert alert-success">
+    {{ message }}
+  </div>
+  {% endif %}
+
+  {% if req.form.errors.length > 0 %}
+  <div class="alert alert-danger">
+    <ul>
+    {% for error in req.form.errors %}
+      <li>{{ error }}</li>
+    {% endfor %}
+    </ul>
+  </div>
+  {% endif %}
+
+  <div id="form-box">
+
+    <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
+    <fieldset>
+      <legend>API Token 設定</legend>
+      <div class="form-group {% if not user.password %}has-error{% endif %}">
+        <label for="" class="col-xs-2 control-label">現在のAPI Token</label>
+        <div class="col-xs-6">
+          {% if user.apiToken %}
+            <input class="form-control" type="text" value="{{ user.apiToken }}">
+          {% else %}
+          <p class="form-control-static">
+            API Token が設定されていません。更新するボタンから発行してください。
+          </p>
+          {% endif %}
+        </div>
+      </div>
+
+      <div class="form-group">
+        <div class="col-xs-offset-2 col-xs-10">
+
+          <p class="alert alert-warning">
+          API Token を更新すると、自動的に新しい Token が生成されます。<br>
+          現在の Token を利用している処理は動かなくなります。
+          </p>
+
+          <button type="submit" value="1" name="apiTokenForm[confirm]" class="btn btn-primary">API Tokenを更新する</button>
+        </div>
+      </div>
+
+    </fieldset>
+    </form>
+  </div>
+
+
+  </div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock %}
+
+{% block footer %}
+{% endblock %}

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

@@ -14,6 +14,7 @@
   <ul class="nav nav-tabs">
   <ul class="nav nav-tabs">
     <li class="active"><a href="/me"><i class="fa fa-gears"></i> ユーザー情報</a></li>
     <li class="active"><a href="/me"><i class="fa fa-gears"></i> ユーザー情報</a></li>
     <li><a href="/me/password"><i class="fa fa-key"></i> パスワード設定</a></li>
     <li><a href="/me/password"><i class="fa fa-key"></i> パスワード設定</a></li>
+    <li><a href="/me/apiToken"><i class="fa fa-rocket"></i> API設定</a></li>
   </ul>
   </ul>
 
 
   <div class="tab-content">
   <div class="tab-content">

+ 1 - 0
lib/views/me/password.html

@@ -14,6 +14,7 @@
   <ul class="nav nav-tabs">
   <ul class="nav nav-tabs">
     <li><a href="/me"><i class="fa fa-gears"></i> ユーザー情報</a></li>
     <li><a href="/me"><i class="fa fa-gears"></i> ユーザー情報</a></li>
     <li class="active"><a href="/me/password"><i class="fa fa-key"></i> パスワード設定</a></li>
     <li class="active"><a href="/me/password"><i class="fa fa-key"></i> パスワード設定</a></li>
+    <li><a href="/me/apiToken"><i class="fa fa-rocket"></i> API設定</a></li>
   </ul>
   </ul>
 
 
   <div class="tab-content">
   <div class="tab-content">

+ 0 - 1
lib/views/page.html

@@ -159,7 +159,6 @@
         var urlBase = res.data.fileBaseUrl;
         var urlBase = res.data.fileBaseUrl;
         if (attachments.length > 0) {
         if (attachments.length > 0) {
           $.each(attachments, function(i, file) {
           $.each(attachments, function(i, file) {
-            console.log(file);
             $pageAttachmentList.append(
             $pageAttachmentList.append(
             '<li><a href="' + urlBase + file.filePath + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
             '<li><a href="' + urlBase + file.filePath + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
             );
             );

+ 25 - 29
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "crowi",
   "name": "crowi",
-  "version": "1.2.0",
+  "version": "1.3.0",
   "description": "The simple & powerful Wiki",
   "description": "The simple & powerful Wiki",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -23,41 +23,42 @@
     "url": "https://github.com/crowi/crowi.git"
     "url": "https://github.com/crowi/crowi.git"
   },
   },
   "engines": {
   "engines": {
-    "node": "0.10.x",
-    "npm": "2.4.x"
+    "node": "4.2.x",
+    "npm": "3.3.x"
   },
   },
   "dependencies": {
   "dependencies": {
-    "async": "~0.9.0",
-    "aws-sdk": "~2.0.0-rc.19",
+    "async": "~1.5.0",
+    "aws-sdk": "~2.2.26",
     "basic-auth-connect": "~1.0.0",
     "basic-auth-connect": "~1.0.0",
-    "bluebird": "^2.9.12",
-    "body-parser": "~1.12.0",
-    "bower": "~1.4.0",
+    "bluebird": "~3.0.5",
+    "body-parser": "~1.14.1",
+    "bower": "~1.7.1",
     "cli": "~0.6.0",
     "cli": "~0.6.0",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-redis": "~2.1.0",
     "connect-redis": "~2.1.0",
     "consolidate": "~0.11.0",
     "consolidate": "~0.11.0",
     "cookie-parser": "~1.3.4",
     "cookie-parser": "~1.3.4",
-    "debug": "~2.1.0",
+    "debug": "~2.2.0",
     "errorhandler": "~1.3.4",
     "errorhandler": "~1.3.4",
-    "express": "~4.12.0",
+    "express": "~4.13.3",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
-    "express-session": "~1.10.0",
+    "express-session": "~1.12.0",
     "facebook-node-sdk": "=0.1.10",
     "facebook-node-sdk": "=0.1.10",
     "googleapis": "=0.4.7",
     "googleapis": "=0.4.7",
-    "gulp": "~3.8.11",
-    "gulp-concat": "^2.5.2",
-    "gulp-cssmin": "^0.1.7",
-    "gulp-jshint": "~1.10.0",
-    "gulp-rename": "^1.2.2",
-    "gulp-sass": "~2.0.4",
-    "gulp-spawn-mocha": "^2.2.1",
-    "gulp-uglify": "~1.2.0",
-    "gulp-watch": "~4.2.4",
-    "jshint-stylish": "^2.0.0",
+    "gulp": "~3.9.0",
+    "gulp-concat": "~2.6.0",
+    "gulp-cssmin": "~0.1.7",
+    "gulp-jshint": "~1.12.0",
+    "gulp-rename": "~1.2.2",
+    "gulp-sass": "~2.1.0",
+    "gulp-spawn-mocha": "~2.2.1",
+    "gulp-uglify": "~1.4.2",
+    "gulp-watch": "~4.3.5",
+    "jshint-stylish": "~2.1.0",
+    "kerberos": "0.0.17",
     "method-override": "~2.3.1",
     "method-override": "~2.3.1",
-    "mongoose": "~3.8.0",
-    "mongoose-paginate": "=3.1.3",
+    "mongoose": "4.2.5",
+    "mongoose-paginate": "4.2.0",
     "morgan": "~1.5.1",
     "morgan": "~1.5.1",
     "multer": "~0.1.8",
     "multer": "~0.1.8",
     "nodemailer": "~1.2.2",
     "nodemailer": "~1.2.2",
@@ -75,12 +76,7 @@
     "sinon": "~1.14.0",
     "sinon": "~1.14.0",
     "sinon-chai": "~2.7.0"
     "sinon-chai": "~2.7.0"
   },
   },
-  "license": [
-    {
-      "type": "MIT",
-      "url": "http://www.opensource.org/licenses/MIT"
-    }
-  ],
+  "license": "MIT",
   "scripts": {
   "scripts": {
     "start": "node app.js",
     "start": "node app.js",
     "test": "gulp test",
     "test": "gulp test",

+ 292 - 0
resource/js/crowi-form.js

@@ -0,0 +1,292 @@
+$(function() {
+  // preview watch
+  var originalContent = $('#form-body').val();
+  var prevContent = "";
+  var watchTimer = setInterval(function() {
+    var content = $('#form-body').val();
+    if (prevContent != content) {
+      var renderer = new Crowi.renderer($('#form-body').val(), $('#preview-body'));
+      renderer.render();
+
+      prevContent = content;
+    }
+  }, 500);
+
+  var getCurrentLine = function(event) {
+    var $target = $(event.target);
+
+    var text = $target.val();
+    var pos = $target.selection('getPos');
+    if (text === null || pos.start !== pos.end) {
+      return null;
+    }
+
+    var startPos = text.lastIndexOf("\n", pos.start - 1) + 1;
+    var endPos = text.indexOf("\n", pos.start);
+    if (endPos === -1) {
+      endPos = text.length;
+    }
+
+    return {
+      text: text.slice(startPos, endPos),
+      start: startPos,
+      end: endPos,
+      caret: pos.start,
+      endOfLine: !$.trim(text.slice(pos.start, endPos))
+    };
+  };
+
+  var getPrevLine = function(event) {
+    var $target = $(event.target);
+    var currentLine = getCurrentLine(event);
+    var text = $target.val().slice(0, currentLine.start);
+    var startPos = text.lastIndexOf("\n", currentLine.start - 2) + 1;
+    var endPos = currentLine.start;
+
+    return {
+      text: text.slice(startPos, endPos),
+      start: startPos,
+      end: endPos
+    };
+  };
+
+  var handleTabKey = function(event) {
+    event.preventDefault();
+
+    var $target = $(event.target);
+    var currentLine = getCurrentLine(event);
+    var text = $target.val();
+    var pos = $target.selection('getPos');
+
+    if (currentLine) {
+      $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
+    }
+
+    if (event.shiftKey === true) {
+      if (currentLine && currentLine.text.charAt(0) === '|') {
+        // prev cell in table
+        var newPos = text.lastIndexOf('|', pos.start - 1);
+        if (newPos > 0) {
+          $target.selection('setPos', {start: newPos - 1, end: newPos - 1});
+        }
+      } else {
+        // re indent
+        var reindentedText = $target.selection().replace(/^ {1,4}/gm, '');
+        var reindentedCount = $target.selection().length - reindentedText.length;
+        $target.selection('replace', {text: reindentedText, mode: 'before'});
+        if (currentLine) {
+          $target.selection('setPos', {start: pos.start - reindentedCount, end: pos.start - reindentedCount});
+        }
+      }
+    } else {
+      if (currentLine && currentLine.text.charAt(0) === '|') {
+        // next cell in table
+        var newPos = text.indexOf('|', pos.start + 1);
+        if (newPos < 0 || newPos === text.lastIndexOf('|', currentLine.end - 1)) {
+          $target.selection('setPos', {start: currentLine.end, end: currentLine.end});
+        } else {
+          $target.selection('setPos', {start: newPos + 2, end: newPos + 2});
+        }
+      } else {
+        // indent
+        $target.selection('replace', {
+          text: '    ' + $target.selection().split("\n").join("\n    "),
+          mode: 'before'
+        });
+        if (currentLine) {
+          $target.selection('setPos', {start: pos.start + 4, end: pos.start + 4});
+        }
+      }
+    }
+
+    $target.trigger('input');
+  };
+
+  var handleEnterKey = function(event) {
+    if (event.metaKey || event.ctrlKey || event.shiftKey) {
+      return;
+    }
+
+    var currentLine = getCurrentLine(event);
+    if (!currentLine || currentLine.start === currentLine.caret) {
+      return;
+    }
+
+    var $target = $(event.target);
+    var match = currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)\s*\S/);
+    if (match) {
+      // smart indent with list
+      if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] ))\s*$/)) {
+        // empty task list
+        $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
+        return;
+      }
+      event.preventDefault();
+      var listMark = match[1].replace(/\[x\]/, '[ ]');
+      var listMarkMatch = listMark.match(/^(\s*)(\d+)\./);
+      if (listMarkMatch) {
+        var indent = listMarkMatch[1];
+        var num = parseInt(listMarkMatch[2]);
+        if (num !== 1) {
+          listMark = listMark.return(/\s*\d+/, indent + (num +1));
+        }
+      }
+      $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
+    } else if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) )/)) {
+      // remove list
+      $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
+    } else if (currentLine.text.match(/^.*\|\s*$/)) {
+      // new row for table
+      if (currentLine.text.match(/^[\|\s]+$/)) {
+        $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
+        return;
+      }
+      if (!currentLine.endOfLine) {
+        return;
+      }
+      event.preventDefault();
+      var row = [];
+      var cellbarMatch = currentLine.text.match(/\|/g);
+      for (var i = 0; i < cellbarMatch.length; i++) {
+        row.push('|');
+      }
+      var prevLine = getPrevLine(event);
+      if (!prevLine || (!currentLine.text.match(/---/) && !prevLine.text.match(/\|/g))) {
+        $target.selection('insert', {text: "\n" + row.join(' --- ') + "\n" + row.join('  '), mode: 'before'});
+        $target.selection('setPos', {start: currentLine.caret + 6 * row.length - 1, end: currentLine.caret + 6 * row.length - 1});
+      } else {
+        $target.selection('insert', {text: "\n" + row.join('  '), mode: 'before'});
+        $target.selection('setPos', {start: currentLine.caret + 3, end: currentLine.caret + 3});
+      }
+    }
+
+    $target.trigger('input');
+  };
+
+  var handleEscapeKey = function(event) {
+    event.preventDefault();
+    var $target = $(event.target);
+    $target.blur();
+  };
+
+  var handleSpaceKey = function(event) {
+    // keybind: alt + shift + space
+    if (!(event.shiftKey && event.altKey)) {
+      return;
+    }
+    var currentLine = getCurrentLine(event);
+    if (!currentLine) {
+      return;
+    }
+
+    var $target = $(event.target);
+    var match = currentLine.text.match(/^(\s*)(-|\+|\*|\d+\.) (?:\[(x| )\] )(.*)/);
+    if (match) {
+      event.preventDefault();
+      var checkMark = (match[3] == ' ') ? 'x' : ' ';
+      var replaceTo = match[1] + match[2] + ' [' + checkMark + '] ' + match[4];
+      $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
+      $target.selection('replace', {text: replaceTo, mode: 'keep'});
+      $target.selection('setPos', {start: currentLine.caret, end: currentLine.caret});
+      $target.trigger('input');
+    }
+  };
+
+  // markdown helper inspired by 'esarea'.
+  // see: https://github.com/fukayatsu/esarea
+  $('textarea#form-body').on('keydown', function(event) {
+    switch (event.which || event.keyCode) {
+      case 9:
+        handleTabKey(event);
+        break;
+      case 13:
+        handleEnterKey(event);
+        break;
+      case 27:
+        handleEscapeKey(event);
+        break;
+      case 32:
+        handleSpaceKey(event);
+        break;
+      default:
+    }
+  });
+
+  var unbindInlineAttachment = function($form) {
+    $form.unbind('.inlineattach');
+  };
+  var bindInlineAttachment = function($form, attachmentOption) {
+    var $this = $form;
+    var editor = createEditorInstance($form);
+    var inlineattach = new inlineAttachment(attachmentOption, editor);
+    $form.bind({
+      'paste.inlineattach': function(e) {
+        inlineattach.onPaste(e.originalEvent);
+      },
+      'drop.inlineattach': function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+        inlineattach.onDrop(e.originalEvent);
+      },
+      'dragenter.inlineattach dragover.inlineattach': function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+      }
+    });
+  };
+  var createEditorInstance = function($form) {
+    var $this = $form;
+
+    return {
+      getValue: function() {
+        return $this.val();
+      },
+      insertValue: function(val) {
+        inlineAttachment.util.insertTextAtCursor($this[0], val);
+      },
+      setValue: function(val) {
+        $this.val(val);
+      }
+    };
+  };
+
+  var $inputForm = $('form.uploadable textarea#form-body');
+  if ($inputForm.length > 0) {
+    var pageId = $('#content-main').data('page-id') || 0;
+    var attachmentOption = {
+      uploadUrl: '/_api/attachment/page/' + pageId,
+      extraParams: {
+        path: location.pathname
+      },
+      progressText: '(Uploading file...)',
+      urlText: "\n![file]({filename})\n"
+    };
+
+    attachmentOption.onFileUploadResponse = function(res) {
+      var result = JSON.parse(res.response);
+
+      if (result.status && result.pageCreated) {
+        var page = result.page,
+            pageId = page._id;
+
+        $('#content-main').data('page-id', page._id);
+        $('#page-form [name="pageForm[currentRevision]"]').val(page.revision)
+
+        unbindInlineAttachment($inputForm);
+
+        attachmentOption.uploadUrl = '/_api/attachment/page/' + pageId,
+        bindInlineAttachment($inputForm, attachmentOption);
+      }
+      return true;
+    };
+
+    bindInlineAttachment($inputForm, attachmentOption);
+
+    $('textarea#form-body').on('dragenter dragover', function() {
+      $(this).addClass('dragover');
+    });
+    $('textarea#form-body').on('drop dragleave dragend', function() {
+      $(this).removeClass('dragover');
+    });
+  }
+});