refactor: project cleanup + onboarding page

This commit is contained in:
NGPixel
2018-02-04 00:53:13 -05:00
parent 1d9f057f38
commit 3471a7a6f9
80 changed files with 177 additions and 2782 deletions

View File

@@ -1,109 +0,0 @@
/* global wiki */
const _ = require('lodash')
const passport = require('passport')
const fs = require('fs-extra')
const path = require('path')
module.exports = {
strategies: {},
init() {
this.passport = passport
// Serialization user methods
passport.serializeUser(function (user, done) {
done(null, user.id)
})
passport.deserializeUser(function (id, done) {
wiki.db.User.findById(id).then((user) => {
if (user) {
done(null, user)
} else {
done(new Error(wiki.lang.t('auth:errors:usernotfound')), null)
}
return true
}).catch((err) => {
done(err, null)
})
})
// Load authentication strategies
_.forOwn(_.omitBy(wiki.config.auth.strategies, s => s.enabled === false), (strategyConfig, strategyKey) => {
strategyConfig.callbackURL = `${wiki.config.site.host}${wiki.config.site.path}login/${strategyKey}/callback`
let strategy = require(`../extensions/authentication/${strategyKey}`)
try {
strategy.init(passport, strategyConfig)
} catch (err) {
wiki.logger.error(`Authentication Provider ${strategyKey}: [ FAILED ]`)
wiki.logger.error(err)
}
fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${strategyKey}.svg`), 'utf8').then(iconData => {
strategy.icon = iconData
}).catch(err => {
if (err.code === 'ENOENT') {
strategy.icon = '[missing icon]'
} else {
wiki.logger.error(err)
}
})
this.strategies[strategy.key] = strategy
wiki.logger.info(`Authentication Provider ${strategyKey}: [ OK ]`)
})
// Create Guest account for first-time
wiki.db.User.findOne({
where: {
provider: 'local',
email: 'guest@example.com'
}
}).then((c) => {
if (c < 1) {
return wiki.db.User.create({
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
role: 'guest'
}).then(() => {
wiki.logger.info('[AUTH] Guest account created successfully!')
return true
}).catch((err) => {
wiki.logger.error('[AUTH] An error occured while creating guest account:')
wiki.logger.error(err)
return err
})
}
})
// .then(() => {
// if (process.env.WIKI_JS_HEROKU) {
// return wiki.db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
// if (c < 1) {
// // Create root admin account (HEROKU ONLY)
// return wiki.db.User.create({
// provider: 'local',
// email: process.env.WIKI_ADMIN_EMAIL,
// name: 'Administrator',
// password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
// role: 'admin'
// }).then(() => {
// wiki.logger.info('[AUTH] Root admin account created successfully!')
// return true
// }).catch((err) => {
// wiki.logger.error('[AUTH] An error occured while creating root admin account:')
// wiki.logger.error(err)
// return err
// })
// } else { return true }
// })
// } else { return true }
// })
return this
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// Auth0 Account
// ------------------------------------
const Auth0Strategy = require('passport-auth0').Strategy
module.exports = {
key: 'auth0',
title: 'Auth0',
useForm: false,
props: ['domain', 'clientId', 'clientSecret'],
init (passport, conf) {
passport.use('auth0',
new Auth0Strategy({
domain: conf.domain,
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,35 @@
/* global wiki */
// ------------------------------------
// Azure AD Account
// ------------------------------------
const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
module.exports = {
key: 'azure',
title: 'Azure Active Directory',
useForm: false,
props: ['clientId', 'clientSecret', 'resource', 'tenant'],
init (passport, conf) {
const jwt = require('jsonwebtoken')
passport.use('azure_ad_oauth2',
new AzureAdOAuth2Strategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
resource: conf.resource,
tenant: conf.tenant
}, (accessToken, refreshToken, params, profile, cb) => {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
wiki.db.User.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// Discord Account
// ------------------------------------
const DiscordStrategy = require('passport-discord').Strategy
module.exports = {
key: 'discord',
title: 'Discord',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('discord',
new DiscordStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: 'identify email'
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// Dropbox Account
// ------------------------------------
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
module.exports = {
key: 'dropbox',
title: 'Dropbox',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('dropbox',
new DropboxStrategy({
apiVersion: '2',
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
})
)
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// Facebook Account
// ------------------------------------
const FacebookStrategy = require('passport-facebook').Strategy
module.exports = {
key: 'facebook',
title: 'Facebook',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('facebook',
new FacebookStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// GitHub Account
// ------------------------------------
const GitHubStrategy = require('passport-github2').Strategy
module.exports = {
key: 'github',
title: 'GitHub',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('github',
new GitHubStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,29 @@
/* global wiki */
// ------------------------------------
// Google ID Account
// ------------------------------------
const GoogleStrategy = require('passport-google-oauth20').Strategy
module.exports = {
key: 'google',
title: 'Google ID',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('google',
new GoogleStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
})
)
}
}

View File

@@ -0,0 +1,44 @@
/* global wiki */
// ------------------------------------
// LDAP Account
// ------------------------------------
const LdapStrategy = require('passport-ldapauth').Strategy
const fs = require('fs')
module.exports = {
key: 'ldap',
title: 'LDAP / Active Directory',
useForm: true,
props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'],
init (passport, conf) {
passport.use('ldapauth',
new LdapStrategy({
server: {
url: conf.url,
bindDn: conf.bindDn,
bindCredentials: conf.bindCredentials,
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
searchAttributes: ['displayName', 'name', 'cn', 'mail'],
tlsOptions: (conf.tlsEnabled) ? {
ca: [
fs.readFileSync(conf.tlsCertPath)
]
} : {}
},
usernameField: 'email',
passReqToCallback: false
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,41 @@
/* global wiki */
// ------------------------------------
// Local Account
// ------------------------------------
const LocalStrategy = require('passport-local').Strategy
module.exports = {
key: 'local',
title: 'Local',
useForm: true,
props: [],
init (passport, conf) {
passport.use('local',
new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
wiki.db.User.findOne({
where: {
email: uEmail,
provider: 'local'
}
}).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)
})
} else {
return done(new wiki.Error.AuthLoginFailed(), null)
}
}).catch((err) => {
done(err, null)
})
}
))
}
}

View File

@@ -0,0 +1,29 @@
/* global wiki */
// ------------------------------------
// Microsoft Account
// ------------------------------------
const WindowsLiveStrategy = require('passport-windowslive').Strategy
module.exports = {
key: 'microsoft',
title: 'Microsoft Account',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('microsoft',
new WindowsLiveStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,31 @@
/* global wiki */
// ------------------------------------
// OAuth2 Account
// ------------------------------------
const OAuth2Strategy = require('passport-oauth2').Strategy
module.exports = {
key: 'oauth2',
title: 'OAuth2',
useForm: false,
props: ['clientId', 'clientSecret', 'authorizationURL', 'tokenURL'],
init (passport, conf) {
passport.use('oauth2',
new OAuth2Strategy({
authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL,
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
})
)
}
}

View File

@@ -0,0 +1,29 @@
/* global wiki */
// ------------------------------------
// Slack Account
// ------------------------------------
const SlackStrategy = require('passport-slack').Strategy
module.exports = {
key: 'slack',
title: 'Slack',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('slack',
new SlackStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -0,0 +1,30 @@
/* global wiki */
// ------------------------------------
// Twitch Account
// ------------------------------------
const TwitchStrategy = require('passport-twitch').Strategy
module.exports = {
key: 'twitch',
title: 'Twitch',
useForm: false,
props: ['clientId', 'clientSecret'],
init (passport, conf) {
passport.use('twitch',
new TwitchStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: 'user_read'
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -1,105 +0,0 @@
/* global wiki */
const fs = require('fs')
const yaml = require('js-yaml')
const _ = require('lodash')
const path = require('path')
const cfgHelper = require('../helpers/config')
module.exports = {
/**
* Load root config from disk
*/
init() {
let confPaths = {
config: path.join(wiki.ROOTPATH, 'config.yml'),
data: path.join(wiki.SERVERPATH, 'app/data.yml'),
dataRegex: path.join(wiki.SERVERPATH, 'app/regex.js')
}
let appconfig = {}
let appdata = {}
try {
appconfig = yaml.safeLoad(
cfgHelper.parseConfigValue(
fs.readFileSync(confPaths.config, 'utf8')
)
)
appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))
appdata.regex = require(confPaths.dataRegex)
} catch (ex) {
console.error(ex)
process.exit(1)
}
// Merge with defaults
appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
if (appconfig.port < 1) {
appconfig.port = process.env.PORT || 80
}
appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
wiki.config = appconfig
wiki.data = appdata
wiki.version = require(path.join(wiki.ROOTPATH, 'package.json')).version
},
/**
* Load config from DB
*
* @param {Array} subsets Array of subsets to load
* @returns Promise
*/
async loadFromDb(subsets) {
if (!_.isArray(subsets) || subsets.length === 0) {
subsets = wiki.data.configNamespaces
}
let results = await wiki.db.Setting.findAll({
attributes: ['key', 'config'],
where: {
key: {
$in: subsets
}
}
})
if (_.isArray(results) && results.length === subsets.length) {
results.forEach(result => {
wiki.config[result.key] = result.config
})
return true
} else {
wiki.logger.warn('DB Configuration is empty or incomplete.')
return false
}
},
/**
* Save config to DB
*
* @param {Array} subsets Array of subsets to save
* @returns Promise
*/
async saveToDb(subsets) {
if (!_.isArray(subsets) || subsets.length === 0) {
subsets = wiki.data.configNamespaces
}
try {
for (let set of subsets) {
await wiki.db.Setting.upsert({
key: set,
config: _.get(wiki.config, set, {})
})
}
} catch (err) {
wiki.logger.error(`Failed to save configuration to DB: ${err.message}`)
return false
}
return true
}
}

View File

@@ -1,136 +0,0 @@
/* global wiki */
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const Promise = require('bluebird')
const Sequelize = require('sequelize')
const Op = Sequelize.Op
const operatorsAliases = {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$not: Op.not,
$in: Op.in,
$notIn: Op.notIn,
$is: Op.is,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$regexp: Op.regexp,
$notRegexp: Op.notRegexp,
$iRegexp: Op.iRegexp,
$notIRegexp: Op.notIRegexp,
$between: Op.between,
$notBetween: Op.notBetween,
$overlap: Op.overlap,
$contains: Op.contains,
$contained: Op.contained,
$adjacent: Op.adjacent,
$strictLeft: Op.strictLeft,
$strictRight: Op.strictRight,
$noExtendRight: Op.noExtendRight,
$noExtendLeft: Op.noExtendLeft,
$and: Op.and,
$or: Op.or,
$any: Op.any,
$all: Op.all,
$values: Op.values,
$col: Op.col
}
/**
* PostgreSQL DB module
*/
module.exports = {
Sequelize,
Op: Sequelize.Op,
/**
* Initialize DB
*
* @return {Object} DB instance
*/
init() {
let self = this
let dbModelsPath = path.join(wiki.SERVERPATH, 'models')
// Define Sequelize instance
self.inst = new self.Sequelize(wiki.config.db.db, wiki.config.db.user, wiki.config.db.pass, {
host: wiki.config.db.host,
port: wiki.config.db.port,
dialect: 'postgres',
pool: {
max: 10,
min: 0,
idle: 10000
},
logging: log => { wiki.logger.log('debug', log) },
operatorsAliases
})
// Attempt to connect and authenticate to DB
self.inst.authenticate().then(() => {
wiki.logger.info('Database (PostgreSQL) connection: [ OK ]')
}).catch(err => {
wiki.logger.error('Failed to connect to PostgreSQL instance.')
wiki.logger.error(err)
process.exit(1)
})
// Load DB Models
fs
.readdirSync(dbModelsPath)
.filter(file => {
return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0)
})
.forEach(file => {
let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
self[modelName] = self.inst.import(path.join(dbModelsPath, file))
})
// Associate DB Models
require(path.join(dbModelsPath, '_relations.js'))(self)
// Set init tasks
let initTasks = {
// -> Sync DB Schemas
syncSchemas() {
return self.inst.sync({
force: false,
logging: log => { wiki.logger.log('debug', log) }
})
},
// -> Set Connection App Name
setAppName() {
return self.inst.query(`set application_name = 'Wiki.js'`, { raw: true })
}
}
let initTasksQueue = (wiki.IS_MASTER) ? [
initTasks.syncSchemas,
initTasks.setAppName
] : [
initTasks.setAppName
]
// Perform init tasks
self.onReady = Promise.each(initTasksQueue, t => t()).return(true)
return self
}
}

View File

@@ -1,169 +0,0 @@
'use strict'
/* global wiki */
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const multer = require('multer')
const os = require('os')
const _ = require('lodash')
/**
* Local Disk Storage
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
uploadImgHandler: null,
/**
* Initialize Local Data Storage model
*/
init () {
this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
this.createBaseDirectories()
// this.initMulter()
return this
},
/**
* Init Multer upload handlers
*/
initMulter () {
let maxFileSizes = {
// img: wiki.config.uploads.maxImageFileSize * 1024 * 1024,
// file: wiki.config.uploads.maxOtherFileSize * 1024 * 1024
img: 3 * 1024 * 1024,
file: 10 * 1024 * 1024
}
// -> IMAGES
this.uploadImgHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
// -> Check filesize
if (f.size > maxFileSizes.img) {
return cb(null, false)
}
// -> Check MIME type (quick check only)
if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
return cb(null, false)
}
cb(null, true)
}
}).array('imgfile', 20)
// -> FILES
this.uploadFileHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
// -> Check filesize
if (f.size > maxFileSizes.file) {
return cb(null, false)
}
cb(null, true)
}
}).array('binfile', 20)
return true
},
/**
* Creates a base directories (Synchronous).
*/
createBaseDirectories () {
try {
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
fs.emptyDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './cache'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './thumbs'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'))
if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'), '755')
}
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'))
if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'), '755')
}
} catch (err) {
wiki.logger.error(err)
}
wiki.logger.info('Disk Data Paths: [ OK ]')
},
/**
* Gets the uploads path.
*
* @return {String} The uploads path.
*/
getUploadsPath () {
return this._uploadsPath
},
/**
* Gets the thumbnails folder path.
*
* @return {String} The thumbs path.
*/
getThumbsPath () {
return this._uploadsThumbsPath
},
/**
* Check if filename is valid and unique
*
* @param {String} f The filename
* @param {String} fld The containing folder
* @param {boolean} isImage Indicates if image
* @return {Promise<String>} Promise of the accepted filename
*/
validateUploadsFilename (f, fld, isImage) {
let fObj = path.parse(f)
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']', 'g'), '')
let fext = _.toLower(fObj.ext)
if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fext = '.png'
}
f = fname + fext
let fpath = path.resolve(this._uploadsPath, fld, f)
return fs.statAsync(fpath).then((s) => {
throw new Error(wiki.lang.t('errors:fileexists', { path: f }))
}).catch((err) => {
if (err.code === 'ENOENT') {
return f
}
throw err
})
}
}

View File

@@ -1,455 +0,0 @@
/* global wiki */
const Promise = require('bluebird')
const path = require('path')
const fs = Promise.promisifyAll(require('fs-extra'))
const _ = require('lodash')
const entryHelper = require('../helpers/entry')
/**
* Documents Model
*/
module.exports = {
_repoPath: 'repo',
_cachePath: 'data/cache',
/**
* Initialize Entries model
*
* @return {Object} Entries model instance
*/
init() {
let self = this
self._repoPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo)
self._cachePath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'cache')
wiki.data.repoPath = self._repoPath
wiki.data.cachePath = self._cachePath
return self
},
/**
* Check if a document already exists
*
* @param {String} entryPath The entry path
* @return {Promise<Boolean>} True if exists, false otherwise
*/
exists(entryPath) {
let self = this
return self.fetchOriginal(entryPath, {
parseMarkdown: false,
parseMeta: false,
parseTree: false,
includeMarkdown: false,
includeParentInfo: false,
cache: false
}).then(() => {
return true
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
})
},
/**
* Fetch a document from cache, otherwise the original
*
* @param {String} entryPath The entry path
* @return {Promise<Object>} Page Data
*/
fetch(entryPath) {
let self = this
let cpath = entryHelper.getCachePath(entryPath)
return fs.statAsync(cpath).then((st) => {
return st.isFile()
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
}).then((isCache) => {
if (isCache) {
// Load from cache
return fs.readFileAsync(cpath).then((contents) => {
return JSON.parse(contents)
}).catch((err) => { // eslint-disable-line handle-callback-err
wiki.logger.error('Corrupted cache file. Deleting it...')
fs.unlinkSync(cpath)
return false
})
} else {
// Load original
return self.fetchOriginal(entryPath)
}
})
},
/**
* Fetches the original document entry
*
* @param {String} entryPath The entry path
* @param {Object} options The options
* @return {Promise<Object>} Page data
*/
fetchOriginal(entryPath, options) {
let self = this
let fpath = entryHelper.getFullPath(entryPath)
let cpath = entryHelper.getCachePath(entryPath)
options = _.defaults(options, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: false,
includeParentInfo: true,
cache: true
})
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let htmlProcessor = (options.parseMarkdown) ? wiki.mark.parseContent(contents) : Promise.resolve('')
// Parse contents
return htmlProcessor.then(html => {
let pageData = {
markdown: (options.includeMarkdown) ? contents : '',
html,
meta: (options.parseMeta) ? wiki.mark.parseMeta(contents) : {},
tree: (options.parseTree) ? wiki.mark.parseTree(contents) : []
}
if (!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath)
}
pageData.meta.path = entryPath
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData)
}).catch((err) => { // eslint-disable-line handle-callback-err
return (pageData.parent = false)
}) : Promise.resolve(true)
return parentPromise.then(() => {
// Cache to disk
if (options.cache) {
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
wiki.logger.error('Unable to write to cache! Performance may be affected.')
wiki.logger.error(err)
return true
})
} else {
return true
}
}).return(pageData)
})
})
} else {
return false
}
}).catch((err) => { // eslint-disable-line handle-callback-err
throw new Promise.OperationalError(wiki.lang.t('errors:notexist', { path: entryPath }))
})
},
/**
* Gets the parent information.
*
* @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information.
*/
getParentInfo(entryPath) {
if (_.includes(entryPath, '/')) {
let parentParts = _.initial(_.split(entryPath, '/'))
let parentPath = _.join(parentParts, '/')
let parentFile = _.last(parentParts)
let fpath = entryHelper.getFullPath(parentPath)
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageMeta = wiki.mark.parseMeta(contents)
return {
path: parentPath,
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
}
})
} else {
return Promise.reject(new Error(wiki.lang.t('errors:parentinvalid')))
}
})
} else {
return Promise.reject(new Error(wiki.lang.t('errors:parentisroot')))
}
},
/**
* Update an existing document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure
*/
update(entryPath, contents, author) {
let self = this
let fpath = entryHelper.getFullPath(entryPath)
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return self.makePersistent(entryPath, contents, author).then(() => {
return self.updateCache(entryPath).then(entry => {
return wiki.search.add(entry)
})
})
} else {
return Promise.reject(new Error(wiki.lang.t('errors:notexist', { path: entryPath })))
}
}).catch((err) => {
wiki.logger.error(err)
return Promise.reject(new Error(wiki.lang.t('errors:savefailed')))
})
},
/**
* Update local cache
*
* @param {String} entryPath The entry path
* @return {Promise} Promise of the operation
*/
updateCache(entryPath) {
let self = this
return self.fetchOriginal(entryPath, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: true,
includeParentInfo: true,
cache: true
}).catch(err => {
wiki.logger.error(err)
return err
}).then((pageData) => {
return {
entryPath,
meta: pageData.meta,
parent: pageData.parent || {},
text: wiki.mark.removeMarkdown(pageData.markdown)
}
}).catch(err => {
wiki.logger.error(err)
return err
}).then((content) => {
let parentPath = _.chain(content.entryPath).split('/').initial().join('/').value()
return wiki.db.Entry.findOneAndUpdate({
_id: content.entryPath
}, {
_id: content.entryPath,
title: content.meta.title || content.entryPath,
subtitle: content.meta.subtitle || '',
parentTitle: content.parent.title || '',
parentPath: parentPath,
isDirectory: false,
isEntry: true
}, {
new: true,
upsert: true
}).then(result => {
let plainResult = result.toObject()
plainResult.text = content.text
return plainResult
})
}).then(result => {
return self.updateTreeInfo().then(() => {
return result
})
}).catch(err => {
wiki.logger.error(err)
return err
})
},
/**
* Update tree info for all directory and parent entries
*
* @returns {Promise<Boolean>} Promise of the operation
*/
updateTreeInfo() {
return wiki.db.Entry.distinct('parentPath', { parentPath: { $ne: '' } }).then(allPaths => {
if (allPaths.length > 0) {
return Promise.map(allPaths, pathItem => {
let parentPath = _.chain(pathItem).split('/').initial().join('/').value()
let guessedTitle = _.chain(pathItem).split('/').last().startCase().value()
return wiki.db.Entry.update({ _id: pathItem }, {
$set: { isDirectory: true },
$setOnInsert: { isEntry: false, title: guessedTitle, parentPath }
}, { upsert: true })
})
} else {
return true
}
})
},
/**
* Create a new document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure
*/
create(entryPath, contents, author) {
let self = this
return self.exists(entryPath).then((docExists) => {
if (!docExists) {
return self.makePersistent(entryPath, contents, author).then(() => {
return self.updateCache(entryPath).then(entry => {
return wiki.search.add(entry)
})
})
} else {
return Promise.reject(new Error(wiki.lang.t('errors:alreadyexists')))
}
}).catch((err) => {
wiki.logger.error(err)
return Promise.reject(new Error(wiki.lang.t('errors:generic')))
})
},
/**
* Makes a document persistent to disk and git repository
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure
*/
makePersistent(entryPath, contents, author) {
let fpath = entryHelper.getFullPath(entryPath)
return fs.outputFileAsync(fpath, contents).then(() => {
return wiki.git.commitDocument(entryPath, author)
})
},
/**
* Move a document
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @param {Object} author The author user object
* @return {Promise} Promise of the operation
*/
move(entryPath, newEntryPath, author) {
let self = this
if (_.isEmpty(entryPath) || entryPath === 'home') {
return Promise.reject(new Error(wiki.lang.t('errors:invalidpath')))
}
return wiki.git.moveDocument(entryPath, newEntryPath).then(() => {
return wiki.git.commitDocument(newEntryPath, author).then(() => {
// Delete old cache version
let oldEntryCachePath = entryHelper.getCachePath(entryPath)
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
// Delete old index entry
wiki.search.delete(entryPath)
// Create cache for new entry
return Promise.join(
wiki.db.Entry.deleteOne({ _id: entryPath }),
self.updateCache(newEntryPath).then(entry => {
return wiki.search.add(entry)
})
)
})
})
},
/**
* Delete a document
*
* @param {String} entryPath The current entry path
* @param {Object} author The author user object
* @return {Promise} Promise of the operation
*/
remove(entryPath, author) {
if (_.isEmpty(entryPath) || entryPath === 'home') {
return Promise.reject(new Error(wiki.lang.t('errors:invalidpath')))
}
return wiki.git.deleteDocument(entryPath, author).then(() => {
// Delete old cache version
let oldEntryCachePath = entryHelper.getCachePath(entryPath)
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
// Delete old index entry
wiki.search.delete(entryPath)
// Delete entry
return wiki.db.Entry.deleteOne({ _id: entryPath })
})
},
/**
* Generate a starter page content based on the entry path
*
* @param {String} entryPath The entry path
* @return {Promise<String>} Starter content
*/
getStarter(entryPath) {
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
return fs.readFileAsync(path.join(wiki.SERVERPATH, 'app/content/create.md'), 'utf8').then((contents) => {
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)
})
},
/**
* Get all entries from base path
*
* @param {String} basePath Path to list from
* @param {Object} usr Current user
* @return {Promise<Array>} List of entries
*/
getFromTree(basePath, usr) {
return wiki.db.Entry.find({ parentPath: basePath }, 'title parentPath isDirectory isEntry').sort({ title: 'asc' }).then(results => {
return _.filter(results, r => {
return wiki.rights.checkRole('/' + r._id, usr.rights, 'read')
})
})
},
getHistory(entryPath) {
return wiki.db.Entry.findOne({ _id: entryPath, isEntry: true }).then(entry => {
if (!entry) { return false }
return wiki.git.getHistory(entryPath).then(history => {
return {
meta: entry,
history
}
})
})
}
}

View File

@@ -1,306 +0,0 @@
'use strict'
/* global wiki */
const Git = require('git-wrapper2-promise')
const Promise = require('bluebird')
const path = require('path')
const fs = Promise.promisifyAll(require('fs-extra'))
const _ = require('lodash')
const URL = require('url')
const moment = require('moment')
const securityHelper = require('../helpers/security')
/**
* Git Model
*/
module.exports = {
_git: null,
_url: '',
_repo: {
path: '',
branch: 'master',
exists: false
},
_signature: {
email: 'wiki@example.com'
},
_opts: {
clone: {},
push: {}
},
onReady: null,
/**
* Initialize Git model
*
* @return {Object} Git model instance
*/
init() {
let self = this
// -> Build repository path
if (_.isEmpty(wiki.config.paths.repo)) {
self._repo.path = path.join(wiki.ROOTPATH, 'repo')
} else {
self._repo.path = wiki.config.paths.repo
}
// -> Initialize repository
self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve()
if (wiki.config.git) {
self._repo.branch = wiki.config.git.branch || 'master'
self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com'
}
return self
},
/**
* Initialize Git repository
*
* @param {Object} wiki.config The application config
* @return {Object} Promise
*/
_initRepo() {
let self = this
// -> Check if path is accessible
return fs.mkdirAsync(self._repo.path).catch((err) => {
if (err.code !== 'EEXIST') {
wiki.logger.error('Invalid Git repository path or missing permissions.')
}
}).then(() => {
self._git = new Git({ 'git-dir': self._repo.path })
// -> Check if path already contains a git working folder
return self._git.isRepo().then((isRepo) => {
self._repo.exists = isRepo
return (!isRepo) ? self._git.exec('init') : true
}).catch((err) => { // eslint-disable-line handle-callback-err
self._repo.exists = false
})
}).then(() => {
if (wiki.config.git.enabled === false) {
wiki.logger.warn('Git Remote Sync: [ DISABLED ]')
return Promise.resolve(true)
}
// Initialize remote
let urlObj = URL.parse(wiki.config.git.url)
if (wiki.config.git.auth.type !== 'ssh') {
urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password
}
self._url = URL.format(urlObj)
let gitConfigs = [
() => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
() => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
() => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) }
]
if (wiki.config.git.auth.type === 'ssh') {
gitConfigs.push(() => {
return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
})
}
return self._git.exec('remote', 'show').then((cProc) => {
let out = cProc.stdout.toString()
return Promise.each(gitConfigs, fn => { return fn() }).then(() => {
if (!_.includes(out, 'origin')) {
return self._git.exec('remote', ['add', 'origin', self._url])
} else {
return self._git.exec('remote', ['set-url', 'origin', self._url])
}
}).catch(err => {
wiki.logger.error(err)
})
})
}).catch((err) => {
wiki.logger.error('Git remote error!')
throw err
}).then(() => {
wiki.logger.info('Git Repository: [ OK ]')
return true
})
},
/**
* Gets the repo path.
*
* @return {String} The repo path.
*/
getRepoPath() {
return this._repo.path || path.join(wiki.ROOTPATH, 'repo')
},
/**
* Sync with the remote repository
*
* @return {Promise} Resolve on sync success
*/
resync() {
let self = this
// Is git remote disabled?
if (wiki.config.git === false) {
return Promise.resolve(true)
}
// Fetch
wiki.logger.info('Performing pull from remote Git repository...')
return self._git.pull('origin', self._repo.branch).then((cProc) => {
wiki.logger.info('Git Pull completed.')
})
.catch((err) => {
wiki.logger.error('Unable to fetch from git origin!')
throw err
})
.then(() => {
// Check for changes
return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'commit')) {
wiki.logger.info('Performing push to remote Git repository...')
return self._git.push('origin', self._repo.branch).then(() => {
return wiki.logger.info('Git Push completed.')
})
} else {
wiki.logger.info('Git Push skipped. Repository is already in sync.')
}
return true
})
})
.catch((err) => {
wiki.logger.error('Unable to push changes to remote Git repository!')
throw err
})
},
/**
* Commits a document.
*
* @param {String} entryPath The entry path
* @return {Promise} Resolve on commit success
*/
commitDocument(entryPath, author) {
let self = this
let gitFilePath = entryPath + '.md'
let commitMsg = ''
return self._git.exec('ls-files', gitFilePath).then((cProc) => {
let out = cProc.stdout.toString()
return _.includes(out, gitFilePath)
}).then((isTracked) => {
commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath })
return self._git.add(gitFilePath)
}).then(() => {
let commitUsr = securityHelper.sanitizeCommitUser(author)
return self._git.exec('commit', ['-m', commitMsg, '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
},
/**
* Move a document.
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @return {Promise<Boolean>} Resolve on success
*/
moveDocument(entryPath, newEntryPath) {
let self = this
let gitFilePath = entryPath + '.md'
let gitNewFilePath = newEntryPath + '.md'
let destPathObj = path.parse(this.getRepoPath() + '/' + gitNewFilePath)
return fs.ensureDir(destPathObj.dir).then(() => {
return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'fatal')) {
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
throw new Error(errorMsg)
}
return true
})
})
},
/**
* Commits uploads changes.
*
* @param {String} msg The commit message
* @return {Promise} Resolve on commit success
*/
commitUploads(msg) {
let self = this
msg = msg || 'Uploads repository sync'
return self._git.add('uploads').then(() => {
return self._git.commit(msg).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
},
getHistory(entryPath) {
let self = this
let gitFilePath = entryPath + '.md'
return self._git.exec('log', ['--max-count=25', '--skip=1', '--format=format:%H %h %cI %cE %cN', '--', gitFilePath]).then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'fatal')) {
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
throw new Error(errorMsg)
}
let hist = _.chain(out).split('\n').map(h => {
let hParts = h.split(' ', 4)
let hDate = moment(hParts[2])
return {
commit: hParts[0],
commitAbbr: hParts[1],
date: hParts[2],
dateFull: hDate.format('LLLL'),
dateCalendar: hDate.calendar(null, { sameElse: 'llll' }),
email: hParts[3],
name: hParts[4]
}
}).value()
return hist
})
},
getHistoryDiff(path, commit, comparewith) {
let self = this
if (!comparewith) {
comparewith = 'HEAD'
}
return self._git.exec('diff', ['--no-color', `${commit}:${path}.md`, `${comparewith}:${path}.md`]).then((cProc) => {
let out = cProc.stdout.toString()
if (_.startsWith(out, 'fatal: ')) {
throw new Error(out)
} else if (!_.includes(out, 'diff')) {
throw new Error('Unable to query diff data.')
} else {
return out
}
})
}
}

View File

@@ -1,43 +0,0 @@
'use strict'
/* global wiki */
const gqlTools = require('graphql-tools')
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8')
const DateScalar = require('../schemas/scalar-date')
const AuthenticationResolvers = require('../schemas/resolvers-authentication')
const CommentResolvers = require('../schemas/resolvers-comment')
const DocumentResolvers = require('../schemas/resolvers-document')
const FileResolvers = require('../schemas/resolvers-file')
const FolderResolvers = require('../schemas/resolvers-folder')
const GroupResolvers = require('../schemas/resolvers-group')
const SettingResolvers = require('../schemas/resolvers-setting')
const TagResolvers = require('../schemas/resolvers-tag')
const TranslationResolvers = require('../schemas/resolvers-translation')
const UserResolvers = require('../schemas/resolvers-user')
const resolvers = _.merge(
AuthenticationResolvers,
CommentResolvers,
DocumentResolvers,
FileResolvers,
FolderResolvers,
GroupResolvers,
SettingResolvers,
TagResolvers,
TranslationResolvers,
UserResolvers,
DateScalar
)
const Schema = gqlTools.makeExecutableSchema({
typeDefs,
resolvers
})
module.exports = Schema

View File

@@ -1,95 +0,0 @@
const cluster = require('cluster')
const Promise = require('bluebird')
const _ = require('lodash')
/* global wiki */
module.exports = {
numWorkers: 1,
workers: [],
init() {
if (cluster.isMaster) {
wiki.logger.info('=======================================')
wiki.logger.info('= Wiki.js =============================')
wiki.logger.info('=======================================')
wiki.redis = require('./redis').init()
wiki.queue = require('./queue').init()
this.setWorkerLimit()
this.bootMaster()
} else {
this.bootWorker()
}
},
/**
* Pre-Master Boot Sequence
*/
preBootMaster() {
return Promise.mapSeries([
() => { return wiki.db.onReady },
() => { return wiki.configSvc.loadFromDb() },
() => { return wiki.queue.clean() }
], fn => { return fn() })
},
/**
* Boot Master Process
*/
bootMaster() {
this.preBootMaster().then(sequenceResults => {
if (_.every(sequenceResults, rs => rs === true) && wiki.config.configMode !== 'setup') {
this.postBootMaster()
} else {
wiki.logger.info('Starting configuration manager...')
require('../setup')()
}
return true
}).catch(err => {
wiki.logger.error(err)
process.exit(1)
})
},
/**
* Post-Master Boot Sequence
*/
async postBootMaster() {
await require('../master')()
_.times(this.numWorkers, () => {
this.spawnWorker()
})
wiki.queue.uplClearTemp.add({}, {
repeat: { cron: '*/15 * * * *' }
})
cluster.on('exit', (worker, code, signal) => {
if (!global.DEV) {
wiki.logger.info(`Background Worker #${worker.id} was terminated.`)
}
})
},
/**
* Boot Worker Process
*/
bootWorker() {
wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`)
require('../worker')
},
/**
* Spawn new Worker process
*/
spawnWorker() {
this.workers.push(cluster.fork())
},
/**
* Set Worker count based on config + system capabilities
*/
setWorkerLimit() {
const numCPUs = require('os').cpus().length
this.numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs
if (this.numWorkers > numCPUs) {
this.numWorkers = numCPUs
}
}
}

View File

@@ -1,53 +0,0 @@
const _ = require('lodash')
const dotize = require('dotize')
const i18nBackend = require('i18next-node-fs-backend')
const i18next = require('i18next')
const path = require('path')
const Promise = require('bluebird')
/* global wiki */
module.exports = {
engine: null,
namespaces: [],
init() {
this.namespaces = wiki.data.localeNamespaces
this.engine = i18next
this.engine.use(i18nBackend).init({
load: 'languageOnly',
ns: this.namespaces,
defaultNS: 'common',
saveMissing: false,
preload: [wiki.config.site.lang],
lng: wiki.config.site.lang,
fallbackLng: 'en',
backend: {
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.yml')
}
})
return this
},
async getByNamespace(locale, namespace) {
if (this.engine.hasResourceBundle(locale, namespace)) {
let data = this.engine.getResourceBundle(locale, namespace)
return _.map(dotize.convert(data), (value, key) => {
return {
key,
value
}
})
} else {
throw new Error('Invalid locale or namespace')
}
},
async loadLocale(locale) {
return Promise.fromCallback(cb => {
return this.engine.loadLanguages(locale, cb)
})
},
async setCurrentLocale(locale) {
return Promise.fromCallback(cb => {
return this.engine.changeLanguage(locale, cb)
})
}
}

View File

@@ -1,40 +0,0 @@
/* global wiki */
const cluster = require('cluster')
const _ = require('lodash')
const fs = require('fs-extra')
const path = require('path')
module.exports = {
loggers: {},
init() {
let winston = require('winston')
let logger = new (winston.Logger)({
level: wiki.config.logLevel,
transports: []
})
logger.filters.push((level, msg) => {
let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}`
return '[' + processName + '] ' + msg
})
_.forOwn(_.omitBy(wiki.config.logging.loggers, s => s.enabled === false), (loggerConfig, loggerKey) => {
let loggerModule = require(`../extensions/logging/${loggerKey}`)
loggerModule.init(logger, loggerConfig)
fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${loggerKey}.svg`), 'utf8').then(iconData => {
logger.icon = iconData
}).catch(err => {
if (err.code === 'ENOENT') {
logger.icon = '[missing icon]'
} else {
logger.error(err)
}
})
this.loggers[logger.key] = loggerModule
})
return logger
}
}

View File

@@ -0,0 +1,32 @@
const util = require('util')
const winston = require('winston')
const _ = require('lodash')
// ------------------------------------
// Bugsnag
// ------------------------------------
module.exports = {
key: 'bugsnag',
title: 'Bugsnag',
props: ['key'],
init (logger, conf) {
let BugsnagLogger = winston.transports.BugsnagLogger = function (options) {
this.name = 'bugsnagLogger'
this.level = options.level || 'warn'
this.bugsnag = require('bugsnag')
this.bugsnag.register(options.key)
}
util.inherits(BugsnagLogger, winston.Transport)
BugsnagLogger.prototype.log = function (level, msg, meta, callback) {
this.bugsnag.notify(new Error(msg), _.assignIn(meta, { severity: level }))
callback(null, true)
}
logger.add(BugsnagLogger, {
level: 'warn',
key: conf.key
})
}
}

View File

@@ -0,0 +1,22 @@
const winston = require('winston')
/* global wiki */
// ------------------------------------
// Console
// ------------------------------------
module.exports = {
key: 'console',
title: 'Console',
props: [],
init (logger, conf) {
logger.add(winston.transports.Console, {
level: wiki.config.logLevel,
prettyPrint: true,
colorize: true,
silent: false,
timestamp: true
})
}
}

View File

@@ -0,0 +1,21 @@
const winston = require('winston')
// ------------------------------------
// Loggly
// ------------------------------------
module.exports = {
key: 'loggly',
title: 'Loggly',
props: ['token', 'subdomain'],
init (logger, conf) {
require('winston-loggly-bulk')
logger.add(winston.transports.Loggly, {
token: conf.token,
subdomain: conf.subdomain,
tags: ['wiki-js'],
level: 'warn',
json: true
})
}
}

View File

@@ -0,0 +1,20 @@
const winston = require('winston')
// ------------------------------------
// Papertrail
// ------------------------------------
module.exports = {
key: 'papertrail',
title: 'Papertrail',
props: ['host', 'port'],
init (logger, conf) {
require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
logger.add(winston.transports.Papertrail, {
host: conf.host,
port: conf.port,
level: 'warn',
program: 'wiki.js'
})
}
}

View File

@@ -0,0 +1,32 @@
const util = require('util')
const winston = require('winston')
const _ = require('lodash')
// ------------------------------------
// Rollbar
// ------------------------------------
module.exports = {
key: 'rollbar',
title: 'Rollbar',
props: ['key'],
init (logger, conf) {
let RollbarLogger = winston.transports.RollbarLogger = function (options) {
this.name = 'rollbarLogger'
this.level = options.level || 'warn'
this.rollbar = require('rollbar')
this.rollbar.init(options.key)
}
util.inherits(RollbarLogger, winston.Transport)
RollbarLogger.prototype.log = function (level, msg, meta, callback) {
this.rollbar.handleErrorWithPayloadData(new Error(msg), _.assignIn(meta, { level }))
callback(null, true)
}
logger.add(RollbarLogger, {
level: 'warn',
key: conf.key
})
}
}

View File

@@ -0,0 +1,32 @@
const util = require('util')
const winston = require('winston')
// ------------------------------------
// Sentry
// ------------------------------------
module.exports = {
key: 'sentry',
title: 'Sentry',
props: ['key'],
init (logger, conf) {
let SentryLogger = winston.transports.RollbarLogger = function (options) {
this.name = 'sentryLogger'
this.level = options.level || 'warn'
this.raven = require('raven')
this.raven.config(options.key).install()
}
util.inherits(SentryLogger, winston.Transport)
SentryLogger.prototype.log = function (level, msg, meta, callback) {
level = (level === 'warn') ? 'warning' : level
this.raven.captureMessage(msg, { level, extra: meta })
callback(null, true)
}
logger.add(SentryLogger, {
level: 'warn',
key: conf.key
})
}
}

View File

@@ -1,417 +0,0 @@
/* global wiki */
const Promise = require('bluebird')
const md = require('markdown-it')
const mdEmoji = require('markdown-it-emoji')
const mdTaskLists = require('markdown-it-task-lists')
const mdAbbr = require('markdown-it-abbr')
const mdAnchor = require('markdown-it-anchor')
const mdFootnote = require('markdown-it-footnote')
const mdExternalLinks = require('markdown-it-external-links')
const mdExpandTabs = require('markdown-it-expand-tabs')
const mdAttrs = require('markdown-it-attrs')
const mdMathjax = require('markdown-it-mathjax')()
const mathjax = require('mathjax-node')
const hljs = require('highlight.js')
const cheerio = require('cheerio')
const _ = require('lodash')
const mdRemove = require('remove-markdown')
// Load plugins
var mkdown = md({
html: true,
// breaks: wiki.config.features.linebreaks,
breaks: true,
linkify: true,
typography: true,
highlight(str, lang) {
if (wiki.config.theme.code.colorize && lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
} catch (err) {
return '<pre><code>' + _.escape(str) + '</code></pre>'
}
}
return '<pre><code>' + _.escape(str) + '</code></pre>'
}
})
.use(mdEmoji)
.use(mdTaskLists)
.use(mdAbbr)
.use(mdAnchor, {
slugify: _.kebabCase,
permalink: true,
permalinkClass: 'toc-anchor nc-icon-outline location_bookmark-add',
permalinkSymbol: '',
permalinkBefore: true
})
.use(mdFootnote)
.use(mdExternalLinks, {
externalClassName: 'external-link',
internalClassName: 'internal-link'
})
.use(mdExpandTabs, {
tabWidth: 4
})
.use(mdAttrs)
if (wiki.config.features.mathjax) {
mkdown.use(mdMathjax)
}
// Rendering rules
mkdown.renderer.rules.emoji = function (token, idx) {
return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'
}
// Video rules
const videoRules = [
{
selector: 'a.youtube',
regexp: new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/i),
output: '<iframe width="640" height="360" src="https://www.youtube.com/embed/{0}?rel=0" frameborder="0" allowfullscreen></iframe>'
},
{
selector: 'a.vimeo',
regexp: new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/i),
output: '<iframe src="https://player.vimeo.com/video/{0}" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
},
{
selector: 'a.dailymotion',
regexp: new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/i),
output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>'
},
{
selector: 'a.video',
regexp: false,
output: '<video width="640" height="360" controls preload="metadata"><source src="{0}" type="video/mp4"></video>'
}
]
// Regex
const textRegex = new RegExp('\\b[a-z0-9-.,' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\b', 'g')
const mathRegex = [
{
format: 'TeX',
regex: /\\\[([\s\S]*?)\\\]/g
},
{
format: 'inline-TeX',
regex: /\\\((.*?)\\\)/g
},
{
format: 'MathML',
regex: /<math([\s\S]*?)<\/math>/g
}
]
// MathJax
mathjax.config({
MathJax: {
jax: ['input/TeX', 'input/MathML', 'output/SVG'],
extensions: ['tex2jax.js', 'mml2jax.js'],
TeX: {
extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js']
},
SVG: {
scale: 120,
font: 'STIX-Web'
}
}
})
/**
* Parse markdown content and build TOC tree
*
* @param {(Function|string)} content Markdown content
* @return {Array} TOC tree
*/
const parseTree = (content) => {
content = content.replace(/<!--(.|\t|\n|\r)*?-->/g, '')
let tokens = md().parse(content, {})
let tocArray = []
// -> Extract headings and their respective levels
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type !== 'heading_close') {
continue
}
const heading = tokens[i - 1]
const headingclose = tokens[i]
if (heading.type === 'inline') {
let content = ''
let anchor = ''
if (heading.children && heading.children.length > 0 && heading.children[0].type === 'link_open') {
content = mdRemove(heading.children[1].content)
anchor = _.kebabCase(content)
} else {
content = mdRemove(heading.content)
anchor = _.kebabCase(heading.children.reduce((acc, t) => acc + t.content, ''))
}
tocArray.push({
content,
anchor,
level: +headingclose.tag.substr(1, 1)
})
}
}
// -> Exclude levels deeper than 2
_.remove(tocArray, (n) => { return n.level > 2 })
// -> Build tree from flat array
return _.reduce(tocArray, (tree, v) => {
let treeLength = tree.length - 1
if (v.level < 2) {
tree.push({
content: v.content,
anchor: v.anchor,
nodes: []
})
} else {
let lastNodeLevel = 1
let GetNodePath = (startPos) => {
lastNodeLevel++
if (_.isEmpty(startPos)) {
startPos = 'nodes'
}
if (lastNodeLevel === v.level) {
return startPos
} else {
return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes')
}
}
let lastNodePath = GetNodePath()
let lastNode = _.get(tree[treeLength], lastNodePath)
if (lastNode) {
lastNode.push({
content: v.content,
anchor: v.anchor,
nodes: []
})
_.set(tree[treeLength], lastNodePath, lastNode)
}
}
return tree
}, [])
}
/**
* Parse markdown content to HTML
*
* @param {String} content Markdown content
* @return {Promise<String>} Promise
*/
const parseContent = (content) => {
let cr = cheerio.load(mkdown.render(content))
if (cr.root().children().length < 1) {
return ''
}
// -> Check for empty first element
let firstElm = cr.root().children().first()[0]
if (firstElm.type === 'tag' && firstElm.name === 'p') {
let firstElmChildren = firstElm.children
if (firstElmChildren.length < 1) {
firstElm.remove()
} else if (firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
cr(firstElm).addClass('is-gapless')
}
}
// -> Remove links in headers
cr('h1 > a:not(.toc-anchor), h2 > a:not(.toc-anchor), h3 > a:not(.toc-anchor)').each((i, elm) => {
let txtLink = cr(elm).text()
cr(elm).replaceWith(txtLink)
})
// -> Re-attach blockquote styling classes to their parents
cr('blockquote').each((i, elm) => {
if (cr(elm).children().length > 0) {
let bqLastChild = cr(elm).children().last()[0]
let bqLastChildClasses = cr(bqLastChild).attr('class')
if (bqLastChildClasses && bqLastChildClasses.length > 0) {
cr(bqLastChild).removeAttr('class')
cr(elm).addClass(bqLastChildClasses)
}
}
})
// -> Enclose content below headers
cr('h2').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2')
cr(elm).after('<div class="indent-h2"></div>')
let subH2Container = cr(elm).next('.indent-h2')
_.forEach(subH2Content, (ch) => {
cr(subH2Container).append(ch)
})
})
cr('h3').each((i, elm) => {
let subH3Content = cr(elm).nextUntil('h1, h2, h3')
cr(elm).after('<div class="indent-h3"></div>')
let subH3Container = cr(elm).next('.indent-h3')
_.forEach(subH3Content, (ch) => {
cr(subH3Container).append(ch)
})
})
// Replace video links with embeds
_.forEach(videoRules, (vrule) => {
cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href')
if (vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp)
if ((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f)
})
originLink = _.last(vidMatches)
}
}
let processedLink = _.replace(vrule.output, '{0}', originLink)
cr(elm).replaceWith(processedLink)
})
})
// Apply align-center to parent
cr('img.align-center').each((i, elm) => {
cr(elm).parent().addClass('align-center')
cr(elm).removeClass('align-center')
})
// Mathjax Post-processor
if (wiki.config.features.mathjax) {
return processMathjax(cr.html())
} else {
return Promise.resolve(cr.html())
}
}
/**
* Process MathJax expressions
*
* @param {String} content HTML content
* @returns {Promise<String>} Promise
*/
const processMathjax = (content) => {
let matchStack = []
let replaceStack = []
let currentMatch
let mathjaxState = {}
_.forEach(mathRegex, mode => {
do {
currentMatch = mode.regex.exec(content)
if (currentMatch) {
matchStack.push(currentMatch[0])
replaceStack.push(
new Promise((resolve, reject) => {
mathjax.typeset({
math: (mode.format === 'MathML') ? currentMatch[0] : currentMatch[1],
format: mode.format,
speakText: false,
svg: true,
state: mathjaxState,
timeout: 30 * 1000
}, result => {
if (!result.errors) {
resolve(result.svg)
} else {
resolve(currentMatch[0])
wiki.logger.warn(result.errors.join(', '))
}
})
})
)
}
} while (currentMatch)
})
return (matchStack.length > 0) ? Promise.all(replaceStack).then(results => {
_.forEach(matchStack, (repMatch, idx) => {
content = content.replace(repMatch, results[idx])
})
return content
}) : Promise.resolve(content)
}
/**
* Parse meta-data tags from content
*
* @param {String} content Markdown content
* @return {Object} Properties found in the content and their values
*/
const parseMeta = (content) => {
let commentMeta = new RegExp('<!-- ?([a-zA-Z]+):(.*)-->', 'g')
let results = {}
let match
while ((match = commentMeta.exec(content)) !== null) {
results[_.toLower(match[1])] = _.trim(match[2])
}
return results
}
/**
* Strips non-text elements from Markdown content
*
* @param {String} content Markdown-formatted content
* @return {String} Text-only version
*/
const removeMarkdown = (content) => {
return _.join(mdRemove(_.chain(content)
.replace(/<!-- ?([a-zA-Z]+):(.*)-->/g, '')
.replace(/```([^`]|`)+?```/g, '')
.replace(/`[^`]+`/g, '')
.replace(new RegExp('(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?', 'g'), '')
.deburr()
.toLower()
.value()
).replace(/\r?\n|\r/g, ' ').match(textRegex), ' ')
}
module.exports = {
/**
* Parse content and return all data
*
* @param {String} content Markdown-formatted content
* @return {Object} Object containing meta, html and tree data
*/
parse(content) {
return parseContent(content).then(html => {
return {
meta: parseMeta(content),
html,
tree: parseTree(content)
}
})
},
parseContent,
parseMeta,
parseTree,
removeMarkdown
}

