diff --git a/README.md b/README.md index 725ecfe0..22f25dd6 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,14 @@ [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack) [![Twitter Follow](https://img.shields.io/badge/follow-%40requarks-blue.svg?style=flat&logo=twitter)](https://twitter.com/requarks) +[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/) [![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe) ##### A modern, lightweight and powerful wiki app built on NodeJS -- **[Official Website](https://wiki.js.org/)** +- **[Official Website](https://js.wiki/)** - **[Documentation](https://docs.requarks.io/)** - [Requirements](https://docs.requarks.io/install/requirements) - [Installation](https://docs.requarks.io/install) @@ -107,6 +108,7 @@ Support this project by becoming a sponsor. Your name will show up in the Contri - Jay Lee ([@polyglotm](https://github.com/polyglotm)) - Kelly Wardrop ([@dropcoded](https://github.com/dropcoded)) - Loki ([@binaryloki](https://github.com/binaryloki)) +- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto)) - Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson)) - Mitchell Rowton ([@mrowton](https://github.com/mrowton)) - M. Scott Ford ([@mscottford](https://github.com/mscottford)) @@ -230,6 +232,7 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re - Alex Zen - Arti Zirk - Brandon Curtis +- Dave 'Sri' Seah - djagoo - Douglas Lassance - Ernie Reid @@ -248,6 +251,7 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re - Matt Gedigian - Patryk - Philipp Schürch +- Tracey Duffy - Richeir - SmartNET.works - Stepan Sokolovskyi diff --git a/dev/helm/README.md b/dev/helm/README.md index 7d45fba1..00f11570 100644 --- a/dev/helm/README.md +++ b/dev/helm/README.md @@ -107,6 +107,8 @@ The following table lists the configurable parameters of the Wiki.js chart and t | `postgresql.postgresqlUser` | Postgres username | `postgres` | | `postgresql.postgresqlHost` | External postgres host | `nil` | | `postgresql.postgresqlPassword` | External postgres password | `nil` | +| `postgresql.existingSecret` | Provide an existing `Secret` for postgres | `nil` | +| `postgresql.existingSecretKey` | The postgres password key in the existing `Secret` | `postgresql-password` | | `postgresql.postgresqlPort` | External postgres port | `5432` | | `postgresql.ssl` | Enable external postgres SSL connection | `false` | | `postgresql.ca` | Certificate of Authority path for postgres | `nil` | @@ -137,11 +139,11 @@ By default, PostgreSQL is installed as part of the chart. ### Using an external PostgreSQL server -To use an external PostgreSQL server, set `postgresql.enabled` to `false` and then set `postgresql.postgresqlHost` and `postgresql.postgresqlPassword`. The other options (`postgresql.postgresqlDatabase`, `postgresql.postgresqlUser` and `postgresql.postgresqlPort`) may also want changing from their default values. +To use an external PostgreSQL server, set `postgresql.enabled` to `false` and then set `postgresql.postgresqlHost` and `postgresql.postgresqlPassword`. To use an existing `Secret`, set `postgresql.existingSecret`. The other options (`postgresql.postgresqlDatabase`, `postgresql.postgresqlUser`, `postgresql.postgresqlPort` and `postgresql.existingSecretKey`) may also want changing from their default values. To use an SSL connection you can set `postgresql.ssl` to `true` and if needed the path to a Certificate of Authority can be set using `postgresql.ca` to `/path/to/ca`. Default `postgresql.ssl` value is `false`. -You also need to add the follow Helm template to your deployment: +If `postgresql.existingSecret` is not specified, you also need to add the following Helm template to your deployment in order to create the postgresql `Secret`: ```yaml kind: Secret diff --git a/server/controllers/upload.js b/server/controllers/upload.js index c6a3685d..3da4dcac 100644 --- a/server/controllers/upload.js +++ b/server/controllers/upload.js @@ -76,7 +76,7 @@ router.post('/u', (req, res, next) => { } // Sanitize filename - fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\s,;]+/g, '_')) + fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\s,;#]+/g, '_')) // Check if user can upload at path const assetPath = (folderId) ? hierarchy.map(h => h.slug).join('/') + `/${fileMeta.originalname}` : fileMeta.originalname diff --git a/server/core/asar.js b/server/core/asar.js index 6c76468d..25099016 100644 --- a/server/core/asar.js +++ b/server/core/asar.js @@ -40,11 +40,11 @@ module.exports = { } }, async unload () { - if (this.fdCache) { + const fds = Object.values(this.fdCache) + if (fds.length > 0) { WIKI.logger.info('Closing ASAR file descriptors...') - for (const fdItem in this.fdCache) { - fs.closeSync(this.fdCache[fdItem].fd) - } + const closeAsync = require('util').promisify(fs.close) + await Promise.all(fds.map(x => closeAsync(x.fd))) this.fdCache = {} } }, diff --git a/server/core/kernel.js b/server/core/kernel.js index af22c902..6dcb17cf 100644 --- a/server/core/kernel.js +++ b/server/core/kernel.js @@ -107,19 +107,21 @@ module.exports = { * Graceful shutdown */ async shutdown () { - if (WIKI.models) { - await WIKI.models.unsubscribeToNotifications() - await WIKI.models.knex.client.pool.destroy() - await WIKI.models.knex.destroy() + if (WIKI.servers) { + await WIKI.servers.stopServers() } if (WIKI.scheduler) { - WIKI.scheduler.stop() + await WIKI.scheduler.stop() + } + if (WIKI.models) { + await WIKI.models.unsubscribeToNotifications() + if (WIKI.models.knex) { + await WIKI.models.knex.destroy() + } } if (WIKI.asar) { await WIKI.asar.unload() } - if (WIKI.servers) { - await WIKI.servers.stopServers() - } + process.exit(0) } } diff --git a/server/core/scheduler.js b/server/core/scheduler.js index 34c4e1f6..67fdde58 100644 --- a/server/core/scheduler.js +++ b/server/core/scheduler.js @@ -12,7 +12,8 @@ class Job { schedule = 'P1D', repeat = false, worker = false - }) { + }, queue) { + this.queue = queue this.finished = Promise.resolve() this.name = name this.immediate = immediate @@ -27,6 +28,7 @@ class Job { * @param {Object} data Job Data */ start(data) { + this.queue.jobs.push(this) if (this.immediate) { this.invoke(data) } else { @@ -55,14 +57,22 @@ class Job { `--job=${this.name}`, `--data=${data}` ], { - cwd: WIKI.ROOTPATH + cwd: WIKI.ROOTPATH, + stdio: ['inherit', 'inherit', 'pipe', 'ipc'] }) + const stderr = []; + proc.stderr.on('data', chunk => stderr.push(chunk)) this.finished = new Promise((resolve, reject) => { proc.on('exit', (code, signal) => { + const data = Buffer.concat(stderr).toString() if (code === 0) { - resolve() + resolve(data) } else { - reject(signal) + const err = new Error(`Error when running job ${this.name}: ${data}`) + err.exitSignal = signal + err.exitCode = code + err.stderr = data + reject(err) } proc.kill() }) @@ -74,16 +84,20 @@ class Job { } catch (err) { WIKI.logger.warn(err) } - if (this.repeat) { + if (this.repeat && this.queue.jobs.includes(this)) { this.queue(data) + } else { + this.stop().catch(() => {}) } } /** * Stop any future job invocation from occuring */ - stop() { + async stop() { clearTimeout(this.timeout) + this.queue.jobs = this.queue.jobs.filter(x => x !== this) + return this.finished } } @@ -110,16 +124,11 @@ module.exports = { }) }, registerJob(opts, data) { - const job = new Job(opts) + const job = new Job(opts, this) job.start(data) - if (job.repeat) { - this.jobs.push(job) - } return job }, - stop() { - this.jobs.forEach(job => { - job.stop() - }) + async stop() { + return Promise.all(this.jobs.map(job => job.stop())) } } diff --git a/server/core/worker.js b/server/core/worker.js index bb983c07..d648a678 100644 --- a/server/core/worker.js +++ b/server/core/worker.js @@ -14,6 +14,11 @@ WIKI.logger = require('./logger').init('JOB') const args = require('yargs').argv ;(async () => { - await require(`../jobs/${args.job}`)(args.data) - process.exit(0) + try { + await require(`../jobs/${args.job}`)(args.data) + process.exit(0) + } catch (e) { + await new Promise(resolve => process.stderr.write(e.message, resolve)) + process.exit(1) + } })() diff --git a/server/index.js b/server/index.js index 254335c9..87eff16b 100644 --- a/server/index.js +++ b/server/index.js @@ -33,3 +33,16 @@ WIKI.logger = require('./core/logger').init('MASTER') // ---------------------------------------- WIKI.kernel.init() + +// ---------------------------------------- +// Register exit handler +// ---------------------------------------- + +process.on('SIGINT', () => { + WIKI.kernel.shutdown() +}) +process.on('message', (msg) => { + if (msg === 'shutdown') { + WIKI.kernel.shutdown() + } +}) diff --git a/server/jobs/rebuild-tree.js b/server/jobs/rebuild-tree.js index 53d3898a..c2fc3728 100644 --- a/server/jobs/rebuild-tree.js +++ b/server/jobs/rebuild-tree.js @@ -74,5 +74,7 @@ module.exports = async (pageId) => { } catch (err) { WIKI.logger.error(`Rebuilding page tree: [ FAILED ]`) WIKI.logger.error(err.message) + // exit process with error code + throw err } } diff --git a/server/jobs/render-page.js b/server/jobs/render-page.js index 8f4bc45a..3a88b375 100644 --- a/server/jobs/render-page.js +++ b/server/jobs/render-page.js @@ -90,5 +90,7 @@ module.exports = async (pageId) => { } catch (err) { WIKI.logger.error(`Rendering page ID ${pageId}: [ FAILED ]`) WIKI.logger.error(err.message) + // exit process with error code + throw err } } diff --git a/server/modules/authentication/google/authentication.js b/server/modules/authentication/google/authentication.js index 23eb40af..d7ba3b32 100644 --- a/server/modules/authentication/google/authentication.js +++ b/server/modules/authentication/google/authentication.js @@ -9,27 +9,38 @@ const _ = require('lodash') module.exports = { init (passport, conf) { - passport.use('google', - new GoogleStrategy({ - clientID: conf.clientId, - clientSecret: conf.clientSecret, - callbackURL: conf.callbackURL, - passReqToCallback: true - }, async (req, accessToken, refreshToken, profile, cb) => { - try { - const user = await WIKI.models.users.processProfile({ - providerKey: req.params.strategy, - profile: { - ...profile, - picture: _.get(profile, 'photos[0].value', '') - } - }) - cb(null, user) - } catch (err) { - cb(err, null) + const strategy = new GoogleStrategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + passReqToCallback: true + }, async (req, accessToken, refreshToken, profile, cb) => { + try { + if (conf.hostedDomain && conf.hostedDomain != profile._json.hd) { + throw new Error('Google authentication should have been performed with domain ' + conf.hostedDomain) } - }) - ) + const user = await WIKI.models.users.processProfile({ + providerKey: req.params.strategy, + profile: { + ...profile, + picture: _.get(profile, 'photos[0].value', '') + } + }) + cb(null, user) + } catch (err) { + cb(err, null) + } + }) + + if (conf.hostedDomain) { + strategy.authorizationParams = function(options) { + return { + hd: conf.hostedDomain + } + } + } + + passport.use('google', strategy) }, logout (conf) { return '/' diff --git a/server/modules/authentication/google/definition.yml b/server/modules/authentication/google/definition.yml index 70f2892d..51747c37 100644 --- a/server/modules/authentication/google/definition.yml +++ b/server/modules/authentication/google/definition.yml @@ -22,3 +22,8 @@ props: title: Client Secret hint: Application Client Secret order: 2 + hostedDomain: + type: String + title: Hosted Domain + hint: (optional) Only for G Suite hosted domain. Leave empty otherwise. + order: 3 diff --git a/server/modules/rendering/html-core/renderer.js b/server/modules/rendering/html-core/renderer.js index faa3549a..6ec35a66 100644 --- a/server/modules/rendering/html-core/renderer.js +++ b/server/modules/rendering/html-core/renderer.js @@ -237,7 +237,7 @@ module.exports = { // -------------------------------- $('body').contents().toArray().forEach(item => { - if (item.type === 'text' && item.parent.name === 'body') { + if (item && item.type === 'text' && item.parent.name === 'body') { $(item).wrap('
') } }) @@ -249,7 +249,7 @@ module.exports = { function iterateMustacheNode (node) { const list = $(node).contents().toArray() list.forEach(item => { - if (item.type === 'text') { + if (item && item.type === 'text') { const rawText = $(item).text().replace(/\r?\n|\r/g, '') if (mustacheRegExp.test(rawText)) { $(item).parent().attr('v-pre', true)