فهرست منبع

Merge branch 'master' into feature-search

Sotaro KARASAWA 10 سال پیش
والد
کامیت
4bef5144ef

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@ node_modules/
 bower_components/
 public/js/*
 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`.
 * `PASSWORD_SEED`: A password seed is used by password hash generator.
 * `SECRET_TOKEN`: A secret key for verifying the integrity of signed cookies.
+* `FILE_UPLOAD`: `aws` (default), `local`, `none`
 
 
 License

+ 9 - 0
lib/models/attachment.js

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

+ 6 - 3
lib/models/config.js

@@ -166,14 +166,17 @@ module.exports = function(crowi) {
 
   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:region'] ||
-        !config.crowi['aws:bucket']) {
+        !config.crowi['aws:bucket'])) {
       return false;
     }
 
-    return true;
+    return method != 'none';
   };
 
   /*

+ 3 - 0
lib/models/page.js

@@ -326,6 +326,9 @@ module.exports = function(crowi) {
           return reject(err);
         }
 
+        if (pageData == null) {
+          return reject(new Error('Page not found'));
+        }
         return Page.populatePageData(pageData, null).then(resolve);
       });
     });

+ 1 - 1
lib/models/user.js

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

+ 8 - 11
lib/routes/attachment.js

@@ -8,6 +8,7 @@ module.exports = function(crowi, app) {
     , Promise = require('bluebird')
     , config = crowi.getConfig()
     , fs = require('fs')
+    , fileUploader = require('../util/fileUploader')(crowi, app)
     , actions = {}
     , api = {};
 
@@ -21,7 +22,6 @@ module.exports = function(crowi, app) {
       res.json({
         status: true,
         data: {
-          fileBaseUrl: 'https://' + config.crowi['aws:bucket'] +'.s3.amazonaws.com/', // FIXME: ベタ書きよくない
           attachments: attachments
         }
       });
@@ -39,7 +39,6 @@ module.exports = function(crowi, app) {
 
     debug('id and path are: ', id, path);
 
-    var fileUploader = require('../util/fileUploader')(crowi, app);
     var tmpFile = req.files.file || null;
     debug('Uploaded tmpFile: ', tmpFile);
     if (!tmpFile) {
@@ -52,14 +51,12 @@ module.exports = function(crowi, app) {
     new Promise(function(resolve, reject) {
       if (id == 0) {
         debug('Create page before file upload');
-        Page.create(path, '# '  + path, req.user, {grant: Page.GRANT_OWNER}, function(err, pageData) {
-          if (err) {
-            debug('Page create error', err);
-            return reject(err);
-          }
-          pageCreated = true;
-          return resolve(pageData);
-        });
+        Page.create(path, '# '  + path, req.user, {grant: Page.GRANT_OWNER})
+          .then(function(page) {
+            pageCreated = true;
+            resolve(page);
+          })
+          .catch(reject);
       } else {
         Page.findPageById(id).then(resolve).catch(reject);
       }
@@ -82,7 +79,7 @@ module.exports = function(crowi, app) {
           // TODO size
           return Attachment.create(id, req.user, filePath, originalName, fileName, fileType, fileSize);
         }).then(function(data) {
-          var imageUrl = fileUploader.generateS3FileUrl(data.filePath);
+          var imageUrl = fileUploader.generateUrl(data.filePath);
           return res.json({
             status: true,
             filename: imageUrl,

+ 1 - 1
lib/routes/me.js

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

+ 13 - 7
lib/routes/page.js

@@ -230,8 +230,15 @@ module.exports = function(crowi, app) {
         return res.redirect('/');
       }
 
-      debug('Catch pageShow', err);
-      return renderPage(null, req, res);
+      Page.hasPortalPage(path + '/', req.user)
+      .then(function(page) {
+        if (page) {
+          return res.redirect(path + '/');
+        } else {
+          debug('Catch pageShow', err);
+          return renderPage(null, req, res);
+        }
+      });
     });
   };
 
@@ -257,14 +264,12 @@ module.exports = function(crowi, app) {
     Page.findPage(path, req.user, null, ignoreNotFound)
     .then(function(pageData) {
       if (!req.form.isValid) {
-        renderPage(pageData, req, res);
-        return Promise.reject(new Error('form error'));
+        return renderPage(pageData, req, res);
       }
 
       if (pageData && !pageData.isUpdatable(currentRevision)) {
         req.form.errors.push('すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。');
-        renderPage(pageData, req, res);
-        return Promise.reject(new Error('form error'));
+        return renderPage(pageData, req, res);
       }
 
       if (pageData) {
@@ -376,6 +381,7 @@ module.exports = function(crowi, app) {
 
     Page.findPageById(id)
     .then(function(pageData) {
+
       if (pageData.grant == Page.GRANT_RESTRICTED && !pageData.isGrantedFor(req.user)) {
         return Page.pushToGrantedUsers(pageData, req.user);
       }
@@ -408,7 +414,7 @@ module.exports = function(crowi, app) {
       var result = {};
       result.page = pageData;
 
-      return res.json(ApiResponse.success(pageData));
+      return res.json(ApiResponse.success(result));
     }).catch(function(err) {
       return res.json(ApiResponse.error(err));
     });

+ 4 - 61
lib/util/fileUploader.js

@@ -2,69 +2,12 @@
  * fileUploader
  */
 
