Skip to content

Commit

Permalink
Merge pull request #1835 from hackmdio/release/2.5.0
Browse files Browse the repository at this point in the history
Release 2.5.0
  • Loading branch information
jackycute authored Dec 26, 2023
2 parents 5d84066 + afe49f4 commit 78e6663
Show file tree
Hide file tree
Showing 34 changed files with 4,013 additions and 2,415 deletions.
2 changes: 1 addition & 1 deletion .buildpacks
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
https://github.com/alex88/heroku-buildpack-vips
https://github.com/Scalingo/apt-buildpack
https://github.com/Scalingo/nodejs-buildpack
4 changes: 2 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# [Choice] Node.js version: 16, 14, 12
ARG VARIANT=12-buster
# [Choice] Node.js version: 16, 14
ARG VARIANT=14-buster
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

# [Optional] Uncomment this section to install additional OS packages.
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
VARIANT: 12-buster
VARIANT: 14-buster
environment:
- CMD_DB_URL=postgres://codimd:codimd@localhost/codimd
- CMD_USECDN=false
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
node-version: [14.x, 16.x]

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -39,9 +39,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
name: Use Node.js 12
name: Use Node.js 14
with:
node-version: 12
node-version: 14
check-latest: true
- name: Install doctoc-check
run: |
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v10.20.1
v16.20.2
1 change: 1 addition & 0 deletions Aptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
libvips-dev
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ CodiMD is a service that runs on Node.js, while users use the service through br
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" /> Chrome >= 47, Chrome for Android >= 47
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" /> Safari >= 9, iOS Safari >= 8.4
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" /> Firefox >= 44
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" /> IE >= 9, Edge >= 12
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" /> Edge >= 12
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" /> Opera >= 34, Opera Mini not supported
- Android Browser >= 4.4

Expand Down
8 changes: 8 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@
"description": "GitHub API client secret",
"required": false
},
"CMD_GITHUB_ORGANIZATIONS": {
"description": "GitHub whitelist of orgs",
"required": false
},
"CMD_GITHUB_SCOPES": {
"description": "GitHub OAuth API scopes",
"required": false
},
"CMD_BITBUCKET_CLIENTID": {
"description": "Bitbucket API client id",
"required": false
Expand Down
4 changes: 3 additions & 1 deletion config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
},
"github": {
"clientID": "change this",
"clientSecret": "change this"
"clientSecret": "change this",
"organizations": ["names of github organizations allowed, optional"],
"scopes": ["defaults to 'read:user' scope for auth user"]
},
"gitlab": {
"baseURL": "change this",
Expand Down
37 changes: 35 additions & 2 deletions lib/auth/github/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
'use strict'

const Router = require('express').Router
const request = require('request')
const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const { InternalOAuthError } = require('passport-oauth2')
const config = require('../../config')
const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { URL } = require('url')
const { promisify } = require('util')

const rp = promisify(request)

const githubAuth = module.exports = Router()

Expand All @@ -15,20 +20,48 @@ function githubUrl (path) {
}

passport.use(new GithubStrategy({
scope: (config.github.organizations ? config.github.scopes.concat(['read:org']) : config.github.scope),
clientID: config.github.clientID,
clientSecret: config.github.clientSecret,
callbackURL: config.serverURL + '/auth/github/callback',
authorizationURL: githubUrl('login/oauth/authorize'),
tokenURL: githubUrl('login/oauth/access_token'),
userProfileURL: githubUrl('api/v3/user')
}, passportGeneralCallback))
}, async (accessToken, refreshToken, profile, done) => {
if (!config.github.organizations) {
return passportGeneralCallback(accessToken, refreshToken, profile, done)
}
const { statusCode, body: data } = await rp({
url: `https://api.github.com/user/orgs`,
method: 'GET',
json: true,
timeout: 2000,
headers: {
Authorization: `token ${accessToken}`,
'User-Agent': 'nodejs-http'
}
})
if (statusCode !== 200) {
return done(InternalOAuthError(
`Failed to query organizations for user: ${profile.username}`
))
}
const orgs = data.map(({ login }) => login)
for (const org of orgs) {
if (config.github.organizations.includes(org)) {
return passportGeneralCallback(accessToken, refreshToken, profile, done)
}
}
return done(InternalOAuthError(
`User orgs not whitelisted: ${profile.username} (${orgs.join(',')})`
))
}))

