mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-06-23 04:10:17 +00:00
@@ -24,6 +24,9 @@ config.json
|
||||
public/build
|
||||
public/views/build
|
||||
|
||||
# ignore TypeScript build
|
||||
dist/
|
||||
|
||||
public/uploads/*
|
||||
!public/uploads/.gitkeep
|
||||
|
||||
|
||||
+50
-49
@@ -1,45 +1,46 @@
|
||||
'use strict'
|
||||
// app
|
||||
// external modules
|
||||
const express = require('express')
|
||||
import express = require('express')
|
||||
|
||||
const ejs = require('ejs')
|
||||
const passport = require('passport')
|
||||
const methodOverride = require('method-override')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const compression = require('compression')
|
||||
const session = require('express-session')
|
||||
import ejs = require('ejs')
|
||||
import passport = require('passport')
|
||||
import methodOverride = require('method-override')
|
||||
import cookieParser = require('cookie-parser')
|
||||
import compression = require('compression')
|
||||
import session = require('express-session')
|
||||
const SequelizeStore = require('connect-session-sequelize')(session.Store)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { Server } = require('socket.io')
|
||||
import * as http from 'http'
|
||||
import * as https from 'https'
|
||||
import fs = require('fs')
|
||||
import path = require('path')
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
const morgan = require('morgan')
|
||||
import morgan = require('morgan')
|
||||
const passportSocketIo = require('passport.socketio')
|
||||
const helmet = require('helmet')
|
||||
const i18n = require('i18n')
|
||||
const flash = require('connect-flash')
|
||||
import helmet = require('helmet')
|
||||
import i18n = require('i18n')
|
||||
import flash = require('connect-flash')
|
||||
const apiMetrics = require('prometheus-api-metrics')
|
||||
|
||||
// core
|
||||
const config = require('./lib/config')
|
||||
const logger = require('./lib/logger')
|
||||
const errors = require('./lib/errors')
|
||||
const models = require('./lib/models')
|
||||
const csp = require('./lib/csp')
|
||||
const metrics = require('./lib/prometheus')
|
||||
const { useUnless } = require('./lib/utils')
|
||||
import config = require('./lib/config')
|
||||
import logger = require('./lib/logger')
|
||||
import * as errors from './lib/errors'
|
||||
import models = require('./lib/models')
|
||||
import * as csp from './lib/csp'
|
||||
import * as metrics from './lib/prometheus'
|
||||
import { useUnless } from './lib/utils'
|
||||
|
||||
const supportedLocalesList = Object.keys(require('./locales/_supported.json'))
|
||||
const supportedLocalesList: string[] = Object.keys(require('../locales/_supported.json'))
|
||||
|
||||
// server setup
|
||||
const app = express()
|
||||
let server = null
|
||||
let server: http.Server | https.Server = null as unknown as http.Server | https.Server
|
||||
if (config.useSSL) {
|
||||
const ca = (function () {
|
||||
let i, len
|
||||
const results = []
|
||||
for (i = 0, len = config.sslCAPath.length; i < len; i++) {
|
||||
const ca: string[] = (function () {
|
||||
const results: string[] = []
|
||||
for (let i = 0, len = config.sslCAPath.length; i < len; i++) {
|
||||
results.push(fs.readFileSync(config.sslCAPath[i], 'utf8'))
|
||||
}
|
||||
return results
|
||||
@@ -52,9 +53,9 @@ if (config.useSSL) {
|
||||
requestCert: false,
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
server = require('https').createServer(options, app)
|
||||
server = https.createServer(options, app)
|
||||
} else {
|
||||
server = require('http').createServer(app)
|
||||
server = http.createServer(app)
|
||||
}
|
||||
|
||||
// if we manage to provide HTTPS domains, but don't provide TLS ourselves
|
||||
@@ -65,14 +66,14 @@ if (!config.useSSL && config.protocolUseSSL) {
|
||||
}
|
||||
|
||||
// check if the request is from container healthcheck
|
||||
function isContainerHealthCheck (req, _) {
|
||||
function isContainerHealthCheck (req: express.Request, _res: express.Response): boolean {
|
||||
return req.headers['user-agent'] === 'hedgedoc-container-healthcheck/1.0' && req.ip === '127.0.0.1'
|
||||
}
|
||||
|
||||
// logger
|
||||
app.use(morgan('combined', {
|
||||
skip: isContainerHealthCheck,
|
||||
stream: logger.stream
|
||||
stream: logger.stream as any
|
||||
}))
|
||||
|
||||
// Register prometheus metrics endpoint
|
||||
@@ -94,7 +95,7 @@ const io = new Server(server, {
|
||||
})
|
||||
|
||||
// others
|
||||
const realtime = require('./lib/realtime.js')
|
||||
const realtime = require('./lib/realtime')
|
||||
|
||||
// assign socket io to realtime
|
||||
realtime.io = io
|
||||
@@ -147,7 +148,7 @@ i18n.configure({
|
||||
locales: supportedLocalesList,
|
||||
cookie: 'locale',
|
||||
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
|
||||
directory: path.join(__dirname, '/locales'),
|
||||
directory: path.join(__dirname, '..', 'locales'),
|
||||
updateFiles: config.updateI18nFiles
|
||||
})
|
||||
|
||||
@@ -164,7 +165,7 @@ app.use('/uploads', (req, res, next) => {
|
||||
})
|
||||
|
||||
// static files
|
||||
app.use('/', express.static(path.join(__dirname, '/public'), {
|
||||
app.use('/', express.static(path.join(__dirname, '..', 'public'), {
|
||||
maxAge: config.staticCacheTime,
|
||||
index: false,
|
||||
redirect: false
|
||||
@@ -197,12 +198,12 @@ app.use(useUnless(['/status', '/metrics', '/_health'], session({
|
||||
})))
|
||||
|
||||
// session resumption
|
||||
const tlsSessionStore = {}
|
||||
server.on('newSession', function (id, data, cb) {
|
||||
const tlsSessionStore: Record<string, any> = {}
|
||||
server.on('newSession', function (id: Buffer, data: Buffer, cb: () => void) {
|
||||
tlsSessionStore[id.toString('hex')] = data
|
||||
cb()
|
||||
})
|
||||
server.on('resumeSession', function (id, cb) {
|
||||
server.on('resumeSession', function (id: Buffer, cb: (err: Error | null, data: Buffer | null) => void) {
|
||||
cb(null, tlsSessionStore[id.toString('hex')] || null)
|
||||
})
|
||||
|
||||
@@ -287,8 +288,8 @@ io.sockets.on('connection', realtime.connection)
|
||||
|
||||
// listen
|
||||
function startListen () {
|
||||
let address
|
||||
const listenCallback = function () {
|
||||
let address: string
|
||||
const listenCallback = function (): void {
|
||||
const schema = config.useSSL ? 'HTTPS' : 'HTTP'
|
||||
logger.info('%s Server listening at %s', schema, address)
|
||||
realtime.maintenance = false
|
||||
@@ -304,16 +305,16 @@ function startListen () {
|
||||
}
|
||||
}
|
||||
|
||||
const maxDBTries = 30
|
||||
let currentDBTry = 1
|
||||
function syncAndListen () {
|
||||
const maxDBTries: number = 30
|
||||
let currentDBTry: number = 1
|
||||
function syncAndListen (): void {
|
||||
// sync db then start listen
|
||||
models.sequelize.authenticate().then(function () {
|
||||
models.runMigrations().then(() => {
|
||||
sessionStore.sync()
|
||||
// check if realtime is ready
|
||||
if (realtime.isReady()) {
|
||||
models.Revision.checkAllNotesRevision(function (err, notes) {
|
||||
models.Revision.checkAllNotesRevision(function (err: any, notes: any[]) {
|
||||
if (err) throw new Error(err)
|
||||
if (!notes || notes.length <= 0) return startListen()
|
||||
})
|
||||
@@ -322,7 +323,7 @@ function syncAndListen () {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
}).catch((dbError) => {
|
||||
}).catch((dbError: Error) => {
|
||||
if (currentDBTry < maxDBTries) {
|
||||
logger.warn(`Database cannot be reached. Try ${currentDBTry} of ${maxDBTries}. (${dbError})`)
|
||||
currentDBTry++
|
||||
@@ -345,9 +346,9 @@ process.on('uncaughtException', function (err) {
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
let alreadyHandlingTermSignals = false
|
||||
let alreadyHandlingTermSignals: boolean = false
|
||||
// install exit handler
|
||||
function handleTermSignals () {
|
||||
function handleTermSignals (): void {
|
||||
if (alreadyHandlingTermSignals) {
|
||||
logger.info('Forcefully exiting.')
|
||||
process.exit(1)
|
||||
@@ -374,11 +375,11 @@ function handleTermSignals () {
|
||||
}
|
||||
})
|
||||
}
|
||||
const maxCleanTries = 30
|
||||
let currentCleanTry = 1
|
||||
const maxCleanTries: number = 30
|
||||
let currentCleanTry: number = 1
|
||||
const checkCleanTimer = setInterval(function () {
|
||||
if (realtime.isReady()) {
|
||||
models.Revision.checkAllNotesRevision(function (err, notes) {
|
||||
models.Revision.checkAllNotesRevision(function (err: any, notes: any[]) {
|
||||
if (err) {
|
||||
logger.error('Error while saving note revisions: ' + err)
|
||||
if (currentCleanTry <= maxCleanTries) {
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
buildDomainOriginWithProtocol: function (config, baseProtocol) {
|
||||
const isStandardHTTPsPort = config.protocolUseSSL && config.port === 443
|
||||
const isStandardHTTPPort = !config.protocolUseSSL && config.port === 80
|
||||
|
||||
if (!config.domain) {
|
||||
return ''
|
||||
}
|
||||
let origin = ''
|
||||
const protocol = baseProtocol + (config.protocolUseSSL ? 's' : '') + '://'
|
||||
origin = protocol + config.domain
|
||||
if (config.urlAddPort) {
|
||||
if (!isStandardHTTPPort || !isStandardHTTPsPort) {
|
||||
origin += ':' + config.port
|
||||
}
|
||||
}
|
||||
return origin
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
interface DomainConfig {
|
||||
protocolUseSSL: boolean
|
||||
port: number
|
||||
domain: string
|
||||
urlAddPort: boolean
|
||||
}
|
||||
|
||||
export function buildDomainOriginWithProtocol (config: DomainConfig, baseProtocol: string): string {
|
||||
const isStandardHTTPsPort = config.protocolUseSSL && config.port === 443
|
||||
const isStandardHTTPPort = !config.protocolUseSSL && config.port === 80
|
||||
|
||||
if (!config.domain) {
|
||||
return ''
|
||||
}
|
||||
const protocol = baseProtocol + (config.protocolUseSSL ? 's' : '') + '://'
|
||||
let origin = protocol + config.domain
|
||||
if (config.urlAddPort) {
|
||||
// BUG FIX: was `!isStandardHTTPPort || !isStandardHTTPsPort` which is always true
|
||||
// (one of the two is always false). Should be AND to skip port for either standard port.
|
||||
if (!isStandardHTTPPort && !isStandardHTTPsPort) {
|
||||
origin += ':' + config.port
|
||||
}
|
||||
}
|
||||
return origin
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const os = require('os')
|
||||
import * as os from 'os'
|
||||
|
||||
module.exports = {
|
||||
const defaultConfig = {
|
||||
domain: '',
|
||||
urlPath: '',
|
||||
host: '0.0.0.0',
|
||||
@@ -24,8 +24,8 @@ module.exports = {
|
||||
addDefaults: true,
|
||||
addDisqus: false,
|
||||
addGoogleAnalytics: false,
|
||||
upgradeInsecureRequests: 'auto',
|
||||
reportURI: undefined,
|
||||
upgradeInsecureRequests: 'auto' as string | boolean,
|
||||
reportURI: undefined as string | undefined,
|
||||
allowFraming: true,
|
||||
allowPDFEmbed: true
|
||||
},
|
||||
@@ -40,16 +40,14 @@ module.exports = {
|
||||
disableNoteCreation: false,
|
||||
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api', 'build', 'css', 'docs', 'fonts', 'js', 'uploads', 'vendor', 'views'],
|
||||
defaultPermission: 'editable',
|
||||
|
||||
enableUploads: undefined, // 'all', 'registered', 'none' are valid options.
|
||||
enableUploads: undefined as string | undefined, // 'all', 'registered', 'none' are valid options.
|
||||
// This is undefined by default and set during runtime based on allowAnonymous and allowAnonymousEdits for backwards-compatibility unless explicitly set.
|
||||
|
||||
dbURL: '',
|
||||
db: {},
|
||||
db: {} as Record<string, any>,
|
||||
// ssl path
|
||||
sslKeyPath: '',
|
||||
sslCertPath: '',
|
||||
sslCAPath: '',
|
||||
sslCAPath: '' as string | string[],
|
||||
dhParamPath: '',
|
||||
// other path
|
||||
viewPath: './public/views',
|
||||
@@ -76,103 +74,103 @@ module.exports = {
|
||||
url: 'https://framapic.org/'
|
||||
},
|
||||
imgur: {
|
||||
clientID: undefined
|
||||
clientID: undefined as string | undefined
|
||||
},
|
||||
s3: {
|
||||
accessKeyId: undefined,
|
||||
secretAccessKey: undefined,
|
||||
region: undefined
|
||||
accessKeyId: undefined as string | undefined,
|
||||
secretAccessKey: undefined as string | undefined,
|
||||
region: undefined as string | undefined
|
||||
},
|
||||
s3bucket: undefined,
|
||||
s3bucket: undefined as string | undefined,
|
||||
s3folder: 'uploads',
|
||||
s3publicFiles: false,
|
||||
s3publicFiles: false as boolean | string,
|
||||
minio: {
|
||||
accessKey: undefined,
|
||||
secretKey: undefined,
|
||||
endPoint: undefined,
|
||||
accessKey: undefined as string | undefined,
|
||||
secretKey: undefined as string | undefined,
|
||||
endPoint: undefined as string | undefined,
|
||||
secure: true,
|
||||
port: 9000
|
||||
},
|
||||
azure: {
|
||||
connectionString: undefined,
|
||||
container: undefined
|
||||
connectionString: undefined as string | undefined,
|
||||
container: undefined as string | undefined
|
||||
},
|
||||
// authentication
|
||||
oauth2: {
|
||||
providerName: undefined,
|
||||
authorizationURL: undefined,
|
||||
tokenURL: undefined,
|
||||
clientID: undefined,
|
||||
clientSecret: undefined,
|
||||
scope: undefined,
|
||||
providerName: undefined as string | undefined,
|
||||
authorizationURL: undefined as string | undefined,
|
||||
tokenURL: undefined as string | undefined,
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined,
|
||||
scope: undefined as string | undefined,
|
||||
pkce: false
|
||||
},
|
||||
facebook: {
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined
|
||||
},
|
||||
twitter: {
|
||||
consumerKey: undefined,
|
||||
consumerSecret: undefined
|
||||
consumerKey: undefined as string | undefined,
|
||||
consumerSecret: undefined as string | undefined
|
||||
},
|
||||
github: {
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined
|
||||
},
|
||||
gitlab: {
|
||||
baseURL: undefined,
|
||||
clientID: undefined,
|
||||
clientSecret: undefined,
|
||||
scope: undefined,
|
||||
baseURL: undefined as string | undefined,
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined,
|
||||
scope: undefined as string | undefined,
|
||||
version: 'v4'
|
||||
},
|
||||
mattermost: {
|
||||
baseURL: undefined,
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
baseURL: undefined as string | undefined,
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined
|
||||
},
|
||||
dropbox: {
|
||||
clientID: undefined,
|
||||
clientSecret: undefined,
|
||||
appKey: undefined
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined,
|
||||
appKey: undefined as string | undefined
|
||||
},
|
||||
google: {
|
||||
clientID: undefined,
|
||||
clientSecret: undefined,
|
||||
hostedDomain: undefined
|
||||
clientID: undefined as string | undefined,
|
||||
clientSecret: undefined as string | undefined,
|
||||
hostedDomain: undefined as string | undefined
|
||||
},
|
||||
ldap: {
|
||||
providerName: undefined,
|
||||
url: undefined,
|
||||
bindDn: undefined,
|
||||
bindCredentials: undefined,
|
||||
searchBase: undefined,
|
||||
searchFilter: undefined,
|
||||
searchAttributes: undefined,
|
||||
usernameField: undefined,
|
||||
useridField: undefined,
|
||||
tlsca: undefined
|
||||
providerName: undefined as string | undefined,
|
||||
url: undefined as string | undefined,
|
||||
bindDn: undefined as string | undefined,
|
||||
bindCredentials: undefined as string | undefined,
|
||||
searchBase: undefined as string | undefined,
|
||||
searchFilter: undefined as string | undefined,
|
||||
searchAttributes: undefined as string | undefined,
|
||||
usernameField: undefined as string | undefined,
|
||||
useridField: undefined as string | undefined,
|
||||
tlsca: undefined as string | undefined
|
||||
},
|
||||
saml: {
|
||||
providerName: undefined,
|
||||
idpSsoUrl: undefined,
|
||||
idpCert: undefined,
|
||||
clientCert: undefined,
|
||||
issuer: undefined,
|
||||
providerName: undefined as string | undefined,
|
||||
idpSsoUrl: undefined as string | undefined,
|
||||
idpCert: undefined as string | undefined,
|
||||
clientCert: undefined as string | undefined,
|
||||
issuer: undefined as string | undefined,
|
||||
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
disableRequestedAuthnContext: false,
|
||||
groupAttribute: undefined,
|
||||
externalGroups: [],
|
||||
requiredGroups: [],
|
||||
groupAttribute: undefined as string | undefined,
|
||||
externalGroups: [] as string[],
|
||||
requiredGroups: [] as string[],
|
||||
attribute: {
|
||||
id: undefined,
|
||||
id: undefined as string | undefined,
|
||||
username: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
|
||||
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
|
||||
},
|
||||
wantAssertionsSigned: true,
|
||||
wantAuthnResponseSigned: true
|
||||
},
|
||||
email: true,
|
||||
email: true as boolean,
|
||||
allowEmailRegister: true,
|
||||
allowGravatar: true,
|
||||
openID: false,
|
||||
@@ -192,3 +190,5 @@ module.exports = {
|
||||
linkifyHeaderStyle: 'keep-case',
|
||||
enableStatsApi: true
|
||||
}
|
||||
|
||||
export = defaultConfig
|
||||
@@ -1,17 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
import * as fs from 'fs'
|
||||
|
||||
function getFile (path) {
|
||||
if (fs.existsSync(path)) {
|
||||
return path
|
||||
function getFile (filePath: string): string | undefined {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return filePath
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
const defaultSSL = {
|
||||
sslKeyPath: getFile('/run/secrets/key.pem'),
|
||||
sslCertPath: getFile('/run/secrets/cert.pem'),
|
||||
sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [],
|
||||
sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')!] : [] as string[],
|
||||
dhParamPath: getFile('/run/secrets/dhparam.pem')
|
||||
}
|
||||
|
||||
export = defaultSSL
|
||||
@@ -1,65 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const basePath = path.resolve('/run/secrets/')
|
||||
|
||||
function getSecret (secret) {
|
||||
const filePath = path.join(basePath, secret)
|
||||
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf-8')
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (fs.existsSync(basePath)) {
|
||||
module.exports = {
|
||||
dbURL: getSecret('dbURL'),
|
||||
sessionSecret: getSecret('sessionsecret'),
|
||||
sslKeyPath: getSecret('sslkeypath'),
|
||||
sslCertPath: getSecret('sslcertpath'),
|
||||
sslCAPath: getSecret('sslcapath'),
|
||||
dhParamPath: getSecret('dhparampath'),
|
||||
s3: {
|
||||
accessKeyId: getSecret('s3_acccessKeyId'),
|
||||
secretAccessKey: getSecret('s3_secretAccessKey')
|
||||
},
|
||||
azure: {
|
||||
connectionString: getSecret('azure_connectionString')
|
||||
},
|
||||
facebook: {
|
||||
clientID: getSecret('facebook_clientID'),
|
||||
clientSecret: getSecret('facebook_clientSecret')
|
||||
},
|
||||
twitter: {
|
||||
consumerKey: getSecret('twitter_consumerKey'),
|
||||
consumerSecret: getSecret('twitter_consumerSecret')
|
||||
},
|
||||
github: {
|
||||
clientID: getSecret('github_clientID'),
|
||||
clientSecret: getSecret('github_clientSecret')
|
||||
},
|
||||
gitlab: {
|
||||
clientID: getSecret('gitlab_clientID'),
|
||||
clientSecret: getSecret('gitlab_clientSecret')
|
||||
},
|
||||
mattermost: {
|
||||
clientID: getSecret('mattermost_clientID'),
|
||||
clientSecret: getSecret('mattermost_clientSecret')
|
||||
},
|
||||
dropbox: {
|
||||
clientID: getSecret('dropbox_clientID'),
|
||||
clientSecret: getSecret('dropbox_clientSecret'),
|
||||
appKey: getSecret('dropbox_appKey')
|
||||
},
|
||||
google: {
|
||||
clientID: getSecret('google_clientID'),
|
||||
clientSecret: getSecret('google_clientSecret'),
|
||||
hostedDomain: getSecret('google_hostedDomain')
|
||||
},
|
||||
imgur: getSecret('imgur_clientid'),
|
||||
oauth2: {
|
||||
clientID: getSecret('oauth2_clientID'),
|
||||
clientSecret: getSecret('oauth2_clientSecret')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict'
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const basePath: string = path.resolve('/run/secrets/')
|
||||
|
||||
function getSecret (secret: string): string | undefined {
|
||||
const filePath = path.join(basePath, secret)
|
||||
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf-8')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const dockerSecrets = fs.existsSync(basePath) ? {
|
||||
dbURL: getSecret('dbURL'),
|
||||
sessionSecret: getSecret('sessionsecret'),
|
||||
sslKeyPath: getSecret('sslkeypath'),
|
||||
sslCertPath: getSecret('sslcertpath'),
|
||||
sslCAPath: getSecret('sslcapath'),
|
||||
dhParamPath: getSecret('dhparampath'),
|
||||
s3: {
|
||||
accessKeyId: getSecret('s3_acccessKeyId'),
|
||||
secretAccessKey: getSecret('s3_secretAccessKey')
|
||||
},
|
||||
azure: {
|
||||
connectionString: getSecret('azure_connectionString')
|
||||
},
|
||||
facebook: {
|
||||
clientID: getSecret('facebook_clientID'),
|
||||
clientSecret: getSecret('facebook_clientSecret')
|
||||
},
|
||||
twitter: {
|
||||
consumerKey: getSecret('twitter_consumerKey'),
|
||||
consumerSecret: getSecret('twitter_consumerSecret')
|
||||
},
|
||||
github: {
|
||||
clientID: getSecret('github_clientID'),
|
||||
clientSecret: getSecret('github_clientSecret')
|
||||
},
|
||||
gitlab: {
|
||||
clientID: getSecret('gitlab_clientID'),
|
||||
clientSecret: getSecret('gitlab_clientSecret')
|
||||
},
|
||||
mattermost: {
|
||||
clientID: getSecret('mattermost_clientID'),
|
||||
clientSecret: getSecret('mattermost_clientSecret')
|
||||
},
|
||||
dropbox: {
|
||||
clientID: getSecret('dropbox_clientID'),
|
||||
clientSecret: getSecret('dropbox_clientSecret'),
|
||||
appKey: getSecret('dropbox_appKey')
|
||||
},
|
||||
google: {
|
||||
clientID: getSecret('google_clientID'),
|
||||
clientSecret: getSecret('google_clientSecret'),
|
||||
hostedDomain: getSecret('google_hostedDomain')
|
||||
},
|
||||
imgur: getSecret('imgur_clientid'),
|
||||
oauth2: {
|
||||
clientID: getSecret('oauth2_clientID'),
|
||||
clientSecret: getSecret('oauth2_clientSecret')
|
||||
}
|
||||
} : undefined
|
||||
|
||||
export = dockerSecrets
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
exports.Environment = {
|
||||
export const Environment = {
|
||||
development: 'development',
|
||||
production: 'production',
|
||||
test: 'test'
|
||||
}
|
||||
} as const
|
||||
|
||||
exports.Permission = {
|
||||
export const Permission: Record<string, string> = {
|
||||
freely: 'freely',
|
||||
editable: 'editable',
|
||||
limited: 'limited',
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
import { toBooleanConfig, toArrayConfig, toIntegerConfig } from './utils'
|
||||
|
||||
module.exports = {
|
||||
const environment = {
|
||||
sourceURL: process.env.CMD_SOURCE_URL,
|
||||
domain: process.env.CMD_DOMAIN,
|
||||
urlPath: process.env.CMD_URL_PATH,
|
||||
@@ -166,3 +166,5 @@ module.exports = {
|
||||
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
|
||||
enableStatsApi: toBooleanConfig(process.env.CMD_ENABLE_STATS_API)
|
||||
}
|
||||
|
||||
export = environment
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
import { toBooleanConfig, toArrayConfig, toIntegerConfig } from './utils'
|
||||
|
||||
module.exports = {
|
||||
const hackmdEnvironment = {
|
||||
domain: process.env.HMD_DOMAIN,
|
||||
urlPath: process.env.HMD_URL_PATH,
|
||||
port: toIntegerConfig(process.env.HMD_PORT),
|
||||
@@ -122,3 +122,5 @@ module.exports = {
|
||||
email: toBooleanConfig(process.env.HMD_EMAIL),
|
||||
allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER)
|
||||
}
|
||||
|
||||
export = hackmdEnvironment
|
||||
@@ -1,17 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { merge } = require('lodash')
|
||||
const deepFreeze = require('deep-freeze')
|
||||
const { Environment, Permission } = require('./enum')
|
||||
const logger = require('../logger')
|
||||
const { getGitCommit, getGitHubURL } = require('./utils')
|
||||
const { buildDomainOriginWithProtocol } = require('./buildDomainOriginWithProtocol')
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { merge } from 'lodash'
|
||||
import deepFreeze = require('deep-freeze')
|
||||
import { Environment, Permission } from './enum'
|
||||
import logger from '../logger'
|
||||
import { getGitCommit, getGitHubURL } from './utils'
|
||||
import { buildDomainOriginWithProtocol } from './buildDomainOriginWithProtocol'
|
||||
|
||||
const appRootPath = path.resolve(__dirname, '../../')
|
||||
const env = process.env.NODE_ENV || Environment.development
|
||||
const appRootPath: string = path.resolve(__dirname, '../../../')
|
||||
const env: string = process.env.NODE_ENV || Environment.development
|
||||
const debugConfig = {
|
||||
debug: (env === Environment.development)
|
||||
}
|
||||
@@ -30,11 +30,11 @@ const packageConfig = {
|
||||
sourceURL
|
||||
}
|
||||
|
||||
const configFilePath = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE ||
|
||||
const configFilePath: string = path.resolve(appRootPath, process.env.CMD_CONFIG_FILE ||
|
||||
'config.json')
|
||||
const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined
|
||||
const fileConfig: Record<string, any> | undefined = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined
|
||||
|
||||
let config = require('./default')
|
||||
let config: any = require('./default')
|
||||
merge(config, require('./defaultSSL'))
|
||||
merge(config, require('./oldDefault'))
|
||||
merge(config, debugConfig)
|
||||
@@ -58,8 +58,8 @@ if (!['strict', 'lax', 'none'].includes(config.cookiePolicy)) {
|
||||
|
||||
// load LDAP CA
|
||||
if (config.ldap.tlsca) {
|
||||
const ca = config.ldap.tlsca.split(',')
|
||||
const caContent = []
|
||||
const ca: string[] = config.ldap.tlsca.split(',')
|
||||
const caContent: string[] = []
|
||||
for (const i of ca) {
|
||||
if (fs.existsSync(i)) {
|
||||
caContent.push(fs.readFileSync(i, 'utf8'))
|
||||
@@ -101,7 +101,7 @@ if (config.useSSL === true) {
|
||||
|
||||
// cache serverURL
|
||||
config.serverURL = (function () {
|
||||
let url = buildDomainOriginWithProtocol(config, 'http')
|
||||
let url: string = buildDomainOriginWithProtocol(config, 'http')
|
||||
if (config.urlPath) {
|
||||
url += '/' + config.urlPath
|
||||
}
|
||||
@@ -150,7 +150,7 @@ for (let i = keys.length; i--;) {
|
||||
// we set the new config using the old key.
|
||||
if (uppercase.test(keys[i]) &&
|
||||
config[lowercaseKey] !== undefined &&
|
||||
fileConfig[keys[i]] === undefined) {
|
||||
fileConfig && fileConfig[keys[i]] === undefined) {
|
||||
logger.warn('config.json contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.json file to replace ' + lowercaseKey + ' with ' + keys[i])
|
||||
config[keys[i]] = config[lowercaseKey]
|
||||
}
|
||||
@@ -198,9 +198,11 @@ switch (config.imageUploadType) {
|
||||
}
|
||||
|
||||
// generate correct path
|
||||
config.sslCAPath.forEach(function (capath, i, array) {
|
||||
array[i] = path.resolve(appRootPath, capath)
|
||||
})
|
||||
if (Array.isArray(config.sslCAPath)) {
|
||||
config.sslCAPath.forEach(function (capath: string, i: number, array: string[]) {
|
||||
array[i] = path.resolve(appRootPath, capath)
|
||||
})
|
||||
}
|
||||
|
||||
config.sslCertPath = path.resolve(appRootPath, config.sslCertPath)
|
||||
config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath)
|
||||
@@ -214,4 +216,4 @@ config.uploadsPath = path.resolve(appRootPath, config.uploadsPath)
|
||||
// make config readonly
|
||||
config = deepFreeze(config)
|
||||
|
||||
module.exports = config
|
||||
export = config
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
const oldDefault: Record<string, any> = {
|
||||
urlpath: undefined,
|
||||
urladdport: undefined,
|
||||
alloworigin: undefined,
|
||||
@@ -38,3 +38,5 @@ module.exports = {
|
||||
imageuploadtype: undefined,
|
||||
allowemailregister: undefined
|
||||
}
|
||||
|
||||
export = oldDefault
|
||||
@@ -1,10 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const { toBooleanConfig } = require('./utils')
|
||||
import { toBooleanConfig } from './utils'
|
||||
|
||||
module.exports = {
|
||||
const oldEnvironment = {
|
||||
debug: toBooleanConfig(process.env.DEBUG),
|
||||
dburl: process.env.DATABASE_URL,
|
||||
urlpath: process.env.URL_PATH,
|
||||
port: process.env.PORT
|
||||
}
|
||||
|
||||
export = oldEnvironment
|
||||
@@ -1,35 +1,37 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
function isPositiveAnswer (value) {
|
||||
function isPositiveAnswer (value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
return lowerValue === 'yes' || lowerValue === '1' || lowerValue === 'true'
|
||||
}
|
||||
|
||||
exports.toBooleanConfig = function toBooleanConfig (configValue) {
|
||||
export function toBooleanConfig (configValue: string | undefined): boolean | undefined {
|
||||
if (configValue && typeof configValue === 'string') {
|
||||
return (isPositiveAnswer(configValue))
|
||||
}
|
||||
return configValue
|
||||
// there is no HEAD information
|
||||
// ref does not exist in .git/ref/heads
|
||||
return undefined
|
||||
}
|
||||
|
||||
exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) {
|
||||
export function toArrayConfig (configValue: string | undefined, separator: string = ',', fallback?: string[]): string[] | undefined {
|
||||
if (configValue && typeof configValue === 'string') {
|
||||
return (configValue.split(separator).map(arrayItem => arrayItem.trim()))
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
exports.toIntegerConfig = function toIntegerConfig (configValue) {
|
||||
export function toIntegerConfig (configValue: string | undefined): number | undefined {
|
||||
if (configValue && typeof configValue === 'string') {
|
||||
return parseInt(configValue)
|
||||
}
|
||||
return configValue
|
||||
return undefined
|
||||
}
|
||||
|
||||
exports.getGitCommit = function getGitCommit (repodir) {
|
||||
export function getGitCommit (repodir: string): string | undefined {
|
||||
try {
|
||||
// prefer using git to get the current ref, as poking in .git is very fragile
|
||||
return require('child_process').execSync('git rev-parse HEAD', {
|
||||
@@ -39,7 +41,6 @@ exports.getGitCommit = function getGitCommit (repodir) {
|
||||
} catch (e) {
|
||||
// there was an error running git, try to parse refs ourselves
|
||||
if (!fs.existsSync(repodir + '/.git/HEAD')) {
|
||||
// there is no HEAD information
|
||||
return undefined
|
||||
}
|
||||
let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8')
|
||||
@@ -48,7 +49,6 @@ exports.getGitCommit = function getGitCommit (repodir) {
|
||||
reference = reference.substr(5).replace('\n', '')
|
||||
const refPath = path.resolve(repodir + '/.git', reference)
|
||||
if (!fs.existsSync(refPath)) {
|
||||
// ref does not exist in .git/ref/heads
|
||||
return undefined
|
||||
}
|
||||
reference = fs.readFileSync(refPath, 'utf8')
|
||||
@@ -58,7 +58,7 @@ exports.getGitCommit = function getGitCommit (repodir) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.getGitHubURL = function getGitHubURL (repo, reference) {
|
||||
export function getGitHubURL (repo: string, reference: string): string {
|
||||
// if it's not a github reference, we handle handle that anyway
|
||||
if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) {
|
||||
return repo
|
||||
+56
-57
@@ -1,10 +1,11 @@
|
||||
const config = require('./config')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
const { buildDomainOriginWithProtocol } = require('./config/buildDomainOriginWithProtocol')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import config = require('./config')
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { buildDomainOriginWithProtocol } from './config/buildDomainOriginWithProtocol'
|
||||
|
||||
const CspStrategy = {}
|
||||
type CspDirectives = Record<string, any[]>
|
||||
|
||||
const defaultDirectives = {
|
||||
const defaultDirectives: CspDirectives = {
|
||||
defaultSrc: ['\'none\''],
|
||||
baseUri: ['\'self\''],
|
||||
connectSrc: ['\'self\'', buildDomainOriginWithProtocol(config, 'ws'), 'https://vimeo.com/api/v2/video/'],
|
||||
@@ -24,35 +25,76 @@ const defaultDirectives = {
|
||||
mediaSrc: ['*']
|
||||
}
|
||||
|
||||
const disqusDirectives = {
|
||||
const disqusDirectives: CspDirectives = {
|
||||
scriptSrc: ['https://disqus.com', 'https://*.disqus.com', 'https://*.disquscdn.com'],
|
||||
styleSrc: ['https://*.disquscdn.com'],
|
||||
fontSrc: ['https://*.disquscdn.com']
|
||||
}
|
||||
|
||||
const googleAnalyticsDirectives = {
|
||||
const googleAnalyticsDirectives: CspDirectives = {
|
||||
scriptSrc: ['https://www.google-analytics.com']
|
||||
}
|
||||
|
||||
const dropboxDirectives = {
|
||||
const dropboxDirectives: CspDirectives = {
|
||||
scriptSrc: ['https://www.dropbox.com', '\'unsafe-inline\'']
|
||||
}
|
||||
|
||||
const disallowFramingDirectives = {
|
||||
const disallowFramingDirectives: CspDirectives = {
|
||||
frameAncestors: ['\'self\'']
|
||||
}
|
||||
|
||||
const allowPDFEmbedDirectives = {
|
||||
const allowPDFEmbedDirectives: CspDirectives = {
|
||||
objectSrc: ['*'], // Chrome and Firefox treat PDFs as objects
|
||||
frameSrc: ['*'] // Chrome also checks PDFs against frame-src
|
||||
}
|
||||
|
||||
const configuredGitLabInstanceDirectives = {
|
||||
const configuredGitLabInstanceDirectives: CspDirectives = {
|
||||
connectSrc: [config.gitlab.baseURL]
|
||||
}
|
||||
|
||||
CspStrategy.computeDirectives = function () {
|
||||
const directives = {}
|
||||
function mergeDirectives (existingDirectives: CspDirectives, newDirectives: CspDirectives): void {
|
||||
for (const propertyName in newDirectives) {
|
||||
const newDirective = newDirectives[propertyName]
|
||||
if (newDirective) {
|
||||
const existingDirective = existingDirectives[propertyName] || []
|
||||
existingDirectives[propertyName] = existingDirective.concat(newDirective)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDirectivesIf (condition: any, existingDirectives: CspDirectives, newDirectives: CspDirectives): void {
|
||||
if (condition) {
|
||||
mergeDirectives(existingDirectives, newDirectives)
|
||||
}
|
||||
}
|
||||
|
||||
function getCspNonce (_req: Request, res: Response): string {
|
||||
return '\'nonce-' + res.locals.nonce + '\''
|
||||
}
|
||||
|
||||
function addInlineScriptExceptions (directives: CspDirectives): void {
|
||||
directives.scriptSrc.push(getCspNonce)
|
||||
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
|
||||
// Any more clean solution appreciated.
|
||||
directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
|
||||
}
|
||||
|
||||
function addUpgradeUnsafeRequestsOptionTo (directives: CspDirectives): void {
|
||||
if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) {
|
||||
directives.upgradeInsecureRequests = []
|
||||
} else if (config.csp.upgradeInsecureRequests === true) {
|
||||
directives.upgradeInsecureRequests = []
|
||||
}
|
||||
}
|
||||
|
||||
function addReportURI (directives: CspDirectives): void {
|
||||
if (config.csp.reportURI) {
|
||||
directives.reportUri = config.csp.reportURI
|
||||
}
|
||||
}
|
||||
|
||||
export function computeDirectives (): CspDirectives {
|
||||
const directives: CspDirectives = {}
|
||||
mergeDirectives(directives, config.csp.directives)
|
||||
mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives)
|
||||
mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives)
|
||||
@@ -67,50 +109,7 @@ CspStrategy.computeDirectives = function () {
|
||||
return directives
|
||||
}
|
||||
|
||||
function mergeDirectives (existingDirectives, newDirectives) {
|
||||
for (const propertyName in newDirectives) {
|
||||
const newDirective = newDirectives[propertyName]
|
||||
if (newDirective) {
|
||||
const existingDirective = existingDirectives[propertyName] || []
|
||||
existingDirectives[propertyName] = existingDirective.concat(newDirective)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDirectivesIf (condition, existingDirectives, newDirectives) {
|
||||
if (condition) {
|
||||
mergeDirectives(existingDirectives, newDirectives)
|
||||
}
|
||||
}
|
||||
|
||||
function addInlineScriptExceptions (directives) {
|
||||
directives.scriptSrc.push(getCspNonce)
|
||||
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
|
||||
// Any more clean solution appreciated.
|
||||
directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
|
||||
}
|
||||
|
||||
function getCspNonce (req, res) {
|
||||
return '\'nonce-' + res.locals.nonce + '\''
|
||||
}
|
||||
|
||||
function addUpgradeUnsafeRequestsOptionTo (directives) {
|
||||
if (config.csp.upgradeInsecureRequests === 'auto' && (config.useSSL || config.protocolUseSSL)) {
|
||||
directives.upgradeInsecureRequests = []
|
||||
} else if (config.csp.upgradeInsecureRequests === true) {
|
||||
directives.upgradeInsecureRequests = []
|
||||
}
|
||||
}
|
||||
|
||||
function addReportURI (directives) {
|
||||
if (config.csp.reportURI) {
|
||||
directives.reportUri = config.csp.reportURI
|
||||
}
|
||||
}
|
||||
|
||||
CspStrategy.addNonceToLocals = function (req, res, next) {
|
||||
export function addNonceToLocals (_req: Request, res: Response, next: NextFunction): void {
|
||||
res.locals.nonce = uuidv4()
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = CspStrategy
|
||||
@@ -1,48 +0,0 @@
|
||||
const config = require('./config')
|
||||
|
||||
module.exports = {
|
||||
errorForbidden: function (res) {
|
||||
const { req } = res
|
||||
if (req.user) {
|
||||
responseError(res, 403, 'Forbidden', 'oh no.')
|
||||
} else {
|
||||
if (!req.session) req.session = {}
|
||||
if (req.originalUrl !== '/403') {
|
||||
req.session.returnTo = config.serverURL + (req.originalUrl || '/')
|
||||
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
|
||||
}
|
||||
res.redirect(config.serverURL + '/')
|
||||
}
|
||||
},
|
||||
errorNotFound: function (res) {
|
||||
responseError(res, 404, 'Not Found', 'oops.')
|
||||
},
|
||||
errorBadRequest: function (res) {
|
||||
responseError(res, 400, 'Bad Request', 'something not right.')
|
||||
},
|
||||
errorConflict: function (res) {
|
||||
responseError(res, 409, 'Conflict', 'This note already exists.')
|
||||
},
|
||||
errorTooLong: function (res) {
|
||||
responseError(res, 413, 'Payload Too Large', 'Shorten your note!')
|
||||
},
|
||||
errorTooManyRequests: function (res) {
|
||||
responseError(res, 429, 'Too Many Requests', 'Try again later.')
|
||||
},
|
||||
errorInternalError: function (res) {
|
||||
responseError(res, 500, 'Internal Error', 'wtf.')
|
||||
},
|
||||
errorServiceUnavailable: function (res) {
|
||||
res.status(503).send('I\'m busy right now, try again later.')
|
||||
}
|
||||
}
|
||||
|
||||
function responseError (res, code, detail, msg) {
|
||||
res.status(code).render('error.ejs', {
|
||||
title: code + ' ' + detail + ' ' + msg,
|
||||
code,
|
||||
detail,
|
||||
msg,
|
||||
opengraph: []
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import config = require('./config')
|
||||
|
||||
function responseError (res: Response, code: number, detail: string, msg: string): void {
|
||||
res.status(code).render('error.ejs', {
|
||||
title: code + ' ' + detail + ' ' + msg,
|
||||
code,
|
||||
detail,
|
||||
msg,
|
||||
opengraph: []
|
||||
})
|
||||
}
|
||||
|
||||
export function errorForbidden (res: Response): void {
|
||||
const req: Request = res.req
|
||||
if ((req as any).user) {
|
||||
responseError(res, 403, 'Forbidden', 'oh no.')
|
||||
} else {
|
||||
if (!(req as any).session) (req as any).session = {}
|
||||
if (req.originalUrl !== '/403') {
|
||||
(req as any).session.returnTo = config.serverURL + (req.originalUrl || '/')
|
||||
;(req as any).flash('error', 'You are not allowed to access this page. Maybe try logging in?')
|
||||
}
|
||||
res.redirect(config.serverURL + '/')
|
||||
}
|
||||
}
|
||||
|
||||
export function errorNotFound (res: Response): void {
|
||||
responseError(res, 404, 'Not Found', 'oops.')
|
||||
}
|
||||
|
||||
export function errorBadRequest (res: Response): void {
|
||||
responseError(res, 400, 'Bad Request', 'something not right.')
|
||||
}
|
||||
|
||||
export function errorConflict (res: Response): void {
|
||||
responseError(res, 409, 'Conflict', 'This note already exists.')
|
||||
}
|
||||
|
||||
export function errorTooLong (res: Response): void {
|
||||
responseError(res, 413, 'Payload Too Large', 'Shorten your note!')
|
||||
}
|
||||
|
||||
export function errorTooManyRequests (res: Response): void {
|
||||
responseError(res, 429, 'Too Many Requests', 'Try again later.')
|
||||
}
|
||||
|
||||
export function errorInternalError (res: Response): void {
|
||||
responseError(res, 500, 'Internal Error', 'wtf.')
|
||||
}
|
||||
|
||||
export function errorServiceUnavailable (res: Response): void {
|
||||
res.status(503).send('I\'m busy right now, try again later.')
|
||||
}
|
||||
@@ -1,89 +1,90 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response } from 'express'
|
||||
// history
|
||||
// external modules
|
||||
const LZString = require('lz-string')
|
||||
|
||||
import LZString = require('lz-string')
|
||||
// core
|
||||
const logger = require('./logger')
|
||||
const models = require('./models')
|
||||
const errors = require('./errors')
|
||||
import logger = require('./logger')
|
||||
import models = require('./models')
|
||||
import * as errors from './errors'
|
||||
|
||||
// public
|
||||
const History = {
|
||||
historyGet,
|
||||
historyPost,
|
||||
historyDelete,
|
||||
updateHistory
|
||||
interface HistoryItem {
|
||||
id: string
|
||||
text?: string
|
||||
time?: number
|
||||
tags?: string[]
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
function getHistory (userid, callback) {
|
||||
function getHistory (userid: string, callback: (err: Error | null, history: Record<string, HistoryItem> | null) => void): void {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: userid
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) {
|
||||
return callback(null, null)
|
||||
}
|
||||
let history = {}
|
||||
let historyObj: Record<string, HistoryItem> = {}
|
||||
if (user.history) {
|
||||
history = JSON.parse(user.history)
|
||||
const historyArr: HistoryItem[] = JSON.parse(user.history)
|
||||
// migrate LZString encoded note id to base64url encoded note id
|
||||
for (let i = 0, l = history.length; i < l; i++) {
|
||||
for (let i = 0, l = historyArr.length; i < l; i++) {
|
||||
// Calculate minimal string length for an UUID that is encoded
|
||||
// base64 encoded and optimize comparsion by using -1
|
||||
// this should make a lot of LZ-String parsing errors obsolete
|
||||
// as we can assume that a nodeId that is 48 chars or longer is a
|
||||
// noteID.
|
||||
const base64UuidLength = ((4 * 36) / 3) - 1
|
||||
if (!(history[i].id.length > base64UuidLength)) {
|
||||
if (!(historyArr[i].id.length > base64UuidLength)) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const id = LZString.decompressFromBase64(history[i].id)
|
||||
const id = LZString.decompressFromBase64(historyArr[i].id)
|
||||
if (id && models.Note.checkNoteIdValid(id)) {
|
||||
history[i].id = models.Note.encodeNoteId(id)
|
||||
historyArr[i].id = models.Note.encodeNoteId(id)
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// most error here comes from LZString, ignore
|
||||
if (err.message === 'Cannot read property \'charAt\' of undefined') {
|
||||
logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.')
|
||||
logger.warn('Looks like we can not decode "' + historyArr[i].id + '" with LZString. Can be ignored.')
|
||||
} else {
|
||||
logger.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
history = parseHistoryToObject(history)
|
||||
historyObj = parseHistoryToObject(historyArr)
|
||||
}
|
||||
logger.debug(`read history success: ${user.id}`)
|
||||
return callback(null, history)
|
||||
}).catch(function (err) {
|
||||
return callback(null, historyObj)
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('read history failed: ' + err)
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
|
||||
function setHistory (userid, history, callback) {
|
||||
function setHistory (userid: string, history: Record<string, HistoryItem> | HistoryItem[], callback: (err: Error | null, count: number[] | null) => void): void {
|
||||
models.User.update({
|
||||
history: JSON.stringify(parseHistoryToArray(history))
|
||||
}, {
|
||||
where: {
|
||||
id: userid
|
||||
}
|
||||
}).then(function (count) {
|
||||
}).then(function (count: number[]) {
|
||||
return callback(null, count)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('set history failed: ' + err)
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
|
||||
function updateHistory (userid, noteId, document, time) {
|
||||
function updateHistory (userid: string, noteId: string, document: any, time?: number): void {
|
||||
if (userid && noteId && typeof document !== 'undefined') {
|
||||
getHistory(userid, function (err, history) {
|
||||
if (err || !history) return
|
||||
if (!history[noteId]) {
|
||||
history[noteId] = {}
|
||||
history[noteId] = { id: noteId }
|
||||
}
|
||||
const noteHistory = history[noteId]
|
||||
const noteInfo = models.Note.parseNoteInfo(document)
|
||||
@@ -91,26 +92,26 @@ function updateHistory (userid, noteId, document, time) {
|
||||
noteHistory.text = noteInfo.title
|
||||
noteHistory.time = time || Date.now()
|
||||
noteHistory.tags = noteInfo.tags
|
||||
setHistory(userid, history, function (err, count) {
|
||||
setHistory(userid, history, function (err, _count) {
|
||||
if (err) {
|
||||
logger.log(err)
|
||||
logger.error('set history error:', err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseHistoryToArray (history) {
|
||||
const _history = []
|
||||
function parseHistoryToArray (history: Record<string, HistoryItem> | HistoryItem[]): HistoryItem[] {
|
||||
const _history: HistoryItem[] = []
|
||||
Object.keys(history).forEach(function (key) {
|
||||
const item = history[key]
|
||||
const item = (history as Record<string, HistoryItem>)[key]
|
||||
_history.push(item)
|
||||
})
|
||||
return _history
|
||||
}
|
||||
|
||||
function parseHistoryToObject (history) {
|
||||
const _history = {}
|
||||
function parseHistoryToObject (history: HistoryItem[]): Record<string, HistoryItem> {
|
||||
const _history: Record<string, HistoryItem> = {}
|
||||
for (let i = 0, l = history.length; i < l; i++) {
|
||||
const item = history[i]
|
||||
_history[item.id] = item
|
||||
@@ -118,9 +119,9 @@ function parseHistoryToObject (history) {
|
||||
return _history
|
||||
}
|
||||
|
||||
function historyGet (req, res) {
|
||||
function historyGet (req: Request, res: Response): void {
|
||||
if (req.isAuthenticated()) {
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
getHistory((req as any).user.id, function (err, history) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
if (!history) return errors.errorNotFound(res)
|
||||
res.send({
|
||||
@@ -132,16 +133,16 @@ function historyGet (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
function historyPost (req, res) {
|
||||
function historyPost (req: Request, res: Response): void {
|
||||
if (req.isAuthenticated()) {
|
||||
const noteId = req.params.noteId
|
||||
const noteId: string = req.params.noteId as string
|
||||
if (!noteId) {
|
||||
if (typeof req.body.history === 'undefined') return errors.errorBadRequest(res)
|
||||
logger.debug(`SERVER received history from [${req.user.id}]: ${req.body.history}`)
|
||||
logger.debug(`SERVER received history from [${(req as any).user.id}]: ${req.body.history}`)
|
||||
try {
|
||||
const history = JSON.parse(req.body.history)
|
||||
if (Array.isArray(history)) {
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
setHistory((req as any).user.id, history, function (err, _count) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
res.end()
|
||||
})
|
||||
@@ -153,13 +154,13 @@ function historyPost (req, res) {
|
||||
}
|
||||
} else {
|
||||
if (typeof req.body.pinned === 'undefined') return errors.errorBadRequest(res)
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
getHistory((req as any).user.id, function (err, history) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
if (!history) return errors.errorNotFound(res)
|
||||
if (!history[noteId]) return errors.errorNotFound(res)
|
||||
if (req.body.pinned === 'true' || req.body.pinned === 'false') {
|
||||
history[noteId].pinned = (req.body.pinned === 'true')
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
setHistory((req as any).user.id, history, function (err, _count) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
res.end()
|
||||
})
|
||||
@@ -173,28 +174,28 @@ function historyPost (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
function historyDelete (req, res) {
|
||||
function historyDelete (req: Request, res: Response): void {
|
||||
if (!req.isAuthenticated()) {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
|
||||
const token = req.query.token
|
||||
if (!token || token !== req.user.deleteToken) {
|
||||
if (!token || token !== (req as any).user.deleteToken) {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
|
||||
const noteId = req.params.noteId
|
||||
const noteId: string = req.params.noteId as string
|
||||
if (!noteId) {
|
||||
setHistory(req.user.id, [], function (err, count) {
|
||||
setHistory((req as any).user.id, [], function (err, _count) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
res.end()
|
||||
})
|
||||
} else {
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
getHistory((req as any).user.id, function (err, history) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
if (!history) return errors.errorNotFound(res)
|
||||
delete history[noteId]
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
setHistory((req as any).user.id, history, function (err, _count) {
|
||||
if (err) return errors.errorInternalError(res)
|
||||
res.end()
|
||||
})
|
||||
@@ -202,4 +203,12 @@ function historyDelete (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = History
|
||||
// public
|
||||
const History = {
|
||||
historyGet,
|
||||
historyPost,
|
||||
historyDelete,
|
||||
updateHistory
|
||||
}
|
||||
|
||||
export = History
|
||||
@@ -1,19 +1,21 @@
|
||||
'use strict'
|
||||
|
||||
// external modules
|
||||
const crypto = require('crypto')
|
||||
const Chance = require('chance')
|
||||
const config = require('./config')
|
||||
import * as crypto from 'crypto'
|
||||
import Chance = require('chance')
|
||||
|
||||
// core
|
||||
exports.generateAvatar = function (name) {
|
||||
import config = require('./config')
|
||||
|
||||
export function generateAvatar (name: string): string {
|
||||
// use darker colors for better contrast
|
||||
const color = new Chance(name).color({
|
||||
const color: string = new Chance(name).color({
|
||||
format: 'hex',
|
||||
max_red: 150,
|
||||
max_green: 150,
|
||||
max_blue: 150
|
||||
})
|
||||
const letter = name.substring(0, 1).toUpperCase()
|
||||
const letter: string = name.substring(0, 1).toUpperCase()
|
||||
|
||||
let svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
|
||||
svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">'
|
||||
@@ -28,8 +30,8 @@ exports.generateAvatar = function (name) {
|
||||
return svg
|
||||
}
|
||||
|
||||
exports.generateAvatarURL = function (name, email = '', big = true) {
|
||||
let photo
|
||||
export function generateAvatarURL (name: string, email: string = '', big: boolean = true): string {
|
||||
let photo: string
|
||||
if (typeof email !== 'string') {
|
||||
email = '' + name + '@example.com'
|
||||
}
|
||||
@@ -37,7 +39,7 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
|
||||
|
||||
const hash = crypto.createHash('md5')
|
||||
hash.update(email.toLowerCase())
|
||||
const hexDigest = hash.digest('hex')
|
||||
const hexDigest: string = hash.digest('hex')
|
||||
|
||||
if (email !== '' && config.allowGravatar) {
|
||||
photo = `https://cdn.libravatar.org/avatar/${hexDigest}?default=identicon`
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict'
|
||||
const { createLogger, format, transports } = require('winston')
|
||||
|
||||
import { createLogger, format, transports } from 'winston'
|
||||
|
||||
const logger = createLogger({
|
||||
level: 'debug',
|
||||
@@ -18,10 +19,10 @@ const logger = createLogger({
|
||||
exitOnError: false
|
||||
})
|
||||
|
||||
logger.stream = {
|
||||
write: function (message, encoding) {
|
||||
;(logger as any).stream = {
|
||||
write: function (message: string) {
|
||||
logger.info(message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = logger
|
||||
export = logger
|
||||
@@ -2,7 +2,7 @@
|
||||
// external modules
|
||||
const Sequelize = require('sequelize')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
module.exports = function (sequelize: any, DataTypes: any): any {
|
||||
const Author = sequelize.define('Author', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
@@ -21,7 +21,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
]
|
||||
})
|
||||
|
||||
Author.associate = function (models) {
|
||||
Author.associate = function (models: any): void {
|
||||
Author.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
@@ -1,24 +1,24 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
const Sequelize = require('sequelize')
|
||||
const { cloneDeep } = require('lodash')
|
||||
const Umzug = require('umzug')
|
||||
import { cloneDeep } from 'lodash'
|
||||
import Umzug = require('umzug')
|
||||
|
||||
// core
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const { isSQLite } = require('../utils')
|
||||
import config = require('../config')
|
||||
import logger = require('../logger')
|
||||
import { isSQLite } from '../utils'
|
||||
|
||||
const dbconfig = cloneDeep(config.db)
|
||||
dbconfig.logging = config.debug
|
||||
? (data) => {
|
||||
? (data: any) => {
|
||||
logger.info(data)
|
||||
}
|
||||
: false
|
||||
|
||||
let sequelize = null
|
||||
let sequelize: any = null
|
||||
|
||||
// Heroku specific
|
||||
if (config.dbURL) {
|
||||
@@ -29,7 +29,7 @@ if (config.dbURL) {
|
||||
|
||||
// [Postgres] Handling NULL bytes
|
||||
// https://github.com/sequelize/sequelize/issues/6485
|
||||
function stripNullByte (value) {
|
||||
function stripNullByte (value: any): string {
|
||||
value = '' + value
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return value ? value.replace(/\u0000/g, '') : value
|
||||
@@ -37,7 +37,7 @@ function stripNullByte (value) {
|
||||
|
||||
sequelize.stripNullByte = stripNullByte
|
||||
|
||||
function processData (data, _default, process) {
|
||||
function processData (data: any, _default: any, process?: (data: any) => any): any {
|
||||
if (data === undefined) {
|
||||
return data
|
||||
} else {
|
||||
@@ -47,18 +47,18 @@ function processData (data, _default, process) {
|
||||
|
||||
sequelize.processData = processData
|
||||
|
||||
const db = {}
|
||||
const db: any = {}
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter(function (file) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js')
|
||||
.filter(function (file: string) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js') && file.endsWith('.js') && !file.endsWith('.js.map')
|
||||
})
|
||||
.forEach(function (file) {
|
||||
.forEach(function (file: string) {
|
||||
const model = sequelize.import(path.join(__dirname, file))
|
||||
db[model.name] = model
|
||||
})
|
||||
|
||||
Object.keys(db).forEach(function (modelName) {
|
||||
Object.keys(db).forEach(function (modelName: string) {
|
||||
if ('associate' in db[modelName]) {
|
||||
db[modelName].associate(db)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ Object.keys(db).forEach(function (modelName) {
|
||||
|
||||
const umzug = new Umzug({
|
||||
migrations: {
|
||||
path: path.resolve(__dirname, '..', 'migrations'),
|
||||
path: path.resolve(__dirname, '..', '..', '..', 'lib', 'migrations'),
|
||||
params: [
|
||||
sequelize.getQueryInterface(),
|
||||
Sequelize.DataTypes
|
||||
@@ -74,7 +74,7 @@ const umzug = new Umzug({
|
||||
},
|
||||
// Required wrapper function required to prevent winstion issue
|
||||
// https://github.com/winstonjs/winston/issues/1577
|
||||
logging: message => {
|
||||
logging: (message: string) => {
|
||||
logger.info(message)
|
||||
},
|
||||
storage: 'sequelize',
|
||||
@@ -83,7 +83,7 @@ const umzug = new Umzug({
|
||||
}
|
||||
})
|
||||
|
||||
db.runMigrations = async function runMigrations () {
|
||||
db.runMigrations = async function runMigrations (): Promise<void> {
|
||||
// checks migrations and run them if they are not already applied
|
||||
// exit in case of unsuccessful migrations
|
||||
const savepointName = 'migration'
|
||||
@@ -124,4 +124,4 @@ Exiting…`)
|
||||
db.sequelize = sequelize
|
||||
db.Sequelize = Sequelize
|
||||
|
||||
module.exports = db
|
||||
export = db
|
||||
@@ -1,23 +1,23 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const LZString = require('lz-string')
|
||||
const base64url = require('base64url')
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import LZString = require('lz-string')
|
||||
const base64url: any = require('base64url')
|
||||
const md = require('markdown-it')()
|
||||
const metaMarked = require('@hedgedoc/meta-marked')
|
||||
const cheerio = require('cheerio')
|
||||
const nanoid = require('nanoid')
|
||||
const metaMarked: any = require('@hedgedoc/meta-marked')
|
||||
const cheerio: any = require('cheerio')
|
||||
import { nanoid } from 'nanoid'
|
||||
const Sequelize = require('sequelize')
|
||||
const async = require('async')
|
||||
const moment = require('moment')
|
||||
const DiffMatchPatch = require('diff-match-patch')
|
||||
import async = require('async')
|
||||
import moment = require('moment')
|
||||
import DiffMatchPatch = require('diff-match-patch')
|
||||
const dmp = new DiffMatchPatch()
|
||||
|
||||
// core
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const utils = require('../utils')
|
||||
import config = require('../config')
|
||||
import logger = require('../logger')
|
||||
import { isMySQL } from '../utils'
|
||||
|
||||
// ot
|
||||
const ot = require('../ot')
|
||||
@@ -25,7 +25,7 @@ const ot = require('../ot')
|
||||
// permission types
|
||||
const permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private']
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
module.exports = function (sequelize: any, DataTypes: any): any {
|
||||
const Note = sequelize.define('Note', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -36,7 +36,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
defaultValue: () => nanoid.nanoid(10)
|
||||
defaultValue: () => nanoid(10)
|
||||
},
|
||||
alias: {
|
||||
type: DataTypes.STRING,
|
||||
@@ -53,28 +53,28 @@ module.exports = function (sequelize, DataTypes) {
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('title'), '')
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: string) {
|
||||
this.setDataValue('title', sequelize.stripNullByte(value))
|
||||
}
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('content'), '')
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: string) {
|
||||
this.setDataValue('content', sequelize.stripNullByte(value))
|
||||
}
|
||||
},
|
||||
authorship: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse)
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: any) {
|
||||
this.setDataValue('authorship', JSON.stringify(value))
|
||||
}
|
||||
},
|
||||
@@ -87,12 +87,12 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}, {
|
||||
paranoid: false,
|
||||
hooks: {
|
||||
beforeCreate: function (note, options) {
|
||||
beforeCreate: function (note: any, options: any) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
// if no content specified then use default note
|
||||
if (!note.content) {
|
||||
let body = null
|
||||
let filePath = null
|
||||
let body: string | null = null
|
||||
let filePath: string | null = null
|
||||
if (note.alias) {
|
||||
filePath = path.join(config.docsPath, path.basename(note.alias) + '.md')
|
||||
}
|
||||
@@ -100,8 +100,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
filePath = config.defaultNotePath
|
||||
}
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
const fsCreatedTime = moment(fs.statSync(filePath).ctime)
|
||||
body = fs.readFileSync(filePath, 'utf8')
|
||||
const fsCreatedTime = moment(fs.statSync(filePath!).ctime)
|
||||
body = fs.readFileSync(filePath!, 'utf8')
|
||||
note.title = Note.parseNoteTitle(body)
|
||||
note.content = body
|
||||
if (filePath !== config.defaultNotePath) {
|
||||
@@ -120,9 +120,9 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return resolve(note)
|
||||
})
|
||||
},
|
||||
afterCreate: function (note, options, callback) {
|
||||
afterCreate: function (note: any, options: any) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err: any, revision: any) {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
@@ -133,7 +133,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
})
|
||||
|
||||
Note.associate = function (models) {
|
||||
Note.associate = function (models: any): void {
|
||||
Note.belongsTo(models.User, {
|
||||
foreignKey: 'ownerId',
|
||||
as: 'owner',
|
||||
@@ -156,24 +156,24 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
Note.checkFileExist = function (filePath) {
|
||||
Note.checkFileExist = function (filePath: string): boolean {
|
||||
try {
|
||||
return fs.statSync(filePath).isFile()
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Note.encodeNoteId = function (id) {
|
||||
Note.encodeNoteId = function (id: string): string {
|
||||
// remove dashes in UUID and encode in url-safe base64
|
||||
const str = id.replace(/-/g, '')
|
||||
const hexStr = Buffer.from(str, 'hex')
|
||||
return base64url.encode(hexStr)
|
||||
}
|
||||
Note.decodeNoteId = function (encodedId) {
|
||||
Note.decodeNoteId = function (encodedId: string): string {
|
||||
// decode from url-safe base64
|
||||
const id = base64url.toBuffer(encodedId).toString('hex')
|
||||
// add dashes between the UUID string parts
|
||||
const idParts = []
|
||||
const idParts: string[] = []
|
||||
idParts.push(id.substr(0, 8))
|
||||
idParts.push(id.substr(8, 4))
|
||||
idParts.push(id.substr(12, 4))
|
||||
@@ -181,22 +181,22 @@ module.exports = function (sequelize, DataTypes) {
|
||||
idParts.push(id.substr(20, 12))
|
||||
return idParts.join('-')
|
||||
}
|
||||
Note.checkNoteIdValid = function (id) {
|
||||
Note.checkNoteIdValid = function (id: string): boolean {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
const result = id.match(uuidRegex)
|
||||
if (result && result.length === 1) { return true } else { return false }
|
||||
}
|
||||
Note.parseNoteId = function (noteId, callback) {
|
||||
Note.parseNoteId = function (noteId: string, callback: (err: any, id: string | null) => void): void {
|
||||
async.series({
|
||||
parseNoteIdByAlias: function (_callback) {
|
||||
parseNoteIdByAlias: function (_callback: any) {
|
||||
// try to parse note id by alias (e.g. doc)
|
||||
Note.findOne({
|
||||
where: utils.isMySQL(sequelize)
|
||||
where: isMySQL(sequelize)
|
||||
? sequelize.where(sequelize.fn('BINARY', sequelize.col('alias')), noteId)
|
||||
: {
|
||||
alias: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
if (note) {
|
||||
const filePath = path.join(config.docsPath, path.basename(noteId) + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
@@ -212,8 +212,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
title,
|
||||
content: body,
|
||||
lastchangeAt: fsModifiedTime
|
||||
}).then(function (note) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
}).then(function (note: any) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err: any, revision: any) {
|
||||
if (err) return _callback(err, null)
|
||||
// update authorship on after making revision of docs
|
||||
const patch = dmp.patch_fromText(revision.patch)
|
||||
@@ -224,13 +224,13 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
note.update({
|
||||
authorship
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
@@ -246,21 +246,21 @@ module.exports = function (sequelize, DataTypes) {
|
||||
alias: noteId,
|
||||
owner: null,
|
||||
permission: 'locked'
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
},
|
||||
// parse note id by LZString is deprecated, here for compability
|
||||
parseNoteIdByLZString: function (_callback) {
|
||||
parseNoteIdByLZString: function (_callback: any) {
|
||||
// Calculate minimal string length for an UUID that is encoded
|
||||
// base64 encoded and optimize comparsion by using -1
|
||||
// this should make a lot of LZ-String parsing errors obsolete
|
||||
@@ -274,7 +274,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
try {
|
||||
const id = LZString.decompressFromBase64(noteId)
|
||||
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Cannot read property \'charAt\' of undefined') {
|
||||
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
|
||||
} else {
|
||||
@@ -283,7 +283,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByBase64Url: function (_callback) {
|
||||
parseNoteIdByBase64Url: function (_callback: any) {
|
||||
// try to parse note id by base64url
|
||||
try {
|
||||
const id = Note.decodeNoteId(noteId)
|
||||
@@ -293,7 +293,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByShortId: function (_callback) {
|
||||
parseNoteIdByShortId: function (_callback: any) {
|
||||
// try to parse note id by shortId
|
||||
try {
|
||||
// old short ids generated by the `shortid` package could be from 7 to 14 characters long
|
||||
@@ -302,15 +302,15 @@ module.exports = function (sequelize, DataTypes) {
|
||||
Note.findOne({
|
||||
// MariaDB and MySQL do case-insensitive comparison by default (unless a collation charset like utf8mb4 is used)
|
||||
// The binary conversion ensures, case-sensitive comparison.
|
||||
where: utils.isMySQL(sequelize)
|
||||
where: isMySQL(sequelize)
|
||||
? sequelize.where(sequelize.fn('BINARY', sequelize.col('shortid')), noteId)
|
||||
: {
|
||||
shortid: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
if (!note) return _callback(null, null)
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
@@ -320,7 +320,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return _callback(err, null)
|
||||
}
|
||||
}
|
||||
}, function (err, result) {
|
||||
}, function (err: any, result: any) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
@@ -328,7 +328,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return callback(null, null)
|
||||
})
|
||||
}
|
||||
Note.parseNoteInfo = function (body) {
|
||||
Note.parseNoteInfo = function (body: string): { title: string; tags: string[] } {
|
||||
const parsed = Note.extractMeta(body)
|
||||
const $ = cheerio.load(md.render(parsed.markdown))
|
||||
return {
|
||||
@@ -336,13 +336,13 @@ module.exports = function (sequelize, DataTypes) {
|
||||
tags: Note.extractNoteTags(parsed.meta, $)
|
||||
}
|
||||
}
|
||||
Note.parseNoteTitle = function (body) {
|
||||
Note.parseNoteTitle = function (body: string): string {
|
||||
const parsed = Note.extractMeta(body)
|
||||
const $ = cheerio.load(md.render(parsed.markdown))
|
||||
return Note.extractNoteTitle(parsed.meta, $)
|
||||
}
|
||||
Note.extractNoteTitle = function (meta, $) {
|
||||
let title = ''
|
||||
Note.extractNoteTitle = function (meta: any, $: any): string {
|
||||
let title: string | number = ''
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
|
||||
title = meta.title
|
||||
} else {
|
||||
@@ -352,21 +352,21 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
}
|
||||
if (!title) title = 'Untitled'
|
||||
return title
|
||||
return '' + title
|
||||
}
|
||||
Note.generateDescription = function (markdown) {
|
||||
Note.generateDescription = function (markdown: string): string {
|
||||
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
|
||||
}
|
||||
Note.decodeTitle = function (title) {
|
||||
Note.decodeTitle = function (title: string): string {
|
||||
return title || 'Untitled'
|
||||
}
|
||||
Note.generateWebTitle = function (title) {
|
||||
Note.generateWebTitle = function (title: string): string {
|
||||
title = !title || title === 'Untitled' ? 'HedgeDoc - Collaborative markdown notes' : title + ' - HedgeDoc'
|
||||
return title
|
||||
}
|
||||
Note.extractNoteTags = function (meta, $) {
|
||||
const tags = []
|
||||
const rawtags = []
|
||||
Note.extractNoteTags = function (meta: any, $: any): string[] {
|
||||
const tags: string[] = []
|
||||
const rawtags: string[] = []
|
||||
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
|
||||
const metaTags = ('' + meta.tags).split(',')
|
||||
for (let i = 0; i < metaTags.length; i++) {
|
||||
@@ -375,7 +375,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
} else {
|
||||
const h6s = $('h6')
|
||||
h6s.each(function (key, value) {
|
||||
h6s.each(function (key: number, value: any) {
|
||||
if (/^tags/gmi.test($(value).text())) {
|
||||
const codes = $(value).find('code')
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
@@ -397,8 +397,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return tags
|
||||
}
|
||||
Note.extractMeta = function (content) {
|
||||
let obj = null
|
||||
Note.extractMeta = function (content: string): { markdown: string; meta: any } {
|
||||
let obj: any = null
|
||||
try {
|
||||
obj = metaMarked(content)
|
||||
if (!obj.markdown) obj.markdown = ''
|
||||
@@ -411,8 +411,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return obj
|
||||
}
|
||||
Note.parseMeta = function (meta) {
|
||||
const _meta = {}
|
||||
Note.parseMeta = function (meta: any): any {
|
||||
const _meta: any = {}
|
||||
if (meta) {
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
|
||||
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
|
||||
@@ -425,15 +425,15 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return _meta
|
||||
}
|
||||
Note.parseOpengraph = function (meta, title) {
|
||||
let _ogdata = {}
|
||||
Note.parseOpengraph = function (meta: any, title: string): any {
|
||||
let _ogdata: any = {}
|
||||
if (meta.opengraph) { _ogdata = meta.opengraph }
|
||||
if (!(_ogdata.title && (typeof _ogdata.title === 'string' || typeof _ogdata.title === 'number'))) { _ogdata.title = title }
|
||||
if (!(_ogdata.description && (typeof _ogdata.description === 'string' || typeof _ogdata.description === 'number'))) { _ogdata.description = meta.description || '' }
|
||||
if (!(_ogdata.type && (typeof _ogdata.type === 'string'))) { _ogdata.type = 'website' }
|
||||
return _ogdata
|
||||
}
|
||||
Note.updateAuthorshipByOperation = function (operation, userId, authorships) {
|
||||
Note.updateAuthorshipByOperation = function (operation: any[], userId: string | null, authorships: any[]): any[] {
|
||||
let index = 0
|
||||
const timestamp = Date.now()
|
||||
for (let i = 0; i < operation.length; i++) {
|
||||
@@ -534,8 +534,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return authorships
|
||||
}
|
||||
Note.transformPatchToOperations = function (patch, contentLength) {
|
||||
const operations = []
|
||||
Note.transformPatchToOperations = function (patch: any[], contentLength: number): any[][] {
|
||||
const operations: any[][] = []
|
||||
if (patch.length > 0) {
|
||||
// calculate original content length
|
||||
for (let j = patch.length - 1; j >= 0; j--) {
|
||||
@@ -556,7 +556,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
let bias = 0
|
||||
let lengthBias = 0
|
||||
for (let j = 0; j < patch.length; j++) {
|
||||
const operation = []
|
||||
const operation: any[] = []
|
||||
const p = patch[j]
|
||||
let currIndex = p.start1
|
||||
const currLength = contentLength - bias
|
||||
@@ -1,26 +1,35 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const Sequelize = require('sequelize')
|
||||
const async = require('async')
|
||||
const moment = require('moment')
|
||||
const childProcess = require('child_process')
|
||||
const nanoid = require('nanoid')
|
||||
const path = require('path')
|
||||
import async = require('async')
|
||||
import moment = require('moment')
|
||||
import * as childProcess from 'child_process'
|
||||
import { nanoid } from 'nanoid'
|
||||
import * as path from 'path'
|
||||
|
||||
const Op = Sequelize.Op
|
||||
|
||||
// core
|
||||
const logger = require('../logger')
|
||||
import logger = require('../logger')
|
||||
|
||||
let dmpWorker = createDmpWorker()
|
||||
const dmpCallbackCache = {}
|
||||
interface DmpWorkerMessage {
|
||||
msg: string
|
||||
cacheKey: string
|
||||
error?: any
|
||||
result?: any
|
||||
}
|
||||
|
||||
function createDmpWorker () {
|
||||
type DmpCallback = (err: any, result: any) => void
|
||||
|
||||
let dmpWorker: childProcess.ChildProcess | null = createDmpWorker()
|
||||
const dmpCallbackCache: Record<string, DmpCallback> = {}
|
||||
|
||||
function createDmpWorker (): childProcess.ChildProcess {
|
||||
const worker = childProcess.fork(path.resolve(__dirname, '../workers/dmpWorker.js'), {
|
||||
stdio: 'ignore'
|
||||
})
|
||||
logger.debug('dmp worker process started')
|
||||
worker.on('message', function (data) {
|
||||
worker.on('message', function (data: DmpWorkerMessage) {
|
||||
if (!data || !data.msg || !data.cacheKey) {
|
||||
return logger.error('dmp worker error: not enough data on message')
|
||||
}
|
||||
@@ -35,16 +44,16 @@ function createDmpWorker () {
|
||||
}
|
||||
delete dmpCallbackCache[cacheKey]
|
||||
})
|
||||
worker.on('close', function (code) {
|
||||
worker.on('close', function (code: number | null) {
|
||||
dmpWorker = null
|
||||
logger.debug(`dmp worker process exited with code ${code}`)
|
||||
})
|
||||
return worker
|
||||
}
|
||||
|
||||
function sendDmpWorker (data, callback) {
|
||||
function sendDmpWorker (data: any, callback: DmpCallback): void {
|
||||
if (!dmpWorker) dmpWorker = createDmpWorker()
|
||||
const cacheKey = Date.now() + '_' + nanoid.nanoid()
|
||||
const cacheKey = Date.now() + '_' + nanoid()
|
||||
dmpCallbackCache[cacheKey] = callback
|
||||
data = Object.assign(data, {
|
||||
cacheKey
|
||||
@@ -52,7 +61,7 @@ function sendDmpWorker (data, callback) {
|
||||
dmpWorker.send(data)
|
||||
}
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
module.exports = function (sequelize: any, DataTypes: any): any {
|
||||
const Revision = sequelize.define('Revision', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -61,28 +70,28 @@ module.exports = function (sequelize, DataTypes) {
|
||||
},
|
||||
patch: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('patch'), '')
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: string) {
|
||||
this.setDataValue('patch', sequelize.stripNullByte(value))
|
||||
}
|
||||
},
|
||||
lastContent: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('lastContent'), '')
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: string) {
|
||||
this.setDataValue('lastContent', sequelize.stripNullByte(value))
|
||||
}
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('content'), '')
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: string) {
|
||||
this.setDataValue('content', sequelize.stripNullByte(value))
|
||||
}
|
||||
},
|
||||
@@ -91,16 +100,16 @@ module.exports = function (sequelize, DataTypes) {
|
||||
},
|
||||
authorship: {
|
||||
type: DataTypes.TEXT('long'),
|
||||
get: function () {
|
||||
get: function (this: any) {
|
||||
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse)
|
||||
},
|
||||
set: function (value) {
|
||||
set: function (this: any, value: any) {
|
||||
this.setDataValue('authorship', value ? JSON.stringify(value) : value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Revision.associate = function (models) {
|
||||
Revision.associate = function (models: any): void {
|
||||
Revision.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
@@ -109,14 +118,14 @@ module.exports = function (sequelize, DataTypes) {
|
||||
hooks: true
|
||||
})
|
||||
}
|
||||
Revision.getNoteRevisions = function (note, callback) {
|
||||
Revision.getNoteRevisions = function (note: any, callback: DmpCallback): void {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
const data = []
|
||||
}).then(function (revisions: any[]) {
|
||||
const data: Array<{ time: number; length: number }> = []
|
||||
for (let i = 0, l = revisions.length; i < l; i++) {
|
||||
const revision = revisions[i]
|
||||
data.push({
|
||||
@@ -125,18 +134,18 @@ module.exports = function (sequelize, DataTypes) {
|
||||
})
|
||||
}
|
||||
callback(null, data)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.getPatchedNoteRevisionByTime = function (note, time, callback) {
|
||||
Revision.getPatchedNoteRevisionByTime = function (note: any, time: any, callback: DmpCallback): void {
|
||||
// find all revisions to prepare for all possible calculation
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
}).then(function (revisions: any[]) {
|
||||
if (revisions.length <= 0) return callback(null, null)
|
||||
// measure target revision position
|
||||
Revision.count({
|
||||
@@ -147,22 +156,22 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (count) {
|
||||
}).then(function (count: number) {
|
||||
if (count <= 0) return callback(null, null)
|
||||
sendDmpWorker({
|
||||
msg: 'get revision',
|
||||
revisions,
|
||||
count
|
||||
}, callback)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.checkAllNotesRevision = function (callback) {
|
||||
Revision.saveAllNotesRevision(function (err, notes) {
|
||||
Revision.checkAllNotesRevision = function (callback: DmpCallback): void {
|
||||
Revision.saveAllNotesRevision(function (err: any, notes: any) {
|
||||
if (err) return callback(err, null)
|
||||
if (!notes || notes.length <= 0) {
|
||||
return callback(null, notes)
|
||||
@@ -171,7 +180,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
})
|
||||
}
|
||||
Revision.saveAllNotesRevision = function (callback) {
|
||||
Revision.saveAllNotesRevision = function (callback: DmpCallback): void {
|
||||
sequelize.models.Note.findAll({
|
||||
// query all notes that need to save for revision
|
||||
where: {
|
||||
@@ -197,10 +206,10 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
]
|
||||
}
|
||||
}).then(function (notes) {
|
||||
}).then(function (notes: any[]) {
|
||||
if (notes.length <= 0) return callback(null, notes)
|
||||
const savedNotes = []
|
||||
async.each(notes, function (note, _callback) {
|
||||
const savedNotes: any[] = []
|
||||
async.each(notes, function (note: any, _callback: any) {
|
||||
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
|
||||
if (note.lastchangeAt && note.savedAt) {
|
||||
const lastchangeAt = moment(note.lastchangeAt)
|
||||
@@ -218,7 +227,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
}
|
||||
}, function (err) {
|
||||
}, function (err: any) {
|
||||
if (err) {
|
||||
return callback(err, null)
|
||||
}
|
||||
@@ -226,17 +235,17 @@ module.exports = function (sequelize, DataTypes) {
|
||||
const result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
|
||||
return callback(null, result)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.saveNoteRevision = function (note, callback) {
|
||||
Revision.saveNoteRevision = function (note: any, callback: DmpCallback): void {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
}).then(function (revisions: any[]) {
|
||||
if (revisions.length <= 0) {
|
||||
// if no revision available
|
||||
Revision.create({
|
||||
@@ -244,9 +253,9 @@ module.exports = function (sequelize, DataTypes) {
|
||||
lastContent: note.content ? note.content : '',
|
||||
length: note.content ? note.content.length : 0,
|
||||
authorship: note.authorship
|
||||
}).then(function (revision) {
|
||||
}).then(function (revision: any) {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
} else {
|
||||
@@ -257,16 +266,16 @@ module.exports = function (sequelize, DataTypes) {
|
||||
msg: 'create patch',
|
||||
lastDoc: lastContent,
|
||||
currDoc: content
|
||||
}, function (err, patch) {
|
||||
}, function (err: any, patch: any) {
|
||||
if (err) logger.error('save note revision error', err)
|
||||
if (!patch) {
|
||||
// if patch is empty (means no difference) then just update the latest revision updated time
|
||||
latestRevision.changed('updatedAt', true)
|
||||
latestRevision.update({
|
||||
updatedAt: Date.now()
|
||||
}).then(function (revision) {
|
||||
}).then(function (revision: any) {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
} else {
|
||||
@@ -276,31 +285,31 @@ module.exports = function (sequelize, DataTypes) {
|
||||
content: note.content,
|
||||
length: note.content.length,
|
||||
authorship: note.authorship
|
||||
}).then(function (revision) {
|
||||
}).then(function (revision: any) {
|
||||
// clear last revision content to reduce db size
|
||||
latestRevision.update({
|
||||
content: null
|
||||
}).then(function () {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.finishSaveNoteRevision = function (note, revision, callback) {
|
||||
Revision.finishSaveNoteRevision = function (note: any, revision: any, callback: DmpCallback): void {
|
||||
note.update({
|
||||
savedAt: revision.updatedAt
|
||||
}).then(function () {
|
||||
return callback(null, revision)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const nanoid = require('nanoid')
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
module.exports = function (sequelize: any, DataTypes: any): any {
|
||||
const Temp = sequelize.define('Temp', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
defaultValue: nanoid.nanoid
|
||||
defaultValue: nanoid
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.TEXT
|
||||
@@ -1,14 +1,14 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const Sequelize = require('sequelize')
|
||||
const scrypt = require('scrypt-kdf')
|
||||
const filterXSS = require('xss')
|
||||
import scrypt = require('scrypt-kdf')
|
||||
const filterXSS: any = require('xss')
|
||||
|
||||
// core
|
||||
const logger = require('../logger')
|
||||
const { generateAvatarURL } = require('../letter-avatars')
|
||||
import logger = require('../logger')
|
||||
import { generateAvatarURL } from '../letter-avatars'
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
module.exports = function (sequelize: any, DataTypes: any): any {
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -46,11 +46,11 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
})
|
||||
|
||||
User.prototype.verifyPassword = function (attempt) {
|
||||
User.prototype.verifyPassword = function (attempt: string): Promise<boolean> {
|
||||
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
|
||||
}
|
||||
|
||||
User.associate = function (models) {
|
||||
User.associate = function (models: any): void {
|
||||
User.hasMany(models.Note, {
|
||||
foreignKey: 'ownerId',
|
||||
constraints: false
|
||||
@@ -60,13 +60,13 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
User.getProfile = function (user) {
|
||||
User.getProfile = function (user: any): any {
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
|
||||
}
|
||||
User.parseProfile = function (profile) {
|
||||
User.parseProfile = function (profile: any): any {
|
||||
try {
|
||||
profile = JSON.parse(profile)
|
||||
} catch (err) {
|
||||
@@ -82,8 +82,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return profile
|
||||
}
|
||||
User.parsePhotoByProfile = function (profile, bigger) {
|
||||
let photo = null
|
||||
User.parsePhotoByProfile = function (profile: any, bigger?: boolean): string | null {
|
||||
let photo: string | null = null
|
||||
switch (profile.provider) {
|
||||
case 'facebook':
|
||||
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
|
||||
@@ -123,8 +123,8 @@ module.exports = function (sequelize, DataTypes) {
|
||||
break
|
||||
case 'google':
|
||||
photo = profile.photos[0].value
|
||||
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
|
||||
if (bigger) photo = photo!.replace(/(\?sz=)\d*$/i, '$1400')
|
||||
else photo = photo!.replace(/(\?sz=)\d*$/i, '$196')
|
||||
break
|
||||
case 'ldap':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
@@ -142,7 +142,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
return filterXSS(photo)
|
||||
}
|
||||
User.parseProfileByEmail = function (email) {
|
||||
User.parseProfileByEmail = function (email: string): { name: string; photo: string; biggerphoto: string } {
|
||||
return {
|
||||
name: email.substring(0, email.lastIndexOf('@')),
|
||||
photo: generateAvatarURL('', email, false),
|
||||
@@ -150,7 +150,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
}
|
||||
}
|
||||
|
||||
function updatePasswordHashHook (user, options) {
|
||||
function updatePasswordHashHook (user: any, options: any): Promise<void> {
|
||||
// suggested way to hash passwords to be able to do this asynchronously:
|
||||
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
|
||||
|
||||
@@ -158,7 +158,7 @@ module.exports = function (sequelize, DataTypes) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
|
||||
return scrypt.kdf(user.getDataValue('password'), { logN: 15 } as any).then((keyBuf: Buffer) => {
|
||||
user.setDataValue('password', keyBuf.toString('hex'))
|
||||
})
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
|
||||
|
||||
if (typeof ot === 'undefined') {
|
||||
var ot = {};
|
||||
}
|
||||
|
||||
ot.Client = (function (global) {
|
||||
'use strict';
|
||||
|
||||
// Client constructor
|
||||
function Client (revision) {
|
||||
this.revision = revision; // the next expected revision number
|
||||
this.setState(synchronized_); // start state
|
||||
}
|
||||
|
||||
Client.prototype.setState = function (state) {
|
||||
this.state = state;
|
||||
};
|
||||
|
||||
// Call this method when the user changes the document.
|
||||
Client.prototype.applyClient = function (operation) {
|
||||
this.setState(this.state.applyClient(this, operation));
|
||||
};
|
||||
|
||||
// Call this method with a new operation from the server
|
||||
Client.prototype.applyServer = function (revision, operation) {
|
||||
this.setState(this.state.applyServer(this, revision, operation));
|
||||
};
|
||||
|
||||
Client.prototype.applyOperations = function (head, operations) {
|
||||
this.setState(this.state.applyOperations(this, head, operations));
|
||||
};
|
||||
|
||||
Client.prototype.serverAck = function (revision) {
|
||||
this.setState(this.state.serverAck(this, revision));
|
||||
};
|
||||
|
||||
Client.prototype.serverReconnect = function () {
|
||||
if (typeof this.state.resend === 'function') { this.state.resend(this); }
|
||||
};
|
||||
|
||||
// Transforms a selection from the latest known server state to the current
|
||||
// client state. For example, if we get from the server the information that
|
||||
// another user's cursor is at position 3, but the server hasn't yet received
|
||||
// our newest operation, an insertion of 5 characters at the beginning of the
|
||||
// document, the correct position of the other user's cursor in our current
|
||||
// document is 8.
|
||||
Client.prototype.transformSelection = function (selection) {
|
||||
return this.state.transformSelection(selection);
|
||||
};
|
||||
|
||||
// Override this method.
|
||||
Client.prototype.sendOperation = function (revision, operation) {
|
||||
throw new Error("sendOperation must be defined in child class");
|
||||
};
|
||||
|
||||
// Override this method.
|
||||
Client.prototype.applyOperation = function (operation) {
|
||||
throw new Error("applyOperation must be defined in child class");
|
||||
};
|
||||
|
||||
|
||||
// In the 'Synchronized' state, there is no pending operation that the client
|
||||
// has sent to the server.
|
||||
function Synchronized () {}
|
||||
Client.Synchronized = Synchronized;
|
||||
|
||||
Synchronized.prototype.applyClient = function (client, operation) {
|
||||
// When the user makes an edit, send the operation to the server and
|
||||
// switch to the 'AwaitingConfirm' state
|
||||
client.sendOperation(client.revision, operation);
|
||||
return new AwaitingConfirm(operation);
|
||||
};
|
||||
|
||||
Synchronized.prototype.applyServer = function (client, revision, operation) {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// When we receive a new operation from the server, the operation can be
|
||||
// simply applied to the current document
|
||||
client.applyOperation(operation);
|
||||
return this;
|
||||
};
|
||||
|
||||
Synchronized.prototype.serverAck = function (client, revision) {
|
||||
throw new Error("There is no pending operation.");
|
||||
};
|
||||
|
||||
// Nothing to do because the latest server state and client state are the same.
|
||||
Synchronized.prototype.transformSelection = function (x) { return x; };
|
||||
|
||||
// Singleton
|
||||
var synchronized_ = new Synchronized();
|
||||
|
||||
|
||||
// In the 'AwaitingConfirm' state, there's one operation the client has sent
|
||||
// to the server and is still waiting for an acknowledgement.
|
||||
function AwaitingConfirm (outstanding) {
|
||||
// Save the pending operation
|
||||
this.outstanding = outstanding;
|
||||
}
|
||||
Client.AwaitingConfirm = AwaitingConfirm;
|
||||
|
||||
AwaitingConfirm.prototype.applyClient = function (client, operation) {
|
||||
// When the user makes an edit, don't send the operation immediately,
|
||||
// instead switch to 'AwaitingWithBuffer' state
|
||||
return new AwaitingWithBuffer(this.outstanding, operation);
|
||||
};
|
||||
|
||||
AwaitingConfirm.prototype.applyServer = function (client, revision, operation) {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// This is another client's operation. Visualization:
|
||||
//
|
||||
// /\
|
||||
// this.outstanding / \ operation
|
||||
// / \
|
||||
// \ /
|
||||
// pair[1] \ / pair[0] (new outstanding)
|
||||
// (can be applied \/
|
||||
// to the client's
|
||||
// current document)
|
||||
var pair = operation.constructor.transform(this.outstanding, operation);
|
||||
client.applyOperation(pair[1]);
|
||||
return new AwaitingConfirm(pair[0]);
|
||||
};
|
||||
|
||||
AwaitingConfirm.prototype.serverAck = function (client, revision) {
|
||||
if (revision - client.revision > 1) {
|
||||
return new Stale(this.outstanding, client, revision).getOperations();
|
||||
}
|
||||
client.revision = revision;
|
||||
// The client's operation has been acknowledged
|
||||
// => switch to synchronized state
|
||||
return synchronized_;
|
||||
};
|
||||
|
||||
AwaitingConfirm.prototype.transformSelection = function (selection) {
|
||||
return selection.transform(this.outstanding);
|
||||
};
|
||||
|
||||
AwaitingConfirm.prototype.resend = function (client) {
|
||||
// The confirm didn't come because the client was disconnected.
|
||||
// Now that it has reconnected, we resend the outstanding operation.
|
||||
client.sendOperation(client.revision, this.outstanding);
|
||||
};
|
||||
|
||||
|
||||
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
|
||||
// to be acknowledged by the server while buffering the edits the user makes
|
||||
function AwaitingWithBuffer (outstanding, buffer) {
|
||||
// Save the pending operation and the user's edits since then
|
||||
this.outstanding = outstanding;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
Client.AwaitingWithBuffer = AwaitingWithBuffer;
|
||||
|
||||
AwaitingWithBuffer.prototype.applyClient = function (client, operation) {
|
||||
// Compose the user's changes onto the buffer
|
||||
var newBuffer = this.buffer.compose(operation);
|
||||
return new AwaitingWithBuffer(this.outstanding, newBuffer);
|
||||
};
|
||||
|
||||
AwaitingWithBuffer.prototype.applyServer = function (client, revision, operation) {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// Operation comes from another client
|
||||
//
|
||||
// /\
|
||||
// this.outstanding / \ operation
|
||||
// / \
|
||||
// /\ /
|
||||
// this.buffer / \* / pair1[0] (new outstanding)
|
||||
// / \/
|
||||
// \ /
|
||||
// pair2[1] \ / pair2[0] (new buffer)
|
||||
// the transformed \/
|
||||
// operation -- can
|
||||
// be applied to the
|
||||
// client's current
|
||||
// document
|
||||
//
|
||||
// * pair1[1]
|
||||
var transform = operation.constructor.transform;
|
||||
var pair1 = transform(this.outstanding, operation);
|
||||
var pair2 = transform(this.buffer, pair1[1]);
|
||||
client.applyOperation(pair2[1]);
|
||||
return new AwaitingWithBuffer(pair1[0], pair2[0]);
|
||||
};
|
||||
|
||||
AwaitingWithBuffer.prototype.serverAck = function (client, revision) {
|
||||
if (revision - client.revision > 1) {
|
||||
return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
|
||||
}
|
||||
client.revision = revision;
|
||||
// The pending operation has been acknowledged
|
||||
// => send buffer
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
};
|
||||
|
||||
AwaitingWithBuffer.prototype.transformSelection = function (selection) {
|
||||
return selection.transform(this.outstanding).transform(this.buffer);
|
||||
};
|
||||
|
||||
AwaitingWithBuffer.prototype.resend = function (client) {
|
||||
// The confirm didn't come because the client was disconnected.
|
||||
// Now that it has reconnected, we resend the outstanding operation.
|
||||
client.sendOperation(client.revision, this.outstanding);
|
||||
};
|
||||
|
||||
|
||||
function Stale(acknowlaged, client, revision) {
|
||||
this.acknowlaged = acknowlaged;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
}
|
||||
Client.Stale = Stale;
|
||||
|
||||
Stale.prototype.applyClient = function (client, operation) {
|
||||
return new StaleWithBuffer(this.acknowlaged, operation, client, this.revision);
|
||||
};
|
||||
|
||||
Stale.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
};
|
||||
|
||||
Stale.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair = transform(this.acknowlaged, op);
|
||||
client.applyOperation(pair[1]);
|
||||
this.acknowlaged = pair[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
return synchronized_;
|
||||
};
|
||||
|
||||
Stale.prototype.serverAck = function (client, revision) {
|
||||
throw new Error("There is no pending operation.");
|
||||
};
|
||||
|
||||
Stale.prototype.transformSelection = function (selection) {
|
||||
return selection;
|
||||
};
|
||||
|
||||
Stale.prototype.getOperations = function () {
|
||||
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
function StaleWithBuffer(acknowlaged, buffer, client, revision) {
|
||||
this.acknowlaged = acknowlaged;
|
||||
this.buffer = buffer;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
}
|
||||
Client.StaleWithBuffer = StaleWithBuffer;
|
||||
|
||||
StaleWithBuffer.prototype.applyClient = function (client, operation) {
|
||||
var buffer = this.buffer.compose(operation);
|
||||
return new StaleWithBuffer(this.acknowlaged, buffer, client, this.revision);
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyServer = function (client, revision, operation) {
|
||||
throw new Error("Ignored server-side change.");
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.applyOperations = function (client, head, operations) {
|
||||
var transform = this.acknowlaged.constructor.transform;
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
var op = ot.TextOperation.fromJSON(operations[i]);
|
||||
var pair1 = transform(this.acknowlaged, op);
|
||||
var pair2 = transform(this.buffer, pair1[1]);
|
||||
client.applyOperation(pair2[1]);
|
||||
this.acknowlaged = pair1[0];
|
||||
this.buffer = pair2[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.serverAck = function (client, revision) {
|
||||
throw new Error("There is no pending operation.");
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.transformSelection = function (selection) {
|
||||
return selection;
|
||||
};
|
||||
|
||||
StaleWithBuffer.prototype.getOperations = function () {
|
||||
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
return Client;
|
||||
|
||||
}(this));
|
||||
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.Client;
|
||||
}
|
||||
|
||||
Executable
+351
@@ -0,0 +1,351 @@
|
||||
// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala
|
||||
|
||||
'use strict';
|
||||
|
||||
import TextOperation = require('./text-operation');
|
||||
import WrappedOperation = require('./wrapped-operation');
|
||||
|
||||
interface ClientState {
|
||||
applyClient(client: Client, operation: WrappedOperation): ClientState;
|
||||
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState;
|
||||
applyOperations?(client: Client, head: number, operations: (number | string)[][]): ClientState;
|
||||
serverAck(client: Client, revision: number): ClientState;
|
||||
transformSelection(selection: any): any;
|
||||
resend?(client: Client): void;
|
||||
getOperations?(): ClientState;
|
||||
}
|
||||
|
||||
// Client constructor
|
||||
class Client {
|
||||
revision: number;
|
||||
state: ClientState;
|
||||
|
||||
static Synchronized: typeof Synchronized;
|
||||
static AwaitingConfirm: typeof AwaitingConfirm;
|
||||
static AwaitingWithBuffer: typeof AwaitingWithBuffer;
|
||||
static Stale: typeof Stale;
|
||||
static StaleWithBuffer: typeof StaleWithBuffer;
|
||||
|
||||
constructor(revision: number) {
|
||||
this.revision = revision; // the next expected revision number
|
||||
this.state = synchronized_; // start state
|
||||
}
|
||||
|
||||
setState(state: ClientState): void {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
// Call this method when the user changes the document.
|
||||
applyClient(operation: WrappedOperation): void {
|
||||
this.setState(this.state.applyClient(this, operation));
|
||||
}
|
||||
|
||||
// Call this method with a new operation from the server
|
||||
applyServer(revision: number, operation: WrappedOperation): void {
|
||||
this.setState(this.state.applyServer(this, revision, operation));
|
||||
}
|
||||
|
||||
applyOperations(head: number, operations: (number | string)[][]): void {
|
||||
this.setState(this.state.applyOperations!(this, head, operations));
|
||||
}
|
||||
|
||||
serverAck(revision: number): void {
|
||||
this.setState(this.state.serverAck(this, revision));
|
||||
}
|
||||
|
||||
serverReconnect(): void {
|
||||
if (typeof this.state.resend === 'function') { this.state.resend(this); }
|
||||
}
|
||||
|
||||
// Transforms a selection from the latest known server state to the current
|
||||
// client state. For example, if we get from the server the information that
|
||||
// another user's cursor is at position 3, but the server hasn't yet received
|
||||
// our newest operation, an insertion of 5 characters at the beginning of the
|
||||
// document, the correct position of the other user's cursor in our current
|
||||
// document is 8.
|
||||
transformSelection(selection: any): any {
|
||||
return this.state.transformSelection(selection);
|
||||
}
|
||||
|
||||
// Override this method.
|
||||
sendOperation(revision: number, operation: WrappedOperation): void {
|
||||
throw new Error("sendOperation must be defined in child class");
|
||||
}
|
||||
|
||||
// Override this method.
|
||||
applyOperation(operation: WrappedOperation): void {
|
||||
throw new Error("applyOperation must be defined in child class");
|
||||
}
|
||||
|
||||
// Override this method.
|
||||
getOperations(base: number, head: number): void {
|
||||
throw new Error("getOperations must be defined in child class");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// In the 'Synchronized' state, there is no pending operation that the client
|
||||
// has sent to the server.
|
||||
class Synchronized implements ClientState {
|
||||
applyClient(client: Client, operation: WrappedOperation): ClientState {
|
||||
// When the user makes an edit, send the operation to the server and
|
||||
// switch to the 'AwaitingConfirm' state
|
||||
client.sendOperation(client.revision, operation);
|
||||
return new AwaitingConfirm(operation);
|
||||
}
|
||||
|
||||
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// When we receive a new operation from the server, the operation can be
|
||||
// simply applied to the current document
|
||||
client.applyOperation(operation);
|
||||
return this;
|
||||
}
|
||||
|
||||
serverAck(_client: Client, _revision: number): ClientState {
|
||||
throw new Error("There is no pending operation.");
|
||||
}
|
||||
|
||||
// Nothing to do because the latest server state and client state are the same.
|
||||
transformSelection(x: any): any { return x; }
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const synchronized_ = new Synchronized();
|
||||
|
||||
|
||||
// In the 'AwaitingConfirm' state, there's one operation the client has sent
|
||||
// to the server and is still waiting for an acknowledgement.
|
||||
class AwaitingConfirm implements ClientState {
|
||||
outstanding: WrappedOperation;
|
||||
|
||||
constructor(outstanding: WrappedOperation) {
|
||||
// Save the pending operation
|
||||
this.outstanding = outstanding;
|
||||
}
|
||||
|
||||
applyClient(_client: Client, operation: WrappedOperation): ClientState {
|
||||
// When the user makes an edit, don't send the operation immediately,
|
||||
// instead switch to 'AwaitingWithBuffer' state
|
||||
return new AwaitingWithBuffer(this.outstanding, operation);
|
||||
}
|
||||
|
||||
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// This is another client's operation. Visualization:
|
||||
//
|
||||
// /\
|
||||
// this.outstanding / \ operation
|
||||
// / \
|
||||
// \ /
|
||||
// pair[1] \ / pair[0] (new outstanding)
|
||||
// (can be applied \/
|
||||
// to the client's
|
||||
// current document)
|
||||
const pair = (operation.constructor as typeof WrappedOperation).transform(this.outstanding, operation);
|
||||
client.applyOperation(pair[1]);
|
||||
return new AwaitingConfirm(pair[0]);
|
||||
}
|
||||
|
||||
serverAck(client: Client, revision: number): ClientState {
|
||||
if (revision - client.revision > 1) {
|
||||
return new Stale(this.outstanding, client, revision).getOperations();
|
||||
}
|
||||
client.revision = revision;
|
||||
// The client's operation has been acknowledged
|
||||
// => switch to synchronized state
|
||||
return synchronized_;
|
||||
}
|
||||
|
||||
transformSelection(selection: any): any {
|
||||
return selection.transform(this.outstanding);
|
||||
}
|
||||
|
||||
resend(client: Client): void {
|
||||
// The confirm didn't come because the client was disconnected.
|
||||
// Now that it has reconnected, we resend the outstanding operation.
|
||||
client.sendOperation(client.revision, this.outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// In the 'AwaitingWithBuffer' state, the client is waiting for an operation
|
||||
// to be acknowledged by the server while buffering the edits the user makes
|
||||
class AwaitingWithBuffer implements ClientState {
|
||||
outstanding: WrappedOperation;
|
||||
buffer: WrappedOperation;
|
||||
|
||||
constructor(outstanding: WrappedOperation, buffer: WrappedOperation) {
|
||||
// Save the pending operation and the user's edits since then
|
||||
this.outstanding = outstanding;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
applyClient(_client: Client, operation: WrappedOperation): ClientState {
|
||||
// Compose the user's changes onto the buffer
|
||||
const newBuffer = this.buffer.compose(operation);
|
||||
return new AwaitingWithBuffer(this.outstanding, newBuffer);
|
||||
}
|
||||
|
||||
applyServer(client: Client, revision: number, operation: WrappedOperation): ClientState {
|
||||
if (revision - client.revision > 1) {
|
||||
throw new Error("Invalid revision.");
|
||||
}
|
||||
client.revision = revision;
|
||||
// Operation comes from another client
|
||||
//
|
||||
// /\
|
||||
// this.outstanding / \ operation
|
||||
// / \
|
||||
// /\ /
|
||||
// this.buffer / \* / pair1[0] (new outstanding)
|
||||
// / \/
|
||||
// \ /
|
||||
// pair2[1] \ / pair2[0] (new buffer)
|
||||
// the transformed \/
|
||||
// operation -- can
|
||||
// be applied to the
|
||||
// client's current
|
||||
// document
|
||||
//
|
||||
// * pair1[1]
|
||||
const transform = (operation.constructor as typeof WrappedOperation).transform;
|
||||
const pair1 = transform(this.outstanding, operation);
|
||||
const pair2 = transform(this.buffer, pair1[1]);
|
||||
client.applyOperation(pair2[1]);
|
||||
return new AwaitingWithBuffer(pair1[0], pair2[0]);
|
||||
}
|
||||
|
||||
serverAck(client: Client, revision: number): ClientState {
|
||||
if (revision - client.revision > 1) {
|
||||
return new StaleWithBuffer(this.outstanding, this.buffer, client, revision).getOperations();
|
||||
}
|
||||
client.revision = revision;
|
||||
// The pending operation has been acknowledged
|
||||
// => send buffer
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
}
|
||||
|
||||
transformSelection(selection: any): any {
|
||||
return selection.transform(this.outstanding).transform(this.buffer);
|
||||
}
|
||||
|
||||
resend(client: Client): void {
|
||||
// The confirm didn't come because the client was disconnected.
|
||||
// Now that it has reconnected, we resend the outstanding operation.
|
||||
client.sendOperation(client.revision, this.outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Stale implements ClientState {
|
||||
acknowledged: WrappedOperation;
|
||||
client: Client;
|
||||
revision: number;
|
||||
|
||||
constructor(acknowledged: WrappedOperation, client: Client, revision: number) {
|
||||
this.acknowledged = acknowledged;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
applyClient(client: Client, operation: WrappedOperation): ClientState {
|
||||
return new StaleWithBuffer(this.acknowledged, operation, client, this.revision);
|
||||
}
|
||||
|
||||
applyServer(_client: Client, _revision: number, _operation: WrappedOperation): ClientState {
|
||||
throw new Error("Ignored server-side change.");
|
||||
}
|
||||
|
||||
applyOperations(client: Client, _head: number, operations: (number | string)[][]): ClientState {
|
||||
const transform = (this.acknowledged.constructor as typeof WrappedOperation).transform;
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
const op = TextOperation.fromJSON(operations[i]);
|
||||
const pair = transform(this.acknowledged, op as any);
|
||||
client.applyOperation(pair[1]);
|
||||
this.acknowledged = pair[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
return synchronized_;
|
||||
}
|
||||
|
||||
serverAck(_client: Client, _revision: number): ClientState {
|
||||
throw new Error("There is no pending operation.");
|
||||
}
|
||||
|
||||
transformSelection(selection: any): any {
|
||||
return selection;
|
||||
}
|
||||
|
||||
getOperations(): ClientState {
|
||||
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StaleWithBuffer implements ClientState {
|
||||
acknowledged: WrappedOperation;
|
||||
buffer: WrappedOperation;
|
||||
client: Client;
|
||||
revision: number;
|
||||
|
||||
constructor(acknowledged: WrappedOperation, buffer: WrappedOperation, client: Client, revision: number) {
|
||||
this.acknowledged = acknowledged;
|
||||
this.buffer = buffer;
|
||||
this.client = client;
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
applyClient(client: Client, operation: WrappedOperation): ClientState {
|
||||
const buffer = this.buffer.compose(operation);
|
||||
return new StaleWithBuffer(this.acknowledged, buffer, client, this.revision);
|
||||
}
|
||||
|
||||
applyServer(_client: Client, _revision: number, _operation: WrappedOperation): ClientState {
|
||||
throw new Error("Ignored server-side change.");
|
||||
}
|
||||
|
||||
applyOperations(client: Client, _head: number, operations: (number | string)[][]): ClientState {
|
||||
const transform = (this.acknowledged.constructor as typeof WrappedOperation).transform;
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
const op = TextOperation.fromJSON(operations[i]);
|
||||
const pair1 = transform(this.acknowledged, op as any);
|
||||
const pair2 = transform(this.buffer, pair1[1]);
|
||||
client.applyOperation(pair2[1]);
|
||||
this.acknowledged = pair1[0];
|
||||
this.buffer = pair2[0];
|
||||
}
|
||||
client.revision = this.revision;
|
||||
client.sendOperation(client.revision, this.buffer);
|
||||
return new AwaitingConfirm(this.buffer);
|
||||
}
|
||||
|
||||
serverAck(_client: Client, _revision: number): ClientState {
|
||||
throw new Error("There is no pending operation.");
|
||||
}
|
||||
|
||||
transformSelection(selection: any): any {
|
||||
return selection;
|
||||
}
|
||||
|
||||
getOperations(): ClientState {
|
||||
this.client.getOperations(this.client.revision, this.revision - 1); // acknowlaged is the one at revision
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Client.Synchronized = Synchronized;
|
||||
Client.AwaitingConfirm = AwaitingConfirm;
|
||||
Client.AwaitingWithBuffer = AwaitingWithBuffer;
|
||||
Client.Stale = Stale;
|
||||
Client.StaleWithBuffer = StaleWithBuffer;
|
||||
|
||||
export = Client;
|
||||
@@ -1,164 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var TextOperation = require('./text-operation');
|
||||
var WrappedOperation = require('./wrapped-operation');
|
||||
var Server = require('./server');
|
||||
var Selection = require('./selection');
|
||||
var util = require('util');
|
||||
|
||||
var logger = require('../logger');
|
||||
|
||||
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
|
||||
EventEmitter.call(this);
|
||||
Server.call(this, document, operations);
|
||||
this.users = {};
|
||||
this.docId = docId;
|
||||
this.mayWrite = mayWrite || function (_, cb) {
|
||||
cb(true);
|
||||
};
|
||||
this.operationCallback = operationCallback;
|
||||
}
|
||||
|
||||
util.inherits(EditorSocketIOServer, Server);
|
||||
extend(EditorSocketIOServer.prototype, EventEmitter.prototype);
|
||||
|
||||
function extend(target, source) {
|
||||
for (var key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorSocketIOServer.prototype.addClient = function (socket) {
|
||||
var self = this;
|
||||
socket.join(this.docId);
|
||||
var docOut = {
|
||||
str: this.document,
|
||||
revision: this.operations.length,
|
||||
clients: this.users
|
||||
};
|
||||
socket.emit('doc', docOut);
|
||||
socket.on('operation', function (revision, operation, selection) {
|
||||
socket.origin = 'operation';
|
||||
self.mayWrite(socket, function (mayWrite) {
|
||||
if (!mayWrite) {
|
||||
logger.info("User doesn't have the right to edit.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self.onOperation(socket, revision, operation, selection);
|
||||
if (typeof self.operationCallback === 'function')
|
||||
self.operationCallback(socket, operation);
|
||||
} catch (err) {
|
||||
setTimeout(function() {
|
||||
var docOut = {
|
||||
str: self.document,
|
||||
revision: self.operations.length,
|
||||
clients: self.users,
|
||||
force: true
|
||||
};
|
||||
socket.emit('doc', docOut);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on('get_operations', function (base, head) {
|
||||
self.onGetOperations(socket, base, head);
|
||||
});
|
||||
socket.on('selection', function (obj) {
|
||||
socket.origin = 'selection';
|
||||
self.mayWrite(socket, function (mayWrite) {
|
||||
if (!mayWrite) {
|
||||
logger.info("User doesn't have the right to edit.");
|
||||
return;
|
||||
}
|
||||
self.updateSelection(socket, obj && Selection.fromJSON(obj));
|
||||
});
|
||||
});
|
||||
socket.on('disconnect', function () {
|
||||
logger.debug("Disconnect");
|
||||
socket.leave(self.docId);
|
||||
self.onDisconnect(socket);
|
||||
/*
|
||||
if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
|
||||
self.emit('empty-room');
|
||||
}
|
||||
*/
|
||||
});
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.onOperation = function (socket, revision, operation, selection) {
|
||||
var wrapped;
|
||||
try {
|
||||
wrapped = new WrappedOperation(
|
||||
TextOperation.fromJSON(operation),
|
||||
selection && Selection.fromJSON(selection)
|
||||
);
|
||||
} catch (exc) {
|
||||
logger.error("Invalid operation received: ");
|
||||
logger.error(exc);
|
||||
throw new Error(exc);
|
||||
}
|
||||
|
||||
try {
|
||||
var clientId = socket.id;
|
||||
var wrappedPrime = this.receiveOperation(revision, wrapped);
|
||||
if(!wrappedPrime) return;
|
||||
logger.debug("new operation: " + JSON.stringify(wrapped));
|
||||
this.getClient(clientId).selection = wrappedPrime.meta;
|
||||
revision = this.operations.length;
|
||||
socket.emit('ack', revision);
|
||||
socket.broadcast.in(this.docId).emit(
|
||||
'operation', clientId, revision,
|
||||
wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
|
||||
);
|
||||
//set document is dirty
|
||||
this.isDirty = true;
|
||||
} catch (exc) {
|
||||
logger.error(exc);
|
||||
throw new Error(exc);
|
||||
}
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.onGetOperations = function (socket, base, head) {
|
||||
var operations = this.operations.slice(base, head).map(function (op) {
|
||||
return op.wrapped.toJSON();
|
||||
});
|
||||
socket.emit('operations', head, operations);
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.updateSelection = function (socket, selection) {
|
||||
var clientId = socket.id;
|
||||
if (selection) {
|
||||
this.getClient(clientId).selection = selection;
|
||||
} else {
|
||||
delete this.getClient(clientId).selection;
|
||||
}
|
||||
socket.broadcast.to(this.docId).emit('selection', clientId, selection);
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.setName = function (socket, name) {
|
||||
var clientId = socket.id;
|
||||
this.getClient(clientId).name = name;
|
||||
socket.broadcast.to(this.docId).emit('set_name', clientId, name);
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.setColor = function (socket, color) {
|
||||
var clientId = socket.id;
|
||||
this.getClient(clientId).color = color;
|
||||
socket.broadcast.to(this.docId).emit('set_color', clientId, color);
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.getClient = function (clientId) {
|
||||
return this.users[clientId] || (this.users[clientId] = {});
|
||||
};
|
||||
|
||||
EditorSocketIOServer.prototype.onDisconnect = function (socket) {
|
||||
var clientId = socket.id;
|
||||
delete this.users[clientId];
|
||||
socket.broadcast.to(this.docId).emit('client_left', clientId);
|
||||
};
|
||||
|
||||
module.exports = EditorSocketIOServer;
|
||||
Executable
+179
@@ -0,0 +1,179 @@
|
||||
'use strict';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import TextOperation = require('./text-operation');
|
||||
import WrappedOperation = require('./wrapped-operation');
|
||||
import Server = require('./server');
|
||||
import Selection = require('./selection');
|
||||
|
||||
import logger from '../logger';
|
||||
|
||||
interface UserInfo {
|
||||
name?: string;
|
||||
color?: string;
|
||||
selection?: Selection;
|
||||
}
|
||||
|
||||
interface Users {
|
||||
[clientId: string]: UserInfo;
|
||||
}
|
||||
|
||||
class EditorSocketIOServer extends Server {
|
||||
users: Users;
|
||||
docId: string;
|
||||
mayWrite: (socket: any, cb: (allowed: boolean) => void) => void;
|
||||
operationCallback?: (socket: any, operation: any) => void;
|
||||
isDirty: boolean;
|
||||
|
||||
constructor(
|
||||
document: string,
|
||||
operations: WrappedOperation[],
|
||||
docId: string,
|
||||
mayWrite?: (socket: any, cb: (allowed: boolean) => void) => void,
|
||||
operationCallback?: (socket: any, operation: any) => void
|
||||
) {
|
||||
super(document, operations);
|
||||
this.users = {};
|
||||
this.docId = docId;
|
||||
this.mayWrite = mayWrite || function (_, cb) {
|
||||
cb(true);
|
||||
};
|
||||
this.operationCallback = operationCallback;
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
addClient(socket: any): void {
|
||||
const self = this;
|
||||
socket.join(this.docId);
|
||||
const docOut = {
|
||||
str: this.document,
|
||||
revision: this.operations.length,
|
||||
clients: this.users
|
||||
};
|
||||
socket.emit('doc', docOut);
|
||||
socket.on('operation', function (revision: number, operation: any, selection: any) {
|
||||
socket.origin = 'operation';
|
||||
self.mayWrite(socket, function (mayWrite: boolean) {
|
||||
if (!mayWrite) {
|
||||
logger.info("User doesn't have the right to edit.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self.onOperation(socket, revision, operation, selection);
|
||||
if (typeof self.operationCallback === 'function')
|
||||
self.operationCallback(socket, operation);
|
||||
} catch (err) {
|
||||
setTimeout(function () {
|
||||
const docOut = {
|
||||
str: self.document,
|
||||
revision: self.operations.length,
|
||||
clients: self.users,
|
||||
force: true
|
||||
};
|
||||
socket.emit('doc', docOut);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
socket.on('get_operations', function (base: number, head: number) {
|
||||
self.onGetOperations(socket, base, head);
|
||||
});
|
||||
socket.on('selection', function (obj: any) {
|
||||
socket.origin = 'selection';
|
||||
self.mayWrite(socket, function (mayWrite: boolean) {
|
||||
if (!mayWrite) {
|
||||
logger.info("User doesn't have the right to edit.");
|
||||
return;
|
||||
}
|
||||
self.updateSelection(socket, obj && Selection.fromJSON(obj));
|
||||
});
|
||||
});
|
||||
socket.on('disconnect', function () {
|
||||
logger.debug("Disconnect");
|
||||
socket.leave(self.docId);
|
||||
self.onDisconnect(socket);
|
||||
/*
|
||||
if (socket.manager && socket.manager.sockets.clients(self.docId).length === 0) {
|
||||
self.emit('empty-room');
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
onOperation(socket: any, revision: number, operation: any, selection: any): void {
|
||||
let wrapped: WrappedOperation;
|
||||
try {
|
||||
wrapped = new WrappedOperation(
|
||||
TextOperation.fromJSON(operation),
|
||||
selection && Selection.fromJSON(selection)
|
||||
);
|
||||
} catch (exc) {
|
||||
logger.error("Invalid operation received: ");
|
||||
logger.error(exc as any);
|
||||
throw new Error(exc as any);
|
||||
}
|
||||
|
||||
try {
|
||||
const clientId = socket.id;
|
||||
const wrappedPrime = this.receiveOperation(revision, wrapped);
|
||||
if (!wrappedPrime) return;
|
||||
logger.debug("new operation: " + JSON.stringify(wrapped));
|
||||
this.getClient(clientId).selection = wrappedPrime.meta;
|
||||
revision = this.operations.length;
|
||||
socket.emit('ack', revision);
|
||||
socket.broadcast.in(this.docId).emit(
|
||||
'operation', clientId, revision,
|
||||
wrappedPrime.wrapped.toJSON(), wrappedPrime.meta
|
||||
);
|
||||
//set document is dirty
|
||||
this.isDirty = true;
|
||||
} catch (exc) {
|
||||
logger.error(exc as any);
|
||||
throw new Error(exc as any);
|
||||
}
|
||||
}
|
||||
|
||||
onGetOperations(socket: any, base: number, head: number): void {
|
||||
const operations = this.operations.slice(base, head).map(function (op: WrappedOperation) {
|
||||
return op.wrapped.toJSON();
|
||||
});
|
||||
socket.emit('operations', head, operations);
|
||||
}
|
||||
|
||||
updateSelection(socket: any, selection: Selection | null): void {
|
||||
const clientId = socket.id;
|
||||
if (selection) {
|
||||
this.getClient(clientId).selection = selection;
|
||||
} else {
|
||||
delete this.getClient(clientId).selection;
|
||||
}
|
||||
socket.broadcast.to(this.docId).emit('selection', clientId, selection);
|
||||
}
|
||||
|
||||
setName(socket: any, name: string): void {
|
||||
const clientId = socket.id;
|
||||
this.getClient(clientId).name = name;
|
||||
socket.broadcast.to(this.docId).emit('set_name', clientId, name);
|
||||
}
|
||||
|
||||
setColor(socket: any, color: string): void {
|
||||
const clientId = socket.id;
|
||||
this.getClient(clientId).color = color;
|
||||
socket.broadcast.to(this.docId).emit('set_color', clientId, color);
|
||||
}
|
||||
|
||||
getClient(clientId: string): UserInfo {
|
||||
return this.users[clientId] || (this.users[clientId] = {});
|
||||
}
|
||||
|
||||
onDisconnect(socket: any): void {
|
||||
const clientId = socket.id;
|
||||
delete this.users[clientId];
|
||||
socket.broadcast.to(this.docId).emit('client_left', clientId);
|
||||
}
|
||||
}
|
||||
|
||||
// Mixin EventEmitter methods (without overwriting the Server prototype chain)
|
||||
Object.assign(EditorSocketIOServer.prototype, EventEmitter.prototype);
|
||||
|
||||
export = EditorSocketIOServer;
|
||||
@@ -1,8 +0,0 @@
|
||||
exports.version = '0.0.15';
|
||||
|
||||
exports.TextOperation = require('./text-operation');
|
||||
exports.SimpleTextOperation = require('./simple-text-operation');
|
||||
exports.Client = require('./client');
|
||||
exports.Server = require('./server');
|
||||
exports.Selection = require('./selection');
|
||||
exports.EditorSocketIOServer = require('./editor-socketio-server');
|
||||
@@ -0,0 +1,8 @@
|
||||
export const version = '0.0.15';
|
||||
|
||||
export import TextOperation = require('./text-operation');
|
||||
export import SimpleTextOperation = require('./simple-text-operation');
|
||||
export import Client = require('./client');
|
||||
export import Server = require('./server');
|
||||
export import Selection = require('./selection');
|
||||
export import EditorSocketIOServer = require('./editor-socketio-server');
|
||||
@@ -1,117 +0,0 @@
|
||||
if (typeof ot === 'undefined') {
|
||||
// Export for browsers
|
||||
var ot = {};
|
||||
}
|
||||
|
||||
ot.Selection = (function (global) {
|
||||
'use strict';
|
||||
|
||||
var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
|
||||
|
||||
// Range has `anchor` and `head` properties, which are zero-based indices into
|
||||
// the document. The `anchor` is the side of the selection that stays fixed,
|
||||
// `head` is the side of the selection where the cursor is. When both are
|
||||
// equal, the range represents a cursor.
|
||||
function Range (anchor, head) {
|
||||
this.anchor = anchor;
|
||||
this.head = head;
|
||||
}
|
||||
|
||||
Range.fromJSON = function (obj) {
|
||||
return new Range(obj.anchor, obj.head);
|
||||
};
|
||||
|
||||
Range.prototype.equals = function (other) {
|
||||
return this.anchor === other.anchor && this.head === other.head;
|
||||
};
|
||||
|
||||
Range.prototype.isEmpty = function () {
|
||||
return this.anchor === this.head;
|
||||
};
|
||||
|
||||
Range.prototype.transform = function (other) {
|
||||
function transformIndex (index) {
|
||||
var newIndex = index;
|
||||
var ops = other.ops;
|
||||
for (var i = 0, l = other.ops.length; i < l; i++) {
|
||||
if (TextOperation.isRetain(ops[i])) {
|
||||
index -= ops[i];
|
||||
} else if (TextOperation.isInsert(ops[i])) {
|
||||
newIndex += ops[i].length;
|
||||
} else {
|
||||
newIndex -= Math.min(index, -ops[i]);
|
||||
index += ops[i];
|
||||
}
|
||||
if (index < 0) { break; }
|
||||
}
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
var newAnchor = transformIndex(this.anchor);
|
||||
if (this.anchor === this.head) {
|
||||
return new Range(newAnchor, newAnchor);
|
||||
}
|
||||
return new Range(newAnchor, transformIndex(this.head));
|
||||
};
|
||||
|
||||
// A selection is basically an array of ranges. Every range represents a real
|
||||
// selection or a cursor in the document (when the start position equals the
|
||||
// end position of the range). The array must not be empty.
|
||||
function Selection (ranges) {
|
||||
this.ranges = ranges || [];
|
||||
}
|
||||
|
||||
Selection.Range = Range;
|
||||
|
||||
// Convenience method for creating selections only containing a single cursor
|
||||
// and no real selection range.
|
||||
Selection.createCursor = function (position) {
|
||||
return new Selection([new Range(position, position)]);
|
||||
};
|
||||
|
||||
Selection.fromJSON = function (obj) {
|
||||
var objRanges = obj.ranges || obj;
|
||||
for (var i = 0, ranges = []; i < objRanges.length; i++) {
|
||||
ranges[i] = Range.fromJSON(objRanges[i]);
|
||||
}
|
||||
return new Selection(ranges);
|
||||
};
|
||||
|
||||
Selection.prototype.equals = function (other) {
|
||||
if (this.position !== other.position) { return false; }
|
||||
if (this.ranges.length !== other.ranges.length) { return false; }
|
||||
// FIXME: Sort ranges before comparing them?
|
||||
for (var i = 0; i < this.ranges.length; i++) {
|
||||
if (!this.ranges[i].equals(other.ranges[i])) { return false; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Selection.prototype.somethingSelected = function () {
|
||||
for (var i = 0; i < this.ranges.length; i++) {
|
||||
if (!this.ranges[i].isEmpty()) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Return the more current selection information.
|
||||
Selection.prototype.compose = function (other) {
|
||||
return other;
|
||||
};
|
||||
|
||||
// Update the selection with respect to an operation.
|
||||
Selection.prototype.transform = function (other) {
|
||||
for (var i = 0, newRanges = []; i < this.ranges.length; i++) {
|
||||
newRanges[i] = this.ranges[i].transform(other);
|
||||
}
|
||||
return new Selection(newRanges);
|
||||
};
|
||||
|
||||
return Selection;
|
||||
|
||||
}(this));
|
||||
|
||||
// Export for CommonJS
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.Selection;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Export for browsers
|
||||
|
||||
'use strict';
|
||||
|
||||
import TextOperation = require('./text-operation');
|
||||
|
||||
// Range has `anchor` and `head` properties, which are zero-based indices into
|
||||
// the document. The `anchor` is the side of the selection that stays fixed,
|
||||
// `head` is the side of the selection where the cursor is. When both are
|
||||
// equal, the range represents a cursor.
|
||||
class Range {
|
||||
anchor: number;
|
||||
head: number;
|
||||
|
||||
constructor(anchor: number, head: number) {
|
||||
this.anchor = anchor;
|
||||
this.head = head;
|
||||
}
|
||||
|
||||
static fromJSON(obj: { anchor: number; head: number }): Range {
|
||||
return new Range(obj.anchor, obj.head);
|
||||
}
|
||||
|
||||
equals(other: Range): boolean {
|
||||
return this.anchor === other.anchor && this.head === other.head;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.anchor === this.head;
|
||||
}
|
||||
|
||||
transform(other: TextOperation): Range {
|
||||
function transformIndex(index: number): number {
|
||||
let newIndex = index;
|
||||
const ops = other.ops;
|
||||
for (let i = 0, l = other.ops.length; i < l; i++) {
|
||||
if (TextOperation.isRetain(ops[i])) {
|
||||
index -= ops[i] as number;
|
||||
} else if (TextOperation.isInsert(ops[i])) {
|
||||
newIndex += (ops[i] as string).length;
|
||||
} else {
|
||||
newIndex -= Math.min(index, -(ops[i] as number));
|
||||
index += ops[i] as number;
|
||||
}
|
||||
if (index < 0) { break; }
|
||||
}
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
const newAnchor = transformIndex(this.anchor);
|
||||
if (this.anchor === this.head) {
|
||||
return new Range(newAnchor, newAnchor);
|
||||
}
|
||||
return new Range(newAnchor, transformIndex(this.head));
|
||||
}
|
||||
}
|
||||
|
||||
// A selection is basically an array of ranges. Every range represents a real
|
||||
// selection or a cursor in the document (when the start position equals the
|
||||
// end position of the range). The array must not be empty.
|
||||
class Selection {
|
||||
ranges: Range[];
|
||||
|
||||
static Range = Range;
|
||||
|
||||
constructor(ranges?: Range[]) {
|
||||
this.ranges = ranges || [];
|
||||
}
|
||||
|
||||
// Convenience method for creating selections only containing a single cursor
|
||||
// and no real selection range.
|
||||
static createCursor(position: number): Selection {
|
||||
return new Selection([new Range(position, position)]);
|
||||
}
|
||||
|
||||
static fromJSON(obj: { ranges?: { anchor: number; head: number }[] } | { anchor: number; head: number }[]): Selection {
|
||||
const objRanges = (obj as any).ranges || obj;
|
||||
const ranges: Range[] = [];
|
||||
for (let i = 0; i < objRanges.length; i++) {
|
||||
ranges[i] = Range.fromJSON(objRanges[i]);
|
||||
}
|
||||
return new Selection(ranges);
|
||||
}
|
||||
|
||||
equals(other: Selection): boolean {
|
||||
if (this.ranges.length !== other.ranges.length) { return false; }
|
||||
// FIXME: Sort ranges before comparing them?
|
||||
for (let i = 0; i < this.ranges.length; i++) {
|
||||
if (!this.ranges[i].equals(other.ranges[i])) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
somethingSelected(): boolean {
|
||||
for (let i = 0; i < this.ranges.length; i++) {
|
||||
if (!this.ranges[i].isEmpty()) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the more current selection information.
|
||||
compose(other: Selection): Selection {
|
||||
return other;
|
||||
}
|
||||
|
||||
// Update the selection with respect to an operation.
|
||||
transform(other: TextOperation): Selection {
|
||||
const newRanges: Range[] = [];
|
||||
for (let i = 0; i < this.ranges.length; i++) {
|
||||
newRanges[i] = this.ranges[i].transform(other);
|
||||
}
|
||||
return new Selection(newRanges);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for CommonJS
|
||||
export = Selection;
|
||||
@@ -1,39 +1,39 @@
|
||||
var config = require('../config');
|
||||
'use strict';
|
||||
|
||||
if (typeof ot === 'undefined') {
|
||||
var ot = {};
|
||||
}
|
||||
import config = require('../config');
|
||||
import WrappedOperation = require('./wrapped-operation');
|
||||
|
||||
ot.Server = (function (global) {
|
||||
'use strict';
|
||||
class Server {
|
||||
document: string;
|
||||
operations: WrappedOperation[];
|
||||
|
||||
// Constructor. Takes the current document as a string and optionally the array
|
||||
// of all operations.
|
||||
function Server (document, operations) {
|
||||
constructor(document: string, operations?: WrappedOperation[]) {
|
||||
this.document = document;
|
||||
this.operations = operations || [];
|
||||
}
|
||||
|
||||
// Call this method whenever you receive an operation from a client.
|
||||
Server.prototype.receiveOperation = function (revision, operation) {
|
||||
receiveOperation(revision: number, operation: WrappedOperation): WrappedOperation | undefined {
|
||||
if (revision < 0 || this.operations.length < revision) {
|
||||
throw new Error("operation revision not in history");
|
||||
}
|
||||
// Find all operations that the client didn't know of when it sent the
|
||||
// operation ...
|
||||
var concurrentOperations = this.operations.slice(revision);
|
||||
const concurrentOperations = this.operations.slice(revision);
|
||||
|
||||
// ... and transform the operation against all these operations ...
|
||||
var transform = operation.constructor.transform;
|
||||
for (var i = 0; i < concurrentOperations.length; i++) {
|
||||
const transform = (operation.constructor as typeof WrappedOperation).transform;
|
||||
for (let i = 0; i < concurrentOperations.length; i++) {
|
||||
operation = transform(operation, concurrentOperations[i])[0];
|
||||
}
|
||||
|
||||
// ... and apply that on the document.
|
||||
var newDocument = operation.apply(this.document);
|
||||
const newDocument = operation.apply(this.document);
|
||||
// ignore if exceed the max length of document
|
||||
if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length)
|
||||
return;
|
||||
if (newDocument.length > config.documentMaxLength && newDocument.length > this.document.length)
|
||||
return;
|
||||
this.document = newDocument;
|
||||
// Store operation in history.
|
||||
this.operations.push(operation);
|
||||
@@ -41,12 +41,7 @@ ot.Server = (function (global) {
|
||||
// It's the caller's responsibility to send the operation to all connected
|
||||
// clients and an acknowledgement to the creator.
|
||||
return operation;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Server;
|
||||
|
||||
}(this));
|
||||
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.Server;
|
||||
}
|
||||
export = Server;
|
||||
@@ -1,91 +1,15 @@
|
||||
if (typeof ot === 'undefined') {
|
||||
// Export for browsers
|
||||
var ot = {};
|
||||
}
|
||||
// Export for browsers
|
||||
|
||||
ot.SimpleTextOperation = (function (global) {
|
||||
'use strict';
|
||||
|
||||
var TextOperation = global.ot ? global.ot.TextOperation : require('./text-operation');
|
||||
import TextOperation = require('./text-operation');
|
||||
|
||||
function SimpleTextOperation () {}
|
||||
class SimpleTextOperation {
|
||||
static Insert: typeof Insert;
|
||||
static Delete: typeof Delete;
|
||||
static Noop: typeof Noop;
|
||||
|
||||
|
||||
// Insert the string `str` at the zero-based `position` in the document.
|
||||
function Insert (str, position) {
|
||||
if (!this || this.constructor !== SimpleTextOperation) {
|
||||
// => function was called without 'new'
|
||||
return new Insert(str, position);
|
||||
}
|
||||
this.str = str;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
Insert.prototype = new SimpleTextOperation();
|
||||
SimpleTextOperation.Insert = Insert;
|
||||
|
||||
Insert.prototype.toString = function () {
|
||||
return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')';
|
||||
};
|
||||
|
||||
Insert.prototype.equals = function (other) {
|
||||
return other instanceof Insert &&
|
||||
this.str === other.str &&
|
||||
this.position === other.position;
|
||||
};
|
||||
|
||||
Insert.prototype.apply = function (doc) {
|
||||
return doc.slice(0, this.position) + this.str + doc.slice(this.position);
|
||||
};
|
||||
|
||||
|
||||
// Delete `count` many characters at the zero-based `position` in the document.
|
||||
function Delete (count, position) {
|
||||
if (!this || this.constructor !== SimpleTextOperation) {
|
||||
return new Delete(count, position);
|
||||
}
|
||||
this.count = count;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
Delete.prototype = new SimpleTextOperation();
|
||||
SimpleTextOperation.Delete = Delete;
|
||||
|
||||
Delete.prototype.toString = function () {
|
||||
return 'Delete(' + this.count + ', ' + this.position + ')';
|
||||
};
|
||||
|
||||
Delete.prototype.equals = function (other) {
|
||||
return other instanceof Delete &&
|
||||
this.count === other.count &&
|
||||
this.position === other.position;
|
||||
};
|
||||
|
||||
Delete.prototype.apply = function (doc) {
|
||||
return doc.slice(0, this.position) + doc.slice(this.position + this.count);
|
||||
};
|
||||
|
||||
|
||||
// An operation that does nothing. This is needed for the result of the
|
||||
// transformation of two deletions of the same character.
|
||||
function Noop () {
|
||||
if (!this || this.constructor !== SimpleTextOperation) { return new Noop(); }
|
||||
}
|
||||
|
||||
Noop.prototype = new SimpleTextOperation();
|
||||
SimpleTextOperation.Noop = Noop;
|
||||
|
||||
Noop.prototype.toString = function () {
|
||||
return 'Noop()';
|
||||
};
|
||||
|
||||
Noop.prototype.equals = function (other) { return other instanceof Noop; };
|
||||
|
||||
Noop.prototype.apply = function (doc) { return doc; };
|
||||
|
||||
var noop = new Noop();
|
||||
|
||||
|
||||
SimpleTextOperation.transform = function (a, b) {
|
||||
static transform(a: SimpleTextOperation, b: SimpleTextOperation): [SimpleTextOperation, SimpleTextOperation] {
|
||||
if (a instanceof Noop || b instanceof Noop) { return [a, b]; }
|
||||
|
||||
if (a instanceof Insert && b instanceof Insert) {
|
||||
@@ -157,32 +81,100 @@ ot.SimpleTextOperation = (function (global) {
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Should never reach here
|
||||
return [a, b];
|
||||
}
|
||||
|
||||
// Convert a normal, composable `TextOperation` into an array of
|
||||
// `SimpleTextOperation`s.
|
||||
SimpleTextOperation.fromTextOperation = function (operation) {
|
||||
var simpleOperations = [];
|
||||
var index = 0;
|
||||
for (var i = 0; i < operation.ops.length; i++) {
|
||||
var op = operation.ops[i];
|
||||
static fromTextOperation(operation: TextOperation): SimpleTextOperation[] {
|
||||
const simpleOperations: SimpleTextOperation[] = [];
|
||||
let index = 0;
|
||||
for (let i = 0; i < operation.ops.length; i++) {
|
||||
const op = operation.ops[i];
|
||||
if (TextOperation.isRetain(op)) {
|
||||
index += op;
|
||||
} else if (TextOperation.isInsert(op)) {
|
||||
simpleOperations.push(new Insert(op, index));
|
||||
index += op.length;
|
||||
} else {
|
||||
simpleOperations.push(new Delete(Math.abs(op), index));
|
||||
simpleOperations.push(new Delete(Math.abs(op as number), index));
|
||||
}
|
||||
}
|
||||
return simpleOperations;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the string `str` at the zero-based `position` in the document.
|
||||
class Insert extends SimpleTextOperation {
|
||||
str: string;
|
||||
position: number;
|
||||
|
||||
return SimpleTextOperation;
|
||||
})(this);
|
||||
constructor(str: string, position: number) {
|
||||
super();
|
||||
this.str = str;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'Insert(' + JSON.stringify(this.str) + ', ' + this.position + ')';
|
||||
}
|
||||
|
||||
equals(other: SimpleTextOperation): boolean {
|
||||
return other instanceof Insert &&
|
||||
this.str === other.str &&
|
||||
this.position === other.position;
|
||||
}
|
||||
|
||||
apply(doc: string): string {
|
||||
return doc.slice(0, this.position) + this.str + doc.slice(this.position);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete `count` many characters at the zero-based `position` in the document.
|
||||
class Delete extends SimpleTextOperation {
|
||||
count: number;
|
||||
position: number;
|
||||
|
||||
constructor(count: number, position: number) {
|
||||
super();
|
||||
this.count = count;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return 'Delete(' + this.count + ', ' + this.position + ')';
|
||||
}
|
||||
|
||||
equals(other: SimpleTextOperation): boolean {
|
||||
return other instanceof Delete &&
|
||||
this.count === other.count &&
|
||||
this.position === other.position;
|
||||
}
|
||||
|
||||
apply(doc: string): string {
|
||||
return doc.slice(0, this.position) + doc.slice(this.position + this.count);
|
||||
}
|
||||
}
|
||||
|
||||
// An operation that does nothing. This is needed for the result of the
|
||||
// transformation of two deletions of the same character.
|
||||
class Noop extends SimpleTextOperation {
|
||||
toString(): string {
|
||||
return 'Noop()';
|
||||
}
|
||||
|
||||
equals(other: SimpleTextOperation): boolean { return other instanceof Noop; }
|
||||
|
||||
apply(doc: string): string { return doc; }
|
||||
}
|
||||
|
||||
const noop = new Noop();
|
||||
|
||||
SimpleTextOperation.Insert = Insert;
|
||||
SimpleTextOperation.Delete = Delete;
|
||||
SimpleTextOperation.Noop = Noop;
|
||||
|
||||
// Export for CommonJS
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.SimpleTextOperation;
|
||||
}
|
||||
export = SimpleTextOperation;
|
||||
@@ -1,18 +1,40 @@
|
||||
if (typeof ot === 'undefined') {
|
||||
// Export for browsers
|
||||
var ot = {};
|
||||
// Export for browsers
|
||||
|
||||
'use strict';
|
||||
|
||||
// Operation are essentially lists of ops. There are three types of ops:
|
||||
//
|
||||
// * Retain ops: Advance the cursor position by a given number of characters.
|
||||
// Represented by positive ints.
|
||||
// * Insert ops: Insert a given string at the current cursor position.
|
||||
// Represented by strings.
|
||||
// * Delete ops: Delete the next n characters. Represented by negative ints.
|
||||
|
||||
type Op = number | string;
|
||||
|
||||
function isRetain(op: Op | undefined): op is number {
|
||||
return typeof op === 'number' && op > 0;
|
||||
}
|
||||
|
||||
ot.TextOperation = (function () {
|
||||
'use strict';
|
||||
function isInsert(op: Op | undefined): op is string {
|
||||
return typeof op === 'string';
|
||||
}
|
||||
|
||||
// Constructor for new operations.
|
||||
function TextOperation () {
|
||||
if (!this || this.constructor !== TextOperation) {
|
||||
// => function was called without 'new'
|
||||
return new TextOperation();
|
||||
}
|
||||
function isDelete(op: Op | undefined): op is number {
|
||||
return typeof op === 'number' && op < 0;
|
||||
}
|
||||
|
||||
// Constructor for new operations.
|
||||
class TextOperation {
|
||||
ops: Op[];
|
||||
baseLength: number;
|
||||
targetLength: number;
|
||||
|
||||
static isRetain = isRetain;
|
||||
static isInsert = isInsert;
|
||||
static isDelete = isDelete;
|
||||
|
||||
constructor() {
|
||||
// When an operation is applied to an input string, you can think of this as
|
||||
// if an imaginary cursor runs over the entire string and skips over some
|
||||
// parts, deletes some parts and inserts characters at some positions. These
|
||||
@@ -26,90 +48,69 @@ ot.TextOperation = (function () {
|
||||
this.targetLength = 0;
|
||||
}
|
||||
|
||||
TextOperation.prototype.equals = function (other) {
|
||||
equals(other: TextOperation): boolean {
|
||||
if (this.baseLength !== other.baseLength) { return false; }
|
||||
if (this.targetLength !== other.targetLength) { return false; }
|
||||
if (this.ops.length !== other.ops.length) { return false; }
|
||||
for (var i = 0; i < this.ops.length; i++) {
|
||||
if (this.ops[i] !== other.ops[i]) { return false; }
|
||||
for (let i = 0; i < this.ops.length; i++) {
|
||||
if (this.ops[i] !== other.ops[i]) { return false; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Operation are essentially lists of ops. There are three types of ops:
|
||||
//
|
||||
// * Retain ops: Advance the cursor position by a given number of characters.
|
||||
// Represented by positive ints.
|
||||
// * Insert ops: Insert a given string at the current cursor position.
|
||||
// Represented by strings.
|
||||
// * Delete ops: Delete the next n characters. Represented by negative ints.
|
||||
|
||||
var isRetain = TextOperation.isRetain = function (op) {
|
||||
return typeof op === 'number' && op > 0;
|
||||
};
|
||||
|
||||
var isInsert = TextOperation.isInsert = function (op) {
|
||||
return typeof op === 'string';
|
||||
};
|
||||
|
||||
var isDelete = TextOperation.isDelete = function (op) {
|
||||
return typeof op === 'number' && op < 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// After an operation is constructed, the user of the library can specify the
|
||||
// actions of an operation (skip/insert/delete) with these three builder
|
||||
// methods. They all return the operation for convenient chaining.
|
||||
|
||||
// Skip over a given number of characters.
|
||||
TextOperation.prototype.retain = function (n) {
|
||||
retain(n: number): this {
|
||||
if (typeof n !== 'number') {
|
||||
throw new Error("retain expects an integer");
|
||||
}
|
||||
if (n === 0) { return this; }
|
||||
this.baseLength += n;
|
||||
this.targetLength += n;
|
||||
if (isRetain(this.ops[this.ops.length-1])) {
|
||||
if (isRetain(this.ops[this.ops.length - 1])) {
|
||||
// The last op is a retain op => we can merge them into one op.
|
||||
this.ops[this.ops.length-1] += n;
|
||||
(this.ops[this.ops.length - 1] as number) += n;
|
||||
} else {
|
||||
// Create a new op.
|
||||
this.ops.push(n);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
// Insert a string at the current position.
|
||||
TextOperation.prototype.insert = function (str) {
|
||||
insert(str: string): this {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error("insert expects a string");
|
||||
}
|
||||
if (str === '') { return this; }
|
||||
this.targetLength += str.length;
|
||||
var ops = this.ops;
|
||||
if (isInsert(ops[ops.length-1])) {
|
||||
const ops = this.ops;
|
||||
if (isInsert(ops[ops.length - 1])) {
|
||||
// Merge insert op.
|
||||
ops[ops.length-1] += str;
|
||||
} else if (isDelete(ops[ops.length-1])) {
|
||||
ops[ops.length - 1] += str;
|
||||
} else if (isDelete(ops[ops.length - 1])) {
|
||||
// It doesn't matter when an operation is applied whether the operation
|
||||
// is delete(3), insert("something") or insert("something"), delete(3).
|
||||
// Here we enforce that in this case, the insert op always comes first.
|
||||
// This makes all operations that have the same effect when applied to
|
||||
// a document of the right length equal in respect to the `equals` method.
|
||||
if (isInsert(ops[ops.length-2])) {
|
||||
ops[ops.length-2] += str;
|
||||
if (isInsert(ops[ops.length - 2])) {
|
||||
ops[ops.length - 2] += str;
|
||||
} else {
|
||||
ops[ops.length] = ops[ops.length-1];
|
||||
ops[ops.length-2] = str;
|
||||
ops[ops.length] = ops[ops.length - 1];
|
||||
ops[ops.length - 2] = str;
|
||||
}
|
||||
} else {
|
||||
ops.push(str);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
// Delete a string at the current position.
|
||||
TextOperation.prototype['delete'] = function (n) {
|
||||
delete(n: number | string): this {
|
||||
if (typeof n === 'string') { n = n.length; }
|
||||
if (typeof n !== 'number') {
|
||||
throw new Error("delete expects an integer or a string");
|
||||
@@ -117,77 +118,69 @@ ot.TextOperation = (function () {
|
||||
if (n === 0) { return this; }
|
||||
if (n > 0) { n = -n; }
|
||||
this.baseLength -= n;
|
||||
if (isDelete(this.ops[this.ops.length-1])) {
|
||||
this.ops[this.ops.length-1] += n;
|
||||
if (isDelete(this.ops[this.ops.length - 1])) {
|
||||
(this.ops[this.ops.length - 1] as number) += n;
|
||||
} else {
|
||||
this.ops.push(n);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
// Tests whether this operation has no effect.
|
||||
TextOperation.prototype.isNoop = function () {
|
||||
isNoop(): boolean {
|
||||
return this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0]));
|
||||
};
|
||||
}
|
||||
|
||||
// Pretty printing.
|
||||
TextOperation.prototype.toString = function () {
|
||||
toString(): string {
|
||||
// map: build a new array by applying a function to every element in an old
|
||||
// array.
|
||||
var map = Array.prototype.map || function (fn) {
|
||||
var arr = this;
|
||||
var newArr = [];
|
||||
for (var i = 0, l = arr.length; i < l; i++) {
|
||||
newArr[i] = fn(arr[i]);
|
||||
}
|
||||
return newArr;
|
||||
};
|
||||
return map.call(this.ops, function (op) {
|
||||
return this.ops.map(function (op) {
|
||||
if (isRetain(op)) {
|
||||
return "retain " + op;
|
||||
} else if (isInsert(op)) {
|
||||
return "insert '" + op + "'";
|
||||
} else {
|
||||
return "delete " + (-op);
|
||||
return "delete " + (-(op as number));
|
||||
}
|
||||
}).join(', ');
|
||||
};
|
||||
}
|
||||
|
||||
// Converts operation into a JSON value.
|
||||
TextOperation.prototype.toJSON = function () {
|
||||
toJSON(): Op[] {
|
||||
return this.ops;
|
||||
};
|
||||
}
|
||||
|
||||
// Converts a plain JS object into an operation and validates it.
|
||||
TextOperation.fromJSON = function (ops) {
|
||||
var o = new TextOperation();
|
||||
for (var i = 0, l = ops.length; i < l; i++) {
|
||||
var op = ops[i];
|
||||
static fromJSON(ops: Op[]): TextOperation {
|
||||
const o = new TextOperation();
|
||||
for (let i = 0, l = ops.length; i < l; i++) {
|
||||
const op = ops[i];
|
||||
if (isRetain(op)) {
|
||||
o.retain(op);
|
||||
} else if (isInsert(op)) {
|
||||
o.insert(op);
|
||||
} else if (isDelete(op)) {
|
||||
o['delete'](op);
|
||||
o.delete(op);
|
||||
} else {
|
||||
throw new Error("unknown operation: " + JSON.stringify(op));
|
||||
}
|
||||
}
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
// Apply an operation to a string, returning a new string. Throws an error if
|
||||
// there's a mismatch between the input string and the operation.
|
||||
TextOperation.prototype.apply = function (str) {
|
||||
var operation = this;
|
||||
if (str.length !== operation.baseLength) {
|
||||
apply(str: string): string {
|
||||
if (str.length !== this.baseLength) {
|
||||
throw new Error("The operation's base length must be equal to the string's length.");
|
||||
}
|
||||
var newStr = [], j = 0;
|
||||
var strIndex = 0;
|
||||
var ops = this.ops;
|
||||
for (var i = 0, l = ops.length; i < l; i++) {
|
||||
var op = ops[i];
|
||||
const newStr: string[] = [];
|
||||
let j = 0;
|
||||
let strIndex = 0;
|
||||
const ops = this.ops;
|
||||
for (let i = 0, l = ops.length; i < l; i++) {
|
||||
const op = ops[i];
|
||||
if (isRetain(op)) {
|
||||
if (strIndex + op > str.length) {
|
||||
throw new Error("Operation can't retain more characters than are left in the string.");
|
||||
@@ -199,52 +192,52 @@ ot.TextOperation = (function () {
|
||||
// Insert string.
|
||||
newStr[j++] = op;
|
||||
} else { // delete op
|
||||
strIndex -= op;
|
||||
strIndex -= (op as number);
|
||||
}
|
||||
}
|
||||
if (strIndex !== str.length) {
|
||||
throw new Error("The operation didn't operate on the whole string.");
|
||||
}
|
||||
return newStr.join('');
|
||||
};
|
||||
}
|
||||
|
||||
// Computes the inverse of an operation. The inverse of an operation is the
|
||||
// operation that reverts the effects of the operation, e.g. when you have an
|
||||
// operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
|
||||
// skip(6);'. The inverse should be used for implementing undo.
|
||||
TextOperation.prototype.invert = function (str) {
|
||||
var strIndex = 0;
|
||||
var inverse = new TextOperation();
|
||||
var ops = this.ops;
|
||||
for (var i = 0, l = ops.length; i < l; i++) {
|
||||
var op = ops[i];
|
||||
invert(str: string): TextOperation {
|
||||
let strIndex = 0;
|
||||
const inverse = new TextOperation();
|
||||
const ops = this.ops;
|
||||
for (let i = 0, l = ops.length; i < l; i++) {
|
||||
const op = ops[i];
|
||||
if (isRetain(op)) {
|
||||
inverse.retain(op);
|
||||
strIndex += op;
|
||||
} else if (isInsert(op)) {
|
||||
inverse['delete'](op.length);
|
||||
inverse.delete(op.length);
|
||||
} else { // delete op
|
||||
inverse.insert(str.slice(strIndex, strIndex - op));
|
||||
strIndex -= op;
|
||||
inverse.insert(str.slice(strIndex, strIndex - (op as number)));
|
||||
strIndex -= (op as number);
|
||||
}
|
||||
}
|
||||
return inverse;
|
||||
};
|
||||
}
|
||||
|
||||
// Compose merges two consecutive operations into one operation, that
|
||||
// preserves the changes of both. Or, in other words, for each input string S
|
||||
// and a pair of consecutive operations A and B,
|
||||
// apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
|
||||
TextOperation.prototype.compose = function (operation2) {
|
||||
var operation1 = this;
|
||||
compose(operation2: TextOperation): TextOperation {
|
||||
const operation1 = this;
|
||||
if (operation1.targetLength !== operation2.baseLength) {
|
||||
throw new Error("The base length of the second operation has to be the target length of the first operation");
|
||||
}
|
||||
|
||||
var operation = new TextOperation(); // the combined operation
|
||||
var ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
|
||||
var i1 = 0, i2 = 0; // current index into ops1 respectively ops2
|
||||
var op1 = ops1[i1++], op2 = ops2[i2++]; // current ops
|
||||
const operation = new TextOperation(); // the combined operation
|
||||
const ops1 = operation1.ops, ops2 = operation2.ops; // for fast access
|
||||
let i1 = 0, i2 = 0; // current index into ops1 respectively ops2
|
||||
let op1: Op | undefined = ops1[i1++], op2: Op | undefined = ops2[i2++]; // current ops
|
||||
while (true) {
|
||||
// Dispatch on the type of op1 and op2
|
||||
if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
|
||||
@@ -253,7 +246,7 @@ ot.TextOperation = (function () {
|
||||
}
|
||||
|
||||
if (isDelete(op1)) {
|
||||
operation['delete'](op1);
|
||||
operation.delete(op1);
|
||||
op1 = ops1[i1++];
|
||||
continue;
|
||||
}
|
||||
@@ -311,15 +304,15 @@ ot.TextOperation = (function () {
|
||||
}
|
||||
} else if (isRetain(op1) && isDelete(op2)) {
|
||||
if (op1 > -op2) {
|
||||
operation['delete'](op2);
|
||||
operation.delete(op2);
|
||||
op1 = op1 + op2;
|
||||
op2 = ops2[i2++];
|
||||
} else if (op1 === -op2) {
|
||||
operation['delete'](op2);
|
||||
operation.delete(op2);
|
||||
op1 = ops1[i1++];
|
||||
op2 = ops2[i2++];
|
||||
} else {
|
||||
operation['delete'](op1);
|
||||
operation.delete(op1);
|
||||
op2 = op2 + op1;
|
||||
op1 = ops1[i1++];
|
||||
}
|
||||
@@ -332,25 +325,6 @@ ot.TextOperation = (function () {
|
||||
}
|
||||
}
|
||||
return operation;
|
||||
};
|
||||
|
||||
function getSimpleOp (operation, fn) {
|
||||
var ops = operation.ops;
|
||||
var isRetain = TextOperation.isRetain;
|
||||
switch (ops.length) {
|
||||
case 1:
|
||||
return ops[0];
|
||||
case 2:
|
||||
return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
|
||||
case 3:
|
||||
if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getStartIndex (operation) {
|
||||
if (isRetain(operation.ops[0])) { return operation.ops[0]; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// When you use ctrl-z to undo your latest changes, you expect the program not
|
||||
@@ -361,11 +335,11 @@ ot.TextOperation = (function () {
|
||||
// returns true if the operations are consecutive insert operations or both
|
||||
// operations delete text at the same position. You may want to include other
|
||||
// factors like the time since the last change in your decision.
|
||||
TextOperation.prototype.shouldBeComposedWith = function (other) {
|
||||
shouldBeComposedWith(other: TextOperation): boolean {
|
||||
if (this.isNoop() || other.isNoop()) { return true; }
|
||||
|
||||
var startA = getStartIndex(this), startB = getStartIndex(other);
|
||||
var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
|
||||
const startA = getStartIndex(this), startB = getStartIndex(other);
|
||||
const simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
|
||||
if (!simpleA || !simpleB) { return false; }
|
||||
|
||||
if (isInsert(simpleA) && isInsert(simpleB)) {
|
||||
@@ -379,16 +353,16 @@ ot.TextOperation = (function () {
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Decides whether two operations should be composed with each other
|
||||
// if they were inverted, that is
|
||||
// `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
|
||||
TextOperation.prototype.shouldBeComposedWithInverted = function (other) {
|
||||
shouldBeComposedWithInverted(other: TextOperation): boolean {
|
||||
if (this.isNoop() || other.isNoop()) { return true; }
|
||||
|
||||
var startA = getStartIndex(this), startB = getStartIndex(other);
|
||||
var simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
|
||||
const startA = getStartIndex(this), startB = getStartIndex(other);
|
||||
const simpleA = getSimpleOp(this), simpleB = getSimpleOp(other);
|
||||
if (!simpleA || !simpleB) { return false; }
|
||||
|
||||
if (isInsert(simpleA) && isInsert(simpleB)) {
|
||||
@@ -400,22 +374,22 @@ ot.TextOperation = (function () {
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Transform takes two operations A and B that happened concurrently and
|
||||
// produces two operations A' and B' (in an array) such that
|
||||
// `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
|
||||
// heart of OT.
|
||||
TextOperation.transform = function (operation1, operation2) {
|
||||
static transform(operation1: TextOperation, operation2: TextOperation): [TextOperation, TextOperation] {
|
||||
if (operation1.baseLength !== operation2.baseLength) {
|
||||
throw new Error("Both operations have to have the same base length");
|
||||
}
|
||||
|
||||
var operation1prime = new TextOperation();
|
||||
var operation2prime = new TextOperation();
|
||||
var ops1 = operation1.ops, ops2 = operation2.ops;
|
||||
var i1 = 0, i2 = 0;
|
||||
var op1 = ops1[i1++], op2 = ops2[i2++];
|
||||
const operation1prime = new TextOperation();
|
||||
const operation2prime = new TextOperation();
|
||||
const ops1 = operation1.ops, ops2 = operation2.ops;
|
||||
let i1 = 0, i2 = 0;
|
||||
let op1: Op | undefined = ops1[i1++], op2: Op | undefined = ops2[i2++];
|
||||
while (true) {
|
||||
// At every iteration of the loop, the imaginary cursor that both
|
||||
// operation1 and operation2 have that operates on the input string must
|
||||
@@ -449,7 +423,7 @@ ot.TextOperation = (function () {
|
||||
throw new Error("Cannot compose operations: first operation is too long.");
|
||||
}
|
||||
|
||||
var minl;
|
||||
let minl: number;
|
||||
if (isRetain(op1) && isRetain(op2)) {
|
||||
// Simple case: retain/retain
|
||||
if (op1 > op2) {
|
||||
@@ -496,7 +470,7 @@ ot.TextOperation = (function () {
|
||||
op2 = op2 + op1;
|
||||
op1 = ops1[i1++];
|
||||
}
|
||||
operation1prime['delete'](minl);
|
||||
operation1prime.delete(minl);
|
||||
} else if (isRetain(op1) && isDelete(op2)) {
|
||||
if (op1 > -op2) {
|
||||
minl = -op2;
|
||||
@@ -511,20 +485,33 @@ ot.TextOperation = (function () {
|
||||
op2 = op2 + op1;
|
||||
op1 = ops1[i1++];
|
||||
}
|
||||
operation2prime['delete'](minl);
|
||||
operation2prime.delete(minl);
|
||||
} else {
|
||||
throw new Error("The two operations aren't compatible");
|
||||
}
|
||||
}
|
||||
|
||||
return [operation1prime, operation2prime];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return TextOperation;
|
||||
function getSimpleOp(operation: TextOperation): Op | null {
|
||||
const ops = operation.ops;
|
||||
switch (ops.length) {
|
||||
case 1:
|
||||
return ops[0];
|
||||
case 2:
|
||||
return isRetain(ops[0]) ? ops[1] : (isRetain(ops[1]) ? ops[0] : null);
|
||||
case 3:
|
||||
if (isRetain(ops[0]) && isRetain(ops[2])) { return ops[1]; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}());
|
||||
function getStartIndex(operation: TextOperation): number {
|
||||
if (isRetain(operation.ops[0])) { return operation.ops[0]; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Export for CommonJS
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.TextOperation;
|
||||
}
|
||||
export = TextOperation;
|
||||
@@ -1,80 +0,0 @@
|
||||
if (typeof ot === 'undefined') {
|
||||
// Export for browsers
|
||||
var ot = {};
|
||||
}
|
||||
|
||||
ot.WrappedOperation = (function (global) {
|
||||
'use strict';
|
||||
|
||||
// A WrappedOperation contains an operation and corresponing metadata.
|
||||
function WrappedOperation (operation, meta) {
|
||||
this.wrapped = operation;
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
WrappedOperation.prototype.apply = function () {
|
||||
return this.wrapped.apply.apply(this.wrapped, arguments);
|
||||
};
|
||||
|
||||
WrappedOperation.prototype.invert = function () {
|
||||
var meta = this.meta;
|
||||
return new WrappedOperation(
|
||||
this.wrapped.invert.apply(this.wrapped, arguments),
|
||||
meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
|
||||
meta.invert.apply(meta, arguments) : meta
|
||||
);
|
||||
};
|
||||
|
||||
// Copy all properties from source to target.
|
||||
function copy (source, target) {
|
||||
for (var key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function composeMeta (a, b) {
|
||||
if (a && typeof a === 'object') {
|
||||
if (typeof a.compose === 'function') { return a.compose(b); }
|
||||
var meta = {};
|
||||
copy(a, meta);
|
||||
copy(b, meta);
|
||||
return meta;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
WrappedOperation.prototype.compose = function (other) {
|
||||
return new WrappedOperation(
|
||||
this.wrapped.compose(other.wrapped),
|
||||
composeMeta(this.meta, other.meta)
|
||||
);
|
||||
};
|
||||
|
||||
function transformMeta (meta, operation) {
|
||||
if (meta && typeof meta === 'object') {
|
||||
if (typeof meta.transform === 'function') {
|
||||
return meta.transform(operation);
|
||||
}
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
WrappedOperation.transform = function (a, b) {
|
||||
var transform = a.wrapped.constructor.transform;
|
||||
var pair = transform(a.wrapped, b.wrapped);
|
||||
return [
|
||||
new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
|
||||
new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
|
||||
];
|
||||
};
|
||||
|
||||
return WrappedOperation;
|
||||
|
||||
}(this));
|
||||
|
||||
// Export for CommonJS
|
||||
if (typeof module === 'object') {
|
||||
module.exports = ot.WrappedOperation;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Export for browsers
|
||||
|
||||
'use strict';
|
||||
|
||||
import TextOperation = require('./text-operation');
|
||||
|
||||
interface MetaWithInvert {
|
||||
invert(...args: any[]): any;
|
||||
}
|
||||
|
||||
interface MetaWithCompose {
|
||||
compose(other: any): any;
|
||||
}
|
||||
|
||||
interface MetaWithTransform {
|
||||
transform(operation: TextOperation): any;
|
||||
}
|
||||
|
||||
type Meta = any;
|
||||
|
||||
// A WrappedOperation contains an operation and corresponing metadata.
|
||||
class WrappedOperation {
|
||||
wrapped: TextOperation;
|
||||
meta: Meta;
|
||||
|
||||
constructor(operation: TextOperation, meta?: Meta) {
|
||||
this.wrapped = operation;
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
apply(...args: [string]): string {
|
||||
return this.wrapped.apply(...args);
|
||||
}
|
||||
|
||||
invert(...args: [string]): WrappedOperation {
|
||||
const meta = this.meta;
|
||||
return new WrappedOperation(
|
||||
this.wrapped.invert(...args),
|
||||
meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
|
||||
meta.invert(...args) : meta
|
||||
);
|
||||
}
|
||||
|
||||
compose(other: WrappedOperation): WrappedOperation {
|
||||
return new WrappedOperation(
|
||||
this.wrapped.compose(other.wrapped),
|
||||
composeMeta(this.meta, other.meta)
|
||||
);
|
||||
}
|
||||
|
||||
static transform(a: WrappedOperation, b: WrappedOperation): [WrappedOperation, WrappedOperation] {
|
||||
const pair = TextOperation.transform(a.wrapped, b.wrapped);
|
||||
return [
|
||||
new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
|
||||
new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all properties from source to target.
|
||||
function copy(source: Record<string, any>, target: Record<string, any>): void {
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function composeMeta(a: Meta, b: Meta): Meta {
|
||||
if (a && typeof a === 'object') {
|
||||
if (typeof a.compose === 'function') { return a.compose(b); }
|
||||
const meta: Record<string, any> = {};
|
||||
copy(a, meta);
|
||||
copy(b, meta);
|
||||
return meta;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
function transformMeta(meta: Meta, operation: TextOperation): Meta {
|
||||
if (meta && typeof meta === 'object') {
|
||||
if (typeof meta.transform === 'function') {
|
||||
return meta.transform(operation);
|
||||
}
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
// Export for CommonJS
|
||||
export = WrappedOperation;
|
||||
@@ -1,7 +1,7 @@
|
||||
const promClient = require('prom-client')
|
||||
const realtime = require('./realtime')
|
||||
import promClient = require('prom-client')
|
||||
import realtime = require('./realtime')
|
||||
|
||||
exports.setupCustomPrometheusMetrics = function () {
|
||||
export function setupCustomPrometheusMetrics (): void {
|
||||
const onlineNotes = new promClient.Gauge({
|
||||
name: 'hedgedoc_online_notes',
|
||||
help: 'Notes currently being edited'
|
||||
@@ -28,12 +28,12 @@ exports.setupCustomPrometheusMetrics = function () {
|
||||
name: 'hedgedoc_connection_busy',
|
||||
help: 'Indicates that realtime currently connecting'
|
||||
})
|
||||
const connectionSocketQueueLength = new promClient.Gauge({
|
||||
const connectionSocketQueueGauge = new promClient.Gauge({
|
||||
name: 'hedgedoc_connection_socket_queue_length',
|
||||
help: 'Length of connection socket queue',
|
||||
// The last gauge provides the collect callback for all metrics
|
||||
collect () {
|
||||
realtime.getStatus(function (data) {
|
||||
realtime.getStatus(function (data: any) {
|
||||
onlineNotes.set(data.onlineNotes)
|
||||
onlineSessions.set({ type: 'all' }, data.onlineUsers)
|
||||
onlineSessions.set({ type: 'signed-in' }, data.onlineRegisteredUsers)
|
||||
@@ -42,7 +42,7 @@ exports.setupCustomPrometheusMetrics = function () {
|
||||
notesCount.set(data.notesCount)
|
||||
registeredUsers.set(data.registeredUsers)
|
||||
isConnectionBusy.set(data.isConnectionBusy ? 1 : 0)
|
||||
connectionSocketQueueLength.set(data.connectionSocketQueueLength)
|
||||
connectionSocketQueueGauge.set(data.connectionSocketQueueLength)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,25 +1,25 @@
|
||||
'use strict'
|
||||
// realtime
|
||||
// external modules
|
||||
const cookie = require('cookie')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const async = require('async')
|
||||
const Chance = require('chance')
|
||||
import cookie = require('cookie')
|
||||
import cookieParser = require('cookie-parser')
|
||||
import async = require('async')
|
||||
import Chance = require('chance')
|
||||
const chance = new Chance()
|
||||
const moment = require('moment')
|
||||
import moment = require('moment')
|
||||
|
||||
// core
|
||||
const config = require('./config')
|
||||
const logger = require('./logger')
|
||||
const history = require('./history')
|
||||
const models = require('./models')
|
||||
import config = require('./config')
|
||||
import logger = require('./logger')
|
||||
import history = require('./history')
|
||||
import models = require('./models')
|
||||
|
||||
// ot
|
||||
const ot = require('./ot')
|
||||
import ot = require('./ot')
|
||||
|
||||
// public
|
||||
const realtime = {
|
||||
io: null,
|
||||
io: null as any,
|
||||
onAuthorizeSuccess,
|
||||
onAuthorizeFail,
|
||||
secure,
|
||||
@@ -29,16 +29,16 @@ const realtime = {
|
||||
maintenance: true
|
||||
}
|
||||
|
||||
function onAuthorizeSuccess (data, accept) {
|
||||
function onAuthorizeSuccess (data: any, accept: () => void): void {
|
||||
accept()
|
||||
}
|
||||
|
||||
function onAuthorizeFail (data, message, error, accept) {
|
||||
function onAuthorizeFail (data: any, message: string, error: any, accept: () => void): void {
|
||||
accept() // accept whether authorize or not to allow anonymous usage
|
||||
}
|
||||
|
||||
// secure the origin by the cookie
|
||||
function secure (socket, next) {
|
||||
function secure (socket: any, next: (err?: Error) => void): void {
|
||||
try {
|
||||
const handshakeData = socket.request
|
||||
if (handshakeData.headers.cookie) {
|
||||
@@ -60,7 +60,7 @@ function secure (socket, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function emitCheck (note) {
|
||||
function emitCheck (note: any): void {
|
||||
const out = {
|
||||
title: note.title,
|
||||
updatetime: note.updatetime,
|
||||
@@ -73,8 +73,8 @@ function emitCheck (note) {
|
||||
}
|
||||
|
||||
// actions
|
||||
const users = {}
|
||||
const notes = {}
|
||||
const users: Record<string, any> = {}
|
||||
const notes: Record<string, any> = {}
|
||||
// update when the note is dirty
|
||||
setInterval(function () {
|
||||
async.each(Object.keys(notes), function (key, callback) {
|
||||
@@ -84,7 +84,7 @@ setInterval(function () {
|
||||
note.server.isDirty = false
|
||||
updateNote(note, function (err, _note) {
|
||||
// handle when note already been clean up
|
||||
if (!notes[key] || !notes[key].server) return callback(null, null)
|
||||
if (!notes[key] || !notes[key].server) return callback(null)
|
||||
if (!_note) {
|
||||
realtime.io.to(note.id).emit('info', {
|
||||
code: 404
|
||||
@@ -100,26 +100,26 @@ setInterval(function () {
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
return callback(err, null)
|
||||
return callback(err)
|
||||
}
|
||||
note.updatetime = moment(_note.lastchangeAt).valueOf()
|
||||
emitCheck(note)
|
||||
return callback(null, null)
|
||||
return callback(null)
|
||||
})
|
||||
} else {
|
||||
return callback(null, null)
|
||||
return callback(null)
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) return logger.error('updater error', err)
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
function updateNote (note, callback) {
|
||||
function updateNote (note: any, callback: (err: any, note: any) => void): void {
|
||||
models.Note.findOne({
|
||||
where: {
|
||||
id: note.id
|
||||
}
|
||||
}).then(function (_note) {
|
||||
}).then(function (_note: any) {
|
||||
if (!_note) return callback(null, null)
|
||||
// update user note history
|
||||
const tempUsers = Object.assign({}, note.tempUsers)
|
||||
@@ -133,11 +133,11 @@ function updateNote (note, callback) {
|
||||
where: {
|
||||
id: note.lastchangeuser
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) return callback(null, null)
|
||||
note.lastchangeuserprofile = models.User.getProfile(user)
|
||||
return finishUpdateNote(note, _note, callback)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
})
|
||||
@@ -148,13 +148,13 @@ function updateNote (note, callback) {
|
||||
note.lastchangeuserprofile = null
|
||||
return finishUpdateNote(note, _note, callback)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
|
||||
function finishUpdateNote (note, _note, callback) {
|
||||
function finishUpdateNote (note: any, _note: any, callback: (err: any, note: any) => void): void {
|
||||
if (!note || !note.server) return callback(null, null)
|
||||
const body = note.server.document
|
||||
const title = note.title = models.Note.parseNoteTitle(body)
|
||||
@@ -165,10 +165,10 @@ function finishUpdateNote (note, _note, callback) {
|
||||
lastchangeuserId: note.lastchangeuser,
|
||||
lastchangeAt: Date.now()
|
||||
}
|
||||
_note.update(values).then(function (_note) {
|
||||
_note.update(values).then(function (_note: any) {
|
||||
saverSleep = false
|
||||
return callback(null, _note)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
})
|
||||
@@ -189,7 +189,7 @@ setInterval(function () {
|
||||
disconnectSocketQueue.push(socket)
|
||||
disconnect(socket)
|
||||
}
|
||||
return callback(null, null)
|
||||
return callback(null)
|
||||
}, function (err) {
|
||||
if (err) return logger.error('cleaner error', err)
|
||||
})
|
||||
@@ -199,7 +199,7 @@ let saverSleep = false
|
||||
// save note revision in interval
|
||||
setInterval(function () {
|
||||
if (saverSleep) return
|
||||
models.Revision.saveAllNotesRevision(function (err, notes) {
|
||||
models.Revision.saveAllNotesRevision(function (err: any, notes: any) {
|
||||
if (err) return logger.error('revision saver failed: ' + err)
|
||||
if (notes && notes.length <= 0) {
|
||||
saverSleep = true
|
||||
@@ -207,11 +207,11 @@ setInterval(function () {
|
||||
})
|
||||
}, 60000 * 5)
|
||||
|
||||
function getStatus (callback) {
|
||||
models.Note.count().then(function (notecount) {
|
||||
const distinctaddresses = []
|
||||
const regaddresses = []
|
||||
const distinctregaddresses = []
|
||||
function getStatus (callback: (data: any) => void): void {
|
||||
models.Note.count().then(function (notecount: any) {
|
||||
const distinctaddresses: any[] = []
|
||||
const regaddresses: any[] = []
|
||||
const distinctregaddresses: any[] = []
|
||||
Object.keys(users).forEach(function (key) {
|
||||
const user = users[key]
|
||||
if (!user) return
|
||||
@@ -239,7 +239,7 @@ function getStatus (callback) {
|
||||
}
|
||||
}
|
||||
})
|
||||
models.User.count().then(function (regcount) {
|
||||
models.User.count().then(function (regcount: any) {
|
||||
return callback
|
||||
|
||||
? callback({
|
||||
@@ -256,22 +256,22 @@ function getStatus (callback) {
|
||||
disconnectSocketQueueLength: disconnectSocketQueue.length
|
||||
})
|
||||
: null
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return logger.error('count user failed: ' + err)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return logger.error('count note failed: ' + err)
|
||||
})
|
||||
}
|
||||
|
||||
function isReady () {
|
||||
function isReady (): boolean {
|
||||
return realtime.io &&
|
||||
Object.keys(notes).length === 0 && Object.keys(users).length === 0 &&
|
||||
connectionSocketQueue.length === 0 && !isConnectionBusy &&
|
||||
disconnectSocketQueue.length === 0 && !isDisconnectBusy
|
||||
}
|
||||
|
||||
function extractNoteIdFromSocket (socket) {
|
||||
function extractNoteIdFromSocket (socket: any): string | false {
|
||||
if (!socket || !socket.handshake) {
|
||||
return false
|
||||
}
|
||||
@@ -282,21 +282,21 @@ function extractNoteIdFromSocket (socket) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseNoteIdFromSocket (socket, callback) {
|
||||
function parseNoteIdFromSocket (socket: any, callback: (err: any, id: string | null) => void): void {
|
||||
const noteId = extractNoteIdFromSocket(socket)
|
||||
if (!noteId) {
|
||||
return callback(null, null)
|
||||
}
|
||||
models.Note.parseNoteId(noteId, function (err, id) {
|
||||
models.Note.parseNoteId(noteId, function (err: any, id: any) {
|
||||
if (err || !id) return callback(err, id)
|
||||
return callback(null, id)
|
||||
})
|
||||
}
|
||||
|
||||
function emitOnlineUsers (socket) {
|
||||
function emitOnlineUsers (socket: any): void {
|
||||
const noteId = socket.noteId
|
||||
if (!noteId || !notes[noteId]) return
|
||||
const users = []
|
||||
const users: any[] = []
|
||||
Object.keys(notes[noteId].users).forEach(function (key) {
|
||||
const user = notes[noteId].users[key]
|
||||
if (user) { users.push(buildUserOutData(user)) }
|
||||
@@ -307,7 +307,7 @@ function emitOnlineUsers (socket) {
|
||||
realtime.io.to(noteId).emit('online users', out)
|
||||
}
|
||||
|
||||
function emitUserStatus (socket) {
|
||||
function emitUserStatus (socket: any): void {
|
||||
const noteId = socket.noteId
|
||||
const user = users[socket.id]
|
||||
if (!noteId || !notes[noteId] || !user) return
|
||||
@@ -315,7 +315,7 @@ function emitUserStatus (socket) {
|
||||
socket.broadcast.to(noteId).emit('user status', out)
|
||||
}
|
||||
|
||||
function emitRefresh (socket) {
|
||||
function emitRefresh (socket: any): void {
|
||||
const noteId = socket.noteId
|
||||
if (!noteId || !notes[noteId]) return
|
||||
const note = notes[noteId]
|
||||
@@ -335,7 +335,7 @@ function emitRefresh (socket) {
|
||||
socket.emit('refresh', out)
|
||||
}
|
||||
|
||||
function isDuplicatedInSocketQueue (queue, socket) {
|
||||
function isDuplicatedInSocketQueue (queue: any[], socket: any): boolean {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
if (queue[i] && queue[i].id === socket.id) {
|
||||
return true
|
||||
@@ -344,7 +344,7 @@ function isDuplicatedInSocketQueue (queue, socket) {
|
||||
return false
|
||||
}
|
||||
|
||||
function clearSocketQueue (queue, socket) {
|
||||
function clearSocketQueue (queue: any[], socket: any): void {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
if (!queue[i] || queue[i].id === socket.id) {
|
||||
queue.splice(i, 1)
|
||||
@@ -353,7 +353,7 @@ function clearSocketQueue (queue, socket) {
|
||||
}
|
||||
}
|
||||
|
||||
function connectNextSocket () {
|
||||
function connectNextSocket (): void {
|
||||
setTimeout(function () {
|
||||
isConnectionBusy = false
|
||||
if (connectionSocketQueue.length > 0) {
|
||||
@@ -362,7 +362,7 @@ function connectNextSocket () {
|
||||
}, 1)
|
||||
}
|
||||
|
||||
function checkViewPermission (req, note) {
|
||||
function checkViewPermission (req: any, note: any): boolean {
|
||||
if (note.permission === 'private') {
|
||||
if (req.user && req.user.logged_in && req.user.id === note.owner) { return true } else { return false }
|
||||
} else if (note.permission === 'limited' || note.permission === 'protected') {
|
||||
@@ -373,11 +373,11 @@ function checkViewPermission (req, note) {
|
||||
}
|
||||
|
||||
let isConnectionBusy = false
|
||||
const connectionSocketQueue = []
|
||||
const connectionSocketQueue: any[] = []
|
||||
let isDisconnectBusy = false
|
||||
const disconnectSocketQueue = []
|
||||
const disconnectSocketQueue: any[] = []
|
||||
|
||||
function finishConnection (socket, noteId, socketId) {
|
||||
function finishConnection (socket: any, noteId: string, socketId: string): void {
|
||||
// check view permission
|
||||
if (!checkViewPermission(socket.request, notes[noteId])) {
|
||||
return failConnection(403, 'connection forbidden', socket)
|
||||
@@ -416,7 +416,7 @@ function finishConnection (socket, noteId, socketId) {
|
||||
}
|
||||
}
|
||||
|
||||
function startConnection (socket) {
|
||||
function startConnection (socket: any): void {
|
||||
if (isConnectionBusy) return
|
||||
isConnectionBusy = true
|
||||
|
||||
@@ -446,7 +446,7 @@ function startConnection (socket) {
|
||||
id: noteId
|
||||
},
|
||||
include
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
// if client disconnected while we waited for the note, disconnect() cleaned up users[socket.id]
|
||||
if (!users[socket.id]) {
|
||||
clearSocketQueue(connectionSocketQueue, socket)
|
||||
@@ -468,7 +468,7 @@ function startConnection (socket) {
|
||||
const updatetime = note.lastchangeAt
|
||||
const server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback)
|
||||
|
||||
const authors = {}
|
||||
const authors: Record<string, any> = {}
|
||||
for (let i = 0; i < note.authors.length; i++) {
|
||||
const author = note.authors[i]
|
||||
const profile = models.User.getProfile(author.user)
|
||||
@@ -502,7 +502,7 @@ function startConnection (socket) {
|
||||
}
|
||||
|
||||
return finishConnection(socket, noteId, socket.id)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return failConnection(500, err, socket)
|
||||
})
|
||||
} else {
|
||||
@@ -510,7 +510,7 @@ function startConnection (socket) {
|
||||
}
|
||||
}
|
||||
|
||||
function failConnection (code, err, socket) {
|
||||
function failConnection (code: number, err: any, socket: any): void {
|
||||
logger.error(err)
|
||||
// clear error socket in queue
|
||||
clearSocketQueue(connectionSocketQueue, socket)
|
||||
@@ -522,7 +522,7 @@ function failConnection (code, err, socket) {
|
||||
return socket.disconnect(true)
|
||||
}
|
||||
|
||||
function disconnect (socket) {
|
||||
function disconnect (socket: any): void {
|
||||
if (isDisconnectBusy) return
|
||||
isDisconnectBusy = true
|
||||
|
||||
@@ -586,7 +586,7 @@ function disconnect (socket) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildUserOutData (user) {
|
||||
function buildUserOutData (user: any): any {
|
||||
const out = {
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
@@ -601,7 +601,7 @@ function buildUserOutData (user) {
|
||||
return out
|
||||
}
|
||||
|
||||
function updateUserData (socket, user) {
|
||||
function updateUserData (socket: any, user: any): void {
|
||||
// retrieve user data from passport
|
||||
if (socket.request.user && socket.request.user.logged_in) {
|
||||
const profile = models.User.getProfile(socket.request.user)
|
||||
@@ -616,7 +616,7 @@ function updateUserData (socket, user) {
|
||||
}
|
||||
}
|
||||
|
||||
function ifMayEdit (socket, callback) {
|
||||
function ifMayEdit (socket: any, callback: (mayEdit: boolean) => void): void {
|
||||
const noteId = socket.noteId
|
||||
if (!noteId || !notes[noteId]) return
|
||||
const note = notes[noteId]
|
||||
@@ -646,7 +646,7 @@ function ifMayEdit (socket, callback) {
|
||||
return callback(mayEdit)
|
||||
}
|
||||
|
||||
function operationCallback (socket, operation) {
|
||||
function operationCallback (socket: any, operation: any): void {
|
||||
const noteId = socket.noteId
|
||||
if (!noteId || !notes[noteId]) return
|
||||
const note = notes[noteId]
|
||||
@@ -667,7 +667,7 @@ function operationCallback (socket, operation) {
|
||||
userId,
|
||||
color: user.color
|
||||
}
|
||||
}).spread(function (author, created) {
|
||||
}).spread(function (author: any, created: any) {
|
||||
if (author) {
|
||||
note.authors[author.userId] = {
|
||||
userid: author.userId,
|
||||
@@ -676,7 +676,7 @@ function operationCallback (socket, operation) {
|
||||
name: user.name
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return logger.error('operation callback failed: ' + err)
|
||||
})
|
||||
}
|
||||
@@ -688,12 +688,12 @@ function operationCallback (socket, operation) {
|
||||
})
|
||||
}
|
||||
|
||||
function updateHistory (userId, note, time) {
|
||||
function updateHistory (userId: string, note: any, time?: number): void {
|
||||
const noteId = note.alias ? note.alias : models.Note.encodeNoteId(note.id)
|
||||
if (note.server) history.updateHistory(userId, noteId, note.server.document, time)
|
||||
}
|
||||
|
||||
function connection (socket) {
|
||||
function connection (socket: any): void {
|
||||
if (realtime.maintenance) return
|
||||
parseNoteIdFromSocket(socket, function (err, noteId) {
|
||||
if (err) {
|
||||
@@ -754,7 +754,7 @@ function connection (socket) {
|
||||
})
|
||||
|
||||
// received user status
|
||||
socket.on('user status', function (data) {
|
||||
socket.on('user status', function (data: any) {
|
||||
const noteId = socket.noteId
|
||||
const user = users[socket.id]
|
||||
if (!noteId || !notes[noteId] || !user) return
|
||||
@@ -767,7 +767,7 @@ function connection (socket) {
|
||||
})
|
||||
|
||||
// received note permission change request
|
||||
socket.on('permission', function (permission) {
|
||||
socket.on('permission', function (permission: any) {
|
||||
// need login to do more actions
|
||||
if (socket.request.user && socket.request.user.logged_in) {
|
||||
const noteId = socket.noteId
|
||||
@@ -783,7 +783,7 @@ function connection (socket) {
|
||||
where: {
|
||||
id: noteId
|
||||
}
|
||||
}).then(function (count) {
|
||||
}).then(function (count: any) {
|
||||
if (!count) {
|
||||
return
|
||||
}
|
||||
@@ -805,7 +805,7 @@ function connection (socket) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return logger.error('update note permission failed: ' + err)
|
||||
})
|
||||
}
|
||||
@@ -825,7 +825,7 @@ function connection (socket) {
|
||||
where: {
|
||||
id: noteId
|
||||
}
|
||||
}).then(function (count) {
|
||||
}).then(function (count: any) {
|
||||
if (!count) return
|
||||
for (let i = 0, l = note.socks.length; i < l; i++) {
|
||||
const sock = note.socks[i]
|
||||
@@ -836,7 +836,7 @@ function connection (socket) {
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
return logger.error('delete note failed: ' + err)
|
||||
})
|
||||
}
|
||||
@@ -858,7 +858,7 @@ function connection (socket) {
|
||||
socket.on('online users', function () {
|
||||
const noteId = socket.noteId
|
||||
if (!noteId || !notes[noteId]) return
|
||||
const users = []
|
||||
const users: any[] = []
|
||||
Object.keys(notes[noteId].users).forEach(function (key) {
|
||||
const user = notes[noteId].users[key]
|
||||
if (user) { users.push(buildUserOutData(user)) }
|
||||
@@ -878,7 +878,7 @@ function connection (socket) {
|
||||
})
|
||||
|
||||
// received cursor focus
|
||||
socket.on('cursor focus', function (data) {
|
||||
socket.on('cursor focus', function (data: any) {
|
||||
const noteId = socket.noteId
|
||||
const user = users[socket.id]
|
||||
if (!noteId || !notes[noteId] || !user) return
|
||||
@@ -888,7 +888,7 @@ function connection (socket) {
|
||||
})
|
||||
|
||||
// received cursor activity
|
||||
socket.on('cursor activity', function (data) {
|
||||
socket.on('cursor activity', function (data: any) {
|
||||
const noteId = socket.noteId
|
||||
const user = users[socket.id]
|
||||
if (!noteId || !notes[noteId] || !user) return
|
||||
@@ -917,4 +917,4 @@ function connection (socket) {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = realtime
|
||||
export = realtime
|
||||
@@ -1,30 +1,24 @@
|
||||
'use strict'
|
||||
// response
|
||||
// external modules
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
// core
|
||||
const config = require('./config')
|
||||
const logger = require('./logger')
|
||||
const models = require('./models')
|
||||
const noteUtil = require('./web/note/util')
|
||||
const errors = require('./errors')
|
||||
import config = require('./config')
|
||||
import logger = require('./logger')
|
||||
import models = require('./models')
|
||||
import * as noteUtil from './web/note/util'
|
||||
import * as errors from './errors'
|
||||
|
||||
// public
|
||||
const response = {
|
||||
showIndex,
|
||||
githubActions,
|
||||
gitlabActions
|
||||
}
|
||||
|
||||
function showIndex (req, res, next) {
|
||||
const authStatus = req.isAuthenticated()
|
||||
function showIndex (req: Request, res: Response, _next: NextFunction): void {
|
||||
const authStatus: boolean = req.isAuthenticated()
|
||||
const deleteToken = ''
|
||||
|
||||
const data = {
|
||||
const data: Record<string, unknown> = {
|
||||
signin: authStatus,
|
||||
infoMessage: req.flash('info'),
|
||||
errorMessage: req.flash('error'),
|
||||
infoMessage: (req as any).flash('info'),
|
||||
errorMessage: (req as any).flash('error'),
|
||||
imprint: fs.existsSync(path.join(config.docsPath, 'imprint.md')),
|
||||
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
|
||||
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
|
||||
@@ -34,9 +28,9 @@ function showIndex (req, res, next) {
|
||||
if (authStatus) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
id: (req as any).user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (user) {
|
||||
data.deleteToken = user.deleteToken
|
||||
res.render('index.ejs', data)
|
||||
@@ -47,22 +41,7 @@ function showIndex (req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
function githubActions (req, res, next) {
|
||||
const noteId = req.params.noteId
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'gist':
|
||||
githubActionGist(req, res, note)
|
||||
break
|
||||
default:
|
||||
res.redirect(config.serverURL + '/' + noteId)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function githubActionGist (req, res, note) {
|
||||
function githubActionGist (req: Request, res: Response, note: Record<string, unknown>): void {
|
||||
const code = req.query.code
|
||||
const state = req.query.state
|
||||
if (!code || !state) {
|
||||
@@ -92,10 +71,10 @@ function githubActionGist (req, res, note) {
|
||||
if (!accessToken) {
|
||||
throw new Error('forbidden')
|
||||
}
|
||||
const content = note.content
|
||||
const content = note.content as string
|
||||
const title = models.Note.decodeTitle(note.title)
|
||||
const filename = title.replace('/', ' ') + '.md'
|
||||
const gist = {
|
||||
const gist: { files: Record<string, { content: string }> } = {
|
||||
files: {}
|
||||
}
|
||||
gist.files[filename] = {
|
||||
@@ -130,13 +109,13 @@ function githubActionGist (req, res, note) {
|
||||
}
|
||||
}
|
||||
|
||||
function gitlabActions (req, res, next) {
|
||||
function githubActions (req: Request, res: Response, next: NextFunction): void {
|
||||
const noteId = req.params.noteId
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'projects':
|
||||
gitlabActionProjects(req, res, note)
|
||||
case 'gist':
|
||||
githubActionGist(req, res, note)
|
||||
break
|
||||
default:
|
||||
res.redirect(config.serverURL + '/' + noteId)
|
||||
@@ -145,17 +124,17 @@ function gitlabActions (req, res, next) {
|
||||
})
|
||||
}
|
||||
|
||||
function gitlabActionProjects (req, res, note) {
|
||||
function gitlabActionProjects (req: Request, res: Response, _note: Record<string, unknown>): void {
|
||||
if (req.isAuthenticated()) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
id: (req as any).user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) {
|
||||
return errors.errorNotFound(res)
|
||||
}
|
||||
const ret = {
|
||||
const ret: Record<string, unknown> = {
|
||||
baseURL: config.gitlab.baseURL,
|
||||
version: config.gitlab.version,
|
||||
accesstoken: user.accessToken,
|
||||
@@ -175,7 +154,7 @@ function gitlabActionProjects (req, res, note) {
|
||||
}).catch(err => {
|
||||
logger.error('gitlab action projects failed: ', err)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('gitlab action projects failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -184,4 +163,26 @@ function gitlabActionProjects (req, res, note) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = response
|
||||
function gitlabActions (req: Request, res: Response, next: NextFunction): void {
|
||||
const noteId = req.params.noteId
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'projects':
|
||||
gitlabActionProjects(req, res, note)
|
||||
break
|
||||
default:
|
||||
res.redirect(config.serverURL + '/' + noteId)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// public
|
||||
const response = {
|
||||
showIndex,
|
||||
githubActions,
|
||||
gitlabActions
|
||||
}
|
||||
|
||||
export = response
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
declare module 'cheerio' {
|
||||
function load(html: string): any;
|
||||
export = load;
|
||||
}
|
||||
|
||||
declare module 'umzug' {
|
||||
class Umzug {
|
||||
constructor(options: any);
|
||||
up(): Promise<any[]>;
|
||||
down(): Promise<any[]>;
|
||||
pending(): Promise<any[]>;
|
||||
executed(): Promise<any[]>;
|
||||
}
|
||||
export = Umzug;
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
exports.isSQLite = function isSQLite (sequelize) {
|
||||
import type { Request, Response, NextFunction, RequestHandler } from 'express'
|
||||
|
||||
export function isSQLite (sequelize: any): boolean {
|
||||
return sequelize.options.dialect === 'sqlite'
|
||||
}
|
||||
|
||||
exports.isMySQL = function isMySQL (sequelize) {
|
||||
export function isMySQL (sequelize: any): boolean {
|
||||
return ['mysql', 'mariadb'].includes(sequelize.options.dialect)
|
||||
}
|
||||
|
||||
exports.getImageMimeType = function getImageMimeType (imagePath) {
|
||||
export function getImageMimeType (imagePath: string): string | undefined {
|
||||
const fileExtension = /[^.]+$/.exec(imagePath)
|
||||
|
||||
if (!fileExtension) return undefined
|
||||
|
||||
switch (fileExtension[0].toLowerCase()) {
|
||||
case 'bmp':
|
||||
return 'image/bmp'
|
||||
@@ -30,8 +34,8 @@ exports.getImageMimeType = function getImageMimeType (imagePath) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.useUnless = function excludeRoute (paths, middleware) {
|
||||
return function (req, res, next) {
|
||||
export function useUnless (paths: string[], middleware: RequestHandler): RequestHandler {
|
||||
return function (req: Request, res: Response, next: NextFunction) {
|
||||
if (paths.includes(req.path)) {
|
||||
return next()
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const dropboxAuth = module.exports = Router()
|
||||
const dropboxAuth = Router()
|
||||
|
||||
passport.use(new DropboxStrategy({
|
||||
apiVersion: '2',
|
||||
@@ -17,7 +18,7 @@ passport.use(new DropboxStrategy({
|
||||
pkce: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
dropboxAuth.get('/auth/dropbox', function (req, res, next) {
|
||||
dropboxAuth.get('/auth/dropbox', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('dropbox-oauth2')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -28,3 +29,5 @@ dropboxAuth.get('/auth/dropbox/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = dropboxAuth
|
||||
@@ -1,29 +1,30 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const validator = require('validator')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
import validator = require('validator')
|
||||
const LocalStrategy = require('passport-local').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const errors = require('../../../errors')
|
||||
const rateLimit = require('../../middleware/rateLimit')
|
||||
import config = require('../../../config')
|
||||
import models = require('../../../models')
|
||||
import logger = require('../../../logger')
|
||||
import { urlencodedParser } from '../../utils'
|
||||
import * as errors from '../../../errors'
|
||||
import rateLimit = require('../../middleware/rateLimit')
|
||||
|
||||
const emailAuth = module.exports = Router()
|
||||
const emailAuth = Router()
|
||||
|
||||
passport.use(new LocalStrategy({
|
||||
usernameField: 'email'
|
||||
}, function (email, password, done) {
|
||||
}, function (email: string, password: string, done: Function) {
|
||||
if (!validator.isEmail(email)) return done(null, false)
|
||||
models.User.findOne({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) return done(null, false)
|
||||
user.verifyPassword(password).then(verified => {
|
||||
user.verifyPassword(password).then((verified: boolean) => {
|
||||
if (verified) {
|
||||
return done(null, user)
|
||||
} else {
|
||||
@@ -31,14 +32,14 @@ passport.use(new LocalStrategy({
|
||||
return done(null, false)
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error(err)
|
||||
return done(err)
|
||||
})
|
||||
}))
|
||||
|
||||
if (config.allowEmailRegister) {
|
||||
emailAuth.post('/register', rateLimit.userEndpoints, urlencodedParser, function (req, res, next) {
|
||||
emailAuth.post('/register', rateLimit.userEndpoints, urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.body.email || !req.body.password) return errors.errorBadRequest(res)
|
||||
if (!validator.isEmail(req.body.email)) return errors.errorBadRequest(res)
|
||||
models.User.findOrCreate({
|
||||
@@ -48,23 +49,23 @@ if (config.allowEmailRegister) {
|
||||
defaults: {
|
||||
password: req.body.password
|
||||
}
|
||||
}).spread(function (user, created) {
|
||||
}).spread(function (user: any, created: boolean) {
|
||||
if (user && created) {
|
||||
logger.debug('user registered: ' + user.id)
|
||||
req.flash('info', "You've successfully registered, please sign in.")
|
||||
;(req as any).flash('info', "You've successfully registered, please sign in.")
|
||||
return res.redirect(config.serverURL + '/')
|
||||
}
|
||||
logger.debug('registration failed. user: ', user)
|
||||
req.flash('error', 'Failed to register your account.')
|
||||
;(req as any).flash('error', 'Failed to register your account.')
|
||||
return res.redirect(config.serverURL + '/')
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error('auth callback failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (req, res, next) {
|
||||
emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.body.email || !req.body.password) return errors.errorBadRequest(res)
|
||||
if (!validator.isEmail(req.body.email)) return errors.errorBadRequest(res)
|
||||
passport.authenticate('local', {
|
||||
@@ -73,3 +74,5 @@ emailAuth.post('/login', rateLimit.userEndpoints, urlencodedParser, function (re
|
||||
failureFlash: 'Invalid email or password.'
|
||||
})(req, res, next)
|
||||
})
|
||||
|
||||
export = emailAuth
|
||||
@@ -1,13 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const FacebookStrategy = require('passport-facebook').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const facebookAuth = module.exports = Router()
|
||||
const facebookAuth = Router()
|
||||
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: config.facebook.clientID,
|
||||
@@ -17,7 +18,7 @@ passport.use(new FacebookStrategy({
|
||||
pkce: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
facebookAuth.get('/auth/facebook', function (req, res, next) {
|
||||
facebookAuth.get('/auth/facebook', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('facebook')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -28,3 +29,5 @@ facebookAuth.get('/auth/facebook/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = facebookAuth
|
||||
@@ -1,13 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const GithubStrategy = require('passport-github').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import response = require('../../../response')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const githubAuth = module.exports = Router()
|
||||
const githubAuth = Router()
|
||||
|
||||
passport.use(new GithubStrategy({
|
||||
clientID: config.github.clientID,
|
||||
@@ -17,7 +18,7 @@ passport.use(new GithubStrategy({
|
||||
state: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
githubAuth.get('/auth/github', function (req, res, next) {
|
||||
githubAuth.get('/auth/github', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('github')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -31,3 +32,5 @@ githubAuth.get('/auth/github/callback',
|
||||
|
||||
// github callback actions
|
||||
githubAuth.get('/auth/github/callback/:noteId/:action', response.githubActions)
|
||||
|
||||
export = githubAuth
|
||||
@@ -1,13 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const GitlabStrategy = require('passport-gitlab2').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import response = require('../../../response')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const gitlabAuth = module.exports = Router()
|
||||
const gitlabAuth = Router()
|
||||
|
||||
passport.use(new GitlabStrategy({
|
||||
baseURL: config.gitlab.baseURL,
|
||||
@@ -19,7 +20,7 @@ passport.use(new GitlabStrategy({
|
||||
state: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
gitlabAuth.get('/auth/gitlab', function (req, res, next) {
|
||||
gitlabAuth.get('/auth/gitlab', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('gitlab')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -35,3 +36,5 @@ if (!config.gitlab.scope || config.gitlab.scope === 'api') {
|
||||
// gitlab callback actions
|
||||
gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions)
|
||||
}
|
||||
|
||||
export = gitlabAuth
|
||||
@@ -1,12 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const GoogleStrategy = require('passport-google-oauth20').Strategy
|
||||
const config = require('../../../config')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const googleAuth = module.exports = Router()
|
||||
const googleAuth = Router()
|
||||
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID: config.google.clientID,
|
||||
@@ -17,7 +18,7 @@ passport.use(new GoogleStrategy({
|
||||
state: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
googleAuth.get('/auth/google', function (req, res, next) {
|
||||
googleAuth.get('/auth/google', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('google', { scope: ['profile'], hostedDomain: config.google.hostedDomain })(req, res, next)
|
||||
})
|
||||
// google auth callback
|
||||
@@ -27,3 +28,5 @@ googleAuth.get('/auth/google/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = googleAuth
|
||||
@@ -1,26 +1,27 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const models = require('../../models')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
import models = require('../../models')
|
||||
|
||||
const authRouter = module.exports = Router()
|
||||
const authRouter = Router()
|
||||
|
||||
// serialize and deserialize
|
||||
passport.serializeUser(function (user, done) {
|
||||
passport.serializeUser(function (user: any, done: Function) {
|
||||
logger.info('serializeUser: ' + user.id)
|
||||
return done(null, user.id)
|
||||
})
|
||||
|
||||
passport.deserializeUser(function (id, done) {
|
||||
passport.deserializeUser(function (id: string, done: Function) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
// Don't die on non-existent user
|
||||
if (user == null) {
|
||||
return done(null, false, { message: 'Invalid UserID' })
|
||||
@@ -28,7 +29,7 @@ passport.deserializeUser(function (id, done) {
|
||||
|
||||
logger.info('deserializeUser: ' + user.id)
|
||||
return done(null, user)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error(err)
|
||||
return done(err, null)
|
||||
})
|
||||
@@ -48,11 +49,13 @@ 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: Request, res: Response) {
|
||||
if (config.debug && req.isAuthenticated()) {
|
||||
logger.debug('user logout: ' + req.user.id)
|
||||
logger.debug('user logout: ' + (req as any).user.id)
|
||||
}
|
||||
req.logout(() => {
|
||||
res.redirect(config.serverURL + '/')
|
||||
})
|
||||
})
|
||||
|
||||
export = authRouter
|
||||
@@ -1,16 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const LDAPStrategy = require('passport-ldapauth')
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const errors = require('../../../errors')
|
||||
const { cloneDeep } = require('lodash')
|
||||
import config = require('../../../config')
|
||||
import models = require('../../../models')
|
||||
import logger = require('../../../logger')
|
||||
import { urlencodedParser } from '../../utils'
|
||||
import * as errors from '../../../errors'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const ldapAuth = module.exports = Router()
|
||||
const ldapAuth = Router()
|
||||
|
||||
// ldapauth-fork mutates the config object, so we need to make a clone of our deep-frozen config
|
||||
const mutableLdapConfig = cloneDeep(config.ldap)
|
||||
@@ -25,7 +26,7 @@ passport.use(new LDAPStrategy({
|
||||
searchAttributes: mutableLdapConfig.searchAttributes || null,
|
||||
tlsOptions: mutableLdapConfig.tlsOptions || null
|
||||
}
|
||||
}, function (user, done) {
|
||||
}, function (user: any, done: Function) {
|
||||
let uuid = user.uidNumber || user.uid || user.sAMAccountName || undefined
|
||||
if (config.ldap.useridField && user[config.ldap.useridField]) {
|
||||
uuid = user[config.ldap.useridField]
|
||||
@@ -60,7 +61,7 @@ passport.use(new LDAPStrategy({
|
||||
defaults: {
|
||||
profile: stringifiedProfile
|
||||
}
|
||||
}).spread(function (user, created) {
|
||||
}).spread(function (user: any, created: boolean) {
|
||||
if (user) {
|
||||
let needSave = false
|
||||
if (user.profile !== stringifiedProfile) {
|
||||
@@ -77,13 +78,13 @@ passport.use(new LDAPStrategy({
|
||||
return done(null, user)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error('ldap auth failed: ' + err)
|
||||
return done(err, null)
|
||||
})
|
||||
}))
|
||||
|
||||
ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
|
||||
ldapAuth.post('/auth/ldap', urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.body.username || !req.body.password) return errors.errorBadRequest(res)
|
||||
passport.authenticate('ldapauth', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
@@ -91,3 +92,5 @@ ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
|
||||
failureFlash: true
|
||||
})(req, res, next)
|
||||
})
|
||||
|
||||
export = ldapAuth
|
||||
@@ -1,15 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const Mattermost = require('mattermost')
|
||||
const OAuthStrategy = require('passport-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const mattermost = new Mattermost.Client()
|
||||
|
||||
const mattermostAuth = module.exports = Router()
|
||||
const mattermostAuth = Router()
|
||||
|
||||
const mattermostStrategy = new OAuthStrategy({
|
||||
authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
|
||||
@@ -20,15 +21,15 @@ const mattermostStrategy = new OAuthStrategy({
|
||||
state: true
|
||||
}, passportGeneralCallback)
|
||||
|
||||
mattermostStrategy.userProfile = (accessToken, done) => {
|
||||
mattermostStrategy.userProfile = (accessToken: string, done: Function) => {
|
||||
mattermost.setUrl(config.mattermost.baseURL)
|
||||
mattermost.token = accessToken
|
||||
mattermost.useHeaderToken()
|
||||
mattermost.getMe(
|
||||
(data) => {
|
||||
(data: any) => {
|
||||
done(null, data)
|
||||
},
|
||||
(err) => {
|
||||
(err: any) => {
|
||||
done(err)
|
||||
}
|
||||
)
|
||||
@@ -36,7 +37,7 @@ mattermostStrategy.userProfile = (accessToken, done) => {
|
||||
|
||||
passport.use(mattermostStrategy)
|
||||
|
||||
mattermostAuth.get('/auth/mattermost', function (req, res, next) {
|
||||
mattermostAuth.get('/auth/mattermost', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('oauth2')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -47,3 +48,5 @@ mattermostAuth.get('/auth/mattermost/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = mattermostAuth
|
||||
@@ -1,26 +1,29 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
||||
const config = require('../../../config')
|
||||
const logger = require('../../../logger')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
import { Strategy, InternalOAuthError } from 'passport-oauth2'
|
||||
import config = require('../../../config')
|
||||
import logger = require('../../../logger')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const oauth2Auth = module.exports = Router()
|
||||
const oauth2Auth = Router()
|
||||
|
||||
class OAuth2CustomStrategy extends Strategy {
|
||||
constructor (options, verify) {
|
||||
_userProfileURL: string
|
||||
|
||||
constructor (options: any, verify: any) {
|
||||
options.customHeaders = options.customHeaders || {}
|
||||
super(options, verify)
|
||||
this.name = 'oauth2'
|
||||
this._userProfileURL = options.userProfileURL
|
||||
this._oauth2.useAuthorizationHeaderforGET(true)
|
||||
;(this as any)._oauth2.useAuthorizationHeaderforGET(true)
|
||||
}
|
||||
|
||||
userProfile (accessToken, done) {
|
||||
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
|
||||
let json, profile
|
||||
userProfile (accessToken: string, done: Function): void {
|
||||
;(this as any)._oauth2.get(this._userProfileURL, accessToken, function (err: any, body: string, res: any) {
|
||||
let json: any, profile: any
|
||||
|
||||
if (err) {
|
||||
return done(new InternalOAuthError('Failed to fetch user profile', err))
|
||||
@@ -45,17 +48,17 @@ class OAuth2CustomStrategy extends Strategy {
|
||||
}
|
||||
}
|
||||
|
||||
function extractProfileAttribute (data, path) {
|
||||
function extractProfileAttribute (data: any, path: string): any {
|
||||
// can handle stuff like `attrs[0].name`
|
||||
path = path.split('.')
|
||||
for (const segment of path) {
|
||||
const segments = path.split('.')
|
||||
for (const segment of segments) {
|
||||
const m = segment.match(/([\d\w]+)\[(.*)\]/)
|
||||
data = m ? data[m[1]][m[2]] : data[segment]
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
function parseProfile (data) {
|
||||
function parseProfile (data: any): any {
|
||||
// only try to parse the id if a claim is configured
|
||||
const id = config.oauth2.userProfileIdAttr ? extractProfileAttribute(data, config.oauth2.userProfileIdAttr) : undefined
|
||||
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
|
||||
@@ -75,7 +78,7 @@ function parseProfile (data) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkAuthorization (data, done) {
|
||||
function checkAuthorization (data: any, done: Function): void {
|
||||
// a role the user must have is set in the config
|
||||
if (config.oauth2.accessRole) {
|
||||
// check if we know which claim contains the list of groups a user is in
|
||||
@@ -84,7 +87,7 @@ function checkAuthorization (data, done) {
|
||||
logger.error('oauth2: "accessRole" is configured, but "rolesClaim" is missing from the config. Can\'t check group membership!')
|
||||
} else {
|
||||
// parse and check role data
|
||||
let roles = []
|
||||
let roles: any[] = []
|
||||
try {
|
||||
roles = extractProfileAttribute(data, config.oauth2.rolesClaim)
|
||||
} catch (err) {
|
||||
@@ -104,9 +107,9 @@ function checkAuthorization (data, done) {
|
||||
}
|
||||
}
|
||||
|
||||
OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
|
||||
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
|
||||
let json, profile
|
||||
OAuth2CustomStrategy.prototype.userProfile = function (accessToken: string, done: Function): void {
|
||||
;(this as any)._oauth2.get(this._userProfileURL, accessToken, function (err: any, body: string, res: any) {
|
||||
let json: any, profile: any
|
||||
|
||||
if (err) {
|
||||
return done(new InternalOAuthError('Failed to fetch user profile', err))
|
||||
@@ -142,7 +145,7 @@ passport.use(new OAuth2CustomStrategy({
|
||||
state: true
|
||||
}, passportGeneralCallback))
|
||||
|
||||
oauth2Auth.get('/auth/oauth2', function (req, res, next) {
|
||||
oauth2Auth.get('/auth/oauth2', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('oauth2')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -153,3 +156,5 @@ oauth2Auth.get('/auth/oauth2/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = oauth2Auth
|
||||
@@ -1,20 +1,21 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
import config = require('../../../config')
|
||||
import models = require('../../../models')
|
||||
import logger = require('../../../logger')
|
||||
import { urlencodedParser } from '../../utils'
|
||||
|
||||
const openIDAuth = module.exports = Router()
|
||||
const openIDAuth = Router()
|
||||
|
||||
passport.use(new OpenIDStrategy({
|
||||
returnURL: config.serverURL + '/auth/openid/callback',
|
||||
realm: config.serverURL,
|
||||
profile: true
|
||||
}, function (openid, profile, done) {
|
||||
}, function (openid: string, profile: any, done: Function) {
|
||||
const stringifiedProfile = JSON.stringify(profile)
|
||||
models.User.findOrCreate({
|
||||
where: {
|
||||
@@ -23,7 +24,7 @@ passport.use(new OpenIDStrategy({
|
||||
defaults: {
|
||||
profile: stringifiedProfile
|
||||
}
|
||||
}).spread(function (user, created) {
|
||||
}).spread(function (user: any, created: boolean) {
|
||||
if (user) {
|
||||
let needSave = false
|
||||
if (user.profile !== stringifiedProfile) {
|
||||
@@ -40,13 +41,13 @@ passport.use(new OpenIDStrategy({
|
||||
return done(null, user)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error('auth callback failed: ' + err)
|
||||
return done(err, null)
|
||||
})
|
||||
}))
|
||||
|
||||
openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) {
|
||||
openIDAuth.post('/auth/openid', urlencodedParser, function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('openid')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -57,3 +58,5 @@ openIDAuth.get('/auth/openid/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = openIDAuth
|
||||
@@ -1,16 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const SamlStrategy = require('@node-saml/passport-saml').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const fs = require('fs')
|
||||
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
|
||||
import config = require('../../../config')
|
||||
import models = require('../../../models')
|
||||
import logger = require('../../../logger')
|
||||
import { urlencodedParser } from '../../utils'
|
||||
import * as fs from 'fs'
|
||||
const intersection = function (array1: string[], array2: string[]): string[] { return array1.filter((n) => array2.includes(n)) }
|
||||
|
||||
const samlAuth = module.exports = Router()
|
||||
const samlAuth = Router()
|
||||
|
||||
passport.use(
|
||||
new SamlStrategy(
|
||||
@@ -23,14 +24,14 @@ passport.use(
|
||||
: (function () {
|
||||
try {
|
||||
return fs.readFileSync(config.saml.clientCert, 'utf-8')
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logger.error(`SAML client certificate: ${e.message}`)
|
||||
}
|
||||
}()),
|
||||
idpCert: (function () {
|
||||
try {
|
||||
return fs.readFileSync(config.saml.idpCert, 'utf-8')
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logger.error(`SAML idp certificate: ${e.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -41,7 +42,7 @@ passport.use(
|
||||
wantAuthnResponseSigned: config.saml.wantAuthnResponseSigned
|
||||
},
|
||||
// sign-in
|
||||
function (user, done) {
|
||||
function (user: any, done: Function) {
|
||||
// check authorization if needed
|
||||
if (config.saml.externalGroups && config.saml.groupAttribute) {
|
||||
const externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute])
|
||||
@@ -76,7 +77,7 @@ passport.use(
|
||||
defaults: {
|
||||
profile: stringifiedProfile
|
||||
}
|
||||
}).spread(function (user, created) {
|
||||
}).spread(function (user: any, created: boolean) {
|
||||
if (user) {
|
||||
let needSave = false
|
||||
if (user.profile !== stringifiedProfile) {
|
||||
@@ -93,13 +94,13 @@ passport.use(
|
||||
return done(null, user)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
logger.error('saml auth failed: ' + err.message)
|
||||
return done(err, null)
|
||||
})
|
||||
},
|
||||
// logout
|
||||
function (profile, done) {
|
||||
function (profile: any, done: Function) {
|
||||
return done(null, profile)
|
||||
}
|
||||
)
|
||||
@@ -110,13 +111,13 @@ samlAuth.get('/auth/saml',
|
||||
failureRedirect: config.serverURL + '/',
|
||||
failureFlash: true
|
||||
}),
|
||||
function (req, res) {
|
||||
function (req: Request, res: Response) {
|
||||
res.redirect('/')
|
||||
}
|
||||
)
|
||||
|
||||
samlAuth.use('/auth/saml/callback', urlencodedParser,
|
||||
function (req, res, next) {
|
||||
function (req: Request, res: Response, next: NextFunction) {
|
||||
if (req.method !== 'GET' && req.method !== 'POST') {
|
||||
return res.status(405).end()
|
||||
}
|
||||
@@ -126,12 +127,14 @@ samlAuth.use('/auth/saml/callback', urlencodedParser,
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
}),
|
||||
function (req, res) {
|
||||
function (req: Request, res: Response) {
|
||||
res.redirect('/')
|
||||
}
|
||||
)
|
||||
|
||||
samlAuth.get('/auth/saml/metadata', function (req, res) {
|
||||
samlAuth.get('/auth/saml/metadata', function (req: Request, res: Response) {
|
||||
res.type('application/xml')
|
||||
res.send(passport._strategy('saml').generateServiceProviderMetadata())
|
||||
res.send((passport as any)._strategy('saml').generateServiceProviderMetadata())
|
||||
})
|
||||
|
||||
export = samlAuth
|
||||
@@ -1,13 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import passport = require('passport')
|
||||
const TwitterStrategy = require('passport-twitter').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const { passportGeneralCallback } = require('../utils')
|
||||
import config = require('../../../config')
|
||||
import { passportGeneralCallback } from '../utils'
|
||||
|
||||
const twitterAuth = module.exports = Router()
|
||||
const twitterAuth = Router()
|
||||
|
||||
passport.use(new TwitterStrategy({
|
||||
consumerKey: config.twitter.consumerKey,
|
||||
@@ -15,7 +16,7 @@ passport.use(new TwitterStrategy({
|
||||
callbackURL: config.serverURL + '/auth/twitter/callback'
|
||||
}, passportGeneralCallback))
|
||||
|
||||
twitterAuth.get('/auth/twitter', function (req, res, next) {
|
||||
twitterAuth.get('/auth/twitter', function (req: Request, res: Response, next: NextFunction) {
|
||||
passport.authenticate('twitter')(req, res, next)
|
||||
})
|
||||
|
||||
@@ -26,3 +27,5 @@ twitterAuth.get('/auth/twitter/callback',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
||||
|
||||
export = twitterAuth
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
import models = require('../../models')
|
||||
import logger = require('../../logger')
|
||||
|
||||
exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {
|
||||
export function passportGeneralCallback (accessToken: string, refreshToken: string, profile: any, done: (err: Error | null, user?: any) => void): void {
|
||||
const stringifiedProfile = JSON.stringify(profile)
|
||||
models.User.findOrCreate({
|
||||
where: {
|
||||
@@ -14,7 +14,7 @@ exports.passportGeneralCallback = function callback (accessToken, refreshToken,
|
||||
accessToken,
|
||||
refreshToken
|
||||
}
|
||||
}).spread(function (user, created) {
|
||||
}).spread(function (user: any, created: boolean) {
|
||||
if (user) {
|
||||
let needSave = false
|
||||
if (user.profile !== stringifiedProfile) {
|
||||
@@ -39,7 +39,7 @@ exports.passportGeneralCallback = function callback (accessToken, refreshToken,
|
||||
return done(null, user)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('auth callback failed: ' + err)
|
||||
return done(err, null)
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
|
||||
const response = require('../response')
|
||||
|
||||
const baseRouter = module.exports = Router()
|
||||
|
||||
const errors = require('../errors')
|
||||
|
||||
// get index
|
||||
baseRouter.get('/', response.showIndex)
|
||||
// get 403 forbidden
|
||||
baseRouter.get('/403', function (req, res) {
|
||||
errors.errorForbidden(res)
|
||||
})
|
||||
// get 404 not found
|
||||
baseRouter.get('/404', function (req, res) {
|
||||
errors.errorNotFound(res)
|
||||
})
|
||||
// get 500 internal error
|
||||
baseRouter.get('/500', function (req, res) {
|
||||
errors.errorInternalError(res)
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response } from 'express'
|
||||
import { Router } from 'express'
|
||||
import response = require('../response')
|
||||
import * as errors from '../errors'
|
||||
|
||||
const baseRouter = Router()
|
||||
|
||||
// get index
|
||||
baseRouter.get('/', response.showIndex)
|
||||
// get 403 forbidden
|
||||
baseRouter.get('/403', function (_req: Request, res: Response) {
|
||||
errors.errorForbidden(res)
|
||||
})
|
||||
// get 404 not found
|
||||
baseRouter.get('/404', function (_req: Request, res: Response) {
|
||||
errors.errorNotFound(res)
|
||||
})
|
||||
// get 500 internal error
|
||||
baseRouter.get('/500', function (_req: Request, res: Response) {
|
||||
errors.errorInternalError(res)
|
||||
})
|
||||
|
||||
export = baseRouter
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
import { Router } from 'express'
|
||||
import { urlencodedParser } from './utils'
|
||||
import history = require('../history')
|
||||
|
||||
const { urlencodedParser } = require('./utils')
|
||||
const history = require('../history')
|
||||
const historyRouter = module.exports = Router()
|
||||
const historyRouter = Router()
|
||||
|
||||
// get history
|
||||
historyRouter.get('/history', history.historyGet)
|
||||
@@ -16,3 +16,5 @@ historyRouter.post('/history/:noteId', urlencodedParser, history.historyPost)
|
||||
historyRouter.delete('/history', history.historyDelete)
|
||||
// delete history by note id
|
||||
historyRouter.delete('/history/:noteId', history.historyDelete)
|
||||
|
||||
export = historyRouter
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict'
|
||||
const path = require('path')
|
||||
import * as path from 'path'
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
|
||||
const azure = require('azure-storage')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!callback || typeof callback !== 'function') {
|
||||
logger.error('Callback has to be a function')
|
||||
return
|
||||
@@ -19,11 +19,11 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
|
||||
const azureBlobService = azure.createBlobService(config.azure.connectionString)
|
||||
|
||||
azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) {
|
||||
azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err: any, result: any, response: any) {
|
||||
if (err) {
|
||||
callback(new Error(err.message), null)
|
||||
} else {
|
||||
azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) {
|
||||
azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err: any, result: any, response: any) {
|
||||
if (err) {
|
||||
callback(new Error(err.message), null)
|
||||
} else {
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict'
|
||||
const URL = require('url').URL
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
import { URL } from 'url'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!callback || typeof callback !== 'function') {
|
||||
logger.error('Callback has to be a function')
|
||||
return
|
||||
@@ -21,7 +21,7 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
// move image from temporary path to upload directory
|
||||
try {
|
||||
fs.copyFileSync(imagePath, path.join(config.uploadsPath, fileName))
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
callback(new Error(`Error while moving file: ${e.message}`), null)
|
||||
return
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const fs = require('fs')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
import * as fs from 'fs'
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!callback || typeof callback !== 'function') {
|
||||
logger.error('Callback has to be a function')
|
||||
return
|
||||
@@ -34,10 +34,10 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.then((json) => {
|
||||
.then((json: any) => {
|
||||
logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`)
|
||||
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
|
||||
}).catch((err) => {
|
||||
}).catch((err: any) => {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
}
|
||||
@@ -1,35 +1,36 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
import type { Request, Response } from 'express'
|
||||
import { Router } from 'express'
|
||||
const formidable = require('formidable')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
const os = require('os')
|
||||
const rimraf = require('rimraf')
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as os from 'os'
|
||||
import rimraf = require('rimraf')
|
||||
const isSvg = require('is-svg')
|
||||
const { JSDOM } = require('jsdom')
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const errors = require('../../errors')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
import * as errors from '../../errors'
|
||||
|
||||
let DOMPurify
|
||||
let DOMPurify: any
|
||||
try {
|
||||
const window = new JSDOM('').window
|
||||
global.window = window
|
||||
;(global as any).window = window
|
||||
const createDOMPurify = require('dompurify')
|
||||
DOMPurify = createDOMPurify(window)
|
||||
} catch (err) {
|
||||
logger.error('Failed to initialize DOMPurify for SVG sanitization:', err)
|
||||
}
|
||||
|
||||
const imageRouter = (module.exports = Router())
|
||||
const imageRouter = Router()
|
||||
|
||||
async function checkUploadType (filePath) {
|
||||
async function checkUploadType (filePath: string): Promise<boolean> {
|
||||
const extension = path.extname(filePath).toLowerCase()
|
||||
const FileType = await import('file-type')
|
||||
let typeFromMagic = await FileType.fileTypeFromFile(filePath)
|
||||
let typeFromMagic = await FileType.fileTypeFromFile(filePath) as { ext: string; mime: string } | undefined
|
||||
if (extension === '.svg' && (typeFromMagic === undefined || typeFromMagic.mime === 'application/xml')) {
|
||||
const fileContent = fs.readFileSync(filePath)
|
||||
if (isSvg(fileContent)) {
|
||||
@@ -45,7 +46,7 @@ async function checkUploadType (filePath) {
|
||||
}
|
||||
// .jpeg, .jfif, .jpe files are identified by FileType to have the extension jpg
|
||||
if (['.jpeg', '.jfif', '.jpe'].includes(extension) && typeFromMagic.ext === 'jpg') {
|
||||
typeFromMagic.ext = extension.substr(1)
|
||||
(typeFromMagic as any).ext = extension.substr(1)
|
||||
}
|
||||
if (extension !== '.' + typeFromMagic.ext) {
|
||||
logger.error(
|
||||
@@ -66,7 +67,7 @@ async function checkUploadType (filePath) {
|
||||
return true
|
||||
}
|
||||
|
||||
function sanitizeSvg (filePath) {
|
||||
function sanitizeSvg (filePath: string): boolean {
|
||||
if (!DOMPurify) {
|
||||
logger.error('SVG sanitization failed: DOMPurify not initialized')
|
||||
return false
|
||||
@@ -92,7 +93,7 @@ function sanitizeSvg (filePath) {
|
||||
}
|
||||
|
||||
// upload image
|
||||
imageRouter.post('/uploadimage', function (req, res) {
|
||||
imageRouter.post('/uploadimage', function (req: Request, res: Response) {
|
||||
const uploadsEnabled = config.enableUploads
|
||||
if (uploadsEnabled === 'none') {
|
||||
logger.error('Image upload error: Uploads are disabled')
|
||||
@@ -112,7 +113,7 @@ imageRouter.post('/uploadimage', function (req, res) {
|
||||
const form = formidable({
|
||||
keepExtensions: true,
|
||||
uploadDir: tmpDir,
|
||||
filename: function (filename, ext) {
|
||||
filename: function (filename: string, ext: string) {
|
||||
if (typeof ext !== 'string') {
|
||||
ext = '.invalid'
|
||||
}
|
||||
@@ -120,7 +121,7 @@ imageRouter.post('/uploadimage', function (req, res) {
|
||||
}
|
||||
})
|
||||
|
||||
form.parse(req, async function (err, fields, files) {
|
||||
form.parse(req, async function (err: any, fields: any, files: any) {
|
||||
if (err) {
|
||||
logger.error(`Image upload error: formidable error: ${err}`)
|
||||
rimraf.sync(tmpDir)
|
||||
@@ -148,7 +149,7 @@ imageRouter.post('/uploadimage', function (req, res) {
|
||||
logger.debug(
|
||||
`imageRouter: Uploading ${files.image.filepath} using ${config.imageUploadType}`
|
||||
)
|
||||
uploadProvider.uploadImage(files.image.filepath, function (err, url) {
|
||||
uploadProvider.uploadImage(files.image.filepath, function (err: Error | null, url: string) {
|
||||
rimraf.sync(tmpDir)
|
||||
if (err !== null) {
|
||||
logger.error(err)
|
||||
@@ -162,3 +163,5 @@ imageRouter.post('/uploadimage', function (req, res) {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export = imageRouter
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict'
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import logger = require('../../logger')
|
||||
|
||||
const lutim = require('lutim')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!callback || typeof callback !== 'function') {
|
||||
logger.error('Callback has to be a function')
|
||||
return
|
||||
@@ -21,10 +21,10 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
}
|
||||
|
||||
lutim.uploadImage(imagePath)
|
||||
.then(function (json) {
|
||||
.then(function (json: any) {
|
||||
logger.debug(`SERVER uploadimage success: ${JSON.stringify(json)}`)
|
||||
callback(null, lutim.getAPIUrl() + json.msg.short)
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: any) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const config = require('../../config')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import { getImageMimeType } from '../../utils'
|
||||
import logger = require('../../logger')
|
||||
|
||||
const Minio = require('minio')
|
||||
const minioClient = new Minio.Client({
|
||||
@@ -15,7 +15,7 @@ const minioClient = new Minio.Client({
|
||||
secretKey: config.minio.secretKey
|
||||
})
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
||||
callback(new Error('Image path is missing or wrong'), null)
|
||||
return
|
||||
@@ -26,20 +26,20 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
return
|
||||
}
|
||||
|
||||
fs.readFile(imagePath, function (err, buffer) {
|
||||
fs.readFile(imagePath, function (err: NodeJS.ErrnoException | null, buffer: Buffer) {
|
||||
if (err) {
|
||||
callback(new Error(err), null)
|
||||
callback(new Error(String(err)), null)
|
||||
return
|
||||
}
|
||||
|
||||
const key = path.join('uploads', path.basename(imagePath))
|
||||
const protocol = config.minio.secure ? 'https' : 'http'
|
||||
|
||||
minioClient.putObject(config.s3bucket, key, buffer, buffer.size, {
|
||||
minioClient.putObject(config.s3bucket, key, buffer, (buffer as any).size, {
|
||||
'Content-Type': getImageMimeType(imagePath) || 'application/octet-stream'
|
||||
}, function (err, data) {
|
||||
}, function (err: any, data: any) {
|
||||
if (err) {
|
||||
callback(new Error(err), null)
|
||||
callback(new Error(String(err)), null)
|
||||
return
|
||||
}
|
||||
const hidePort = [80, 443].includes(config.minio.port)
|
||||
@@ -1,16 +1,16 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const config = require('../../config')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import { getImageMimeType } from '../../utils'
|
||||
import logger = require('../../logger')
|
||||
|
||||
const AWS = require('aws-sdk')
|
||||
const awsConfig = new AWS.Config(config.s3)
|
||||
const s3 = new AWS.S3(awsConfig)
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
export function uploadImage (imagePath: string, callback: (err: Error | null, url: string | null) => void): void {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
||||
callback(new Error('Image path is missing or wrong'), null)
|
||||
return
|
||||
@@ -21,12 +21,12 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
return
|
||||
}
|
||||
|
||||
fs.readFile(imagePath, function (err, buffer) {
|
||||
fs.readFile(imagePath, function (err: NodeJS.ErrnoException | null, buffer: Buffer) {
|
||||
if (err) {
|
||||
callback(new Error(err), null)
|
||||
callback(new Error(String(err)), null)
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
const params: any = {
|
||||
Bucket: config.s3bucket,
|
||||
Key: path.join(config.s3folder, path.basename(imagePath)),
|
||||
Body: buffer
|
||||
@@ -37,9 +37,9 @@ exports.uploadImage = function (imagePath, callback) {
|
||||
if (config.s3publicFiles) { params.ACL = 'public-read' }
|
||||
|
||||
logger.debug(`S3 object parameters: ${JSON.stringify(params)}`)
|
||||
s3.putObject(params, function (err, data) {
|
||||
s3.putObject(params, function (err: any, data: any) {
|
||||
if (err) {
|
||||
callback(new Error(err), null)
|
||||
callback(new Error(String(err)), null)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const logger = require('../../logger')
|
||||
const errors = require('../../errors')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
try {
|
||||
decodeURIComponent(req.path)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return errors.errorBadRequest(res)
|
||||
}
|
||||
next()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import logger = require('../../logger')
|
||||
import * as errors from '../../errors'
|
||||
|
||||
const checkURIValid = function (req: Request, res: Response, next: NextFunction): void {
|
||||
try {
|
||||
decodeURIComponent(req.path)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return errors.errorBadRequest(res)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
export = checkURIValid
|
||||
@@ -1,10 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
res.set({
|
||||
'HedgeDoc-Version': config.version
|
||||
})
|
||||
return next()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import config = require('../../config')
|
||||
|
||||
const hedgeDocVersion = function (_req: Request, res: Response, next: NextFunction): void {
|
||||
res.set({
|
||||
'HedgeDoc-Version': config.version
|
||||
})
|
||||
return next()
|
||||
}
|
||||
|
||||
export = hedgeDocVersion
|
||||
@@ -1,33 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { rateLimit, ipKeyGenerator } = require('express-rate-limit')
|
||||
const errors = require('../../errors')
|
||||
const config = require('../../config')
|
||||
|
||||
const determineKey = (req) => {
|
||||
if (req.user) {
|
||||
return req.user.id
|
||||
}
|
||||
return ipKeyGenerator(req.header('cf-connecting-ip') || req.ip)
|
||||
}
|
||||
|
||||
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
|
||||
const userEndpoints = rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
limit: 10,
|
||||
keyGenerator: determineKey,
|
||||
handler: (req, res) => errors.errorTooManyRequests(res)
|
||||
})
|
||||
|
||||
// limits the amount of requests to the new note endpoint per 5 minutes based on configuration
|
||||
const newNotes = rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
limit: config.rateLimitNewNotes,
|
||||
keyGenerator: determineKey,
|
||||
handler: (req, res) => errors.errorTooManyRequests(res)
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
userEndpoints,
|
||||
newNotes
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { rateLimit } from 'express-rate-limit'
|
||||
import * as errors from '../../errors'
|
||||
import config = require('../../config')
|
||||
|
||||
const determineKey = (req: Request): string => {
|
||||
if ((req as any).user) {
|
||||
return (req as any).user.id
|
||||
}
|
||||
return req.header('cf-connecting-ip') || req.ip || 'unknown'
|
||||
}
|
||||
|
||||
// limits requests to user endpoints (login, signup) to 10 requests per 5 minutes
|
||||
export const userEndpoints = rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
limit: 10,
|
||||
keyGenerator: determineKey,
|
||||
handler: (_req: Request, res: Response) => errors.errorTooManyRequests(res)
|
||||
})
|
||||
|
||||
// limits the amount of requests to the new note endpoint per 5 minutes based on configuration
|
||||
export const newNotes = rateLimit({
|
||||
windowMs: 5 * 60 * 1000,
|
||||
limit: config.rateLimitNewNotes,
|
||||
keyGenerator: determineKey,
|
||||
handler: (_req: Request, res: Response) => errors.errorTooManyRequests(res)
|
||||
})
|
||||
+6
-3
@@ -1,12 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const config = require('../../config')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import config = require('../../config')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
const redirectWithoutTrailingSlashes = function (req: Request, res: Response, next: NextFunction): void {
|
||||
if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {
|
||||
const queryString = req.url.slice(req.path.length)
|
||||
const urlPath = req.path.slice(0, -1)
|
||||
let serverURL = config.serverURL
|
||||
let serverURL: string = config.serverURL
|
||||
if (config.urlPath) {
|
||||
serverURL = serverURL.slice(0, -(config.urlPath.length + 1))
|
||||
}
|
||||
@@ -15,3 +16,5 @@ module.exports = function (req, res, next) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export = redirectWithoutTrailingSlashes
|
||||
@@ -1,21 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const toobusy = require('toobusy-js')
|
||||
|
||||
const errors = require('../../errors')
|
||||
const config = require('../../config')
|
||||
|
||||
toobusy.maxLag(config.tooBusyLag)
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
// We dont want to return "toobusy" errors for healthchecks, as that
|
||||
// will cause the process to be restarted
|
||||
if (req.baseUrl === '/_health') {
|
||||
next()
|
||||
}
|
||||
if (toobusy()) {
|
||||
errors.errorServiceUnavailable(res)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import toobusy = require('toobusy-js')
|
||||
import * as errors from '../../errors'
|
||||
import config = require('../../config')
|
||||
|
||||
toobusy.maxLag(config.tooBusyLag)
|
||||
|
||||
const tooBusyMiddleware = function (req: Request, res: Response, next: NextFunction): void {
|
||||
// We dont want to return "toobusy" errors for healthchecks, as that
|
||||
// will cause the process to be restarted
|
||||
if (req.baseUrl === '/_health') {
|
||||
return next()
|
||||
}
|
||||
if (toobusy()) {
|
||||
errors.errorServiceUnavailable(res)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export = tooBusyMiddleware
|
||||
@@ -1,12 +1,13 @@
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../../config')
|
||||
const errors = require('../../errors')
|
||||
const nanoid = require('nanoid')
|
||||
const moment = require('moment')
|
||||
const querystring = require('querystring')
|
||||
import type { Request, Response } from 'express'
|
||||
import models = require('../../models')
|
||||
import logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import * as errors from '../../errors'
|
||||
import nanoid = require('nanoid')
|
||||
import moment = require('moment')
|
||||
import querystring = require('querystring')
|
||||
|
||||
exports.getInfo = function getInfo (req, res, note) {
|
||||
export function getInfo (req: Request, res: Response, note: any): void {
|
||||
const body = note.content
|
||||
const extracted = models.Note.extractMeta(body)
|
||||
const markdown = extracted.markdown
|
||||
@@ -31,7 +32,7 @@ exports.getInfo = function getInfo (req, res, note) {
|
||||
res.send(data)
|
||||
}
|
||||
|
||||
exports.createGist = function createGist (req, res, note) {
|
||||
export function createGist (req: Request, res: Response, note: any): void {
|
||||
const data = {
|
||||
client_id: config.github.clientID,
|
||||
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
|
||||
@@ -42,12 +43,12 @@ exports.createGist = function createGist (req, res, note) {
|
||||
res.redirect('https://github.com/login/oauth/authorize?' + query)
|
||||
}
|
||||
|
||||
exports.getRevision = function getRevision (req, res, note) {
|
||||
const actionId = req.params.actionId
|
||||
export function getRevision (req: Request, res: Response, note: any): void {
|
||||
const actionId = req.params.actionId as string
|
||||
if (actionId) {
|
||||
const time = moment(parseInt(actionId))
|
||||
if (time.isValid()) {
|
||||
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
|
||||
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err: Error | null, content: string | null) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
@@ -68,7 +69,7 @@ exports.getRevision = function getRevision (req, res, note) {
|
||||
return errors.errorNotFound(res)
|
||||
}
|
||||
} else {
|
||||
models.Revision.getNoteRevisions(note, function (err, data) {
|
||||
models.Revision.getNoteRevisions(note, function (err: Error | null, data: unknown) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
@@ -1,15 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../../config')
|
||||
const errors = require('../../errors')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import models = require('../../models')
|
||||
import logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import * as errors from '../../errors'
|
||||
|
||||
const noteUtil = require('./util')
|
||||
const noteActions = require('./actions')
|
||||
import * as noteUtil from './util'
|
||||
import * as noteActions from './actions'
|
||||
|
||||
exports.publishNoteActions = function (req, res, next) {
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
export function publishNoteActions (req: Request, res: Response, next: NextFunction): void {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'download':
|
||||
@@ -25,7 +26,7 @@ exports.publishNoteActions = function (req, res, next) {
|
||||
})
|
||||
}
|
||||
|
||||
exports.showPublishNote = function (req, res, next) {
|
||||
export function showPublishNote (req: Request, res: Response, next: NextFunction): void {
|
||||
const include = [{
|
||||
model: models.User,
|
||||
as: 'owner'
|
||||
@@ -33,31 +34,31 @@ exports.showPublishNote = function (req, res, next) {
|
||||
model: models.User,
|
||||
as: 'lastchangeuser'
|
||||
}]
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
// force to use short id
|
||||
const shortid = req.params.shortid
|
||||
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
|
||||
return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
|
||||
}
|
||||
note.increment('viewcount').then(function (note) {
|
||||
note.increment('viewcount').then(function (note: any) {
|
||||
if (!note) {
|
||||
return errors.errorNotFound(res)
|
||||
}
|
||||
noteUtil.getPublishData(req, res, note, (data) => {
|
||||
noteUtil.getPublishData(req, res, note, (data: Record<string, unknown>) => {
|
||||
res.set({
|
||||
'Cache-Control': 'private' // only cache by client
|
||||
})
|
||||
return res.render('pretty.ejs', data)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}, include)
|
||||
}
|
||||
|
||||
exports.showNote = function (req, res, next) {
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
export function showNote (req: Request, res: Response, next: NextFunction): void {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
// force to use note id
|
||||
const noteId = req.params.noteId
|
||||
const id = models.Note.encodeNoteId(note.id)
|
||||
@@ -81,7 +82,7 @@ exports.showNote = function (req, res, next) {
|
||||
}, null, true)
|
||||
}
|
||||
|
||||
exports.createFromPOST = function (req, res, next) {
|
||||
export function createFromPOST (req: Request, res: Response, next: NextFunction): void {
|
||||
if (config.disableNoteCreation) {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
@@ -92,12 +93,12 @@ exports.createFromPOST = function (req, res, next) {
|
||||
body = req.body
|
||||
}
|
||||
body = body.replace(/[\r]/g, '')
|
||||
return noteUtil.newNote(req, res, body)
|
||||
noteUtil.newNote(req, res, body)
|
||||
}
|
||||
|
||||
exports.doAction = function (req, res, next) {
|
||||
export function doAction (req: Request, res: Response, next: NextFunction): void {
|
||||
const noteId = req.params.noteId
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'publish':
|
||||
@@ -125,7 +126,7 @@ exports.doAction = function (req, res, next) {
|
||||
})
|
||||
}
|
||||
|
||||
exports.downloadMarkdown = function (req, res, note) {
|
||||
export function downloadMarkdown (req: Request, res: Response, note: any): void {
|
||||
const body = note.content
|
||||
let filename = models.Note.decodeTitle(note.title)
|
||||
filename = encodeURIComponent(filename)
|
||||
@@ -1,18 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const { markdownParser } = require('../utils')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import { markdownParser } from '../utils'
|
||||
|
||||
const router = module.exports = Router()
|
||||
const router = Router()
|
||||
|
||||
const noteController = require('./controller')
|
||||
const slide = require('./slide')
|
||||
const rateLimit = require('../middleware/rateLimit')
|
||||
const config = require('../../config')
|
||||
import noteController = require('./controller')
|
||||
import slide = require('./slide')
|
||||
import rateLimit = require('../middleware/rateLimit')
|
||||
import config = require('../../config')
|
||||
|
||||
const applyRateLimitIfConfigured = (req, res, next) => {
|
||||
const applyRateLimitIfConfigured = (req: Request, res: Response, next: NextFunction): void => {
|
||||
if (config.rateLimitNewNotes > 0) {
|
||||
return rateLimit.newNotes(req, res, next)
|
||||
rateLimit.newNotes(req, res, next)
|
||||
return
|
||||
}
|
||||
next()
|
||||
}
|
||||
@@ -37,3 +39,5 @@ router.get('/:noteId', noteController.showNote)
|
||||
router.get('/:noteId/:action', noteController.doAction)
|
||||
// note actions with action id
|
||||
router.get('/:noteId/:action/:actionId', noteController.doAction)
|
||||
|
||||
export = router
|
||||
@@ -1,11 +1,12 @@
|
||||
const noteUtil = require('./util')
|
||||
const models = require('../../models')
|
||||
const errors = require('../../errors')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../../config')
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import * as noteUtil from './util'
|
||||
import models = require('../../models')
|
||||
import * as errors from '../../errors'
|
||||
import logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
|
||||
exports.publishSlideActions = function (req, res, next) {
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
export function publishSlideActions (req: Request, res: Response, next: NextFunction): void {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
const action = req.params.action
|
||||
if (action === 'edit') {
|
||||
res.redirect(config.serverURL + '/' + (note.alias ? note.alias : models.Note.encodeNoteId(note.id)) + '?both')
|
||||
@@ -13,7 +14,7 @@ exports.publishSlideActions = function (req, res, next) {
|
||||
})
|
||||
}
|
||||
|
||||
exports.showPublishSlide = function (req, res, next) {
|
||||
export function showPublishSlide (req: Request, res: Response, next: NextFunction): void {
|
||||
const include = [{
|
||||
model: models.User,
|
||||
as: 'owner'
|
||||
@@ -21,23 +22,23 @@ exports.showPublishSlide = function (req, res, next) {
|
||||
model: models.User,
|
||||
as: 'lastchangeuser'
|
||||
}]
|
||||
noteUtil.findNote(req, res, function (note) {
|
||||
noteUtil.findNote(req, res, function (note: any) {
|
||||
// force to use short id
|
||||
const shortid = req.params.shortid
|
||||
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
|
||||
return res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid))
|
||||
}
|
||||
note.increment('viewcount').then(function (note) {
|
||||
note.increment('viewcount').then(function (note: any) {
|
||||
if (!note) {
|
||||
return errors.errorNotFound(res)
|
||||
}
|
||||
noteUtil.getPublishData(req, res, note, (data) => {
|
||||
noteUtil.getPublishData(req, res, note, (data: Record<string, unknown>) => {
|
||||
res.set({
|
||||
'Cache-Control': 'private' // only cache by client
|
||||
})
|
||||
return res.render('slide.ejs', data)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -1,13 +1,14 @@
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../../config')
|
||||
const errors = require('../../errors')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import type { Request, Response } from 'express'
|
||||
import models = require('../../models')
|
||||
import logger = require('../../logger')
|
||||
import config = require('../../config')
|
||||
import * as errors from '../../errors'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
exports.findNote = function (req, res, callback, include = null, createIfNotFound = false) {
|
||||
export function findNote (req: Request, res: Response, callback: (note: any) => void, include: any[] | null = null, createIfNotFound: boolean = false): void {
|
||||
const id = req.params.noteId || req.params.shortid
|
||||
models.Note.parseNoteId(id, function (err, _id) {
|
||||
models.Note.parseNoteId(id, function (err: Error | null, _id: string) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
@@ -17,7 +18,7 @@ exports.findNote = function (req, res, callback, include = null, createIfNotFoun
|
||||
id: _id
|
||||
},
|
||||
include: include || null
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
if (!note && createIfNotFound) {
|
||||
if (config.disableNoteCreation) {
|
||||
return errors.errorNotFound(res)
|
||||
@@ -33,16 +34,16 @@ exports.findNote = function (req, res, callback, include = null, createIfNotFoun
|
||||
} else {
|
||||
return callback(note)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.checkViewPermission = function (req, note) {
|
||||
export function checkViewPermission (req: Request, note: any): boolean {
|
||||
if (note.permission === 'private') {
|
||||
return !(!req.isAuthenticated() || note.ownerId !== req.user.id)
|
||||
return !(!req.isAuthenticated() || note.ownerId !== (req as any).user.id)
|
||||
} else if (note.permission === 'limited' || note.permission === 'protected') {
|
||||
return req.isAuthenticated()
|
||||
} else {
|
||||
@@ -50,24 +51,24 @@ exports.checkViewPermission = function (req, note) {
|
||||
}
|
||||
}
|
||||
|
||||
exports.newNote = async function (req, res, body) {
|
||||
let owner = null
|
||||
export async function newNote (req: Request, res: Response, body: string): Promise<void> {
|
||||
let owner: string | null = null
|
||||
const noteId = req.params.noteId ? req.params.noteId : null
|
||||
if (req.isAuthenticated()) {
|
||||
owner = req.user.id
|
||||
owner = (req as any).user.id
|
||||
} else if (!config.allowAnonymous) {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
if (noteId) {
|
||||
if (config.allowFreeURL && !config.forbiddenNoteIDs.includes(noteId) && (!config.requireFreeURLAuthentication || req.isAuthenticated())) {
|
||||
req.alias = noteId
|
||||
(req as any).alias = noteId
|
||||
} else {
|
||||
return req.method === 'POST' ? errors.errorForbidden(res) : errors.errorNotFound(res)
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
models.Note.parseNoteId(noteId, (err, id) => {
|
||||
const id = await new Promise<string>((resolve, reject) => {
|
||||
models.Note.parseNoteId(noteId, (err: Error | null, id: string) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
@@ -86,18 +87,18 @@ exports.newNote = async function (req, res, body) {
|
||||
}
|
||||
models.Note.create({
|
||||
ownerId: owner,
|
||||
alias: req.alias ? req.alias : null,
|
||||
alias: (req as any).alias ? (req as any).alias : null,
|
||||
content: body,
|
||||
title: models.Note.parseNoteTitle(body)
|
||||
}).then(function (note) {
|
||||
}).then(function (note: any) {
|
||||
return res.redirect(config.serverURL + '/' + (note.alias ? note.alias : models.Note.encodeNoteId(note.id)))
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('Note could not be created: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
|
||||
exports.getPublishData = function (req, res, note, callback) {
|
||||
export function getPublishData (req: Request, res: Response, note: any, callback: (data: Record<string, unknown>) => void): void {
|
||||
const body = note.content
|
||||
const extracted = models.Note.extractMeta(body)
|
||||
let markdown = extracted.markdown
|
||||
@@ -129,15 +130,15 @@ exports.getPublishData = function (req, res, note, callback) {
|
||||
robots: meta.robots || false, // default allow robots
|
||||
GA: meta.GA,
|
||||
disqus: meta.disqus,
|
||||
cspNonce: res.locals.nonce,
|
||||
cspNonce: (res as any).locals.nonce,
|
||||
dnt: req.headers.dnt,
|
||||
opengraph: ogdata
|
||||
}
|
||||
callback(data)
|
||||
}
|
||||
|
||||
function isRevealTheme (theme) {
|
||||
if (fs.existsSync(path.join(__dirname, '..', '..', '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) {
|
||||
function isRevealTheme (theme: string): string | undefined {
|
||||
if (fs.existsSync(path.join(__dirname, '..', '..', '..', '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) {
|
||||
return theme
|
||||
}
|
||||
return undefined
|
||||
@@ -1,18 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import { Router } from 'express'
|
||||
import * as errors from '../errors'
|
||||
import realtime = require('../realtime')
|
||||
import config = require('../config')
|
||||
import models = require('../models')
|
||||
import logger = require('../logger')
|
||||
import { urlencodedParser } from './utils'
|
||||
|
||||
const errors = require('../errors')
|
||||
const realtime = require('../realtime')
|
||||
const config = require('../config')
|
||||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
const statusRouter = Router()
|
||||
|
||||
const { urlencodedParser } = require('./utils')
|
||||
|
||||
const statusRouter = module.exports = Router()
|
||||
|
||||
statusRouter.get('/_health', function (req, res) {
|
||||
// get status
|
||||
statusRouter.get('/_health', function (_req: Request, res: Response) {
|
||||
res.set({
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
|
||||
@@ -23,11 +23,11 @@ statusRouter.get('/_health', function (req, res) {
|
||||
})
|
||||
|
||||
// get status
|
||||
statusRouter.get('/status', function (req, res, next) {
|
||||
statusRouter.get('/status', function (req: Request, res: Response, _next: NextFunction) {
|
||||
if (!config.enableStatsApi) {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
realtime.getStatus(function (data) {
|
||||
realtime.getStatus(function (data: Record<string, unknown>) {
|
||||
res.set({
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
|
||||
@@ -36,8 +36,8 @@ statusRouter.get('/status', function (req, res, next) {
|
||||
res.send(data)
|
||||
})
|
||||
})
|
||||
// get status
|
||||
statusRouter.get('/temp', function (req, res) {
|
||||
|
||||
statusRouter.get('/temp', function (req: Request, res: Response) {
|
||||
const host = req.get('host')
|
||||
if (config.allowOrigin.indexOf(host) === -1) {
|
||||
errors.errorForbidden(res)
|
||||
@@ -50,7 +50,7 @@ statusRouter.get('/temp', function (req, res) {
|
||||
where: {
|
||||
id: tempid
|
||||
}
|
||||
}).then(function (temp) {
|
||||
}).then(function (temp: any) {
|
||||
if (!temp) {
|
||||
errors.errorNotFound(res)
|
||||
} else {
|
||||
@@ -58,21 +58,22 @@ statusRouter.get('/temp', function (req, res) {
|
||||
res.send({
|
||||
temp: temp.data
|
||||
})
|
||||
temp.destroy().catch(function (err) {
|
||||
temp.destroy().catch(function (err: Error) {
|
||||
if (err) {
|
||||
logger.error('remove temp failed: ' + err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// post status
|
||||
statusRouter.post('/temp', urlencodedParser, function (req, res) {
|
||||
statusRouter.post('/temp', urlencodedParser, function (req: Request, res: Response) {
|
||||
const host = req.get('host')
|
||||
if (config.allowOrigin.indexOf(host) === -1) {
|
||||
errors.errorForbidden(res)
|
||||
@@ -84,7 +85,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
|
||||
logger.debug(`SERVER received temp from [${host}]: ${req.body.data}`)
|
||||
models.Temp.create({
|
||||
data
|
||||
}).then(function (temp) {
|
||||
}).then(function (temp: any) {
|
||||
if (temp) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.send({
|
||||
@@ -94,7 +95,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
|
||||
} else {
|
||||
errors.errorInternalError(res)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error(err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -102,7 +103,7 @@ statusRouter.post('/temp', urlencodedParser, function (req, res) {
|
||||
}
|
||||
})
|
||||
|
||||
statusRouter.get('/config', function (req, res) {
|
||||
statusRouter.get('/config', function (req: Request, res: Response) {
|
||||
const data = {
|
||||
domain: config.domain,
|
||||
urlpath: config.urlPath,
|
||||
@@ -113,7 +114,7 @@ statusRouter.get('/config', function (req, res) {
|
||||
linkifyHeaderStyle: config.linkifyHeaderStyle,
|
||||
cookiePolicy: config.cookiePolicy,
|
||||
enableUploads: config.enableUploads,
|
||||
userToken: req.user ? req.user.deleteToken : ''
|
||||
userToken: (req as any).user ? (req as any).user.deleteToken : ''
|
||||
}
|
||||
res.set({
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
@@ -122,3 +123,5 @@ statusRouter.get('/config', function (req, res) {
|
||||
})
|
||||
res.render('../js/lib/common/constant.ejs', data)
|
||||
})
|
||||
|
||||
export = statusRouter
|
||||
@@ -1,35 +1,35 @@
|
||||
'use strict'
|
||||
|
||||
const archiver = require('archiver')
|
||||
const sanitizeFilename = require('sanitize-filename')
|
||||
const async = require('async')
|
||||
const Router = require('express').Router
|
||||
import type { Request, Response, NextFunction } from 'express'
|
||||
import archiver = require('archiver')
|
||||
import sanitizeFilename = require('sanitize-filename')
|
||||
import async = require('async')
|
||||
import { Router } from 'express'
|
||||
import * as errors from '../errors'
|
||||
import config = require('../config')
|
||||
import models = require('../models')
|
||||
import logger = require('../logger')
|
||||
import { generateAvatar } from '../letter-avatars'
|
||||
|
||||
const errors = require('../errors')
|
||||
const config = require('../config')
|
||||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
const { generateAvatar } = require('../letter-avatars')
|
||||
|
||||
const UserRouter = module.exports = Router()
|
||||
const UserRouter = Router()
|
||||
|
||||
// get me info
|
||||
UserRouter.get('/me', function (req, res) {
|
||||
UserRouter.get('/me', function (req: Request, res: Response) {
|
||||
if (req.isAuthenticated()) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
id: (req as any).user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) { return errors.errorNotFound(res) }
|
||||
const profile = models.User.getProfile(user)
|
||||
res.send({
|
||||
status: 'ok',
|
||||
id: req.user.id,
|
||||
id: (req as any).user.id,
|
||||
name: profile.name,
|
||||
photo: profile.photo
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('read me failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -41,13 +41,13 @@ UserRouter.get('/me', function (req, res) {
|
||||
})
|
||||
|
||||
// delete the currently authenticated user
|
||||
UserRouter.get('/me/delete/:token?', function (req, res) {
|
||||
UserRouter.get('/me/delete/:token?', function (req: Request, res: Response) {
|
||||
if (req.isAuthenticated()) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
id: (req as any).user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
if (!user) {
|
||||
return errors.errorNotFound(res)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
|
||||
} else {
|
||||
return errors.errorForbidden(res)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('delete user failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -68,7 +68,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
|
||||
})
|
||||
|
||||
// export the data of the authenticated user
|
||||
UserRouter.get('/me/export', function (req, res) {
|
||||
UserRouter.get('/me/export', function (req: Request, res: Response) {
|
||||
if (req.isAuthenticated()) {
|
||||
// let output = fs.createWriteStream(__dirname + '/example.zip');
|
||||
const archive = archiver('zip', {
|
||||
@@ -77,44 +77,43 @@ UserRouter.get('/me/export', function (req, res) {
|
||||
res.setHeader('Content-Type', 'application/zip')
|
||||
res.attachment('archive.zip')
|
||||
archive.pipe(res)
|
||||
archive.on('error', function (err) {
|
||||
archive.on('error', function (err: Error) {
|
||||
logger.error('export user data failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
models.User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
id: (req as any).user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
}).then(function (user: any) {
|
||||
models.Note.findAll({
|
||||
where: {
|
||||
ownerId: user.id
|
||||
}
|
||||
}).then(function (notes) {
|
||||
const filenames = {}
|
||||
async.each(notes, function (note, callback) {
|
||||
const basename = sanitizeFilename(note.title, { replacement: '_' })
|
||||
let filename
|
||||
let suffix = ''
|
||||
}).then(function (notes: any[]) {
|
||||
const filenames: Record<string, boolean> = {}
|
||||
async.each(notes, function (note: any, callback: async.ErrorCallback) {
|
||||
const basename: string = sanitizeFilename(note.title, { replacement: '_' })
|
||||
let filename: string
|
||||
let suffix: number | string = ''
|
||||
do {
|
||||
const seperator = typeof suffix === 'number' ? '-' : ''
|
||||
filename = basename + seperator + suffix + '.md'
|
||||
suffix++
|
||||
suffix = typeof suffix === 'number' ? suffix + 1 : 0
|
||||
} while (filenames[filename])
|
||||
filenames[filename] = true
|
||||
|
||||
logger.debug('Write: ' + filename)
|
||||
archive.append(Buffer.from(note.content), { name: filename, date: note.lastchangeAt })
|
||||
callback(null, null)
|
||||
}, function (err) {
|
||||
callback(null)
|
||||
}, function (err: Error | null | undefined) {
|
||||
if (err) {
|
||||
return errors.errorInternalError(res)
|
||||
}
|
||||
|
||||
archive.finalize()
|
||||
})
|
||||
})
|
||||
}).catch(function (err) {
|
||||
}).catch(function (err: Error) {
|
||||
logger.error('export user data failed: ' + err)
|
||||
return errors.errorInternalError(res)
|
||||
})
|
||||
@@ -123,8 +122,10 @@ UserRouter.get('/me/export', function (req, res) {
|
||||
}
|
||||
})
|
||||
|
||||
UserRouter.get('/user/:username/avatar.svg', function (req, res, next) {
|
||||
UserRouter.get('/user/:username/avatar.svg', function (req: Request, res: Response, _next: NextFunction) {
|
||||
res.setHeader('Content-Type', 'image/svg+xml')
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||
res.send(generateAvatar(req.params.username))
|
||||
res.send(generateAvatar(req.params.username as string))
|
||||
})
|
||||
|
||||
export = UserRouter
|
||||
@@ -1,15 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const bodyParser = require('body-parser')
|
||||
import bodyParser = require('body-parser')
|
||||
|
||||
// create application/x-www-form-urlencoded parser
|
||||
exports.urlencodedParser = bodyParser.urlencoded({
|
||||
export const urlencodedParser = bodyParser.urlencoded({
|
||||
extended: false,
|
||||
limit: 1024 * 1024 * 10 // 10 mb
|
||||
})
|
||||
|
||||
// create text/markdown parser
|
||||
exports.markdownParser = bodyParser.text({
|
||||
export const markdownParser = bodyParser.text({
|
||||
inflate: true,
|
||||
type: ['text/plain', 'text/markdown'],
|
||||
limit: 1024 * 1024 * 10 // 10 mb
|
||||
@@ -1,12 +1,22 @@
|
||||
'use strict'
|
||||
// external modules
|
||||
const DiffMatchPatch = require('diff-match-patch')
|
||||
import DiffMatchPatch = require('diff-match-patch')
|
||||
const dmp = new DiffMatchPatch()
|
||||
|
||||
// core
|
||||
const logger = require('../logger')
|
||||
import logger = require('../logger')
|
||||
|
||||
process.on('message', function (data) {
|
||||
interface DmpWorkerMessage {
|
||||
msg: string
|
||||
cacheKey: string
|
||||
lastDoc?: string
|
||||
currDoc?: string
|
||||
revisions?: any[]
|
||||
count?: number
|
||||
}
|
||||
|
||||
process.on('message', function (raw: DmpWorkerMessage) {
|
||||
const data = raw
|
||||
if (!data || !data.msg || !data.cacheKey) {
|
||||
return logger.error('dmp worker error: not enough data')
|
||||
}
|
||||
@@ -21,15 +31,15 @@ process.on('message', function (data) {
|
||||
)
|
||||
}
|
||||
try {
|
||||
const patch = createPatch(data.lastDoc, data.currDoc)
|
||||
process.send({
|
||||
const patch = createPatch(data.lastDoc!, data.currDoc!)
|
||||
process.send!({
|
||||
msg: 'check',
|
||||
result: patch,
|
||||
cacheKey: data.cacheKey
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('dmp worker error', err)
|
||||
process.send({
|
||||
process.send!({
|
||||
msg: 'error',
|
||||
error: err,
|
||||
cacheKey: data.cacheKey
|
||||
@@ -46,15 +56,15 @@ process.on('message', function (data) {
|
||||
)
|
||||
}
|
||||
try {
|
||||
const result = getRevision(data.revisions, data.count)
|
||||
process.send({
|
||||
const result = getRevision(data.revisions!, data.count!)
|
||||
process.send!({
|
||||
msg: 'check',
|
||||
result,
|
||||
cacheKey: data.cacheKey
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('dmp worker error', err)
|
||||
process.send({
|
||||
process.send!({
|
||||
msg: 'error',
|
||||
error: err,
|
||||
cacheKey: data.cacheKey
|
||||
@@ -64,36 +74,48 @@ process.on('message', function (data) {
|
||||
}
|
||||
})
|
||||
|
||||
function createPatch (lastDoc, currDoc) {
|
||||
function createPatch (lastDoc: string, currDoc: string): string {
|
||||
const msStart = new Date().getTime()
|
||||
const diff = dmp.diff_main(lastDoc, currDoc)
|
||||
let patch = dmp.patch_make(lastDoc, diff)
|
||||
patch = dmp.patch_toText(patch)
|
||||
const patch = dmp.patch_toText(dmp.patch_make(lastDoc, diff))
|
||||
const msEnd = new Date().getTime()
|
||||
logger.debug(patch)
|
||||
logger.debug(msEnd - msStart + 'ms')
|
||||
return patch
|
||||
}
|
||||
|
||||
function getRevision (revisions, count) {
|
||||
interface RevisionEntry {
|
||||
content?: string
|
||||
lastContent?: string
|
||||
patch?: string
|
||||
authorship?: unknown[]
|
||||
}
|
||||
|
||||
interface RevisionResult {
|
||||
content: string
|
||||
patch: any[]
|
||||
authorship: unknown[]
|
||||
}
|
||||
|
||||
function getRevision (revisions: RevisionEntry[], count: number): RevisionResult {
|
||||
const msStart = new Date().getTime()
|
||||
let startContent = null
|
||||
let lastPatch = []
|
||||
let applyPatches = []
|
||||
let authorship = []
|
||||
let startContent: string | null = null
|
||||
let lastPatch = ''
|
||||
let applyPatches: any[] = []
|
||||
let authorship: unknown[] = []
|
||||
if (count <= Math.round(revisions.length / 2)) {
|
||||
// start from top to target
|
||||
for (let i = 0; i < count; i++) {
|
||||
const revision = revisions[i]
|
||||
if (i === 0) {
|
||||
startContent = revision.content || revision.lastContent
|
||||
startContent = revision.content || revision.lastContent || null
|
||||
}
|
||||
if (i !== count - 1) {
|
||||
const patch = dmp.patch_fromText(revision.patch)
|
||||
const patch = dmp.patch_fromText(revision.patch!)
|
||||
applyPatches = applyPatches.concat(patch)
|
||||
}
|
||||
lastPatch = revision.patch
|
||||
authorship = revision.authorship
|
||||
lastPatch = revision.patch || ''
|
||||
authorship = revision.authorship || []
|
||||
}
|
||||
// swap DIFF_INSERT and DIFF_DELETE to achieve unpatching
|
||||
for (let i = 0, l = applyPatches.length; i < l; i++) {
|
||||
@@ -112,19 +134,19 @@ function getRevision (revisions, count) {
|
||||
for (let i = l; i >= count - 1; i--) {
|
||||
const revision = revisions[i]
|
||||
if (i === l) {
|
||||
startContent = revision.lastContent
|
||||
authorship = revision.authorship
|
||||
startContent = revision.lastContent || null
|
||||
authorship = revision.authorship || []
|
||||
}
|
||||
if (revision.patch) {
|
||||
const patch = dmp.patch_fromText(revision.patch)
|
||||
applyPatches = applyPatches.concat(patch)
|
||||
}
|
||||
lastPatch = revision.patch
|
||||
authorship = revision.authorship
|
||||
lastPatch = revision.patch || ''
|
||||
authorship = revision.authorship || []
|
||||
}
|
||||
}
|
||||
try {
|
||||
const finalContent = dmp.patch_apply(applyPatches, startContent)[0]
|
||||
const finalContent = dmp.patch_apply(applyPatches, startContent as string)[0]
|
||||
const data = {
|
||||
content: finalContent,
|
||||
patch: dmp.patch_fromText(lastPatch),
|
||||
@@ -134,12 +156,12 @@ function getRevision (revisions, count) {
|
||||
logger.debug(msEnd - msStart + 'ms')
|
||||
return data
|
||||
} catch (err) {
|
||||
throw new Error(err)
|
||||
throw new Error(err as string)
|
||||
}
|
||||
}
|
||||
|
||||
// log uncaught exception
|
||||
process.on('uncaughtException', function (err) {
|
||||
process.on('uncaughtException', function (err: Error) {
|
||||
logger.error('An uncaught exception has occured.')
|
||||
logger.error(err)
|
||||
logger.error('Process will exit now.')
|
||||
+33
-1
@@ -14,7 +14,8 @@
|
||||
"dev": "webpack --config webpack.dev.js --progress --watch",
|
||||
"heroku-prebuild": "bin/heroku",
|
||||
"build": "webpack --config webpack.prod.js --progress",
|
||||
"start": "node app.js"
|
||||
"build:server": "tsgo -p tsconfig.json",
|
||||
"start": "node dist/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hedgedoc/meta-marked": "14.1.0",
|
||||
@@ -138,6 +139,37 @@
|
||||
"@eslint/eslintrc": "3.3.3",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@hedgedoc/codemirror-5": "5.65.12",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/async": "^3.2.25",
|
||||
"@types/chance": "^1.1.7",
|
||||
"@types/cheerio": "^1.0.0",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/connect-flash": "^0.0.40",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/deep-freeze": "^0.1.5",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/helmet": "^4.0.0",
|
||||
"@types/i18n": "^0.13.12",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/method-override": "^3.0.0",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/passport-facebook": "^3.0.4",
|
||||
"@types/passport-github": "^1.1.13",
|
||||
"@types/passport-google-oauth20": "^2.0.17",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/passport-oauth2": "^1.8.0",
|
||||
"@types/sanitize-filename": "^1.6.3",
|
||||
"@types/toobusy-js": "^0.5.4",
|
||||
"@types/umzug": "^3.0.0",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260217.1",
|
||||
"abcjs": "6.6.0",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "6.26.3",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"declaration": false
|
||||
},
|
||||
"include": ["lib/**/*", "app.ts"],
|
||||
"exclude": ["node_modules", "dist", "lib/migrations"]
|
||||
}
|
||||
Reference in New Issue
Block a user