diff --git a/README.md b/README.md index 725ecfe0..6c4d9cf2 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) @@ -74,6 +75,11 @@ Support this project by becoming a sponsor. Your name will show up in the Contri + + + Jay Daley
(@JayDaley) +
+ Oleksii
(@idokka) @@ -84,18 +90,20 @@ Support this project by becoming a sponsor. Your name will show up in the Contri Theodore Chu
(@TheodoreChu)
- + - Akira Suenami ([@a-suenami](https://github.com/a-suenami)) +- Arnaud Marchand ([@snuids](https://github.com/snuids)) - Bryon Vandiver ([@asterick](https://github.com/asterick)) +- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer)) - Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC)) - CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin)) - David Christian Holin ([@SirGibihm](https://github.com/SirGibihm)) @@ -104,9 +112,11 @@ Support this project by becoming a sponsor. Your name will show up in the Contri - Ernie ([@iamernie](https://github.com/iamernie)) - Florian Moss ([@florianmoss](https://github.com/florianmoss)) - HeavenBay ([@HeavenBay](https://github.com/heavenbay)) +- Jaimyn Mayer ([@jabelone](https://github.com/jabelone)) - 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)) @@ -122,6 +132,7 @@ Support this project by becoming a sponsor. Your name will show up in the Contri - aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani)) - aytaa ([@aytaa](https://github.com/aytaa)) - magicpotato ([@fortheday](https://github.com/fortheday)) +- motoacs ([@motoacs](https://github.com/motoacs)) - scorpion ([@scorpion](https://github.com/scorpion)) - valantien ([@valantien](https://github.com/valantien)) @@ -230,6 +241,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 @@ -238,6 +250,7 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re - Florent - Günter Pavlas - hong +- Hope - Ian - Iskander Callos - Josh Stewart @@ -245,9 +258,11 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re - Keir - Loïc CRAMPON - Ludgeir Ibanez +- Mark Mansur - Matt Gedigian - Patryk - Philipp Schürch +- Tracey Duffy - Richeir - SmartNET.works - Stepan Sokolovskyi diff --git a/client/components/admin/admin-pages.vue b/client/components/admin/admin-pages.vue index 4bd5958c..0ca26de1 100644 --- a/client/components/admin/admin-pages.vue +++ b/client/components/admin/admin-pages.vue @@ -61,6 +61,7 @@ sort-by='updatedAt', sort-desc, hide-default-footer + @page-count="pageTotal = $event" ) template(slot='item', slot-scope='props') tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)') @@ -89,6 +90,7 @@ export default { selectedPage: {}, pagination: 1, pages: [], + pageTotal: 0, headers: [ { text: 'ID', value: 'id', width: 80, sortable: true }, { text: 'Title', value: 'title' }, @@ -108,9 +110,6 @@ export default { } }, computed: { - pageTotal () { - return Math.ceil(this.filteredPages.length / 15) - }, filteredPages () { return _.filter(this.pages, pg => { if (this.selectedLang !== null && this.selectedLang !== pg.locale) { diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index b02c40e8..42d0b2c5 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -564,11 +564,11 @@ export default { if (window.location.hash && window.location.hash.length > 1) { if (document.readyState === 'complete') { this.$nextTick(() => { - this.$vuetify.goTo(window.location.hash, this.scrollOpts) + this.$vuetify.goTo(decodeURIComponent(window.location.hash), this.scrollOpts) }) } else { window.addEventListener('load', () => { - this.$vuetify.goTo(window.location.hash, this.scrollOpts) + this.$vuetify.goTo(decodeURIComponent(window.location.hash), this.scrollOpts) }) } } @@ -579,7 +579,7 @@ export default { el.onclick = ev => { ev.preventDefault() ev.stopPropagation() - this.$vuetify.goTo(decodeURIComponent(ev.target.hash), this.scrollOpts) + this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts) } }) }) diff --git a/config.sample.yml b/config.sample.yml index c149f702..316dd603 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -43,6 +43,9 @@ db: # pfx: path/to/cert.pfx # passphrase: xyz123 + # Optional - PostgreSQL only: + schema: public + # SQLite only: storage: path/to/database.sqlite diff --git a/dev/helm/Chart.yaml b/dev/helm/Chart.yaml index e284c368..614473a0 100644 --- a/dev/helm/Chart.yaml +++ b/dev/helm/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: wiki # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 2.1.0 +version: 2.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. AppVersion: latest diff --git a/dev/helm/README.md b/dev/helm/README.md index 7d45fba1..9e6192db 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 @@ -159,4 +161,4 @@ See the [Configuration](#configuration) section to configure the PVC or to disab ## Ingress -This chart provides support for Ingress resource. If you have an available Ingress Controller such as Nginx or Traefik you maybe want to set `ingress.enabled` to true and choose an `ingress.hostname` for the URL. Then, you should be able to access the installation using that address. +This chart provides support for Ingress resource. If you have an available Ingress Controller such as Nginx or Traefik you maybe want to set `ingress.enabled` to true and add `ingress.hosts` for the URL. Then, you should be able to access the installation using that address. diff --git a/dev/helm/templates/deployment.yaml b/dev/helm/templates/deployment.yaml index 0c728770..6c6a2ec9 100644 --- a/dev/helm/templates/deployment.yaml +++ b/dev/helm/templates/deployment.yaml @@ -39,9 +39,9 @@ spec: - name: DB_USER value: {{ default "wiki" .Values.postgresql.postgresqlUser }} - name: DB_SSL - value: "{{ default "false" .Values.postgresql.ssl }}" + value: "{{ default "false" .Values.postgresql.ssl }}" - name: DB_SSL_CA - value: "{{ default "" .Values.postgresql.ca }}" + value: "{{ default "" .Values.postgresql.ca }}" - name: DB_PASS valueFrom: secretKeyRef: @@ -51,18 +51,16 @@ spec: name: {{ template "wiki.postgresql.secret" . }} {{- end }} key: {{ template "wiki.postgresql.secretKey" . }} + - name: HA + value: {{ .Values.replicaCount | int | le 2 | quote }} ports: - name: http containerPort: 3000 protocol: TCP livenessProbe: - httpGet: - path: /healthz - port: http + {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: - httpGet: - path: /healthz - port: http + {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} diff --git a/dev/helm/templates/ingress.yaml b/dev/helm/templates/ingress.yaml index 0b08134d..adf10b65 100644 --- a/dev/helm/templates/ingress.yaml +++ b/dev/helm/templates/ingress.yaml @@ -1,11 +1,18 @@ {{- if .Values.ingress.enabled -}} -{{- $fullName := include "wiki.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} + {{- $fullName := include "wiki.fullname" . -}} + {{- $svcPort := .Values.service.port -}} + {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} + {{- end }} + {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 + {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 -{{- else -}} + {{- else -}} apiVersion: extensions/v1beta1 -{{- end }} + {{- end }} kind: Ingress metadata: name: {{ $fullName }} @@ -31,11 +38,21 @@ spec: - host: {{ .host | quote }} http: paths: - {{- range .paths }} - - path: {{ . }} + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} serviceName: {{ $fullName }} servicePort: {{ $svcPort }} - {{- end }} + {{- end }} + {{- end }} + {{- end }} {{- end }} -{{- end }} diff --git a/dev/helm/values.yaml b/dev/helm/values.yaml index 87701fb2..bfa9fb3c 100644 --- a/dev/helm/values.yaml +++ b/dev/helm/values.yaml @@ -21,6 +21,16 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template name: +livenessProbe: + httpGet: + path: /healthz + port: http + +readinessProbe: + httpGet: + path: /healthz + port: http + podSecurityContext: {} # fsGroup: 2000 @@ -42,13 +52,16 @@ service: # annotations: {} ingress: - enabled: false + enabled: true annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - - host: wiki.local - paths: ["/"] + - host: wiki.minikube.local + paths: + - path: "/" + pathType: Prefix + tls: [] # - secretName: chart-example-tls # hosts: @@ -82,7 +95,7 @@ postgresql: enabled: true ## ssl enforce SSL communication with PostgresSQL ## Default to false - ## + ## # ssl: false ## ca Certificate of Authority ## Default to empty, point to location of CA @@ -92,7 +105,7 @@ postgresql: ## Default to postgres ## # postgresqlHost: postgres - ## postgresqlPort port for postgres + ## postgresqlPort port for postgres ## Default to 5432 ## # postgresqlPort: 5432 diff --git a/package.json b/package.json index 682b4138..587381db 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "node": ">=10.12" }, "dependencies": { - "@aoberoi/passport-slack": "1.0.5", "@azure/storage-blob": "12.2.1", "@exlinc/keycloak-passport": "1.0.2", "@joplin/turndown-plugin-gfm": "1.0.27", @@ -151,6 +150,7 @@ "passport-okta-oauth": "0.0.1", "passport-openidconnect": "0.0.2", "passport-saml": "1.3.5", + "passport-slack-oauth2": "1.1.1", "passport-twitch-oauth": "1.0.0", "pem-jwk": "2.0.0", "pg": "8.4.1", 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/db.js b/server/core/db.js index f1ac88b3..2d614c55 100644 --- a/server/core/db.js +++ b/server/core/db.js @@ -138,6 +138,10 @@ module.exports = { switch (WIKI.config.db.type) { case 'postgres': await conn.query(`set application_name = 'Wiki.js'`) + // -> Set schema if it's not public + if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') { + await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`) + } done() break case 'mysql': 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..9eec70a7 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,10 +28,11 @@ class Job { * @param {Object} data Job Data */ start(data) { + this.queue.jobs.push(this) if (this.immediate) { this.invoke(data) } else { - this.queue(data) + this.enqueue(data) } } @@ -39,7 +41,7 @@ class Job { * * @param {Object} data Job Data */ - queue(data) { + enqueue(data) { this.timeout = setTimeout(this.invoke.bind(this), this.schedule.asMilliseconds(), data) } @@ -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) { - this.queue(data) + if (this.repeat && this.queue.jobs.includes(this)) { + this.enqueue(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/middlewares/upload.js b/server/middlewares/upload.js deleted file mode 100644 index 15cd0d02..00000000 --- a/server/middlewares/upload.js +++ /dev/null @@ -1,6 +0,0 @@ -const { graphqlUploadExpress } = require('graphql-upload') - -/** - * GraphQL File Upload Middleware - */ -module.exports = graphqlUploadExpress({ maxFileSize: 5000000, maxFiles: 20 }) diff --git a/server/models/pages.js b/server/models/pages.js index abe2bc6e..cd786374 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -650,7 +650,15 @@ module.exports = class Page extends Model { * @returns {Promise} Promise with no value */ static async movePage(opts) { - const page = await WIKI.models.pages.query().findById(opts.id) + let page + if (_.has(opts, 'id')) { + page = await WIKI.models.pages.query().findById(opts.id) + } else { + page = await WIKI.models.pages.query().findOne({ + path: opts.path, + localeCode: opts.locale + }) + } if (!page) { throw new WIKI.Error.PageNotFound() } @@ -704,9 +712,11 @@ module.exports = class Page extends Model { const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' }) // -> Move page + const destinationTitle = (page.title === page.path ? opts.destinationPath : page.title) await WIKI.models.pages.query().patch({ path: opts.destinationPath, localeCode: opts.destinationLocale, + title: destinationTitle, hash: destinationHash }).findById(page.id) await WIKI.models.pages.deletePageFromCache(page.hash) @@ -775,7 +785,7 @@ module.exports = class Page extends Model { }) } if (!page) { - throw new Error('Invalid Page Id') + throw new WIKI.Error.PageNotFound() } // -> Check for page access diff --git a/server/modules/analytics/newrelic/code.yml b/server/modules/analytics/newrelic/code.yml index 366e4b19..06c5bb7e 100644 --- a/server/modules/analytics/newrelic/code.yml +++ b/server/modules/analytics/newrelic/code.yml @@ -1,5 +1,5 @@ head: | diff --git a/server/modules/analytics/newrelic/definition.yml b/server/modules/analytics/newrelic/definition.yml index e8dd2d45..01e92235 100644 --- a/server/modules/analytics/newrelic/definition.yml +++ b/server/modules/analytics/newrelic/definition.yml @@ -16,3 +16,15 @@ props: title: Application ID hint: Found at the very end of the code snippet provided by New Relic Browser order: 2 + beacon: + type: String + title: Beacon + default: bam.nr-data.net + hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers. + order: 3 + errorBeacon: + type: String + title: Error Beacon + default: bam.nr-data.net + hint: Found at the very end of the code snippet provided by New Relic Browser. Differs for US and EU servers. + order: 4 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/authentication/slack/authentication.js b/server/modules/authentication/slack/authentication.js index 3e4b48c1..984ce28a 100644 --- a/server/modules/authentication/slack/authentication.js +++ b/server/modules/authentication/slack/authentication.js @@ -4,7 +4,7 @@ // Slack Account // ------------------------------------ -const SlackStrategy = require('@aoberoi/passport-slack').default.Strategy +const SlackStrategy = require('passport-slack-oauth2').Strategy const _ = require('lodash') module.exports = { @@ -15,8 +15,9 @@ module.exports = { clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, team: conf.team, + scope: ['identity.basic', 'identity.email', 'identity.avatar'], passReqToCallback: true - }, async (req, accessToken, scopes, team, extra, { user: userProfile }, cb) => { + }, async (req, accessToken, refreshToken, { user: userProfile }, cb) => { try { const user = await WIKI.models.users.processProfile({ providerKey: req.params.strategy, 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) diff --git a/server/modules/storage/git/storage.js b/server/modules/storage/git/storage.js index c023c7e8..aff0c0d3 100644 --- a/server/modules/storage/git/storage.js +++ b/server/modules/storage/git/storage.js @@ -142,7 +142,9 @@ module.exports = { if (_.get(diff, 'files', []).length > 0) { let filesToProcess = [] for (const f of diff.files) { - const fPath = path.join(this.repoPath, f.file) + const fMoved = f.file.split(' => ') + const fName = fMoved.length === 2 ? fMoved[1] : fMoved[0] + const fPath = path.join(this.repoPath, fName) let fStats = { size: 0 } try { fStats = await fs.stat(fPath) @@ -159,7 +161,8 @@ module.exports = { path: fPath, stats: fStats }, - relPath: f.file + oldPath: fMoved[0], + relPath: fName }) } await this.processFiles(filesToProcess, rootUser) @@ -174,11 +177,25 @@ module.exports = { async processFiles(files, user) { for (const item of files) { const contentType = pageHelper.getContentType(item.relPath) - const fileExists = await fs.pathExists(item.file) + const fileExists = await fs.pathExists(item.file.path) if (!item.binary && contentType) { // -> Page - if (!fileExists && item.deletions > 0 && item.insertions === 0) { + if (fileExists && item.relPath !== item.oldPath) { + // Page was renamed by git, so rename in DB + WIKI.logger.info(`(STORAGE/GIT) Page marked as renamed: from ${item.oldPath} to ${item.relPath}`) + + const contentPath = pageHelper.getPagePath(item.oldPath) + const contentDestinationPath = pageHelper.getPagePath(item.relPath) + await WIKI.models.pages.movePage({ + user: user, + path: contentPath.path, + destinationPath: contentDestinationPath.path, + locale: contentPath.locale, + destinationLocale: contentPath.locale, + skipStorage: true + }) + } else if (!fileExists && item.deletions > 0 && item.insertions === 0) { // Page was deleted by git, can safely mark as deleted in DB WIKI.logger.info(`(STORAGE/GIT) Page marked as deleted: ${item.relPath}`) @@ -207,7 +224,23 @@ module.exports = { } else { // -> Asset - if (!fileExists && ((item.before > 0 && item.after === 0) || (item.deletions > 0 && item.insertions === 0))) { + if (fileExists && ((item.before === item.after) || (item.deletions === 0 && item.insertions === 0))) { + // Asset was renamed by git, so rename in DB + WIKI.logger.info(`(STORAGE/GIT) Asset marked as renamed: from ${item.oldPath} to ${item.relPath}`) + + const fileHash = assetHelper.generateHash(item.relPath) + const assetToRename = await WIKI.models.assets.query().findOne({ hash: fileHash }) + if (assetToRename) { + await WIKI.models.assets.query().patch({ + filename: item.relPath, + hash: fileHash + }).findById(assetToRename.id) + await assetToRename.deleteAssetCache() + } else { + WIKI.logger.info(`(STORAGE/GIT) Asset was not found in the DB, nothing to rename: ${item.relPath}`) + } + continue + } else if (!fileExists && ((item.before > 0 && item.after === 0) || (item.deletions > 0 && item.insertions === 0))) { // Asset was deleted by git, can safely mark as deleted in DB WIKI.logger.info(`(STORAGE/GIT) Asset marked as deleted: ${item.relPath}`) @@ -419,7 +452,7 @@ module.exports = { transform: async (page, enc, cb) => { const pageObject = await WIKI.models.pages.query().findById(page.id) page.tags = await pageObject.$relatedQuery('tags') - + let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}` if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { fileName = `${page.localeCode}/${fileName}` diff --git a/server/modules/storage/sftp/storage.js b/server/modules/storage/sftp/storage.js index e1f99af4..49d5ae1d 100644 --- a/server/modules/storage/sftp/storage.js +++ b/server/modules/storage/sftp/storage.js @@ -155,7 +155,12 @@ module.exports = { const folderPaths = _.dropRight(filePath.split('/')) for (let i = 1; i <= folderPaths.length; i++) { const folderSection = _.take(folderPaths, i).join('/') - await this.sftp.mkdir(path.posix.join(this.config.basePath, folderSection)) + const folderDir = path.posix.join(this.config.basePath, folderSection) + try { + await this.sftp.readdir(folderDir) + } catch (err) { + await this.sftp.mkdir(folderDir) + } } } catch (err) {} } diff --git a/yarn.lock b/yarn.lock index 858f674b..aacf1b04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,18 +106,6 @@ "@algolia/logger-common" "4.5.1" "@algolia/requester-common" "4.5.1" -"@aoberoi/passport-slack@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@aoberoi/passport-slack/-/passport-slack-1.0.5.tgz#08dcd2d951e94d8e2934bd567c01410a0cc42bec" - integrity sha1-CNzS2VHpTY4pNL1WfAFBCgzEK+w= - dependencies: - babel-polyfill "^6.16.0" - lodash.defaults "^4.2.0" - lodash.isfunction "^3.0.8" - lodash.pickby "^4.6.0" - needle "^1.4.2" - passport-oauth2 "^1.3.0" - "@apollo/client@^3.1.5": version "3.2.2" resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.2.2.tgz#fe5cad4d53373979f13a925e9da02d8743e798a5" @@ -5415,15 +5403,6 @@ babel-plugin-transform-imports@2.0.0: "@babel/types" "^7.4" is-valid-path "^0.1.1" -babel-polyfill@^6.16.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" - integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= - dependencies: - babel-runtime "^6.26.0" - core-js "^2.5.0" - regenerator-runtime "^0.10.5" - babel-preset-current-node-syntax@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" @@ -6989,7 +6968,7 @@ core-js@3.6.5, core-js@^3.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5: +core-js@^2.4.0, core-js@^2.6.5: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -8012,7 +7991,7 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -12364,11 +12343,6 @@ lodash.clonedeep@4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -12379,11 +12353,6 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= -lodash.isfunction@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -12419,11 +12388,6 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.pickby@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" - integrity sha1-feoh2MGNdwOifHBMFdO4SmfjOv8= - lodash.repeat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44" @@ -13299,14 +13263,6 @@ ncp@~2.0.0: resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= -needle@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-1.6.0.tgz#f52a5858972121618e002f8e6384cadac22d624f" - integrity sha1-9SpYWJchIWGOAC+OY4TK2sItYk8= - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - needle@^2.2.1: version "2.4.0" resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" @@ -14298,7 +14254,7 @@ passport-oauth2@1.2.0: passport-strategy "1.x.x" uid2 "0.0.x" -passport-oauth2@1.5.0, passport-oauth2@1.x.x, passport-oauth2@^1.2.0, passport-oauth2@^1.3.0, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: +passport-oauth2@1.5.0, passport-oauth2@1.x.x, passport-oauth2@^1.2.0, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== @@ -14350,6 +14306,14 @@ passport-saml@1.3.5: xmlbuilder "^11.0.0" xmldom "0.1.x" +passport-slack-oauth2@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/passport-slack-oauth2/-/passport-slack-oauth2-1.1.1.tgz#d831ffc3f1e968fcc3622e6ecf41643c8d8f9cbc" + integrity sha512-xC+yMKFXximP5TzSNt4lr9TP78MMos5B+acC7bJNCxBAVNyL9e02AEpVpVtyMIqHv4nNZnv1vyoOb50J8VCcZQ== + dependencies: + passport-oauth2 "^1.5.0" + pkginfo "^0.4.1" + passport-strategy@*, passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" @@ -14732,6 +14696,11 @@ pkginfo@0.2.x, pkginfo@^0.2.3: resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8" integrity sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg= +pkginfo@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" + integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= + pleeease-filters@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-4.0.0.tgz#6632b2fb05648d2758d865384fbced79e1ccaec7" @@ -16479,11 +16448,6 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== -regenerator-runtime@^0.10.5: - version "0.10.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" - integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= - regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"