View File

@@ -1,37 +0,0 @@
'use strict'
/* global wiki */
const Bull = require('bull')
const Promise = require('bluebird')
module.exports = {
init() {
wiki.data.queues.forEach(queueName => {
this[queueName] = new Bull(queueName, {
prefix: `q-${wiki.config.ha.nodeuid}`,
redis: wiki.config.redis
})
})
return this
},
clean() {
return Promise.each(wiki.data.queues, queueName => {
return new Promise((resolve, reject) => {
let keyStream = wiki.redis.scanStream({
match: `q-${wiki.config.ha.nodeuid}:${queueName}:*`
})
keyStream.on('data', resultKeys => {
if (resultKeys.length > 0) {
wiki.redis.del(resultKeys)
}
})
keyStream.on('end', resolve)
})
}).then(() => {
wiki.logger.info('Purging old queue jobs: [ OK ]')
}).return(true).catch(err => {
wiki.logger.error(err)
})
}
}

View File

@@ -1,37 +0,0 @@
'use strict'
/* global wiki */
const Redis = require('ioredis')
const { isPlainObject } = require('lodash')
/**
* Redis module
*
* @return {Object} Redis client wrapper instance
*/
module.exports = {
/**
* Initialize Redis client
*
* @return {Object} Redis client instance
*/
init() {
if (isPlainObject(wiki.config.redis)) {
let red = new Redis(wiki.config.redis)
red.on('ready', () => {
wiki.logger.info('Redis connection: [ OK ]')
})
red.on('error', () => {
wiki.logger.error('Failed to connect to Redis instance!')
process.exit(1)
})
return red
} else {
wiki.logger.error('Invalid Redis configuration!')
process.exit(1)
}
}
}

