mirror of https://github.com/status-im/codimd.git
Export pdf with puppeteer
Signed-off-by: Yukai Huang <yukaihuangtw@gmail.com>
This commit is contained in:
parent
42ae89cc72
commit
c8462c72b3
|
@ -1,6 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
const os = require('os')
|
||||
const uuid = require('uuid/v4')
|
||||
|
||||
module.exports = {
|
||||
domain: '',
|
||||
|
@ -186,5 +187,6 @@ module.exports = {
|
|||
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
|
||||
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
|
||||
linkifyHeaderStyle: 'keep-case',
|
||||
autoVersionCheck: true
|
||||
autoVersionCheck: true,
|
||||
codimdSignKey: uuid()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
|
@ -77,6 +79,24 @@ async function showNote (req, res) {
|
|||
return responseCodiMD(res, note)
|
||||
}
|
||||
|
||||
async function authenticateUser (req) {
|
||||
const authHeader = req.header('Authorization')
|
||||
const token = authHeader && authHeader.replace('Bearer ', '')
|
||||
|
||||
if (token) {
|
||||
const { userId } = jwt.verify(token, config.codimdSignKey)
|
||||
const user = await User.findOne({ id: userId })
|
||||
|
||||
if (user) {
|
||||
return [true, user]
|
||||
} else {
|
||||
return [false]
|
||||
}
|
||||
} else {
|
||||
return [req.isAuthenticated(), req.user]
|
||||
}
|
||||
}
|
||||
|
||||
function canViewNote (note, isLogin, userId) {
|
||||
if (note.permission === 'private') {
|
||||
return note.ownerId === userId
|
||||
|
@ -87,6 +107,26 @@ function canViewNote (note, isLogin, userId) {
|
|||
return true
|
||||
}
|
||||
|
||||
async function findNote (req, res, next) {
|
||||
const noteId = req.params.noteId
|
||||
|
||||
const note = await getNoteById(noteId)
|
||||
|
||||
if (!note) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
|
||||
const [isAuthenticated, user] = await authenticateUser(req)
|
||||
|
||||
if (!canViewNote(note, isAuthenticated, user ? user.id : null)) {
|
||||
return errorForbidden(req, res)
|
||||
}
|
||||
|
||||
res.locals.note = note
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
async function showPublishNote (req, res) {
|
||||
const shortid = req.params.shortid
|
||||
|
||||
|
@ -141,18 +181,9 @@ async function showPublishNote (req, res) {
|
|||
}
|
||||
|
||||
async function noteActions (req, res) {
|
||||
const { note } = res.locals
|
||||
const noteId = req.params.noteId
|
||||
|
||||
const note = await getNoteById(noteId)
|
||||
|
||||
if (!note) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
|
||||
if (!canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) {
|
||||
return errorForbidden(req, res)
|
||||
}
|
||||
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'publish':
|
||||
|
@ -191,3 +222,5 @@ async function noteActions (req, res) {
|
|||
exports.showNote = showNote
|
||||
exports.showPublishNote = showPublishNote
|
||||
exports.noteActions = noteActions
|
||||
exports.actionPDF = actionPDF
|
||||
exports.findNote = findNote
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const markdownpdf = require('markdown-pdf')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const puppeteer = require('puppeteer')
|
||||
const shortId = require('shortid')
|
||||
const querystring = require('querystring')
|
||||
const moment = require('moment')
|
||||
|
@ -10,7 +10,7 @@ const { Pandoc } = require('@hackmd/pandoc.js')
|
|||
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const { Note, Revision } = require('../models')
|
||||
const { Note, Revision, User } = require('../models')
|
||||
const { errorInternalError, errorNotFound } = require('../response')
|
||||
|
||||
function actionPublish (req, res, note) {
|
||||
|
@ -64,43 +64,67 @@ function actionInfo (req, res, note) {
|
|||
res.send(data)
|
||||
}
|
||||
|
||||
function actionPDF (req, res, note) {
|
||||
const url = config.serverURL || 'http://' + req.get('host')
|
||||
const body = note.content
|
||||
const extracted = Note.extractMeta(body)
|
||||
let content = extracted.markdown
|
||||
const title = Note.decodeTitle(note.title)
|
||||
|
||||
const highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
|
||||
|
||||
if (!fs.existsSync(config.tmpPath)) {
|
||||
fs.mkdirSync(config.tmpPath)
|
||||
}
|
||||
const pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
|
||||
content = content.replace(/\]\(\//g, '](' + url + '/')
|
||||
const markdownpdfOptions = {
|
||||
highlightCssPath: highlightCssPath
|
||||
}
|
||||
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
|
||||
if (!fs.existsSync(pdfPath)) {
|
||||
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
|
||||
return errorInternalError(req, res)
|
||||
}
|
||||
const stream = fs.createReadStream(pdfPath)
|
||||
let 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.on('end', () => {
|
||||
stream.close()
|
||||
fs.unlinkSync(pdfPath)
|
||||
})
|
||||
stream.pipe(res)
|
||||
async function printPDF (noteUrl, headers = {}) {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--disable-dev-shm-usage']
|
||||
})
|
||||
|
||||
const page = await browser.newPage()
|
||||
await page.setExtraHTTPHeaders(headers)
|
||||
await page.goto(noteUrl, { waitUntil: 'networkidle0' })
|
||||
const pdf = await page.pdf({ format: 'A4' })
|
||||
|
||||
await browser.close()
|
||||
return pdf
|
||||
}
|
||||
|
||||
function actionPDF (req, res, note) {
|
||||
const noteId = req.params.noteId
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const token = jwt.sign({ userId: req.user.id.toString() }, config.codimdSignKey, { expiresIn: 5 * 60 })
|
||||
|
||||
const noteURL = `${config.serverURL}/${noteId}/pdf`
|
||||
|
||||
return printPDF(noteURL, {
|
||||
Authorization: `Bearer ${token}`
|
||||
}).then(pdf => {
|
||||
res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length })
|
||||
res.send(pdf)
|
||||
})
|
||||
} else {
|
||||
const body = note.content
|
||||
const extracted = Note.extractMeta(body)
|
||||
const markdown = extracted.markdown
|
||||
const meta = Note.parseMeta(extracted.meta)
|
||||
const createTime = note.createdAt
|
||||
const updateTime = note.lastchangeAt
|
||||
const title = Note.generateWebTitle(meta.title || Note.decodeTitle(note.title))
|
||||
|
||||
const data = {
|
||||
title: title,
|
||||
description: meta.description || (markdown ? Note.generateDescription(markdown) : null),
|
||||
viewcount: note.viewcount,
|
||||
createtime: createTime,
|
||||
updatetime: updateTime,
|
||||
body: body,
|
||||
owner: note.owner ? note.owner.id : null,
|
||||
ownerprofile: note.owner ? User.getProfile(note.owner) : null,
|
||||
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
|
||||
lastchangeuserprofile: note.lastchangeuser ? User.getProfile(note.lastchangeuser) : null,
|
||||
robots: meta.robots || false, // default allow robots
|
||||
GA: meta.GA,
|
||||
disqus: meta.disqus,
|
||||
cspNonce: res.locals.nonce
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'private' // only cache by client
|
||||
})
|
||||
|
||||
return res.render('pretty.ejs', data)
|
||||
}
|
||||
}
|
||||
|
||||
const outputFormats = {
|
||||
|
|
|
@ -74,7 +74,8 @@ appRouter.get('/p/:shortid/:action', response.publishSlideActions)
|
|||
// get note by id
|
||||
appRouter.get('/:noteId', wrap(noteController.showNote))
|
||||
// note actions
|
||||
appRouter.get('/:noteId/:action', noteController.noteActions)
|
||||
appRouter.get('/:noteId/:action', noteController.findNote, noteController.noteActions)
|
||||
appRouter.post('/:noteId/:action', noteController.findNote, noteController.noteActions)
|
||||
// note actions with action id
|
||||
appRouter.get('/:noteId/:action/:actionId', noteController.noteActions)
|
||||
|
||||
|
|
|
@ -586,6 +586,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
|
||||
},
|
||||
"@types/mime-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz",
|
||||
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.17.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.14.tgz",
|
||||
|
@ -8699,6 +8704,23 @@
|
|||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz",
|
||||
"integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70="
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
|
||||
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
|
||||
"requires": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
|
@ -9046,11 +9068,36 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
},
|
||||
"lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
},
|
||||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
},
|
||||
"lodash.map": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
|
||||
|
@ -9067,6 +9114,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
},
|
||||
"lodash.pick": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
|
||||
|
@ -12694,6 +12746,11 @@
|
|||
"ipaddr.js": "1.9.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
|
@ -12761,6 +12818,60 @@
|
|||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"puppeteer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.1.1.tgz",
|
||||
"integrity": "sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg==",
|
||||
"requires": {
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"debug": "^4.1.0",
|
||||
"extract-zip": "^1.6.6",
|
||||
"https-proxy-agent": "^4.0.0",
|
||||
"mime": "^2.0.3",
|
||||
"mime-types": "^2.1.25",
|
||||
"progress": "^2.0.1",
|
||||
"proxy-from-env": "^1.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"ws": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"agent-base": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
|
||||
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
|
||||
"requires": {
|
||||
"agent-base": "5",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
|
||||
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
|
||||
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"js-cookie": "~2.2.0",
|
||||
"js-yaml": "~3.13.1",
|
||||
"jsdom-nogyp": "~0.8.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"keymaster": "~1.6.2",
|
||||
"list.js": "~1.5.0",
|
||||
"lodash": "~4.17.15",
|
||||
|
@ -124,6 +125,7 @@
|
|||
"pg-hstore": "~2.3.2",
|
||||
"plantuml-encoder": "^1.2.5",
|
||||
"prismjs": "~1.17.1",
|
||||
"puppeteer": "^2.1.1",
|
||||
"randomcolor": "~0.5.4",
|
||||
"raphael": "~2.2.8",
|
||||
"readline-sync": "~1.4.7",
|
||||
|
|
|
@ -951,7 +951,18 @@ ui.toolbar.download.rawhtml.click(function (e) {
|
|||
exportToRawHTML(ui.area.markdown)
|
||||
})
|
||||
// pdf
|
||||
ui.toolbar.download.pdf.attr('download', '').attr('href', noteurl + '/pdf')
|
||||
ui.toolbar.download.pdf.click(function (e) {
|
||||
// TODO: using ajax
|
||||
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.target = '_blank'
|
||||
form.action = `${noteurl}/pdf`
|
||||
|
||||
document.body.appendChild(form)
|
||||
|
||||
form.submit()
|
||||
})
|
||||
|
||||
ui.modal.pandocExport.find('#pandoc-export-download').click(function (e) {
|
||||
e.preventDefault()
|
||||
|
|
Loading…
Reference in New Issue