githubAuth.get('/auth/github', function (req, res, next) {
setReturnToFromReferer(req)
passport.authenticate('github')(req, res, next)
})

// github auth callback
githubAuth.get('/auth/github/callback',
passport.authenticate('github', {
successReturnToOrRedirect: config.serverURL + '/',
Expand Down
10 changes: 7 additions & 3 deletions lib/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ if (config.isEmailEnable) authRouter.use(require('./email'))
if (config.isOpenIDEnable) authRouter.use(require('./openid'))

// logout
authRouter.get('/logout', function (req, res) {
authRouter.get('/logout', function (req, res, next) {
if (config.debug && req.isAuthenticated()) {
logger.debug('user logout: ' + req.user.id)
}
req.logout()
res.redirect(config.serverURL + '/')

req.logout((err) => {
if (err) { return next(err) }

res.redirect(config.serverURL + '/')
})
})
14 changes: 13 additions & 1 deletion lib/auth/oauth2/strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../config')

function parseProfile (data) {
const id = extractProfileAttribute(data, config.oauth2.userProfileIdAttr)
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
Expand All @@ -14,7 +15,7 @@ function parseProfile (data) {
}

return {
id: username,
id: id || username,
username: username,
displayName: displayName,
email: email,
Expand All @@ -41,6 +42,16 @@ function extractProfileAttribute (data, path) {
return data
}

function checkAuthorization (data, done) {
const roles = extractProfileAttribute(data, config.oauth2.rolesClaim)

if (config.oauth2.accessRole && roles) {
if (!roles.includes(config.oauth2.accessRole)) {
return done('Permission denied', null)
}
}
}

class OAuth2CustomStrategy extends Strategy {
constructor (options, verify) {
options.customHeaders = options.customHeaders || {}
Expand All @@ -59,6 +70,7 @@ class OAuth2CustomStrategy extends Strategy {
let profile, json
try {
json = JSON.parse(body)
checkAuthorization(json, done)
profile = parseProfile(json)
} catch (ex) {
return done(new InternalOAuthError('Failed to parse user profile' + ex.toString()))
Expand Down
4 changes: 3 additions & 1 deletion lib/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ module.exports = {
github: {
enterpriseURL: undefined, // if you use github.com, not need to specify
clientID: undefined,
clientSecret: undefined
clientSecret: undefined,
organizations: [],
scopes: ['read:user']
},
gitlab: {
baseURL: undefined,
Expand Down
7 changes: 6 additions & 1 deletion lib/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ module.exports = {
github: {
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
clientID: process.env.CMD_GITHUB_CLIENTID,
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET,
organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS),
scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES)
},
bitbucket: {
clientID: process.env.CMD_BITBUCKET_CLIENTID,
Expand All @@ -96,6 +98,9 @@ module.exports = {
userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL,
scope: process.env.CMD_OAUTH2_SCOPE,
state: process.env.CMD_OAUTH2_STATE,
rolesClaim: process.env.CMD_OAUTH2_ROLES_CLAIM,
accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE,
userProfileIdAttr: process.env.CMD_OAUTH2_USER_PROFILE_ID_ATTR,
userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,
Expand Down
31 changes: 18 additions & 13 deletions lib/imageRouter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,42 @@ const response = require('../response')
const imageRouter = module.exports = Router()

function checkImageValid (filepath) {
const buffer = readChunk.sync(filepath, 0, 12)
/** @type {{ ext: string, mime: string } | null} */
const mimetypeFromBuf = imageType(buffer)
const mimeTypeFromExt = mime.lookup(path.extname(filepath))
try {
const buffer = readChunk.sync(filepath, 0, 12)
/** @type {{ ext: string, mime: string } | null} */
const mimetypeFromBuf = imageType(buffer)
const mimeTypeFromExt = mime.lookup(path.extname(filepath))

return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
} catch (err) {
logger.error(err)
return false
}
}

// upload image
imageRouter.post('/uploadimage', function (req, res) {
var form = new formidable.IncomingForm()

form.keepExtensions = true
var form = new formidable.IncomingForm({
keepExtensions: true
})

form.parse(req, function (err, fields, files) {
if (err || !files.image || !files.image.path) {
if (err || !files.image || !files.image.filepath) {
response.errorForbidden(req, res)
} else {
if (config.debug) {
logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image))
}

if (!checkImageValid(files.image.path)) {
if (!checkImageValid(files.image.filepath)) {
return response.errorForbidden(req, res)
}

const uploadProvider = require('./' + config.imageUploadType)
uploadProvider.uploadImage(files.image.path, function (err, url) {
uploadProvider.uploadImage(files.image.filepath, function (err, url) {
// remove temporary upload file, and ignore any error
fs.unlink(files.image.path, () => {})
fs.unlink(files.image.filepath, () => {})
if (err !== null) {
logger.error(err)
return res.status(500).end('upload image error')
Expand Down
11 changes: 7 additions & 4 deletions lib/imageRouter/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ exports.uploadImage = function (imagePath, callback) {
const command = new PutObjectCommand(params)

s3.send(command).then(data => {
let s3Endpoint = 's3.amazonaws.com'
// default scheme settings to https
let s3Endpoint = 'https://s3.amazonaws.com'
if (config.s3.region && config.s3.region !== 'us-east-1') {
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
}
// rewrite endpoint from config
if (config.s3.endpoint) {
s3Endpoint = config.s3.endpoint
} else if (config.s3.region && config.s3.region !== 'us-east-1') {
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
}
callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`)
callback(null, `${s3Endpoint}/${config.s3bucket}/${params.Key}`)
}).catch(err => {
if (err) {
callback(new Error(err), null)
Expand Down
2 changes: 1 addition & 1 deletion lib/note/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ const updateNote = async (req, res) => {
}

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

Revision.saveNoteRevision(note, (err, revision) => {
Expand Down
9 changes: 6 additions & 3 deletions lib/note/noteActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,17 @@ async function actionPandoc (req, res, note) {
var path = config.tmpPath + '/' + Date.now()
content = content.replace(/\]\(\//g, '](' + url + '/')

// TODO: check export type
const { exportType } = req.query
const contentType = outputFormats[exportType]

try {
// TODO: timeout rejection
if (!contentType) {
return res.sendStatus(400)
}

await pandoc.convertToFile(content, 'markdown', exportType, path, [
'--metadata', `title=${title}`
'--metadata', `title=${title}`, '--sandbox'
])

var stream = fs.createReadStream(path)
Expand All @@ -149,7 +152,7 @@ async function actionPandoc (req, res, note) {
// Ideally this should strip them
res.setHeader('Content-disposition', `attachment; filename="${filename}.${exportType}"`)
res.setHeader('Cache-Control', 'private')
res.setHeader('Content-Type', `${outputFormats[exportType]}; charset=UTF-8`)
res.setHeader('Content-Type', `${contentType}; charset=UTF-8`)
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
stream.pipe(res)
} catch (err) {
Expand Down
8 changes: 4 additions & 4 deletions locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"No history": "Pas d'historique",
"Import from browser": "Importer depuis le navigateur",
"Releases": "Versions",
"Are you sure?": "Ëtes-vous sûr ?",
"Are you sure?": "Êtes-vous sûr ?",
"Do you really want to delete this note?": "Voulez-vous vraiment supprimer cette note ?",
"All users will lose their connection.": "Tous les utilisateurs perdront leur connexion.",
"Cancel": "Annuler",
Expand Down Expand Up @@ -73,7 +73,7 @@
"Syntax": "Syntaxe",
"Header": "En-tête",
"Unordered List": "Liste à puce",
"Ordered List": "List numérotée",
"Ordered List": "Liste numérotée",
"Todo List": "Liste de tâches",
"Blockquote": "Citation",
"Bold font": "Gras",
Expand All @@ -84,7 +84,7 @@
"Link": "Lien",
"Image": "Image",
"Code": "Code",
"Externals": "Externes",
"Externals": "Contenus externes",
"This is a alert area.": "Ceci est un texte d'alerte.",
"Revert": "Revenir en arrière",
"Import from clipboard": "Importer depuis le presse-papier",
Expand Down Expand Up @@ -116,4 +116,4 @@
"Source Code": "Code source",
"Register": "S'enregistrer",
"Powered by %s": "Propulsé par %s"
}
}
Loading

0 comments on commit 78e6663

Please sign in to comment.