summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md101
-rw-r--r--app.js298
-rw-r--r--config.js88
-rw-r--r--config.json43
-rw-r--r--hackmd_schema.sql77
-rw-r--r--lib/auth.js76
-rw-r--r--lib/config.js112
-rw-r--r--lib/db.js151
-rw-r--r--lib/models/index.js37
-rw-r--r--lib/models/note.js208
-rw-r--r--lib/models/temp.js19
-rw-r--r--lib/models/user.js77
-rw-r--r--lib/note.js237
-rw-r--r--lib/ot/server.js2
-rw-r--r--lib/realtime.js675
-rw-r--r--lib/response.js766
-rw-r--r--lib/temp.js84
-rw-r--r--lib/user.js110
-rw-r--r--package.json11
-rw-r--r--public/docs/features.md (renamed from public/features.md)52
-rw-r--r--public/docs/slide-example.md81
-rw-r--r--public/docs/yaml-metadata.md97
-rw-r--r--public/index.ejs237
-rw-r--r--public/js/extra.js15
-rw-r--r--public/js/history.js10
-rw-r--r--public/js/index.js17
-rw-r--r--public/js/pretty.js3
-rw-r--r--public/views/body.ejs28
-rw-r--r--public/views/hackmd.ejs15
-rw-r--r--public/views/header.ejs6
-rw-r--r--public/views/index.ejs206
-rw-r--r--public/views/modal.ejs34
-rw-r--r--public/views/pretty.ejs4
-rw-r--r--public/views/slide.hbs (renamed from public/views/slide/reveal.hbs)2
-rw-r--r--public/views/slide/listing.hbs22
35 files changed, 1879 insertions, 2122 deletions
diff --git a/README.md b/README.md
index e15da922..7e1cc446 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,17 @@ Still in early stage, feel free to fork or contribute to this.
Thanks for your using! :smile:
+[docker-hackmd](https://github.com/hackmdio/docker-hackmd)
+---
+Before you going too far, here is the great docker repo for HackMD.
+With docker, you can deploy a server in minutes without any hardtime.
+
+[migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0)
+---
+We've dropped MongoDB after version 0.4.0.
+So here is the migration tool for you to transfer old DB data to new DB.
+This tool is also used for official service.
+
Browsers Requirement
---
- Chrome >= 45, Chrome for Android >= 47
@@ -20,33 +31,24 @@ Browsers Requirement
Prerequisite
---
-- Node.js 4.x or up (test up to 5.8.0)
-- PostgreSQL 9.3.x or 9.4.x
-- MongoDB 3.0.x
+- Node.js 4.x or up (test up to 5.10.1)
+- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL)
- npm and bower
Get started
---
1. Download a release and unzip or clone into a directory
2. Enter the directory and type `npm install && bower install`, will install all the dependencies
-3. Install PostgreSQL and MongoDB (yes, currently we need both)
-4. Import database schema, see more on below
-5. Setup the configs, see more on below
-6. Setup environment variables, which will overwrite the configs
-7. Run the server as you like (node, forever, pm2)
-
-Import database schema
----
-The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
-To import the sql file in PostgreSQL, see http://www.postgresql.org/docs/9.4/static/backup-dump.html
-
-The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
+3. Setup the configs, see more on below
+4. Setup environment variables, which will overwrite the configs
+5. Run the server as you like (node, forever, pm2)
Structure
---
```
hackmd/
├── tmp/ --- temporary files
+├── docs/ --- document files
├── lib/ --- server libraries
└── public/ --- client files
├── css/ --- css styles
@@ -57,63 +59,58 @@ hackmd/
Configuration files
---
-There are some config you need to change in below files
+There are some configs you need to change in below files
```
-./config.js --- for server settings
-./public/js/index.js --- for client settings
+./config.json --- for server settings
./public/js/common.js --- for client settings
```
-Client-side index.js settings
+Client settings `common.js`
---
| variables | example values | description |
| --------- | ------ | ----------- |
| debug | `true` or `false` | set debug mode, show more logs |
-| version | `0.3.2` | current version, must match same var in server side `config.js` |
-
-Client-side common.js settings
----
-| variables | example values | description |
-| --------- | ------ | ----------- |
| domain | `localhost` | domain name |
| urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` |
-Environment variables
+Environment variables (will overwrite other server configs)
---
| variables | example values | description |
| --------- | ------ | ----------- |
-| NODE_ENV | `production` or `development` | show current environment status |
-| DATABASE_URL | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string |
-| MONGOLAB_URI | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string |
-| PORT | `80` | web port |
-| SSLPORT | `443` | ssl web port |
-| DOMAIN | `localhost` | domain name |
-| URL_PATH | `hackmd` | sub url path, like `www.example.com/<URL_PATH>` |
-
-Server-side config.js settings
+| NODE_ENV | `production` or `development` | set current environment (will apply correspond settings in the `config.json`) |
+| PORT | `80` | web app port |
+| DEBUG | `true` or `false` | set debug mode, show more logs |
+
+Server settings `config.json`
---
| variables | example values | description |
| --------- | ------ | ----------- |
-| testport | `3000` | debug web port, fallback to this when not set in environment |
-| testsslport | `3001` | debug web ssl port, fallback to this when not set in environment |
-| usessl | `true` or `false` | set to use ssl |
-| protocolusessl | `true` or `false` | set to use ssl protocol |
-| urladdport | `true` or `false` | set to add port on oauth callback url |
| debug | `true` or `false` | set debug mode, show more logs |
-| usecdn | `true` or `false` | set to use CDN resources or not |
-| version | `0.3.2` | currnet version, must match same var in client side `index.js` |
+| domain | `localhost` | domain name |
+| urlpath | `hackmd` | sub url path, like `www.example.com/<urlpath>` |
+| port | `80` | web app port |
| alloworigin | `['localhost']` | domain name whitelist |
-| sslkeypath | `./cert/client.key` | ssl key path |
-| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path |
-| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain |
-| dhparampath | `./cert/dhparam.pem` | ssl dhparam path |
-| tmppath | `./tmp/` | temp file path |
-| postgresqlstring | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string, fallback to this when not set in environment |
-| mongodbstring | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string, fallback to this when not set in environment |
+| usessl | `true` or `false` | set to use ssl server (if true will auto turn on `protocolusessl`) |
+| protocolusessl | `true` or `false` | set to use ssl protocol for resources path |
+| urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) |
+| usecdn | `true` or `false` | set to use CDN resources or not |
+| db | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) |
+| sslkeypath | `./cert/client.key` | ssl key path (only need when you set usessl) |
+| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path (only need when you set usessl) |
+| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain (only need when you set usessl) |
+| dhparampath | `./cert/dhparam.pem` | ssl dhparam path (only need when you set usessl) |
+| tmppath | `./tmp/` | temp directory path |
+| defaultnotepath | `./public/default.md` | default note file path |
+| docspath | `./public/docs` | docs directory path |
+| indexpath | `./public/views/index.ejs` | index template file path |
+| hackmdpath | `./public/views/hackmd.ejs` | hackmd template file path |
+| errorpath | `./public/views/error.ejs` | error template file path |
+| prettypath | `./public/views/pretty.ejs` | pretty template file path |
+| slidepath | `./public/views/slide.hbs` | slide template file path |
| sessionname | `connect.sid` | cookie session name |
| sessionsecret | `secret` | cookie session secret |
| sessionlife | `14 * 24 * 60 * 60 * 1000` | cookie session life |
-| sessiontouch | `1 * 3600` | cookie session touch |
+| staticcachetime | `1 * 24 * 60 * 60 * 1000` | static file cache time |
| heartbeatinterval | `5000` | socket.io heartbeat interval |
| heartbeattimeout | `10000` | socket.io heartbeat timeout |
| documentmaxlength | `100000` | note max length |
@@ -122,8 +119,8 @@ Third-party integration api key settings
---
| service | file path | description |
| ------- | --------- | ----------- |
-| facebook, twitter, github, dropbox | `config.js` | for signin |
-| imgur | `config.js` | for image upload |
+| facebook, twitter, github, dropbox | `config.json` | for signin |
+| imgur | `config.json` | for image upload |
| dropbox | `public/views/foot.ejs` | for chooser and saver |
| google drive | `public/js/common.js` | for export and import |
diff --git a/app.js b/app.js
index 2261cc3c..1dae8a41 100644
--- a/app.js
+++ b/app.js
@@ -7,12 +7,10 @@ var passport = require('passport');
var methodOverride = require('method-override');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
-var mongoose = require('mongoose');
var compression = require('compression')
var session = require('express-session');
-var MongoStore = require('connect-mongo')(session);
+var SequelizeStore = require('connect-session-sequelize')(session.Store);
var fs = require('fs');
-var shortid = require('shortid');
var imgur = require('imgur');
var formidable = require('formidable');
var morgan = require('morgan');
@@ -20,12 +18,11 @@ var passportSocketIo = require("passport.socketio");
var helmet = require('helmet');
//core
-var config = require("./config.js");
+var config = require("./lib/config.js");
var logger = require("./lib/logger.js");
-var User = require("./lib/user.js");
-var Temp = require("./lib/temp.js");
var auth = require("./lib/auth.js");
var response = require("./lib/response.js");
+var models = require("./lib/models");
//server setup
if (config.usessl) {
@@ -60,11 +57,7 @@ app.use(morgan('combined', {
//socket io
var io = require('socket.io')(server);
-// connect to the mongodb
-mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
-
//others
-var db = require("./lib/db.js");
var realtime = require("./lib/realtime.js");
//assign socket io to realtime
@@ -82,13 +75,9 @@ var urlencodedParser = bodyParser.urlencoded({
});
//session store
-var sessionStore = new MongoStore({
- mongooseConnection: mongoose.connection,
- touchAfter: config.sessiontouch
- },
- function (err) {
- logger.info(err);
- });
+var sessionStore = new SequelizeStore({
+ db: models.sequelize
+});
//compression
app.use(compression());
@@ -139,15 +128,21 @@ app.use(passport.session());
//serialize and deserialize
passport.serializeUser(function (user, done) {
- //logger.info('serializeUser: ' + user._id);
- done(null, user._id);
+ logger.info('serializeUser: ' + user.id);
+ return done(null, user.id);
});
passport.deserializeUser(function (id, done) {
- User.model.findById(id, function (err, user) {
- //logger.info(user)
- if (!err) done(null, user);
- else done(err, null);
- })
+ models.User.findOne({
+ where: {
+ id: id
+ }
+ }).then(function (user) {
+ logger.info('deserializeUser: ' + user.id);
+ return done(null, user);
+ }).catch(function (err) {
+ logger.error(err);
+ return done(err, null);
+ });
});
//routes
@@ -161,13 +156,17 @@ app.engine('html', ejs.renderFile);
//get index
app.get("/", response.showIndex);
//get 403 forbidden
-app.get("/403", function(req, res) {
+app.get("/403", function (req, res) {
response.errorForbidden(res);
});
//get 404 not found
-app.get("/404", function(req, res) {
+app.get("/404", function (req, res) {
response.errorNotFound(res);
});
+//get 500 internal error
+app.get("/500", function (req, res) {
+ response.errorInternalError(res);
+});
//get status
app.get("/status", function (req, res, next) {
realtime.getStatus(function (data) {
@@ -184,19 +183,26 @@ app.get("/temp", function (req, res) {
if (!tempid)
response.errorForbidden(res);
else {
- Temp.findTemp(tempid, function (err, temp) {
- if (err || !temp)
- response.errorForbidden(res);
+ models.Temp.findOne({
+ where: {
+ id: tempid
+ }
+ }).then(function (temp) {
+ if (!temp)
+ response.errorNotFound(res);
else {
res.header("Access-Control-Allow-Origin", "*");
res.send({
temp: temp.data
});
- temp.remove(function (err) {
+ temp.destroy().catch(function (err) {
if (err)
logger.error('remove temp failed: ' + err);
});
}
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
}
}
@@ -207,15 +213,16 @@ app.post("/temp", urlencodedParser, function (req, res) {
if (config.alloworigin.indexOf(host) == -1)
response.errorForbidden(res);
else {
- var id = shortid.generate();
var data = req.body.data;
- if (!id || !data)
+ if (!data)
response.errorForbidden(res);
else {
if (config.debug)
logger.info('SERVER received temp from [' + host + ']: ' + req.body.data);
- Temp.newTemp(id, data, function (err, temp) {
- if (!err && temp) {
+ models.Temp.create({
+ data: data
+ }).then(function (temp) {
+ if (temp) {
res.header("Access-Control-Allow-Origin", "*");
res.send({
status: 'ok',
@@ -223,125 +230,149 @@ app.post("/temp", urlencodedParser, function (req, res) {
});
} else
response.errorInternalError(res);
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
}
}
});
//facebook auth
-app.get('/auth/facebook',
- passport.authenticate('facebook'),
- function (req, res) {});
-//facebook auth callback
-app.get('/auth/facebook/callback',
- passport.authenticate('facebook', {
- failureRedirect: config.getserverurl()
- }),
- function (req, res) {
- res.redirect(config.getserverurl());
- });
+if (config.facebook) {
+ app.get('/auth/facebook',
+ passport.authenticate('facebook'),
+ function (req, res) {});
+ //facebook auth callback
+ app.get('/auth/facebook/callback',
+ passport.authenticate('facebook', {
+ failureRedirect: config.serverurl
+ }),
+ function (req, res) {
+ res.redirect(config.serverurl);
+ });
+}
//twitter auth
-app.get('/auth/twitter',
- passport.authenticate('twitter'),
- function (req, res) {});
-//twitter auth callback
-app.get('/auth/twitter/callback',
- passport.authenticate('twitter', {
- failureRedirect: config.getserverurl()
- }),
- function (req, res) {
- res.redirect(config.getserverurl());
- });
+if (config.twitter) {
+ app.get('/auth/twitter',
+ passport.authenticate('twitter'),
+ function (req, res) {});
+ //twitter auth callback
+ app.get('/auth/twitter/callback',
+ passport.authenticate('twitter', {
+ failureRedirect: config.serverurl
+ }),
+ function (req, res) {
+ res.redirect(config.serverurl);
+ });
+}
//github auth
-app.get('/auth/github',
- passport.authenticate('github'),
- function (req, res) {});
-//github auth callback
-app.get('/auth/github/callback',
- passport.authenticate('github', {
- failureRedirect: config.getserverurl()
- }),
- function (req, res) {
- res.redirect(config.getserverurl());
- });
-//github callback actions
-app.get('/auth/github/callback/:noteId/:action', response.githubActions);
+if (config.github) {
+ app.get('/auth/github',
+ passport.authenticate('github'),
+ function (req, res) {});
+ //github auth callback
+ app.get('/auth/github/callback',
+ passport.authenticate('github', {
+ failureRedirect: config.serverurl
+ }),
+ function (req, res) {
+ res.redirect(config.serverurl);
+ });
+ //github callback actions
+ app.get('/auth/github/callback/:noteId/:action', response.githubActions);
+}
//dropbox auth
-app.get('/auth/dropbox',
- passport.authenticate('dropbox-oauth2'),
- function (req, res) {});
-//dropbox auth callback
-app.get('/auth/dropbox/callback',
- passport.authenticate('dropbox-oauth2', {
- failureRedirect: config.getserverurl()
- }),
- function (req, res) {
- res.redirect(config.getserverurl());
- });
+if (config.dropbox) {
+ app.get('/auth/dropbox',
+ passport.authenticate('dropbox-oauth2'),
+ function (req, res) {});
+ //dropbox auth callback
+ app.get('/auth/dropbox/callback',
+ passport.authenticate('dropbox-oauth2', {
+ failureRedirect: config.serverurl
+ }),
+ function (req, res) {
+ res.redirect(config.serverurl);
+ });
+}
//logout
app.get('/logout', function (req, res) {
if (config.debug && req.isAuthenticated())
- logger.info('user logout: ' + req.user._id);
+ logger.info('user logout: ' + req.user.id);
req.logout();
- res.redirect(config.getserverurl());
+ res.redirect(config.serverurl);
});
//get history
app.get('/history', function (req, res) {
if (req.isAuthenticated()) {
- User.model.findById(req.user._id, function (err, user) {
- if (err) {
- logger.error('read history failed: ' + err);
- } else {
- var history = [];
- if (user.history)
- history = JSON.parse(user.history);
- res.send({
- history: history
- });
+ models.User.findOne({
+ where: {
+ id: req.user.id
}
+ }).then(function (user) {
+ if (!user)
+ return response.errorNotFound(res);
+ var history = [];
+ if (user.history)
+ history = JSON.parse(user.history);
+ res.send({
+ history: history
+ });
+ if (config.debug)
+ logger.info('read history success: ' + user.id);
+ }).catch(function (err) {
+ logger.error('read history failed: ' + err);
+ return response.errorInternalError(res);
});
} else {
- response.errorForbidden(res);
+ return response.errorForbidden(res);
}
});
//post history
app.post('/history', urlencodedParser, function (req, res) {
if (req.isAuthenticated()) {
if (config.debug)
- logger.info('SERVER received history from [' + req.user._id + ']: ' + req.body.history);
- User.model.findById(req.user._id, function (err, user) {
- if (err) {
- logger.error('write history failed: ' + err);
- } else {
- user.history = req.body.history;
- user.save(function (err) {
- if (err) {
- logger.error('write user history failed: ' + err);
- } else {
- if (config.debug)
- logger.info("write user history success: " + user._id);
- };
- });
+ logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history);
+ models.User.update({
+ history: req.body.history
+ }, {
+ where: {
+ id: req.user.id
}
+ }).then(function (count) {
+ if (!count)
+ return response.errorNotFound(res);
+ if (config.debug)
+ logger.info("write user history success: " + req.user.id);
+ }).catch(function (err) {
+ logger.error('write history failed: ' + err);
+ return response.errorInternalError(res);
});
res.end();
} else {
- response.errorForbidden(res);
+ return response.errorForbidden(res);
}
});
//get me info
app.get('/me', function (req, res) {
if (req.isAuthenticated()) {
- User.model.findById(req.user._id, function (err, user) {
- if (err) {
- logger.error('read me failed: ' + err);
- } else {
- var profile = JSON.parse(user.profile);
- res.send({
- status: 'ok',
- id: req.user._id,
- name: profile.displayName || profile.username
- });
+ models.User.findOne({
+ where: {
+ id: req.user.id
}
+ }).then(function (user) {
+ if (!user)
+ return response.errorNotFound(res);
+ var profile = models.User.parseProfile(user.profile);
+ res.send({
+ status: 'ok',
+ id: req.user.id,
+ name: profile.name,
+ photo: profile.photo
+ });
+ }).catch(function (err) {
+ logger.error('read me failed: ' + err);
+ return response.errorInternalError(res);
});
} else {
res.send({
@@ -370,19 +401,17 @@ app.post('/uploadimage', function (req, res) {
})
.catch(function (err) {
logger.error(err);
- res.send('upload image error');
+ return res.send('upload image error');
});
} catch (err) {
logger.error(err);
- res.send('upload image error');
+ return res.send('upload image error');
}
}
});
});
//get new note
app.get("/new", response.newNote);
-//get features
-app.get("/features", response.showFeatures);
//get publish note
app.get("/s/:shortid", response.showPublishNote);
//publish note actions
@@ -412,15 +441,22 @@ io.set('heartbeat timeout', config.heartbeattimeout);
io.sockets.on('connection', realtime.connection);
//listen
-if (config.usessl) {
- server.listen(config.sslport, function () {
- logger.info('HTTPS Server listening at sslport %d', config.sslport);
- });
-} else {
- server.listen(config.port, function () {
- logger.info('HTTP Server listening at port %d', config.port);
- });
+function startListen() {
+ if (config.usessl) {
+ server.listen(config.port, function () {
+ logger.info('HTTPS Server listening at port %d', config.port);
+ });
+ } else {
+ server.listen(config.port, function () {
+ logger.info('HTTP Server listening at port %d', config.port);
+ });
+ }
}
+
+// sync db then start listen
+models.sequelize.sync().then(startListen);
+
+// log uncaught exception
process.on('uncaughtException', function (err) {
logger.error(err);
-});
+}); \ No newline at end of file
diff --git a/config.js b/config.js
deleted file mode 100644
index 972fc3b4..00000000
--- a/config.js
+++ /dev/null
@@ -1,88 +0,0 @@
-//config
-var path = require('path');
-
-var domain = process.env.DOMAIN;
-var urlpath = process.env.URL_PATH;
-var testport = '3000';
-var testsslport = '3001';
-var port = process.env.PORT || testport;
-var sslport = process.env.SSLPORT || testsslport;
-var usessl = false; // use node https server
-var protocolusessl = false; // use ssl protocol
-var urladdport = true; //add port on getserverurl
-
-var config = {
- debug: false,
- usecdn: false,
- version: '0.3.4',
- domain: domain,
- alloworigin: ['add here to allow origin to cross'],
- urlpath: urlpath,
- testport: testport,
- testsslport: testsslport,
- port: port,
- sslport: sslport,
- sslkeypath: 'change this',
- sslcertpath: 'change this',
- sslcapath: ['change this'],
- dhparampath: 'change this',
- usessl: usessl,
- protocolusessl: protocolusessl,
- getserverurl: function() {
- var protocol = protocolusessl ? 'https://' : 'http://';
- var url = domain;
- if (usessl)
- url = protocol + url + (sslport == 443 || !urladdport ? '' : ':' + sslport);
- else
- url = protocol + url + (port == 80 || !urladdport ? '' : ':' + port);
- if (urlpath)
- url = url + '/' + urlpath;
- return url;
- },
- //path
- tmppath: "./tmp/",
- defaultnotepath: path.join(__dirname, '/public', "default.md"),
- defaultfeaturespath: path.join(__dirname, '/public', "features.md"),
- indexpath: path.join(__dirname, '/public/', "index.ejs"),
- hackmdpath: path.join(__dirname, '/public/views', "index.ejs"),
- errorpath: path.join(__dirname, '/public/views', "error.ejs"),
- prettypath: path.join(__dirname, '/public/views', 'pretty.ejs'),
- //db string
- postgresqlstring: "change this",
- mongodbstring: "change this",
- //constants
- featuresnotename: "features",
- sessionname: 'change this',
- sessionsecret: 'change this',
- sessionlife: 14 * 24 * 60 * 60 * 1000, //14 days
- sessiontouch: 1 * 3600, //1 hour
- heartbeatinterval: 5000,
- heartbeattimeout: 10000,
- documentmaxlength: 100000,
- //auth
- facebook: {
- clientID: 'change this',
- clientSecret: 'change this',
- callbackPath: '/auth/facebook/callback'
- },
- twitter: {
- consumerKey: 'change this',
- consumerSecret: 'change this',
- callbackPath: '/auth/twitter/callback'
- },
- github: {
- clientID: 'change this',
- clientSecret: 'change this',
- callbackPath: '/auth/github/callback'
- },
- dropbox: {
- clientID: 'change this',
- clientSecret: 'change this',
- callbackPath: '/auth/dropbox/callback'
- },
- imgur: {
- clientID: 'change this'
- }
-};
-
-module.exports = config;
diff --git a/config.json b/config.json
new file mode 100644
index 00000000..16ad258b
--- /dev/null
+++ b/config.json
@@ -0,0 +1,43 @@
+{
+ "development": {
+ "domain": "localhost",
+ "db": {
+ "username": "",
+ "password": "",
+ "database": "hackmd",
+ "host": "localhost",
+ "port": "3306",
+ "dialect": "mysql"
+ }
+ },
+ "production": {
+ "domain": "localhost",
+ "db": {
+ "username": "",
+ "password": "",
+ "database": "hackmd",
+ "host": "localhost",
+ "port": "5432",
+ "dialect": "postgres"
+ },
+ "facebook": {
+ "clientID": "change this",
+ "clientSecret": "change this"
+ },
+ "twitter": {
+ "consumerKey": "change this",
+ "consumerSecret": "change this"
+ },
+ "github": {
+ "clientID": "change this",
+ "clientSecret": "change this"
+ },
+ "dropbox": {
+ "clientID": "change this",
+ "clientSecret": "change this"
+ },
+ "imgur": {
+ "clientID": "change this"
+ }
+ }
+} \ No newline at end of file
diff --git a/hackmd_schema.sql b/hackmd_schema.sql
deleted file mode 100644
index ac7d671a..00000000
--- a/hackmd_schema.sql
+++ /dev/null
@@ -1,77 +0,0 @@
---
--- PostgreSQL database dump
---
-
-SET statement_timeout = 0;
-SET lock_timeout = 0;
-SET client_encoding = 'UTF8';
-SET standard_conforming_strings = on;
-SET check_function_bodies = false;
-SET client_min_messages = warning;
-
---
--- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
---
-
-CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
-
-
---
--- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
---
-
-COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
-
-
-SET search_path = public, pg_catalog;
-
-SET default_tablespace = '';
-
-SET default_with_oids = false;
-
---
--- Name: notes; Type: TABLE; Schema: public; Owner: postgres; Tablespace:
---
-
-CREATE TABLE notes (
- id character varying(256) NOT NULL,
- owner character varying(256) NOT NULL,
- content text,
- title text,
- create_time timestamp without time zone DEFAULT now() NOT NULL,
- update_time timestamp without time zone DEFAULT now() NOT NULL
-);
-
-
-ALTER TABLE notes OWNER TO "postgres";
-
---
--- Name: notes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:
---
-
-ALTER TABLE ONLY notes
- ADD CONSTRAINT notes_pkey PRIMARY KEY (id);
-
-
---
--- Name: unique_notes; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:
---
-
-ALTER TABLE ONLY notes
- ADD CONSTRAINT unique_notes UNIQUE (id);
-
-
---
--- Name: public; Type: ACL; Schema: -; Owner: postgres
---
-
-REVOKE ALL ON SCHEMA public FROM PUBLIC;
-REVOKE ALL ON SCHEMA public FROM "postgres";
-GRANT ALL ON SCHEMA public TO "postgres";
-GRANT ALL ON SCHEMA public TO PUBLIC;
-
-
---
--- PostgreSQL database dump complete
---
-
diff --git a/lib/auth.js b/lib/auth.js
index dc8b94ca..af3e8d1d 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -7,44 +7,60 @@ var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//core
-var User = require('./user.js');
-var config = require('../config.js');
+var config = require('./config.js');
var logger = require("./logger.js");
+var models = require("./models");
function callback(accessToken, refreshToken, profile, done) {
//logger.info(profile.displayName || profile.username);
- User.findOrNewUser(profile.id, profile, function (err, user) {
- if (err || user == null) {
- logger.error('auth callback failed: ' + err);
- } else {
- if (config.debug && user)
- logger.info('user login: ' + user._id);
- done(null, user);
+ models.User.findOrCreate({
+ where: {
+ profileid: profile.id.toString()
+ },
+ defaults: {
+ profile: JSON.stringify(profile)
}
- });
+ }).spread(function(user, created) {
+ if (user) {
+ if (config.debug)
+ logger.info('user login: ' + user.id);
+ return done(null, user);
+ }
+ }).catch(function(err) {
+ logger.error('auth callback failed: ' + err);
+ return done(err, null);
+ })
}
//facebook
-module.exports = passport.use(new FacebookStrategy({
- clientID: config.facebook.clientID,
- clientSecret: config.facebook.clientSecret,
- callbackURL: config.getserverurl() + config.facebook.callbackPath
-}, callback));
+if (config.facebook) {
+ module.exports = passport.use(new FacebookStrategy({
+ clientID: config.facebook.clientID,
+ clientSecret: config.facebook.clientSecret,
+ callbackURL: config.serverurl + '/auth/facebook/callback'
+ }, callback));
+}
//twitter
-passport.use(new TwitterStrategy({
- consumerKey: config.twitter.consumerKey,
- consumerSecret: config.twitter.consumerSecret,
- callbackURL: config.getserverurl() + config.twitter.callbackPath
-}, callback));
+if (config.twitter) {
+ passport.use(new TwitterStrategy({
+ consumerKey: config.twitter.consumerKey,
+ consumerSecret: config.twitter.consumerSecret,
+ callbackURL: config.serverurl + '/auth/twitter/callback'
+ }, callback));
+}
//github
-passport.use(new GithubStrategy({
- clientID: config.github.clientID,
- clientSecret: config.github.clientSecret,
- callbackURL: config.getserverurl() + config.github.callbackPath
-}, callback));
+if (config.github) {
+ passport.use(new GithubStrategy({
+ clientID: config.github.clientID,
+ clientSecret: config.github.clientSecret,
+ callbackURL: config.serverurl + '/auth/github/callback'
+ }, callback));
+}
//dropbox
-passport.use(new DropboxStrategy({
- clientID: config.dropbox.clientID,
- clientSecret: config.dropbox.clientSecret,
- callbackURL: config.getserverurl() + config.dropbox.callbackPath
-}, callback)); \ No newline at end of file
+if (config.dropbox) {
+ passport.use(new DropboxStrategy({
+ clientID: config.dropbox.clientID,
+ clientSecret: config.dropbox.clientSecret,
+ callbackURL: config.serverurl + '/auth/dropbox/callback'
+ }, callback));
+} \ No newline at end of file
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 00000000..386b0885
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,112 @@
+// external modules
+var path = require('path');
+
+// configs
+var env = process.env.NODE_ENV || 'development';
+var config = require(path.join(__dirname, '..', 'config.json'))[env];
+var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development'));
+
+// url
+var domain = config.domain || 'localhost';
+var urlpath = config.urlpath || '';
+var port = process.env.PORT || config.port || 3000;
+var alloworigin = config.alloworigin || ['localhost'];
+
+var usessl = !!config.usessl;
+var protocolusessl = (config.usessl === true && typeof config.protocolusessl === 'undefined') ? true : !!config.protocolusessl;
+var urladdport = !!config.urladdport;
+
+var usecdn = !!config.usecdn;
+
+// db
+var db = config.db || {
+ dialect: 'sqlite',
+ storage: './db.hackmd.sqlite'
+};
+
+// ssl path
+var sslkeypath = config.sslkeypath || ''
+var sslcertpath = config.sslcertpath || '';
+var sslcapath = config.sslcapath || '';
+var dhparampath = config.dhparampath || '';
+
+// other path
+var tmppath = config.tmppath || './tmp';
+var defaultnotepath = config.defaultnotepath || './public/default.md';
+var docspath = config.docspath || './public/docs';
+var indexpath = config.indexpath || './public/views/index.ejs';
+var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs';
+var errorpath = config.errorpath || './public/views/error.ejs';
+var prettypath = config.prettypath || './public/views/pretty.ejs';
+var slidepath = config.slidepath || './public/views/slide.hbs';
+
+// session
+var sessionname = config.sessionname || 'connect.sid';
+var sessionsecret = config.sessionsecret || 'secret';
+var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000; //14 days
+
+// static files
+var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000; // 1 day
+
+// socket.io
+var heartbeatinterval = config.heartbeatinterval || 5000;
+var heartbeattimeout = config.heartbeattimeout || 10000;
+
+// document
+var documentmaxlength = config.documentmaxlength || 100000;
+
+// auth
+var facebook = config.facebook || false;
+var twitter = config.twitter || false;
+var github = config.github || false;
+var dropbox = config.dropbox || false;
+var imgur = config.imgur || false;
+
+function getserverurl() {
+ var protocol = protocolusessl ? 'https://' : 'http://';
+ var url = protocol + domain;
+ if (urladdport && ((usessl && port != 443) || (!usessl && port != 80)))
+ url += ':' + port;
+ if (urlpath)
+ url += '/' + urlpath;
+ return url;
+}
+
+var version = '0.4.0';
+var cwd = path.join(__dirname, '..');
+
+module.exports = {
+ version: version,
+ debug: debug,
+ urlpath: urlpath,
+ port: port,
+ alloworigin: alloworigin,
+ usessl: usessl,
+ serverurl: getserverurl(),
+ usecdn: usecdn,
+ db: db,
+ sslkeypath: path.join(cwd, sslkeypath),
+ sslcertpath: path.join(cwd, sslcertpath),
+ sslcapath: path.join(cwd, sslcapath),
+ dhparampath: path.join(cwd, dhparampath),
+ tmppath: path.join(cwd, tmppath),
+ defaultnotepath: path.join(cwd, defaultnotepath),
+ docspath: path.join(cwd, docspath),
+ indexpath: path.join(cwd, indexpath),
+ hackmdpath: path.join(cwd, hackmdpath),
+ errorpath: path.join(cwd, errorpath),
+ prettypath: path.join(cwd, prettypath),
+ slidepath: path.join(cwd, slidepath),
+ sessionname: sessionname,
+ sessionsecret: sessionsecret,
+ sessionlife: sessionlife,
+ staticcachetime: staticcachetime,
+ heartbeatinterval: heartbeatinterval,
+ heartbeattimeout: heartbeattimeout,
+ documentmaxlength: documentmaxlength,
+ facebook: facebook,
+ twitter: twitter,
+ github: github,
+ dropbox: dropbox,
+ imgur: imgur
+}; \ No newline at end of file
diff --git a/lib/db.js b/lib/db.js
deleted file mode 100644
index 1ac6f0e1..00000000
--- a/lib/db.js
+++ /dev/null
@@ -1,151 +0,0 @@
-//db
-//external modules
-var pg = require('pg');
-var fs = require('fs');
-var util = require('util');
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-//public
-var db = {
- readFromFile: readFromDB,
- saveToFile: saveToFile,
- newToDB: newToDB,
- readFromDB: readFromDB,
- saveToDB: saveToDB,
- countFromDB: countFromDB
-};
-
-function getDBClient() {
- return new pg.Client(process.env.DATABASE_URL || config.postgresqlstring);
-}
-
-function readFromFile(callback) {
- fs.readFile('hackmd', 'utf8', function (err, data) {
- if (err) throw err;
- callback(data);
- });
-}
-
-function saveToFile(doc) {
- fs.writeFile('hackmd', doc, function (err) {
- if (err) throw err;
- });
-}
-
-var updatequery = "UPDATE notes SET title='%s', content='%s', update_time=NOW() WHERE id='%s';";
-var insertquery = "INSERT INTO notes (id, owner, content) VALUES ('%s', '%s', '%s');";
-var insertifnotexistquery = "INSERT INTO notes (id, owner, content) \
-SELECT '%s', '%s', '%s' \
-WHERE NOT EXISTS (SELECT 1 FROM notes WHERE id='%s') RETURNING *;";
-var selectquery = "SELECT * FROM notes WHERE id='%s';";
-var countquery = "SELECT count(*) FROM notes;";
-
-function newToDB(id, owner, body, callback) {
- var client = getDBClient();
- client.connect(function (err) {
- if (err) {
- client.end();
- callback(err, null);
- return logger.error('could not connect to postgres', err);
- }
- var newnotequery = util.format(insertquery, id, owner, body);
- //logger.info(newnotequery);
- client.query(newnotequery, function (err, result) {
- client.end();
- if (err) {
- callback(err, null);
- return logger.error("new note to db failed: " + err);
- } else {
- if (config.debug)
- logger.info("new note to db success");
- callback(null, result);
- }
- });
- });
-}
-
-function readFromDB(id, callback) {
- var client = getDBClient();
- client.connect(function (err) {
- if (err) {
- client.end();
- callback(err, null);
- return logger.error('could not connect to postgres', err);
- }
- var readquery = util.format(selectquery, id);
- //logger.info(readquery);
- client.query(readquery, function (err, result) {
- client.end();
- if (err) {
- callback(err, null);
- return logger.error("read from db failed: " + err);
- } else {
- //logger.info(result.rows);
- if (result.rows.length <= 0) {
- callback("not found note in db", null);
- return logger.error("not found note in db: " + id, err);
- } else {
- if(config.debug)
- logger.info("read from db success");
- callback(null, result);
- }
- }
- });
- });
-}
-
-function saveToDB(id, title, data, callback) {
- var client = getDBClient();
- client.connect(function (err) {
- if (err) {
- client.end();
- callback(err, null);
- return logger.error('could not connect to postgres', err);
- }
- var savequery = util.format(updatequery, title, data, id);
- //logger.info(savequery);
- client.query(savequery, function (err, result) {
- client.end();
- if (err) {
- callback(err, null);
- return logger.error("save to db failed: " + err);
- } else {
- if (config.debug)
- logger.info("save to db success");
- callback(null, result);
- }
- });
- });
-}
-
-function countFromDB(callback) {
- var client = getDBClient();
- client.connect(function (err) {
- if (err) {
- client.end();
- callback(err, null);
- return logger.error('could not connect to postgres', err);
- }
- client.query(countquery, function (err, result) {
- client.end();
- if (err) {
- callback(err, null);
- return logger.error("count from db failed: " + err);
- } else {
- //logger.info(result.rows);
- if (result.rows.length <= 0) {
- callback("not found note in db", null);
- } else {
- if(config.debug)
- logger.info("count from db success");
- callback(null, result);
- }
- }
- });
- });
-}
-
-module.exports = db; \ No newline at end of file
diff --git a/lib/models/index.js b/lib/models/index.js
new file mode 100644
index 00000000..3b49d459
--- /dev/null
+++ b/lib/models/index.js
@@ -0,0 +1,37 @@
+"use strict";
+
+// external modules
+var fs = require("fs");
+var path = require("path");
+var Sequelize = require("sequelize");
+
+// core
+var config = require('../config.js');
+var logger = require("../logger.js");
+
+var dbconfig = config.db;
+dbconfig.logging = config.debug ? logger.info : false;
+var sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig);
+
+var db = {};
+
+fs
+ .readdirSync(__dirname)
+ .filter(function (file) {
+ return (file.indexOf(".") !== 0) && (file !== "index.js");
+ })
+ .forEach(function (file) {
+ var model = sequelize.import(path.join(__dirname, file));
+ db[model.name] = model;
+ });
+
+Object.keys(db).forEach(function (modelName) {
+ if ("associate" in db[modelName]) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+module.exports = db; \ No newline at end of file
diff --git a/lib/models/note.js b/lib/models/note.js
new file mode 100644
index 00000000..96043b75
--- /dev/null
+++ b/lib/models/note.js
@@ -0,0 +1,208 @@
+"use strict";
+
+// external modules
+var fs = require('fs');
+var path = require('path');
+var LZString = require('lz-string');
+var marked = require('marked');
+var cheerio = require('cheerio');
+var shortId = require('shortid');
+var Sequelize = require("sequelize");
+var async = require('async');
+
+// 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
+ },
+ lastchangeAt: {
+ type: DataTypes.DATE
+ }
+ }, {
+ classMethods: {
+ associate: function (models) {
+ Note.belongsTo(models.User, {
+ foreignKey: "ownerId",
+ as: "owner",
+ constraints: false
+ });
+ Note.belongsTo(models.User, {
+ foreignKey: "lastchangeuserId",
+ as: "lastchangeuser",
+ constraints: false
+ });
+ },
+ 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) {
+ return callback(null, note.id);
+ } 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 $ = cheerio.load(marked(body));
+ var h1s = $("h1");
+ var title = "";
+ if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
+ title = h1s.first().text();
+ else
+ title = "Untitled";
+ return title;
+ },
+ decodeTitle: function (title) {
+ var decodedTitle = LZString.decompressFromBase64(title);
+ if (decodedTitle) title = decodedTitle;
+ else title = 'Untitled';
+ return title;
+ },
+ generateWebTitle: function (title) {
+ title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
+ return title;
+ }
+ },
+ 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)) {
+ body = fs.readFileSync(filePath, 'utf8');
+ note.title = LZString.compressToBase64(Note.parseNoteTitle(body));
+ note.content = LZString.compressToBase64(body);
+ }
+ }
+ // 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);
+ }
+ }
+ });
+
+ return Note;
+}; \ No newline at end of file
diff --git a/lib/models/temp.js b/lib/models/temp.js
new file mode 100644
index 00000000..6eeff153
--- /dev/null
+++ b/lib/models/temp.js
@@ -0,0 +1,19 @@
+"use strict";
+
+//external modules
+var shortId = require('shortid');
+
+module.exports = function (sequelize, DataTypes) {
+ var Temp = sequelize.define("Temp", {
+ id: {
+ type: DataTypes.STRING,
+ primaryKey: true,
+ defaultValue: shortId.generate
+ },
+ data: {
+ type: DataTypes.TEXT
+ }
+ });
+
+ return Temp;
+}; \ No newline at end of file
diff --git a/lib/models/user.js b/lib/models/user.js
new file mode 100644
index 00000000..e1a373d6
--- /dev/null
+++ b/lib/models/user.js
@@ -0,0 +1,77 @@
+"use strict";
+
+// external modules
+var md5 = require("blueimp-md5");
+var Sequelize = require("sequelize");
+
+// core
+var logger = require("../logger.js");
+
+module.exports = function (sequelize, DataTypes) {
+ var User = sequelize.define("User", {
+ id: {
+ type: DataTypes.UUID,
+ primaryKey: true,
+ defaultValue: Sequelize.UUIDV4
+ },
+ profileid: {
+ type: DataTypes.STRING,
+ unique: true
+ },
+ profile: {
+ type: DataTypes.TEXT
+ },
+ history: {
+ type: DataTypes.TEXT
+ }
+ }, {
+ classMethods: {
+ associate: function (models) {
+ User.hasMany(models.Note, {
+ foreignKey: "ownerId",
+ constraints: false
+ });
+ User.hasMany(models.Note, {
+ foreignKey: "lastchangeuserId",
+ constraints: false
+ });
+ },
+ parseProfile: function (profile) {
+ try {
+ var profile = JSON.parse(profile);
+ } catch (err) {
+ logger.error(err);
+ profile = null;
+ }
+ if (profile) {
+ profile = {
+ name: profile.displayName || profile.username,
+ photo: User.parsePhotoByProfile(profile)
+ }
+ }
+ return profile;
+ },
+ parsePhotoByProfile: function (profile) {
+ var photo = null;
+ switch (profile.provider) {
+ case "facebook":
+ photo = 'https://graph.facebook.com/' + profile.id + '/picture';
+ break;
+ case "twitter":
+ photo = profile.photos[0].value;
+ break;
+ case "github":
+ photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
+ break;
+ case "dropbox":
+ //no image api provided, use gravatar
+ photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
+ break;
+ }
+ return photo;
+ }
+ }
+ });
+
+ return User;
+}; \ No newline at end of file
diff --git a/lib/note.js b/lib/note.js
deleted file mode 100644
index 0965b23e..00000000
--- a/lib/note.js
+++ /dev/null
@@ -1,237 +0,0 @@
-//note
-//external modules
-var mongoose = require('mongoose');
-var Schema = mongoose.Schema;
-var LZString = require('lz-string');
-var marked = require('marked');
-var cheerio = require('cheerio');
-var shortId = require('shortid');
-
-//others
-var db = require("./db.js");
-var logger = require("./logger.js");
-
-//permission types
-permissionTypes = ["freely", "editable", "locked", "private"];
-
-// create a note model
-var model = mongoose.model('note', {
- id: String,
- shortid: {
- type: String,
- unique: true,
- default: shortId.generate
- },
- permission: {
- type: String,
- enum: permissionTypes
- },
- lastchangeuser: {
- type: Schema.Types.ObjectId,
- ref: 'user'
- },
- viewcount: {
- type: Number,
- default: 0
- },
- updated: Date,
- created: Date
-});
-
-//public
-var note = {
- model: model,
- findNote: findNote,
- newNote: newNote,
- findOrNewNote: findOrNewNote,
- checkNoteIdValid: checkNoteIdValid,
- checkNoteExist: checkNoteExist,
- getNoteTitle: getNoteTitle,
- decodeTitle: decodeTitle,
- generateWebTitle: generateWebTitle,
- increaseViewCount: increaseViewCount,
- updatePermission: updatePermission,
- updateLastChangeUser: updateLastChangeUser
-};
-
-function checkNoteIdValid(noteId) {
- try {
- //logger.info(noteId);
- var id = LZString.decompressFromBase64(noteId);
- if (!id) return false;
- 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;
- } catch (err) {
- logger.error(err);
- return false;
- }
-}
-
-function checkNoteExist(noteId) {
- try {
- //logger.info(noteId);
- var id = LZString.decompressFromBase64(noteId);
- db.readFromDB(id, function (err, result) {
- if (err) return false;
- return true;
- });
- } catch (err) {
- logger.error(err);
- return false;
- }
-}
-
-//get title
-function getNoteTitle(body) {
- var $ = cheerio.load(marked(body));
- var h1s = $("h1");
- var title = "";
- if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
- title = h1s.first().text();
- else
- title = "Untitled";
- return title;
-}
-
-// decode title
-function decodeTitle(title) {
- var decodedTitle = LZString.decompressFromBase64(title);
- if (decodedTitle) title = decodedTitle;
- else title = 'Untitled';
- return title;
-}
-
-//generate note web page title
-function generateWebTitle(title) {
- title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
- return title;
-}
-
-function findNote(id, callback) {
- model.findOne({
- $or: [
- {
- id: id
- },
- {
- shortid: id
- }
- ]
- }, function (err, note) {
- if (err) {
- logger.error('find note failed: ' + err);
- callback(err, null);
- }
- if (!err && note) {
- callback(null, note);
- } else {
- logger.error('find note failed: ' + err);
- callback(err, null);
- };
- });
-}
-
-function newNote(id, owner, callback) {
- var permission = "freely";
- if (owner && owner != "null") {
- permission = "editable";
- }
- var note = new model({
- id: id,
- permission: permission,
- updated: Date.now(),
- created: Date.now()
- });
- note.save(function (err) {
- if (err) {
- logger.error('new note failed: ' + err);
- callback(err, null);
- } else {
- logger.info("new note success: " + note.id);
- callback(null, note);
- };
- });
-}
-
-function findOrNewNote(id, owner, callback) {
- findNote(id, function (err, note) {
- if (err || !note) {
- newNote(id, owner, function (err, note) {
- if (err) {
- logger.error('find or new note failed: ' + err);
- callback(err, null);
- } else {
- callback(null, note);
- }
- });
- } else {
- if (!note.permission) {
- var permission = "freely";
- if (owner && owner != "null") {
- permission = "editable";
- }
- note.permission = permission;
- note.updated = Date.now();
- note.save(function (err) {
- if (err) {
- logger.error('add note permission failed: ' + err);
- callback(err, null);
- } else {
- logger.info("add note permission success: " + note.id);
- callback(null, note);
- };
- });
- } else {
- callback(null, note);
- }
- }
- });
-}
-
-function increaseViewCount(note, callback) {
- note.viewcount++;
- note.updated = Date.now();
- note.save(function (err) {
- if (err) {
- logger.error('increase note viewcount failed: ' + err);
- callback(err, null);
- } else {
- logger.info("increase note viewcount success: " + note.id);
- callback(null, note);
- };
- });
-}
-
-function updatePermission(note, permission, callback) {
- note.permission = permission;
- note.updated = Date.now();
- note.save(function (err) {
- if (err) {
- logger.error('update note permission failed: ' + err);
- callback(err, null);
- } else {
- logger.info("update note permission success: " + note.id);
- callback(null, note);
- };
- });
-}
-
-function updateLastChangeUser(note, lastchangeuser, callback) {
- note.lastchangeuser = lastchangeuser;
- note.updated = Date.now();
- note.save(function (err) {
- if (err) {
- logger.error('update note lastchangeuser failed: ' + err);
- callback(err, null);
- } else {
- logger.info("update note lastchangeuser success: " + note.id);
- callback(null, note);
- };
- });
-}
-
-module.exports = note; \ No newline at end of file
diff --git a/lib/ot/server.js b/lib/ot/server.js
index b6559291..227eba25 100644
--- a/lib/ot/server.js
+++ b/lib/ot/server.js
@@ -1,4 +1,4 @@
-var config = require('../../config');
+var config = require('../config');
if (typeof ot === 'undefined') {
var ot = {};
diff --git a/lib/realtime.js b/lib/realtime.js
index 081d401e..a8bef97a 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -5,24 +5,19 @@ var cookieParser = require('cookie-parser');
var url = require('url');
var async = require('async');
var LZString = require('lz-string');
-var shortId = require('shortid');
var randomcolor = require("randomcolor");
var Chance = require('chance'),
chance = new Chance();
var moment = require('moment');
//core
-var config = require("../config.js");
+var config = require("./config.js");
var logger = require("./logger.js");
+var models = require("./models");
//ot
var ot = require("./ot/index.js");
-//others
-var db = require("./db.js");
-var Note = require("./note.js");
-var User = require("./user.js");
-
//public
var realtime = {
io: null,
@@ -72,12 +67,6 @@ function emitCheck(note) {
lastchangeuserprofile: note.lastchangeuserprofile
};
realtime.io.to(note.id).emit('check', out);
- /*
- for (var i = 0, l = note.socks.length; i < l; i++) {
- var sock = note.socks[i];
- sock.emit('check', out);
- };
- */
}
//actions
@@ -88,70 +77,82 @@ var updater = setInterval(function () {
async.each(Object.keys(notes), function (key, callback) {
var note = notes[key];
if (note.server.isDirty) {
- if (config.debug)
- logger.info("updater found dirty note: " + key);
- updaterUpdateMongo(note, function(err, result) {
- if (err) return callback(err, null);
- updaterUpdatePostgres(note, function(err, result) {
- if (err) return callback(err, null);
- callback(null, null);
- });
+ if (config.debug) logger.info("updater found dirty note: " + key);
+ updateNote(note, function(err, _note) {
+ if (!_note) {
+ realtime.io.to(note.id).emit('info', {
+ code: 404
+ });
+ logger.error('note not found: ', note.id);
+ }
+ if (err || !_note) {
+ for (var i = 0, l = note.socks.length; i < l; i++) {
+ var sock = note.socks[i];
+ sock.disconnect(true);
+ }
+ return callback(err, null);
+ }
+ note.server.isDirty = false;
+ note.updatetime = moment(_note.lastchangeAt).valueOf();
+ emitCheck(note);
+ return callback(null, null);
});
} else {
- callback(null, null);
+ return callback(null, null);
}
}, function (err) {
if (err) return logger.error('updater error', err);
});
}, 1000);
-function updaterUpdateMongo(note, callback) {
- Note.findNote(note.id, function (err, _note) {
- if (err || !_note) return callback(err, null);
+function updateNote(note, callback) {
+ models.Note.findOne({
+ where: {
+ id: note.id
+ }
+ }).then(function (_note) {
+ if (!_note) return callback(null, null);
if (note.lastchangeuser) {
- if (_note.lastchangeuser != note.lastchangeuser) {
- var lastchangeuser = note.lastchangeuser;
- var lastchangeuserprofile = null;
- User.findUser(lastchangeuser, function (err, user) {
- if (err) return callback(err, null);
- if (user && user.profile) {
- var profile = JSON.parse(user.profile);
- if (profile) {
- lastchangeuserprofile = {
- name: profile.displayName || profile.username,
- photo: User.parsePhotoByProfile(profile)
- }
- _note.lastchangeuser = lastchangeuser;
- note.lastchangeuserprofile = lastchangeuserprofile;
- Note.updateLastChangeUser(_note, lastchangeuser, function (err, result) {
- if (err) return callback(err, null);
- callback(null, null);
- });
- }
+ if (_note.lastchangeuserId != note.lastchangeuser) {
+ models.User.findOne({
+ where: {
+ id: note.lastchangeuser
}
+ }).then(function (user) {
+ if (!user) return callback(null, null);
+ note.lastchangeuserprofile = models.User.parseProfile(user.profile);
+ return finishUpdateNote(note, _note, callback);
+ }).catch(function (err) {
+ logger.error(err);
+ return callback(err, null);
});
+ } else {
+ return finishUpdateNote(note, _note, callback);
}
} else {
- _note.lastchangeuser = null;
note.lastchangeuserprofile = null;
- Note.updateLastChangeUser(_note, null, function (err, result) {
- if (err) return callback(err, null);
- callback(null, null);
- });
+ return finishUpdateNote(note, _note, callback);
}
+ }).catch(function (err) {
+ logger.error(err);
+ return callback(err, null);
});
}
-function updaterUpdatePostgres(note, callback) {
- //postgres update
+function finishUpdateNote(note, _note, callback) {
var body = note.server.document;
- var title = Note.getNoteTitle(body);
+ var title = models.Note.parseNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
- db.saveToDB(note.id, title, body, function (err, result) {
- if (err) return callback(err, null);
- note.server.isDirty = false;
- note.updatetime = Date.now();
- emitCheck(note);
- callback(null, null);
+ var values = {
+ title: title,
+ content: body,
+ lastchangeuserId: note.lastchangeuser,
+ lastchangeAt: Date.now()
+ };
+ _note.update(values).then(function (_note) {
+ return callback(null, _note);
+ }).catch(function (err) {
+ logger.error(err);
+ return callback(err, null);
});
}
//clean when user not in any rooms or user not in connected list
@@ -170,15 +171,14 @@ var cleaner = setInterval(function () {
disconnectSocketQueue.push(socket);
disconnect(socket);
}
- callback(null, null);
+ return callback(null, null);
}, function (err) {
if (err) return logger.error('cleaner error', err);
});
}, 60000);
function getStatus(callback) {
- db.countFromDB(function (err, data) {
- if (err) return logger.info(err);
+ models.Note.count().then(function (notecount) {
var distinctaddresses = [];
var regaddresses = [];
var distinctregaddresses = [];
@@ -208,58 +208,58 @@ function getStatus(callback) {
}
}
});
- User.getUserCount(function (err, regcount) {
- if (err) {
- logger.error('get status failed: ' + err);
- return;
- }
- if (callback)
- callback({
- onlineNotes: Object.keys(notes).length,
- onlineUsers: Object.keys(users).length,
- distinctOnlineUsers: distinctaddresses.length,
- notesCount: data.rows[0].count,
- registeredUsers: regcount,
- onlineRegisteredUsers: regaddresses.length,
- distinctOnlineRegisteredUsers: distinctregaddresses.length,
- isConnectionBusy: isConnectionBusy,
- connectionSocketQueueLength: connectionSocketQueue.length,
- isDisconnectBusy: isDisconnectBusy,
- disconnectSocketQueueLength: disconnectSocketQueue.length
- });
+ models.User.count().then(function (regcount) {
+ return callback ? callback({
+ onlineNotes: Object.keys(notes).length,
+ onlineUsers: Object.keys(users).length,
+ distinctOnlineUsers: distinctaddresses.length,
+ notesCount: notecount,
+ registeredUsers: regcount,
+ onlineRegisteredUsers: regaddresses.length,
+ distinctOnlineRegisteredUsers: distinctregaddresses.length,
+ isConnectionBusy: isConnectionBusy,
+ connectionSocketQueueLength: connectionSocketQueue.length,
+ isDisconnectBusy: isDisconnectBusy,
+ disconnectSocketQueueLength: disconnectSocketQueue.length
+ }) : null;
+ }).catch(function (err) {
+ return logger.error('count user failed: ' + err);
});
+ }).catch(function (err) {
+ return logger.error('count note failed: ' + err);
});
}
-function getNotenameFromSocket(socket) {
+function extractNoteIdFromSocket(socket) {
if (!socket || !socket.handshake || !socket.handshake.headers) {
- return;
+ return false;
}
var referer = socket.handshake.headers.referer;
if (!referer) {
- return socket.disconnect(true);
+ return false;
}
var hostUrl = url.parse(referer);
- var notename = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1];
- if (notename == config.featuresnotename) {
- return notename;
- }
- if (!Note.checkNoteIdValid(notename)) {
- socket.emit('info', {
- code: 404
- });
- return socket.disconnect(true);
+ var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1];
+ return noteId;
+}
+
+function parseNoteIdFromSocket(socket, callback) {
+ var noteId = extractNoteIdFromSocket(socket);
+ if (!noteId) {
+ return callback(null, null);
}
- notename = LZString.decompressFromBase64(notename);
- return notename;
+ models.Note.parseNoteId(noteId, function (err, id) {
+ if (err || !id) return callback(err, id);
+ return callback(null, id);
+ });
}
function emitOnlineUsers(socket) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
var users = [];
- Object.keys(notes[notename].users).forEach(function (key) {
- var user = notes[notename].users[key];
+ Object.keys(notes[noteId].users).forEach(function (key) {
+ var user = notes[noteId].users[key];
if (user)
users.push(buildUserOutData(user));
});
@@ -267,35 +267,20 @@ function emitOnlineUsers(socket) {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
- realtime.io.to(notename).emit('online users', out);
- /*
- for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
- var sock = notes[notename].socks[i];
- if (sock && out)
- sock.emit('online users', out);
- };
- */
+ realtime.io.to(noteId).emit('online users', out);
}
function emitUserStatus(socket) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
var out = buildUserOutData(users[socket.id]);
- socket.broadcast.to(notename).emit('user status', out);
- /*
- for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
- var sock = notes[notename].socks[i];
- if (sock != socket) {
- sock.emit('user status', out);
- }
- };
- */
+ socket.broadcast.to(noteId).emit('user status', out);
}
function emitRefresh(socket) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- var note = notes[notename];
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
+ var note = notes[noteId];
socket.emit('refresh', {
docmaxlength: config.documentmaxlength,
owner: note.owner,
@@ -326,15 +311,10 @@ function finishConnection(socket, note, user) {
if (!socket || !note || !user) return;
//check view permission
if (note.permission == 'private') {
- if (socket.request.user && socket.request.user.logged_in && socket.request.user._id == note.owner) {
+ if (socket.request.user && socket.request.user.logged_in && socket.request.user.id == note.owner) {
//na
} else {
- socket.emit('info', {
- code: 403
- });
- clearSocketQueue(connectionSocketQueue, socket);
- isConnectionBusy = false;
- return socket.disconnect(true);
+ return failConnection(403, 'connection forbidden', socket);
}
}
note.users[socket.id] = user;
@@ -354,8 +334,8 @@ function finishConnection(socket, note, user) {
startConnection(connectionSocketQueue[0]);
if (config.debug) {
- var notename = getNotenameFromSocket(socket);
- logger.info('SERVER connected a client to [' + notename + ']:');
+ var noteId = socket.noteId;
+ logger.info('SERVER connected a client to [' + noteId + ']:');
logger.info(JSON.stringify(user));
//logger.info(notes);
getStatus(function (data) {
@@ -367,117 +347,76 @@ function finishConnection(socket, note, user) {
function startConnection(socket) {
if (isConnectionBusy) return;
isConnectionBusy = true;
+
+ var noteId = socket.noteId;
+ if (!noteId) {
+ return failConnection(404, 'note id not found', socket);
+ }
- var notename = getNotenameFromSocket(socket);
- if (!notename) {
- clearSocketQueue(connectionSocketQueue, socket);
- isConnectionBusy = false;
- return;
- }
-
- if (!notes[notename]) {
- db.readFromDB(notename, function (err, data) {
- if (err) {
- socket.emit('info', {
- code: 404
- });
- socket.disconnect(true);
- //clear err socket in queue
- clearSocketQueue(connectionSocketQueue, socket);
- isConnectionBusy = false;
- return logger.error(err);
+ if (!notes[noteId]) {
+ var include = [{
+ model: models.User,
+ as: "owner"
+ }, {
+ model: models.User,
+ as: "lastchangeuser"
+ }];
+
+ models.Note.findOne({
+ where: {
+ id: noteId
+ },
+ include: include
+ }).then(function (note) {
+ if (!note) {
+ return failConnection(404, 'note not found', socket);
}
-
- var owner = data.rows[0].owner;
- var ownerprofile = null;
-
- //find or new note
- Note.findOrNewNote(notename, owner, function (err, note) {
- if (err) {
- socket.emit('info', {
- code: 404
- });
- socket.disconnect(true);
- clearSocketQueue(connectionSocketQueue, socket);
- isConnectionBusy = false;
- return logger.error(err);
- }
-
- var body = LZString.decompressFromBase64(data.rows[0].content);
- //body = LZString.compressToUTF16(body);
- var createtime = data.rows[0].create_time;
- var updatetime = data.rows[0].update_time;
- var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit);
-
- var lastchangeuser = note.lastchangeuser || null;
- var lastchangeuserprofile = null;
-
- notes[notename] = {
- id: notename,
- owner: owner,
- ownerprofile: ownerprofile,
- permission: note.permission,
- lastchangeuser: lastchangeuser,
- lastchangeuserprofile: lastchangeuserprofile,
- socks: [],
- users: {},
- createtime: moment(createtime).valueOf(),
- updatetime: moment(updatetime).valueOf(),
- server: server
- };
-
- async.parallel([
- function getlastchangeuser(callback) {
- if (lastchangeuser) {
- //find last change user profile if lastchangeuser exists
- User.findUser(lastchangeuser, function (err, user) {
- if (!err && user && user.profile) {
- var profile = JSON.parse(user.profile);
- if (profile) {
- lastchangeuserprofile = {
- name: profile.displayName || profile.username,
- photo: User.parsePhotoByProfile(profile)
- }
- notes[notename].lastchangeuserprofile = lastchangeuserprofile;
- }
- }
- callback(null, null);
- });
- } else {
- callback(null, null);
- }
- },
- function getowner(callback) {
- if (owner && owner != "null") {
- //find owner profile if owner exists
- User.findUser(owner, function (err, user) {
- if (!err && user && user.profile) {
- var profile = JSON.parse(user.profile);
- if (profile) {
- ownerprofile = {
- name: profile.displayName || profile.username,
- photo: User.parsePhotoByProfile(profile)
- }
- notes[notename].ownerprofile = ownerprofile;
- }
- }
- callback(null, null);
- });
- } else {
- callback(null, null);
- }
- }
- ], function(err, results){
- if (err) return;
- finishConnection(socket, notes[notename], users[socket.id]);
- });
- });
+ var owner = note.ownerId;
+ var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null;
+
+ var lastchangeuser = note.lastchangeuserId;
+ var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null;
+
+ var body = LZString.decompressFromBase64(note.content);
+ var createtime = note.createdAt;
+ var updatetime = note.lastchangeAt;
+ var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
+
+ notes[noteId] = {
+ id: noteId,
+ owner: owner,
+ ownerprofile: ownerprofile,
+ permission: note.permission,
+ lastchangeuser: lastchangeuser,
+ lastchangeuserprofile: lastchangeuserprofile,
+ socks: [],
+ users: {},
+ createtime: moment(createtime).valueOf(),
+ updatetime: moment(updatetime).valueOf(),
+ server: server
+ };
+
+ return finishConnection(socket, notes[noteId], users[socket.id]);
+ }).catch(function (err) {
+ return failConnection(500, err, socket);
});
} else {
- finishConnection(socket, notes[notename], users[socket.id]);
+ return finishConnection(socket, notes[noteId], users[socket.id]);
}
}
+function failConnection(code, err, socket) {
+ logger.error(err);
+ // clear error socket in queue
+ clearSocketQueue(connectionSocketQueue, socket);
+ isConnectionBusy = false;
+ // emit error info
+ socket.emit('info', {
+ code: code
+ });
+ return socket.disconnect(true);
+}
+
function disconnect(socket) {
if (isDisconnectBusy) return;
isDisconnectBusy = true;
@@ -490,8 +429,8 @@ function disconnect(socket) {
if (users[socket.id]) {
delete users[socket.id];
}
- var notename = getNotenameFromSocket(socket);
- var note = notes[notename];
+ var noteId = socket.noteId;
+ var note = notes[noteId];
if (note) {
delete note.users[socket.id];
do {
@@ -502,22 +441,18 @@ function disconnect(socket) {
} while (index != -1);
if (Object.keys(note.users).length <= 0) {
if (note.server.isDirty) {
- var body = note.server.document;
- var title = Note.getNoteTitle(body);
- title = LZString.compressToBase64(title);
- body = LZString.compressToBase64(body);
- db.saveToDB(notename, title, body,
- function (err, result) {
- delete notes[notename];
- if (config.debug) {
- //logger.info(notes);
- getStatus(function (data) {
- logger.info(JSON.stringify(data));
- });
- }
- });
+ updateNote(note, function (err, _note) {
+ if (err) return logger.error('disconnect note failed: ' + err);
+ delete notes[noteId];
+ if (config.debug) {
+ //logger.info(notes);
+ getStatus(function (data) {
+ logger.info(JSON.stringify(data));
+ });
+ }
+ });
} else {
- delete notes[notename];
+ delete notes[noteId];
}
}
}
@@ -556,10 +491,10 @@ function buildUserOutData(user) {
function updateUserData(socket, user) {
//retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) {
- var profile = JSON.parse(socket.request.user.profile);
- user.photo = User.parsePhotoByProfile(profile);
- user.name = profile.displayName || profile.username;
- user.userid = socket.request.user._id;
+ var profile = models.User.parseProfile(socket.request.user.profile);
+ user.photo = profile.photo;
+ user.name = profile.name;
+ user.userid = socket.request.user.id;
user.login = true;
} else {
user.userid = null;
@@ -569,9 +504,9 @@ function updateUserData(socket, user) {
}
function ifMayEdit(socket, callback) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- var note = notes[notename];
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
+ var note = notes[noteId];
var mayEdit = true;
switch (note.permission) {
case "freely":
@@ -584,69 +519,78 @@ function ifMayEdit(socket, callback) {
break;
case "locked": case "private":
//only owner can change
- if (note.owner != socket.request.user._id)
+ if (note.owner != socket.request.user.id)
mayEdit = false;
break;
}
//if user may edit and this note have owner (not anonymous usage)
- if (socket.origin == 'operation' && mayEdit && note.owner && note.owner != "null") {
+ if (socket.origin == 'operation' && mayEdit && note.owner) {
//save for the last change user id
if (socket.request.user && socket.request.user.logged_in) {
- note.lastchangeuser = socket.request.user._id;
+ note.lastchangeuser = socket.request.user.id;
} else {
note.lastchangeuser = null;
}
}
- callback(mayEdit);
+ return callback(mayEdit);
}
function connection(socket) {
- //split notename from socket
- var notename = getNotenameFromSocket(socket);
-
- //initialize user data
- //random color
- var color = randomcolor({
- luminosity: 'light'
- });
- //make sure color not duplicated or reach max random count
- if (notename && notes[notename]) {
- var randomcount = 0;
- var maxrandomcount = 5;
- var found = false;
- do {
- Object.keys(notes[notename].users).forEach(function (user) {
- if (user.color == color) {
- found = true;
- return;
- }
- });
- if (found) {
- color = randomcolor({
- luminosity: 'light'
+ parseNoteIdFromSocket(socket, function (err, noteId) {
+ if (err) {
+ return failConnection(500, err, socket);
+ }
+ if (!noteId) {
+ return failConnection(404, 'note id not found', socket);
+ }
+
+ // store noteId in this socket session
+ socket.noteId = noteId;
+
+ //initialize user data
+ //random color
+ var color = randomcolor({
+ luminosity: 'light'
+ });
+ //make sure color not duplicated or reach max random count
+ if (notes[noteId]) {
+ var randomcount = 0;
+ var maxrandomcount = 5;
+ var found = false;
+ do {
+ Object.keys(notes[noteId].users).forEach(function (user) {
+ if (user.color == color) {
+ found = true;
+ return;
+ }
});
- randomcount++;
- }
- } while (found && randomcount < maxrandomcount);
- }
- //create user data
- users[socket.id] = {
- id: socket.id,
- address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
- 'user-agent': socket.handshake.headers['user-agent'],
- color: color,
- cursor: null,
- login: false,
- userid: null,
- name: null,
- idle: false,
- type: null
- };
- updateUserData(socket, users[socket.id]);
+ if (found) {
+ color = randomcolor({
+ luminosity: 'light'
+ });
+ randomcount++;
+ }
+ } while (found && randomcount < maxrandomcount);
+ }
+ //create user data
+ users[socket.id] = {
+ id: socket.id,
+ address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
+ 'user-agent': socket.handshake.headers['user-agent'],
+ color: color,
+ cursor: null,
+ login: false,
+ userid: null,
+ name: null,
+ idle: false,
+ type: null
+ };
+ updateUserData(socket, users[socket.id]);
- //start connection
- connectionSocketQueue.push(socket);
- startConnection(socket);
+ //start connection
+ connectionSocketQueue.push(socket);
+ startConnection(socket);
+ });
//received client refresh request
socket.on('refresh', function () {
@@ -655,10 +599,10 @@ function connection(socket) {
//received user status
socket.on('user status', function (data) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
if (config.debug)
- logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
+ logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
if (data) {
var user = users[socket.id];
user.idle = data.idle;
@@ -671,41 +615,44 @@ function connection(socket) {
socket.on('permission', function (permission) {
//need login to do more actions
if (socket.request.user && socket.request.user.logged_in) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- var note = notes[notename];
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
+ var note = notes[noteId];
//Only owner can change permission
- if (note.owner == socket.request.user._id) {
+ if (note.owner == socket.request.user.id) {
note.permission = permission;
- Note.findNote(notename, function (err, _note) {
- if (err || !_note) {
+ models.Note.update({
+ permission: permission
+ }, {
+ where: {
+ id: noteId
+ }
+ }).then(function (count) {
+ if (!count) {
return;
}
- Note.updatePermission(_note, permission, function (err, _note) {
- if (err || !_note) {
- return;
- }
- var out = {
- permission: permission
- };
- realtime.io.to(note.id).emit('permission', out);
- for (var i = 0, l = note.socks.length; i < l; i++) {
- var sock = note.socks[i];
- if (typeof sock !== 'undefined' && sock) {
- //check view permission
- if (permission == 'private') {
- if (sock.request.user && sock.request.user.logged_in && sock.request.user._id == note.owner) {
- //na
- } else {
- sock.emit('info', {
- code: 403
- });
- return sock.disconnect(true);
- }
+ var out = {
+ permission: permission
+ };
+ realtime.io.to(note.id).emit('permission', out);
+ for (var i = 0, l = note.socks.length; i < l; i++) {
+ var sock = note.socks[i];
+ if (typeof sock !== 'undefined' && sock) {
+ //check view permission
+ if (permission == 'private') {
+ if (sock.request.user && sock.request.user.logged_in && sock.request.user.id == note.owner) {
+ //na
+ } else {
+ sock.emit('info', {
+ code: 403
+ });
+ return sock.disconnect(true);
}
}
}
- });
+ }
+ }).catch(function (err) {
+ return logger.error('update note permission failed: ' + err);
});
}
}
@@ -714,19 +661,19 @@ function connection(socket) {
//reveiced when user logout or changed
socket.on('user changed', function () {
logger.info('user changed');
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
- updateUserData(socket, notes[notename].users[socket.id]);
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
+ updateUserData(socket, notes[noteId].users[socket.id]);
emitOnlineUsers(socket);
});
//received sync of online users request
socket.on('online users', function () {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
var users = [];
- Object.keys(notes[notename].users).forEach(function (key) {
- var user = notes[notename].users[key];
+ Object.keys(notes[noteId].users).forEach(function (key) {
+ var user = notes[noteId].users[key];
if (user)
users.push(buildUserOutData(user));
});
@@ -744,55 +691,31 @@ function connection(socket) {
//received cursor focus
socket.on('cursor focus', function (data) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
- socket.broadcast.to(notename).emit('cursor focus', out);
- /*
- for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
- var sock = notes[notename].socks[i];
- if (sock != socket) {
- sock.emit('cursor focus', out);
- }
- };
- */
+ socket.broadcast.to(noteId).emit('cursor focus', out);
});
//received cursor activity
socket.on('cursor activity', function (data) {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
- socket.broadcast.to(notename).emit('cursor activity', out);
- /*
- for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
- var sock = notes[notename].socks[i];
- if (sock != socket) {
- sock.emit('cursor activity', out);
- }
- };
- */
+ socket.broadcast.to(noteId).emit('cursor activity', out);
});
//received cursor blur
socket.on('cursor blur', function () {
- var notename = getNotenameFromSocket(socket);
- if (!notename || !notes[notename]) return;
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = null;
var out = {
id: socket.id
};
- socket.broadcast.to(notename).emit('cursor blur', out);
- /*
- for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
- var sock = notes[notename].socks[i];
- if (sock != socket) {
- sock.emit('cursor blur', out);
- }
- };
- */
+ socket.broadcast.to(noteId).emit('cursor blur', out);
});
//when a new client disconnect
diff --git a/lib/response.js b/lib/response.js
index 4e5fac3b..7a75e234 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -3,7 +3,6 @@
var ejs = require('ejs');
var fs = require('fs');
var path = require('path');
-var uuid = require('node-uuid');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
var S = require('string');
@@ -13,12 +12,9 @@ var querystring = require('querystring');
var request = require('request');
//core
-var config = require("../config.js");
-
-//others
-var db = require("./db.js");
-var Note = require("./note.js");
-var User = require("./user.js");
+var config = require("./config.js");
+var logger = require("./logger.js");
+var models = require("./models");
//slides
var md = require('reveal.js/plugin/markdown/markdown');
@@ -26,10 +22,7 @@ var Mustache = require('mustache');
//reveal.js
var opts = {
- userBasePath: process.cwd(),
- revealBasePath: path.resolve(require.resolve('reveal.js'), '..', '..'),
- template: fs.readFileSync(path.join('.', '/public/views/slide', 'reveal.hbs')).toString(),
- templateListing: fs.readFileSync(path.join('.', '/public/views/slide', 'listing.hbs')).toString(),
+ template: fs.readFileSync(config.slidepath).toString(),
theme: 'css/theme/black.css',
highlightTheme: 'zenburn',
separator: '^(\r\n?|\n)---(\r\n?|\n)$',
@@ -52,7 +45,6 @@ var response = {
res.status(503).send("I'm busy right now, try again later.");
},
newNote: newNote,
- showFeatures: showFeatures,
showNote: showNote,
showPublishNote: showPublishNote,
showPublishSlide: showPublishSlide,
@@ -67,8 +59,13 @@ function responseError(res, code, detail, msg) {
'Content-Type': 'text/html'
});
var template = config.errorpath;
- var content = ejs.render(fs.readFileSync(template, 'utf8'), {
- url: config.getserverurl(),
+ var options = {
+ cache: !config.debug,
+ filename: template
+ };
+ var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+ var content = compiled({
+ url: config.serverurl,
title: code + ' ' + detail + ' ' + msg,
cache: !config.debug,
filename: template,
@@ -86,193 +83,163 @@ function showIndex(req, res, next) {
'Content-Type': 'text/html'
});
var template = config.indexpath;
- var content = ejs.render(fs.readFileSync(template, 'utf8'), {
- url: config.getserverurl(),
- useCDN: config.usecdn
+ var options = {
+ cache: !config.debug,
+ filename: template
+ };
+ var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+ var content = compiled({
+ url: config.serverurl,
+ useCDN: config.usecdn,
+ facebook: config.facebook,
+ twitter: config.twitter,
+ github: config.github,
+ dropbox: config.dropbox,
});
res.write(content);
res.end();
}
-function responseHackMD(res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- var body = LZString.decompressFromBase64(notedata.content);
- var meta = null;
- try {
- meta = metaMarked(body).meta;
- } catch(err) {
- //na
- }
- var title = Note.decodeTitle(notedata.title);
- title = Note.generateWebTitle(title);
- var template = config.hackmdpath;
- var options = {
- cache: !config.debug,
- filename: template
- };
- var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
- var html = compiled({
- url: config.getserverurl(),
- title: title,
- useCDN: config.usecdn,
- robots: (meta && meta.robots) || false //default allow robots
- });
- var buf = html;
- res.writeHead(200, {
- 'Content-Type': 'text/html; charset=UTF-8',
- 'Cache-Control': 'private',
- 'Content-Length': buf.length
- });
- res.end(buf);
+function responseHackMD(res, note) {
+ var body = LZString.decompressFromBase64(note.content);
+ var meta = null;
+ try {
+ meta = metaMarked(body).meta;
+ } catch(err) {
+ //na
+ }
+ var title = models.Note.decodeTitle(note.title);
+ title = models.Note.generateWebTitle(title);
+ var template = config.hackmdpath;
+ var options = {
+ cache: !config.debug,
+ filename: template
+ };
+ var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+ var html = compiled({
+ url: config.serverurl,
+ title: title,
+ useCDN: config.usecdn,
+ robots: (meta && meta.robots) || false, //default allow robots
+ facebook: config.facebook,
+ twitter: config.twitter,
+ github: config.github,
+ dropbox: config.dropbox,
});
+ var buf = html;
+ res.writeHead(200, {
+ 'Content-Type': 'text/html; charset=UTF-8',
+ 'Cache-Control': 'private',
+ 'Content-Length': buf.length
+ });
+ res.end(buf);
}
function newNote(req, res, next) {
- var newId = uuid.v4();
- var body = fs.readFileSync(config.defaultnotepath, 'utf8');
- body = LZString.compressToBase64(body);
var owner = null;
if (req.isAuthenticated()) {
- owner = req.user._id;
+ owner = req.user.id;
}
- db.newToDB(newId, owner, body, function (err, result) {
- if (err) {
- return response.errorInternalError(res);
- }
- Note.newNote(newId, owner, function(err, result) {
- if (err) {
- return response.errorInternalError(res);
- }
- res.redirect(config.getserverurl() + "/" + LZString.compressToBase64(newId));
- });
+ models.Note.create({
+ ownerId: owner
+ }).then(function (note) {
+ return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id));
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
}
-function showFeatures(req, res, next) {
- db.readFromDB(config.featuresnotename, function (err, data) {
- if (err) {
- var body = fs.readFileSync(config.defaultfeaturespath, 'utf8');
- body = LZString.compressToBase64(body);
- db.newToDB(config.featuresnotename, null, body, function (err, result) {
- if (err) {
- return response.errorInternalError(res);
- }
- responseHackMD(res, config.featuresnotename);
- });
- } else {
- responseHackMD(res, config.featuresnotename);
- }
- });
+function checkViewPermission(req, note) {
+ if (note.permission == 'private') {
+ if (!req.isAuthenticated() || note.ownerId != req.user.id)
+ return false;
+ else
+ return true;
+ } else {
+ return true;
+ }
}
-function showNote(req, res, next) {
- var noteId = req.params.noteId;
- if (noteId != config.featuresnotename) {
- if (!Note.checkNoteIdValid(noteId)) {
- return response.errorNotFound(res);
- }
- noteId = LZString.decompressFromBase64(noteId);
- if (!noteId) {
- return response.errorNotFound(res);
- }
- }
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- Note.findOrNewNote(noteId, notedata.owner, function (err, note) {
- if (err || !note) {
+function findNote(req, res, callback, include) {
+ var id = req.params.noteId || req.params.shortid;
+ models.Note.parseNoteId(id, function (err, _id) {
+ models.Note.findOne({
+ where: {
+ id: _id
+ },
+ include: include || null
+ }).then(function (note) {
+ if (!note) {
return response.errorNotFound(res);
}
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
+ if (!checkViewPermission(req, note)) {
+ return response.errorForbidden(res);
+ } else {
+ return callback(note);
}
- responseHackMD(res, noteId);
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
});
}
+function showNote(req, res, next) {
+ findNote(req, res, function (note) {
+ return responseHackMD(res, note);
+ });
+}
+
function showPublishNote(req, res, next) {
- var shortid = req.params.shortid;
- if (shortId.isValid(shortid)) {
- Note.findNote(shortid, function (err, note) {
- if (err || !note) {
+ var include = [{
+ model: models.User,
+ as: "owner"
+ }, {
+ model: models.User,
+ as: "lastchangeuser"
+ }];
+ findNote(req, res, function (note) {
+ note.increment('viewcount').then(function (note) {
+ if (!note) {
return response.errorNotFound(res);
}
- db.readFromDB(note.id, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
- }
- //increase note viewcount
- Note.increaseViewCount(note, function (err, note) {
- if (err || !note) {
- return response.errorNotFound(res);
- }
- var body = LZString.decompressFromBase64(notedata.content);
- var meta = null;
- try {
- meta = metaMarked(body).meta;
- } catch(err) {
- //na
- }
- var updatetime = notedata.update_time;
- var text = S(body).escapeHTML().s;
- var title = Note.decodeTitle(notedata.title);
- title = Note.generateWebTitle(title);
- var origin = config.getserverurl();
- var data = {
- title: title,
- viewcount: note.viewcount,
- updatetime: updatetime,
- url: origin,
- body: text,
- useCDN: config.usecdn,
- lastchangeuserprofile: null,
- robots: (meta && meta.robots) || false //default allow robots
- };
- if (note.lastchangeuser) {
- //find last change user profile if lastchangeuser exists
- User.findUser(note.lastchangeuser, function (err, user) {
- if (!err && user && user.profile) {
- var profile = JSON.parse(user.profile);
- if (profile) {
- data.lastchangeuserprofile = {
- name: profile.displayName || profile.username,
- photo: User.parsePhotoByProfile(profile)
- }
- renderPublish(data, res);
- }
- }
- });
- } else {
- renderPublish(data, res);
- }
-
- });
- });
+ var body = LZString.decompressFromBase64(note.content);
+ var meta = null;
+ try {
+ meta = metaMarked(body).meta;
+ } catch(err) {
+ //na
+ }
+ var createtime = note.createdAt;
+ var updatetime = note.lastchangeAt;
+ var text = S(body).escapeHTML().s;
+ var title = models.Note.decodeTitle(note.title);
+ title = models.Note.generateWebTitle(title);
+ var origin = config.serverurl;
+ var data = {
+ title: title,
+ viewcount: note.viewcount,
+ createtime: createtime,
+ updatetime: updatetime,
+ url: origin,
+ body: text,
+ useCDN: config.usecdn,
+ lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
+ robots: (meta && meta.robots) || false //default allow robots
+ };
+ return renderPublish(data, res);
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
- } else {
- return response.errorNotFound(res);
- }
+ }, include);
}
function renderPublish(data, res) {
var template = config.prettypath;
var options = {
- url: config.getserverurl(),
+ url: config.serverurl,
cache: !config.debug,
filename: template
};
@@ -287,343 +254,206 @@ function renderPublish(data, res) {
res.end(buf);
}
-function actionPublish(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var owner = data.rows[0].owner;
- Note.findOrNewNote(noteId, owner, function (err, note) {
- if (err) {
- return response.errorNotFound(res);
- }
- res.redirect(config.getserverurl() + "/s/" + note.shortid);
- });
- });
+function actionPublish(req, res, note) {
+ res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid));
}
-function actionSlide(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var owner = data.rows[0].owner;
- Note.findOrNewNote(noteId, owner, function (err, note) {
- if (err) {
- return response.errorNotFound(res);
- }
- res.redirect(config.getserverurl() + "/p/" + note.shortid);
- });
- });
+function actionSlide(req, res, note) {
+ res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid));
}
-function actionDownload(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- var body = LZString.decompressFromBase64(notedata.content);
- var title = Note.decodeTitle(notedata.title);
- var filename = title;
- filename = encodeURIComponent(filename);
- res.writeHead(200, {
- 'Access-Control-Allow-Origin': '*', //allow CORS as API
- 'Access-Control-Allow-Headers': 'Range',
- 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
- 'Content-Type': 'text/markdown; charset=UTF-8',
- 'Cache-Control': 'private',
- 'Content-disposition': 'attachment; filename=' + filename + '.md',
- 'Content-Length': body.length,
- 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
- });
- res.end(body);
+function actionDownload(req, res, note) {
+ var body = LZString.decompressFromBase64(note.content);
+ var title = models.Note.decodeTitle(note.title);
+ var filename = title;
+ filename = encodeURIComponent(filename);
+ res.writeHead(200, {
+ 'Access-Control-Allow-Origin': '*', //allow CORS as API
+ 'Access-Control-Allow-Headers': 'Range',
+ 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
+ 'Content-Type': 'text/markdown; charset=UTF-8',
+ 'Cache-Control': 'private',
+ 'Content-disposition': 'attachment; filename=' + filename + '.md',
+ 'Content-Length': body.length,
+ 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
});
+ res.end(body);
}
-function actionPDF(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- var body = LZString.decompressFromBase64(notedata.content);
- try {
- body = metaMarked(body).markdown;
- } catch(err) {
- //na
- }
- var title = Note.decodeTitle(notedata.title);
+function actionPDF(req, res, note) {
+ var body = LZString.decompressFromBase64(note.content);
+ try {
+ body = metaMarked(body).markdown;
+ } catch(err) {
+ //na
+ }
+ var title = models.Note.decodeTitle(note.title);
- if (!fs.existsSync(config.tmppath)) {
- fs.mkdirSync(config.tmppath);
- }
- var path = config.tmppath + Date.now() + '.pdf';
- markdownpdf().from.string(body).to(path, function () {
- var stream = fs.createReadStream(path);
- var filename = title;
- // Be careful of special characters
- filename = encodeURIComponent(filename);
- // Ideally this should strip them
- res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
- res.setHeader('Cache-Control', 'private');
- res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
- res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
- stream.pipe(res);
- fs.unlink(path);
- });
+ if (!fs.existsSync(config.tmppath)) {
+ fs.mkdirSync(config.tmppath);
+ }
+ var path = config.tmppath + Date.now() + '.pdf';
+ markdownpdf().from.string(body).to(path, function () {
+ var stream = fs.createReadStream(path);
+ var filename = title;
+ // Be careful of special characters
+ filename = encodeURIComponent(filename);
+ // Ideally this should strip them
+ res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
+ res.setHeader('Cache-Control', 'private');
+ res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
+ stream.pipe(res);
+ fs.unlink(path);
});
}
-function actionGist(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var owner = data.rows[0].owner;
- Note.findOrNewNote(noteId, owner, function (err, note) {
- if (err) {
- return response.errorNotFound(res);
- }
- var data = {
- client_id: config.github.clientID,
- redirect_uri: config.getserverurl() + '/auth/github/callback/' + LZString.compressToBase64(noteId) + '/gist',
- scope: "gist",
- state: shortId.generate()
- };
- var query = querystring.stringify(data);
- res.redirect("https://github.com/login/oauth/authorize?" + query);
- });
- });
+function actionGist(req, res, note) {
+ var data = {
+ client_id: config.github.clientID,
+ redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
+ scope: "gist",
+ state: shortId.generate()
+ };
+ var query = querystring.stringify(data);
+ res.redirect("https://github.com/login/oauth/authorize?" + query);
}
function noteActions(req, res, next) {
var noteId = req.params.noteId;
- if (noteId != config.featuresnotename) {
- if (!Note.checkNoteIdValid(noteId)) {
- return response.errorNotFound(res);
+ findNote(req, res, function (note) {
+ var action = req.params.action;
+ switch (action) {
+ case "publish":
+ case "pretty": //pretty deprecated
+ actionPublish(req, res, note);
+ break;
+ case "slide":
+ actionSlide(req, res, note);
+ break;
+ case "download":
+ actionDownload(req, res, note);
+ break;
+ case "pdf":
+ actionPDF(req, res, note);
+ break;
+ case "gist":
+ actionGist(req, res, note);
+ break;
+ default:
+ return res.redirect(config.serverurl + '/' + noteId);
+ break;
}
- noteId = LZString.decompressFromBase64(noteId);
- if (!noteId) {
- return response.errorNotFound(res);
- }
- }
- Note.findNote(noteId, function (err, note) {
- if (err || !note) {
- return response.errorNotFound(res);
- }
- db.readFromDB(note.id, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
- }
- var action = req.params.action;
- switch (action) {
- case "publish":
- case "pretty": //pretty deprecated
- actionPublish(req, res, noteId);
- break;
- case "slide":
- actionSlide(req, res, noteId);
- break;
- case "download":
- actionDownload(req, res, noteId);
- break;
- case "pdf":
- actionPDF(req, res, noteId);
- break;
- case "gist":
- actionGist(req, res, noteId);
- break;
- default:
- if (noteId != config.featuresnotename)
- res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
- else
- res.redirect(config.getserverurl() + '/' + noteId);
- break;
- }
- });
});
}
function publishNoteActions(req, res, next) {
- var shortid = req.params.shortid;
- if (shortId.isValid(shortid)) {
- Note.findNote(shortid, function (err, note) {
- if (err || !note) {
- return response.errorNotFound(res);
- }
- db.readFromDB(note.id, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
- }
- var action = req.params.action;
- switch (action) {
- case "edit":
- if (note.id != config.featuresnotename)
- res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(note.id));
- else
- res.redirect(config.getserverurl() + '/' + note.id);
- break;
- }
- });
- });
- }
+ findNote(req, res, function (note) {
+ var action = req.params.action;
+ switch (action) {
+ case "edit":
+ res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
+ break;
+ default:
+ res.redirect(config.serverurl + '/s/' + note.shortid);
+ break;
+ }
+ });
}
function githubActions(req, res, next) {
var noteId = req.params.noteId;
- if (noteId != config.featuresnotename) {
- if (!Note.checkNoteIdValid(noteId)) {
- return response.errorNotFound(res);
- }
- noteId = LZString.decompressFromBase64(noteId);
- if (!noteId) {
- return response.errorNotFound(res);
+ findNote(req, res, function (note) {
+ var action = req.params.action;
+ switch (action) {
+ case "gist":
+ githubActionGist(req, res, note);
+ break;
+ default:
+ res.redirect(config.serverurl + '/' + noteId);
+ break;
}
- }
- Note.findNote(noteId, function (err, note) {
- if (err || !note) {
- return response.errorNotFound(res);
- }
- db.readFromDB(note.id, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
- }
- var action = req.params.action;
- switch (action) {
- case "gist":
- githubActionGist(req, res, noteId);
- break;
- default:
- if (noteId != config.featuresnotename)
- res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
- else
- res.redirect(config.getserverurl() + '/' + noteId);
- break;
- }
- });
});
}
-function githubActionGist(req, res, noteId) {
- db.readFromDB(noteId, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
+function githubActionGist(req, res, note) {
+ var code = req.query.code;
+ var state = req.query.state;
+ if (!code || !state) {
+ return response.errorForbidden(res);
+ } else {
+ var data = {
+ client_id: config.github.clientID,
+ client_secret: config.github.clientSecret,
+ code: code,
+ state: state
}
- var notedata = data.rows[0];
- var code = req.query.code;
- var state = req.query.state;
- if (!code || !state) {
- return response.errorForbidden(res);
- } else {
- var data = {
- client_id: config.github.clientID,
- client_secret: config.github.clientSecret,
- code: code,
- state: state
- }
- var auth_url = 'https://github.com/login/oauth/access_token';
- request({
- url: auth_url,
- method: "POST",
- json: data
- }, function (error, httpResponse, body) {
- if (!error && httpResponse.statusCode == 200) {
- var access_token = body.access_token;
- if (access_token) {
- var content = LZString.decompressFromBase64(notedata.content);
- var title = Note.decodeTitle(notedata.title);
- var filename = title.replace('/', ' ') + '.md';
- var gist = {
- "files": {}
- };
- gist.files[filename] = {
- "content": content
- };
- var gist_url = "https://api.github.com/gists";
- request({
- url: gist_url,
- headers: {
- 'User-Agent': 'HackMD',
- 'Authorization': 'token ' + access_token
- },
- method: "POST",
- json: gist
- }, function (error, httpResponse, body) {
- if (!error && httpResponse.statusCode == 201) {
- res.setHeader('referer', '');
- res.redirect(body.html_url);
- } else {
- return response.errorForbidden(res);
- }
- });
- } else {
- return response.errorForbidden(res);
- }
+ var auth_url = 'https://github.com/login/oauth/access_token';
+ request({
+ url: auth_url,
+ method: "POST",
+ json: data
+ }, function (error, httpResponse, body) {
+ if (!error && httpResponse.statusCode == 200) {
+ var access_token = body.access_token;
+ if (access_token) {
+ var content = LZString.decompressFromBase64(note.content);
+ var title = models.Note.decodeTitle(note.title);
+ var filename = title.replace('/', ' ') + '.md';
+ var gist = {
+ "files": {}
+ };
+ gist.files[filename] = {
+ "content": content
+ };
+ var gist_url = "https://api.github.com/gists";
+ request({
+ url: gist_url,
+ headers: {
+ 'User-Agent': 'HackMD',
+ 'Authorization': 'token ' + access_token
+ },
+ method: "POST",
+ json: gist
+ }, function (error, httpResponse, body) {
+ if (!error && httpResponse.statusCode == 201) {
+ res.setHeader('referer', '');
+ res.redirect(body.html_url);
+ } else {
+ return response.errorForbidden(res);
+ }
+ });
} else {
return response.errorForbidden(res);
}
- })
- }
- });
+ } else {
+ return response.errorForbidden(res);
+ }
+ })
+ }
}
function showPublishSlide(req, res, next) {
- var shortid = req.params.shortid;
- if (shortId.isValid(shortid)) {
- Note.findNote(shortid, function (err, note) {
- if (err || !note) {
+ findNote(req, res, function (note) {
+ note.increment('viewcount').then(function (note) {
+ if (!note) {
return response.errorNotFound(res);
}
- db.readFromDB(note.id, function (err, data) {
- if (err) {
- return response.errorNotFound(res);
- }
- var notedata = data.rows[0];
- //check view permission
- if (note.permission == 'private') {
- if (!req.isAuthenticated() || notedata.owner != req.user._id)
- return response.errorForbidden(res);
- }
- //increase note viewcount
- Note.increaseViewCount(note, function (err, note) {
- if (err || !note) {
- return response.errorNotFound(res);
- }
- var body = LZString.decompressFromBase64(notedata.content);
- try {
- body = metaMarked(body).markdown;
- } catch(err) {
- //na
- }
- var title = Note.decodeTitle(notedata.title);
- title = Note.generateWebTitle(title);
- var text = S(body).escapeHTML().s;
- render(res, title, text);
- });
- });
+ var body = LZString.decompressFromBase64(note.content);
+ try {
+ body = metaMarked(body).markdown;
+ } catch(err) {
+ //na
+ }
+ var title = models.Note.decodeTitle(note.title);
+ title = models.Note.generateWebTitle(title);
+ var text = S(body).escapeHTML().s;
+ render(res, title, text);
+ }).catch(function (err) {
+ logger.error(err);
+ return response.errorInternalError(res);
});
- } else {
- return response.errorNotFound(res);
- }
+ });
}
//reveal.js render
@@ -631,7 +461,7 @@ var render = function (res, title, markdown) {
var slides = md.slidify(markdown, opts);
res.end(Mustache.to_html(opts.template, {
- url: config.getserverurl(),
+ url: config.serverurl,
title: title,
theme: opts.theme,
highlightTheme: opts.highlightTheme,
diff --git a/lib/temp.js b/lib/temp.js
deleted file mode 100644
index b635644d..00000000
--- a/lib/temp.js
+++ /dev/null
@@ -1,84 +0,0 @@
-//temp
-//external modules
-var mongoose = require('mongoose');
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-// create a temp model
-var model = mongoose.model('temp', {
- id: String,
- data: String,
- created: Date
-});
-
-//public
-var temp = {
- model: model,
- findTemp: findTemp,
- newTemp: newTemp,
- removeTemp: removeTemp,
- getTempCount: getTempCount
-};
-
-function getTempCount(callback) {
- model.count(function(err, count){
- if(err) callback(err, null);
- else callback(null, count);
- });
-}
-
-function findTemp(id, callback) {
- model.findOne({
- id: id
- }, function (err, temp) {
- if (err) {
- logger.error('find temp failed: ' + err);
- callback(err, null);
- }
- if (!err && temp) {
- callback(null, temp);
- } else {
- logger.error('find temp failed: ' + err);
- callback(err, null);
- };
- });
-}
-
-function newTemp(id, data, callback) {
- var temp = new model({
- id: id,
- data: data,
- created: Date.now()
- });
- temp.save(function (err) {
- if (err) {
- logger.error('new temp failed: ' + err);
- callback(err, null);
- } else {
- logger.info("new temp success: " + temp.id);
- callback(null, temp);
- };
- });
-}
-
-function removeTemp(id, callback) {
- findTemp(id, function(err, temp) {
- if(!err && temp) {
- temp.remove(function(err) {
- if(err) {
- logger.error('remove temp failed: ' + err);
- callback(err, null);
- } else {
- callback(null, null);
- }
- });
- } else {
- logger.error('remove temp failed: ' + err);
- callback(err, null);
- }
- });
-}
-
-module.exports = temp; \ No newline at end of file
diff --git a/lib/user.js b/lib/user.js
deleted file mode 100644
index 639d66c3..00000000
--- a/lib/user.js
+++ /dev/null
@@ -1,110 +0,0 @@
-//user
-//external modules
-var mongoose = require('mongoose');
-var md5 = require("md5");
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-// create a user model
-var model = mongoose.model('user', {
- id: String,
- profile: String,
- history: String,
- created: Date
-});
-
-//public
-var user = {
- model: model,
- findUser: findUser,
- newUser: newUser,
- findOrNewUser: findOrNewUser,
- getUserCount: getUserCount,
- parsePhotoByProfile: parsePhotoByProfile
-};
-
-function parsePhotoByProfile(profile) {
- var photo = null;
- switch (profile.provider) {
- case "facebook":
- photo = 'https://graph.facebook.com/' + profile.id + '/picture';
- break;
- case "twitter":
- photo = profile.photos[0].value;
- break;
- case "github":
- photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
- break;
- case "dropbox":
- //no image api provided, use gravatar
- photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
- break;
- }
- return photo;
-}
-
-function getUserCount(callback) {
- model.count(function(err, count){
- if(err) callback(err, null);
- else callback(null, count);
- });
-}
-
-function findUser(id, callback) {
- var rule = {};
- var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
- if (checkForHexRegExp.test(id))
- rule._id = id;
- else
- rule.id = id;
- model.findOne(rule, function (err, user) {
- if (err) {
- logger.error('find user failed: ' + err);
- callback(err, null);
- }
- if (!err && user) {
- callback(null, user);
- } else {
- logger.error('find user failed: ' + err);
- callback(err, null);
- };
- });
-}
-
-function newUser(id, profile, callback) {
- var user = new model({
- id: id,
- profile: JSON.stringify(profile),
- created: Date.now()
- });
- user.save(function (err) {
- if (err) {
- logger.error('new user failed: ' + err);
- callback(err, null);
- } else {
- logger.info("new user success: " + user.id);
- callback(null, user);
- };
- });
-}
-
-function findOrNewUser(id, profile, callback) {
- findUser(id, function(err, user) {
- if(err || !user) {
- newUser(id, profile, function(err, user) {
- if(err) {
- logger.error('find or new user failed: ' + err);
- callback(err, null);
- } else {
- callback(null, user);
- }
- });
- } else {
- callback(null, user);
- }
- });
-}
-
-module.exports = user; \ No newline at end of file
diff --git a/package.json b/package.json
index 9619c1f7..2988c37d 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"chance": "^1.0.1",
"cheerio": "^0.20.0",
"compression": "^1.6.1",
- "connect-mongo": "^1.1.0",
+ "connect-session-sequelize": "^3.0.0",
"cookie": "0.2.3",
"cookie-parser": "1.4.1",
"ejs": "^2.4.1",
@@ -25,16 +25,15 @@
"highlight.js": "^9.2.0",
"imgur": "^0.1.7",
"jsdom-nogyp": "^0.8.3",
- "kerberos": "0.0.19",
"lz-string": "1.4.4",
"markdown-pdf": "^7.0.0",
"marked": "^0.3.5",
"meta-marked": "^0.4.0",
"method-override": "^2.3.5",
"moment": "^2.12.0",
- "mongoose": "^4.4.7",
"morgan": "^1.7.0",
"mustache": "2.2.1",
+ "mysql": "^2.10.2",
"node-uuid": "^1.4.7",
"passport": "^0.3.2",
"passport-dropbox-oauth2": "^1.0.0",
@@ -42,13 +41,17 @@
"passport-github": "^1.1.0",
"passport-twitter": "^1.0.4",
"passport.socketio": "^3.6.1",
- "pg": "4.x",
"randomcolor": "^0.4.3",
"request": "^2.69.0",
+ "pg": "^4.5.3",
+ "pg-hstore": "^2.3.2",
"reveal.js": "3.2.0",
"shortid": "2.2.4",
+ "sequelize": "^3.21.0",
"socket.io": "1.4.5",
+ "sqlite3": "^3.1.3",
"string": "^3.3.1",
+ "tedious": "^1.14.0",
"toobusy-js": "^0.4.3",
"winston": "^2.2.0"
},
diff --git a/public/features.md b/public/docs/features.md
index dde852f0..a734f3c2 100644
--- a/public/features.md
+++ b/public/docs/features.md
@@ -3,7 +3,7 @@ Features
Introduction
===
-<i class="fa fa-file-text"></i> HackMD is a realtime collaborate markdown note in all platforms.
+<i class="fa fa-file-text"></i> **HackMD** is a realtime collaborate markdown note in all platforms.
This mean you can do some notes with any other in **Desktop, Tablet or even Phone**.
You can Sign in via **Facebook, Twitter, GitHub, Dropbox** in the **[homepage](/)**.
@@ -37,11 +37,11 @@ If you want to share a **editable** note, just copy the url.
If you want to share a **read-only** note, simply press share button <i class="fa fa-share-alt"></i> and copy the url.
## Save
-Currently, you can save to **dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local.
+Currently, you can save to **Dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local.
## Import
-Like save feature, you can also import **.md** from **dropbox** <i class="fa fa-dropbox"></i>.
-Or import from your **clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley:
+Like save feature, you can also import **.md** from **Dropbox** <i class="fa fa-dropbox"></i>.
+Or import from your **Clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley:
## Permission
There is a little button on the top right of the view.
@@ -60,6 +60,11 @@ It might be one of below:
<iframe width="100%" height="500" src="http://hackmd.io/features" frameborder="0"></iframe>
```
+## [Slide Mode](./slide-example)
+You can use some syntax to divide your note into slides.
+Then use **Slide Mode** <i class="fa fa-tv"></i> to made a presentation.
+Visit above link for detail.
+
View
===
## Table of content
@@ -93,12 +98,13 @@ This will take the first **level 1 header** as the note title.
Using tags like below, these will show in your **history**.
###### tags: `features` `cool` `updated`
-## [YAML metadata](https://hackmd.io/IwFgZgxiBsBGCsBaAnPYAORJm07gDMImGAKYnrwDsUI8QA==)
+## [YAML metadata](./yaml-metadata)
Provide advanced note information to set the browse behavior, visit above link for detail
-- robots: set search engine to index or not
+- robots: set web robots meta
- lang: set browse language
- dir: set text direction
-- breaks: set to use line breaks
+- breaks: set to use line breaks or not
+- mathjax: set to parse mathjax or not
## Emoji
You can type any emoji like this :smile: :smiley: :cry: :wink:
@@ -241,9 +247,41 @@ digraph hierarchy {
}
```
+### Mermaid
+```mermaid
+gantt
+ title A Gantt Diagram
+
+ section Section
+ A task :a1, 2014-01-01, 30d
+ Another task :after a1 , 20d
+ section Another
+ Task in sec :2014-01-12 , 12d
+ anther task : 24d
+```
+
> More information about **Sequence diagrams** syntax [here](http://bramp.github.io/js-sequence-diagrams/).
> More information about **Flow charts** syntax [here](http://adrai.github.io/flowchart.js/).
> More information about **Graphviz** syntax [here](http://www.tonyballantyne.com/graphs.html)
+> More information about **Mermaid** syntax [here](http://knsv.github.io/mermaid)
+
+Alert area
+---
+:::success
+Yes :tada:
+:::
+
+:::info
+This is a message :mega:
+:::
+
+:::warning
+Watch out :zap:
+:::
+
+:::danger
+Oh No :fire:
+:::
## Typography
diff --git a/public/docs/slide-example.md b/public/docs/slide-example.md
new file mode 100644
index 00000000..3d9a70c4
--- /dev/null
+++ b/public/docs/slide-example.md
@@ -0,0 +1,81 @@
+Slide example
+===
+This feature still in beta, may have some issues.
+
+For details:
+https://github.com/hakimel/reveal.js/
+
+---
+
+## First slide
+
+`---`
+
+Is the divder of slides
+
+----
+
+### First branch of fisrt slide
+
+`----`
+
+Is the divder of branches
+
+----
+
+### Second branch of first slide
+
+`<!-- .element: class="fragment" data-fragment-index="1" -->`
+
+Is the fragment syntax
+
+- Item 1<!-- .element: class="fragment" data-fragment-index="1" -->
+- Item 2<!-- .element: class="fragment" data-fragment-index="2" -->
+
+---
+
+## Second slide
+
+<!-- .slide: data-background="#1A237E" -->
+
+`<!-- .slide: data-background="#1A237E" -->`
+
+Is the background syntax
+
+---
+
+<!-- .slide: data-transition="zoom" -->
+
+`<!-- .slide: data-transition="zoom" -->`
+
+Is the transition syntax
+
+you can use:
+none/fade/slide/convex/concave/zoom
+
+---
+
+<!-- .slide: data-transition="fade-in convex-out" -->
+
+`<!-- .slide: data-transition="fade-in convex-out" -->`
+
+Also can set different in/out transition
+
+you can use:
+none/fade/slide/convex/concave/zoom
+postfix with `-in` or `-out`
+
+---
+
+<!-- .slide: data-transition-speed="fast" -->
+
+`<!-- .slide: data-transition-speed="fast" -->`
+
+Custom the transition speed!
+
+you can use:
+default/fast/slow
+
+---
+
+# The End \ No newline at end of file
diff --git a/public/docs/yaml-metadata.md b/public/docs/yaml-metadata.md
new file mode 100644
index 00000000..5fc6e1b8
--- /dev/null
+++ b/public/docs/yaml-metadata.md
@@ -0,0 +1,97 @@
+---
+robots: index, follow
+lang: en
+dir: ltr
+breaks: true
+---
+
+Supported YAML metadata
+===
+
+First you need to insert syntax like this at the **start** of the note:
+```
+---
+YAML metas
+---
+```
+
+Replace the "YAML metas" in this section with any YAML options as below.
+You can also refer to this note's source code.
+
+robots
+---
+This option will give below meta in the note head meta:
+```xml
+<meta name="robots" content="your_meta">
+```
+So you can prevent any search engine index your note by set `noindex, nofollow`.
+
+> default: not
+
+**Example**
+```xml
+robots: noindex, nofollow
+```
+
+lang
+---
+This option will set the language of the note, that might alter some typography of it.
+You can find your the language code in ISO 639-1 standard:
+https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+
+> default: not set (which will be en)
+
+**Example**
+```xml
+langs: ja-jp
+```
+
+dir
+---
+This option provide to describe the direction of the text in this note.
+You can only use whether `rtl` or `ltr`.
+Look more at here:
+http://www.w3.org/International/questions/qa-html-dir
+
+> default: not set (which will be ltr)
+
+**Example**
+```xml
+dir: rtl
+```
+
+breaks
+---
+This option means the hardbreaks in the note will be parsed or be ignore.
+The original markdown syntax breaks only if you put space twice, but HackMD choose to breaks every time you enter a break.
+You can only use whether `true` or `false`.
+
+> default: not set (which will be true)
+
+**Example**
+```xml
+breaks: false
+```
+
+mathjax
+---
+This option let you to choose to parse mathjax syntax or not.
+
+> default: not set (which will be true)
+
+**Example**
+```xml
+mathjax: false
+```
+
+spellcheck
+---
+**Warning: Experimental feature!**
+This option let you to choose to enable spell checking feature or not.
+
+> default: not set (which will be false)
+
+**Example**
+```xml
+spellcheck: true
+``` \ No newline at end of file
diff --git a/public/index.ejs b/public/index.ejs
deleted file mode 100644
index 87e86f2b..00000000
--- a/public/index.ejs
+++ /dev/null
@@ -1,237 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
- <meta name="apple-mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-status-bar-style" content="black">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="description" content="Realtime collaborative markdown notes on all platforms.">
- <meta name="author" content="jackycute">
- <title>HackMD - Collaborative notes</title>
- <link rel="icon" type="image/png" href="<%- url %>/favicon.png">
- <link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
-
- <!-- Bootstrap core CSS -->
- <% if(useCDN) { %>
- <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
- <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
- <% } else { %>
- <link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
- <link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
- <link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
- <% } %>
- <link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
- <link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
- <!-- Custom styles for this template -->
- <link rel="stylesheet" href="<%- url %>/css/cover.css">
- <link rel="stylesheet" href="<%- url %>/css/site.css">
-</head>
-
-<body>
- <div class="site-wrapper">
- <div class="site-wrapper-inner">
- <div class="cover-container">
-
- <div class="masthead clearfix">
- <div class="inner">
- <h3 class="masthead-brand"></h3>
- <nav>
- <ul class="nav masthead-nav">
- <li class="ui-home active"><a href="#">Home</a>
- </li>
- <li class="ui-history"><a href="#">History</a>
- </li>
- <li class="ui-releasenotes"><a href="#">Release Notes</a>
- </li>
- </ul>
- </nav>
- </div>
- </div>
-
- <div id="home" class="section">
- <div class="inner cover">
- <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
- <p class="lead">
- Realtime collaborative markdown notes on all platforms.
- </p>
- <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
- <div class="ui-or" style="display:none;">Or</div>
- <p class="lead">
- <a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
- </p>
- <h5>Share directly with URL <i class="fa fa-link"></i></h5>
- <a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
- </div>
- <br>
- </div>
-
- <div id="history" class="section" style="display:none;">
- <div class="ui-signin">
- <h4>
- <a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
- </h4>
- <p>Below are history from browser</p>
- </div>
- <div class="ui-signout" style="display:none;">
- <h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
- <a href="<%- url %>/new" class="btn btn-default">New note</a> Or
- <a href="#" class="btn btn-danger ui-logout">Sign Out</a>
- </div>
- <hr>
- <form class="form-inline">
- <div class="form-group" style="vertical-align: bottom;">
- <input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
- </div>
- <div class="form-group">
- <input class="search form-control" placeholder="Search anything..." />
- </div>
- <a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
- Title
- </a>
- <a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
- Time
- </a>
- <span class="hidden-xs hidden-sm">
- <a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
- <span class="btn btn-default btn-file ui-open-history" title="Import history">
- <i class="fa fa-folder-open-o"></i><input type="file" />
- </span>
- <a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
- </span>
- <a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
- </form>
- <h4 class="ui-nohistory" style="display:none;">
- No history
- </h4>
- <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
- <ul id="history-list" class="list">
- </ul>
- </div>
- <div id="releasenotes" class="section" style="display:none;">
- <div id="template" style="display:none;">
- {{#each release}}
- <div class="inner cover">
- <h5 class="cover-heading">
- <div class="text-left">
- <i class="fa fa-tag"></i> {{version}}
- &nbsp;<span class="label label-default">{{tag}}</span>
- <div class="pull-right">
- <i class="fa fa-clock-o"></i> {{date}}
- </div>
- </div>
- </h5>
- <hr>{{#each detail}}
- <div class="text-left">
- <h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
- <ul>
- {{#each item}}
- <li>
- {{this}}
- </li>
- {{/each}}
- </ul>
- </div>
- {{/each}}
- </div>
- {{#unless @last}}
- <br>{{/unless}} {{/each}}
- </div>
- </div>
-
- <div class="mastfoot">
- <div class="inner">
- <h6>
- <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
- </h6>
- <p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
- </p>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- signin modal -->
- <div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-sm">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
- </button>
- <h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
- </div>
- <div class="modal-body">
- <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
- <i class="fa fa-facebook"></i> Sign in via Facebook
- </a>
- <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
- <i class="fa fa-twitter"></i> Sign in via Twitter
- </a>
- <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
- <i class="fa fa-github"></i> Sign in via GitHub
- </a>
- <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
- <i class="fa fa-dropbox"></i> Sign in via Dropbox
- </a>
- </div>
- </div>
- </div>
- </div>
- <!-- delete modal -->
- <div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-sm">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
- </button>
- <h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
- </div>
- <div class="modal-body" style="color:black;">
- <h5 class="ui-delete-modal-msg"></h5>
- <strong class="ui-delete-modal-item"></strong>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
- <button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
- </div>
- </div>
- </div>
- </div>
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <% if(useCDN) { %>
- <script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
- <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment-with-locales.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
- <script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
- <% } else { %>
- <script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
- <script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
- <script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
- <script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
- <script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
- <script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
- <script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
- <script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
- <% } %>
- <script src="<%- url %>/vendor/js.cookie.js" defer></script>
- <script src="<%- url %>/vendor/list.min.js" defer></script>
- <script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
- <script src="<%- url %>/vendor/store.min.js" defer></script>
- <script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
- <script src="<%- url %>/js/common.js" defer></script>
- <script src="<%- url %>/js/history.js" defer></script>
- <script src="<%- url %>/js/cover.js" defer></script>
-</body>
-
-</html> \ No newline at end of file
diff --git a/public/js/extra.js b/public/js/extra.js
index 41b984dc..be454ed7 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -1,15 +1,24 @@
//auto update last change
+var createtime = null;
var lastchangetime = null;
var lastchangeui = {
+ status: $(".ui-status-lastchange"),
time: $(".ui-lastchange"),
user: $(".ui-lastchangeuser"),
nouser: $(".ui-no-lastchangeuser")
}
function updateLastChange() {
- if (lastchangetime && lastchangeui) {
- lastchangeui.time.html(moment(lastchangetime).fromNow());
- lastchangeui.time.attr('title', moment(lastchangetime).format('llll'));
+ if (!lastchangeui) return;
+ if (createtime) {
+ if (createtime && !lastchangetime) {
+ lastchangeui.status.text('created');
+ } else {
+ lastchangeui.status.text('changed');
+ }
+ var time = lastchangetime || createtime;
+ lastchangeui.time.html(moment(time).fromNow());
+ lastchangeui.time.attr('title', moment(time).format('llll'));
}
}
setInterval(updateLastChange, 60000);
diff --git a/public/js/history.js b/public/js/history.js
index edecde1d..b3bae980 100644
--- a/public/js/history.js
+++ b/public/js/history.js
@@ -93,8 +93,14 @@ function clearDuplicatedHistory(notehistory) {
for (var i = 0; i < notehistory.length; i++) {
var found = false;
for (var j = 0; j < newnotehistory.length; j++) {
- var id = LZString.decompressFromBase64(notehistory[i].id);
- var newId = LZString.decompressFromBase64(newnotehistory[j].id);
+ var id = notehistory[i].id;
+ var newId = newnotehistory[j].id;
+ try {
+ id = LZString.decompressFromBase64(id);
+ newId = LZString.decompressFromBase64(newId);
+ } catch (err) {
+ // na
+ }
if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
var time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a');
var newTime = moment(newnotehistory[j].time, 'MMMM Do YYYY, h:mm:ss a');
diff --git a/public/js/index.js b/public/js/index.js
index 6cbaf066..0e4fd21a 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1423,7 +1423,7 @@ function updatePermission(newPermission) {
title = "Only owner can view & edit";
break;
}
- if (personalInfo.userid == owner) {
+ if (personalInfo.userid && personalInfo.userid == owner) {
label += ' <i class="fa fa-caret-down"></i>';
ui.infobar.permission.label.removeClass('disabled');
} else {
@@ -1476,11 +1476,14 @@ socket.emit = function () {
socket.on('info', function (data) {
console.error(data);
switch (data.code) {
+ case 403:
+ location.href = "./403";
+ break;
case 404:
location.href = "./404";
break;
- case 403:
- location.href = "./403";
+ case 500:
+ location.href = "./500";
break;
}
});
@@ -1517,11 +1520,15 @@ socket.on('version', function (data) {
});
function updateLastInfo(data) {
//console.log(data);
- if (lastchangetime !== data.updatetime) {
+ if (data.hasOwnProperty('createtime') && createtime !== data.createtime) {
+ createtime = data.createtime;
+ updateLastChange();
+ }
+ if (data.hasOwnProperty('updatetime') && lastchangetime !== data.updatetime) {
lastchangetime = data.updatetime;
updateLastChange();
}
- if (lastchangeuser !== data.lastchangeuser) {
+ if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) {
lastchangeuser = data.lastchangeuser;
lastchangeuserprofile = data.lastchangeuserprofile;
updateLastChangeUser();
diff --git a/public/js/pretty.js b/public/js/pretty.js
index 44d27e54..2d1f27de 100644
--- a/public/js/pretty.js
+++ b/public/js/pretty.js
@@ -20,7 +20,8 @@ renderTOC(markdown);
generateToc('toc');
generateToc('toc-affix');
smoothHashScroll();
-lastchangetime = lastchangeui.time.text();
+createtime = lastchangeui.time.attr('data-createtime');
+lastchangetime = lastchangeui.time.attr('data-updatetime');
updateLastChange();
var url = window.location.pathname;
$('.ui-edit').attr('href', url + '/edit');
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 54562ea6..044f7e6d 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -8,7 +8,7 @@
<span>
<span class="ui-lastchangeuser" style="display: none;">&thinsp;<i class="ui-user-icon small" data-toggle="tooltip" data-placement="right"></i></span>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
- &nbsp;<span class="text-uppercase">changed</span>
+ &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
<span class="ui-lastchange text-uppercase"></span>
</span>
<span class="ui-permission dropdown pull-right">
@@ -73,32 +73,6 @@
</div>
</div>
</div>
-<!-- signin modal -->
-<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-sm">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
- </button>
- <h4 class="modal-title" id="mySmallModalLabel">Please sign in to edit</h4>
- </div>
- <div class="modal-body">
- <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
- <i class="fa fa-facebook"></i> Sign in via Facebook
- </a>
- <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
- <i class="fa fa-twitter"></i> Sign in via Twitter
- </a>
- <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
- <i class="fa fa-github"></i> Sign in via GitHub
- </a>
- <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
- <i class="fa fa-dropbox"></i> Sign in via Dropbox
- </a>
- </div>
- </div>
- </div>
-</div>
<!-- locked modal -->
<div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
diff --git a/public/views/hackmd.ejs b/public/views/hackmd.ejs
new file mode 100644
index 00000000..c5778fc9
--- /dev/null
+++ b/public/views/hackmd.ejs
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <%- include head %>
+</head>
+
+<body>
+ <%- include header %>
+ <%- include body %>
+ <%- include footer %>
+ <%- include foot %>
+</body>
+
+</html> \ No newline at end of file
diff --git a/public/views/header.ejs b/public/views/header.ejs
index 410886d1..bf8f9f66 100644
--- a/public/views/header.ejs
+++ b/public/views/header.ejs
@@ -38,8 +38,10 @@
</li>
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
</li>
+ <% if(typeof github !== 'undefined' && github) { %>
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
</li>
+ <% } %>
<li class="divider"></li>
<li class="dropdown-header">Import</li>
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
@@ -119,8 +121,10 @@
</li>
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
</li>
+ <% if(typeof github !== 'undefined' && github) { %>
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
- </li>
+ </li>
+ <% } %>
<li class="divider"></li>
<li class="dropdown-header">Import</li>
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
diff --git a/public/views/index.ejs b/public/views/index.ejs
index c5778fc9..85d955b7 100644
--- a/public/views/index.ejs
+++ b/public/views/index.ejs
@@ -2,14 +2,210 @@
<html lang="en">
<head>
- <%- include head %>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
+ <meta name="mobile-web-app-capable" content="yes">
+ <meta name="description" content="Realtime collaborative markdown notes on all platforms.">
+ <meta name="author" content="jackycute">
+ <title>HackMD - Collaborative notes</title>
+ <link rel="icon" type="image/png" href="<%- url %>/favicon.png">
+ <link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
+
+ <!-- Bootstrap core CSS -->
+ <% if(useCDN) { %>
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
+ <% } else { %>
+ <link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
+ <link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
+ <link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
+ <% } %>
+ <link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
+ <link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
+ <!-- Custom styles for this template -->
+ <link rel="stylesheet" href="<%- url %>/css/cover.css">
+ <link rel="stylesheet" href="<%- url %>/css/site.css">
</head>
<body>
- <%- include header %>
- <%- include body %>
- <%- include footer %>
- <%- include foot %>
+ <div class="site-wrapper">
+ <div class="site-wrapper-inner">
+ <div class="cover-container">
+
+ <div class="masthead clearfix">
+ <div class="inner">
+ <h3 class="masthead-brand"></h3>
+ <nav>
+ <ul class="nav masthead-nav">
+ <li class="ui-home active"><a href="#">Home</a>
+ </li>
+ <li class="ui-history"><a href="#">History</a>
+ </li>
+ <li class="ui-releasenotes"><a href="#">Release Notes</a>
+ </li>
+ </ul>
+ </nav>
+ </div>
+ </div>
+
+ <div id="home" class="section">
+ <div class="inner cover">
+ <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
+ <p class="lead">
+ Realtime collaborative markdown notes on all platforms.
+ </p>
+ <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
+ <div class="ui-or" style="display:none;">Or</div>
+ <p class="lead">
+ <a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
+ </p>
+ <h5>Share directly with URL <i class="fa fa-link"></i></h5>
+ <a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
+ </div>
+ <br>
+ </div>
+
+ <div id="history" class="section" style="display:none;">
+ <div class="ui-signin">
+ <h4>
+ <a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
+ </h4>
+ <p>Below are history from browser</p>
+ </div>
+ <div class="ui-signout" style="display:none;">
+ <h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
+ <a href="<%- url %>/new" class="btn btn-default">New note</a> Or
+ <a href="#" class="btn btn-danger ui-logout">Sign Out</a>
+ </div>
+ <hr>
+ <form class="form-inline">
+ <div class="form-group" style="vertical-align: bottom;">
+ <input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
+ </div>
+ <div class="form-group">
+ <input class="search form-control" placeholder="Search anything..." />
+ </div>
+ <a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
+ Title
+ </a>
+ <a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
+ Time
+ </a>
+ <span class="hidden-xs hidden-sm">
+ <a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
+ <span class="btn btn-default btn-file ui-open-history" title="Import history">
+ <i class="fa fa-folder-open-o"></i><input type="file" />
+ </span>
+ <a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
+ </span>
+ <a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
+ </form>
+ <h4 class="ui-nohistory" style="display:none;">
+ No history
+ </h4>
+ <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
+ <ul id="history-list" class="list">
+ </ul>
+ </div>
+ <div id="releasenotes" class="section" style="display:none;">
+ <div id="template" style="display:none;">
+ {{#each release}}
+ <div class="inner cover">
+ <h5 class="cover-heading">
+ <div class="text-left">
+ <i class="fa fa-tag"></i> {{version}}
+ &nbsp;<span class="label label-default">{{tag}}</span>
+ <div class="pull-right">
+ <i class="fa fa-clock-o"></i> {{date}}
+ </div>
+ </div>
+ </h5>
+ <hr>{{#each detail}}
+ <div class="text-left">
+ <h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
+ <ul>
+ {{#each item}}
+ <li>
+ {{this}}
+ </li>
+ {{/each}}
+ </ul>
+ </div>
+ {{/each}}
+ </div>
+ {{#unless @last}}
+ <br>{{/unless}} {{/each}}
+ </div>
+ </div>
+
+ <div class="mastfoot">
+ <div class="inner">
+ <h6>
+ <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
+ </h6>
+ <p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- delete modal -->
+ <div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-dialog modal-sm">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+ </button>
+ <h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
+ </div>
+ <div class="modal-body" style="color:black;">
+ <h5 class="ui-delete-modal-msg"></h5>
+ <strong class="ui-delete-modal-item"></strong>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <%- include modal %>
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <% if(useCDN) { %>
+ <script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment-with-locales.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
+ <% } else { %>
+ <script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
+ <script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
+ <script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
+ <script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
+ <script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
+ <script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
+ <script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
+ <script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
+ <% } %>
+ <script src="<%- url %>/vendor/js.cookie.js" defer></script>
+ <script src="<%- url %>/vendor/list.min.js" defer></script>
+ <script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
+ <script src="<%- url %>/vendor/store.min.js" defer></script>
+ <script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
+ <script src="<%- url %>/js/common.js" defer></script>
+ <script src="<%- url %>/js/history.js" defer></script>
+ <script src="<%- url %>/js/cover.js" defer></script>
</body>
</html> \ No newline at end of file
diff --git a/public/views/modal.ejs b/public/views/modal.ejs
new file mode 100644
index 00000000..260ff423
--- /dev/null
+++ b/public/views/modal.ejs
@@ -0,0 +1,34 @@
+<!-- signin modal -->
+<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
+ <div class="modal-dialog modal-sm">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+ </button>
+ <h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
+ </div>
+ <div class="modal-body">
+ <% if(facebook) { %>
+ <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
+ <i class="fa fa-facebook"></i> Sign in via Facebook
+ </a>
+ <% } %>
+ <% if(twitter) { %>
+ <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
+ <i class="fa fa-twitter"></i> Sign in via Twitter
+ </a>
+ <% } %>
+ <% if(github) { %>
+ <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
+ <i class="fa fa-github"></i> Sign in via GitHub
+ </a>
+ <% } %>
+ <% if(dropbox) { %>
+ <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
+ <i class="fa fa-dropbox"></i> Sign in via Dropbox
+ </a>
+ <% } %>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/public/views/pretty.ejs b/public/views/pretty.ejs
index ed6e638f..c8e959f4 100644
--- a/public/views/pretty.ejs
+++ b/public/views/pretty.ejs
@@ -49,8 +49,8 @@
<% } else { %>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
<% } %>
- &nbsp;<span class="text-uppercase">changed</span>
- <span class="ui-lastchange text-uppercase"><%- updatetime %></span>
+ &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
+ <span class="ui-lastchange text-uppercase" data-createtime="<%- createtime %>" data-updatetime="<%- updatetime %>"></span>
</span>
<span class="pull-right"><%- viewcount %> views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-fw fa-pencil"></i></a></span>
</small>
diff --git a/public/views/slide/reveal.hbs b/public/views/slide.hbs
index 8a93c267..262a5df7 100644
--- a/public/views/slide/reveal.hbs
+++ b/public/views/slide.hbs
@@ -20,7 +20,7 @@
<script>
document.write( '<link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/css/print/' + ( window.location.search.match( /print-pdf/gi ) ? 'pdf' : 'paper' ) + '.css" type="text/css" media="print">' );
</script>
- <script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
+ <script src="{{{url}}}/vendor/jquery/dist/jquery.min.js"></script>
</head>
<body>
diff --git a/public/views/slide/listing.hbs b/public/views/slide/listing.hbs
deleted file mode 100644
index 7da8ebab..00000000
--- a/public/views/slide/listing.hbs
+++ /dev/null
@@ -1,22 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Directory Listing</title>
- <link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/{{{theme}}}" id="theme">
- <style type="text/css">
- body {
- margin: 1em;
- }
- a {
- color: white;
- display: block;
- }
- </style>
- <link rel="icon" href="http://i.imgur.com/IVlU2PU.png" sizes="512x512" />
- </head>
-
- <body>
- {{{listing}}}
- </body>
-</html>