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

Merge branch 'master' into slack-notification

Sotaro KARASAWA 10 лет назад
Родитель
Сommit
570326f599

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@ node_modules/
 bower_components/
 bower_components/
 public/js/*
 public/js/*
 public/css/*
 public/css/*
+public/uploads/*

+ 1 - 0
README.md

@@ -61,6 +61,7 @@ $ PASSWORD_SEED=somesecretstring MONGO_URI=mongodb://username:password@localhost
 * `REDIS_URL`: URI to connect Redis (to session store). This parameter is also by `REDISTOGO_URL`.
 * `REDIS_URL`: URI to connect Redis (to session store). This parameter is also by `REDISTOGO_URL`.
 * `PASSWORD_SEED`: A password seed is used by password hash generator.
 * `PASSWORD_SEED`: A password seed is used by password hash generator.
 * `SECRET_TOKEN`: A secret key for verifying the integrity of signed cookies.
 * `SECRET_TOKEN`: A secret key for verifying the integrity of signed cookies.
+* `FILE_UPLOAD`: `aws` (default), `local`, `none`
 
 
 
 
 License
 License

+ 9 - 0
lib/models/attachment.js

@@ -3,6 +3,7 @@ module.exports = function(crowi) {
     , mongoose = require('mongoose')
     , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
     , Promise = require('bluebird')
     , Promise = require('bluebird')
+    , fileUploader = require('../util/fileUploader')(crowi)
   ;
   ;
 
 
   function generateFileHash (fileName) {
   function generateFileHash (fileName) {
@@ -21,6 +22,14 @@ module.exports = function(crowi) {
     fileFormat: { type: String, required: true },
     fileFormat: { type: String, required: true },
     fileSize: { type: Number, default: 0 },
     fileSize: { type: Number, default: 0 },
     createdAt: { type: Date, default: Date.now }
     createdAt: { type: Date, default: Date.now }
+  }, {
+    toJSON: {
+      virtuals: true
+    }
+  });
+
+  attachmentSchema.virtual('fileUrl').get(function() {
+    return fileUploader.generateUrl(this.filePath);
   });
   });
 
 
   attachmentSchema.statics.getListByPageId = function(id) {
   attachmentSchema.statics.getListByPageId = function(id) {

+ 29 - 19
lib/models/bookmark.js

@@ -12,32 +12,32 @@ module.exports = function(crowi) {
   });
   });
   bookmarkSchema.index({page: 1, user: 1}, {unique: true});
   bookmarkSchema.index({page: 1, user: 1}, {unique: true});
 
 
-  bookmarkSchema.statics.populatePage = function(bookmarks) {
+  bookmarkSchema.statics.populatePage = function(bookmarks, requestUser) {
     var Bookmark = this;
     var Bookmark = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
     var Page = crowi.model('Page');
     var Page = crowi.model('Page');
 
 
-    return new Promise(function(resolve, reject) {
-      Bookmark.populate(bookmarks, {path: 'page'}, function(err, bookmarks) {
-        if (err) {
-          return reject(err);
-        }
-
-        Bookmark.populate(bookmarks, {path: 'page.revision', model: 'Revision'}, function(err, bookmarks) {
-          if (err) {
-            return reject(err);
+    requestUser = requestUser || null;
+
+    // mongoose promise に置き換えてみたものの、こいつは not native promise but original promise だったので
+    // これ以上は置き換えないことにする ...
+    // @see http://eddywashere.com/blog/switching-out-callbacks-with-promises-in-mongoose/
+    return Bookmark.populate(bookmarks, {path: 'page'})
+      .then(function(bookmarks) {
+        return Bookmark.populate(bookmarks, {path: 'page.revision', model: 'Revision'});
+      }).then(function(bookmarks) {
+        // hmm...
+        bookmarks = bookmarks.filter(function(bookmark) {
+          // requestUser を指定しない場合 public のみを返す
+          if (requestUser === null) {
+            return bookmark.page.isPublic();
           }
           }
 
 
-          Bookmark.populate(bookmarks, {path: 'page.revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, bookmarks) {
-            if (err) {
-              return reject(err);
-            }
-
-            return resolve(bookmarks);
-          });
+          return bookmark.page.isGrantedFor(requestUser);
         });
         });
+
+        return Bookmark.populate(bookmarks, {path: 'page.revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS});
       });
       });
-    });
   };
   };
 
 
   // bookmark チェック用
   // bookmark チェック用
@@ -55,9 +55,19 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  /**
+   * option = {
+   *  limit: Int
+   *  offset: Int
+   *  requestUser: User
+   * }
+   */
   bookmarkSchema.statics.findByUser = function(user, option) {
   bookmarkSchema.statics.findByUser = function(user, option) {
     var User = crowi.model('User');
     var User = crowi.model('User');
     var Bookmark = this;
     var Bookmark = this;
+    var requestUser = option.requestUser || null;
+
+    debug('Finding bookmark with requesting user:', requestUser);
 
 
     var limit = option.limit || 50;
     var limit = option.limit || 50;
     var offset = option.offset || 0;
     var offset = option.offset || 0;
@@ -78,7 +88,7 @@ module.exports = function(crowi) {
             return resolve(bookmarks);
             return resolve(bookmarks);
           }
           }
 
 
-          return Bookmark.populatePage(bookmarks).then(resolve).catch(reject);
+          return Bookmark.populatePage(bookmarks, requestUser).then(resolve);
         });
         });
     });
     });
   };
   };

