17 changed files with 46 additions and 719 deletions
@ -1,234 +0,0 @@ |
|||||||
var frisby = require('frisby'); |
|
||||||
var md5 = require('md5'); |
|
||||||
|
|
||||||
frisby.create('Landing page') |
|
||||||
.get('http://localhost:3000/') |
|
||||||
.expectStatus(200) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Hassle-free') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Open note page') |
|
||||||
.get('http://localhost:3000/new') |
|
||||||
.expectStatus(200) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Terms of Service') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Open TOS') |
|
||||||
.get('http://localhost:3000/TOS') |
|
||||||
.expectStatus(200) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Site Terms of Use Modifications') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Incurrect URL') |
|
||||||
.get('http://localhost:3000/abcdef') |
|
||||||
.expectStatus(404) |
|
||||||
.expectBodyContains('Not found') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Invalid posting') |
|
||||||
.post('http://localhost:3000/note') |
|
||||||
.expectStatus(400) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Bad request') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
let testNote = 'This is a test note'; |
|
||||||
|
|
||||||
frisby.create('Invalid posting 2') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'POST', |
|
||||||
note: testNote |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Bad request') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Invalid posting 3') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'POST', |
|
||||||
session: md5('new'), |
|
||||||
signature: 'assdss', |
|
||||||
note: testNote |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Signature mismatch') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Valid posting') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'POST', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote), |
|
||||||
password: '', |
|
||||||
note: testNote |
|
||||||
}) |
|
||||||
.expectStatus(302) |
|
||||||
.expectBodyContains('Found. Redirecting to') |
|
||||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8') |
|
||||||
.after(function(err, res, body) { |
|
||||||
let noteId = body.replace('Found. Redirecting to /', ''); |
|
||||||
frisby.create('Read posted note') |
|
||||||
.get('http://localhost:3000/' + noteId) |
|
||||||
.expectStatus(200) |
|
||||||
.expectBodyContains(testNote) |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Illegal note editing attempt with empty password') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
id: noteId, |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote+'!!!'), |
|
||||||
note: testNote + '!!!', |
|
||||||
password: '' |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectBodyContains('Password is wrong') |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Illegal note editing attempt') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
id: noteId, |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote+'!!!'), |
|
||||||
note: testNote + '!!!', |
|
||||||
password: 'aaabbb' |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectBodyContains('Password is wrong') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
}) |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Valid posting, editing and removal') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'POST', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote), |
|
||||||
password: 'aabbcc', |
|
||||||
note: testNote |
|
||||||
}) |
|
||||||
.expectStatus(302) |
|
||||||
.expectBodyContains('Found. Redirecting to') |
|
||||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8') |
|
||||||
.after(function(err, res, body) { |
|
||||||
var noteId = body.replace('Found. Redirecting to /', ''); |
|
||||||
frisby.create('Export posted note') |
|
||||||
.get('http://localhost:3000/' + noteId + '/export') |
|
||||||
.expectStatus(200) |
|
||||||
.expectHeaderContains('content-type', 'text/plain; charset=utf-8') |
|
||||||
.expectBodyContains(testNote) |
|
||||||
.toss(); |
|
||||||
frisby.create('Read posted note') |
|
||||||
.get('http://localhost:3000/' + noteId) |
|
||||||
.expectStatus(200) |
|
||||||
.expectBodyContains(testNote) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Unauthorized note editing attempt') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
id: noteId, |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote+'!!!'), |
|
||||||
note: testNote + '!!!', |
|
||||||
password: 'abbcc' |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectBodyContains('Password is wrong') |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Valid note editing attempt') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
id: noteId, |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + 'Changed!'), |
|
||||||
note: 'Changed!', |
|
||||||
password: 'aabbcc' |
|
||||||
}) |
|
||||||
.expectStatus(302) |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Read changed note') |
|
||||||
.get('http://localhost:3000/' + noteId) |
|
||||||
.expectStatus(200) |
|
||||||
.expectBodyContains('Changed!') |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.after((err, res, body) => { |
|
||||||
frisby.create('Delete posted note') |
|
||||||
.post('http://localhost:3000/note',{ |
|
||||||
id: noteId, |
|
||||||
button: 'Delete', |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + 'Changed!'), |
|
||||||
note: 'Changed!', |
|
||||||
password: 'aabbcc' |
|
||||||
}) |
|
||||||
.expectStatus(200) |
|
||||||
.expectBodyContains('Note deleted') |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.toss(); |
|
||||||
frisby.create('Read stats of posted note') |
|
||||||
.expectStatus(200) |
|
||||||
.get('http://localhost:3000/' + noteId).toss(); |
|
||||||
frisby.create('Read stats of posted note') |
|
||||||
.expectStatus(200) |
|
||||||
.get('http://localhost:3000/' + noteId).toss(); |
|
||||||
frisby.create('Read stats of posted note') |
|
||||||
.expectStatus(200) |
|
||||||
.get('http://localhost:3000/' + noteId).toss(); |
|
||||||
frisby.create('Read stats of posted note') |
|
||||||
.get('http://localhost:3000/' + noteId + '/stats') |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectStatus(200) |
|
||||||
.expectBodyContains('Statistics') |
|
||||||
.expectBodyContains('<tr><td>Views</td><td>4</td></tr>') |
|
||||||
.toss(); |
|
||||||
}) |
|
||||||
.toss(); |
|
||||||
|
|
||||||
var tooLongNote = 'ABCD'; |
|
||||||
|
|
||||||
while (tooLongNote.length < 1024*200) tooLongNote += tooLongNote; |
|
||||||
|
|
||||||
frisby.create('Invalid posting of too long note') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'POST', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + testNote), |
|
||||||
password: 'aabbcc', |
|
||||||
note: tooLongNote |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('too large') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
frisby.create('Invalid update without id') |
|
||||||
.post('http://localhost:3000/note', { |
|
||||||
action: 'UPDATE', |
|
||||||
session: md5('new'), |
|
||||||
signature: md5(md5('new') + 'Any note'), |
|
||||||
password: 'aabbcc', |
|
||||||
note: 'Any note' |
|
||||||
}) |
|
||||||
.expectStatus(400) |
|
||||||
.expectHeaderContains('content-type', 'text/html; charset=utf-8') |
|
||||||
.expectBodyContains('Wrong note ID') |
|
||||||
.toss(); |
|
||||||
|
|
||||||
@ -0,0 +1,36 @@ |
|||||||
|
# Demo Note |
||||||
|
|
||||||
|
## Text Formatting |
||||||
|
|
||||||
|
This is a _short_ note demonstrating the **capabilities** of Markdown. [Markdown](http://en.wikipedia.org/wiki/Markdown) is a _markup language_ with plain text |
||||||
|
formatting syntax. But you also can use <u>standard HTML</u> tags. |
||||||
|
|
||||||
|
## Backquotes, Lists & Code |
||||||
|
|
||||||
|
This is a backquote: |
||||||
|
|
||||||
|
> _"Our greatest glory is not in never falling but in rising every time we fall."_ |
||||||
|
> — Confucius |
||||||
|
|
||||||
|
To create simple lists, just enumerate all items using a dash in the prefix: |
||||||
|
|
||||||
|
- Alpha |
||||||
|
- Beta |
||||||
|
- Gamma |
||||||
|
|
||||||
|
Also you can either mark some special `words` or write entire `code` blocks: |
||||||
|
|
||||||
|
(defn fact [n] |
||||||
|
(if (< n 2) 1 |
||||||
|
(* n (fact (- n 1))))) |
||||||
|
|
||||||
|
## Tables |
||||||
|
|
||||||
|
Also simple tables is a piece of cake: |
||||||
|
|
||||||
|
Column 1 | Column 2 | Column 3 |
||||||
|
--- | --- | --- |
||||||
|
Text 1 | Text 3 | <s>Text 5</s> |
||||||
|
Text 2 | Text 4 | <mark>Text 6</mark> |
||||||
|
|
||||||
|
Take a look at the [source code](/Demo.md/export) of this page, to see how it works. |
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,39 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "NoteHub", |
|
||||||
"version": "3.0.0", |
|
||||||
"description": "Free Pastebin for One-Off Markdown Publishing", |
|
||||||
"main": "server.js", |
|
||||||
"scripts": { |
|
||||||
"start": "node server.js", |
|
||||||
"dev": "./node_modules/nodemon/bin/nodemon.js server.js", |
|
||||||
"test": "jasmine-node ." |
|
||||||
}, |
|
||||||
"repository": { |
|
||||||
"type": "git", |
|
||||||
"url": "https://github.com/chmllr/NoteHub.git" |
|
||||||
}, |
|
||||||
"keywords": [ |
|
||||||
"markdown", |
|
||||||
"pastebin" |
|
||||||
], |
|
||||||
"author": "Christian Müller <notehub@icloud.com> (http://twitter.com/drmllr)", |
|
||||||
"license": "ISC", |
|
||||||
"bugs": { |
|
||||||
"url": "https://github.com/chmllr/NoteHub/issues" |
|
||||||
}, |
|
||||||
"homepage": "https://github.com/chmllr/NoteHub", |
|
||||||
"dependencies": { |
|
||||||
"body-parser": "^1.15.0", |
|
||||||
"express": "^4.13.4", |
|
||||||
"highlight.js": "^9.5.0", |
|
||||||
"lru-cache": "^4.0.0", |
|
||||||
"marked": "^0.3.5", |
|
||||||
"md5": "^2.1.0", |
|
||||||
"sequelize": "^3.19.3", |
|
||||||
"sqlite3": "*" |
|
||||||
}, |
|
||||||
"devDependencies": { |
|
||||||
"frisby": "^0.8.5", |
|
||||||
"nodemon": "^1.9.1" |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,45 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html style="height: 100%;"> |
|
||||||
|
|
||||||
<head> |
|
||||||
<title>NoteHub — New Page</title> |
|
||||||
<meta charset="UTF-8" /> |
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" /> |
|
||||||
<link href="/style.css" rel="stylesheet" type="text/css" /> |
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" type="text/javascript"></script> |
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js" type="text/javascript"></script> |
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js" type="text/javascript"></script> |
|
||||||
<script src="/js/publishing.js" type="text/javascript"></script> |
|
||||||
<base target="_blank"> |
|
||||||
</head> |
|
||||||
|
|
||||||
<body onload="onLoad()" style="height: 100%"> |
|
||||||
<div id="editContainer"> |
|
||||||
<form id="editPane" action="/note" autocomplete="off" method="POST" target="_self"> |
|
||||||
<input id="action" name="action" value="%ACTION%" type="hidden" /> |
|
||||||
<input id="id" name="id" value="%ID%" type="hidden" /> |
|
||||||
<input id="password" name="password" type="hidden" /> |
|
||||||
<input id="session" name="session" type="hidden" value="%SESSION%" /> |
|
||||||
<input id="signature" name="signature" type="hidden" /> |
|
||||||
<textarea id="note" name="note">%CONTENT%</textarea> |
|
||||||
<fieldset id="input-elems"> |
|
||||||
<input class="ui-elem" id="plain-password" name="plain-password" |
|
||||||
placeholder="Password for editing" type="text" autocomplete="off"> |
|
||||||
<label style="margin-right: 1em"> |
|
||||||
<input id="tos" type="checkbox" onClick="enableButton()"> |
|
||||||
Accept <a href="/TOS">Terms of Service</a> |
|
||||||
</label> |
|
||||||
<input class="button ui-elem" disabled id="publish-button" name="button" type="submit" value="Publish"> |
|
||||||
<input class="button ui-elem" id="delete-button" name="button" type="submit" value="Delete"> |
|
||||||
<span id="tableau"> |
|
||||||
0 words |
|
||||||
</span> |
|
||||||
<br> |
|
||||||
<br> |
|
||||||
</fieldset> |
|
||||||
</form> |
|
||||||
<div id="previewPane"><article id="draft"></article></div> |
|
||||||
</div> |
|
||||||
</body> |
|
||||||
|
|
||||||
</html> |
|
||||||
@ -1,73 +0,0 @@ |
|||||||
var $ = function(id) { |
|
||||||
return document.getElementById(id); |
|
||||||
}; |
|
||||||
var iosDetected = navigator.userAgent.match('(iPad|iPod|iPhone)'); |
|
||||||
var timer = null; |
|
||||||
var timerDelay = iosDetected ? 800 : 400; |
|
||||||
var $note, $action, $preview, $plain_password, $tableau; |
|
||||||
var backendTimer; |
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () { |
|
||||||
marked.setOptions({ |
|
||||||
langPrefix: 'hljs lang-', |
|
||||||
highlight: function (code) { |
|
||||||
return hljs.highlightAuto(code).value; |
|
||||||
}, |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
function md2html(input) { |
|
||||||
return marked(input); |
|
||||||
} |
|
||||||
|
|
||||||
function saveDraft() { |
|
||||||
if ($action == 'UPDATE') return; |
|
||||||
console.log('draft autosave...'); |
|
||||||
$tableau.innerHTML = 'Draft autosaved.'; |
|
||||||
localStorage.setItem('draft', $note.value); |
|
||||||
} |
|
||||||
|
|
||||||
function enableButton() { |
|
||||||
var checkbox = $('tos'); |
|
||||||
var button = $('publish-button'); |
|
||||||
button.disabled = !checkbox.checked; |
|
||||||
} |
|
||||||
|
|
||||||
function onLoad() { |
|
||||||
$note = $('note'); |
|
||||||
$action = $('action').value; |
|
||||||
$preview = $('draft'); |
|
||||||
$tableau = $('tableau'); |
|
||||||
$plain_password = $('plain-password'); |
|
||||||
var updatePreview = function() { |
|
||||||
clearTimeout(timer); |
|
||||||
var content = $note.value; |
|
||||||
var delay = Math.min(timerDelay, timerDelay * (content.length / 400)); |
|
||||||
timer = setTimeout(function() { |
|
||||||
$preview.innerHTML = md2html(content); |
|
||||||
$tableau.innerHTML = content.split(/\s+/).length + ' words'; |
|
||||||
}, delay); |
|
||||||
}; |
|
||||||
if ($action == 'UPDATE') updatePreview(); |
|
||||||
else { |
|
||||||
$('delete-button').style.display = 'none'; |
|
||||||
$note.value = ''; |
|
||||||
var draft = localStorage.getItem('draft'); |
|
||||||
if (draft) { |
|
||||||
$note.value = draft; |
|
||||||
updatePreview(); |
|
||||||
} |
|
||||||
} |
|
||||||
$note.onkeyup = updatePreview; |
|
||||||
$('delete-button').onclick = $('publish-button').onclick = function(e) { |
|
||||||
localStorage.removeItem('draft'); |
|
||||||
self.onbeforeunload = null; |
|
||||||
if ($plain_password.value !== '') $('password').value = md5($plain_password.value); |
|
||||||
$plain_password.value = null; |
|
||||||
$('signature').value = md5($('session').value + $note.value.replace(/[\n\r]/g, '')); |
|
||||||
}; |
|
||||||
if (iosDetected) $note.className += ' ui-border'; |
|
||||||
else $note.focus(); |
|
||||||
self.onbeforeunload = saveDraft; |
|
||||||
setInterval(saveDraft, 60 * 1000); |
|
||||||
} |
|
||||||
@ -1,181 +0,0 @@ |
|||||||
var express = require('express'); |
|
||||||
var view = require('./view'); |
|
||||||
var storage = require('./storage'); |
|
||||||
var md5 = require('md5'); |
|
||||||
var LRU = require('lru-cache'); |
|
||||||
var bodyParser = require('body-parser'); |
|
||||||
var fs = require('fs'); |
|
||||||
var blackList = new Set(); |
|
||||||
|
|
||||||
var app = express(); |
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true, limit: '200kb' })); |
|
||||||
app.use(express.static(__dirname + '/resources/public')); |
|
||||||
app.use(function (error, req, res, next) { |
|
||||||
if (error) { |
|
||||||
sendResponse(res, 400, 'Bad request', error.message); |
|
||||||
log('REQUEST ERROR:', error); |
|
||||||
} else { |
|
||||||
next(); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
var MODELS = {}; |
|
||||||
var CACHE = new LRU({ |
|
||||||
max: 50, |
|
||||||
dispose: key => { |
|
||||||
log('disposing', key, 'from cache'); |
|
||||||
var model = MODELS[key]; |
|
||||||
if (model) model.save(); |
|
||||||
delete MODELS[key]; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
var log = function() { |
|
||||||
var date = new Date(); |
|
||||||
var timestamp = date.getDate() + '/' + date.getMonth() + ' ' + date.getHours() + ':' + |
|
||||||
date.getMinutes() + ':' + date.getSeconds() + '.' + date.getMilliseconds(); |
|
||||||
var message = Array.prototype.slice.call(arguments); |
|
||||||
message.unshift('--'); |
|
||||||
message.unshift(timestamp); |
|
||||||
console.log.apply(console, message); |
|
||||||
}; |
|
||||||
|
|
||||||
app.get('/TOS', (req, res) => res.send(view.renderTOS())); |
|
||||||
|
|
||||||
app.get('/new', (req, res) => { |
|
||||||
log(req.ip, 'opens /new'); |
|
||||||
res.send(view.newNotePage(md5('new'))); |
|
||||||
}); |
|
||||||
|
|
||||||
app.post('/note', (req, res) => { |
|
||||||
var body = req.body, |
|
||||||
session = body.session, |
|
||||||
note = body.note, |
|
||||||
password = body.password, |
|
||||||
action = body.action, |
|
||||||
id = body.id; |
|
||||||
log(req.ip, 'calls /note to', action, id); |
|
||||||
var goToNote = note => res.redirect('/' + note.id); |
|
||||||
if (!note || !session || session.indexOf(md5('edit/' + id)) !== 0 && session.indexOf(md5('new')) !== 0) |
|
||||||
return sendResponse(res, 400, 'Bad request'); |
|
||||||
if (body.signature != md5(session + note.replace(/[\n\r]/g, ''))) |
|
||||||
return sendResponse(res, 400, 'Signature mismatch'); |
|
||||||
if (action == 'POST') |
|
||||||
storage.addNote(note, password).then(goToNote); |
|
||||||
else { |
|
||||||
if (!id) return sendResponse(res, 400, 'Wrong note ID'); |
|
||||||
CACHE.del(id); |
|
||||||
if (body.button == 'Delete') { |
|
||||||
log('deleting note', id); |
|
||||||
storage.deleteNote(id, password).then( |
|
||||||
() => sendResponse(res, 200, 'Note deleted'), |
|
||||||
error => sendResponse(res, 400, 'Bad request', error.message)); |
|
||||||
} else { |
|
||||||
log('updating note', id); |
|
||||||
storage.updateNote(id, password, note).then(goToNote, |
|
||||||
error => sendResponse(res, 400, 'Bad request', error.message)); |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
app.get('/:year/:month/:day/:title', (req, res) => { |
|
||||||
var P = req.params, url = P.year + '/' + P.month + '/' + P.day + '/' + P.title; |
|
||||||
log(req.ip, 'resolves deprecated id', url); |
|
||||||
if (CACHE.has(url)) { |
|
||||||
log(url, 'is cached!'); |
|
||||||
var id = CACHE.get(url); |
|
||||||
if (id) res.redirect('/' + id); |
|
||||||
else notFound(res); |
|
||||||
} else storage.getNoteId(url).then(note => { |
|
||||||
log(url, 'is not cached, resolving...'); |
|
||||||
if (note) { |
|
||||||
CACHE.set(url, note.id); |
|
||||||
res.redirect('/' + note.id); |
|
||||||
} else { |
|
||||||
CACHE.set(url, null); |
|
||||||
notFound(res); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
app.get(/\/([a-z0-9]+)\/edit/, (req, res) => { |
|
||||||
var id = req.params['0']; |
|
||||||
log(req.ip, 'calls /edit on', id); |
|
||||||
storage.getNote(id).then(note => res.send(note ? |
|
||||||
view.editNotePage(md5('edit/' + id), note) : |
|
||||||
notFound(res))); |
|
||||||
}); |
|
||||||
|
|
||||||
app.get(/\/([a-z0-9]+)\/export/, (req, res) => { |
|
||||||
var id = req.params['0']; |
|
||||||
log(req.ip, 'calls /export on', id); |
|
||||||
res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' }); |
|
||||||
storage.getNote(id).then(note => note ? |
|
||||||
res.send(note.text) : |
|
||||||
notFound(res)); |
|
||||||
}); |
|
||||||
|
|
||||||
app.get(/\/([a-z0-9]+)\/stats/, (req, res) => { |
|
||||||
var id = req.params['0']; |
|
||||||
log(req.ip, 'calls /stats on', id); |
|
||||||
var promise = id in MODELS ? |
|
||||||
new Promise(resolve => resolve(MODELS[id])) : |
|
||||||
storage.getNote(id); |
|
||||||
promise.then(note => note ? |
|
||||||
res.send(view.renderStats(note)) : |
|
||||||
notFound(res)); |
|
||||||
}); |
|
||||||
|
|
||||||
app.get(/\/([a-z0-9]+)/, (req, res) => { |
|
||||||
var id = req.params['0']; |
|
||||||
log(req.ip, 'open note', id, 'from', req.get('Referer')); |
|
||||||
if (CACHE.has(id)) { |
|
||||||
log(id, 'is cached!'); |
|
||||||
var obj = MODELS[id]; |
|
||||||
if (!obj) return notFound(res); |
|
||||||
obj.views++; |
|
||||||
res.send(CACHE.get(id)); |
|
||||||
} else storage.getNote(id).then(note => { |
|
||||||
log(id, 'is not cached, resolving...'); |
|
||||||
if (!note) { |
|
||||||
CACHE.set(id, null); |
|
||||||
return notFound(res); |
|
||||||
} |
|
||||||
var content = view.renderNote(note, blackList); |
|
||||||
CACHE.set(id, content); |
|
||||||
MODELS[id] = note; |
|
||||||
note.views++; |
|
||||||
res.send(content); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
var sendResponse = (res, code, message, details) => { |
|
||||||
log('sending response', code, message); |
|
||||||
res.status(code).send(view.renderPage(null, message, |
|
||||||
`<h1>${message}</h1><br/>` + |
|
||||||
`<center>${details || '¯\\_(ツ)_/¯'}</center>`, '')); |
|
||||||
}; |
|
||||||
|
|
||||||
var notFound = res => sendResponse(res, 404, 'Not found'); |
|
||||||
|
|
||||||
var server = app.listen(process.env.PORT || 3000, |
|
||||||
() => log('NoteHub server listening on port', server.address().port)); |
|
||||||
|
|
||||||
setInterval(() => { |
|
||||||
var keys = Object.keys(MODELS); |
|
||||||
log('saving stats for', keys.length, 'models...'); |
|
||||||
keys.forEach(id => MODELS[id].save()); |
|
||||||
}, 5 * 60 * 1000); |
|
||||||
|
|
||||||
var updateBlackList = () => { |
|
||||||
var ids = fs.readFileSync(process.env.BLACK_LIST || '/dev/null', 'utf-8').split(/\n+/).filter(Boolean); |
|
||||||
ids.filter(id => !blackList.has(id)).forEach(id => CACHE.del(id)); |
|
||||||
blackList = new Set(ids); |
|
||||||
log('black list updated, entries:', blackList.size); |
|
||||||
}; |
|
||||||
|
|
||||||
setInterval(updateBlackList, 60 * 60 * 1000); |
|
||||||
|
|
||||||
updateBlackList(); |
|
||||||
|
|
||||||
@ -1,64 +0,0 @@ |
|||||||
var Sequelize = require('sequelize'); |
|
||||||
var sequelize = new Sequelize('database', null, null, { |
|
||||||
dialect: 'sqlite', |
|
||||||
pool: { |
|
||||||
max: 5, |
|
||||||
min: 0, |
|
||||||
idle: 10000 |
|
||||||
}, |
|
||||||
storage: 'database.sqlite' |
|
||||||
}); |
|
||||||
|
|
||||||
var Note = sequelize.define('Note', { |
|
||||||
id: { type: Sequelize.STRING(6), unique: true, primaryKey: true }, |
|
||||||
deprecatedId: Sequelize.TEXT, |
|
||||||
text: Sequelize.TEXT, |
|
||||||
published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW }, |
|
||||||
edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null }, |
|
||||||
password: Sequelize.STRING(16), |
|
||||||
views: { type: Sequelize.INTEGER, defaultValue: 0 } |
|
||||||
}); |
|
||||||
|
|
||||||
sequelize.sync(); |
|
||||||
|
|
||||||
module.exports.getNote = id => Note.findById(id); |
|
||||||
|
|
||||||
module.exports.getNoteId = deprecatedId => Note.findOne({ |
|
||||||
where: { deprecatedId: deprecatedId } |
|
||||||
}); |
|
||||||
|
|
||||||
var generateId = () => [1, 1, 1, 1, 1] |
|
||||||
.map(() => { |
|
||||||
var code = Math.floor(Math.random() * 36); |
|
||||||
return String.fromCharCode(code + (code < 10 ? 48 : 87)); |
|
||||||
}) |
|
||||||
.join(''); |
|
||||||
|
|
||||||
var getFreeId = () => { |
|
||||||
var id = generateId(); |
|
||||||
return Note.findById(id).then(result => result ? getFreeId() : id); |
|
||||||
}; |
|
||||||
|
|
||||||
module.exports.addNote = (note, password) => getFreeId().then(id => Note.create({ |
|
||||||
id: id, |
|
||||||
text: note, |
|
||||||
password: password |
|
||||||
})); |
|
||||||
|
|
||||||
var passwordCheck = (note, password, callback) => |
|
||||||
(!note || note.password.length === 0 || note.password !== password) ? |
|
||||||
new Promise((resolve, reject) => reject({ message: 'Password is wrong' })) : |
|
||||||
callback(); |
|
||||||
|
|
||||||
module.exports.updateNote = (id, password, text) => |
|
||||||
Note.findById(id).then(note => |
|
||||||
passwordCheck(note, password, () => { |
|
||||||
note.text = text; |
|
||||||
note.edited = new Date(); |
|
||||||
return note.save(); |
|
||||||
})); |
|
||||||
|
|
||||||
module.exports.deleteNote = (id, password) => |
|
||||||
Note.findById(id).then(note => |
|
||||||
passwordCheck(note, password, () => note.destroy())); |
|
||||||
|
|
||||||
@ -1,55 +0,0 @@ |
|||||||
var marked = require('marked'); |
|
||||||
var fs = require('fs'); |
|
||||||
var hljs = require('highlight.js'); |
|
||||||
|
|
||||||
var TOS = fs.readFileSync('resources/TOS.md', 'utf-8'); |
|
||||||
var pageTemplate = fs.readFileSync('resources/template.html', 'utf-8'); |
|
||||||
var footerTemplate = fs.readFileSync('resources/footer.html', 'utf-8'); |
|
||||||
var editTemplate = fs.readFileSync('resources/edit.html', 'utf-8'); |
|
||||||
var header = fs.readFileSync(process.env.HEADER || '/dev/null', 'utf-8'); |
|
||||||
|
|
||||||
var deriveTitle = text => text |
|
||||||
.split(/[\n\r]/)[0].slice(0,25) |
|
||||||
.replace(/[`~!@#\$%^&\*_|\+=\?;:'",.<>\{\}\\\/]/g, ''); |
|
||||||
|
|
||||||
var renderPage = (id, title, content, footer, blackList) => pageTemplate |
|
||||||
.replace('%HEADER%', blackList && blackList.has(id) ? header : '') |
|
||||||
.replace('%TITLE%', title) |
|
||||||
.replace('%CONTENT%', content.replace(/<meta.*?>/gi, '').replace(/<script[\s\S.]*?\/script>/gi, '')) |
|
||||||
.replace('%FOOTER%', footer || ''); |
|
||||||
|
|
||||||
marked.setOptions({ |
|
||||||
langPrefix: 'hljs lang-', |
|
||||||
highlight: code => hljs.highlightAuto(code).value, |
|
||||||
}); |
|
||||||
|
|
||||||
module.exports.renderPage = renderPage; |
|
||||||
|
|
||||||
module.exports.renderStats = note => renderPage(note.id, deriveTitle(note.text), |
|
||||||
`<h2>Statistics</h2>
|
|
||||||
<table> |
|
||||||
<tr><td>Published</td><td>${note.published}</td></tr> |
|
||||||
<tr><td>Edited</td><td>${note.edited || 'N/A'}</td></tr> |
|
||||||
<tr><td>Views</td><td>${note.views}</td></tr> |
|
||||||
</table>`); |
|
||||||
|
|
||||||
module.exports.renderTOS = () => renderPage('tos', 'Terms of Service', marked(TOS)); |
|
||||||
|
|
||||||
module.exports.renderNote = (note, blackList) => renderPage(note.id, |
|
||||||
deriveTitle(note.text), |
|
||||||
marked(note.text), |
|
||||||
footerTemplate.replace(/%LINK%/g, note.id), |
|
||||||
blackList); |
|
||||||
|
|
||||||
module.exports.newNotePage = session => editTemplate |
|
||||||
.replace('%ACTION%', 'POST') |
|
||||||
.replace('%SESSION%', session) |
|
||||||
.replace('%CONTENT%', 'Loading...'); |
|
||||||
|
|
||||||
module.exports.editNotePage = (session, note) => editTemplate |
|
||||||
.replace('%ACTION%', 'UPDATE') |
|
||||||
.replace('%SESSION%', session) |
|
||||||
.replace('%ID%', note.id) |
|
||||||
.replace('%CONTENT%', escape$(note.text)); |
|
||||||
|
|
||||||
var escape$ = s => s.split('').map(chr => chr == '$' ? '$$' : chr).join(''); |
|
||||||
Loading…
Reference in new issue