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

@ -558,7 +558,7 @@ export default {
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' }, { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' }, { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' }, { 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) Maputo', value: 'Africa/Maputo' },
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' }, { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' }, { 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'), UtilityAuth: () => import(/* webpackChunkName: "admin" */ './admin-utilities-auth.vue'),
UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'), UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'),
UtilityCache: () => import(/* webpackChunkName: "admin" */ './admin-utilities-cache.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'), UtilityImportv1: () => import(/* webpackChunkName: "admin" */ './admin-utilities-importv1.vue'),
UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue') UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue')
}, },
@ -56,6 +57,12 @@ export default {
i18nKey: 'content', i18nKey: 'content',
isAvailable: true isAvailable: true
}, },
{
key: 'UtilityExport',
icon: 'mdi-database-export',
i18nKey: 'export',
isAvailable: true
},
{ {
key: 'UtilityCache', key: 'UtilityCache',
icon: 'mdi-database-refresh', icon: 'mdi-database-refresh',

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

@ -528,7 +528,7 @@ export default {
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' }, { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' }, { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' }, { 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) Maputo', value: 'Africa/Maputo' },
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' }, { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' }, { 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-icon(small) mdi-folder-open
v-list-item-title {{ item.title }} v-list-item-title {{ item.title }}
v-divider.mt-2 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-list-item-avatar(size='24')
v-icon mdi-text-box v-icon mdi-text-box
v-list-item-title {{ currentParent.title }} v-list-item-title {{ currentParent.title }}

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

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

@ -3,7 +3,7 @@
# ==================== # ====================
FROM node:16-alpine AS assets 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 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` | | `image.tag` | Wiki.js image tag | `latest` |
| `imagePullPolicy` | Image pull policy | `IfNotPresent` | | `imagePullPolicy` | Image pull policy | `IfNotPresent` |
| `replicacount` | Amount of wiki.js service pods to run | `1` | | `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.limits` | wiki.js service resource limits | `nil` |
| `resources.requests` | wiki.js service resource requests | `nil` | | `resources.requests` | wiki.js service resource requests | `nil` |
| `nodeSelector` | Node labels for wiki.js pod assignment | `{}` | | `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 | `[]` | | `volumeMounts` | Volume mounts for Wiki.js container | `[]` |
| `volumes` | Volumes for Wiki.js Pod | `[]` | | `volumes` | Volumes for Wiki.js Pod | `[]` |
| `ingress.enabled` | Enable ingress controller resource | `false` | | `ingress.enabled` | Enable ingress controller resource | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` | | `ingress.annotations` | Ingress annotations | `{}` |
| `ingress.hosts` | List of ingress rules | `[{"host": "wiki.local", "paths": ["/"]}]` | | `ingress.hosts` | List of ingress rules | `[{"host": "wiki.local", "paths": ["/"]}]` |
| `ingress.tls` | Ingress TLS configuration | `[]` | | `ingress.tls` | Ingress TLS configuration | `[]` |

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

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

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

@ -60,7 +60,7 @@ const init = {
}, },
async reload() { async reload() {
console.warn(chalk.yellow('--- Gracefully stopping server...')) 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...')) console.warn(chalk.yellow('--- Purging node modules cache...'))

@ -14,6 +14,4 @@ docker network create wikinet
docker volume create pgdata 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=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 -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=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
# 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

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

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

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

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

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

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

@ -3,6 +3,9 @@ const cfgHelper = require('../helpers/config')
const Promise = require('bluebird') const Promise = require('bluebird')
const fs = require('fs-extra') const fs = require('fs-extra')
const path = require('path') const path = require('path')
const zlib = require('zlib')
const stream = require('stream')
const pipeline = Promise.promisify(stream.pipeline)
/* global WIKI */ /* global WIKI */
@ -14,6 +17,12 @@ module.exports = {
minimumVersionRequired: '2.0.0-beta.0', minimumVersionRequired: '2.0.0-beta.0',
minimumNodeRequired: '10.12.0' minimumNodeRequired: '10.12.0'
}, },
exportStatus: {
status: 'notrunning',
progress: 0,
message: '',
updatedAt: null
},
init() { init() {
// Clear content cache // Clear content cache
fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache')) fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
@ -77,5 +86,376 @@ module.exports = {
db.close() 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.') 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 // Update group
await WIKI.models.groups.query().patch({ await WIKI.models.groups.query().patch({
name: args.name, name: args.name,

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

@ -41,6 +41,14 @@ module.exports = {
ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible() ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
} }
return exts 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: { SystemMutation: {
@ -260,6 +268,39 @@ module.exports = {
} catch (err) { } catch (err) {
return graphHelper.generateError(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: { SystemInfo: {

@ -17,7 +17,8 @@ extend type Mutation {
type SystemQuery { type SystemQuery {
flags: [SystemFlag] @auth(requires: ["manage:system"]) flags: [SystemFlag] @auth(requires: ["manage:system"])
info: SystemInfo 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"]) ): DefaultResponse @auth(requires: ["manage:system"])
renewHTTPSCertificate: 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! isInstalled: Boolean!
isCompatible: 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 // Force unsafe extensions to download
if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) { 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)) { if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -19,7 +19,7 @@ props:
title: Admin Bind DN title: Admin Bind DN
type: String type: String
default: cn='root' 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 maxWidth: 600
order: 2 order: 2
bindCredentials: bindCredentials:

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

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

@ -59,3 +59,14 @@ props:
title: Logout URL title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 9 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 = { module.exports = {
init (passport, conf) { init (passport, conf) {
passport.use('oidc', passport.use(conf.key,
new OpenIDConnectStrategy({ new OpenIDConnectStrategy({
authorizationURL: conf.authorizationURL, authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL, tokenURL: conf.tokenURL,

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

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

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

@ -21,18 +21,18 @@ props:
audience: audience:
type: String type: String
title: Audience 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 order: 3
cert: cert:
type: String type: String
title: Certificate 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 multiline: true
order: 4 order: 4
privateCert: privateKey:
type: String type: String
title: Private Certificate title: Private Key
hint: (Optional) - PEM formatted key used to sign the certificate. hint: PEM formatted key used to sign the certificate.
multiline: true multiline: true
order: 5 order: 5
decryptionPvk: decryptionPvk:
@ -52,53 +52,88 @@ props:
- sha1 - sha1
- sha256 - sha256
- sha512 - 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: identifierFormat:
type: String type: String
title: Name Identifier format title: Name Identifier format
default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' 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: acceptedClockSkewMs:
type: Number type: Number
title: Accepted Clock Skew Milleseconds 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. 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 default: 0
order: 9 order: 22
disableRequestedAuthnContext: disableRequestedAuthnContext:
type: Boolean type: Boolean
title: Disable Requested Auth Context 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. 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 default: false
order: 10 order: 23
authnContext: authnContext:
type: String type: String
title: Auth Context 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 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: forceAuthn:
type: Boolean type: Boolean
title: Force Initial Re-authentication 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. 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 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: providerName:
type: String type: String
title: Provider Name title: Provider Name
hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider. hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider.
default: wiki.js default: wiki.js
order: 13 order: 28
skipRequestCompression: skipRequestCompression:
type: Boolean type: Boolean
title: Skip Request Compression title: Skip Request Compression
hint: If enabled, the SAML request from the service provider won't be compressed. hint: If enabled, the SAML request from the service provider won't be compressed.
default: false default: false
order: 14 order: 29
authnRequestBinding: authnRequestBinding:
type: String type: String
title: Request Binding title: Request Binding
hint: Binding used for request authentication from IDP. hint: Binding used for request authentication from IDP.
maxWidth: 400 maxWidth: 400
order: 15 order: 30
default: 'HTTP-POST' default: 'HTTP-POST'
enum: enum:
- HTTP-Redirect - HTTP-Redirect
@ -108,22 +143,22 @@ props:
type: String type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' 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. hint: The field storing the user unique identifier. Can be a variable name or a URI-formatted string.
order: 16 order: 40
mappingEmail: mappingEmail:
title: Email Field Mapping title: Email Field Mapping
type: String type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' 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. hint: The field storing the user email. Can be a variable name or a URI-formatted string.
order: 17 order: 41
mappingDisplayName: mappingDisplayName:
title: Display Name Field Mapping title: Display Name Field Mapping
type: String type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' 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. hint: The field storing the user display name. Can be a variable name or a URI-formatted string.
order: 18 order: 42
mappingPicture: mappingPicture:
title: Avatar Picture Field Mapping title: Avatar Picture Field Mapping
type: String type: String
default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/picture' 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. 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 = { module.exports = {
init (passport, conf) { init (passport, conf) {
passport.use('slack', passport.use(conf.key,
new SlackStrategy({ new SlackStrategy({
clientID: conf.clientId, clientID: conf.clientId,
clientSecret: conf.clientSecret, clientSecret: conf.clientSecret,

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

@ -50,7 +50,7 @@ module.exports = {
} }
// -> Strip host from local links // -> 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, '') href = href.replace(WIKI.config.host, '')
} }

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

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

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