Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Export PDF with puppeteer #1450

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@
},
{
"url": "https://github.com/heroku/heroku-buildpack-nodejs"
},
{
"url": "https://github.com/jontewks/puppeteer-heroku-buildpack"
}
]
}
4 changes: 3 additions & 1 deletion lib/config/default.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const os = require('os')
const uuid = require('uuid/v4')

module.exports = {
domain: '',
Expand Down Expand Up @@ -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()
}
53 changes: 43 additions & 10 deletions lib/note/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const jwt = require('jsonwebtoken')

const config = require('../config')
const logger = require('../logger')

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -191,3 +222,5 @@ async function noteActions (req, res) {
exports.showNote = showNote
exports.showPublishNote = showPublishNote
exports.noteActions = noteActions
exports.actionPDF = actionPDF
exports.findNote = findNote
94 changes: 59 additions & 35 deletions lib/note/noteActions.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
'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')
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) {
Expand Down Expand Up @@ -64,43 +64,67 @@ function actionInfo (req, res, note) {
res.send(data)
}

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 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 noteId = req.params.noteId

const highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
if (req.method === 'POST') {
const token = req.user && jwt.sign({ userId: req.user.id.toString() }, config.codimdSignKey, { expiresIn: 5 * 60 })

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 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
}
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)

res.set({
'Cache-Control': 'private' // only cache by client
})
stream.pipe(res)
})

return res.render('pretty.ejs', data)
}
}

const outputFormats = {
Expand Down
3 changes: 2 additions & 1 deletion lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading