Skip to content

Commit

Permalink
Merge pull request hackmdio#1677 from hackmdio/release/2.4.0
Browse files Browse the repository at this point in the history
Release 2.4.0
  • Loading branch information
Yukaii committed May 11, 2021
2 parents 0963fa9 + 792f705 commit 2e468db
Show file tree
Hide file tree
Showing 31 changed files with 575 additions and 863 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`. You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases)
like `git config --global alias.ci 'commit -s'`. Now you can commit with
`git ci` and the commit will be signed.

[dcofile]: https://github.com/hackmdio/codimd/blob/develop/contribute/developer-certificate-of-origin
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CodiMD
[![build status][travis-image]][travis-url]
[![version][github-version-badge]][github-release-page]
[![Gitter][gitter-image]][gitter-url]
[![Matrix][matrix-image]][matrix-url]
[![POEditor][poeditor-image]][poeditor-url]

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

**License under AGPL.**

[gitter-image]: https://img.shields.io/badge/gitter-hackmdio/codimd-blue.svg
[gitter-image]: https://img.shields.io/badge/gitter-hackmdio/codimd-blue.svg
[gitter-url]: https://gitter.im/hackmdio/hackmd
[travis-image]: https://travis-ci.com/hackmdio/codimd.svg?branch=master
[travis-url]: https://travis-ci.com/hackmdio/codimd
Expand All @@ -99,3 +100,5 @@ To stay up to date with your installation it's recommended to subscribe the [rel
[github-release-feed]: https://github.com/hackmdio/codimd/releases.atom
[poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg
[poeditor-url]: https://poeditor.com/join/project/q0nuPWyztp
[matrix-image]: https://img.shields.io/matrix/hackmdio_hackmd:gitter.im?color=blue&logo=matrix
[matrix-url]: https://matrix.to/#/#hackmdio_hackmd:gitter.im
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ app.set('view engine', 'ejs')
app.locals.useCDN = config.useCDN
app.locals.serverURL = config.serverURL
app.locals.sourceURL = config.sourceURL
app.locals.privacyPolicyURL = config.privacyPolicyURL
app.locals.allowAnonymous = config.allowAnonymous
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
app.locals.permission = config.permission
Expand Down
4 changes: 4 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
"CMD_ALLOW_PDF_EXPORT": {
"description": "Enable or disable PDF exports",
"required": false
},
"PGSSLMODE": {
"description": "Enforce PG SSL mode",
"value": "require"
}
},
"addons": [
Expand Down
17 changes: 15 additions & 2 deletions lib/auth/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ const config = require('../config')
const logger = require('../logger')

exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
var referer = req.get('referer')
if (!req.session) req.session = {}
req.session.returnTo = referer

var referer = req.get('referer')
var refererSearchParams = new URLSearchParams(new URL(referer).search)
var nextURL = refererSearchParams.get('next')

if (nextURL) {
var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//')
if (isRelativeNextURL) {
req.session.returnTo = (new URL(nextURL, config.serverURL)).toString()
} else {
req.session.returnTo = config.serverURL
}
} else {
req.session.returnTo = referer
}
}

exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {
Expand Down
4 changes: 3 additions & 1 deletion lib/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module.exports = {
defaultPermission: 'editable',
dbURL: '',
db: {},
privacyPolicyURL: '',
// ssl path
sslKeyPath: '',
sslCertPath: '',
Expand Down Expand Up @@ -188,5 +189,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,
defaultTocDepth: 3
}
4 changes: 3 additions & 1 deletion lib/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = {
sessionSecret: process.env.CMD_SESSION_SECRET,
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
responseMaxLag: toIntegerConfig(process.env.CMD_RESPONSE_MAX_LAG),
privacyPolicyURL: process.env.CMD_PRIVACY_POLICY_URL,
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
imgur: {
clientID: process.env.CMD_IMGUR_CLIENTID
Expand Down Expand Up @@ -147,5 +148,6 @@ module.exports = {
openID: toBooleanConfig(process.env.CMD_OPENID),
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK)
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK),
defaultTocDepth: toIntegerConfig(process.env.CMD_DEFAULT_TOC_DEPTH)
}
141 changes: 76 additions & 65 deletions lib/models/note.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,22 @@ module.exports = function (sequelize, DataTypes) {
return new Promise(function (resolve, reject) {
// if no content specified then use default note
if (!note.content) {
var body = null
let filePath = null
if (!note.alias) {
filePath = config.defaultNotePath
} else {
filePath = path.join(config.docsPath, note.alias + '.md')
let filePath = config.defaultNotePath

if (note.alias) {
const notePathInDocPath = path.join(config.docsPath, path.basename(note.alias) + '.md')
if (Note.checkFileExist(notePathInDocPath)) {
filePath = notePathInDocPath
}
}

if (Note.checkFileExist(filePath)) {
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
body = fs.readFileSync(filePath, 'utf8')
note.title = Note.parseNoteTitle(body)
note.content = body
const noteInFS = readFileSystemNote(filePath)
note.title = noteInFS.title
note.content = noteInFS.content
if (filePath !== config.defaultNotePath) {
note.createdAt = fsCreatedTime
note.createdAt = noteInFS.lastchangeAt
note.lastchangeAt = noteInFS.lastchangeAt
}
}
}
Expand Down Expand Up @@ -196,6 +198,29 @@ module.exports = function (sequelize, DataTypes) {
})
})
}

async function syncNote (noteInFS, note) {
const contentLength = noteInFS.content.length

let note2 = await note.update({
title: noteInFS.title,
content: noteInFS.content,
lastchangeAt: noteInFS.lastchangeAt
})
const revision = await sequelize.models.Revision.saveNoteRevisionAsync(note2)
// update authorship on after making revision of docs
const patch = dmp.patch_fromText(revision.patch)
const operations = Note.transformPatchToOperations(patch, contentLength)
let authorship = note2.authorship
for (let i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
}
note2 = await note.update({
authorship: authorship
})
return note2.id
}

Note.parseNoteId = function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {
Expand All @@ -204,65 +229,35 @@ module.exports = function (sequelize, DataTypes) {
where: {
alias: noteId
}
}).then(function (note) {
if (note) {
const filePath = path.join(config.docsPath, noteId + '.md')
if (Note.checkFileExist(filePath)) {
// if doc in filesystem have newer modified time than last change time
// then will update the doc in db
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
var body = fs.readFileSync(filePath, 'utf8')
var contentLength = body.length
var title = Note.parseNoteTitle(body)
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
note.update({
title: title,
content: body,
lastchangeAt: fsModifiedTime
}).then(function (note) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) return _callback(err, null)
// update authorship on after making revision of docs
var patch = dmp.patch_fromText(revision.patch)
var operations = Note.transformPatchToOperations(patch, contentLength)
var authorship = note.authorship
for (let i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
}
note.update({
authorship: authorship
}).then(function (note) {
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
})
}).catch(function (err) {
return _callback(err, null)
})
}).then(async function (note) {
const filePath = path.join(config.docsPath, path.basename(noteId) + '.md')
if (Note.checkFileExist(filePath)) {
try {
if (note) {
// if doc in filesystem have newer modified time than last change time
// then will update the doc in db
const noteInFS = readFileSystemNote(filePath)
if (shouldSyncNote(note, noteInFS)) {
const noteId = await syncNote(noteInFS, note)
return callback(null, noteId)
}
} else {
// create new note with alias, and will sync md file in beforeCreateHook
const note = await Note.create({
alias: noteId,
owner: null,
permission: 'locked'
})
return callback(null, note.id)
}
} else {
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 (err) {
return _callback(err, null)
}
}
if (!note) {
return _callback(null, null)
}
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
Expand Down Expand Up @@ -589,5 +584,21 @@ module.exports = function (sequelize, DataTypes) {
return operations
}

function readFileSystemNote (filePath) {
const fsModifiedTime = moment(fs.statSync(filePath).mtime)
const content = fs.readFileSync(filePath, 'utf8')

return {
lastchangeAt: fsModifiedTime,
title: Note.parseNoteTitle(content),
content: content
}
}

function shouldSyncNote (note, noteInFS) {
const dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
return noteInFS.lastchangeAt.isAfter(dbModifiedTime) && note.content !== noteInFS.content
}

return Note
}
2 changes: 2 additions & 0 deletions lib/models/revision.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var moment = require('moment')
var childProcess = require('child_process')
var shortId = require('shortid')
var path = require('path')
var util = require('util')

var Op = Sequelize.Op

Expand Down Expand Up @@ -296,6 +297,7 @@ module.exports = function (sequelize, DataTypes) {
return callback(err, null)
})
}
Revision.saveNoteRevisionAsync = util.promisify(Revision.saveNoteRevision)
Revision.finishSaveNoteRevision = function (note, revision, callback) {
note.update({
savedAt: revision.updatedAt
Expand Down
10 changes: 6 additions & 4 deletions lib/note/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ const deleteNote = async (req, res) => {
}

const updateNote = async (req, res) => {
if (req.isAuthenticated()) {
if (req.isAuthenticated() || config.allowAnonymousEdits) {
const noteId = await Note.parseNoteIdAsync(req.params.noteId)
try {
const note = await Note.findOne({
Expand All @@ -294,7 +294,7 @@ const updateNote = async (req, res) => {
lastchangeAt: now,
authorship: [
[
req.user.id,
req.isAuthenticated() ? req.user.id : null,
0,
content.length,
now,
Expand All @@ -308,7 +308,9 @@ const updateNote = async (req, res) => {
return errorInternalError(req, res)
}

updateHistory(req.user.id, note.id, content)
if (req.isAuthenticated()) {
updateHistory(req.user.id, noteId, content)
}

Revision.saveNoteRevision(note, (err, revision) => {
if (err) {
Expand All @@ -321,7 +323,7 @@ const updateNote = async (req, res) => {
})
})
} catch (err) {
logger.error(err)
logger.error(err.stack)
logger.error('Update note failed: Internal Error.')
return errorInternalError(req, res)
}
Expand Down
4 changes: 3 additions & 1 deletion lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ function errorForbidden (req, res) {
if (req.user) {
responseError(res, '403', 'Forbidden', 'oh no.')
} else {
var nextURL = new URL('', config.serverURL)
nextURL.search = new URLSearchParams({ next: req.originalUrl })
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
res.redirect(config.serverURL + '/')
res.redirect(nextURL.toString())
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ exports.getConfig = (req, res) => {
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
defaultUseHardbreak: config.defaultUseHardbreak,
linkifyHeaderStyle: config.linkifyHeaderStyle,
useCDN: config.useCDN
useCDN: config.useCDN,
defaultTocDepth: config.defaultTocDepth
}
res.set({
'Cache-Control': 'private', // only cache by client
Expand Down
Loading

0 comments on commit 2e468db

Please sign in to comment.