View File

@@ -0,0 +1,86 @@
const mathjax = require('mathjax-node')
const _ = require('lodash')
// ------------------------------------
// Mathjax
// ------------------------------------
/* global wiki */
const mathRegex = [
{
format: 'TeX',
regex: /\\\[([\s\S]*?)\\\]/g
},
{
format: 'inline-TeX',
regex: /\\\((.*?)\\\)/g
},
{
format: 'MathML',
regex: /<math([\s\S]*?)<\/math>/g
}
]
module.exports = {
key: 'common/mathjax',
title: 'Mathjax',
dependsOn: [],
props: [],
init (conf) {
mathjax.config({
MathJax: {
jax: ['input/TeX', 'input/MathML', 'output/SVG'],
extensions: ['tex2jax.js', 'mml2jax.js'],
TeX: {
extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js']
},
SVG: {
scale: 120,
font: 'STIX-Web'
}
}
})
},
async render (content) {
let matchStack = []
let replaceStack = []
let currentMatch
let mathjaxState = {}
_.forEach(mathRegex, mode => {
do {
currentMatch = mode.regex.exec(content)
if (currentMatch) {
matchStack.push(currentMatch[0])
replaceStack.push(
new Promise((resolve, reject) => {
mathjax.typeset({
math: (mode.format === 'MathML') ? currentMatch[0] : currentMatch[1],
format: mode.format,
speakText: false,
svg: true,
state: mathjaxState,
timeout: 30 * 1000
}, result => {
if (!result.errors) {
resolve(result.svg)
} else {
resolve(currentMatch[0])
wiki.logger.warn(result.errors.join(', '))
}
})
})
)
}
} while (currentMatch)
})
return (matchStack.length > 0) ? Promise.all(replaceStack).then(results => {
_.forEach(matchStack, (repMatch, idx) => {
content = content.replace(repMatch, results[idx])
})
return content
}) : Promise.resolve(content)
}
}

