Skip to content

Commit 2e468db

Browse files
authored
Merge pull request #1677 from hackmdio/release/2.4.0
Release 2.4.0
2 parents 0963fa9 + 792f705 commit 2e468db

31 files changed

+575
-863
lines changed

CONTRIBUTING.md

+2
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,5 @@ If you set your `user.name` and `user.email` git configs, you can sign your
7171
commit automatically with `git commit -s`. You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases)
7272
like `git config --global alias.ci 'commit -s'`. Now you can commit with
7373
`git ci` and the commit will be signed.
74+
75+
[dcofile]: https://github.com/hackmdio/codimd/blob/develop/contribute/developer-certificate-of-origin

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CodiMD
44
[![build status][travis-image]][travis-url]
55
[![version][github-version-badge]][github-release-page]
66
[![Gitter][gitter-image]][gitter-url]
7+
[![Matrix][matrix-image]][matrix-url]
78
[![POEditor][poeditor-image]][poeditor-url]
89

910
CodiMD lets you collaborate in real-time with markdown.
@@ -90,7 +91,7 @@ To stay up to date with your installation it's recommended to subscribe the [rel
9091

9192
**License under AGPL.**
9293

93-
[gitter-image]: https://img.shields.io/badge/gitter-hackmdio/codimd-blue.svg
94+
[gitter-image]: https://img.shields.io/badge/gitter-hackmdio/codimd-blue.svg
9495
[gitter-url]: https://gitter.im/hackmdio/hackmd
9596
[travis-image]: https://travis-ci.com/hackmdio/codimd.svg?branch=master
9697
[travis-url]: https://travis-ci.com/hackmdio/codimd
@@ -99,3 +100,5 @@ To stay up to date with your installation it's recommended to subscribe the [rel
99100
[github-release-feed]: https://github.com/hackmdio/codimd/releases.atom
100101
[poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg
101102
[poeditor-url]: https://poeditor.com/join/project/q0nuPWyztp
103+
[matrix-image]: https://img.shields.io/matrix/hackmdio_hackmd:gitter.im?color=blue&logo=matrix
104+
[matrix-url]: https://matrix.to/#/#hackmdio_hackmd:gitter.im

app.js

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ app.set('view engine', 'ejs')
194194
app.locals.useCDN = config.useCDN
195195
app.locals.serverURL = config.serverURL
196196
app.locals.sourceURL = config.sourceURL
197+
app.locals.privacyPolicyURL = config.privacyPolicyURL
197198
app.locals.allowAnonymous = config.allowAnonymous
198199
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
199200
app.locals.permission = config.permission

app.json

+4
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@
143143
"CMD_ALLOW_PDF_EXPORT": {
144144
"description": "Enable or disable PDF exports",
145145
"required": false
146+
},
147+
"PGSSLMODE": {
148+
"description": "Enforce PG SSL mode",
149+
"value": "require"
146150
}
147151
},
148152
"addons": [

lib/auth/utils.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@ const config = require('../config')
55
const logger = require('../logger')
66

77
exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
8-
var referer = req.get('referer')
98
if (!req.session) req.session = {}
10-
req.session.returnTo = referer
9+
10+
var referer = req.get('referer')
11+
var refererSearchParams = new URLSearchParams(new URL(referer).search)
12+
var nextURL = refererSearchParams.get('next')
13+
14+
if (nextURL) {
15+
var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//')
16+
if (isRelativeNextURL) {
17+
req.session.returnTo = (new URL(nextURL, config.serverURL)).toString()
18+
} else {
19+
req.session.returnTo = config.serverURL
20+
}
21+
} else {
22+
req.session.returnTo = referer
23+
}
1124
}
1225

