Merge branch 'requarks:main' into main

pull/5233/head
Mirco T 2 years ago committed by GitHub
commit a5e01ea409
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -358,6 +358,23 @@ jobs:
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Notify Telegram Channel
uses: appleboy/telegram-action@v0.1.1
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
format: markdown
disable_web_page_preview: true
message: |
Wiki.js *${{ github.ref_name }}* has been released!
See [release notes](https://github.com/requarks/wiki/releases) for details.
- name: Notify Discord Channel
uses: sebastianpopp/discord-action@v1.0
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details.
build-do-image:
name: Build DigitalOcean Image

@ -10,11 +10,13 @@
[![Build + Publish](https://github.com/Requarks/wiki/actions/workflows/build.yml/badge.svg)](https://github.com/Requarks/wiki/actions/workflows/build.yml)
[![Huntr](https://img.shields.io/badge/security%20bounty-disclose-brightgreen.svg?style=flat&logo=cachet&logoColor=white)](https://huntr.dev/bounties/disclose)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)
[![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp&logoColor=white)](https://blog.js.wiki/subscribe)
[![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack)
[![Twitter Follow](https://img.shields.io/badge/follow-%40requarks-blue.svg?style=flat&logo=twitter)](https://twitter.com/requarks)
[![Follow on Twitter](https://img.shields.io/badge/twitter-%40requarks-blue.svg?style=flat&logo=twitter&logoColor=white)](https://twitter.com/requarks)
[![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js)
[![Chat on Discord](https://img.shields.io/badge/discord-join-8D96F6.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/rcxt9QS2jd)
[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)
[![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe)
##### A modern, lightweight and powerful wiki app built on NodeJS
@ -25,7 +27,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!)*
@ -39,7 +41,7 @@
<div align="center">
Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://wiki.js.org/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`).
Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://js.wiki/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`).
[![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship)
[![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks)
@ -141,6 +143,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 +162,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))
@ -363,6 +369,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" />
@ -375,6 +382,7 @@ 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
@ -401,9 +409,6 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
<h2 align="center">Special Thanks</h2>
![Algolia](https://js.wiki/legacy/logo_algolia.png)
[Algolia](https://www.algolia.com/) for providing access to their incredible search engine.
![Browserstack](https://js.wiki/legacy/logo_browserstack.png)
[Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools.
@ -411,16 +416,16 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
[Cloudflare](https://www.cloudflare.com/) for providing their great CDN, SSL and advanced networking services.
![DigitalOcean](https://js.wiki/legacy/logo_digitalocean.png)
[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site.
[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site and APIs.
![Icons8](https://static.requarks.io/logo/icons8-text-h40.png)
[Icons8](https://icons8.com/) for providing beautiful icon sets.
[Icons8](https://icons8.com/) for providing access to their beautiful icon sets.
![Lokalise](https://static.requarks.io/logo/lokalise-text-h40.png)
[Lokalise](https://lokalise.com/) for providing access to their great localization tool.
![Netlify](https://js.wiki/legacy/logo_netlify.png)
[Netlify](https://www.netlify.com) for providing hosting for landings and blog websites.
[Netlify](https://www.netlify.com) for providing hosting for our website.
![ngrok](https://static.requarks.io/logo/ngrok-h40.png)
[ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services.

@ -164,6 +164,19 @@
//- disabled
//- )
v-card.mt-5.animated.fadeInUp.wait-p6s
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title.subtitle-1 URL Handling
v-card-text
v-text-field(
outlined
:label='$t(`admin:general.pageExtensions`)'
v-model='config.pageExtensions'
prepend-icon='mdi-format-text-wrapping-overflow'
:hint='$t(`admin:general.pageExtensionsHint`)'
persistent-hint
)
component(:is='activeModal')
</template>
@ -202,7 +215,8 @@ export default {
featurePageRatings: false,
featurePageComments: false,
featurePersonalWikis: false,
featureTinyPNG: false
featureTinyPNG: false,
pageExtensions: ''
},
metaRobots: [
{ text: 'Index', value: 'index' },
@ -247,32 +261,34 @@ export default {
await this.$apollo.mutate({
mutation: gql`
mutation (
$host: String!
$title: String!
$description: String!
$robots: [String]!
$analyticsService: String!
$analyticsId: String!
$company: String!
$contentLicense: String!
$logoUrl: String!
$featurePageRatings: Boolean!
$featurePageComments: Boolean!
$featurePersonalWikis: Boolean!
$host: String
$title: String
$description: String
$robots: [String]
$analyticsService: String
$analyticsId: String
$company: String
$contentLicense: String
$logoUrl: String
$pageExtensions: String
$featurePageRatings: Boolean
$featurePageComments: Boolean
$featurePersonalWikis: Boolean
) {
site {
updateConfig(
host: $host,
title: $title,
description: $description,
robots: $robots,
analyticsService: $analyticsService,
analyticsId: $analyticsId,
company: $company,
contentLicense: $contentLicense,
logoUrl: $logoUrl,
featurePageRatings: $featurePageRatings,
featurePageComments: $featurePageComments,
host: $host
title: $title
description: $description
robots: $robots
analyticsService: $analyticsService
analyticsId: $analyticsId
company: $company
contentLicense: $contentLicense
logoUrl: $logoUrl
pageExtensions: $pageExtensions
featurePageRatings: $featurePageRatings
featurePageComments: $featurePageComments
featurePersonalWikis: $featurePersonalWikis
) {
responseResult {
@ -295,6 +311,7 @@ export default {
company: _.get(this.config, 'company', ''),
contentLicense: _.get(this.config, 'contentLicense', ''),
logoUrl: _.get(this.config, 'logoUrl', ''),
pageExtensions: _.get(this.config, 'pageExtensions', ''),
featurePageRatings: _.get(this.config, 'featurePageRatings', false),
featurePageComments: _.get(this.config, 'featurePageComments', false),
featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false)
@ -347,6 +364,7 @@ export default {
company
contentLicense
logoUrl
pageExtensions
featurePageRatings
featurePageComments
featurePersonalWikis

@ -57,6 +57,16 @@
:hint='$t(`admin:mail.smtpPortHint`)'
style='max-width: 300px;'
)
v-text-field(
outlined
v-model='config.name'
:label='$t(`admin:mail.smtpName`)'
required
:counter='255'
prepend-icon='mdi-server'
persistent-hint
:hint='$t(`admin:mail.smtpNameHint`)'
)
v-switch(
v-model='config.secure'
:label='$t(`admin:mail.smtpTLS`)'
@ -169,6 +179,7 @@ export default {
senderEmail: '',
host: '',
port: 0,
name: '',
secure: false,
verifySSL: false,
user: '',
@ -192,6 +203,7 @@ export default {
senderEmail: this.config.senderEmail || '',
host: this.config.host || '',
port: _.toSafeInteger(this.config.port) || 0,
name: this.config.name || '',
secure: this.config.secure || false,
verifySSL: this.config.verifySSL || false,
user: this.config.user || '',

@ -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' },

@ -1,30 +1,32 @@
mutation (
$senderName: String!,
$senderEmail: String!,
$host: String!,
$port: Int!,
$secure: Boolean!,
$verifySSL: Boolean!,
$user: String!,
$pass: String!,
$useDKIM: Boolean!,
$dkimDomainName: String!,
$dkimKeySelector: String!,
$senderName: String!
$senderEmail: String!
$host: String!
$port: Int!
$name: String!
$secure: Boolean!
$verifySSL: Boolean!
$user: String!
$pass: String!
$useDKIM: Boolean!
$dkimDomainName: String!
$dkimKeySelector: String!
$dkimPrivateKey: String!
) {
mail {
updateConfig(
senderName: $senderName,
senderEmail: $senderEmail,
host: $host,
port: $port,
secure: $secure,
verifySSL: $verifySSL,
user: $user,
pass: $pass,
useDKIM: $useDKIM,
dkimDomainName: $dkimDomainName,
dkimKeySelector: $dkimKeySelector,
senderName: $senderName
senderEmail: $senderEmail
host: $host
port: $port
name: $name
secure: $secure
verifySSL: $verifySSL
user: $user
pass: $pass
useDKIM: $useDKIM
dkimDomainName: $dkimDomainName
dkimKeySelector: $dkimKeySelector
dkimPrivateKey: $dkimPrivateKey
) {
responseResult {

@ -5,6 +5,7 @@
senderEmail
host
port
name
secure
verifySSL
user

@ -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 }}

@ -108,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 | `[]` |

@ -2,7 +2,7 @@
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}

@ -41,6 +41,12 @@ spec:
env:
- name: DB_TYPE
value: postgres
{{- if (.Values.externalPostgresql).databaseURL }}
- name: DATABASE_URL
value: {{ .Values.externalPostgresql.databaseURL }}
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ default "1" .Values.externalPostgresql.NODE_TLS_REJECT_UNAUTHORIZED | quote }}
{{- else }}
- name: DB_HOST
value: {{ template "wiki.postgresql.host" . }}
- name: DB_PORT
@ -62,6 +68,7 @@ spec:
name: {{ template "wiki.postgresql.secret" . }}
{{- end }}
key: {{ template "wiki.postgresql.secretKey" . }}
{{- end }}
- name: HA_ACTIVE
value: {{ .Values.replicaCount | int | le 2 | quote }}
{{- with .Values.volumeMounts }}
@ -76,6 +83,8 @@ spec:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

@ -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 }}

@ -32,6 +32,16 @@ readinessProbe:
path: /healthz
port: http
startupProbe:
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 60
httpGet:
path: /healthz
port: http
podSecurityContext: {}
# fsGroup: 2000
@ -54,6 +64,7 @@ service:
ingress:
enabled: true
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
@ -101,6 +112,13 @@ sideload:
# - name: HTTPS_PROXY
# value: http://my.proxy.com:3128
## This will override the postgresql chart values
# externalPostgresql:
# # note: ?sslmode=require => ?ssl=true
# databaseURL: postgresql://postgres:postgres@postgres:5432/wiki?ssl=true
# # For self signed CAs, like DigitalOcean
# NODE_TLS_REJECT_UNAUTHORIZED: "0"
## Configuration values for the postgresql dependency.
## ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
##

@ -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

@ -150,7 +150,7 @@
"passport-openidconnect": "0.0.2",
"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",

@ -45,6 +45,10 @@ defaults:
company: ''
contentLicense: ''
logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
pageExtensions:
- md
- html
- txt
mail:
host: ''
secure: true
@ -152,8 +156,4 @@ reservedPaths:
- img
- js
- svg
pageExtensions:
- md
- html
- txt
# ---------------------------------

@ -414,7 +414,7 @@ router.get('/_userav/:uid', async (req, res, next) => {
* View document / asset
*/
router.get('/*', async (req, res, next) => {
const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const stripExt = _.some(WIKI.config.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const pageArgs = pageHelper.parsePath(req.path, { stripExt })
const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)

@ -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)
}
}
}

@ -13,6 +13,7 @@ module.exports = {
let conf = {
host: WIKI.config.mail.host,
port: WIKI.config.mail.port,
name: WIKI.config.mail.name,
secure: WIKI.config.mail.secure,
tls: {
rejectUnauthorized: !(WIKI.config.mail.verifySSL === false)

@ -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,

@ -49,6 +49,7 @@ module.exports = {
senderEmail: args.senderEmail,
host: args.host,
port: args.port,
name: args.name,
secure: args.secure,
verifySSL: args.verifySSL,
user: args.user,

@ -18,6 +18,7 @@ module.exports = {
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
logoUrl: WIKI.config.logoUrl,
pageExtensions: WIKI.config.pageExtensions.join(', '),
...WIKI.config.seo,
...WIKI.config.features,
...WIKI.config.security,
@ -62,6 +63,10 @@ module.exports = {
WIKI.config.logoUrl = _.trim(args.logoUrl)
}
if (args.hasOwnProperty('pageExtensions')) {
WIKI.config.pageExtensions = _.trim(args.pageExtensions).split(',').map(p => p.trim().toLowerCase()).filter(p => p !== '')
}
WIKI.config.seo = {
description: _.get(args, 'description', WIKI.config.seo.description),
robots: _.get(args, 'robots', WIKI.config.seo.robots),
@ -104,7 +109,7 @@ module.exports = {
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'features', 'security', 'uploads'])
if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy')

@ -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: {

@ -32,6 +32,7 @@ type MailMutation {
senderEmail: String!
host: String!
port: Int!
name: String!
secure: Boolean!
verifySSL: Boolean!
user: String!
@ -48,16 +49,17 @@ type MailMutation {
# -----------------------------------------------
type MailConfig {
senderName: String!
senderEmail: String!
host: String!
port: Int!
secure: Boolean!
verifySSL: Boolean!
user: String!
pass: String!
useDKIM: Boolean!
dkimDomainName: String!
dkimKeySelector: String!
dkimPrivateKey: String!
senderName: String
senderEmail: String
host: String
port: Int
name: String
secure: Boolean
verifySSL: Boolean
user: String
pass: String
useDKIM: Boolean
dkimDomainName: String
dkimKeySelector: String
dkimPrivateKey: String
}

@ -33,6 +33,7 @@ type SiteMutation {
company: String
contentLicense: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean
@ -74,6 +75,7 @@ type SiteConfig {
company: String
contentLicense: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean

@ -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
}

@ -38,6 +38,9 @@ WIKI.kernel.init()
// Register exit handler
// ----------------------------------------
process.on('SIGTERM', () => {
WIKI.kernel.shutdown()
})
process.on('SIGINT', () => {
WIKI.kernel.shutdown()
})

@ -14,14 +14,14 @@ module.exports = {
callbackUrl: conf.callbackURL,
entryPoint: conf.entryPoint,
issuer: conf.issuer,
cert = _.split(conf.cert, '|'),
cert: (conf.cert || '').split('|'),
signatureAlgorithm: conf.signatureAlgorithm,
digestAlgorithm: conf.digestAlgorithm,
identifierFormat: conf.identifierFormat,
wantAssertionsSigned: conf.wantAssertionsSigned,
acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),
disableRequestedAuthnContext: conf.disableRequestedAuthnContext,
authnContext: conf.authnContext,
authnContext: (conf.authnContext || '').split('|'),
racComparison: conf.racComparison,
forceAuthn: conf.forceAuthn,
passive: conf.passive,

@ -89,7 +89,7 @@ props:
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: 24
racComparison:

@ -4,7 +4,7 @@
// Twitch Account
// ------------------------------------
const TwitchStrategy = require('passport-twitch-oauth').Strategy
const TwitchStrategy = require('passport-twitch-strategy').Strategy
const _ = require('lodash')
module.exports = {
@ -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)

@ -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

@ -103,6 +103,7 @@ module.exports = () => {
senderEmail: '',
host: '',
port: 465,
name: '',
secure: true,
verifySSL: true,
user: '',

@ -14697,7 +14697,7 @@ passport-oauth2@1.6.1, passport-oauth2@^1.6.0:
uid2 "0.0.x"
utils-merge "1.x.x"
passport-oauth2@1.x.x, passport-oauth2@^1.2.0, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0:
passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108"
integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==
@ -14761,12 +14761,13 @@ passport-strategy@1.x.x, passport-strategy@^1.0.0:
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
passport-twitch-oauth@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-twitch-oauth/-/passport-twitch-oauth-1.0.0.tgz#8ba20658ffe18dbeab2201547e0a2e6a459adaf1"
integrity sha512-sX/HZgRP320CICi+ZQn0MpKV8PiVcxuEOaLywOE2EiXQkkUm4glTgBbz9bsr7Ws7985fFmP+LgLIe0/HtMGucw==
passport-twitch-strategy@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/passport-twitch-strategy/-/passport-twitch-strategy-2.2.0.tgz#8a9d78ef478de92d1164fa8d194ee76ff551e0f1"
integrity sha512-PjESpFVnJk6GIX2EOtkuyk01K81ACDG8IiLJu6yOtOyaMti24/afMtqBcqDrsKqz0RHrx206AfWcI6Q4Tvy4OQ==
dependencies:
passport-oauth2 "^1.2.0"
node-fetch "^2.6.1"
passport-oauth2 "^1.5.0"
passport@0.4.1, passport@^0.4.1:
version "0.4.1"

Loading…
Cancel
Save