View File

@@ -0,0 +1,15 @@
const mdAbbr = require('markdown-it-abbr')
// ------------------------------------
// Markdown - Abbreviations
// ------------------------------------
module.exports = {
key: 'markdown/abbreviations',
title: 'Abbreviations',
dependsOn: [],
props: [],
init (md, conf) {
md.use(mdAbbr)
}
}

View File

@@ -0,0 +1,15 @@
const mdEmoji = require('markdown-it-emoji')
// ------------------------------------
// Markdown - Emoji
// ------------------------------------
module.exports = {
key: 'markdown/emoji',
title: 'Emoji',
dependsOn: [],
props: [],
init (md, conf) {
md.use(mdEmoji)
}
}

View File

@@ -0,0 +1,18 @@
const mdExpandTabs = require('markdown-it-expand-tabs')
const _ = require('lodash')
// ------------------------------------
// Markdown - Expand Tabs
// ------------------------------------
module.exports = {
key: 'markdown/expand-tabs',
title: 'Expand Tabs',
dependsOn: [],
props: ['tabWidth'],
init (md, conf) {
md.use(mdExpandTabs, {
tabWidth: _.toInteger(conf.tabWidth || 4)
})
}
}

View File

@@ -0,0 +1,15 @@
const mdFootnote = require('markdown-it-footnote')
// ------------------------------------
// Markdown - Footnotes
// ------------------------------------
module.exports = {
key: 'markdown/footnotes',
title: 'Footnotes',
dependsOn: [],
props: [],
init (md, conf) {
md.use(mdFootnote)
}
}

