summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCheng-Han, Wu2016-04-20 18:03:55 +0800
committerCheng-Han, Wu2016-04-20 18:03:55 +0800
commit49b51e478fa75b8d5254662de3265edcf8906004 (patch)
tree3b09213baae129156339b5ad496924f591790e88
parente613aeba75aec5ceb4f10ae62881a3635183857d (diff)
Refactor server with Sequelize ORM, refactor server configs, now will show note status (created or updated) and support docs (note alias)
Diffstat (limited to '')
-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>