-
 module.exports = function(crowi) {
   '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);
 };

+ 16 - 0
lib/util/middlewares.js

@@ -59,6 +59,22 @@ exports.swigFilters = function(app, swig) {
         .replace(/\n/g, '<br>');
     });
 
+    swig.setFilter('insertSpaceToEachSlashes', function(string) {
+      if (string == '/') {
+        return string;
+      }
+
+      return string.replace(/\//g, ' / ');
+    });
+
+    swig.setFilter('removeLastSlash', function(string) {
+      if (string == '/') {
+        return string;
+      }
+
+      return string.substr(0, string.length - 1);
+    });
+
     swig.setFilter('presentation', function(string) {
       // 手抜き
       return string

+ 8 - 0
lib/util/swigFunctions.js

@@ -20,6 +20,14 @@ module.exports = function(crowi, app, locals) {
     return Config.isUploadable(config);
   };
 
+  locals.isUserPageList = function(path) {
+    if (path.match(/^\/user\/[^\/]+\/$/)) {
+      return true;
+    }
+
+    return false;
+  };
+
   locals.user_page_root = function(user) {
     if (!user) {
       return '';

+ 10 - 1
lib/views/page.html

@@ -15,7 +15,7 @@
     {% if page %}
       <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
     {% endif %}
-    <h1 class="title" id="revision-path">{{ path }}</h1>
+    <h1 class="title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
   </header>
 </div>
 
@@ -30,6 +30,8 @@
 {% endblock %}
 
 <div id="content-main" class="content-main {% if not page or req.body.pageForm %}on-edit{% endif %}"
+  data-path="{{ path }}"
+  data-path-shortname="{{ path|path2name }}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
@@ -216,6 +218,13 @@
 <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="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>
   $(function() {
     var me = {{ user|json|safe }};

+ 32 - 4
lib/views/page_list.html

@@ -14,7 +14,7 @@
 
     {% endif %}
     <h1 class="title" id="revision-path">
-      {{ path }}
+      {{ path|insertSpaceToEachSlashes }}
     </h1>
   </header>
 </div>
@@ -31,6 +31,8 @@
 
 <div class="page-list content-main {% if req.body.pageForm %}on-edit{% endif %}"
   id="content-main"
+  data-path="{{ path }}"
+  data-path-shortname="{{ path|path2name }}"
   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 %}"
@@ -65,8 +67,7 @@
   </ul>
 
   <div class="tab-content">
-    <div class="wiki tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body-content">
-    </div>
+    <div class="wiki tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body-content">{{ page.revision.body|nl2br|safe }}</div>
 
     <script type="text/template" id="raw-text-original">{{ page.revision.body }}</script>
     <script type="text/javascript">
@@ -160,7 +161,7 @@
 
 {% block side_header %}
 
-{% if not page %}
+{% if not page and not isUserPageList(path) %}
 <div class="portal-side">
   <div class="portal-form-button">
     <button class="btn btn-primary" id="create-portal-button">Create Portal</button>
@@ -174,3 +175,30 @@
 
 {% endblock %} {# side_header #}
 
+{% block body_end %}
+<div class="modal fade portal-warning-modal" id="portal-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>既に <strong><a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a></strong> のページが存在します。</p>
+
+        <p>
+          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> をポータル化するには、
+          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> に移動し、「ページを移動」させてください。<br>
+          <a href="{{ path|removeLastSlash }}">{{ path|removeLastSlash }}</a> とは別に、このページ(<code>{{ path }}</code>)にポータルを作成する場合、このまま編集を続けて作成してください。
+        </p>
+
+      </div>
+    </div>
+  </div>
+</div>
+</div>
+{% endblock %} {# body_end #}

+ 1 - 1
lib/views/user_page.html

@@ -7,7 +7,7 @@
 {% if pageUser %}
 
 <div class="header-wrap">
-  <h1 class="title" id="revision-path">{{ path }}</h1>
+  <h1 class="title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
   <div class="user-page-header">
   {% if page %}
     <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-bookmarked="0"><i class="fa fa-star-o"></i></a>

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

@@ -6,7 +6,7 @@
   </li>
   <li data-toggle="tooltip" data-placement="bottom" title="Markdown形式のリンク" class="input-group">
     <span class="input-group-addon">Markdown</span>
-    <input class="copy-link form-control" type="text" value="[{{ path }}]({{ baseUrl }}/_r/{{ revision._id.toString() }})">
+    <input class="copy-link form-control" type="text" value="[{{ path }}]({{ baseUrl }}/{{ page._id.toString() }})">
   </li>
 </ul>
 

+ 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

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

+ 1 - 0
resource/css/_page.scss

@@ -63,6 +63,7 @@
       }
 
       h1 {
+        font-size: 28px;
         margin-top: 0;
 
         a:last-child {

+ 5 - 0
resource/css/_portal.scss

@@ -29,3 +29,8 @@
     text-align: center;
   }
 } // .portal-side
+
+
+.portal-warning-modal {
+  z-index: 1062;
+}

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

@@ -270,7 +270,7 @@ $(function() {
             pageId = page._id;
 
         $('#content-main').data('page-id', page._id);
-        $('#page-form [name="pageForm[currentRevision]"]').val(page.revision)
+        $('#page-form [name="pageForm[currentRevision]"]').val(page.revision._id)
 
         unbindInlineAttachment($inputForm);
 

+ 18 - 6
resource/js/crowi.js

@@ -19,10 +19,13 @@ Crowi.createErrorView = function(msg) {
 Crowi.linkPath = function(revisionPath) {
   var $revisionPath = revisionPath || '#revision-path';
   var $title = $($revisionPath);
-  if (!$title.get(0)) {
-    return;
+  var pathData = $('#content-main').data('path');
+
+  if (!pathData) {
+    return ;
   }
-  var realPath = $title.text().trim();
+
+  var realPath = pathData.trim();
   if (realPath.substr(-1, 1) == '/') {
     realPath = realPath.substr(0, realPath.length - 1);
   }
@@ -287,6 +290,16 @@ $(function() {
     $('.portal').removeClass('hide');
     $('.content-main').addClass('on-edit');
     $('.portal a[data-toggle="tab"][href="#edit-form"]').tab('show');
+
+    var path = $('.content-main').data('path');
+    if (path != '/' && $('.content-main').data('page-id') == '') {
+      var upperPage = path.substr(0, path.length - 1);
+      $.get('/_api/pages.get', {path: upperPage}, function(res) {
+        if (res.ok && res.page) {
+          $('#portal-warning-modal').modal('show');
+        }
+      });
+    }
   });
   $('#portal-form-close').on('click', function(e) {
     $('.portal').addClass('hide');
@@ -416,11 +429,10 @@ $(function() {
     var $pageAttachmentList = $('.page-attachments ul');
     $.get('/_api/attachment/page/' + pageId, function(res) {
       var attachments = res.data.attachments;
-      var urlBase = res.data.fileBaseUrl;
       if (attachments.length > 0) {
         $.each(attachments, function(i, file) {
           $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 {
@@ -537,7 +549,7 @@ $(function() {
 
     var $seenUserList = $("#seen-user-list");
     var seenUsers = $seenUserList.data('seen-users');
-    if (seenUsers && seenUsers.length > 0) {
+    if (seenUsers && seenUsers.length > 0 && seenUsers.length <= 10) {
       // FIXME: user data cache
       $.get('/_api/users.list', {user_ids: seenUsers}, function(res) {
         // ignore unless response has error

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است