View File

@@ -0,0 +1,15 @@
const mdMathjax = require('markdown-it-mathjax')()
// ------------------------------------
// Markdown - Mathjax Preprocessor
// ------------------------------------
module.exports = {
key: 'markdown/mathjax',
title: 'Mathjax Preprocessor',
dependsOn: [],
props: [],
init (md, conf) {
md.use(mdMathjax)
}
}

View File

@@ -0,0 +1,15 @@
const mdTaskLists = require('markdown-it-task-lists')
// ------------------------------------
// Markdown - Task Lists
// ------------------------------------
module.exports = {
key: 'markdown/task-lists',
title: 'Task Lists',
dependsOn: [],
props: [],
init (md, conf) {
md.use(mdTaskLists)
}
}

View File

@@ -1,122 +0,0 @@
'use strict'
/* global wiki */
const _ = require('lodash')
/**
* Rights
*/
module.exports = {
guest: {
provider: 'local',
email: 'guest',
name: 'Guest',
password: '',
rights: [
{
role: 'read',
path: '/',
deny: false,
exact: false
}
]
},
/**
* Initialize Rights module
*
* @return {void} Void
*/
init () {
let self = this
wiki.db.onReady.then(() => {
wiki.db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => {
if (u) {
self.guest = u
}
})
})
},
/**
* Check user permissions for this request
*
* @param {object} req The request object
* @return {object} List of permissions for this request
*/
check (req) {
let self = this
let perm = {
read: false,
write: false,
manage: false
}
let rt = []
let p = _.chain(req.originalUrl).toLower().trim().value()
// Load user rights
if (_.isArray(req.user.rights)) {
rt = req.user.rights
}
// Check rights
if (self.checkRole(p, rt, 'admin')) {
perm.read = true
perm.write = true
perm.manage = true
} else if (self.checkRole(p, rt, 'write')) {
perm.read = true
perm.write = true
} else if (self.checkRole(p, rt, 'read')) {
perm.read = true
}
return perm
},
/**
* Check for a specific role based on list of user rights
*
* @param {String} p Base path
* @param {array<object>} rt The user rights
* @param {string} role The minimum role required
* @return {boolean} True if authorized
*/
checkRole (p, rt, role) {
if (_.find(rt, { role: 'admin' })) { return true }
// Check specific role on path
let filteredRights = _.filter(rt, (r) => {
if (r.role === role || (r.role === 'write' && role === 'read')) {
if ((!r.exact && _.startsWith(p, r.path)) || (r.exact && p === r.path)) {
return true
}
}
return false
})
// Check for deny scenario
let isValid = false
if (filteredRights.length > 1) {
isValid = !_.chain(filteredRights).sortBy((r) => {
return r.path.length + ((r.deny) ? 0.5 : 0)
}).last().get('deny').value()
} else if (filteredRights.length === 1 && filteredRights[0].deny === false) {
isValid = true
}
// Deny by default
return isValid
}
}

