Merge remote-tracking branch 'requarks/main' into tyclipso

pull/7734/head
Carl Richter 4 years ago
commit 474a026f59

@ -25,7 +25,7 @@
- [Requirements](https://docs.requarks.io/install/requirements)
- [Installation](https://docs.requarks.io/install)
- [Demo](https://docs.requarks.io/demo)
- [Changelog](https://docs.requarks.io/releases)
- [Changelog](https://github.com/requarks/wiki/releases)
- [Feature Requests](https://feedback.js.wiki/wiki)
- [Chat with us on Slack](https://wiki.requarks.io/slack)
- [Translations](https://docs.requarks.io/dev/translations) *(We need your help!)*
@ -141,6 +141,7 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Brian Douglass ([@bhdouglass](https://github.com/bhdouglass))
- Bryon Vandiver ([@asterick](https://github.com/asterick))
- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer))
- Charlie Schliesser ([@charlie-s](https://github.com/charlie-s))
- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))
- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))
- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))
@ -159,17 +160,20 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Loki ([@binaryloki](https://github.com/binaryloki))
- MaFarine ([@MaFarine](https://github.com/MaFarine))
- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))
- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
</td><td>
<img width="441" height="1" />
- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))
- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
- M. Scott Ford ([@mscottford](https://github.com/mscottford))
- Nick Halase ([@nhalase](https://github.com/nhalase))
- Nick Price ([@DominoTree](https://github.com/DominoTree))
- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
- Noel Cower ([@nilium](https://github.com/nilium))
- Oleksandr Koltsov ([@crambo](https://github.com/crambo))
- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))
- Robert Lanzke ([@winkelement](https://github.com/winkelement))
- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))
@ -181,6 +185,7 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))
- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
- aytaa ([@aytaa](https://github.com/aytaa))
- chaee ([@chaee](https://github.com/chaee))
- magicpotato ([@fortheday](https://github.com/fortheday))
- motoacs ([@motoacs](https://github.com/motoacs))
- rburckner ([@rburckner](https://github.com/rburckner))
@ -316,6 +321,23 @@ Support this project by becoming a sponsor. Your logo will show up in the Contri
<a href="https://opencollective.com/wikijs/sponsor/34/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/34/avatar.svg"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/35/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/35/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/36/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/36/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/37/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/37/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/38/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/38/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/39/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/39/avatar.svg"></a>
</td>
</tr>
</tbody>
</table>
</div>
@ -335,6 +357,7 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Brandon Curtis
- Dave 'Sri' Seah
- djagoo
- dz
- Douglas Lassance
- Ernie Reid
- Etienne
@ -344,6 +367,7 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- hong
- Hope
- Ian
- Imari Childress
</td><td>
<img width="441" height="1" />
@ -356,10 +380,12 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Ludgeir Ibanez
- Mark Mansur
- Matt Gedigian
- Nate Figz
- Patryk
- Philipp Schürch
- Tracey Duffy
- Richeir
- Shad Narcher
- SmartNET.works
- Stepan Sokolovskyi
- Zach Maynard

@ -558,7 +558,7 @@ export default {
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
{ text: '(GMT+02:00) Kiev', value: 'Europe/Kiev' },
{ text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },
{ text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },

@ -0,0 +1,272 @@
<template lang='pug'>
v-card
v-toolbar(flat, color='primary', dark, dense)
.subtitle-1 {{ $t('admin:utilities.exportTitle') }}
v-card-text
.text-center
img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-big-parcel.svg')
.body-2 Export to tarball / file system
v-divider.my-4
.body-2 What do you want to export?
v-checkbox(
v-for='choice of entityChoices'
:key='choice.key'
:label='choice.label'
:value='choice.key'
color='deep-orange darken-2'
hide-details
v-model='entities'
)
template(v-slot:label)
div
strong.deep-orange--text.text--darken-2 {{choice.label}}
.text-caption {{choice.hint}}
v-text-field.mt-7(
outlined
label='Target Folder Path'
hint='Either an absolute path or relative to the Wiki.js installation folder, where exported content will be saved to. Note that the folder MUST be empty!'
persistent-hint
v-model='filePath'
)
v-alert.mt-3(color='deep-orange', outlined, icon='mdi-alert', prominent)
.body-2 Depending on your selection, the archive could contain sensitive data such as site configuration keys and hashed user passwords. Ensure the exported archive is treated accordingly.
.body-2 For example, you may want to encrypt the archive if stored for backup purposes.
v-card-chin
v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='entities.length < 1', @click='startExport').ml-0
v-icon(left, color='white') mdi-database-export
span.white--text Start Export
v-dialog(
v-model='isLoading'
persistent
max-width='350'
)
v-card(color='deep-orange darken-2', dark)
v-card-text.pa-10.text-center
self-building-square-spinner.animated.fadeIn(
:animation-duration='4500'
:size='40'
color='#FFF'
style='margin: 0 auto;'
)
.mt-5.body-1.white--text Exporting...
.caption Please wait, this may take a while
v-progress-linear.mt-5(
color='white'
:value='progress'
stream
rounded
:buffer-value='0'
)
v-dialog(
v-model='isSuccess'
persistent
max-width='350'
)
v-card(color='green darken-2', dark)
v-card-text.pa-10.text-center
v-icon(size='60') mdi-check-circle-outline
.my-5.body-1.white--text Export completed
v-card-actions.green.darken-1
v-spacer
v-btn.px-5(
color='white'
outlined
@click='isSuccess = false'
) Close
v-spacer
v-dialog(
v-model='isFailed'
persistent
max-width='800'
)
v-card(color='red darken-2', dark)
v-toolbar(color='red darken-2', dense)
v-icon mdi-alert
.body-2.pl-3 Export failed
v-spacer
v-btn.px-5(
color='white'
text
@click='isFailed = false'
) Close
v-card-text.pa-5.red.darken-4.white--text
span {{errorMessage}}
</template>
<script>
import { SelfBuildingSquareSpinner } from 'epic-spinners'
import gql from 'graphql-tag'
import _get from 'lodash/get'
export default {
components: {
SelfBuildingSquareSpinner
},
data() {
return {
entities: [],
filePath: './data/export',
isLoading: false,
isSuccess: false,
isFailed: false,
errorMessage: '',
progress: 0
}
},
computed: {
entityChoices () {
return [
{
key: 'assets',
label: 'Assets',
hint: 'Media files such as images, documents, etc.'
},
{
key: 'comments',
label: 'Comments',
hint: 'Comments made using the default comment module only.'
},
{
key: 'navigation',
label: 'Navigation',
hint: 'Sidebar links when using Static or Custom Navigation.'
},
{
key: 'pages',
label: 'Pages',
hint: 'Page content, tags and related metadata.'
},
{
key: 'history',
label: 'Pages History',
hint: 'All previous versions of pages and their related metadata.'
},
{
key: 'settings',
label: 'Settings',
hint: 'Site configuration and modules settings.'
},
{
key: 'groups',
label: 'User Groups',
hint: 'Group permissions and page rules.'
},
{
key: 'users',
label: 'Users',
hint: 'Users metadata and their group memberships.'
}
]
}
},
methods: {
async checkProgress () {
try {
const respStatus = await this.$apollo.query({
query: gql`
{
system {
exportStatus {
status
progress
message
startedAt
}
}
}
`,
fetchPolicy: 'network-only'
})
const respStatusObj = _get(respStatus, 'data.system.exportStatus', {})
if (!respStatusObj) {
throw new Error('An unexpected error occured.')
} else {
switch (respStatusObj.status) {
case 'error': {
throw new Error(respStatusObj.message || 'An unexpected error occured.')
}
case 'running': {
this.progress = respStatusObj.progress || 0
window.requestAnimationFrame(() => {
setTimeout(() => {
this.checkProgress()
}, 5000)
})
break
}
case 'success': {
this.isLoading = false
this.isSuccess = true
break
}
default: {
throw new Error('Invalid export status.')
}
}
}
} catch (err) {
this.errorMessage = err.message
this.isLoading = false
this.isFailed = true
}
},
async startExport () {
this.isFailed = false
this.isSuccess = false
this.isLoading = true
this.progress = 0
setTimeout(async () => {
try {
// -> Initiate export
const respExport = await this.$apollo.mutate({
mutation: gql`
mutation (
$entities: [String]!
$path: String!
) {
system {
export (
entities: $entities
path: $path
) {
responseResult {
succeeded
message
}
}
}
}
`,
variables: {
entities: this.entities,
path: this.filePath
}
})
const respExportObj = _get(respExport, 'data.system.export', {})
if (!_get(respExportObj, 'responseResult.succeeded', false)) {
this.errorMessage = _get(respExportObj, 'responseResult.message', 'An unexpected error occurred')
this.isLoading = false
this.isFailed = true
return
}
// -> Check for progress
this.checkProgress()
} catch (err) {
this.$store.commit('pushGraphError', err)
this.isLoading = false
}
}, 1500)
}
}
}
</script>
<style lang='scss'>
</style>

@ -37,6 +37,7 @@ export default {
UtilityAuth: () => import(/* webpackChunkName: "admin" */ './admin-utilities-auth.vue'),
UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'),
UtilityCache: () => import(/* webpackChunkName: "admin" */ './admin-utilities-cache.vue'),
UtilityExport: () => import(/* webpackChunkName: "admin" */ './admin-utilities-export.vue'),
UtilityImportv1: () => import(/* webpackChunkName: "admin" */ './admin-utilities-importv1.vue'),
UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue')
},
@ -56,6 +57,12 @@ export default {
i18nKey: 'content',
isAvailable: true
},
{
key: 'UtilityExport',
icon: 'mdi-database-export',
i18nKey: 'export',
isAvailable: true
},
{
key: 'UtilityCache',
icon: 'mdi-database-refresh',

@ -14,7 +14,7 @@
.search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')
img(src='/_assets/svg/icon-no-results.svg', alt='No Results')
.subheading {{$t('common:header.searchNoResult')}}
template(v-if='results && results.length > 0')
template(v-if='search && search.length >= 2 && results && results.length > 0')
v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}
v-list.search-results-items.radius-7.py-0(two-line, dense)
template(v-for='(item, idx) of results')
@ -101,8 +101,6 @@ export default {
search(newValue, oldValue) {
this.cursor = 0
if (!newValue || (newValue && newValue.length < 2)) {
this.response.results = []
this.response.suggestions = []
this.searchIsLoading = false
} else {
this.searchIsLoading = true

@ -528,7 +528,7 @@ export default {
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
{ text: '(GMT+02:00) Kiev', value: 'Europe/Kiev' },
{ text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },
{ text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="144px" height="144px"><linearGradient id="rwH3R4FXAjAwf7QMo6soOa" x1="24.523" x2="39.672" y1="7.827" y2="22.933" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c26715"/><stop offset=".508" stop-color="#b85515"/><stop offset="1" stop-color="#ad3f16"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOa)" d="M42,17H15V6h26c0.552,0,1,0.448,1,1V17z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOb" x1="7.292" x2="27.973" y1="1.98" y2="18.107" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba84b"/><stop offset="1" stop-color="#d97218"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOb)" d="M32,17H7c-0.552,0-1-0.448-1-1V7c0-0.552,0.448-1,1-1h25c0.552,0,1,0.448,1,1v9 C33,16.552,32.552,17,32,17z"/><path d="M42,14H6v2c0,0.552,0.448,1,1,1h8h17h10V14z" opacity=".05"/><path d="M42,14.5H6V16c0,0.552,0.448,1,1,1h8h17h10V14.5z" opacity=".07"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOc" x1="27.534" x2="46.45" y1="492.536" y2="512.013" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOc)" d="M42,42H31V15h12c0.552,0,1,0.448,1,1v24C44,41.105,43.105,42,42,42z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOd" x1="5.418" x2="31.69" y1="488.435" y2="515.487" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOd)" d="M31,42H6c-1.105,0-2-0.895-2-2V16c0-0.552,0.448-1,1-1h28v25C33,41.105,32.105,42,31,42z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOe" x1="17.154" x2="17.154" y1="494.74" y2="463.029" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOe)" d="M33,15H4.618c-0.379,0-0.725,0.214-0.894,0.553l-2.362,4.724C1.196,20.609,1.437,21,1.809,21 h27.573c0.379,0,0.725-0.214,0.894-0.553L33,15z"/><linearGradient id="rwH3R4FXAjAwf7QMo6soOf" x1="39.846" x2="39.846" y1="494.729" y2="490.572" gradientTransform="translate(0 -474)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#rwH3R4FXAjAwf7QMo6soOf)" d="M33,15h10.382c0.379,0,0.725,0.214,0.894,0.553l2.362,4.724 C46.804,20.609,46.563,21,46.191,21h-9.573c-0.379,0-0.725-0.214-0.894-0.553L33,15z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -51,7 +51,7 @@
v-icon(small) mdi-folder-open
v-list-item-title {{ item.title }}
v-divider.mt-2
v-list-item.mt-2(v-if='currentParent.pageId > 0', :href='`/` + currentParent.path', :key='`directorypage-` + currentParent.id', :input-value='path === currentParent.path')
v-list-item.mt-2(v-if='currentParent.pageId > 0', :href='`/` + currentParent.locale + `/` + currentParent.path', :key='`directorypage-` + currentParent.id', :input-value='path === currentParent.path')
v-list-item-avatar(size='24')
v-icon mdi-text-box
v-list-item-title {{ currentParent.title }}

@ -1,12 +1,16 @@
<template lang="pug">
.tabset.elevation-2
ul.tabset-tabs(ref='tabs')
ul.tabset-tabs(ref='tabs', role='tablist')
slot(name='tabs')
.tabset-content(ref='content')
slot(name='content')
</template>
<script>
import { customAlphabet } from 'nanoid/non-secure'
const nanoid = customAlphabet('1234567890abcdef', 10)
export default {
data() {
return {
@ -23,15 +27,19 @@ export default {
this.$refs.tabs.childNodes.forEach((node, idx) => {
if (idx === this.currentTab) {
node.className = 'is-active'
node.setAttribute('aria-selected', 'true')
} else {
node.className = ''
node.setAttribute('aria-selected', 'false')
}
})
this.$refs.content.childNodes.forEach((node, idx) => {
if (idx === this.currentTab) {
node.className = 'tabset-panel is-active'
node.removeAttribute('hidden')
} else {
node.className = 'tabset-panel'
node.setAttribute('hidden', '')
}
})
}
@ -53,10 +61,43 @@ export default {
this.setActiveTab()
const tabRefId = nanoid()
this.$refs.tabs.childNodes.forEach((node, idx) => {
node.setAttribute('id', `${tabRefId}-${idx}`)
node.setAttribute('role', 'tab')
node.setAttribute('aria-controls', `${tabRefId}-${idx}-tab`)
node.setAttribute('tabindex', '0')
node.addEventListener('click', ev => {
this.currentTab = [].indexOf.call(ev.target.parentNode.children, ev.target)
})
node.addEventListener('keydown', ev => {
if (ev.key === 'ArrowLeft' && idx > 0) {
this.currentTab = idx - 1
this.$refs.tabs.childNodes[idx - 1].focus()
} else if (ev.key === 'ArrowRight' && idx < this.$refs.tabs.childNodes.length - 1) {
this.currentTab = idx + 1
this.$refs.tabs.childNodes[idx + 1].focus()
} else if (ev.key === 'Enter' || ev.key === ' ') {
this.currentTab = idx
node.focus()
} else if (ev.key === 'Home') {
this.currentTab = 0
ev.preventDefault()
ev.target.parentNode.children[0].focus()
} else if (ev.key === 'End') {
this.currentTab = this.$refs.tabs.childNodes.length - 1
ev.preventDefault()
ev.target.parentNode.children[this.$refs.tabs.childNodes.length - 1].focus()
}
})
})
this.$refs.content.childNodes.forEach((node, idx) => {
node.setAttribute('id', `${tabRefId}-${idx}-tab`)
node.setAttribute('role', 'tabpanel')
node.setAttribute('aria-labelledby', `${tabRefId}-${idx}`)
node.setAttribute('tabindex', '0')
})
}
}
@ -106,7 +147,9 @@ export default {
background-color: #FFF;
margin-bottom: 0;
padding-bottom: 17px;
padding-top: 13px;
color: mc('blue', '700');
border-top: 3px solid mc('blue', '700');
@at-root .theme--dark & {
background-color: #292929;

@ -1,7 +1,9 @@
# =========================
# --- BUILD NPM MODULES ---
# =========================
FROM node:14 AS build
FROM node:16-alpine AS build
RUN apk add yarn g++ make cmake python3 --no-cache
WORKDIR /wiki
@ -12,7 +14,7 @@ RUN yarn --production --frozen-lockfile --non-interactive --network-timeout 1000
# ===============
# --- Release ---
# ===============
FROM node:14-alpine
FROM node:16-alpine
LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \

@ -3,7 +3,7 @@
# ====================
FROM node:16-alpine AS assets
RUN apk add yarn g++ make --no-cache
RUN apk add yarn g++ make cmake python3 --no-cache
WORKDIR /wiki

@ -98,6 +98,7 @@ The following table lists the configurable parameters of the Wiki.js chart and t
| `image.tag` | Wiki.js image tag | `latest` |
| `imagePullPolicy` | Image pull policy | `IfNotPresent` |
| `replicacount` | Amount of wiki.js service pods to run | `1` |
| `revisionHistoryLimit` | Total amount of revision history points | `10` |
| `resources.limits` | wiki.js service resource limits | `nil` |
| `resources.requests` | wiki.js service resource requests | `nil` |
| `nodeSelector` | Node labels for wiki.js pod assignment | `{}` |
@ -107,6 +108,7 @@ The following table lists the configurable parameters of the Wiki.js chart and t
| `volumeMounts` | Volume mounts for Wiki.js container | `[]` |
| `volumes` | Volumes for Wiki.js Pod | `[]` |
| `ingress.enabled` | Enable ingress controller resource | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` |
| `ingress.hosts` | List of ingress rules | `[{"host": "wiki.local", "paths": ["/"]}]` |
| `ingress.tls` | Ingress TLS configuration | `[]` |

@ -6,6 +6,7 @@ metadata:
{{- include "wiki.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "wiki.selectorLabels" . | nindent 6 }}

@ -23,6 +23,9 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}

@ -3,6 +3,7 @@
# Declare variables to be passed into your templates.
replicaCount: 1
revisionHistoryLimit: 10
image:
repository: requarks/wiki
@ -53,6 +54,7 @@ service:
ingress:
enabled: true
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"

@ -60,7 +60,7 @@ const init = {
},
async reload() {
console.warn(chalk.yellow('--- Gracefully stopping server...'))
await global.WIKI.kernel.shutdown()
await global.WIKI.kernel.shutdown(true)
console.warn(chalk.yellow('--- Purging node modules cache...'))

@ -14,6 +14,4 @@ docker network create wikinet
docker volume create pgdata
docker create --name=db -e POSTGRES_DB=wiki -e POSTGRES_USER=wiki -e POSTGRES_PASSWORD_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -v pgdata:/var/lib/postgresql/data --restart=unless-stopped -h db --network=wikinet postgres:11
docker create --name=wiki -e DB_TYPE=postgres -e DB_HOST=db -e DB_PORT=5432 -e DB_PASS_FILE=/etc/wiki/.db-secret -v /etc/wiki/.db-secret:/etc/wiki/.db-secret:ro -e DB_USER=wiki -e DB_NAME=wiki -e UPGRADE_COMPANION=1 --restart=unless-stopped -h wiki --network=wikinet -p 80:3000 -p 443:3443 ghcr.io/requarks/wiki:2
docker create --name=wiki-update-companion -v /var/run/docker.sock:/var/run/docker.sock:ro --restart=unless-stopped -h wiki-update-companion --network=wikinet requarks/wiki-update-companion:latest
# docker create --name=nginx-proxy -p 80:80 -p 443:443 -e DEFAULT_HOST=wiki.local --network=wikinet -v /var/run/docker.sock:/tmp/docker.sock:ro --restart=unless-stopped jwilder/nginx-proxy
# docker create --name=watchtower --network=wikinet -v /var/run/docker.sock:/var/run/docker.sock --restart=unless-stopped containrrr/watchtower --cleanup --schedule="0 2 * * 6" wiki
docker create --name=wiki-update-companion -v /var/run/docker.sock:/var/run/docker.sock:ro --restart=unless-stopped -h wiki-update-companion --network=wikinet ghcr.io/requarks/wiki-update-companion:latest

@ -36,24 +36,24 @@
"node": ">=10.12"
},
"dependencies": {
"@azure/storage-blob": "12.2.1",
"@azure/storage-blob": "12.9.0",
"@exlinc/keycloak-passport": "1.0.2",
"@joplin/turndown-plugin-gfm": "1.0.43",
"@root/csr": "0.8.1",
"@root/keypairs": "0.10.3",
"@root/pem": "1.0.4",
"acme": "3.0.3",
"akismet-api": "5.2.1",
"akismet-api": "5.3.0",
"algoliasearch": "4.5.1",
"apollo-fetch": "0.7.0",
"apollo-server": "2.25.2",
"apollo-server-express": "2.25.2",
"auto-load": "3.0.4",
"aws-sdk": "2.1043.0",
"aws-sdk": "2.1125.0",
"azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1",
"bluebird": "3.7.2",
"body-parser": "1.19.1",
"body-parser": "1.20.0",
"chalk": "4.1.0",
"cheerio": "1.0.0-rc.5",
"chokidar": "3.5.3",
@ -75,7 +75,7 @@
"elasticsearch7": "npm:@elastic/elasticsearch@7",
"emoji-regex": "9.2.2",
"eventemitter2": "6.4.5",
"express": "4.17.2",
"express": "4.18.1",
"express-brute": "1.0.1",
"express-session": "1.17.2",
"file-type": "15.0.1",
@ -119,8 +119,8 @@
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mathjax": "3.1.2",
"mime-types": "2.1.34",
"moment": "2.29.1",
"mime-types": "2.1.35",
"moment": "2.29.3",
"moment-timezone": "0.5.31",
"mongodb": "3.6.5",
"ms": "2.1.3",
@ -130,7 +130,7 @@
"nanoid": "3.2.0",
"node-2fa": "1.1.2",
"node-cache": "5.1.2",
"nodemailer": "6.7.2",
"nodemailer": "6.7.4",
"objection": "2.2.18",
"passport": "0.4.1",
"passport-auth0": "1.4.2",
@ -143,15 +143,15 @@
"passport-gitlab2": "5.0.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"passport-ldapauth": "2.1.4",
"passport-ldapauth": "3.0.1",
"passport-local": "1.0.0",
"passport-microsoft": "0.1.0",
"passport-oauth2": "1.6.1",
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.0.2",
"passport-saml": "1.3.5",
"passport-saml": "3.2.1",
"passport-slack-oauth2": "1.1.1",
"passport-twitch-oauth": "1.0.0",
"passport-twitch-strategy": "2.2.0",
"pem-jwk": "2.0.0",
"pg": "8.4.1",
"pg-hstore": "2.3.4",
@ -168,11 +168,11 @@
"safe-regex": "2.1.1",
"sanitize-filename": "1.6.3",
"scim-query-filter-parser": "2.0.4",
"semver": "7.3.5",
"semver": "7.3.7",
"serve-favicon": "2.5.0",
"simple-git": "2.21.0",
"solr-node": "1.2.1",
"sqlite3": "5.0.2",
"sqlite3": "5.0.6",
"ssh2": "1.5.0",
"ssh2-promise": "1.0.2",
"striptags": "3.2.0",
@ -184,7 +184,7 @@
"uuid": "8.3.2",
"validate.js": "0.13.1",
"winston": "3.3.3",
"xss": "1.0.10",
"xss": "1.0.11",
"yargs": "16.1.0"
},
"devDependencies": {
@ -226,10 +226,10 @@
"babel-plugin-transform-imports": "2.0.0",
"cache-loader": "4.1.0",
"canvas-confetti": "1.3.1",
"cash-dom": "8.1.0",
"cash-dom": "8.1.1",
"chart.js": "2.9.4",
"clean-webpack-plugin": "3.0.0",
"clipboard": "2.0.8",
"clipboard": "2.0.10",
"codemirror": "5.58.2",
"copy-webpack-plugin": "6.2.1",
"core-js": "3.6.5",
@ -250,7 +250,7 @@
"eslint-plugin-vue": "7.1.0",
"file-loader": "6.1.1",
"filepond": "4.21.1",
"filepond-plugin-file-validate-type": "1.2.6",
"filepond-plugin-file-validate-type": "1.2.7",
"filesize.js": "2.0.0",
"graphql-persisted-document-loader": "2.0.0",
"graphql-tag": "2.11.0",
@ -277,7 +277,7 @@
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"postcss-selector-parser": "6.0.9",
"postcss-selector-parser": "6.0.10",
"prismjs": "1.22.0",
"pug-lint": "2.6.0",
"pug-loader": "2.4.0",

@ -525,14 +525,20 @@ router.get('/*', async (req, res, next) => {
}
// -> Inject comments variables
const commentTmpl = {
codeTemplate: WIKI.data.commentProvider.codeTemplate,
head: WIKI.data.commentProvider.head,
body: WIKI.data.commentProvider.body,
main: WIKI.data.commentProvider.main
}
if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
[
{ key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
{ key: 'pageId', value: page.id }
].forEach((cfg) => {
WIKI.data.commentProvider.head = _.replace(WIKI.data.commentProvider.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
WIKI.data.commentProvider.body = _.replace(WIKI.data.commentProvider.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
WIKI.data.commentProvider.main = _.replace(WIKI.data.commentProvider.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
})
}
@ -541,7 +547,7 @@ router.get('/*', async (req, res, next) => {
page,
sidebar,
injectCode,
comments: WIKI.data.commentProvider,
comments: commentTmpl,
effectivePermissions
})
}

@ -82,6 +82,7 @@ module.exports = {
const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`)
stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
stg.config.key = stg.key;
strategy.init(passport, stg.config)
strategy.config = stg.config

@ -106,7 +106,7 @@ module.exports = {
/**
* Graceful shutdown
*/
async shutdown () {
async shutdown (devMode = false) {
if (WIKI.servers) {
await WIKI.servers.stopServers()
}
@ -122,6 +122,8 @@ module.exports = {
if (WIKI.asar) {
await WIKI.asar.unload()
}
process.exit(0)
if (!devMode) {
process.exit(0)
}
}
}

@ -51,6 +51,9 @@ module.exports = {
}
await this.loadTemplate(opts.template)
return this.transport.sendMail({
headers: {
'x-mailer': 'Wiki.js'
},
from: `"${WIKI.config.mail.senderName}" <${WIKI.config.mail.senderEmail}>`,
to: opts.to,
subject: `${opts.subject} - ${WIKI.config.title}`,

@ -60,7 +60,7 @@ class Job {
cwd: WIKI.ROOTPATH,
stdio: ['inherit', 'inherit', 'pipe', 'ipc']
})
const stderr = [];
const stderr = []
proc.stderr.on('data', chunk => stderr.push(chunk))
this.finished = new Promise((resolve, reject) => {
proc.on('exit', (code, signal) => {

@ -3,6 +3,9 @@ const cfgHelper = require('../helpers/config')
const Promise = require('bluebird')
const fs = require('fs-extra')
const path = require('path')
const zlib = require('zlib')
const stream = require('stream')
const pipeline = Promise.promisify(stream.pipeline)
/* global WIKI */
@ -14,6 +17,12 @@ module.exports = {
minimumVersionRequired: '2.0.0-beta.0',
minimumNodeRequired: '10.12.0'
},
exportStatus: {
status: 'notrunning',
progress: 0,
message: '',
updatedAt: null
},
init() {
// Clear content cache
fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
@ -77,5 +86,376 @@ module.exports = {
db.close()
})
})
},
/**
* Export Wiki to Disk
*/
async export (opts) {
this.exportStatus.status = 'running'
this.exportStatus.progress = 0
this.exportStatus.message = ''
this.exportStatus.startedAt = new Date()
WIKI.logger.info(`Export started to path ${opts.path}`)
WIKI.logger.info(`Entities to export: ${opts.entities.join(', ')}`)
const progressMultiplier = 1 / opts.entities.length
try {
for (const entity of opts.entities) {
switch (entity) {
// -----------------------------------------
// ASSETS
// -----------------------------------------
case 'assets': {
WIKI.logger.info('Exporting assets...')
const assetFolders = await WIKI.models.assetFolders.getAllPaths()
const assetsCountRaw = await WIKI.models.assets.query().count('* as total').first()
const assetsCount = parseInt(assetsCountRaw.total)
if (assetsCount < 1) {
WIKI.logger.warn('There are no assets to export! Skipping...')
break
}
const assetsProgressMultiplier = progressMultiplier / Math.ceil(assetsCount / 50)
WIKI.logger.info(`Found ${assetsCount} assets to export. Streaming to disk...`)
await pipeline(
WIKI.models.knex.select('filename', 'folderId', 'data').from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),
new stream.Transform({
objectMode: true,
transform: async (asset, enc, cb) => {
const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename
WIKI.logger.info(`Exporting asset ${filename}...`)
await fs.outputFile(path.join(opts.path, 'assets', filename), asset.data)
this.exportStatus.progress += assetsProgressMultiplier * 100
cb()
}
})
)
WIKI.logger.info('Export: assets saved to disk successfully.')
break
}
// -----------------------------------------
// COMMENTS
// -----------------------------------------
case 'comments': {
WIKI.logger.info('Exporting comments...')
const outputPath = path.join(opts.path, 'comments.json.gz')
const commentsCountRaw = await WIKI.models.comments.query().count('* as total').first()
const commentsCount = parseInt(commentsCountRaw.total)
if (commentsCount < 1) {
WIKI.logger.warn('There are no comments to export! Skipping...')
break
}
const commentsProgressMultiplier = progressMultiplier / Math.ceil(commentsCount / 50)
WIKI.logger.info(`Found ${commentsCount} comments to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
rs._read = () => {}
const fetchCommentsBatch = async (offset) => {
const comments = await WIKI.models.comments.query().offset(offset).limit(50).withGraphJoined({
author: true,
page: true
}).modifyGraph('author', builder => {
builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
}).modifyGraph('page', builder => {
builder.select('pages.id', 'pages.path', 'pages.localeCode', 'pages.title')
})
if (comments.length > 0) {
for (const cmt of comments) {
rs.push(cmt)
}
fetchCommentsBatch(offset + 50)
} else {
rs.push(null)
}
this.exportStatus.progress += commentsProgressMultiplier * 100
}
fetchCommentsBatch(0)
let marker = 0
await pipeline(
rs,
new stream.Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
let outputStr = marker === 1 ? '[\n' : ''
outputStr += JSON.stringify(chunk, null, 2)
if (marker < commentsCount) {
outputStr += ',\n'
}
callback(null, outputStr)
},
flush (callback) {
callback(null, '\n]')
}
}),
zlib.createGzip(),
fs.createWriteStream(outputPath)
)
WIKI.logger.info('Export: comments.json.gz created successfully.')
break
}
// -----------------------------------------
// GROUPS
// -----------------------------------------
case 'groups': {
WIKI.logger.info('Exporting groups...')
const outputPath = path.join(opts.path, 'groups.json')
const groups = await WIKI.models.groups.query()
await fs.outputJSON(outputPath, groups, { spaces: 2 })
WIKI.logger.info('Export: groups.json created successfully.')
this.exportStatus.progress += progressMultiplier * 100
break
}
// -----------------------------------------
// HISTORY
// -----------------------------------------
case 'history': {
WIKI.logger.info('Exporting pages history...')
const outputPath = path.join(opts.path, 'pages-history.json.gz')
const pagesCountRaw = await WIKI.models.pageHistory.query().count('* as total').first()
const pagesCount = parseInt(pagesCountRaw.total)
if (pagesCount < 1) {
WIKI.logger.warn('There are no pages history to export! Skipping...')
break
}
const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
WIKI.logger.info(`Found ${pagesCount} pages history to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
rs._read = () => {}
const fetchPagesBatch = async (offset) => {
const pages = await WIKI.models.pageHistory.query().offset(offset).limit(10).withGraphJoined({
author: true,
page: true,
tags: true
}).modifyGraph('author', builder => {
builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
}).modifyGraph('page', builder => {
builder.select('pages.id', 'pages.title', 'pages.path', 'pages.localeCode')
}).modifyGraph('tags', builder => {
builder.select('tags.tag', 'tags.title')
})
if (pages.length > 0) {
for (const page of pages) {
rs.push(page)
}
fetchPagesBatch(offset + 10)
} else {
rs.push(null)
}
this.exportStatus.progress += pagesProgressMultiplier * 100
}
fetchPagesBatch(0)
let marker = 0
await pipeline(
rs,
new stream.Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
let outputStr = marker === 1 ? '[\n' : ''
outputStr += JSON.stringify(chunk, null, 2)
if (marker < pagesCount) {
outputStr += ',\n'
}
callback(null, outputStr)
},
flush (callback) {
callback(null, '\n]')
}
}),
zlib.createGzip(),
fs.createWriteStream(outputPath)
)
WIKI.logger.info('Export: pages-history.json.gz created successfully.')
break
}
// -----------------------------------------
// NAVIGATION
// -----------------------------------------
case 'navigation': {
WIKI.logger.info('Exporting navigation...')
const outputPath = path.join(opts.path, 'navigation.json')
const navigationRaw = await WIKI.models.navigation.query()
const navigation = navigationRaw.reduce((obj, cur) => {
obj[cur.key] = cur.config
return obj
}, {})
await fs.outputJSON(outputPath, navigation, { spaces: 2 })
WIKI.logger.info('Export: navigation.json created successfully.')
this.exportStatus.progress += progressMultiplier * 100
break
}
// -----------------------------------------
// PAGES
// -----------------------------------------
case 'pages': {
WIKI.logger.info('Exporting pages...')
const outputPath = path.join(opts.path, 'pages.json.gz')
const pagesCountRaw = await WIKI.models.pages.query().count('* as total').first()
const pagesCount = parseInt(pagesCountRaw.total)
if (pagesCount < 1) {
WIKI.logger.warn('There are no pages to export! Skipping...')
break
}
const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
WIKI.logger.info(`Found ${pagesCount} pages to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
rs._read = () => {}
const fetchPagesBatch = async (offset) => {
const pages = await WIKI.models.pages.query().offset(offset).limit(10).withGraphJoined({
author: true,
creator: true,
tags: true
}).modifyGraph('author', builder => {
builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
}).modifyGraph('creator', builder => {
builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
}).modifyGraph('tags', builder => {
builder.select('tags.tag', 'tags.title')
})
if (pages.length > 0) {
for (const page of pages) {
rs.push(page)
}
fetchPagesBatch(offset + 10)
} else {
rs.push(null)
}
this.exportStatus.progress += pagesProgressMultiplier * 100
}
fetchPagesBatch(0)
let marker = 0
await pipeline(
rs,
new stream.Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
let outputStr = marker === 1 ? '[\n' : ''
outputStr += JSON.stringify(chunk, null, 2)
if (marker < pagesCount) {
outputStr += ',\n'
}
callback(null, outputStr)
},
flush (callback) {
callback(null, '\n]')
}
}),
zlib.createGzip(),
fs.createWriteStream(outputPath)
)
WIKI.logger.info('Export: pages.json.gz created successfully.')
break
}
// -----------------------------------------
// SETTINGS
// -----------------------------------------
case 'settings': {
WIKI.logger.info('Exporting settings...')
const outputPath = path.join(opts.path, 'settings.json')
const config = {
...WIKI.config,
modules: {
analytics: await WIKI.models.analytics.query(),
authentication: (await WIKI.models.authentication.query()).map(a => ({
...a,
domainWhitelist: _.get(a, 'domainWhitelist.v', []),
autoEnrollGroups: _.get(a, 'autoEnrollGroups.v', [])
})),
commentProviders: await WIKI.models.commentProviders.query(),
renderers: await WIKI.models.renderers.query(),
searchEngines: await WIKI.models.searchEngines.query(),
storage: await WIKI.models.storage.query()
},
apiKeys: await WIKI.models.apiKeys.query().where('isRevoked', false)
}
await fs.outputJSON(outputPath, config, { spaces: 2 })
WIKI.logger.info('Export: settings.json created successfully.')
this.exportStatus.progress += progressMultiplier * 100
break
}
// -----------------------------------------
// USERS
// -----------------------------------------
case 'users': {
WIKI.logger.info('Exporting users...')
const outputPath = path.join(opts.path, 'users.json.gz')
const usersCountRaw = await WIKI.models.users.query().count('* as total').first()
const usersCount = parseInt(usersCountRaw.total)
if (usersCount < 1) {
WIKI.logger.warn('There are no users to export! Skipping...')
break
}
const usersProgressMultiplier = progressMultiplier / Math.ceil(usersCount / 50)
WIKI.logger.info(`Found ${usersCount} users to export. Streaming to file...`)
const rs = stream.Readable({ objectMode: true })
rs._read = () => {}
const fetchUsersBatch = async (offset) => {
const users = await WIKI.models.users.query().offset(offset).limit(50).withGraphJoined({
groups: true,
provider: true
}).modifyGraph('groups', builder => {
builder.select('groups.id', 'groups.name')
}).modifyGraph('provider', builder => {
builder.select('authentication.key', 'authentication.strategyKey', 'authentication.displayName')
})
if (users.length > 0) {
for (const usr of users) {
rs.push(usr)
}
fetchUsersBatch(offset + 50)
} else {
rs.push(null)
}
this.exportStatus.progress += usersProgressMultiplier * 100
}
fetchUsersBatch(0)
let marker = 0
await pipeline(
rs,
new stream.Transform({
objectMode: true,
transform (chunk, encoding, callback) {
marker++
let outputStr = marker === 1 ? '[\n' : ''
outputStr += JSON.stringify(chunk, null, 2)
if (marker < usersCount) {
outputStr += ',\n'
}
callback(null, outputStr)
},
flush (callback) {
callback(null, '\n]')
}
}),
zlib.createGzip(),
fs.createWriteStream(outputPath)
)
WIKI.logger.info('Export: users.json.gz created successfully.')
break
}
}
}
this.exportStatus.status = 'success'
this.exportStatus.progress = 100
} catch (err) {
this.exportStatus.status = 'error'
this.exportStatus.message = err.message
}
}
}

@ -173,6 +173,14 @@ module.exports = {
throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.')
}
// Check assigned permissions for manage:groups
if (
WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&
args.permissions.some(p => _.last(p.split(':')) === 'system')
) {
throw new gql.GraphQLError('You are not authorized to manage this group or assign the manage:system permissions.')
}
// Update group
await WIKI.models.groups.query().patch({
name: args.name,

@ -38,7 +38,7 @@ module.exports = {
SiteMutation: {
async updateConfig(obj, args, context) {
try {
if (args.host) {
if (args.hasOwnProperty('host')) {
let siteHost = _.trim(args.host)
if (siteHost.endsWith('/')) {
siteHost = siteHost.slice(0, -1)
@ -46,19 +46,19 @@ module.exports = {
WIKI.config.host = siteHost
}
if (args.title) {
if (args.hasOwnProperty('title')) {
WIKI.config.title = _.trim(args.title)
}
if (args.company) {
if (args.hasOwnProperty('company')) {
WIKI.config.company = _.trim(args.company)
}
if (args.contentLicense) {
if (args.hasOwnProperty('contentLicense')) {
WIKI.config.contentLicense = args.contentLicense
}
if (args.logoUrl) {
if (args.hasOwnProperty('logoUrl')) {
WIKI.config.logoUrl = _.trim(args.logoUrl)
}

@ -41,6 +41,14 @@ module.exports = {
ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
}
return exts
},
async exportStatus () {
return {
status: WIKI.system.exportStatus.status,
progress: Math.ceil(WIKI.system.exportStatus.progress),
message: WIKI.system.exportStatus.message,
startedAt: WIKI.system.exportStatus.startedAt
}
}
},
SystemMutation: {
@ -260,6 +268,39 @@ module.exports = {
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* Export Wiki to Disk
*/
async export (obj, args, context) {
try {
const desiredPath = path.resolve(WIKI.ROOTPATH, args.path)
// -> Check if export process is already running
if (WIKI.system.exportStatus.status === 'running') {
throw new Error('Another export is already running.')
}
// -> Validate entities
if (args.entities.length < 1) {
throw new Error('Must specify at least 1 entity to export.')
}
// -> Check target path
await fs.ensureDir(desiredPath)
const existingFiles = await fs.readdir(desiredPath)
if (existingFiles.length) {
throw new Error('Target directory must be empty!')
}
// -> Start export
WIKI.system.export({
entities: args.entities,
path: desiredPath
})
return {
responseResult: graphHelper.generateSuccess('Export started successfully.')
}
} catch (err) {
return graphHelper.generateError(err)
}
}
},
SystemInfo: {

@ -17,7 +17,8 @@ extend type Mutation {
type SystemQuery {
flags: [SystemFlag] @auth(requires: ["manage:system"])
info: SystemInfo
extensions: [SystemExtension]! @auth(requires: ["manage:system"])
extensions: [SystemExtension] @auth(requires: ["manage:system"])
exportStatus: SystemExportStatus @auth(requires: ["manage:system"])
}
# -----------------------------------------------
@ -47,6 +48,11 @@ type SystemMutation {
): DefaultResponse @auth(requires: ["manage:system"])
renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"])
export(
entities: [String]!
path: String!
): DefaultResponse @auth(requires: ["manage:system"])
}
# -----------------------------------------------
@ -121,3 +127,10 @@ type SystemExtension {
isInstalled: Boolean!
isCompatible: Boolean!
}
type SystemExportStatus {
status: String
progress: Int
message: String
startedAt: Date
}

@ -174,7 +174,7 @@ module.exports = class Asset extends Model {
// Force unsafe extensions to download
if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
res.set('Content-disposition', 'attachment; filename=' + fileInfo.base)
res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
}
if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {

@ -192,35 +192,39 @@ module.exports = class Page extends Model {
*/
static parseMetadata (raw, contentType) {
let result
switch (contentType) {
case 'markdown':
result = frontmatterRegex.markdown.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
} else {
// Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw)
try {
switch (contentType) {
case 'markdown':
result = frontmatterRegex.markdown.exec(raw)
if (result[2]) {
return {
title: result[2],
description: result[4],
content: result[5]
...yaml.safeLoad(result[2]),
content: result[3]
}
} else {
// Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw)
if (result[2]) {
return {
title: result[2],
description: result[4],
content: result[5]
}
}
}
}
break
case 'html':
result = frontmatterRegex.html.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
break
case 'html':
result = frontmatterRegex.html.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
}
}
break
break
}
} catch (err) {
WIKI.logger.warn('Failed to parse page metadata. Invalid syntax.')
}
return {
content: raw
@ -783,15 +787,7 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise with no value
*/
static async deletePage(opts) {
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
})
}
const page = await WIKI.models.pages.getPageFromDb(_.has(opts, 'id') ? opts.id : opts);
if (!page) {
throw new WIKI.Error.PageNotFound()
}

@ -308,7 +308,7 @@ module.exports = class User extends Model {
// Authenticate
return new Promise((resolve, reject) => {
WIKI.auth.passport.authenticate(selStrategy.strategyKey, {
WIKI.auth.passport.authenticate(selStrategy.key, {
session: !strInfo.useForm,
scope: strInfo.scopes ? strInfo.scopes : null
}, async (err, user, info) => {

@ -8,7 +8,7 @@ const Auth0Strategy = require('passport-auth0').Strategy
module.exports = {
init (passport, conf) {
passport.use('auth0',
passport.use(conf.key,
new Auth0Strategy({
domain: conf.domain,
clientID: conf.clientId,

@ -23,7 +23,7 @@ module.exports = {
keyString = keyString.substring(44);
}
}
passport.use('azure',
passport.use(conf.key,
new OIDCStrategy({
identityMetadata: conf.entryPoint,
clientID: conf.clientId,

@ -8,7 +8,7 @@ const CASStrategy = require('passport-cas').Strategy
module.exports = {
init (passport, conf) {
passport.use('cas',
passport.use(conf.key,
new CASStrategy({
ssoBaseURL: conf.ssoBaseURL,
serverBaseURL: conf.serverBaseURL,

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('discord',
passport.use(conf.key,
new DiscordStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('dropbox',
passport.use(conf.key,
new DropboxStrategy({
apiVersion: '2',
clientID: conf.clientId,

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('facebook',
passport.use(conf.key,
new FacebookStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -11,7 +11,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('firebase',
passport.use(conf.key,
new FirebaseStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -24,7 +24,7 @@ module.exports = {
githubConfig.userEmailURL = `${conf.enterpriseUserEndpoint}/emails`
}
passport.use('github',
passport.use(conf.key,
new GitHubStrategy(githubConfig, async (req, accessToken, refreshToken, profile, cb) => {
try {
const user = await WIKI.models.users.processProfile({

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('gitlab',
passport.use(conf.key,
new GitLabStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -40,7 +40,7 @@ module.exports = {
}
}
passport.use('google', strategy)
passport.use(conf.key, strategy)
},
logout (conf) {
return '/'

@ -10,7 +10,7 @@ const KeycloakStrategy = require('@exlinc/keycloak-passport')
module.exports = {
init (passport, conf) {
passport.use('keycloak',
passport.use(conf.key,
new KeycloakStrategy({
authorizationURL: conf.authorizationURL,
userInfoURL: conf.userInfoURL,

@ -10,7 +10,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('ldap',
passport.use(conf.key,
new LdapStrategy({
server: {
url: conf.url,

@ -19,7 +19,7 @@ props:
title: Admin Bind DN
type: String
default: cn='root'
hint: The dstinguished name (dn) of the account used for binding.
hint: The distinguished name (dn) of the account used for binding.
maxWidth: 600
order: 2
bindCredentials:

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('microsoft',
passport.use(conf.key,
new WindowsLiveStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -17,7 +17,8 @@ module.exports = {
clientSecret: conf.clientSecret,
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
passReqToCallback: true
passReqToCallback: true,
scope: conf.scope
}, async (req, accessToken, refreshToken, profile, cb) => {
try {
const user = await WIKI.models.users.processProfile({
@ -36,7 +37,7 @@ module.exports = {
})
client.userProfile = function (accesstoken, done) {
this._oauth2._useAuthorizationHeaderForGET = true
this._oauth2._useAuthorizationHeaderForGET = !conf.useQueryStringForAccessToken
this._oauth2.get(conf.userInfoURL, accesstoken, (err, data) => {
if (err) {
return done(err)
@ -49,7 +50,7 @@ module.exports = {
done(null, data)
})
}
passport.use('oauth2', client)
passport.use(conf.key, client)
},
logout (conf) {
if (!conf.logoutURL) {

@ -59,3 +59,14 @@ props:
title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 9
scope:
type: String
title: Scope
hint: (optional) Application Client permission scopes.
order: 10
useQueryStringForAccessToken:
type: Boolean
default: false
title: Pass access token via GET query string to User Info Endpoint
hint: (optional) Pass the access token in an `access_token` parameter attached to the GET query string of the User Info Endpoint URL. Otherwise the access token will be passed in the Authorization header.
order: 11

@ -10,7 +10,7 @@ const OpenIDConnectStrategy = require('passport-openidconnect').Strategy
module.exports = {
init (passport, conf) {
passport.use('oidc',
passport.use(conf.key,
new OpenIDConnectStrategy({
authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL,

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('okta',
passport.use(conf.key,
new OktaStrategy({
audience: conf.audience,
clientID: conf.clientId,

@ -33,7 +33,7 @@ module.exports = {
})
}
passport.use('rocketchat',
passport.use(conf.key,
new OAuth2Strategy({
authorizationURL: `${siteURL}/oauth/authorize`,
tokenURL: `${siteURL}/oauth/token`,

@ -10,16 +10,21 @@ const SAMLStrategy = require('passport-saml').Strategy
module.exports = {
init (passport, conf) {
let samlConfig = {
const samlConfig = {
callbackUrl: conf.callbackURL,
entryPoint: conf.entryPoint,
issuer: conf.issuer,
cert: _.split(conf.cert || '', '|'),
signatureAlgorithm: conf.signatureAlgorithm,
digestAlgorithm: conf.digestAlgorithm,
identifierFormat: conf.identifierFormat,
wantAssertionsSigned: conf.wantAssertionsSigned,
acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),
disableRequestedAuthnContext: conf.disableRequestedAuthnContext,
authnContext: conf.authnContext,
authnContext: _.split(conf.authnContext, '|'),
racComparison: conf.racComparison,
forceAuthn: conf.forceAuthn,
passive: conf.passive,
providerName: conf.providerName,
skipRequestCompression: conf.skipRequestCompression,
authnRequestBinding: conf.authnRequestBinding,
@ -28,16 +33,13 @@ module.exports = {
if (!_.isEmpty(conf.audience)) {
samlConfig.audience = conf.audience
}
if (!_.isEmpty(conf.cert)) {
samlConfig.cert = _.split(conf.cert, '|')
}
if (!_.isEmpty(conf.privateCert)) {
samlConfig.privateCert = conf.privateCert
if (!_.isEmpty(conf.privateKey)) {
samlConfig.privateKey = conf.privateKey
}
if (!_.isEmpty(conf.decryptionPvk)) {
samlConfig.decryptionPvk = conf.decryptionPvk
}
passport.use('saml',
passport.use(conf.key,
new SAMLStrategy(samlConfig, async (req, profile, cb) => {
try {
const userId = _.get(profile, [conf.mappingUID], null) || _.get(profile, 'nameID', null)

@ -21,18 +21,18 @@ props:
audience:
type: String
title: Audience
hint: (Optional) - Expected SAML response Audience (if not provided, Audience won't be verified)
hint: Expected SAML response Audience (if not provided, audience won't be verified)
order: 3
cert:
type: String
title: Certificate
hint: (Optional) - Public PEM-encoded X.509 signing certificate. If the provider has multiple certificates that are valid, join them together using the | pipe symbol.
hint: Public PEM-encoded X.509 signing certificate. If the provider has multiple certificates that are valid, join them together using the | pipe symbol.
multiline: true
order: 4
privateCert:
privateKey:
type: String
title: Private Certificate
hint: (Optional) - PEM formatted key used to sign the certificate.
title: Private Key
hint: PEM formatted key used to sign the certificate.
multiline: true
order: 5
decryptionPvk:
@ -52,53 +52,88 @@ props:
- sha1
- sha256
- sha512
digestAlgorithm:
type: String
title: Digest Algorithm
hint: Digest algorithm used to provide a digest for the signed data object
maxWidth: 400
order: 8
default: sha1
enum:
- sha1
- sha256
- sha512
identifierFormat:
type: String
title: Name Identifier format
default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
order: 8
order: 20
wantAssertionsSigned:
type: Boolean
title: Always sign assertions
hint: If enabled, add WantAssertionsSigned="true" to the metadata, to specify that the IdP should always sign the assertions.
default: false
order: 21
acceptedClockSkewMs:
type: Number
title: Accepted Clock Skew Milleseconds
hint: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely.
default: -1
order: 9
default: 0
order: 22
disableRequestedAuthnContext:
type: Boolean
title: Disable Requested Auth Context
hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers.
default: false
order: 10
order: 23
authnContext:
type: String
title: Auth Context
hint: Name identifier format to request auth context.
hint: Name identifier format to request auth context. For multiple values, join them together using the | pipe symbol.
default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
order: 11
order: 24
racComparison:
type: String
title: RAC Comparison Type
hint: Requested Authentication Context comparison type.
maxWidth: 400
order: 25
default: exact
enum:
- exact
- minimum
- maximum
- better
forceAuthn:
type: Boolean
title: Force Initial Re-authentication
hint: If enabled, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, even if they possess a valid session.
default: false
order: 12
order: 26
passive:
type: Boolean
title: Passive
hint: If enabled, the initial SAML request from the service provider specifies that the IdP should prevent visible user interaction.
default: false
order: 27
providerName:
type: String
title: Provider Name
hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider.
default: wiki.js
order: 13
order: 28
skipRequestCompression:
type: Boolean
title: Skip Request Compression
hint: If enabled, the SAML request from the service provider won't be compressed.
default: false
order: 14
order: 29
authnRequestBinding:
type: String
title: Request Binding
hint: Binding used for request authentication from IDP.
maxWidth: 400
order: 15
order: 30
default: 'HTTP-POST'
enum:
- HTTP-Redirect
@ -108,22 +143,22 @@ props:
type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
hint: The field storing the user unique identifier. Can be a variable name or a URI-formatted string.
order: 16
order: 40
mappingEmail:
title: Email Field Mapping
type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
hint: The field storing the user email. Can be a variable name or a URI-formatted string.
order: 17
order: 41
mappingDisplayName:
title: Display Name Field Mapping
type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
hint: The field storing the user display name. Can be a variable name or a URI-formatted string.
order: 18
order: 42
mappingPicture:
title: Avatar Picture Field Mapping
type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/picture'
hint: The field storing the user avatar picture. Can be a variable name or a URI-formatted string.
order: 19
order: 43

@ -9,7 +9,7 @@ const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('slack',
passport.use(conf.key,
new SlackStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,

@ -4,12 +4,12 @@
// Twitch Account
// ------------------------------------
const TwitchStrategy = require('passport-twitch-oauth').Strategy
const TwitchStrategy = require('passport-twitch-strategy').Strategy
const _ = require('lodash')
module.exports = {
init (passport, conf) {
passport.use('twitch',
passport.use(conf.key,
new TwitchStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
@ -21,7 +21,7 @@ module.exports = {
providerKey: req.params.strategy,
profile: {
...profile,
picture: _.get(profile, 'avatar', '')
picture: _.get(profile, 'profile_image_url', '')
}
})
cb(null, user)

@ -50,7 +50,7 @@ module.exports = {
}
// -> Strip host from local links
if (isHostSet && href.indexOf(WIKI.config.host) === 0) {
if (isHostSet && href.indexOf(`${WIKI.config.host}/`) === 0) {
href = href.replace(WIKI.config.host, '')
}

@ -20,6 +20,7 @@ props:
- fra1.digitaloceanspaces.com
- nyc3.digitaloceanspaces.com
- sfo2.digitaloceanspaces.com
- sfo3.digitaloceanspaces.com
- sgp1.digitaloceanspaces.com
order: 1
bucket:

@ -45,6 +45,10 @@ module.exports = {
await this.git.init()
}
// Disable quotePath
// Link https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
await this.git.raw(['config', '--local', 'core.quotepath', false])
// Set default author
await this.git.raw(['config', '--local', 'user.email', this.config.defaultEmail])
await this.git.raw(['config', '--local', 'user.name', this.config.defaultName])
@ -285,10 +289,13 @@ module.exports = {
const filePath = path.join(this.repoPath, fileName)
await fs.outputFile(filePath, page.injectMetadata(), 'utf8')
await this.git.add(`./${fileName}`)
await this.git.commit(`docs: create ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
const gitFilePath = `./${fileName}`
if ((await this.git.checkIgnore(gitFilePath)).length === 0) {
await this.git.add(gitFilePath)
await this.git.commit(`docs: create ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
}
},
/**
* UPDATE
@ -304,10 +311,13 @@ module.exports = {
const filePath = path.join(this.repoPath, fileName)
await fs.outputFile(filePath, page.injectMetadata(), 'utf8')
await this.git.add(`./${fileName}`)
await this.git.commit(`docs: update ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
const gitFilePath = `./${fileName}`
if ((await this.git.checkIgnore(gitFilePath)).length === 0) {
await this.git.add(gitFilePath)
await this.git.commit(`docs: update ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
}
},
/**
* DELETE
@ -321,10 +331,13 @@ module.exports = {
fileName = `${page.localeCode}/${fileName}`
}
await this.git.rm(`./${fileName}`)
await this.git.commit(`docs: delete ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
const gitFilePath = `./${fileName}`
if ((await this.git.checkIgnore(gitFilePath)).length === 0) {
await this.git.rm(gitFilePath)
await this.git.commit(`docs: delete ${page.path}`, fileName, {
'--author': `"${page.authorName} <${page.authorEmail}>"`
})
}
},
/**
* RENAME

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save