1326
exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {

lib/config/default.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module.exports = {
3737
defaultPermission: 'editable',
3838
dbURL: '',
3939
db: {},
40+
privacyPolicyURL: '',
4041
// ssl path
4142
sslKeyPath: '',
4243
sslCertPath: '',
@@ -188,5 +189,6 @@ module.exports = {
188189
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
189190
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
190191
linkifyHeaderStyle: 'keep-case',
191-
autoVersionCheck: true
192+
autoVersionCheck: true,
193+
defaultTocDepth: 3
192194
}

lib/config/environment.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = {
3535
sessionSecret: process.env.CMD_SESSION_SECRET,
3636
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
3737
responseMaxLag: toIntegerConfig(process.env.CMD_RESPONSE_MAX_LAG),
38+
privacyPolicyURL: process.env.CMD_PRIVACY_POLICY_URL,
3839
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
3940
imgur: {
4041
clientID: process.env.CMD_IMGUR_CLIENTID
@@ -147,5 +148,6 @@ module.exports = {
147148
openID: toBooleanConfig(process.env.CMD_OPENID),
148149
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
149150
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
150-
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK)
151+
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK),
152+
defaultTocDepth: toIntegerConfig(process.env.CMD_DEFAULT_TOC_DEPTH)
151153
}

lib/models/note.js

+76-65
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,22 @@ module.exports = function (sequelize, DataTypes) {
9292
return new Promise(function (resolve, reject) {
9393
// if no content specified then use default note
9494
if (!note.content) {
95-
var body = null
96-
let filePath = null
97-
if (!note.alias) {
98-
filePath = config.defaultNotePath
99-
} else {
100-
filePath = path.join(config.docsPath, note.alias + '.md')
95+
let filePath = config.defaultNotePath
96+
97+
if (note.alias) {
98+
const notePathInDocPath = path.join(config.docsPath, path.basename(note.alias) + '.md')
99+
if (Note.checkFileExist(notePathInDocPath)) {
100+
filePath = notePathInDocPath
101+
}
101102
}
103+
102104
if (Note.checkFileExist(filePath)) {
103-
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
104-
body = fs.readFileSync(filePath, 'utf8')
105-
note.title = Note.parseNoteTitle(body)
106-
note.content = body
105+
const noteInFS = readFileSystemNote(filePath)
106+
note.title = noteInFS.title
107+
note.content = noteInFS.content
107108
if (filePath !== config.defaultNotePath) {
108-
note.createdAt = fsCreatedTime
109+
note.createdAt = noteInFS.lastchangeAt
110+
note.lastchangeAt = noteInFS.lastchangeAt
109111
}
110112
}
111113
}
@@ -196,6 +198,29 @@ module.exports = function (sequelize, DataTypes) {
196198
})
197199
})
198200
}
201+
202+
async function syncNote (noteInFS, note) {
203+
const contentLength = noteInFS.content.length
204+
205+
let note2 = await note.update({
206+
title: noteInFS.title,
207+
content: noteInFS.content,
208+
lastchangeAt: noteInFS.lastchangeAt
209+
})
210+
const revision = await sequelize.models.Revision.saveNoteRevisionAsync(note2)
211+
// update authorship on after making revision of docs
212+
const patch = dmp.patch_fromText(revision.patch)
213+
const operations = Note.transformPatchToOperations(patch, contentLength)
214+
let authorship = note2.authorship
215+
for (let i = 0; i < operations.length; i++) {
216+
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
217+
}
218+
note2 = await note.update({
219+
authorship: authorship
220+
})
221+
return note2.id
222+
}
223+
199224
Note.parseNoteId = function (noteId, callback) {
200225
async.series({
201226
parseNoteIdByAlias: function (_callback) {
@@ -204,65 +229,35 @@ module.exports = function (sequelize, DataTypes) {
204229
where: {
205230
alias: noteId
206231
}
207-
}).then(function (note) {
208-
if (note) {
209-
const filePath = path.join(config.docsPath, noteId + '.md')
210-
if (Note.checkFileExist(filePath)) {
211-
// if doc in filesystem have newer modified time than last change time
212-
// then will update the doc in db
213-
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
214-
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
215-
var body = fs.readFileSync(filePath, 'utf8')
216-
var contentLength = body.length
217-
var title = Note.parseNoteTitle(body)
218-
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
219-
note.update({
220-
title: title,
221-
content: body,
222-
lastchangeAt: fsModifiedTime
223-
}).then(function (note) {
224-
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
225-
if (err) return _callback(err, null)
226-
// update authorship on after making revision of docs
227-
var patch = dmp.patch_fromText(revision.patch)
228-
var operations = Note.transformPatchToOperations(patch, contentLength)
229-
var authorship = note.authorship
230-
for (let i = 0; i < operations.length; i++) {
231-
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
232-
}
233-
note.update({
234-
authorship: authorship
235-
}).then(function (note) {
236-
return callback(null, note.id)
237-
}).catch(function (err) {
238-
return _callback(err, null)
239-
})
240-
})
241-
}).catch(function (err) {
242-
return _callback(err, null)
243-
})
232+
}).then(async function (note) {
233+
const filePath = path.join(config.docsPath, path.basename(noteId) + '.md')
234+
if (Note.checkFileExist(filePath)) {
235+
try {
236+
if (note) {
237+
// if doc in filesystem have newer modified time than last change time
238+
// then will update the doc in db
239+
const noteInFS = readFileSystemNote(filePath)
240+
if (shouldSyncNote(note, noteInFS)) {
241+
const noteId = await syncNote(noteInFS, note)
242+
return callback(null, noteId)
243+
}
244244
} else {
245+
// create new note with alias, and will sync md file in beforeCreateHook
246+
const note = await Note.create({
247+
alias: noteId,
248+
owner: null,
249+
permission: 'locked'
250+
})
245251
return callback(null, note.id)
246252
}
247-
} else {
248-
return callback(null, note.id)
249-
}
250-
} else {
251-
var filePath = path.join(config.docsPath, noteId + '.md')
252-
if (Note.checkFileExist(filePath)) {
253-
Note.create({
254-
alias: noteId,
255-
owner: null,
256-
permission: 'locked'
257-
}).then(function (note) {
258-
return callback(null, note.id)
259-
}).catch(function (err) {
260-
return _callback(err, null)
261-
})
262-
} else {
263-
return _callback(null, null)
253+
} catch (err) {
254+
return _callback(err, null)
264255
}
265256
}
257+
if (!note) {
258+
return _callback(null, null)
259+
}
260+
return callback(null, note.id)
266261
}).catch(function (err) {
267262
return _callback(err, null)
268263
})
@@ -589,5 +584,21 @@ module.exports = function (sequelize, DataTypes) {
589584
return operations
590585
}
591586

587+
function readFileSystemNote (filePath) {
588+
const fsModifiedTime = moment(fs.statSync(filePath).mtime)
589+
const content = fs.readFileSync(filePath, 'utf8')
590+
591+
return {
592+
lastchangeAt: fsModifiedTime,
593+
title: Note.parseNoteTitle(content),
594+
content: content
595+
}
596+
}
597+
598+
function shouldSyncNote (note, noteInFS) {
599+
const dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
600+
return noteInFS.lastchangeAt.isAfter(dbModifiedTime) && note.content !== noteInFS.content
601+
}
602+
592603
return Note
593604
}

lib/models/revision.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var moment = require('moment')
66
var childProcess = require('child_process')
77
var shortId = require('shortid')
88
var path = require('path')
9+
var util = require('util')
910

1011
var Op = Sequelize.Op
1112

@@ -296,6 +297,7 @@ module.exports = function (sequelize, DataTypes) {
296297
return callback(err, null)
297298
})
298299
}
300+
Revision.saveNoteRevisionAsync = util.promisify(Revision.saveNoteRevision)
299301
Revision.finishSaveNoteRevision = function (note, revision, callback) {
300302
note.update({
301303
savedAt: revision.updatedAt

lib/note/index.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ const deleteNote = async (req, res) => {
268268
}
269269

270270
const updateNote = async (req, res) => {
271-
if (req.isAuthenticated()) {
271+
if (req.isAuthenticated() || config.allowAnonymousEdits) {
272272
const noteId = await Note.parseNoteIdAsync(req.params.noteId)
273273
try {
274274
const note = await Note.findOne({
@@ -294,7 +294,7 @@ const updateNote = async (req, res) => {
294294
lastchangeAt: now,
295295
authorship: [
296296
[
297-
req.user.id,
297+
req.isAuthenticated() ? req.user.id : null,
298298
0,
299299
content.length,
300300
now,
@@ -308,7 +308,9 @@ const updateNote = async (req, res) => {
308308
return errorInternalError(req, res)
309309
}
310310

311-
updateHistory(req.user.id, note.id, content)
311+
if (req.isAuthenticated()) {
312+
updateHistory(req.user.id, noteId, content)
313+
}
312314

313315
Revision.saveNoteRevision(note, (err, revision) => {
314316
if (err) {
@@ -321,7 +323,7 @@ const updateNote = async (req, res) => {
321323
})
322324
})
323325
} catch (err) {
324-
logger.error(err)
326+
logger.error(err.stack)
325327
logger.error('Update note failed: Internal Error.')
326328
return errorInternalError(req, res)
327329
}

lib/response.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ function errorForbidden (req, res) {
3232
if (req.user) {
3333
responseError(res, '403', 'Forbidden', 'oh no.')
3434
} else {
35+
var nextURL = new URL('', config.serverURL)
36+
nextURL.search = new URLSearchParams({ next: req.originalUrl })
3537
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
36-
res.redirect(config.serverURL + '/')
38+
res.redirect(nextURL.toString())
3739
}
3840
}
3941

lib/status/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ exports.getConfig = (req, res) => {
4141
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
4242
defaultUseHardbreak: config.defaultUseHardbreak,
4343
linkifyHeaderStyle: config.linkifyHeaderStyle,
44-
useCDN: config.useCDN
44+
useCDN: config.useCDN,
45+
defaultTocDepth: config.defaultTocDepth
4546
}
4647
res.set({
4748
'Cache-Control': 'private', // only cache by client

0 commit comments

Comments
 (0)