+ 6 - 3
lib/models/config.js

@@ -169,14 +169,17 @@ module.exports = function(crowi) {
 
 
   configSchema.statics.isUploadable = function(config)
   configSchema.statics.isUploadable = function(config)
   {
   {
-    if (!config.crowi['aws:accessKeyId'] ||
+    var method = crowi.env.FILE_UPLOAD || 'aws';
+
+    if (method == 'aws' && (
+        !config.crowi['aws:accessKeyId'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:region'] ||
         !config.crowi['aws:region'] ||
-        !config.crowi['aws:bucket']) {
+        !config.crowi['aws:bucket'])) {
       return false;
       return false;
     }
     }
 
 
-    return true;
+    return method != 'none';
   };
   };
 
 
   configSchema.statics.hasSlackConfig = function(config)
   configSchema.statics.hasSlackConfig = function(config)

+ 37 - 33
lib/models/page.js

@@ -444,6 +444,9 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  /**
+   * とりあえず、公開ページであり、redirectTo が無いものだけを出すためだけのAPI
+   */
   pageSchema.statics.findListByCreator = function(user, option) {
   pageSchema.statics.findListByCreator = function(user, option) {
     var Page = this;
     var Page = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
@@ -452,30 +455,31 @@ module.exports = function(crowi) {
 
 
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       Page
       Page
-      .find({ creator: user._id, grant: GRANT_PUBLIC })
+      .find({ creator: user._id, grant: GRANT_PUBLIC, redirectTo: null })
       .sort({createdAt: -1})
       .sort({createdAt: -1})
       .skip(offset)
       .skip(offset)
       .limit(limit)
       .limit(limit)
       .populate('revision')
       .populate('revision')
-      .exec(function(err, pages) {
-        if (err) {
-          return reject(err);
-        }
-
-        Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, data) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(data);
-        });
+      .exec()
+      .then(function(pages) {
+        return Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS});
+      }).then(function(pages) {
+        return resolve(data);
       });
       });
     });
     });
   };
   };
 
 
+  /**
+   * findListByStartWith
+   *
+   * If `path` has `/` at the end, returns '{path}/*' and '{path}' self.
+   * If `path` doesn't have `/` at the end, returns '{path}*'
+   * e.g.
+   */
   pageSchema.statics.findListByStartWith = function(path, userData, option) {
   pageSchema.statics.findListByStartWith = function(path, userData, option) {
     var Page = this;
     var Page = this;
     var User = crowi.model('User');
     var User = crowi.model('User');
+    var pathCondition = [];
 
 
     if (!option) {
     if (!option) {
       option = {sort: 'updatedAt', desc: -1, offset: 0, limit: 50};
       option = {sort: 'updatedAt', desc: -1, offset: 0, limit: 50};
@@ -491,10 +495,15 @@ module.exports = function(crowi) {
     var queryReg = new RegExp('^' + path);
     var queryReg = new RegExp('^' + path);
     var sliceOption = option.revisionSlice || {$slice: 1};
     var sliceOption = option.revisionSlice || {$slice: 1};
 
 
+    pathCondition.push({path: queryReg});
+    if (path.match(/\/$/)) {
+      debug('Page list by ending with /, so find also upper level page');
+      pathCondition.push({path: path.substr(0, path.length -1)});
+    }
+
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       // FIXME: might be heavy
       // FIXME: might be heavy
       var q = Page.find({
       var q = Page.find({
-        path: queryReg,
         redirectTo: null,
         redirectTo: null,
         $or: [
         $or: [
           {grant: null},
           {grant: null},
@@ -502,26 +511,21 @@ module.exports = function(crowi) {
           {grant: GRANT_RESTRICTED, grantedUsers: userData._id},
           {grant: GRANT_RESTRICTED, grantedUsers: userData._id},
           {grant: GRANT_SPECIFIED, grantedUsers: userData._id},
           {grant: GRANT_SPECIFIED, grantedUsers: userData._id},
           {grant: GRANT_OWNER, grantedUsers: userData._id},
           {grant: GRANT_OWNER, grantedUsers: userData._id},
-        ],
+        ],})
+        .populate('revision')
+        .and({
+          $or: pathCondition
+        })
+        .sort(sortOpt)
+        .skip(opt.offset)
+        .limit(opt.limit);
+
+      q.exec()
+      .then(function(pages) {
+        Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS})
+        .then(resolve)
+        .catch(reject);
       })
       })
-      .populate('revision')
-      .sort(sortOpt)
-      .skip(opt.offset)
-      .limit(opt.limit);
-
-      q.exec(function(err, pages) {
-        if (err) {
-          return reject(err);
-        }
-
-        Page.populate(pages, {path: 'revision.author', model: 'User', select: User.USER_PUBLIC_FIELDS}, function(err, data) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(data);
-        });
-      });
     });
     });
   };
   };
 
 