View File

@@ -1,211 +0,0 @@
'use strict'
/* global wiki */
const Promise = require('bluebird')
const _ = require('lodash')
// const searchIndex = require('./search-index')
// const stopWord = require('stopword')
const streamToPromise = require('stream-to-promise')
const searchAllowedChars = new RegExp('[^a-z0-9' + wiki.data.regex.cjk + wiki.data.regex.arabic + ' ]', 'g')
module.exports = {
_si: null,
_isReady: false,
/**
* Initialize search index
*
* @return {undefined} Void
*/
init () {
let self = this
self._isReady = new Promise((resolve, reject) => {
/* searchIndex({
deletable: true,
fieldedSearch: true,
indexPath: 'wiki',
logLevel: 'error',
stopwords: _.get(stopWord, wiki.config.lang, [])
}, (err, si) => {
if (err) {
wiki.logger.error('Failed to initialize search index.', err)
reject(err)
} else {
self._si = Promise.promisifyAll(si)
self._si.flushAsync().then(() => {
wiki.logger.info('Search index flushed and ready.')
resolve(true)
})
}
}) */
})
return self
},
/**
* Add a document to the index
*
* @param {Object} content Document content
* @return {Promise} Promise of the add operation
*/
add (content) {
let self = this
if (!content.isEntry) {
return Promise.resolve(true)
}
return self._isReady.then(() => {
return self.delete(content._id).then(() => {
return self._si.concurrentAddAsync({
fieldOptions: [{
fieldName: 'entryPath',
searchable: true,
weight: 2
},
{
fieldName: 'title',
nGramLength: [1, 2],
searchable: true,
weight: 3
},
{
fieldName: 'subtitle',
searchable: true,
weight: 1,
storeable: false
},
{
fieldName: 'parent',
searchable: false
},
{
fieldName: 'content',
searchable: true,
weight: 0,
storeable: false
}]
}, [{
entryPath: content._id,
title: content.title,
subtitle: content.subtitle || '',
parent: content.parent || '',
content: content.text || ''
}]).then(() => {
wiki.logger.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
return true
}).catch((err) => {
wiki.logger.error(err)
})
}).catch((err) => {
wiki.logger.error(err)
})
})
},
/**
* Delete an entry from the index
*
* @param {String} The entry path
* @return {Promise} Promise of the operation
*/
delete (entryPath) {
let self = this
return self._isReady.then(() => {
return streamToPromise(self._si.search({
query: [{
AND: { 'entryPath': [entryPath] }
}]
})).then((results) => {
if (results && results.length > 0) {
let delIds = _.map(results, 'id')
return self._si.delAsync(delIds)
} else {
return true
}
}).catch((err) => {
if (err.type === 'NotFoundError') {
return true
} else {
wiki.logger.error(err)
}
})
})
},
/**
* Flush the index
*
* @returns {Promise} Promise of the flush operation
*/
flush () {
let self = this
return self._isReady.then(() => {
return self._si.flushAsync()
})
},
/**
* Search the index
*
* @param {Array<String>} terms
* @returns {Promise<Object>} Hits and suggestions
*/
find (terms) {
let self = this
terms = _.chain(terms)
.deburr()
.toLower()
.trim()
.replace(searchAllowedChars, ' ')
.value()
let arrTerms = _.chain(terms)
.split(' ')
.filter((f) => { return !_.isEmpty(f) })
.value()
return streamToPromise(self._si.search({
query: [{
AND: { '*': arrTerms }
}],
pageSize: 10
})).then((hits) => {
if (hits.length > 0) {
hits = _.map(_.sortBy(hits, ['score']), h => {
return h.document
})
}
if (hits.length < 5) {
return streamToPromise(self._si.match({
beginsWith: terms,
threshold: 3,
limit: 5,
type: 'simple'
})).then((matches) => {
return {
match: hits,
suggest: matches
}
})
} else {
return {
match: hits,
suggest: []
}
}
}).catch((err) => {
if (err.type === 'NotFoundError') {
return {
match: [],
suggest: []
}
} else {
wiki.logger.error(err)
}
})
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
activate() {
},
deactivate() {
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
activate() {
},
deactivate() {
}
}

View File

@@ -1,69 +0,0 @@
/* global wiki */
const Promise = require('bluebird')
// const pm2 = Promise.promisifyAll(require('pm2'))
const _ = require('lodash')
const cfgHelper = require('../helpers/config')
module.exports = {
/**
* Upgrade from Wiki.js 1.x - MongoDB database
*
* @param {Object} opts Options object
*/
async upgradeFromMongo (opts) {
wiki.telemetry.sendEvent('setup', 'upgradeFromMongo')
wiki.logger.info('Upgrading from MongoDB...')
let mongo = require('mongodb').MongoClient
let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr)
return new Promise((resolve, reject) => {
// Connect to MongoDB
mongo.connect(parsedMongoConStr, {
autoReconnect: false,
reconnectTries: 2,
reconnectInterval: 1000,
connectTimeoutMS: 5000,
socketTimeoutMS: 5000
}, async (err, db) => {
try {
if (err !== null) { throw err }
let users = db.collection('users')
// Check if users table is populated
let userCount = await users.count()
if (userCount < 2) {
throw new Error('MongoDB Upgrade: Users table is empty!')
}
// Import all users
let userData = await users.find({
email: {
$not: 'guest'
}
}).toArray()
await wiki.db.User.bulkCreate(_.map(userData, usr => {
return {
email: usr.email,
name: usr.name || 'Imported User',
password: usr.password || '',
provider: usr.provider || 'local',
providerId: usr.providerId || '',
role: 'user',
createdAt: usr.createdAt
}
}))
resolve(true)
} catch (err) {
reject(err)
}
db.close()
})
})
}
}

View File

@@ -1,63 +0,0 @@
const axios = require('axios')
const bugsnag = require('bugsnag')
const path = require('path')
const uuid = require('uuid/v4')
const _ = require('lodash')
/* global wiki */
module.exports = {
cid: '',
enabled: false,
init() {
this.cid = uuid()
bugsnag.register(wiki.data.telemetry.BUGSNAG_ID, {
appVersion: wiki.version,
autoNotify: false,
hostname: this.cid,
notifyReleaseStages: ['production'],
packageJSON: path.join(wiki.ROOTPATH, 'package.json'),
projectRoot: wiki.ROOTPATH,
useSSL: true
})
bugsnag.onBeforeNotify((notification, originalError) => {
if (!this.enabled) { return false }
})
if (_.get(wiki.config, 'logging.telemetry', false) === true) {
this.enabled = true
}
return this
},
sendError(err) {
bugsnag.notify(err, { userId: this.cid })
},
sendEvent(eventCategory, eventAction, eventLabel) {
if (!this.enabled) { return false }
axios({
method: 'post',
url: wiki.data.telemetry.GA_REMOTE,
headers: {
'Content-type': 'application/x-www-form-urlencoded'
},
params: {
v: 1, // API version
tid: wiki.data.telemetry.GA_ID, // Tracking ID
aip: 1, // Anonymize IP
ds: 'server', // Data source
cid: this.cid, // Client ID
t: 'event', // Hit Type
ec: eventCategory, // Event Category
ea: eventAction, // Event Action
el: eventLabel // Event Label
}
}).then(resp => {
if (resp.status !== 200) {
wiki.logger.warn('Unable to send analytics telemetry request.')
}
}, err => {
wiki.logger.warn('Unable to send analytics telemetry request.')
})
}
}

View File

@@ -1,252 +0,0 @@
'use strict'
/* global wiki */
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const readChunk = require('read-chunk')
const fileType = require('file-type')
const mime = require('mime-types')
const crypto = require('crypto')
const chokidar = require('chokidar')
const jimp = require('jimp')
const imageSize = Promise.promisify(require('image-size'))
const _ = require('lodash')
/**
* Uploads - Agent
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
_watcher: null,
/**
* Initialize Uploads model
*
* @return {Object} Uploads model instance
*/
init () {
let self = this
self._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
self._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
return self
},
/**
* Watch the uploads folder for changes
*
* @return {Void} Void
*/
watch () {
let self = this
self._watcher = chokidar.watch(self._uploadsPath, {
persistent: true,
ignoreInitial: true,
cwd: self._uploadsPath,
depth: 1,
awaitWriteFinish: true
})
// -> Add new upload file
self._watcher.on('add', (p) => {
let pInfo = self.parseUploadsRelPath(p)
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
return wiki.db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
}).then(() => {
return wiki.git.commitUploads(wiki.lang.t('git:uploaded', { path: p }))
})
})
// -> Remove upload file
self._watcher.on('unlink', (p) => {
return wiki.git.commitUploads(wiki.lang.t('git:deleted', { path: p }))
})
},
/**
* Initial Uploads scan
*
* @return {Promise<Void>} Promise of the scan operation
*/
initialScan () {
let self = this
return fs.readdirAsync(self._uploadsPath).then((ls) => {
// Get all folders
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s } })
}).filter((s) => { return s.stat.isDirectory() }).then((arrDirs) => {
let folderNames = _.map(arrDirs, 'filename')
folderNames.unshift('')
// Add folders to DB
return wiki.db.UplFolder.remove({}).then(() => {
return wiki.db.UplFolder.insertMany(_.map(folderNames, (f) => {
return {
_id: 'f:' + f,
name: f
}
}))
}).then(() => {
// Travel each directory and scan files
let allFiles = []
return Promise.map(folderNames, (fldName) => {
let fldPath = path.join(self._uploadsPath, fldName)
return fs.readdirAsync(fldPath).then((fList) => {
return Promise.map(fList, (f) => {
return wiki.upl.processFile(fldName, f).then((mData) => {
if (mData) {
allFiles.push(mData)
}
return true
})
}, {concurrency: 3})
})
}, {concurrency: 1}).finally(() => {
// Add files to DB
return wiki.db.UplFile.remove({}).then(() => {
if (_.isArray(allFiles) && allFiles.length > 0) {
return wiki.db.UplFile.insertMany(allFiles)
} else {
return true
}
})
})
})
})
}).then(() => {
// Watch for new changes
return wiki.upl.watch()
})
},
/**
* Parse relative Uploads path
*
* @param {String} f Relative Uploads path
* @return {Object} Parsed path (folder and filename)
*/
parseUploadsRelPath (f) {
let fObj = path.parse(f)
return {
folder: fObj.dir,
filename: fObj.base
}
},
/**
* Get metadata from file and generate thumbnails if necessary
*
* @param {String} fldName The folder name
* @param {String} f The filename
* @return {Promise<Object>} Promise of the file metadata
*/
processFile (fldName, f) {
let self = this
let fldPath = path.join(self._uploadsPath, fldName)
let fPath = path.join(fldPath, f)
let fPathObj = path.parse(fPath)
let fUid = crypto.createHash('md5').update(fldName + '/' + f).digest('hex')
return fs.statAsync(fPath).then((s) => {
if (!s.isFile()) { return false }
// Get MIME info
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262))
if (_.isNil(mimeInfo)) {
mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
}
}
// Images
if (s.size < 3145728) { // ignore files larger than 3MB
if (_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/bmp'], mimeInfo.mime)) {
return self.getImageSize(fPath).then((mImgSize) => {
let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'))
let cacheThumbnailPathStr = path.format(cacheThumbnailPath)
let mData = {
_id: fUid,
category: 'image',
mime: mimeInfo.mime,
extra: mImgSize,
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
}
// Generate thumbnail
return fs.statAsync(cacheThumbnailPathStr).then((st) => {
return st.isFile()
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
}).then((thumbExists) => {
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
return self.generateThumbnail(fPath, cacheThumbnailPathStr)
}).return(mData)
})
})
}
}
// Other Files
return {
_id: fUid,
category: 'binary',
mime: mimeInfo.mime,
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
}
})
},
/**
* Generate thumbnail of image
*
* @param {String} sourcePath The source path
* @param {String} destPath The destination path
* @return {Promise<Object>} Promise returning the resized image info
*/
generateThumbnail (sourcePath, destPath) {
return jimp.read(sourcePath).then(img => {
return img
.contain(150, 150)
.write(destPath)
})
},
/**
* Gets the image dimensions.
*
* @param {String} sourcePath The source path
* @return {Object} The image dimensions.
*/
getImageSize (sourcePath) {
return imageSize(sourcePath)
}
}

