2016-04-20 10:03:55 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
// external modules
|
|
|
|
var fs = require('fs');
|
|
|
|
var path = require('path');
|
|
|
|
var LZString = require('lz-string');
|
2016-07-02 08:11:06 +00:00
|
|
|
var md = require('markdown-it')();
|
2016-06-21 13:42:03 +00:00
|
|
|
var metaMarked = require('meta-marked');
|
2016-04-20 10:03:55 +00:00
|
|
|
var cheerio = require('cheerio');
|
|
|
|
var shortId = require('shortid');
|
|
|
|
var Sequelize = require("sequelize");
|
|
|
|
var async = require('async');
|
2016-05-30 04:43:51 +00:00
|
|
|
var moment = require('moment');
|
2016-04-20 10:03:55 +00:00
|
|
|
|
|
|
|
// core
|
|
|
|
var config = require("../config.js");
|
|
|
|
var logger = require("../logger.js");
|
|
|
|
|
|
|
|
// permission types
|
|
|
|
var permissionTypes = ["freely", "editable", "locked", "private"];
|
|
|
|
|
|
|
|
module.exports = function (sequelize, DataTypes) {
|
|
|
|
var Note = sequelize.define("Note", {
|
|
|
|
id: {
|
|
|
|
type: DataTypes.UUID,
|
|
|
|
primaryKey: true,
|
|
|
|
defaultValue: Sequelize.UUIDV4
|
|
|
|
},
|
|
|
|
shortid: {
|
|
|
|
type: DataTypes.STRING,
|
|
|
|
unique: true,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: shortId.generate
|
|
|
|
},
|
|
|
|
alias: {
|
|
|
|
type: DataTypes.STRING,
|
|
|
|
unique: true
|
|
|
|
},
|
|
|
|
permission: {
|
|
|
|
type: DataTypes.ENUM,
|
|
|
|
values: permissionTypes
|
|
|
|
},
|
|
|
|
viewcount: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: 0
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
type: DataTypes.TEXT
|
|
|
|
},
|
|
|
|
content: {
|
|
|
|
type: DataTypes.TEXT
|
|
|
|
},
|
2016-07-30 03:21:38 +00:00
|
|
|
authorship: {
|
|
|
|
type: DataTypes.TEXT
|
|
|
|
},
|
2016-04-20 10:03:55 +00:00
|
|
|
lastchangeAt: {
|
|
|
|
type: DataTypes.DATE
|
2016-06-17 08:09:33 +00:00
|
|
|
},
|
|
|
|
savedAt: {
|
|
|
|
type: DataTypes.DATE
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
|
|
|
}, {
|
|
|
|
classMethods: {
|
|
|
|
associate: function (models) {
|
|
|
|
Note.belongsTo(models.User, {
|
|
|
|
foreignKey: "ownerId",
|
|
|
|
as: "owner",
|
|
|
|
constraints: false
|
|
|
|
});
|
|
|
|
Note.belongsTo(models.User, {
|
|
|
|
foreignKey: "lastchangeuserId",
|
|
|
|
as: "lastchangeuser",
|
|
|
|
constraints: false
|
|
|
|
});
|
2016-06-17 08:09:33 +00:00
|
|
|
Note.hasMany(models.Revision, {
|
|
|
|
foreignKey: "noteId",
|
|
|
|
constraints: false
|
|
|
|
});
|
2016-07-30 03:21:38 +00:00
|
|
|
Note.hasMany(models.Author, {
|
|
|
|
foreignKey: "noteId",
|
|
|
|
as: "authors",
|
|
|
|
constraints: false
|
|
|
|
});
|
2016-04-20 10:03:55 +00:00
|
|
|
},
|
|
|
|
checkFileExist: function (filePath) {
|
|
|
|
try {
|
|
|
|
return fs.statSync(filePath).isFile();
|
|
|
|
} catch (err) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
checkNoteIdValid: function (id) {
|
|
|
|
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
var result = id.match(uuidRegex);
|
|
|
|
if (result && result.length == 1)
|
|
|
|
return true;
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
parseNoteId: function (noteId, callback) {
|
|
|
|
async.series({
|
|
|
|
parseNoteIdByAlias: function (_callback) {
|
|
|
|
// try to parse note id by alias (e.g. doc)
|
|
|
|
Note.findOne({
|
|
|
|
where: {
|
|
|
|
alias: noteId
|
|
|
|
}
|
|
|
|
}).then(function (note) {
|
|
|
|
if (note) {
|
2016-05-30 04:43:51 +00:00
|
|
|
var filePath = path.join(config.docspath, noteId + '.md');
|
|
|
|
if (Note.checkFileExist(filePath)) {
|
|
|
|
// if doc in filesystem have newer modified time than last change time
|
|
|
|
// then will update the doc in db
|
|
|
|
var fsModifiedTime = moment(fs.statSync(filePath).mtime);
|
|
|
|
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt);
|
|
|
|
if (fsModifiedTime.isAfter(dbModifiedTime)) {
|
|
|
|
var body = fs.readFileSync(filePath, 'utf8');
|
2016-06-17 08:09:33 +00:00
|
|
|
note.update({
|
|
|
|
title: LZString.compressToBase64(Note.parseNoteTitle(body)),
|
|
|
|
content: LZString.compressToBase64(body),
|
|
|
|
lastchangeAt: fsModifiedTime
|
|
|
|
}).then(function (note) {
|
|
|
|
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
|
|
|
if (err) return _callback(err, null);
|
|
|
|
return callback(null, note.id);
|
|
|
|
});
|
2016-05-30 04:43:51 +00:00
|
|
|
}).catch(function (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return callback(null, note.id);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return callback(null, note.id);
|
|
|
|
}
|
2016-04-20 10:03:55 +00:00
|
|
|
} else {
|
|
|
|
var filePath = path.join(config.docspath, noteId + '.md');
|
|
|
|
if (Note.checkFileExist(filePath)) {
|
|
|
|
Note.create({
|
|
|
|
alias: noteId,
|
|
|
|
owner: null,
|
|
|
|
permission: 'locked'
|
|
|
|
}).then(function (note) {
|
|
|
|
return callback(null, note.id);
|
|
|
|
}).catch(function (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return _callback(null, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}).catch(function (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
parseNoteIdByLZString: function (_callback) {
|
|
|
|
// try to parse note id by LZString Base64
|
|
|
|
try {
|
|
|
|
var id = LZString.decompressFromBase64(noteId);
|
|
|
|
if (id && Note.checkNoteIdValid(id))
|
|
|
|
return callback(null, id);
|
|
|
|
else
|
|
|
|
return _callback(null, null);
|
|
|
|
} catch (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
parseNoteIdByShortId: function (_callback) {
|
|
|
|
// try to parse note id by shortId
|
|
|
|
try {
|
|
|
|
if (shortId.isValid(noteId)) {
|
|
|
|
Note.findOne({
|
|
|
|
where: {
|
|
|
|
shortid: noteId
|
|
|
|
}
|
|
|
|
}).then(function (note) {
|
|
|
|
if (!note) return _callback(null, null);
|
|
|
|
return callback(null, note.id);
|
|
|
|
}).catch(function (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return _callback(null, null);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
return _callback(err, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, function (err, result) {
|
|
|
|
if (err) {
|
|
|
|
logger.error(err);
|
|
|
|
return callback(err, null);
|
|
|
|
}
|
|
|
|
return callback(null, null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
parseNoteTitle: function (body) {
|
|
|
|
var title = "";
|
2016-06-21 13:42:03 +00:00
|
|
|
var meta = null;
|
|
|
|
try {
|
|
|
|
var obj = metaMarked(body);
|
|
|
|
body = obj.markdown;
|
|
|
|
meta = obj.meta;
|
|
|
|
} catch (err) {
|
|
|
|
//na
|
|
|
|
}
|
|
|
|
if (meta && meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) {
|
|
|
|
title = meta.title;
|
|
|
|
} else {
|
2016-07-02 08:11:06 +00:00
|
|
|
var $ = cheerio.load(md.render(body));
|
2016-06-21 13:42:03 +00:00
|
|
|
var h1s = $("h1");
|
|
|
|
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
|
|
|
|
title = h1s.first().text();
|
|
|
|
}
|
|
|
|
if (!title) title = "Untitled";
|
2016-04-20 10:03:55 +00:00
|
|
|
return title;
|
|
|
|
},
|
|
|
|
decodeTitle: function (title) {
|
|
|
|
var decodedTitle = LZString.decompressFromBase64(title);
|
|
|
|
if (decodedTitle) title = decodedTitle;
|
|
|
|
else title = 'Untitled';
|
|
|
|
return title;
|
|
|
|
},
|
|
|
|
generateWebTitle: function (title) {
|
2016-05-27 17:51:45 +00:00
|
|
|
title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD";
|
2016-04-20 10:03:55 +00:00
|
|
|
return title;
|
2016-06-21 13:42:03 +00:00
|
|
|
},
|
|
|
|
parseMeta: function (meta) {
|
|
|
|
var _meta = {};
|
|
|
|
if (meta) {
|
|
|
|
if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number"))
|
|
|
|
_meta.title = meta.title;
|
|
|
|
if (meta.description && (typeof meta.description == "string" || typeof meta.description == "number"))
|
|
|
|
_meta.description = meta.description;
|
|
|
|
if (meta.robots && (typeof meta.robots == "string" || typeof meta.robots == "number"))
|
|
|
|
_meta.robots = meta.robots;
|
|
|
|
if (meta.GA && (typeof meta.GA == "string" || typeof meta.GA == "number"))
|
|
|
|
_meta.GA = meta.GA;
|
|
|
|
}
|
|
|
|
return _meta;
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
hooks: {
|
|
|
|
beforeCreate: function (note, options, callback) {
|
|
|
|
// if no content specified then use default note
|
|
|
|
if (!note.content) {
|
|
|
|
var body = null;
|
|
|
|
var filePath = null;
|
|
|
|
if (!note.alias) {
|
|
|
|
filePath = config.defaultnotepath;
|
|
|
|
} else {
|
|
|
|
filePath = path.join(config.docspath, note.alias + '.md');
|
|
|
|
}
|
|
|
|
if (Note.checkFileExist(filePath)) {
|
2016-06-01 15:19:47 +00:00
|
|
|
var fsCreatedTime = moment(fs.statSync(filePath).ctime);
|
2016-04-20 10:03:55 +00:00
|
|
|
body = fs.readFileSync(filePath, 'utf8');
|
|
|
|
note.title = LZString.compressToBase64(Note.parseNoteTitle(body));
|
|
|
|
note.content = LZString.compressToBase64(body);
|
2016-06-17 08:28:04 +00:00
|
|
|
if (filePath !== config.defaultnotepath) {
|
|
|
|
note.createdAt = fsCreatedTime;
|
|
|
|
}
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// if no permission specified and have owner then give editable permission, else default permission is freely
|
|
|
|
if (!note.permission) {
|
|
|
|
if (note.ownerId) {
|
|
|
|
note.permission = "editable";
|
|
|
|
} else {
|
|
|
|
note.permission = "freely";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return callback(null, note);
|
2016-06-17 08:09:33 +00:00
|
|
|
},
|
|
|
|
afterCreate: function (note, options, callback) {
|
|
|
|
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
|
|
|
callback(err, note);
|
|
|
|
});
|
2016-04-20 10:03:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return Note;
|
|
|
|
};
|