+ 1 - 1
lib/models/user.js

@@ -60,7 +60,7 @@ module.exports = function(crowi) {
 
 
   function generatePassword (password) {
   function generatePassword (password) {
     var hasher = crypto.createHash('sha256');
     var hasher = crypto.createHash('sha256');
-    hasher.update(process.env.PASSWORD_SEED + password);
+    hasher.update(crowi.env.PASSWORD_SEED + password);
 
 
     return hasher.digest('hex');
     return hasher.digest('hex');
   }
   }

+ 2 - 3
lib/routes/attachment.js

@@ -8,6 +8,7 @@ module.exports = function(crowi, app) {
     , Promise = require('bluebird')
     , Promise = require('bluebird')
     , config = crowi.getConfig()
     , config = crowi.getConfig()
     , fs = require('fs')
     , fs = require('fs')
+    , fileUploader = require('../util/fileUploader')(crowi, app)
     , actions = {}
     , actions = {}
     , api = {};
     , api = {};
 
 
@@ -21,7 +22,6 @@ module.exports = function(crowi, app) {
       res.json({
       res.json({
         status: true,
         status: true,
         data: {
         data: {
-          fileBaseUrl: 'https://' + config.crowi['aws:bucket'] +'.s3.amazonaws.com/', // FIXME: ベタ書きよくない
           attachments: attachments
           attachments: attachments
         }
         }
       });
       });
@@ -39,7 +39,6 @@ module.exports = function(crowi, app) {
 
 
     debug('id and path are: ', id, path);
     debug('id and path are: ', id, path);
 
 
-    var fileUploader = require('../util/fileUploader')(crowi, app);
     var tmpFile = req.files.file || null;
     var tmpFile = req.files.file || null;
     debug('Uploaded tmpFile: ', tmpFile);
     debug('Uploaded tmpFile: ', tmpFile);
     if (!tmpFile) {
     if (!tmpFile) {
@@ -80,7 +79,7 @@ module.exports = function(crowi, app) {
           // TODO size
           // TODO size
           return Attachment.create(id, req.user, filePath, originalName, fileName, fileType, fileSize);
           return Attachment.create(id, req.user, filePath, originalName, fileName, fileType, fileSize);
         }).then(function(data) {
         }).then(function(data) {
-          var imageUrl = fileUploader.generateS3FileUrl(data.filePath);
+          var imageUrl = fileUploader.generateUrl(data.filePath);
           return res.json({
           return res.json({
             status: true,
             status: true,
             filename: imageUrl,
             filename: imageUrl,

+ 1 - 1
lib/routes/me.js

@@ -50,7 +50,7 @@ module.exports = function(crowi, app) {
 
 
     fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
     fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
     .then(function(data) {
     .then(function(data) {
-      var imageUrl = fileUploader.generateS3FileUrl(filePath);
+      var imageUrl = fileUploader.generateUrl(filePath);
       req.user.updateImage(imageUrl, function(err, data) {
       req.user.updateImage(imageUrl, function(err, data) {
         fs.unlink(tmpPath, function (err) {
         fs.unlink(tmpPath, function (err) {
           // エラー自体は無視
           // エラー自体は無視

+ 3 - 3
lib/routes/page.js

@@ -81,7 +81,7 @@ module.exports = function(crowi, app) {
     .then(function(portalPage) {
     .then(function(portalPage) {
       renderVars.page = portalPage;
       renderVars.page = portalPage;
 
 
-      return Page.findListByStartWith(path.substr(0, path.length -1), req.user, queryOptions);
+      return Page.findListByStartWith(path, req.user, queryOptions);
     }).then(function(pageList) {
     }).then(function(pageList) {
 
 
       if (pageList.length > limit) {
       if (pageList.length > limit) {
@@ -133,7 +133,7 @@ module.exports = function(crowi, app) {
           userData = data;
           userData = data;
           renderVars.pageUser = userData;
           renderVars.pageUser = userData;
 
 
-          return Bookmark.findByUser(userData, {limit: 10, populatePage: true});
+          return Bookmark.findByUser(userData, {limit: 10, populatePage: true, requestUser: req.user});
         }).then(function(bookmarkList) {
         }).then(function(bookmarkList) {
           debug(bookmarkList);
           debug(bookmarkList);
           renderVars.bookmarkList = bookmarkList;
           renderVars.bookmarkList = bookmarkList;
@@ -281,7 +281,7 @@ module.exports = function(crowi, app) {
     var renderVars = {};
     var renderVars = {};
 
 
     var pagerOptions = { offset: offset, limit : limit };
     var pagerOptions = { offset: offset, limit : limit };
-    var queryOptions = { offset: offset, limit : limit + 1, populatePage: true};
+    var queryOptions = { offset: offset, limit : limit + 1, populatePage: true, requestUser: req.user};
 
 
     User.findUserByUsername(username)
     User.findUserByUsername(username)
     .then(function(user) {
     .then(function(user) {

+ 4 - 61
lib/util/fileUploader.js

@@ -2,69 +2,12 @@
  * fileUploader
  * fileUploader
  */
  */
 
 
-
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   'use strict';
   'use strict';
 
 
-  var aws = require('aws-sdk')
-    , debug = require('debug')('crowi:lib:fileUploader')
-    , Promise = require('bluebird')
-    , Config = crowi.model('Config')
-    , config = crowi.getConfig()
-    , lib = {}
-    ;
-
-  lib.getAwsConfig = function()
-  {
-    return {
-      accessKeyId: config.crowi['aws:accessKeyId'],
-      secretAccessKey: config.crowi['aws:secretAccessKey'],
-      region: config.crowi['aws:region'],
-      bucket: config.crowi['aws:bucket']
-    };
-  };
-
-  // lib.deleteFile = function(filePath, callback) {
-  //   // TODO 実装する
-  // };
-  //
-
-  lib.uploadFile = function(filePath, contentType, fileStream, options) {
-    var awsConfig = lib.getAwsConfig();
-    if (!Config.isUploadable(config)) {
-      return new Promise.reject(new Error('AWS is not configured.'));
-    }
-
-    aws.config.update({
-      accessKeyId: awsConfig.accessKeyId,
-      secretAccessKey: awsConfig.secretAccessKey,
-      region: awsConfig.region
-    });
-    var s3 = new aws.S3();
-
-    var params = {Bucket: awsConfig.bucket};
-    params.ContentType = contentType;
-    params.Key = filePath;
-    params.Body = fileStream;
-    params.ACL = 'public-read';
-
-    return new Promise(function(resolve, reject) {
-      s3.putObject(params, function(err, data) {
-        if (err) {
-          return reject(err);
-        }
-
-        return resolve(data);
-      });
-    });
-  };
-
-  lib.generateS3FileUrl = function(filePath) {
-    var awsConfig = lib.getAwsConfig();
-    var url = 'https://' + awsConfig.bucket +'.s3.amazonaws.com/' + filePath;
-
-    return url;
-  };
+  var debug = require('debug')('crowi:lib:fileUploader')
+    , method = crowi.env.FILE_UPLOAD || 'aws'
+    , lib = '../../local_modules/crowi-fileupload-' + method;
 
 
-  return lib;
+  return require(lib)(crowi);
 };
 };

+ 5 - 3
lib/views/admin/app.html

@@ -3,9 +3,11 @@
 {% block html_title %}アプリ設定 · {% endblock %}
 {% block html_title %}アプリ設定 · {% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">アプリ設定</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">アプリ設定</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 5 - 3
lib/views/admin/index.html

@@ -3,9 +3,11 @@
 {% block html_title %}Wiki管理 · {{ path }}{% endblock %}
 {% block html_title %}Wiki管理 · {{ path }}{% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">Wiki管理</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">Wiki管理</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 5 - 3
lib/views/admin/users.html

@@ -3,9 +3,11 @@
 {% block html_title %}ユーザー管理 · {% endblock %}
 {% block html_title %}ユーザー管理 · {% endblock %}
 
 
 {% block content_head %}
 {% block content_head %}
-<header id="page-header">
-  <h1 class="title" id="">ユーザー管理</h1>
-</header>
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">ユーザー管理</h1>
+  </header>
+</div>
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}

+ 0 - 37
lib/views/page.html

@@ -150,37 +150,6 @@
     </div>
     </div>
 
 
   </div>
   </div>
-  <script type="text/javascript">
-    $(function(){
-        var renderer = new Crowi.renderer($('#raw-text-original').html());
-        renderer.render();
-        Crowi.correctHeaders('#revision-body-content');
-        Crowi.revisionToc('#revision-body-content', '#revision-toc');
-
-        $('#edit-form').submit(function()
-        {
-          //console.log('save');
-          //return false;
-        });
-
-        //data-spy="affix" data-offset-top="80"
-        var headerHeight = $('#page-header').outerHeight(true);
-        $('.header-wrap').css({height: headerHeight + 'px'});
-        $('#page-header').affix({
-          offset: {
-            top: function() {
-              return headerHeight + 74; // (54 header + 20 padding-top)
-            }
-          }
-        });
-        $('[data-affix-disable]').on('click', function(e) {
-          $elm = $($(this).data('affix-disable'));
-          $(window).off('.affix');
-          $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
-          return false;
-        });
-    });
-  </script>
   {% endif %}
   {% endif %}
 </div>
 </div>
 
 
@@ -218,12 +187,6 @@
 <div id="notifPageEdited" class="fk-hide fk-notif fk-notif-danger"><i class="fa fa-exclamation-triangle"></i> <span class="edited-user"></span>さんがこのページを編集しました。 <a href="javascript:location.reload();"><i class="fa fa-angle-double-right"></i> 最新版を読み込む</a></div>
 <div id="notifPageEdited" class="fk-hide fk-notif fk-notif-danger"><i class="fa fa-exclamation-triangle"></i> <span class="edited-user"></span>さんがこのページを編集しました。 <a href="javascript:location.reload();"><i class="fa fa-angle-double-right"></i> 最新版を読み込む</a></div>
 <div id="notifPageEditing" class="fk-hide fk-notif fk-notif-warning"><i class="fa fa-exclamation-triangle"></i> 他の人がこのページの編集を開始しました。</div>
 <div id="notifPageEditing" class="fk-hide fk-notif fk-notif-warning"><i class="fa fa-exclamation-triangle"></i> 他の人がこのページの編集を開始しました。</div>
 
 
-<div id="portal-warning-for-page" class="portal-warning-for-page alert alert-danger alert-dismissible" role="alert">
-  <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-
-  <strong>Warning!</strong> /user/hoge のページが存在します。このページをポータル化するには、/ に移動し、「ページを移動」させてください。<br>
-/ とは別に ポータル を作成する場合、このまま編集を続けて作成してください。
-</div>
 
 
 <script>
 <script>
   $(function() {
   $(function() {

+ 1 - 1
lib/views/page_list.html

@@ -41,7 +41,7 @@
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   >
   >
 
 
-<div class="portal {% if not page %}hide{% endif %}">
+<div class="portal {% if not page or req.query.offset > 0 %}hide{% endif %}">
 
 
   <ul class="nav nav-tabs hidden-print">
   <ul class="nav nav-tabs hidden-print">
    {# portal tab #}
    {# portal tab #}

+ 68 - 0
local_modules/crowi-fileupload-aws/index.js

@@ -0,0 +1,68 @@
+// crowi-fileupload-aws
+
+module.exports = function(crowi) {
+  'use strict';
+
+  var aws = require('aws-sdk')
+    , debug = require('debug')('crowi:lib:fileUploaderAws')
+    , Promise = require('bluebird')
+    , Config = crowi.model('Config')
+    , config = crowi.getConfig()
+    , lib = {}
+    , getAwsConfig = function() {
+        var config = crowi.getConfig();
+        return {
+          accessKeyId: config.crowi['aws:accessKeyId'],
+          secretAccessKey: config.crowi['aws:secretAccessKey'],
+          region: config.crowi['aws:region'],
+          bucket: config.crowi['aws:bucket']
+        };
+      };
+
+  lib.deleteFile = function(filePath) {
+    return new Promise(function(resolve, reject) {
+      debug('Unsupported file deletion.');
+      resolve('TODO: ...');
+    });
+  };
+
+  lib.uploadFile = function(filePath, contentType, fileStream, options) {
+    var awsConfig = getAwsConfig();
+    if (!Config.isUploadable(config)) {
+      return new Promise.reject(new Error('AWS is not configured.'));
+    }
+
+    aws.config.update({
+      accessKeyId: awsConfig.accessKeyId,
+      secretAccessKey: awsConfig.secretAccessKey,
+      region: awsConfig.region
+    });
+    var s3 = new aws.S3();
+
+    var params = {Bucket: awsConfig.bucket};
+    params.ContentType = contentType;
+    params.Key = filePath;
+    params.Body = fileStream;
+    params.ACL = 'public-read';
+
+    return new Promise(function(resolve, reject) {
+      s3.putObject(params, function(err, data) {
+        if (err) {
+          return reject(err);
+        }
+
+        return resolve(data);
+      });
+    });
+  };
+
+  lib.generateUrl = function(filePath) {
+    var awsConfig = getAwsConfig()
+      , url = 'https://' + awsConfig.bucket +'.s3.amazonaws.com/' + filePath;
+
+    return url;
+  };
+
+  return lib;
+};
+

+ 61 - 0
local_modules/crowi-fileupload-local/index.js

@@ -0,0 +1,61 @@
+// crowi-fileupload-local
+
+module.exports = function(crowi) {
+  'use strict';
+
+  var debug = require('debug')('crowi:lib:fileUploaderLocal')
+    , fs = require('fs')
+    , path = require('path')
+    , mkdir = require('mkdirp')
+    , Promise = require('bluebird')
+    , Config = crowi.model('Config')
+    , config = crowi.getConfig()
+    , lib = {}
+    , basePath = path.join(crowi.publicDir, 'uploads'); // TODO: to configurable
+
+  lib.deleteFile = function(filePath) {
+    debug('File deletion: ' + filePath);
+    return new Promise(function(resolve, reject) {
+      fs.unlink(path.join(basePath, filePath), function(err) {
+        if (err) {
+          debug(err);
+          return reject(err);
+        }
+
+        resolve();
+      });
+    });
+  };
+
+  lib.uploadFile = function(filePath, contentType, fileStream, options) {
+    debug('File uploading: ' + filePath);
+    return new Promise(function(resolve, reject) {
+      var localFilePath = path.join(basePath, filePath)
+        , dirpath = path.dirname(localFilePath);
+
+      mkdir(dirpath, function(err) {
+        if (err) {
+          return reject(err);
+        }
+
+        var writer = fs.createWriteStream(localFilePath);
+
+        writer.on('error', function(err) {
+          reject(err);
+        }).on('finish', function() {
+          resolve();
+        });
+
+        fileStream.pipe(writer);
+      });
+    });
+  };
+
+  lib.generateUrl = function(filePath) {
+    return path.join('/uploads', filePath);
+  };
+
+  return lib;
+};
+
+

Разница между файлами не показана из-за своего большого размера
+ 25 - 0
local_modules/crowi-fileupload-none/index.js


+ 1 - 0
package.json

@@ -66,6 +66,7 @@
     "kerberos": "0.0.17",
     "kerberos": "0.0.17",
     "marked": "~0.3.5",
     "marked": "~0.3.5",
     "method-override": "~2.3.1",
     "method-override": "~2.3.1",
+    "mkdirp": "^0.5.1",
     "mongoose": "4.2.5",
     "mongoose": "4.2.5",
     "mongoose-paginate": "4.2.0",
     "mongoose-paginate": "4.2.0",
     "morgan": "~1.5.1",
     "morgan": "~1.5.1",

+ 3 - 0
resource/css/_form.scss

@@ -1,4 +1,6 @@
 .crowi.main-container .main .content-main.on-edit { // {{{ Edit Form of Page
 .crowi.main-container .main .content-main.on-edit { // {{{ Edit Form of Page
+  padding: 0;
+
   position: fixed;
   position: fixed;
   z-index: 1060;
   z-index: 1060;
   background: #fff;
   background: #fff;
@@ -105,6 +107,7 @@
 } // }}}
 } // }}}
 
 
 .crowi.main-container .main .page-list.content-main { // {{{ Edit Form of Page List
 .crowi.main-container .main .page-list.content-main { // {{{ Edit Form of Page List
+
   .close-button {
   .close-button {
     display: none;
     display: none;
   }
   }

+ 12 - 1
resource/css/_layout.scss

@@ -61,6 +61,18 @@
 
 
     } // }}}
     } // }}}
 
 
+    .main {
+      padding: 0; // cancel bootstrap padding
+
+      .header-wrap {
+        padding: 16px 16px 0 16px;
+      }
+
+      .content-main {
+        padding: 16px;
+      }
+    }
+
     .layout-control { // {{{
     .layout-control { // {{{
       transition: .3s ease;
       transition: .3s ease;
       -webkit-transition: .3s ease;
       -webkit-transition: .3s ease;
@@ -249,7 +261,6 @@
     .main {
     .main {
       article header {
       article header {
         border-bottom: solid 1px #666;
         border-bottom: solid 1px #666;
-        margin-bottom: 20px;
         h1 {
         h1 {
           font-size: 2em;
           font-size: 2em;
           color: #000;
           color: #000;

+ 3 - 3
resource/css/_page.scss

@@ -1,11 +1,11 @@
 .crowi.main-container {
 .crowi.main-container {
+  // padding controll of .header-wrap and .content-main are moved to _layout and _form
 
 
   .main { // {{{ .main of layout related
   .main { // {{{ .main of layout related
     transition: .5s ease;
     transition: .5s ease;
     -webkit-transition: .5s ease;
     -webkit-transition: .5s ease;
     background: #fff;
     background: #fff;
 
 
-    padding: 20px;
     article {
     article {
       background: #fff;
       background: #fff;
     }
     }
@@ -65,6 +65,7 @@
       h1 {
       h1 {
         font-size: 28px;
         font-size: 28px;
         margin-top: 0;
         margin-top: 0;
+        margin-bottom: 0;
 
 
         a:last-child {
         a:last-child {
           color: #D1E2E4;
           color: #D1E2E4;
@@ -89,7 +90,7 @@
   .main.grant-specified,
   .main.grant-specified,
   .main.grant-owner {
   .main.grant-owner {
     background: #333;
     background: #333;
-    padding: 20px 10px;
+    padding: 16px;
 
 
     .page-grant {
     .page-grant {
       color: #ccc;
       color: #ccc;
@@ -97,7 +98,6 @@
 
 
     article {
     article {
       border-radius: 5px;
       border-radius: 5px;
-      padding: 20px;
     }
     }
   }
   }
   // }}}
   // }}}

+ 0 - 8
resource/css/_user.scss

@@ -1,9 +1,7 @@
 .crowi.main-container {
 .crowi.main-container {
   .main.user-page { // {{{ .main of layout related
   .main.user-page { // {{{ .main of layout related
-    padding: 0;
 
 
     .header-wrap {
     .header-wrap {
-      padding: 20px;
 
 
       h1 {
       h1 {
         margin: 0;
         margin: 0;
@@ -58,11 +56,5 @@
       }
       }
 
 
     }
     }
-    .content-main {
-      padding: 20px;
-      &.on-edit {
-        padding: 0;
-      }
-    }
   } // }}}
   } // }}}
 }
 }

+ 0 - 6
resource/css/crowi.scss

@@ -62,12 +62,6 @@ footer, aside {
   margin-bottom: 1em;
   margin-bottom: 1em;
 }
 }
 
 
-article {
-  header {
-    margin-bottom: 20px;
-  }
-}
-
 footer {
 footer {
   h4,
   h4,
   h3 {
   h3 {

+ 31 - 1
resource/js/crowi-form.js

@@ -128,7 +128,7 @@ $(function() {
         var indent = listMarkMatch[1];
         var indent = listMarkMatch[1];
         var num = parseInt(listMarkMatch[2]);
         var num = parseInt(listMarkMatch[2]);
         if (num !== 1) {
         if (num !== 1) {
-          listMark = listMark.return(/\s*\d+/, indent + (num +1));
+          listMark = listMark.replace(/\s*\d+/, indent + (num +1));
         }
         }
       }
       }
       $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
       $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
@@ -212,6 +212,36 @@ $(function() {
     }
     }
   });
   });
 
 
+  var handlePasteEvent = function(event) {
+    var currentLine = getCurrentLine(event);
+
+    if (!currentLine) {
+      return false;
+    }
+    var $target = $(event.target);
+    var pasteText = event.clipboardData.getData('text');
+
+    var match = currentLine.text.match(/^(\s*(?:>|\-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)/);
+    if (match) {
+      if (pasteText.match(/(?:\r\n|\r|\n)/)) {
+        pasteText = pasteText.replace(/(\r\n|\r|\n)/g, "$1" + match[1]);
+      }
+    }
+
+    $target.selection('insert', {text: pasteText, mode: 'after'});
+
+    var newPos = currentLine.end + pasteText.length;
+    $target.selection('setPos', {start: newPos, end: newPos});
+
+    return true;
+  };
+
+  document.getElementById('form-body').addEventListener('paste', function(event) {
+    if (handlePasteEvent(event)) {
+      event.preventDefault();
+    }
+  });
+
   var unbindInlineAttachment = function($form) {
   var unbindInlineAttachment = function($form) {
     $form.unbind('.inlineattach');
     $form.unbind('.inlineattach');
   };
   };

+ 38 - 4
resource/js/crowi.js

@@ -19,8 +19,13 @@ Crowi.createErrorView = function(msg) {
 Crowi.linkPath = function(revisionPath) {
 Crowi.linkPath = function(revisionPath) {
   var $revisionPath = revisionPath || '#revision-path';
   var $revisionPath = revisionPath || '#revision-path';
   var $title = $($revisionPath);
   var $title = $($revisionPath);
+  var pathData = $('#content-main').data('path');
 
 
-  var realPath = $('#content-main').data('path').trim();
+  if (!pathData) {
+    return ;
+  }
+
+  var realPath = pathData.trim();
   if (realPath.substr(-1, 1) == '/') {
   if (realPath.substr(-1, 1) == '/') {
     realPath = realPath.substr(0, realPath.length - 1);
     realPath = realPath.substr(0, realPath.length - 1);
   }
   }
@@ -316,6 +321,35 @@ $(function() {
 
 
   if (pageId) {
   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();
+      Crowi.correctHeaders('#revision-body-content');
+      Crowi.revisionToc('#revision-body-content', '#revision-toc');
+    }
+
+    // header
+    var $header = $('#page-header');
+    if ($header.length > 0) {
+      var headerHeight = $header.outerHeight(true);
+      $('.header-wrap').css({height: (headerHeight + 16) + 'px'});
+      $header.affix({
+        offset: {
+          top: function() {
+            return headerHeight + 86; // (54 header + 16 header padding-top + 16 content padding-top)
+          }
+        }
+      });
+      $('[data-affix-disable]').on('click', function(e) {
+        $elm = $($(this).data('affix-disable'));
+        $(window).off('.affix');
+        $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
+        return false;
+      });
+    }
+
     // omg
     // omg
     function createCommentHTML(revision, creator, comment, commentedAt) {
     function createCommentHTML(revision, creator, comment, commentedAt) {
       var $comment = $('<div>');
       var $comment = $('<div>');
@@ -424,11 +458,10 @@ $(function() {
     var $pageAttachmentList = $('.page-attachments ul');
     var $pageAttachmentList = $('.page-attachments ul');
     $.get('/_api/attachment/page/' + pageId, function(res) {
     $.get('/_api/attachment/page/' + pageId, function(res) {
       var attachments = res.data.attachments;
       var attachments = res.data.attachments;
-      var urlBase = res.data.fileBaseUrl;
       if (attachments.length > 0) {
       if (attachments.length > 0) {
         $.each(attachments, function(i, file) {
         $.each(attachments, function(i, 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="' + file.fileUrl + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
           );
           );
         })
         })
       } else {
       } else {
@@ -545,7 +578,8 @@ $(function() {
 
 
     var $seenUserList = $("#seen-user-list");
     var $seenUserList = $("#seen-user-list");
     var seenUsers = $seenUserList.data('seen-users');
     var seenUsers = $seenUserList.data('seen-users');
-    if (seenUsers && seenUsers.length > 0 && seenUsers.length <= 10) {
+    var seenUsersArray = seenUsers.split(',');
+    if (seenUsers && seenUsersArray.length > 0 && seenUsersArray.length <= 10) {
       // FIXME: user data cache
       // FIXME: user data cache
       $.get('/_api/users.list', {user_ids: seenUsers}, function(res) {
       $.get('/_api/users.list', {user_ids: seenUsers}, function(res) {
         // ignore unless response has error
         // ignore unless response has error

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