View File

@@ -1,281 +0,0 @@
'use strict'
/* global wiki */
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const request = require('request')
const url = require('url')
const crypto = require('crypto')
const _ = require('lodash')
var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
const maxDownloadFileSize = 3145728 // 3 MB
/**
* Uploads
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
/**
* Initialize Local Data Storage model
*
* @return {Object} Uploads model instance
*/
init () {
this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
return this
},
/**
* Gets the thumbnails folder path.
*
* @return {String} The thumbs path.
*/
getThumbsPath () {
return this._uploadsThumbsPath
},
/**
* Gets the uploads folders.
*
* @return {Array<String>} The uploads folders.
*/
getUploadsFolders () {
return wiki.db.Folder.find({}, 'name').sort('name').exec().then((results) => {
return (results) ? _.map(results, 'name') : [{ name: '' }]
})
},
/**
* Creates an uploads folder.
*
* @param {String} folderName The folder name
* @return {Promise} Promise of the operation
*/
createUploadsFolder (folderName) {
let self = this
folderName = _.kebabCase(_.trim(folderName))
if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
return Promise.resolve(self.getUploadsFolders())
}
return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
return wiki.db.UplFolder.findOneAndUpdate({
_id: 'f:' + folderName
}, {
name: folderName
}, {
upsert: true
})
}).then(() => {
return self.getUploadsFolders()
})
},
/**
* Check if folder is valid and exists
*
* @param {String} folderName The folder name
* @return {Boolean} True if valid
*/
validateUploadsFolder (folderName) {
return wiki.db.UplFolder.findOne({ name: folderName }).then((f) => {
return (f) ? path.resolve(this._uploadsPath, folderName) : false
})
},
/**
* Adds one or more uploads files.
*
* @param {Array<Object>} arrFiles The uploads files
* @return {Void} Void
*/
addUploadsFiles (arrFiles) {
if (_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
// this._uploadswiki.Db.Files.insert(arrFiles);
}
},
/**
* Gets the uploads files.
*
* @param {String} cat Category type
* @param {String} fld Folder
* @return {Array<Object>} The files matching the query
*/
getUploadsFiles (cat, fld) {
return wiki.db.UplFile.find({
category: cat,
folder: 'f:' + fld
}).sort('filename').exec()
},
/**
* Deletes an uploads file.
*
* @param {string} uid The file unique ID
* @return {Promise} Promise of the operation
*/
deleteUploadsFile (uid) {
let self = this
return wiki.db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
if (f) {
return self.deleteUploadsFileTry(f, 0)
}
return true
})
},
deleteUploadsFileTry (f, attempt) {
let self = this
let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
return Promise.join(
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
).catch((err) => {
if (err.code === 'EBUSY' && attempt < 5) {
return Promise.delay(100).then(() => {
return self.deleteUploadsFileTry(f, attempt + 1)
})
} else {
wiki.logger.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.')
return true
}
})
},
/**
* Downloads a file from url.
*
* @param {String} fFolder The folder
* @param {String} fUrl The full URL
* @return {Promise} Promise of the operation
*/
downloadFromUrl (fFolder, fUrl) {
let fUrlObj = url.parse(fUrl)
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'))
let destFolder = _.chain(fFolder).trim().toLower().value()
return wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
return Promise.reject(new Error(wiki.lang.t('errors:invalidfolder')))
}
return wiki.disk.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
let destFilePath = path.resolve(destFolderPath, destFilename)
return new Promise((resolve, reject) => {
let rq = request({
url: fUrl,
method: 'GET',
followRedirect: true,
maxRedirects: 5,
timeout: 10000
})
let destFileStream = fs.createWriteStream(destFilePath)
let curFileSize = 0
rq.on('data', (data) => {
curFileSize += data.length
if (curFileSize > maxDownloadFileSize) {
rq.abort()
destFileStream.destroy()
fs.remove(destFilePath)
reject(new Error(wiki.lang.t('errors:remotetoolarge')))
}
}).on('error', (err) => {
destFileStream.destroy()
fs.remove(destFilePath)
reject(err)
})
destFileStream.on('finish', () => {
resolve(true)
})
rq.pipe(destFileStream)
})
})
})
},
/**
* Move/Rename a file
*
* @param {String} uid The file ID
* @param {String} fld The destination folder
* @param {String} nFilename The new filename (optional)
* @return {Promise} Promise of the operation
*/
moveUploadsFile (uid, fld, nFilename) {
let self = this
return wiki.db.UplFolder.finwiki.dById('f:' + fld).then((folder) => {
if (folder) {
return wiki.db.UplFile.finwiki.dById(uid).then((originFile) => {
// -> Check if rename is valid
let nameCheck = null
if (nFilename) {
let originFileObj = path.parse(originFile.filename)
nameCheck = wiki.disk.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
} else {
nameCheck = Promise.resolve(originFile.filename)
}
return nameCheck.then((destFilename) => {
let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
let preMoveOps = []
// -> Check for invalid operations
if (sourceFilePath === destFilePath) {
return Promise.reject(new Error(wiki.lang.t('errors:invalidoperation')))
}
// -> Delete wiki.DB entry
preMoveOps.push(wiki.db.UplFile.finwiki.dByIdAndRemove(uid))
// -> Move thumbnail ahead to avoid re-generation
if (originFile.category === 'image') {
let fUid = crypto.createHash('md5').update(folder.name + '/' + destFilename).digest('hex')
let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
} else {
preMoveOps.push(Promise.resolve(true))
}
// -> Proceed to move actual file
return Promise.all(preMoveOps).then(() => {
return fs.moveAsync(sourceFilePath, destFilePath, {
clobber: false
})
})
})
})
} else {
return Promise.reject(new Error(wiki.lang.t('errors:invaliddestfolder')))
}
})
}
}