mirror of https://github.com/requarks/wiki
parent
1d9f057f38
commit
3471a7a6f9
@ -0,0 +1,42 @@
|
|||||||
|
.onboarding {
|
||||||
|
background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '800') 100%);
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: mc('grey', '50');
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display:block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-image: url('../static/svg/login-bg-motif.svg');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 500px;
|
||||||
|
z-index: 0;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 500px;
|
||||||
|
filter: grayscale(100%) brightness(160%);
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
@ -1,316 +1,11 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
/* global entries, git, lang, winston */
|
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
const entryHelper = require('../helpers/entry')
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// EDIT MODE
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit document in Markdown
|
|
||||||
*/
|
|
||||||
router.get('/edit/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.render('error-forbidden')
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/edit', ''))
|
|
||||||
|
|
||||||
entries.fetchOriginal(safePath, {
|
|
||||||
parseMarkdown: false,
|
|
||||||
parseMeta: true,
|
|
||||||
parseTree: false,
|
|
||||||
includeMarkdown: true,
|
|
||||||
includeParentInfo: false,
|
|
||||||
cache: false
|
|
||||||
}).then((pageData) => {
|
|
||||||
if (pageData) {
|
|
||||||
res.render('pages/edit', { pageData })
|
|
||||||
} else {
|
|
||||||
throw new Error(lang.t('errors:invalidpath'))
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.render('error', {
|
|
||||||
message: err.message,
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
router.put('/edit/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: lang.t('errors:forbidden')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/edit', ''))
|
|
||||||
|
|
||||||
entries.update(safePath, req.body.markdown, req.user).then(() => {
|
|
||||||
return res.json({
|
|
||||||
ok: true
|
|
||||||
}) || true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.json({
|
|
||||||
ok: false,
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// CREATE MODE
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
router.get('/create/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.render('error-forbidden')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.some(['create', 'edit', 'account', 'source', 'history', 'mk', 'all'], (e) => { return _.startsWith(req.path, '/create/' + e) })) {
|
|
||||||
return res.render('error', {
|
|
||||||
message: lang.t('errors:reservedname'),
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/create', ''))
|
|
||||||
|
|
||||||
entries.exists(safePath).then((docExists) => {
|
|
||||||
if (!docExists) {
|
|
||||||
return entries.getStarter(safePath).then((contents) => {
|
|
||||||
let pageData = {
|
|
||||||
markdown: contents,
|
|
||||||
meta: {
|
|
||||||
title: _.startCase(safePath),
|
|
||||||
path: safePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.render('pages/create', { pageData })
|
|
||||||
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
winston.warn(err)
|
|
||||||
throw new Error(lang.t('errors:starterfailed'))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error(lang.t('errors:alreadyexists'))
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
res.render('error', {
|
|
||||||
message: err.message,
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
router.put('/create/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: lang.t('errors:forbidden')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/create', ''))
|
|
||||||
|
|
||||||
entries.create(safePath, req.body.markdown, req.user).then(() => {
|
|
||||||
return res.json({
|
|
||||||
ok: true
|
|
||||||
}) || true
|
|
||||||
}).catch((err) => {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// LIST ALL PAGES
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View tree view of all pages
|
|
||||||
*/
|
|
||||||
router.use((req, res, next) => {
|
|
||||||
if (_.endsWith(req.url, '/all')) {
|
|
||||||
res.render('pages/all')
|
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// VIEW MODE
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View source of a document
|
|
||||||
*/
|
|
||||||
router.get('/source/*', (req, res, next) => {
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/source', ''))
|
|
||||||
|
|
||||||
entries.fetchOriginal(safePath, {
|
|
||||||
parseMarkdown: false,
|
|
||||||
parseMeta: true,
|
|
||||||
parseTree: false,
|
|
||||||
includeMarkdown: true,
|
|
||||||
includeParentInfo: false,
|
|
||||||
cache: false
|
|
||||||
}).then((pageData) => {
|
|
||||||
if (pageData) {
|
|
||||||
res.render('pages/source', { pageData })
|
|
||||||
} else {
|
|
||||||
throw new Error(lang.t('errors:invalidpath'))
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.render('error', {
|
|
||||||
message: err.message,
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View history of a document
|
|
||||||
*/
|
|
||||||
router.get('/hist/*', (req, res, next) => {
|
|
||||||
let safePath = entryHelper.parsePath(_.replace(req.path, '/hist', ''))
|
|
||||||
|
|
||||||
entries.getHistory(safePath).then((pageData) => {
|
|
||||||
if (pageData) {
|
|
||||||
res.render('pages/history', { pageData })
|
|
||||||
} else {
|
|
||||||
throw new Error(lang.t('errors:invalidpath'))
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.render('error', {
|
|
||||||
message: err.message,
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View history of a document
|
|
||||||
*/
|
|
||||||
router.post('/hist', (req, res, next) => {
|
|
||||||
let commit = req.body.commit
|
|
||||||
let safePath = entryHelper.parsePath(req.body.path)
|
|
||||||
|
|
||||||
if (!/^[a-f0-9]{40}$/.test(commit)) {
|
|
||||||
return res.status(400).json({ ok: false, error: 'Invalid commit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
git.getHistoryDiff(safePath, commit).then((diff) => {
|
|
||||||
res.json({ ok: true, diff })
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(500).json({ ok: false, error: err.message })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View document
|
* View document
|
||||||
*/
|
*/
|
||||||
router.get('/*', (req, res, next) => {
|
router.get('/*', (req, res, next) => {
|
||||||
let safePath = entryHelper.parsePath(req.path)
|
res.render('main/welcome')
|
||||||
|
|
||||||
entries.fetch(safePath).then((pageData) => {
|
|
||||||
if (pageData) {
|
|
||||||
res.render('pages/view', { pageData })
|
|
||||||
} else {
|
|
||||||
res.render('error-notexist', {
|
|
||||||
newpath: safePath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}).error((err) => {
|
|
||||||
if (safePath === 'home') {
|
|
||||||
res.render('pages/welcome')
|
|
||||||
} else {
|
|
||||||
res.render('error-notexist', {
|
|
||||||
message: err.message,
|
|
||||||
newpath: safePath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}).catch((err) => {
|
|
||||||
res.render('error', {
|
|
||||||
message: err.message,
|
|
||||||
error: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move document
|
|
||||||
*/
|
|
||||||
router.put('/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: lang.t('errors:forbidden')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(req.path)
|
|
||||||
|
|
||||||
if (_.isEmpty(req.body.move)) {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: lang.t('errors:invalidaction')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safeNewPath = entryHelper.parsePath(req.body.move)
|
|
||||||
|
|
||||||
entries.move(safePath, safeNewPath, req.user).then(() => {
|
|
||||||
res.json({
|
|
||||||
ok: true
|
|
||||||
})
|
|
||||||
}).catch((err) => {
|
|
||||||
res.json({
|
|
||||||
ok: false,
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete document
|
|
||||||
*/
|
|
||||||
router.delete('/*', (req, res, next) => {
|
|
||||||
if (!res.locals.rights.write) {
|
|
||||||
return res.json({
|
|
||||||
ok: false,
|
|
||||||
error: lang.t('errors:forbidden')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let safePath = entryHelper.parsePath(req.path)
|
|
||||||
|
|
||||||
entries.remove(safePath, req.user).then(() => {
|
|
||||||
res.json({
|
|
||||||
ok: true
|
|
||||||
})
|
|
||||||
}).catch((err) => {
|
|
||||||
res.json({
|
|
||||||
ok: false,
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/* global wiki */
|
|
||||||
|
|
||||||
const fs = require('fs')
|
|
||||||
const yaml = require('js-yaml')
|
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
|
||||||
const cfgHelper = require('../helpers/config')
|
const cfgHelper = require('../helpers/config')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const yaml = require('js-yaml')
|
||||||
|
|
||||||
|
/* global wiki */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
@ -1,11 +1,9 @@
|
|||||||
'use strict'
|
const _ = require('lodash')
|
||||||
|
|
||||||
/* global wiki */
|
|
||||||
|
|
||||||
const gqlTools = require('graphql-tools')
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const gqlTools = require('graphql-tools')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const _ = require('lodash')
|
|
||||||
|
/* global wiki */
|
||||||
|
|
||||||
const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8')
|
const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8')
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
const cluster = require('cluster')
|
const cluster = require('cluster')
|
||||||
const Promise = require('bluebird')
|
const Promise = require('bluebird')
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
/* global wiki */
|
/* global wiki */
|
||||||
|
|
@ -1,10 +1,8 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
/* global wiki */
|
|
||||||
|
|
||||||
const Bull = require('bull')
|
const Bull = require('bull')
|
||||||
const Promise = require('bluebird')
|
const Promise = require('bluebird')
|
||||||
|
|
||||||
|
/* global wiki */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init() {
|
init() {
|
||||||
wiki.data.queues.forEach(queueName => {
|
wiki.data.queues.forEach(queueName => {
|
@ -1,9 +1,8 @@
|
|||||||
/* global wiki */
|
|
||||||
|
|
||||||
const Promise = require('bluebird')
|
|
||||||
// const pm2 = Promise.promisifyAll(require('pm2'))
|
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const cfgHelper = require('../helpers/config')
|
const cfgHelper = require('../helpers/config')
|
||||||
|
const Promise = require('bluebird')
|
||||||
|
|
||||||
|
/* global wiki */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
@ -1,8 +1,8 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const bugsnag = require('bugsnag')
|
const bugsnag = require('bugsnag')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const uuid = require('uuid/v4')
|
const uuid = require('uuid/v4')
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
/* global wiki */
|
/* global wiki */
|
||||||
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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')))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,10 @@
|
|||||||
|
extends ../master.pug
|
||||||
|
|
||||||
|
block body
|
||||||
|
body
|
||||||
|
#app.is-fullscreen
|
||||||
|
.onboarding
|
||||||
|
img(src='/svg/logo-wikijs.svg', alt='Wiki.js')
|
||||||
|
h1= t('welcome.title')
|
||||||
|
h2= t('welcome.subtitle')
|
||||||
|
a.button.is-blue(href='/create/home')= t('welcome.createhome')
|
@ -1,11 +0,0 @@
|
|||||||
extends ../layout.pug
|
|
||||||
|
|
||||||
block rootNavCenter
|
|
||||||
|
|
||||||
block content
|
|
||||||
.container
|
|
||||||
.welcome
|
|
||||||
img(src='/images/logo.png', alt='Wiki.js')
|
|
||||||
h1= t('welcome.title')
|
|
||||||
h2= t('welcome.subtitle')
|
|
||||||
a.button.is-indigo(href='/create/home')= t('welcome.createhome')
|
|
Loading…
Reference in new issue