Merge branch 'requarks:main' into feature/page-list

pull/5613/head
Jan Lucki 11 months ago committed by GitHub
commit b7093045ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +0,0 @@
<!-- Wiki.js 1.x? Go to https://github.com/Requarks/wiki-v1/issues -->
### Actual behavior
### Expected behavior
### Steps to reproduce the behavior
<!-- FOR FEATURE REQUESTS: Use the feature request board instead: https://requests.requarks.io/wiki -->
<!-- Love Wiki.js? Please consider supporting our collective:
👉 https://opencollective.com/wikijs/donate -->

@ -3,9 +3,6 @@ contact_links:
- name: Help / Questions
url: https://github.com/Requarks/wiki/discussions/categories/help-questions
about: Ask the community for help on using or setting up Wiki.js
- name: Report a Security Issue
url: https://github.com/Requarks/wiki/security/policy
about: Privately report security issues so they can be addressed quickly.
- name: Errors / Bug Reports
url: https://github.com/Requarks/wiki/discussions/categories/error-bug-report
about: Create a discussion around the bug / error you're getting. If validated, a proper GitHub issue will be created so that it can be worked on.

@ -19,7 +19,7 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set Build Variables
run: |
@ -42,20 +42,20 @@ jobs:
cat package.json
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
uses: docker/build-push-action@v4.0.0
with:
context: .
file: dev/build/Dockerfile
@ -77,7 +77,7 @@ jobs:
find _dist/wiki/ -printf "%P\n" | tar -czf wiki-js.tar.gz --no-recursion -C _dist/wiki/ -T -
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.1.2
with:
name: drop
path: wiki-js.tar.gz
@ -92,7 +92,7 @@ jobs:
dbtype: [postgres, mysql, mariadb, mssql, sqlite]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set Test Variables
run: |
@ -129,7 +129,7 @@ jobs:
docker: armv7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set Version Variables
run: |
@ -142,26 +142,26 @@ jobs:
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2.4.0
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v3.0.2
with:
name: drop
path: drop
@ -172,11 +172,12 @@ jobs:
tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
uses: docker/build-push-action@v4.0.0
with:
context: .
file: dev/build-arm/Dockerfile
platforms: ${{ matrix.platform }}
provenance: false
push: true
tags: |
requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}
@ -189,12 +190,12 @@ jobs:
steps:
- name: Setup Node.js environment
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: 12.x
node-version: 16.x
- name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v3.0.2
with:
name: drop
path: drop
@ -212,7 +213,7 @@ jobs:
run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win .
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3.1.2
with:
name: drop-win
path: wiki-js-windows.tar.gz
@ -232,13 +233,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -273,13 +274,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -319,13 +320,13 @@ jobs:
docker manifest push -p ghcr.io/requarks/wiki:latest
- name: Download Linux Build
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v3.0.2
with:
name: drop
path: drop
- name: Download Windows Build
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v3.0.2
with:
name: drop-win
path: drop-win
@ -339,15 +340,16 @@ jobs:
writeToFile: false
- name: Update GitHub Release
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@v1.12.0
with:
allowUpdates: true
draft: false
makeLatest: true
name: ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.changes }}
token: ${{ github.token }}
artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz'
- name: Notify Slack Releases Channel
uses: slackapi/slack-github-action@v1.18.0
with:
@ -358,7 +360,7 @@ 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:
@ -369,7 +371,7 @@ jobs:
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:
@ -382,7 +384,7 @@ jobs:
needs: [release]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set Version Variables
run: |

@ -1,6 +1,9 @@
<div align="center">
<img src="https://static.requarks.io/logo/wikijs-full.svg" alt="Wiki.js" width="600" />
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://static.requarks.io/logo/wikijs-full-darktheme.svg">
<img alt="Wiki.js" src="https://static.requarks.io/logo/wikijs-full.svg" width="600">
</picture>
[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat&maxAge=3600)](https://github.com/Requarks/wiki/releases)
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat)](https://github.com/requarks/wiki/blob/master/LICENSE)
@ -126,11 +129,28 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
Oleksii<br />(@idokka)
</a>
</td>
<!--<td align="center" valign="middle" width="148">
</tr>
<tr>
<td align="center" valign="middle" width="148">
<a href="https://www.openhost-network.com/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/114218287?s=200&v=4">
</a>
</td>
<td align="center" valign="middle" width="148">
<a href="https://www.prevo.ch/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/114394792?v=4">
</a>
</td>
<td align="center" valign="middle" width="148">
<a href="https://acceleanation.com/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/41210718?s=200&v=4">
</a>
</td>
<td align="center" valign="middle" colspan="3">
<a href="https://github.com/sponsors/NGPixel" target="_blank">
<img src="https://static.requarks.io/sponsors/become-148x72.png">
</a>
</td>-->
</td>
</tr>
</tbody>
</table>
@ -151,12 +171,14 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))
- Dragan Espenschied ([@despens](https://github.com/despens))
- Elijah Zobenko ([@he110](https://github.com/he110))
- Emerson-Perna ([@Emerson-Perna](https://github.com/Emerson-Perna))
- Ernie ([@iamernie](https://github.com/iamernie))
- Fabio Ferrari ([@devxops](https://github.com/devxops))
- Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa))
- Florian Moss ([@florianmoss](https://github.com/florianmoss))
- GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen))
- HeavenBay ([@HeavenBay](https://github.com/heavenbay))
- HikaruEgashira ([@HikaruEgashira](https://github.com/HikaruEgashira))
- Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy))
- Jaimyn Mayer ([@jabelone](https://github.com/jabelone))
- Jay Lee ([@polyglotm](https://github.com/polyglotm))
@ -178,6 +200,7 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
- Noel Cower ([@nilium](https://github.com/nilium))
- Oleksandr Koltsov ([@crambo](https://github.com/crambo))
- Phi Zeroth ([@phizeroth](https://github.com/phizeroth))
- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))
- Robert Lanzke ([@winkelement](https://github.com/winkelement))
- Ruizhe Li ([@liruizhe1995](https://github.com/liruizhe1995))
@ -188,11 +211,13 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Tyler Denman ([@tylerguy](https://github.com/tylerguy))
- Victor Bilgin ([@vbilgin](https://github.com/vbilgin))
- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))
- ameyrakheja ([@ameyrakheja](https://github.com/ameyrakheja))
- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
- aytaa ([@aytaa](https://github.com/aytaa))
- chaee ([@chaee](https://github.com/chaee))
- magicpotato ([@fortheday](https://github.com/fortheday))
- motoacs ([@motoacs](https://github.com/motoacs))
- muzian666 ([@muzian666](https://github.com/muzian666))
- rburckner ([@rburckner](https://github.com/rburckner))
- scorpion ([@scorpion](https://github.com/scorpion))
- valantien ([@valantien](https://github.com/valantien))
@ -343,6 +368,23 @@ Support this project by becoming a sponsor. Your logo will show up in the Contri
<a href="https://opencollective.com/wikijs/sponsor/39/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/39/avatar.svg"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/40/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/40/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/41/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/41/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/42/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/42/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/43/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/43/avatar.svg"></a>
</td>
<td align="center" valign="middle">
<a href="https://opencollective.com/wikijs/sponsor/44/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/44/avatar.svg"></a>
</td>
</tr>
</tbody>
</table>
</div>
@ -357,14 +399,18 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Al Romano
- Alex Balabanov
- Alex Milanov
- Alex Zen
- Arti Zirk
- Ave
- Brandon Curtis
- Damien Hottelier
- Daniel T. Holtzclaw
- Dave 'Sri' Seah
- djagoo
- dz
- Douglas Lassance
- Ergoflix
- Ernie Reid
- Etienne
- Flemis Jurgenheimer
@ -374,26 +420,34 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Hope
- Ian
- Imari Childress
- Iskander Callos
- Josh Stewart
</td><td>
<img width="441" height="1" />
- Iskander Callos
- Josh Stewart
- Justin Dunsworth
- Keir
- Loïc CRAMPON
- Ludgeir Ibanez
- Lyn Matten
- Mads Rosendahl
- Mark Mansur
- Matt Gedigian
- Mike Ditton
- Nate Figz
- Patryk
- Paul O'Fallon
- Philipp Schürch
- Tracey Duffy
- Quaxim
- Richeir
- Sergio Navarro Fernández
- Shad Narcher
- ShadowVoyd
- SmartNET.works
- Stepan Sokolovskyi
- Zach Crawford
- Zach Maynard
- 张白驹
@ -425,9 +479,15 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
![Icons8](https://static.requarks.io/logo/icons8-text-h40.png)
[Icons8](https://icons8.com/) for providing access to their beautiful icon sets.
![Localazy](https://static.requarks.io/logo/localazy-h40.png)
[Localazy](https://localazy.com/) for providing access to their great localization service.
![Lokalise](https://static.requarks.io/logo/lokalise-text-h40.png)
[Lokalise](https://lokalise.com/) for providing access to their great localization tool.
![MacStadium](https://static.requarks.io/logo/macstadium-h40.png)
[MacStadium](https://www.macstadium.com) for providing access to their Mac hardware in the cloud.
![Netlify](https://js.wiki/legacy/logo_netlify.png)
[Netlify](https://www.netlify.com) for providing hosting for our website.

@ -65,15 +65,6 @@
v-list-item(to='/comments')
v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
v-list-item-title {{ $t('admin:comments.title') }}
v-list-item(to='/editor', disabled)
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
v-list-item-title {{ $t('admin:editor.title') }}
v-list-item(to='/extensions')
v-list-item-avatar(size='24', tile): v-icon mdi-chip
v-list-item-title {{ $t('admin:extensions.title') }}
v-list-item(to='/logging', disabled)
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-script-text-outline
v-list-item-title {{ $t('admin:logging.title') }}
v-list-item(to='/rendering', color='primary')
v-list-item-avatar(size='24', tile): v-icon mdi-cogs
v-list-item-title {{ $t('admin:rendering.title') }}
@ -104,9 +95,6 @@
v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
v-list-item-title {{ $t('admin:utilities.title') }}
v-list-item(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-webhook
v-list-item-title {{ $t('admin:webhooks.title') }}
v-list-group(
to='/dev'
no-action

@ -184,7 +184,7 @@
v-list-item-title Netlify
v-list-item-subtitle Deploy modern static websites with Netlify. Get CDN, Continuous deployment, 1-click HTTPS, and all the services you need.
v-list-item-action
v-btn(icon, href='https://wwwnetlify.com', target='_blank')
v-btn(icon, href='https://www.netlify.com', target='_blank')
v-icon(color='grey') mdi-earth
</template>

@ -82,6 +82,15 @@
:return-object='false'
:hint='$t(`admin:general.contentLicenseHint`)'
persistent-hint
)
v-text-field.mt-3(
outlined
:label='$t(`admin:general.footerOverride`)'
v-model='config.footerOverride'
prepend-icon='mdi-page-layout-footer'
append-icon='mdi-language-markdown'
persistent-hint
:hint='$t(`admin:general.footerOverrideHint`)'
)
v-divider
.overline.grey--text.pa-4 SEO
@ -144,7 +153,7 @@
//- )
//- v-divider.mt-3
v-switch(
v-switch.mt-0(
inset
label='Comments'
color='indigo'
@ -177,6 +186,76 @@
persistent-hint
)
v-card.mt-5.animated.fadeInUp.wait-p7s
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title.subtitle-1 {{$t('admin:general.editShortcuts')}}
v-card-text
v-switch.mt-0(
inset
:label='$t(`admin:general.editFab`)'
color='primary'
v-model='config.editFab'
persistent-hint
:hint='$t(`admin:general.editFabHint`)'
)
v-divider
.overline.grey--text.pa-4 {{$t('admin:general.editMenuBar')}}
.px-3.pb-3
v-switch.mt-0.ml-1(
inset
:label='$t(`admin:general.displayEditMenuBar`)'
color='primary'
v-model='config.editMenuBar'
persistent-hint
:hint='$t(`admin:general.displayEditMenuBarHint`)'
)
v-switch.mt-4.ml-1(
v-if='config.editMenuBar'
inset
:label='$t(`admin:general.displayEditMenuBtn`)'
color='primary'
v-model='config.editMenuBtn'
persistent-hint
:hint='$t(`admin:general.displayEditMenuBtnHint`)'
)
v-switch.mt-4.ml-1(
v-if='config.editMenuBar'
inset
:label='$t(`admin:general.displayEditMenuExternalBtn`)'
color='primary'
v-model='config.editMenuExternalBtn'
persistent-hint
:hint='$t(`admin:general.displayEditMenuExternalBtnHint`)'
)
template(v-if='config.editMenuBar && config.editMenuExternalBtn')
v-divider
.overline.grey--text.pa-4 External Edit Button
.px-3.pb-3
v-text-field(
outlined
:label='$t(`admin:general.editMenuExternalName`)'
v-model='config.editMenuExternalName'
prepend-icon='mdi-format-title'
:hint='$t(`admin:general.editMenuExternalNameHint`)'
persistent-hint
)
v-text-field.mt-3(
outlined
:label='$t(`admin:general.editMenuExternalIcon`)'
v-model='config.editMenuExternalIcon'
prepend-icon='mdi-dice-5'
:hint='$t(`admin:general.editMenuExternalIconHint`)'
persistent-hint
)
v-text-field.mt-3(
outlined
:label='$t(`admin:general.editMenuExternalUrl`)'
v-model='config.editMenuExternalUrl'
prepend-icon='mdi-near-me'
:hint='$t(`admin:general.editMenuExternalUrlHint`)'
persistent-hint
)
component(:is='activeModal')
</template>
@ -210,13 +289,21 @@ export default {
analyticsId: '',
company: '',
contentLicense: '',
footerOverride: '',
logoUrl: '',
featureAnalytics: false,
featurePageRatings: false,
featurePageComments: false,
featurePersonalWikis: false,
featureTinyPNG: false,
pageExtensions: ''
pageExtensions: '',
editFab: false,
editMenuBar: false,
editMenuBtn: false,
editMenuExternalBtn: false,
editMenuExternalName: '',
editMenuExternalIcon: '',
editMenuExternalUrl: ''
},
metaRobots: [
{ text: 'Index', value: 'index' },
@ -231,6 +318,7 @@ export default {
logoUrl: sync('site/logoUrl'),
company: sync('site/company'),
contentLicense: sync('site/contentLicense'),
footerOverride: sync('site/footerOverride'),
activeModal: sync('editor/activeModal'),
contentLicenses () {
return [
@ -269,11 +357,19 @@ export default {
$analyticsId: String
$company: String
$contentLicense: String
$footerOverride: String
$logoUrl: String
$pageExtensions: String
$featurePageRatings: Boolean
$featurePageComments: Boolean
$featurePersonalWikis: Boolean
$editFab: Boolean
$editMenuBar: Boolean
$editMenuBtn: Boolean
$editMenuExternalBtn: Boolean
$editMenuExternalName: String
$editMenuExternalIcon: String
$editMenuExternalUrl: String
) {
site {
updateConfig(
@ -285,11 +381,19 @@ export default {
analyticsId: $analyticsId
company: $company
contentLicense: $contentLicense
footerOverride: $footerOverride
logoUrl: $logoUrl
pageExtensions: $pageExtensions
featurePageRatings: $featurePageRatings
featurePageComments: $featurePageComments
featurePersonalWikis: $featurePersonalWikis
editFab: $editFab
editMenuBar: $editMenuBar
editMenuBtn: $editMenuBtn
editMenuExternalBtn: $editMenuExternalBtn
editMenuExternalName: $editMenuExternalName
editMenuExternalIcon: $editMenuExternalIcon
editMenuExternalUrl: $editMenuExternalUrl
) {
responseResult {
succeeded
@ -310,11 +414,19 @@ export default {
analyticsId: _.get(this.config, 'analyticsId', ''),
company: _.get(this.config, 'company', ''),
contentLicense: _.get(this.config, 'contentLicense', ''),
footerOverride: _.get(this.config, 'footerOverride', ''),
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)
featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),
editFab: _.get(this.config, 'editFab', false),
editMenuBar: _.get(this.config, 'editMenuBar', false),
editMenuBtn: _.get(this.config, 'editMenuBtn', false),
editMenuExternalBtn: _.get(this.config, 'editMenuExternalBtn', false),
editMenuExternalName: _.get(this.config, 'editMenuExternalName', ''),
editMenuExternalIcon: _.get(this.config, 'editMenuExternalIcon', ''),
editMenuExternalUrl: _.get(this.config, 'editMenuExternalUrl', '')
},
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
@ -328,6 +440,7 @@ export default {
this.siteTitle = this.config.title
this.company = this.config.company
this.contentLicense = this.config.contentLicense
this.footerOverride = this.config.footerOverride
this.logoUrl = this.config.logoUrl
} catch (err) {
this.$store.commit('pushGraphError', err)
@ -363,11 +476,19 @@ export default {
analyticsId
company
contentLicense
footerOverride
logoUrl
pageExtensions
featurePageRatings
featurePageComments
featurePersonalWikis
editFab
editMenuBar
editMenuBtn
editMenuExternalBtn
editMenuExternalName
editMenuExternalIcon
editMenuExternalUrl
}
}
}

@ -104,7 +104,7 @@ export default {
const truncatePath = path => _.take(path.split('/'), depth).join('/')
const descendantsByChild =
Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
.map(([childPath, descendantsGroup]) => [getPage(childPath), descendantsGroup])
.map(([childPath, descendantsGroup]) => [getPage(childPath), _.sortBy(descendantsGroup, child => child.path)])
.map(([child, descendantsGroup]) =>
[child, _.filter(descendantsGroup, d => d.path !== child.path)])
return {

@ -55,20 +55,16 @@
v-card.mt-3.animated.fadeInUp.wait-p1s
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}
v-spacer
v-chip(label, color='white', small).primary--text coming soon
v-card-text
v-select(
:items='[]'
:items='tocPositions'
outlined
prepend-icon='mdi-border-vertical'
v-model='config.iconset'
v-model='config.tocPosition'
label='Table of Contents Position'
persistent-hint
hint='Select whether the table of contents is shown on the left, right or not at all.'
disabled
)
v-flex(lg6 xs12)
//- v-card.animated.fadeInUp.wait-p2s
//- v-toolbar(color='teal', dark, dense, flat)
@ -155,6 +151,7 @@ export default {
theme: 'default',
darkMode: false,
iconset: '',
tocPosition: 'left',
injectCSS: '',
injectHead: '',
injectBody: ''
@ -184,6 +181,13 @@ export default {
width: 100
}
]
},
tocPositions () {
return [
{ text: 'Left (default)', value: 'left' },
{ text: 'Right', value: 'right' },
{ text: 'Hidden', value: 'off' }
]
}
},
watch: {
@ -209,6 +213,7 @@ export default {
theme: this.config.theme,
iconset: this.config.iconset,
darkMode: this.darkMode,
tocPosition: this.config.tocPosition,
injectCSS: this.config.injectCSS,
injectHead: this.config.injectHead,
injectBody: this.config.injectBody

@ -499,9 +499,9 @@ export default {
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
{ text: '(GMT-03:00) Sao Paulo', value: 'America/Sao_Paulo' },
{ text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
{ text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
{ text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },

@ -77,6 +77,7 @@ export default {
editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'),
editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.vue'),
editorAsciidoc: () => import(/* webpackChunkName: "editor-asciidoc", webpackMode: "lazy" */ './editor/editor-asciidoc.vue'),
editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),

@ -7,7 +7,13 @@ const maxDepth = 100
const codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/
const codeBlockEndMatch = /^`{3}$/
CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
export default {
register(lang) {
CodeMirror.registerHelper('fold', lang, foldHandler)
}
}
function foldHandler (cm, start) {
const firstLine = cm.getLine(start.line)
const lastLineNo = cm.lastLine()
let end
@ -59,4 +65,4 @@ CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
from: CodeMirror.Pos(start.line, firstLine.length),
to: CodeMirror.Pos(end, cm.getLine(end).length)
}
})
}

@ -0,0 +1,707 @@
<template lang='pug'>
.editor-asciidoc
v-toolbar.editor-asciidoc-toolbar(dense, color='primary', dark, flat, style='overflow-x: hidden;')
template(v-if='isModalShown')
v-spacer
v-btn.animated.fadeInRight(text, @click='closeAllModal')
v-icon(left) mdi-arrow-left-circle
span {{$t('editor:backToEditor')}}
template(v-else)
v-tooltip(bottom, color='primary')
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn(icon, tile, v-on='on', @click='toggleMarkup({ start: `**` })').mx-0
v-icon mdi-format-bold
span {{$t('editor:markup.bold')}}
v-tooltip(bottom, color='primary')
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='toggleMarkup({ start: `__` })').mx-0
v-icon mdi-format-italic
span {{$t('editor:markup.italic')}}
v-menu(offset-y, open-on-hover)
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p3s(icon, tile, v-on='on').mx-0
v-icon mdi-format-header-pound
v-list.py-0
template(v-for='(n, idx) in 6')
v-list-item(@click='setHeaderLine(n)', :key='idx')
v-list-item-action
v-icon(:size='24 - (idx - 1) * 2') mdi-format-header-{{n}}
v-list-item-title {{$t('editor:markup.heading', { level: n })}}
v-divider(v-if='idx < 5')
v-tooltip(bottom, color='primary')
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p4s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~` })').mx-0
v-icon mdi-format-subscript
span {{$t('editor:markup.subscript')}}
v-tooltip(bottom, color='primary')
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p5s(icon, tile, v-on='on', @click='toggleMarkup({ start: `^` })').mx-0
v-icon mdi-format-superscript
span {{$t('editor:markup.superscript')}}
v-menu(offset-y, open-on-hover)
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p6s(icon, tile, v-on='on').mx-0
v-icon mdi-alpha-t-box-outline
v-list.py-0
v-list-item(@click='insertBeforeEachLine({ content: `> `})')
v-list-item-action
v-icon mdi-alpha-t-box-outline
v-list-item-title {{$t('editor:markup.blockquote')}}
v-divider
v-list-item(@click='insertBeforeEachLine({ content: `NOTE: `})')
v-list-item-action
v-icon(color='blue') mdi-alpha-n-box-outline
v-list-item-title {{'Note blockquote'}}
v-divider
v-list-item(@click='insertBeforeEachLine({ content: `TIP: `})')
v-list-item-action
v-icon(color='success') mdi-alpha-t-box-outline
v-list-item-title {{'Tip blockquote'}}
v-divider
v-list-item(@click='insertBeforeEachLine({ content: `WARNING: `})')
v-list-item-action
v-icon(color='warning') mdi-alpha-w-box-outline
v-list-item-title {{$t('editor:markup.blockquoteWarning')}}
v-divider
v-list-item(@click='insertBeforeEachLine({ content: `CAUTION: `})')
v-list-item-action
v-icon(color='purple') mdi-alpha-c-box-outline
v-list-item-title {{'Caution blockquote'}}
v-list-item(@click='insertBeforeEachLine({ content: `IMPORTANT: `})')
v-list-item-action
v-icon(color='error') mdi-alpha-i-box-outline
v-list-item-title {{'Important blockquote'}}
v-divider
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
v-tooltip(bottom, color='primary')
template(v-slot:activator='{ on }')
v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='previewShown = !previewShown').mx-0
v-icon mdi-book-open-outline
span {{$t('editor:markup.togglePreviewPane')}}
.editor-asciidoc-main
.editor-asciidoc-sidebar
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0
v-icon mdi-link-plus
span {{$t('editor:markup.insertLink')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0
v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image
span {{$t('editor:markup.insertAssets')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
v-icon mdi-chart-multiline
span {{$t('editor:markup.insertDiagram')}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
v-icon mdi-arrow-expand-all
span {{$t('editor:markup.distractionFreeMode')}}
.editor-asciidoc-editor
textarea(ref='cm')
transition(name='editor-asciidoc-preview')
.editor-asciidoc-preview(v-if='previewShown')
.editor-asciidoc-preview-content.contents(ref='editorPreviewContainer')
div(
ref='editorPreview'
v-html='previewHTML'
)
v-system-bar.editor-asciidoc-sysbar(dark, status, color='grey darken-3')
.caption.editor-asciidoc-sysbar-locale {{locale.toUpperCase()}}
.caption.px-3 /{{path}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
.caption AsciiDoc
v-spacer
.caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')
</template>
<script>
import _ from 'lodash'
import { get, sync } from 'vuex-pathify'
import DOMPurify from 'dompurify'
// ========================================
// IMPORTS
// ========================================
// Code Mirror
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// Language
import 'codemirror-asciidoc'
// Addons
import 'codemirror/addon/selection/active-line.js'
import 'codemirror/addon/display/fullscreen.js'
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/selection/mark-selection.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
import cmFold from './common/cmFold'
// ========================================
// INIT
// ========================================
const asciidoctor = require('asciidoctor')()
const cheerio = require('cheerio')
// Platform detection
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
// ========================================
// HELPER FUNCTIONS
// ========================================
cmFold.register('asciidoc')
// ========================================
// Vue Component
// ========================================
export default {
data() {
return {
cm: null,
cursorPos: { ch: 0, line: 1 },
previewShown: true, // TODO
insertLinkDialog: false,
helpShown: false,
previewHTML: ''
}
},
computed: {
isMobile() {
return this.$vuetify.breakpoint.smAndDown
},
isModalShown() {
return this.helpShown || this.activeModal !== ''
},
locale: get('page/locale'),
path: get('page/path'),
mode: get('editor/mode'),
activeModal: sync('editor/activeModal')
},
methods: {
toggleModal(key) {
this.activeModal = (this.activeModal === key) ? '' : key
this.helpShown = false
},
closeAllModal() {
this.activeModal = ''
this.helpShown = false
},
onCmInput: _.debounce(function(newContent) {
this.processContent(newContent)
}, 600),
processContent(newContent) {
this.processMarkers(this.cm.firstLine(), this.cm.lastLine())
let html = asciidoctor.convert(newContent, {
standalone: false,
safe: 'safe',
attributes: {
showtitle: true,
icons: 'font'
}
})
const $ = cheerio.load(html, {
decodeEntities: true
})
$('pre.highlight > code.language-diagram').each((i, elm) => {
const diagramContent = Buffer.from($(elm).html(), 'base64').toString()
$(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
})
this.previewHTML = DOMPurify.sanitize($.html(), {
ADD_TAGS: ['foreignObject']
})
},
/**
* Insert content at cursor
*/
insertAtCursor({ content }) {
const cursor = this.cm.doc.getCursor('head')
this.cm.doc.replaceRange(content, cursor)
},
/**
* Insert content after current line
*/
insertAfter({ content, newLine }) {
const curLine = this.cm.doc.getCursor('to').line
const lineLength = this.cm.doc.getLine(curLine).length
this.cm.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
},
/**
* Insert content before current line
*/
insertBeforeEachLine({ content, after }) {
let lines = []
if (!this.cm.doc.somethingSelected()) {
lines.push(this.cm.doc.getCursor('head').line)
} else {
lines = _.flatten(this.cm.doc.listSelections().map(sl => {
const range = Math.abs(sl.anchor.line - sl.head.line) + 1
const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line
return _.times(range, l => l + lowestLine)
}))
}
lines.forEach(ln => {
let lineContent = this.cm.doc.getLine(ln)
const lineLength = lineContent.length
if (_.startsWith(lineContent, content)) {
lineContent = lineContent.substring(content.length)
}
this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })
})
if (after) {
const lastLine = _.last(lines)
this.cm.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })
}
},
/**
* Update cursor state
*/
positionSync(cm) {
this.cursorPos = cm.getCursor('head')
},
toggleMarkup({ start, end }) {
if (!end) { end = start }
if (!this.cm.doc.somethingSelected()) {
return this.$store.commit('showNotification', {
message: this.$t('editor:markup.noSelectionError'),
style: 'warning',
icon: 'warning'
})
}
this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end))
},
setHeaderLine(lvl) {
const curLine = this.cm.doc.getCursor('head').line
let lineContent = this.cm.doc.getLine(curLine)
const lineLength = lineContent.length
if (_.startsWith(lineContent, '=')) {
lineContent = lineContent.replace(/^(=+ )/, '')
}
lineContent = _.times(lvl, n => '=').join('') + ` ` + lineContent
this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })
},
toggleFullscreen () {
this.cm.setOption('fullScreen', true)
},
refresh() {
this.$nextTick(() => {
this.cm.refresh()
})
},
insertLink () {
this.insertLinkDialog = true
},
insertLinkHandler ({ locale, path }) {
const lastPart = _.last(path.split('/'))
this.insertAtCursor({
content: siteLangs.length > 0 ? `link:/${locale}/${path}[${lastPart}]` : `link:/${path}[${lastPart}]`
})
},
processMarkers (from, to) {
let found = null
let foundStart = 0
this.cm.doc.getAllMarks().forEach(mk => {
if (mk.__kind) {
mk.clear()
}
})
this.cm.eachLine(from, to, ln => {
const line = ln.lineNo()
if (ln.text.startsWith('```diagram')) {
found = 'diagram'
foundStart = line
} else if (ln.text === '```' && found) {
switch (found) {
// ------------------------------
// -> DIAGRAM
// ------------------------------
case 'diagram': {
if (line - foundStart !== 2) {
return
}
this.addMarker({
kind: 'diagram',
from: { line: foundStart, ch: 3 },
to: { line: foundStart, ch: 10 },
text: 'Edit Diagram',
action: ((start, end) => {
return (ev) => {
this.cm.doc.setSelection({ line: start, ch: 0 }, { line: end, ch: 3 })
try {
const raw = this.cm.doc.getLine(end - 1)
this.$store.set('editor/activeModalData', Buffer.from(raw, 'base64').toString())
this.toggleModal(`editorModalDrawio`)
} catch (err) {
return this.$store.commit('showNotification', {
message: 'Failed to process diagram data.',
style: 'warning',
icon: 'warning'
})
}
}
})(foundStart, line)
})
if (ln.height > 0) {
this.cm.foldCode(foundStart)
}
break
}
}
found = null
}
})
},
addMarker ({ kind, from, to, text, action }) {
const markerElm = document.createElement('span')
markerElm.appendChild(document.createTextNode(text))
markerElm.className = 'CodeMirror-buttonmarker'
markerElm.addEventListener('click', action)
this.cm.markText(from, to, { replacedWith: markerElm, __kind: kind })
}
},
mounted() {
this.$store.set('editor/editorKey', 'asciidoc')
if (this.mode === 'create') {
this.$store.set('editor/content', '== header\n\ncontent')
}
// Initialize CodeMirror
this.cm = CodeMirror.fromTextArea(this.$refs.cm, {
tabSize: 2,
mode: 'asciidoc',
theme: 'wikijs-dark',
lineNumbers: true,
lineWrapping: true,
line: true,
styleActiveLine: true,
highlightSelectionMatches: {
annotateScrollbar: true
},
viewportMargin: 50,
inputStyle: 'contenteditable',
allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
direction: siteConfig.rtl ? 'rtl' : 'ltr',
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
})
this.cm.setValue(this.$store.get('editor/content'))
this.cm.on('change', c => {
this.$store.set('editor/content', c.getValue())
this.onCmInput(this.$store.get('editor/content'))
})
if (this.$vuetify.breakpoint.mdAndUp) {
this.cm.setSize(null, 'calc(100vh - 137px)')
} else {
this.cm.setSize(null, 'calc(100vh - 112px - 16px)')
}
// Set Keybindings
const keyBindings = {
'F11' (c) {
c.setOption('fullScreen', !c.getOption('fullScreen'))
},
'Esc' (c) {
if (c.getOption('fullScreen')) c.setOption('fullScreen', false)
}
}
_.set(keyBindings, `${CtrlKey}-B`, c => {
this.toggleMarkup({ start: `**` })
return false
})
_.set(keyBindings, `${CtrlKey}-I`, c => {
this.toggleMarkup({ start: `__` })
return false
})
this.cm.setOption('extraKeys', keyBindings)
// Handle cursor movement
this.cm.on('cursorActivity', c => {
this.positionSync(c)
})
// Render initial preview
this.processContent(this.$store.get('editor/content'))
this.$root.$on('editorInsert', opts => {
switch (opts.kind) {
case 'IMAGE':
let img = `image::${opts.path}[${opts.text}]`
this.insertAtCursor({
content: img
})
break
case 'BINARY':
this.insertAtCursor({
content: `link:${opts.path}[${opts.text}]`
})
break
case 'DIAGRAM':
const selStartLine = this.cm.getCursor('from').line
const selEndLine = this.cm.getCursor('to').line + 1
this.cm.doc.replaceSelection('```diagram\n' + opts.text + '\n```\n', 'start')
this.processMarkers(selStartLine, selEndLine)
break
}
})
// Handle save conflict
this.$root.$on('saveConflict', () => {
this.toggleModal(`editorModalConflict`)
})
this.$root.$on('overwriteEditorContent', () => {
this.cm.setValue(this.$store.get('editor/content'))
})
},
beforeDestroy() {
this.$root.$off('editorInsert')
}
}
</script>
<style lang='scss'>
$editor-ascii-height: calc(100vh - 137px);
$editor-ascii-height-mobile: calc(100vh - 112px - 16px);
.editor-asciidoc {
&-main {
display: flex;
width: 100%;
}
&-editor {
background-color: darken(mc('grey', '900'), 4.5%);
flex: 1 1 50%;
display: block;
height: $editor-ascii-height;
position: relative;
@include until($tablet) {
height: $editor-ascii-height-mobile;
}
}
&-preview {
flex: 1 1 50%;
background-color: mc('grey', '100');
position: relative;
height: $editor-ascii-height;
overflow: hidden;
padding: 1rem;
@at-root .theme--dark & {
background-color: mc('grey', '900');
}
@include until($tablet) {
display: none;
}
&-enter-active, &-leave-active {
transition: max-width .5s ease;
max-width: 50vw;
.editor-code-preview-content {
width: 50vw;
overflow:hidden;
}
}
&-enter, &-leave-to {
max-width: 0;
}
&-content {
height: $editor-ascii-height;
overflow-y: scroll;
padding: 0;
width: calc(100% + 17px);
// -ms-overflow-style: none;
// &::-webkit-scrollbar {
// width: 0px;
// background: transparent;
// }
@include until($tablet) {
height: $editor-ascii-height-mobile;
}
> div {
outline: none;
}
p.line {
overflow-wrap: break-word;
}
.tabset {
background-color: mc('teal', '700');
color: mc('teal', '100') !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
border-radius: 5px 0 0 0;
font-style: italic;
&::after {
display: none;
}
&-header {
background-color: mc('teal', '500');
color: #FFF !important;
padding: 5px 12px;
font-size: 14px;
font-weight: 500;
margin-top: 0 !important;
&::after {
display: none;
}
}
&-content {
border-left: 5px solid mc('teal', '500');
background-color: mc('teal', '50');
padding: 0 15px 15px;
overflow: hidden;
@at-root .theme--dark & {
background-color: rgba(mc('teal', '500'), .1);
}
}
}
}
}
&-toolbar {
background-color: mc('blue', '700');
background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%);
color: #FFF;
.v-toolbar__content {
padding-left: 64px;
@include until($tablet) {
padding-left: 8px;
}
}
}
&-insert:not(.v-speed-dial--right) {
@include from($tablet) {
left: 50%;
margin-left: -28px;
}
}
&-sidebar {
background-color: mc('grey', '900');
width: 64px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 24px 0;
@include until($tablet) {
padding: 12px 0;
width: 40px;
}
}
&-sysbar {
padding-left: 0;
&-locale {
background-color: rgba(255,255,255,.25);
display:inline-flex;
padding: 0 12px;
height: 24px;
width: 63px;
justify-content: center;
align-items: center;
}
}
// ==========================================
// Fix FAB revealing under codemirror
// ==========================================
.speed-dial--fixed {
z-index: 8;
}
// ==========================================
// CODE MIRROR
// ==========================================
.CodeMirror {
height: auto;
font-family: 'Roboto Mono', monospace;
font-size: .9rem;
.cm-header-1 {
font-size: 1.5rem;
}
.cm-header-2 {
font-size: 1.25rem;
}
.cm-header-3 {
font-size: 1.15rem;
}
.cm-header-4 {
font-size: 1.1rem;
}
.cm-header-5 {
font-size: 1.05rem;
}
.cm-header-6 {
font-size: 1.025rem;
}
}
.CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {
word-break: break-word;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
.cm-matchhighlight {
background-color: mc('grey', '800');
}
.CodeMirror-selection-highlight-scrollbar {
background-color: mc('green', '600');
}
}
</style>

@ -124,44 +124,19 @@
span {{$t('editor:markup.insertAssets')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, disabled, @click='toggleModal(`editorModalBlocks`)').mx-0
v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline
span {{$t('editor:markup.insertBlock')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, disabled).mx-0
v-icon mdi-code-braces
span {{$t('editor:markup.insertCodeBlock')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0
v-icon mdi-movie
span {{$t('editor:markup.insertVideoAudio')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
v-icon mdi-chart-multiline
span {{$t('editor:markup.insertDiagram')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p6s(icon, tile, v-on='on', dark, disabled).mx-0
v-icon mdi-function-variant
span {{$t('editor:markup.insertMathExpression')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p7s(icon, tile, v-on='on', dark, disabled).mx-0
v-icon mdi-table-plus
span {{$t('editor:markup.tableHelper')}}
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
v-icon mdi-arrow-expand-all
span {{$t('editor:markup.distractionFreeMode')}}
v-tooltip(right, color='teal')
template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p9s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0
v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0
v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle
span {{$t('editor:markup.markdownFormattingHelp')}}
.editor-markdown-editor
@ -220,11 +195,11 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
import './markdown/fold'
// Markdown-it
import MarkdownIt from 'markdown-it'
import mdAttrs from 'markdown-it-attrs'
import mdDecorate from 'markdown-it-decorate'
import mdEmoji from 'markdown-it-emoji'
import mdTaskLists from 'markdown-it-task-lists'
import mdExpandTabs from 'markdown-it-expand-tabs'
@ -250,6 +225,7 @@ import mermaid from 'mermaid'
// Helpers
import katexHelper from './common/katex'
import tabsetHelper from './markdown/tabset'
import cmFold from './common/cmFold'
// ========================================
// INIT
@ -288,6 +264,7 @@ const md = new MarkdownIt({
.use(mdAttrs, {
allowedAttributes: ['id', 'class', 'target']
})
.use(mdDecorate)
.use(underline)
.use(mdEmoji)
.use(mdTaskLists, { label: false, labelAfter: false })
@ -335,6 +312,7 @@ md.renderer.rules.paragraph_open = injectLineNumbers
md.renderer.rules.heading_open = injectLineNumbers
md.renderer.rules.blockquote_open = injectLineNumbers
cmFold.register('markdown')
// ========================================
// PLANTUML
// ========================================
@ -346,11 +324,12 @@ plantuml.init(md, {})
// KATEX
// ========================================
const macros = {}
md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline)
md.renderer.rules.katex_inline = (tokens, idx) => {
try {
return katex.renderToString(tokens[idx].content, {
displayMode: false
displayMode: false, macros
})
} catch (err) {
console.warn(err)
@ -363,7 +342,7 @@ md.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, {
md.renderer.rules.katex_block = (tokens, idx) => {
try {
return `<p>` + katex.renderToString(tokens[idx].content, {
displayMode: true
displayMode: true, macros
}) + `</p>`
} catch (err) {
console.warn(err)

@ -6,57 +6,7 @@
.subtitle-1.white--text {{$t('editor:select.title')}}
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.primary.animated.fadeInUp(
hover
light
ripple
)
v-card-text.text-center(@click='')
img(src='/_assets/svg/editor-icon-api.svg', alt='API', style='width: 36px; opacity: .5;')
.body-2.blue--text.mt-2.text--lighten-2 API Docs
.caption.blue--text.text--lighten-1 REST / GraphQL
v-fade-transition
v-overlay(
v-if='hover'
absolute
color='primary'
opacity='.8'
)
.body-2.mt-7 Coming Soon
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.primary.animated.fadeInUp.wait-p1s(
hover
light
ripple
)
v-card-text.text-center(@click='')
img(src='/_assets/svg/editor-icon-wikitext.svg', alt='WikiText', style='width: 36px; opacity: .5;')
.body-2.blue--text.mt-2.text--lighten-2 Blog
.caption.blue--text.text--lighten-1 Timeline of Posts
v-fade-transition
v-overlay(
v-if='hover'
absolute
color='primary'
opacity='.8'
)
.body-2.mt-7 Coming Soon
v-flex(xs4)
v-card.radius-7.animated.fadeInUp.wait-p2s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("code")')
img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
.body-2.primary--text.mt-2 Code
.caption.grey--text Raw HTML
v-flex(xs4)
v-flex(xs6)
v-card.radius-7.animated.fadeInUp.wait-p1s(
hover
light
@ -66,28 +16,8 @@
img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
.body-2.primary--text.mt-2 Markdown
.caption.grey--text Plain Text Formatting
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.primary.animated.fadeInUp.wait-p2s(
hover
light
ripple
)
v-card-text.text-center(@click='')
img(src='/_assets/svg/editor-icon-tabular.svg', alt='Tabular', style='width: 36px; opacity: .5;')
.body-2.blue--text.mt-2.text--lighten-2 Tabular
.caption.blue--text.text--lighten-1 Excel-like
v-fade-transition
v-overlay(
v-if='hover'
absolute
color='primary'
opacity='.8'
)
.body-2.mt-7 Coming Soon
v-flex(xs4)
v-card.radius-7.animated.fadeInUp.wait-p3s(
v-flex(xs6)
v-card.radius-7.animated.fadeInUp.wait-p2s(
hover
light
ripple
@ -96,85 +26,36 @@
img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
.body-2.mt-2.primary--text Visual Editor
.caption.grey--text Rich-text WYSIWYG
//- .caption.blue--text.text--lighten-2 {{$t('editor:select.cannotChange')}}
v-card.radius-7.mt-2(color='teal darken-3', dark)
v-card-text.text-center.py-4
.subtitle-1.white--text {{$t('editor:select.customView')}}
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.animated.fadeInUp(
hover
light
ripple
)
v-card-text.text-center(@click='fromTemplate')
img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
.body-2.mt-1.teal--text From Template
.caption.grey--text Use an existing page...
v-card.radius-7.animated.fadeInUp.wait-p3s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("asciidoc")')
img(src='/_assets/svg/editor-icon-asciidoc.svg', alt='AsciiDoc', style='width: 36px;')
.body-2.primary--text.mt-2 AsciiDoc
.caption.grey--text Plain Text Formatting
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.teal.animated.fadeInUp.wait-p1s(
hover
light
ripple
)
//- v-card-text.text-center(@click='selectEditor("redirect")')
v-card-text.text-center(@click='')
img(src='/_assets/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
.body-2.mt-1.teal--text.text--lighten-2 Redirection
.caption.teal--text.text--lighten-1 Redirect the user to...
v-card.radius-7.animated.fadeInUp.wait-p4s(
hover
light
ripple
)
v-card-text.text-center(@click='selectEditor("code")')
img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
.body-2.primary--text.mt-2 Code
.caption.grey--text Raw HTML
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.teal.animated.fadeInUp.wait-p2s(
hover
light
ripple
)
v-card-text.text-center(@click='')
img(src='/_assets/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;')
.body-2.mt-1.teal--text.text--lighten-2 Embed
.caption.teal--text.text--lighten-1 Include external pages
v-fade-transition
v-overlay(
v-if='hover'
absolute
color='teal'
opacity='.8'
)
.body-2.mt-7 Coming Soon
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.mt-2(color='indigo darken-3', dark)
v-toolbar(dense, flat, color='light-green darken-3')
v-spacer
.caption.mr-1 or convert from
v-btn.mx-1.animated.fadeInUp(depressed, color='light-green darken-2', @click='', disabled)
v-icon(left) mdi-alpha-a-circle
.body-2.text-none AsciiDoc
v-btn.mx-1.animated.fadeInUp.wait-p1s(depressed, color='light-green darken-2', @click='', disabled)
v-icon(left) mdi-alpha-c-circle
.body-2.text-none CREOLE
v-btn.mx-1.animated.fadeInUp.wait-p2s(depressed, color='light-green darken-2', @click='', disabled)
v-icon(left) mdi-alpha-t-circle
.body-2.text-none Textile
v-btn.mx-1.animated.fadeInUp.wait-p3s(depressed, color='light-green darken-2', @click='', disabled)
v-icon(left) mdi-alpha-w-circle
.body-2.text-none WikiText
v-spacer
v-fade-transition
v-overlay(
v-if='hover'
absolute
color='light-green darken-3'
opacity='.8'
)
.body-2 Coming Soon
v-card.radius-7.animated.fadeInUp.wait-p5s(
hover
light
ripple
)
v-card-text.text-center(@click='fromTemplate')
img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
.body-2.mt-1.teal--text From Template
.caption.grey--text Use an existing page...
page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
</template>

@ -469,9 +469,9 @@ export default {
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
{ text: '(GMT-03:00) Sao Paulo', value: 'America/Sao_Paulo' },
{ text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
{ text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
{ text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },

@ -20,8 +20,7 @@
v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-4`')
v-card-text
pre
code
slot
slot
nav-footer
notify

@ -1,6 +1,6 @@
mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $injectCSS: String, $injectHead: String, $injectBody: String) {
mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $tocPosition: String, $injectCSS: String, $injectHead: String, $injectBody: String) {
theming {
setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, tocPosition: $tocPosition, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
responseResult {
succeeded
errorCode

@ -4,6 +4,7 @@ query {
theme
iconset
darkMode
tocPosition
injectCSS
injectHead
injectBody

@ -30,6 +30,12 @@ html {
}
}
@media only screen and (min-width:960px) {
.v-application .v-footer {
padding-left: 272px
}
}
#root .v-application {
.overline {
line-height: 1rem;

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M1.5 4.5H38.5V35.5H1.5z"/><path fill="#4788c7" d="M38,5v30H2V5H38 M39,4H1v32h38V4L39,4z"/><path fill="#98ccfd" d="M2 5H38V10H2z"/><path fill="#4788c7" d="M29 16H10c-.552 0-1-.448-1-1v0c0-.552.448-1 1-1h19c.552 0 1 .448 1 1v0C30 15.552 29.552 16 29 16zM11.5 22h-2C9.224 22 9 21.776 9 21.5l0 0C9 21.224 9.224 21 9.5 21h2c.276 0 .5.224.5.5l0 0C12 21.776 11.776 22 11.5 22zM29.5 22h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 21.776 29.776 22 29.5 22zM11.5 26h-2C9.224 26 9 25.776 9 25.5l0 0C9 25.224 9.224 25 9.5 25h2c.276 0 .5.224.5.5l0 0C12 25.776 11.776 26 11.5 26zM29.5 26h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 25.776 29.776 26 29.5 26zM11.5 30h-2C9.224 30 9 29.776 9 29.5l0 0C9 29.224 9.224 29 9.5 29h2c.276 0 .5.224.5.5l0 0C12 29.776 11.776 30 11.5 30zM29.5 30h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 29.776 29.776 30 29.5 30z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -41,7 +41,16 @@ const state = {
manage: false
}
},
commentsCount: 0
commentsCount: 0,
editShortcuts: {
editFab: false,
editMenuBar: false,
editMenuBtn: false,
editMenuExternalBtn: false,
editMenuExternalName: '',
editMenuExternalIcon: '',
editMenuExternalUrl: ''
}
}
export default {

@ -5,7 +5,9 @@ import { make } from 'vuex-pathify'
const state = {
company: siteConfig.company,
contentLicense: siteConfig.contentLicense,
footerOverride: siteConfig.footerOverride,
dark: siteConfig.darkMode,
tocPosition: siteConfig.tocPosition,
mascot: true,
title: siteConfig.title,
logoUrl: siteConfig.logoUrl,

@ -1,7 +1,9 @@
<template lang="pug">
v-footer.justify-center(:color='bgColor', inset)
.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`')
template(v-if='company && company.length > 0 && contentLicense !== ``')
template(v-if='footerOverride')
span(v-html='footerOverrideRender + ` |&nbsp;`')
template(v-else-if='company && company.length > 0 && contentLicense !== ``')
span(v-if='contentLicense === `alr`') {{ $t('common:footer.copyright', { company: company, year: currentYear, interpolation: { escapeValue: false } }) }} |&nbsp;
span(v-else) {{ $t('common:footer.license', { company: company, license: $t('common:license.' + contentLicense), interpolation: { escapeValue: false } }) }} |&nbsp;
span {{ $t('common:footer.poweredBy') }} #[a(href='https://wiki.js.org', ref='nofollow') Wiki.js]
@ -9,6 +11,13 @@
<script>
import { get } from 'vuex-pathify'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt({
html: false,
breaks: false,
linkify: true
})
export default {
props: {
@ -29,6 +38,11 @@ export default {
computed: {
company: get('site/company'),
contentLicense: get('site/contentLicense'),
footerOverride: get('site/footerOverride'),
footerOverrideRender () {
if (!this.footerOverride) { return '' }
return md.renderInline(this.footerOverride)
},
bgColor() {
if (!this.$vuetify.theme.dark) {
return this.color

@ -49,15 +49,50 @@
status-indicator.ml-3(negative, pulse)
v-divider
v-container.grey.pa-0(fluid, :class='$vuetify.theme.dark ? `darken-4-l3` : `lighten-4`')
v-row(no-gutters, align-content='center', style='height: 90px;')
v-col.page-col-content.is-page-header(offset-xl='2', offset-lg='3', style='margin-top: auto; margin-bottom: auto;', :class='$vuetify.rtl ? `pr-4` : `pl-4`')
.headline.grey--text(:class='$vuetify.theme.dark ? `text--lighten-2` : `text--darken-3`') {{title}}
.caption.grey--text.text--darken-1 {{description}}
v-row.page-header-section(no-gutters, align-content='center', style='height: 90px;')
v-col.page-col-content.is-page-header(
:offset-xl='tocPosition === `left` ? 2 : 0'
:offset-lg='tocPosition === `left` ? 3 : 0'
:xl='tocPosition === `right` ? 10 : false'
:lg='tocPosition === `right` ? 9 : false'
style='margin-top: auto; margin-bottom: auto;'
:class='$vuetify.rtl ? `pr-4` : `pl-4`'
)
.page-header-headings
.headline.grey--text(:class='$vuetify.theme.dark ? `text--lighten-2` : `text--darken-3`') {{title}}
.caption.grey--text.text--darken-1 {{description}}
.page-edit-shortcuts(
v-if='editShortcutsObj.editMenuBar'
:class='tocPosition === `right` ? `is-right` : ``'
)
v-btn(
v-if='editShortcutsObj.editMenuBtn'
@click='pageEdit'
depressed
small
)
v-icon.mr-2(small) mdi-pencil
span.text-none {{$t(`common:actions.edit`)}}
v-btn(
v-if='editShortcutsObj.editMenuExternalBtn'
:href='editMenuExternalUrl'
target='_blank'
depressed
small
)
v-icon.mr-2(small) {{ editShortcutsObj.editMenuExternalIcon }}
span.text-none {{$t(`common:page.editExternal`, { name: editShortcutsObj.editMenuExternalName })}}
v-divider
v-container.pl-5.pt-4(fluid, grid-list-xl)
v-layout(row)
v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp')
v-card.mb-5(v-if='tocDecoded.length')
v-flex.page-col-sd(
v-if='tocPosition !== `off` && $vuetify.breakpoint.lgAndUp'
:order-xs1='tocPosition !== `right`'
:order-xs2='tocPosition === `right`'
lg3
xl2
)
v-card.page-toc-card.mb-5(v-if='tocDecoded.length')
.overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}}
v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')
template(v-for='(tocItem, tocIdx) in tocDecoded')
@ -71,7 +106,7 @@
v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
//- v-divider(inset, v-if='tocIdx < toc.length - 1')
v-card.mb-5(v-if='tags.length > 0')
v-card.page-tags-card.mb-5(v-if='tags.length > 0')
.pa-5
.overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}
v-chip.mr-1.mb-1(
@ -91,7 +126,7 @@
)
v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
v-card.mb-5(v-if='commentsEnabled && commentsPerms.read')
v-card.page-comments-card.mb-5(v-if='commentsEnabled && commentsPerms.read')
.pa-5
.overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`')
span {{$t('common:comments.sdTitle')}}
@ -127,7 +162,7 @@
v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-1` : `blue-grey darken-2`', dense) mdi-comment-plus
span {{$t('common:comments.newComment')}}
v-card.mb-5
v-card.page-author-card.mb-5
.pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span {{$t('common:page.lastEditedBy')}}
@ -144,8 +179,8 @@
)
v-icon(color='indigo', dense) mdi-history
span {{$t('common:header.history')}}
.body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }}
.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
.page-author-card-name.body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }}
.page-author-card-date.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
//- v-card.mb-5
//- .pa-5
@ -160,13 +195,13 @@
//- )
//- .caption.grey--text 5 votes
v-card(flat)
v-card.page-shortcuts-card(flat)
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense)
v-spacer
v-tooltip(bottom)
template(v-slot:activator='{ on }')
v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark
span {{$t('common:page.bookmark')}}
//- v-tooltip(bottom)
//- template(v-slot:activator='{ on }')
//- v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark
//- span {{$t('common:page.bookmark')}}
v-menu(offset-y, bottom, min-width='300')
template(v-slot:activator='{ on: menu }')
v-tooltip(bottom)
@ -185,8 +220,14 @@
span {{$t('common:page.printFormat')}}
v-spacer
v-flex.page-col-content(xs12, lg9, xl10)
v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasAnyPagePermissions')
v-flex.page-col-content(
xs12
:lg9='tocPosition !== `off`'
:xl10='tocPosition !== `off`'
:order-xs1='tocPosition === `right`'
:order-xs2='tocPosition !== `right`'
)
v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasAnyPagePermissions && editShortcutsObj.editFab')
template(v-slot:activator='{ on: onEditActivator }')
v-speed-dial(
v-model='pageEditFab'
@ -442,6 +483,14 @@ export default {
commentsExternal: {
type: Boolean,
default: false
},
editShortcuts: {
type: String,
default: ''
},
filename: {
type: String,
default: ''
}
},
data() {
@ -480,6 +529,7 @@ export default {
isAuthenticated: get('user/authenticated'),
commentsCount: get('page/commentsCount'),
commentsPerms: get('page/effectivePermissions@comments'),
editShortcutsObj: get('page/editShortcuts'),
rating: {
get () {
return 3.5
@ -511,6 +561,7 @@ export default {
tocDecoded () {
return JSON.parse(Buffer.from(this.toc, 'base64').toString())
},
tocPosition: get('site/tocPosition'),
hasAdminPermission: get('page/effectivePermissions@system.manage'),
hasWritePagesPermission: get('page/effectivePermissions@pages.write'),
hasManagePagesPermission: get('page/effectivePermissions@pages.manage'),
@ -521,7 +572,14 @@ export default {
return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||
this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission
},
printView: sync('site/printView')
printView: sync('site/printView'),
editMenuExternalUrl () {
if (this.editShortcutsObj.editMenuBar && this.editShortcutsObj.editMenuExternalBtn) {
return this.editShortcutsObj.editMenuExternalUrl.replace('{filename}', this.filename)
} else {
return ''
}
}
},
created() {
this.$store.set('page/authorId', this.authorId)
@ -539,6 +597,9 @@ export default {
if (this.effectivePermissions) {
this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))
}
if (this.editShortcuts) {
this.$store.set('page/editShortcuts', JSON.parse(Buffer.from(this.editShortcuts, 'base64').toString()))
}
this.$store.set('page/mode', 'view')
},
@ -584,6 +645,8 @@ export default {
this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts)
}
})
window.boot.notify('page-ready')
})
if (this.containsNavElement) {
@ -766,4 +829,54 @@ export default {
display: none;
}
.page-header-section {
position: relative;
> .is-page-header {
position: relative;
}
.page-header-headings {
min-height: 52px;
display: flex;
justify-content: center;
flex-direction: column;
}
.page-edit-shortcuts {
position: absolute;
bottom: -33px;
right: 10px;
.v-btn {
border-right: 1px solid #DDD !important;
border-bottom: 1px solid #DDD !important;
border-radius: 0;
color: #777;
background-color: #FFF !important;
@at-root .theme--dark & {
background-color: #222 !important;
border-right-color: #444 !important;
border-bottom-color: #444 !important;
color: #CCC;
}
.v-icon {
color: mc('blue', '700');
}
&:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
&:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
}
}
}
</style>

@ -282,7 +282,7 @@
content: "\F02FC";
}
code {
code:not([class^="language-"]) {
background-color: mc('blue', '50');
color: mc('blue', '800');
}
@ -302,7 +302,7 @@
content: "\F0026";
}
code {
code:not([class^="language-"]) {
background-color: mc('orange', '50');
color: mc('orange', '800');
}
@ -323,7 +323,7 @@
content: "\F0159";
}
code {
code:not([class^="language-"]) {
background-color: mc('red', '50');
color: mc('red', '800');
}
@ -343,7 +343,7 @@
content: "\F0E1E";
}
code {
code:not([class^="language-"]) {
background-color: mc('green', '50');
color: mc('green', '800');
}
@ -356,6 +356,148 @@
}
}
// ---------------------------------
// ASCIIDOC SPECIFIC
// ---------------------------------
.admonitionblock {
margin: 1rem 0;
position: relative;
table {
border: none;
background-color: transparent;
width: 100%;
}
td.icon {
border-bottom-left-radius: 7px;
border-top-left-radius: 7px;
text-align: center;
width: 56px;
&::before {
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif !important;
}
}
td.content {
border-bottom-right-radius: 7px;
border-top-right-radius: 7px;
}
&.note {
td.icon {
background-color: mc('blue', '300');
color: mc('blue', '50');
&::before {
content: "\F02FC";
}
}
td.content {
color: darken(mc('blue', '900'), 10%);
background-color: mc('blue', '50');
@at-root .theme--dark & {
background-color: mc('blue', '900');
color: mc('blue', '50');
}
}
}
&.tip {
td.icon {
background-color: mc('green', '300');
color: mc('green', '50');
&::before {
content: "\F0335";
}
}
td.content {
color: darken(mc('green', '900'), 10%);
background-color: mc('green', '50');
@at-root .theme--dark & {
background-color: mc('green', '900');
color: mc('green', '50');
}
}
}
&.warning {
background-color: transparent !important;
td.icon {
background-color: mc('orange', '300');
color: #FFF;
&::before {
content: "\F0026";
}
}
td.content {
color: darken(mc('orange', '900'), 10%);
background-color: mc('orange', '50');
@at-root .theme--dark & {
background-color: darken(mc('orange', '900'), 5%);
color: mc('orange', '100');
}
}
}
&.caution {
td.icon {
background-color: mc('purple', '300');
color: mc('purple', '50');
&::before {
content: "\f0238";
}
}
td.content {
color: darken(mc('purple', '900'), 10%);
background-color: mc('purple', '50');
@at-root .theme--dark & {
background-color: mc('purple', '900');
color: mc('purple', '100');
}
}
}
&.important {
td.icon {
background-color: mc('red', '300');
color: mc('red', '50');
&::before {
content: "\F0159";
}
}
td.content {
color: darken(mc('red', '900'), 10%);
background-color: mc('red', '50');
@at-root .theme--dark & {
background-color: mc('red', '900');
color: mc('red', '100');
}
}
}
}
.exampleblock {
> .title {
font-style: italic;
font-size: 1rem !important;
color: #7a2717;
@at-root .theme--dark & {
color: mc('brown', '300');
}
}
> .content {
border: 1px solid mc('grey', '200');
border-radius: 7px;
margin-bottom: 12px;
padding: 16px;
}
}
// ---------------------------------
// LISTS
// ---------------------------------
@ -543,6 +685,27 @@
display:inline-block;
vertical-align:top;
padding-top:0;
&:first-child {
width: 100%;
}
}
}
}
dl {
dt {
margin-top: 0.3em;
margin-bottom: 0.3em;
font-weight: bold;
}
dd {
margin-left: 1.125em;
margin-bottom: 0.75em;
> p {
padding: 0;
}
}
}
@ -662,13 +825,40 @@
// ---------------------------------
table {
margin: .5rem 1.75rem;
margin: .5rem 0;
border-spacing: 0;
border-radius: 5px;
border: 1px solid mc('grey', '300');
@at-root .theme--dark & {
border-color: mc('grey', '600');
}
&.dense {
td, th {
font-size: .85rem;
padding: .5rem;
}
}
th {
padding: .75rem;
border-bottom: 2px solid mc('grey', '500');
color: mc('grey', '600');
background-color: mc('grey', '100');
@at-root .theme--dark & {
background-color: darken(mc('grey', '900'), 8%);
border-bottom-color: mc('grey', '600');
color: mc('grey', '500');
}
&:first-child {
border-top-left-radius: 7px;
}
&:last-child {
border-top-right-radius: 7px;
}
}
td {
@ -677,7 +867,56 @@
tr {
td {
border-bottom: 1px solid mc('grey', '200');
border-bottom: 1px solid mc('grey', '300');
border-right: 1px solid mc('grey', '100');
@at-root .theme--dark & {
border-bottom-color: mc('grey', '700');
border-right-color: mc('grey', '800');
}
&:nth-child(even) {
background-color: mc('grey', '50');
@at-root .theme--dark & {
background-color: darken(mc('grey', '900'), 4%);
}
}
&:last-child {
border-right: none;
}
}
&:nth-child(even) {
td {
background-color: mc('grey', '50');
@at-root .theme--dark & {
background-color: darken(mc('grey', '800'), 8%);
}
&:nth-child(even) {
background-color: mc('grey', '100');
@at-root .theme--dark & {
background-color: darken(mc('grey', '800'), 10%);
}
}
}
}
&:last-child {
td {
border-bottom: none;
&:first-child {
border-bottom-left-radius: 7px;
}
&:last-child {
border-bottom-right-radius: 7px;
}
}
}
}
}
@ -699,6 +938,7 @@
border: 1px solid mc('blue-grey', '100');
box-shadow: inset -1px -1px 0 0 #FFF, inset 1px 0 0 #FFF;
padding: .5rem .75rem;
border-radius: 0 !important;
@at-root .theme--dark & {
border-color: mc('grey', '700');
@ -735,6 +975,12 @@
}
}
// -> Add horizontal scrollbar when table is too wide
.table-container {
width: 100%;
overflow-x: auto;
}
// ---------------------------------
// IMAGES
// ---------------------------------
@ -1036,4 +1282,8 @@
.comments-container {
display: none;
}
.page-edit-shortcuts {
display: none;
}
}

@ -115,6 +115,7 @@ The following table lists the configurable parameters of the Wiki.js chart and t
| `sideload.enabled` | Enable sideloading of locale files from git | `false` |
| `sideload.repoURL` | Git repository URL containing locale files | `https://github.com/Requarks/wiki-localization` |
| `sideload.env` | Environment variables for sideload Container | `{}` |
| `nodeExtraCaCerts` | Trusted certificates path | `nil` |
| `postgresql.enabled` | Deploy postgres server (see below) | `true` |
| `postgresql.postgresqlDatabase` | Postgres database name | `wiki` |
| `postgresql.postgresqlUser` | Postgres username | `postgres` |
@ -175,3 +176,38 @@ See the [Configuration](#configuration) section to configure the PVC or to disab
## Ingress
This chart provides support for Ingress resource. If you have an available Ingress Controller such as Nginx or Traefik you maybe want to set `ingress.enabled` to true and add `ingress.hosts` for the URL. Then, you should be able to access the installation using that address.
## Extra Trusted Certificates
To append extra CA Certificates:
1. Create a ConfigMap with CAs in PEM format, e.g.:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ca
namespace: your-wikijs-namespace
data:
certs.pem: |-
-----BEGIN CERTIFICATE-----
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-----END CERTIFICATE-----
```
2. Mount your CAs from the ConfigMap to the Wiki.js pod and set `nodeExtraCaCerts` helm variable. Insert the following lines to your Wiki.js `values.yaml`, e.g.:
```yaml
volumeMounts:
- name: ca
mountPath: /cas.pem
subPath: certs.pem
volumes:
- name: ca
configMap:
name: ca
nodeExtraCaCerts: "/cas.pem"
```

@ -39,6 +39,10 @@ spec:
image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}"
imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }}
env:
{{- if .Values.nodeExtraCaCerts }}
- name: NODE_EXTRA_CA_CERTS
value: {{ .Values.nodeExtraCaCerts }}
{{- end }}
- name: DB_TYPE
value: postgres
{{- if (.Values.externalPostgresql).databaseURL }}

@ -11,6 +11,9 @@ metadata:
{{- end }}
spec:
type: {{.Values.service.type}}
{{- if eq .Values.service.type "LoadBalancer" }}
loadBalancerIP: {{ default "" .Values.service.loadBalancerIP }}
{{- end }}
ports:
- port: {{ default "80" .Values.service.port}}
targetPort: http

@ -61,6 +61,7 @@ service:
# type: LoadBalancer
# httpsPort: 443
# annotations: {}
# loadBalancerIP: 172.16.0.1
ingress:
enabled: true
@ -101,7 +102,7 @@ volumeMounts: []
volumes: []
# This will allow us to install locales even without internet access using a initContainer & wikjs "sideloading"
# This will allow us to install locales even without internet access using a initContainer & Wiki.js "sideloading"
sideload:
enabled: false
# Git-Repo containing all locales.json-files you need:
@ -112,6 +113,9 @@ sideload:
# - name: HTTPS_PROXY
# value: http://my.proxy.com:3128
## Append extra trusted certificates for node process from extra volume via NODE_EXTRA_CA_CERTS variable
# nodeExtraCaCerts: "/path/to/certs.pem"
## This will override the postgresql chart values
# externalPostgresql:
# # note: ?sslmode=require => ?ssl=true

@ -78,7 +78,8 @@
"scripts/013-docker-dns.sh",
"scripts/014-ufw-docker.sh",
"scripts/020-application-tag.sh",
"scripts/900-cleanup.sh"
"scripts/900-cleanup.sh",
"scripts/999-img-check.sh"
]
}
]

@ -8,14 +8,17 @@ if [[ ! -d /tmp ]]; then
fi
chmod 1777 /tmp
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -y upgrade
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes
apt-get purge droplet-agent
rm -rf /opt/digitalocean
apt-get -y autoremove
apt-get -y autoclean
rm -rf /tmp/* /var/tmp/*
history -c
cat /dev/null > /root/.bash_history
unset HISTFILE
apt-get -y autoremove
apt-get -y autoclean
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
rm -rf /var/lib/cloud/instances/*

@ -0,0 +1,628 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021-2022 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
VERSION="v. 1.8"
RUNDATE=$( date )
# Script should be run with SUDO
if [ "$EUID" -ne 0 ]
then echo "[Error] - This script must be run with sudo or as the root user."
exit 1
fi
STATUS=0
PASS=0
WARN=0
FAIL=0
# $1 == command to check for
# returns: 0 == true, 1 == false
cmdExists() {
if command -v "$1" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
function getDistro {
if [ -f /etc/os-release ]; then
# freedesktop.org and systemd
# shellcheck disable=SC1091
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
elif type lsb_release >/dev/null 2>&1; then
# linuxbase.org
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
elif [ -f /etc/lsb-release ]; then
# For some versions of Debian/Ubuntu without lsb_release command
# shellcheck disable=SC1091
. /etc/lsb-release
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
elif [ -f /etc/debian_version ]; then
# Older Debian/Ubuntu/etc.
OS=Debian
VER=$(cat /etc/debian_version)
elif [ -f /etc/SuSe-release ]; then
# Older SuSE/etc.
:
elif [ -f /etc/redhat-release ]; then
# Older Red Hat, CentOS, etc.
VER=$(cut -d" " -f3 < /etc/redhat-release | cut -d "." -f1)
d=$(cut -d" " -f1 < /etc/redhat-release | cut -d "." -f1)
if [[ $d == "CentOS" ]]; then
OS="CentOS Linux"
fi
else
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
OS=$(uname -s)
VER=$(uname -r)
fi
}
function loadPasswords {
SHADOW=$(cat /etc/shadow)
}
function checkAgent {
# Check for the presence of the DO directory in the filesystem
if [ -d /opt/digitalocean ];then
echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n"
((FAIL++))
STATUS=2
if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
echo "To uninstall the agent: 'sudo yum remove droplet-agent'"
echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'"
elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then
echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'"
fi
else
echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
((PASS++))
fi
}
function checkLogs {
cp_ignore="/var/log/cpanel-install.log"
echo -en "\nChecking for log files in /var/log\n\n"
# Check if there are log archives or log files that have not been recently cleared.
for f in /var/log/*-????????; do
[[ -e $f ]] || break
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
done
for f in /var/log/*.[0-9];do
[[ -e $f ]] || break
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
done
for f in /var/log/*.log; do
[[ -e $f ]] || break
if [[ "${f}" = '/var/log/lfd.log' && "$(grep -E -v '/var/log/messages has been reset| Watching /var/log/messages' "${f}" | wc -c)" -gt 50 ]]; then
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [[ "${f}" != '/var/log/lfd.log' && "$(wc -c < "${f}")" -gt 50 ]]; then
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
}
function checkTMP {
# Check the /tmp directory to ensure it is empty. Warn on any files found.
return 1
}
function checkRoot {
user="root"
uhome="/root"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
((FAIL++))
STATUS=2
fi
fi
done
if [ -d ${uhome}/ ]; then
if [ -d ${uhome}/.ssh/ ]; then
if ls ${uhome}/.ssh/*> /dev/null 2>&1; then
for key in "${uhome}"/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$(wc -c < "${key}")" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
if [ -f /root/.bash_history ];then
BH_S=$(wc -c < /root/.bash_history)
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
return 1;
else
echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
((PASS++))
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
echo -en "\n\n"
return 1
}
function checkUsers {
# Check each user-created account
awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' < /etc/passwd | while IFS= read -r user;
do
# Skip some other non-user system accounts
if [[ $user == "centos" ]]; then
:
elif [[ $user == "nfsnobody" ]]; then
:
else
echo -en "\nChecking user: ${user}...\n"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
# shellcheck disable=SC2030
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
# shellcheck disable=SC2030
((FAIL++))
STATUS=2
fi
fi
done
#echo "User Found: ${user}"
uhome="/home/${user}"
if [ -d "${uhome}/" ]; then
if [ -d "${uhome}/.ssh/" ]; then
if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
for key in "${uhome}"/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$(wc -c < "${key}")" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
# shellcheck disable=SC2030
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
# Check for an uncleared .bash_history for this user
if [ -f "${uhome}/.bash_history" ]; then
BH_S=$(wc -c < "${uhome}/.bash_history")
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
echo -en "\n\n"
fi
fi
done
}
function checkFirewall {
if [[ $OS == "Ubuntu" ]]; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
# shellcheck disable=SC2031
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
# shellcheck disable=SC2031
((WARN++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
if [ -f /usr/lib/systemd/system/csf.service ]; then
fw="csf"
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
elif cmdExists "firewall-cmd"; then
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
fw="firewalld"
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
elif [[ "$OS" =~ Debian.* ]]; then
# user could be using a number of different services for managing their firewall
# we will check some of the most common
if cmdExists 'ufw'; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
elif cmdExists "firewall-cmd"; then
fw="firewalld"
if [[ $(systemctl is-active --quiet $fw) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
# user could be using vanilla iptables, check if kernel module is loaded
fw="iptables"
if lsmod | grep -q '^ip_tables' 2>/dev/null; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
fi
}
function checkUpdates {
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
if [[ ! -d /tmp ]]; then
mkdir /tmp
fi
chmod 1777 /tmp
echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
apt-get -y update > /dev/null
uc=$(apt-get --just-print upgrade | grep -i "security" -c)
if [[ $uc -gt 0 ]]; then
update_count=$(( uc / 2 ))
else
update_count=0
fi
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
echo -en
echo -en "Here is a list of the security updates that are not installed:\n"
sleep 2
apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
echo -en
# shellcheck disable=SC2031
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
((PASS++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
update_count=$(yum check-update --security --quiet | wc -l)
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
((PASS++))
fi
else
echo "Error encountered"
exit 1
fi
return 1;
}
function checkCloudInit {
if hash cloud-init 2>/dev/null; then
CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
((PASS++))
else
CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
((FAIL++))
STATUS=2
fi
return 1
}
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
clear
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
echo "Executed on: ${RUNDATE}"
echo "Checking local system for Marketplace compatibility..."
getDistro
echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"
ost=0
osv=0
if [[ $OS == "Ubuntu" ]]; then
ost=1
if [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then
osv=1
fi
elif [[ "$OS" =~ Debian.* ]]; then
ost=1
case "$VER" in
9)
osv=1
;;
10)
osv=1
;;
11)
osv=1
;;
*)
osv=2
;;
esac
elif [[ $OS == "CentOS Linux" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
elif [[ $VER == "7" ]]; then
osv=1
elif [[ $VER == "6" ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "CentOS Stream" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "Rocky Linux" ]]; then
ost=1
if [[ $VER =~ 8\. ]]; then
osv=1
else
osv=2
fi
else
ost=0
fi
if [[ $ost == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
((FAIL++))
STATUS=2
fi
if [[ $osv == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
((PASS++))
elif [[ $ost == 1 ]]; then
echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
((FAIL++))
STATUS=2
else
echo "Exiting..."
exit 1
fi
checkCloudInit
echo -en "${CI}"
checkFirewall
echo -en "${FW_VER}"
checkUpdates
loadPasswords
checkLogs
echo -en "\n\nChecking all user-created accounts...\n"
checkUsers
echo -en "\n\nChecking the root account...\n"
checkRoot
checkAgent
# Summary
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
elif [[ $STATUS == 1 ]]; then
echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n"
else
echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n"
fi
echo "---------------------------------------------------------------------------------------------------"
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
echo -en "---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
exit 0
elif [[ $STATUS == 1 ]]; then
echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
exit 0
else
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
exit 1
fi

@ -36,9 +36,9 @@
"node": ">=10.12"
},
"dependencies": {
"@azure/storage-blob": "12.9.0",
"@azure/storage-blob": "12.12.0",
"@exlinc/keycloak-passport": "1.0.2",
"@joplin/turndown-plugin-gfm": "1.0.43",
"@joplin/turndown-plugin-gfm": "1.0.45",
"@root/csr": "0.8.1",
"@root/keypairs": "0.10.3",
"@root/pem": "1.0.4",
@ -48,17 +48,18 @@
"apollo-fetch": "0.7.0",
"apollo-server": "2.25.2",
"apollo-server-express": "2.25.2",
"asciidoctor": "2.2.6",
"auto-load": "3.0.4",
"aws-sdk": "2.1125.0",
"aws-sdk": "2.1309.0",
"azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1",
"bluebird": "3.7.2",
"body-parser": "1.20.0",
"body-parser": "1.20.1",
"chalk": "4.1.0",
"cheerio": "1.0.0-rc.5",
"chokidar": "3.5.3",
"chromium-pickle-js": "0.2.0",
"clean-css": "4.2.3",
"clean-css": "5.3.2",
"command-exists": "1.2.9",
"compression": "1.7.4",
"connect-session-knex": "2.0.0",
@ -66,18 +67,18 @@
"cors": "2.8.5",
"cuint": "0.2.2",
"custom-error-instance": "2.1.2",
"dependency-graph": "0.9.0",
"dependency-graph": "0.11.0",
"diff": "4.0.2",
"diff2html": "3.1.14",
"dompurify": "2.2.7",
"dompurify": "2.4.3",
"dotize": "0.3.0",
"elasticsearch6": "npm:@elastic/elasticsearch@6",
"elasticsearch7": "npm:@elastic/elasticsearch@7",
"emoji-regex": "9.2.2",
"eventemitter2": "6.4.5",
"express": "4.18.1",
"emoji-regex": "10.2.1",
"eventemitter2": "6.4.9",
"express": "4.18.2",
"express-brute": "1.0.1",
"express-session": "1.17.2",
"express-session": "1.17.3",
"file-type": "15.0.1",
"filesize": "6.1.0",
"fs-extra": "9.0.1",
@ -93,11 +94,11 @@
"i18next-express-middleware": "2.0.0",
"i18next-node-fs-backend": "2.1.3",
"image-size": "0.9.2",
"js-base64": "3.7.2",
"js-base64": "3.7.4",
"js-binary": "1.2.0",
"js-yaml": "3.14.0",
"jsdom": "16.4.0",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "9.0.0",
"katex": "0.12.0",
"klaw": "3.0.0",
"knex": "0.21.7",
@ -106,6 +107,7 @@
"markdown-it": "11.0.1",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "3.0.3",
"markdown-it-decorate": "1.2.2",
"markdown-it-emoji": "1.4.0",
"markdown-it-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6",
@ -117,23 +119,24 @@
"markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mathjax": "3.1.2",
"markdown-it-pivot-table": "1.0.1",
"mathjax": "3.2.2",
"mime-types": "2.1.35",
"moment": "2.29.3",
"moment-timezone": "0.5.31",
"moment": "2.29.4",
"moment-timezone": "0.5.40",
"mongodb": "3.6.5",
"ms": "2.1.3",
"mssql": "6.2.3",
"multer": "1.4.4",
"mysql2": "2.3.3",
"mysql2": "3.1.0",
"nanoid": "3.2.0",
"node-2fa": "1.1.2",
"node-cache": "5.1.2",
"nodemailer": "6.7.4",
"nodemailer": "6.9.1",
"objection": "2.2.18",
"passport": "0.4.1",
"passport-auth0": "1.4.2",
"passport-azure-ad": "4.3.1",
"passport-auth0": "1.4.3",
"passport-azure-ad": "4.3.4",
"passport-cas": "0.1.1",
"passport-discord": "0.1.4",
"passport-dropbox-oauth2": "1.1.0",
@ -141,50 +144,50 @@
"passport-github2": "0.1.12",
"passport-gitlab2": "5.0.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"passport-jwt": "4.0.1",
"passport-ldapauth": "3.0.1",
"passport-local": "1.0.0",
"passport-microsoft": "0.1.0",
"passport-oauth2": "1.6.1",
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.0.2",
"passport-saml": "3.2.1",
"passport-openidconnect": "0.1.1",
"passport-saml": "3.2.4",
"passport-slack-oauth2": "1.1.1",
"passport-twitch-strategy": "2.2.0",
"pem-jwk": "2.0.0",
"pg": "8.4.1",
"pg": "8.9.0",
"pg-hstore": "2.3.4",
"pg-pubsub": "0.5.0",
"pg-query-stream": "3.3.1",
"pg-tsquery": "8.1.0",
"pg-query-stream": "4.3.0",
"pg-tsquery": "8.4.1",
"pug": "3.0.2",
"punycode": "2.1.1",
"punycode": "2.3.0",
"qr-image": "3.2.0",
"raven": "2.6.4",
"remove-markdown": "0.3.0",
"remove-markdown": "0.5.0",
"request": "2.88.2",
"request-promise": "4.2.6",
"safe-regex": "2.1.1",
"sanitize-filename": "1.6.3",
"scim-query-filter-parser": "2.0.4",
"semver": "7.3.7",
"semver": "7.3.8",
"serve-favicon": "2.5.0",
"simple-git": "2.21.0",
"simple-git": "3.16.0",
"solr-node": "1.2.1",
"sqlite3": "5.0.6",
"ssh2": "1.5.0",
"ssh2-promise": "1.0.2",
"sqlite3": "5.1.4",
"ssh2": "1.11.0",
"ssh2-promise": "1.0.3",
"striptags": "3.2.0",
"subscriptions-transport-ws": "0.9.18",
"tar-fs": "2.1.1",
"turndown": "7.1.1",
"twemoji": "13.1.0",
"twemoji": "14.0.2",
"uslug": "1.0.4",
"uuid": "8.3.2",
"uuid": "9.0.0",
"validate.js": "0.13.1",
"winston": "3.3.3",
"xss": "1.0.11",
"yargs": "16.1.0"
"winston": "3.8.2",
"xss": "1.0.14",
"yargs": "17.6.2"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
@ -212,7 +215,7 @@
"apollo-link-batch-http": "1.2.14",
"apollo-link-error": "1.1.13",
"apollo-link-http": "1.5.17",
"apollo-link-persisted-queries": "0.2.2",
"apollo-link-persisted-queries": "0.2.5",
"apollo-link-ws": "1.0.20",
"apollo-utilities": "1.3.4",
"autoprefixer": "9.8.6",
@ -225,11 +228,12 @@
"babel-plugin-transform-imports": "2.0.0",
"cache-loader": "4.1.0",
"canvas-confetti": "1.3.1",
"cash-dom": "8.1.1",
"cash-dom": "8.1.3",
"chart.js": "2.9.4",
"clean-webpack-plugin": "3.0.0",
"clipboard": "2.0.10",
"clipboard": "2.0.11",
"codemirror": "5.58.2",
"codemirror-asciidoc": "1.0.4",
"copy-webpack-plugin": "6.2.1",
"core-js": "3.6.5",
"css-loader": "4.3.0",
@ -249,7 +253,7 @@
"eslint-plugin-vue": "7.1.0",
"file-loader": "6.1.1",
"filepond": "4.21.1",
"filepond-plugin-file-validate-type": "1.2.7",
"filepond-plugin-file-validate-type": "1.2.8",
"filesize.js": "2.0.0",
"graphql-persisted-document-loader": "2.0.0",
"graphql-tag": "2.11.0",
@ -276,7 +280,7 @@
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"postcss-selector-parser": "6.0.10",
"postcss-selector-parser": "6.0.11",
"prismjs": "1.22.0",
"pug-lint": "2.6.0",
"pug-loader": "2.4.0",
@ -317,7 +321,7 @@
"webpack-bundle-analyzer": "3.9.0",
"webpack-cli": "3.3.12",
"webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.1",
"webpack-hot-middleware": "2.25.3",
"webpack-merge": "5.2.0",
"webpack-modernizr-loader": "5.0.0",
"webpack-subresource-integrity": "1.5.1",

@ -44,6 +44,7 @@ defaults:
title: Wiki.js
company: ''
contentLicense: ''
footerOverride: ''
logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
pageExtensions:
- md
@ -59,6 +60,7 @@ defaults:
theme: 'default'
iconset: 'md'
darkMode: false
tocPosition: 'left'
auth:
autoLogin: false
enforce2FA: false
@ -67,6 +69,14 @@ defaults:
audience: 'urn:wiki.js'
tokenExpiration: '30m'
tokenRenewal: '14d'
editShortcuts:
editFab: true
editMenuBar: false
editMenuBtn: true
editMenuExternalBtn: true
editMenuExternalName: 'GitHub'
editMenuExternalIcon: 'mdi-github'
editMenuExternalUrl: 'https://github.com/org/repo/blob/main/{filename}'
features:
featurePageRatings: true
featurePageComments: true

@ -4,6 +4,7 @@ const pageHelper = require('../helpers/page')
const _ = require('lodash')
const CleanCSS = require('clean-css')
const moment = require('moment')
const qs = require('querystring')
/* global WIKI */
@ -420,7 +421,8 @@ router.get('/*', async (req, res, next) => {
if (isPage) {
if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
const query = !_.isEmpty(req.query) ? `?${qs.stringify(req.query)}` : ''
return res.redirect(`/${pageArgs.locale}/${pageArgs.path}${query}`)
}
req.i18n.changeLanguage(pageArgs.locale)
@ -542,13 +544,18 @@ router.get('/*', async (req, res, next) => {
})
}
// -> Page Filename (for edit on external repo button)
let pageFilename = WIKI.config.lang.namespacing ? `${pageArgs.locale}/${page.path}` : page.path
pageFilename += page.contentType === 'markdown' ? '.md' : '.html'
// -> Render view
res.render('page', {
page,
sidebar,
injectCode,
comments: commentTmpl,
effectivePermissions
effectivePermissions,
pageFilename
})
}
} else if (pageArgs.path === 'home') {

@ -28,8 +28,7 @@ router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
*/
router.all('/*', (req, res, next) => {
if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
return res.redirect(`https://${req.hostname}${req.originalUrl}`)
} else {
next()
}

@ -138,7 +138,7 @@ module.exports = {
switch (WIKI.config.db.type) {
case 'postgres':
await conn.query(`set application_name = 'Wiki.js'`)
// -> Set schema if it's not public
// -> Set schema if it's not public
if (WIKI.config.db.schema && WIKI.config.db.schema !== 'public') {
await conn.query(`set search_path TO ${WIKI.config.db.schema}, public;`)
}

@ -113,7 +113,7 @@ module.exports = {
}
}
})
WIKI.logger.info(`(LETSENCRYPT) New certifiate received successfully: [ COMPLETED ]`)
WIKI.logger.info(`(LETSENCRYPT) New certificate received successfully: [ COMPLETED ]`)
WIKI.config.letsencrypt.payload = certResp
WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain
await WIKI.configSvc.saveToDb(['letsencrypt'])

@ -52,7 +52,8 @@ module.exports = {
strings: lcObj,
isRTL: locale.isRTL,
name: locale.name,
nativeName: locale.nativeName
nativeName: locale.nativeName,
availability: locale.availability || 0
}).where('code', locale.code)
} else {
await WIKI.models.locales.query().insert({
@ -60,7 +61,8 @@ module.exports = {
strings: lcObj,
isRTL: locale.isRTL,
name: locale.name,
nativeName: locale.nativeName
nativeName: locale.nativeName,
availability: locale.availability || 0
})
}
importedLocales++

@ -40,9 +40,13 @@ module.exports = {
* Fetch list of comments for a page
*/
async list (obj, args, context) {
const page = await WIKI.models.pages.query().select('id').findOne({ localeCode: args.locale, path: args.path })
const page = await WIKI.models.pages.query().select('pages.id').findOne({ localeCode: args.locale, path: args.path })
.withGraphJoined('tags')
.modifyGraph('tags', builder => {
builder.select('tag')
})
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], { tags: page.tags, ...args })) {
const comments = await WIKI.models.comments.query().where('pageId', page.id).orderBy('createdAt')
return comments.map(c => ({
...c,
@ -66,10 +70,15 @@ module.exports = {
throw new WIKI.Error.CommentNotFound()
}
const page = await WIKI.models.pages.query().select('localeCode', 'path').findById(cm.pageId)
.withGraphJoined('tags')
.modifyGraph('tags', builder => {
builder.select('tag')
})
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], {
path: page.path,
locale: page.localeCode
locale: page.localeCode,
tags: page.tags
})) {
return {
...cm,

@ -159,7 +159,33 @@ module.exports = {
return {
...page,
locale: page.localeCode,
editor: page.editorKey
editor: page.editorKey,
scriptJs: page.extra.js,
scriptCss: page.extra.css
}
} else {
throw new WIKI.Error.PageViewForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
},
async singleByPath(obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb({
path: args.path,
locale: args.locale,
});
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {
path: page.path,
locale: page.localeCode
})) {
return {
...page,
locale: page.localeCode,
editor: page.editorKey,
scriptJs: page.extra.js,
scriptCss: page.extra.css
}
} else {
throw new WIKI.Error.PageViewForbidden()

@ -17,9 +17,11 @@ module.exports = {
title: WIKI.config.title,
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl,
pageExtensions: WIKI.config.pageExtensions.join(', '),
...WIKI.config.seo,
...WIKI.config.editShortcuts,
...WIKI.config.features,
...WIKI.config.security,
authAutoLogin: WIKI.config.auth.autoLogin,
@ -59,6 +61,10 @@ module.exports = {
WIKI.config.contentLicense = args.contentLicense
}
if (args.hasOwnProperty('footerOverride')) {
WIKI.config.footerOverride = args.footerOverride
}
if (args.hasOwnProperty('logoUrl')) {
WIKI.config.logoUrl = _.trim(args.logoUrl)
}
@ -84,6 +90,16 @@ module.exports = {
tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal)
}
WIKI.config.editShortcuts = {
editFab: _.get(args, 'editFab', WIKI.config.editShortcuts.editFab),
editMenuBar: _.get(args, 'editMenuBar', WIKI.config.editShortcuts.editMenuBar),
editMenuBtn: _.get(args, 'editMenuBtn', WIKI.config.editShortcuts.editMenuBtn),
editMenuExternalBtn: _.get(args, 'editMenuExternalBtn', WIKI.config.editShortcuts.editMenuExternalBtn),
editMenuExternalName: _.get(args, 'editMenuExternalName', WIKI.config.editShortcuts.editMenuExternalName),
editMenuExternalIcon: _.get(args, 'editMenuExternalIcon', WIKI.config.editShortcuts.editMenuExternalIcon),
editMenuExternalUrl: _.get(args, 'editMenuExternalUrl', WIKI.config.editShortcuts.editMenuExternalUrl)
}
WIKI.config.features = {
featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),
featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),
@ -109,7 +125,7 @@ module.exports = {
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'features', 'security', 'uploads'])
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'footerOverride', 'seo', 'logoUrl', 'pageExtensions', 'auth', 'editShortcuts', 'features', 'security', 'uploads'])
if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy')

@ -90,7 +90,10 @@ module.exports = {
if (process.env.UPGRADE_COMPANION) {
await request({
method: 'POST',
uri: 'http://wiki-update-companion/upgrade'
uri: 'http://wiki-update-companion/upgrade',
qs: {
...process.env.UPGRADE_COMPANION_REF && { container: process.env.UPGRADE_COMPANION_REF }
}
})
return {
responseResult: graphHelper.generateSuccess('Upgrade has started.')

@ -24,6 +24,7 @@ module.exports = {
theme: WIKI.config.theming.theme,
iconset: WIKI.config.theming.iconset,
darkMode: WIKI.config.theming.darkMode,
tocPosition: WIKI.config.theming.tocPosition || 'left',
injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles,
injectHead: WIKI.config.theming.injectHead,
injectBody: WIKI.config.theming.injectBody
@ -44,6 +45,7 @@ module.exports = {
theme: args.theme,
iconset: args.iconset,
darkMode: args.darkMode,
tocPosition: args.tocPosition || 'left',
injectCSS: args.injectCSS || '',
injectHead: args.injectHead || '',
injectBody: args.injectBody || ''

@ -46,6 +46,11 @@ type PageQuery {
id: Int!
): Page @auth(requires: ["read:pages", "manage:system"])
singleByPath(
path: String!
locale: String!
): Page @auth(requires: ["read:pages", "manage:system"])
tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
searchTags(

@ -32,6 +32,7 @@ type SiteMutation {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
@ -41,6 +42,13 @@ type SiteMutation {
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
editFab: Boolean
editMenuBar: Boolean
editMenuBtn: Boolean
editMenuExternalBtn: Boolean
editMenuExternalName: String
editMenuExternalIcon: String
editMenuExternalUrl: String
featurePageRatings: Boolean
featurePageComments: Boolean
featurePersonalWikis: Boolean
@ -74,6 +82,7 @@ type SiteConfig {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
@ -83,6 +92,13 @@ type SiteConfig {
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
editFab: Boolean
editMenuBar: Boolean
editMenuBtn: Boolean
editMenuExternalBtn: Boolean
editMenuExternalName: String
editMenuExternalIcon: String
editMenuExternalUrl: String
featurePageRatings: Boolean
featurePageComments: Boolean
featurePersonalWikis: Boolean

@ -28,6 +28,7 @@ type ThemingMutation {
theme: String!
iconset: String!
darkMode: Boolean!
tocPosition: String
injectCSS: String
injectHead: String
injectBody: String
@ -42,6 +43,7 @@ type ThemingConfig {
theme: String!
iconset: String!
darkMode: Boolean!
tocPosition: String
injectCSS: String
injectHead: String
injectBody: String

@ -10,6 +10,7 @@ const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
const contentToExt = {
markdown: 'md',
asciidoc: 'adoc',
html: 'html'
}
const extToContent = _.invert(contentToExt)

@ -6,6 +6,19 @@
const path = require('path')
const { nanoid } = require('nanoid')
const { DateTime } = require('luxon')
const { gte } = require('semver')
// ----------------------------------------
// Check Node.js version
// ----------------------------------------
if (gte(process.version, '18.0.0')) {
console.error('You\'re using an unsupported Node.js version. Please read the requirements.')
process.exit(1)
}
// ----------------------------------------
// Init WIKI instance
// ----------------------------------------
let WIKI = {
IS_DEBUG: process.env.NODE_ENV === 'development',

@ -149,10 +149,12 @@ module.exports = async () => {
title: WIKI.config.title,
theme: WIKI.config.theming.theme,
darkMode: WIKI.config.theming.darkMode,
tocPosition: WIKI.config.theming.tocPosition || 'left',
lang: WIKI.config.lang.code,
rtl: WIKI.config.lang.rtl,
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl
}
res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true })

@ -92,6 +92,7 @@ module.exports = class Authentication extends Model {
}
for (const strategy of dbStrategies) {
let newProps = false
const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey])
if (!strategyDef) {
await WIKI.models.authentication.query().delete().where('key', strategy.key)
@ -101,6 +102,8 @@ module.exports = class Authentication extends Model {
strategy.config = _.transform(strategyDef.props, (result, value, key) => {
if (!_.has(result, key)) {
_.set(result, key, value.default)
// we have some new properties added to an existing auth strategy to write to the database
newProps = true
}
return result
}, strategy.config)
@ -111,6 +114,12 @@ module.exports = class Authentication extends Model {
displayName: strategyDef.title
}).where('key', strategy.key)
}
// write existing auth model to database with new properties and defaults
if (newProps) {
await WIKI.models.authentication.query().patch({
config: strategy.config
}).where('key', strategy.key)
}
}
WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`)

@ -99,7 +99,8 @@ module.exports = class Comment extends Model {
if (page) {
if (!WIKI.auth.checkAccess(user, ['write:comments'], {
path: page.path,
locale: page.localeCode
locale: page.localeCode,
tags: page.tags
})) {
throw new WIKI.Error.CommentPostForbidden()
}
@ -136,7 +137,8 @@ module.exports = class Comment extends Model {
if (page) {
if (!WIKI.auth.checkAccess(user, ['manage:comments'], {
path: page.path,
locale: page.localeCode
locale: page.localeCode,
tags: page.tags
})) {
throw new WIKI.Error.CommentManageForbidden()
}
@ -169,7 +171,8 @@ module.exports = class Comment extends Model {
if (page) {
if (!WIKI.auth.checkAccess(user, ['manage:comments'], {
path: page.path,
locale: page.localeCode
locale: page.localeCode,
tags: page.tags
})) {
throw new WIKI.Error.CommentManageForbidden()
}

@ -101,6 +101,8 @@ module.exports = class Editor extends Model {
return 'markdown'
case 'html':
return 'ckeditor'
case 'asciidoc':
return 'asciidoc'
default:
return 'code'
}

@ -148,6 +148,7 @@ module.exports = class Page extends Model {
isPublished: 'boolean',
publishEndDate: 'string',
publishStartDate: 'string',
contentType: 'string',
render: 'string',
tags: [
{
@ -724,7 +725,7 @@ module.exports = class Page extends Model {
const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
// -> Move page
const destinationTitle = (page.title === page.path ? opts.destinationPath : page.title)
const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title)
await WIKI.models.pages.query().patch({
path: opts.destinationPath,
localeCode: opts.destinationLocale,
@ -744,6 +745,7 @@ module.exports = class Page extends Model {
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
title: destinationTitle,
destinationHash
})
@ -787,7 +789,7 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise with no value
*/
static async deletePage(opts) {
const page = await WIKI.models.pages.getPageFromDb(_.has(opts, 'id') ? opts.id : opts);
const page = await WIKI.models.pages.getPageFromDb(_.has(opts, 'id') ? opts.id : opts)
if (!page) {
throw new WIKI.Error.PageNotFound()
}
@ -958,9 +960,8 @@ module.exports = class Page extends Model {
// -> Save render to cache
await WIKI.models.pages.savePageToCache(page)
} else {
// -> No render? Possible duplicate issue
/* TODO: Detect duplicate and delete */
throw new Error('Error while fetching page. Duplicate entry detected. Reload the page to try again.')
// -> No render? Last page render failed...
throw new Error('Page has no rendered version. Looks like the Last page render failed. Try to edit the page and save it again.')
}
}
}
@ -1067,6 +1068,7 @@ module.exports = class Page extends Model {
isPublished: page.isPublished === 1 || page.isPublished === true,
publishEndDate: page.publishEndDate,
publishStartDate: page.publishStartDate,
contentType: page.contentType,
render: page.render,
tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
title: page.title,

@ -9,5 +9,5 @@ props:
propertyTrackingId:
type: String
title: Property Tracking ID
hint: UA-XXXXXXX-X
hint: G-XXXXXXXXXX
order: 1

@ -0,0 +1,2 @@
head: |
<script async defer data-website-id="{{websiteID}}" src="{{url}}/umami.js"></script>

@ -0,0 +1,17 @@
key: umami
title: Umami Analytics v1
description: Umami is a simple, fast, privacy-focused alternative to Google Analytics.
author: CDN18
logo: https://static.requarks.io/logo/umami.svg
website: https://umami.is
isAvailable: true
props:
websiteID:
type: String
title: Website ID
order: 1
url:
type: String
title: Umami Server URL
hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com)
order: 2

@ -0,0 +1,2 @@
head: |
<script async src="{{url}}/script.js" data-website-id="{{websiteID}}"></script>

@ -0,0 +1,17 @@
key: umami2
title: Umami Analytics v2
description: Umami is a simple, fast, privacy-focused alternative to Google Analytics.
author: CDN18
logo: https://static.requarks.io/logo/umami.svg
website: https://umami.is
isAvailable: true
props:
websiteID:
type: String
title: Website ID
order: 1
url:
type: String
title: Umami Server URL
hint: The URL of your Umami instance. It should start with http/https and omit the trailing slash. (e.g. https://umami.example.com)
order: 2

@ -15,6 +15,8 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
baseURL: conf.baseUrl,
authorizationURL: conf.authorizationURL || (conf.baseUrl + '/oauth/authorize'),
tokenURL: conf.tokenURL || (conf.baseUrl + '/oauth/token'),
scope: ['read_user'],
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => {

@ -24,3 +24,13 @@ props:
hint: For self-managed GitLab instances, define the base URL (e.g. https://gitlab.example.com). Leave default for GitLab.com SaaS (https://gitlab.com).
default: https://gitlab.com
order: 3
authorizationURL:
type: String
title: Authorization URL
hint: For self-managed GitLab instances, define an alternate authorization URL (e.g. http://example.com/oauth/authorize). Leave empty otherwise.
order: 4
tokenURL:
type: String
title: Token URL
hint: For self-managed GitLab instances, define an alternate token URL (e.g. http://example.com/oauth/token). Leave empty otherwise.
order: 5

@ -7,6 +7,10 @@ color: blue-grey darken-2
website: https://www.keycloak.org/
useForm: false
isAvailable: true
scopes:
- openid
- profile
- email
props:
host:
type: String

@ -19,6 +19,13 @@ module.exports = {
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
tlsOptions: getTlsOptions(conf),
...conf.mapGroups && {
groupSearchBase: conf.groupSearchBase,
groupSearchFilter: conf.groupSearchFilter,
groupSearchScope: conf.groupSearchScope,
groupDnProperty: conf.groupDnProperty,
groupSearchAttributes: [conf.groupNameField]
},
includeRaw: true
},
usernameField: 'email',
@ -40,6 +47,21 @@ module.exports = {
picture: _.get(profile, `_raw.${conf.mappingPicture}`, '')
}
})
// map users LDAP groups to wiki groups with the same name, and remove any groups that don't match LDAP
if (conf.mapGroups) {
const ldapGroups = _.get(profile, '_groups')
if (ldapGroups && _.isArray(ldapGroups)) {
const groups = ldapGroups.map(g => g[conf.groupNameField])
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}
cb(null, user)
} catch (err) {
if (WIKI.config.flags.ldapdebug) {
@ -59,7 +81,7 @@ function getTlsOptions(conf) {
if (!conf.tlsCertPath) {
return {
rejectUnauthorized: conf.verifyTLSCertificate,
rejectUnauthorized: conf.verifyTLSCertificate
}
}

@ -83,3 +83,39 @@ props:
hint: The field storing the user avatar picture. Usually "jpegPhoto" or "thumbnailPhoto".
maxWidth: 500
order: 23
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the users LDAP/Active Directory groups. Group Search Base must also be defined for this to work. Note this will remove any groups the user has that doesn't match an LDAP/Active Directory group.
default: false
order: 24
groupSearchBase:
type: String
title: Group Search Base
hint: The base DN from which to search for groups.
default: OU=groups,dc=example,dc=com
order: 25
groupSearchFilter:
type: String
title: Group Search Filter
hint: LDAP search filter for groups. (member={{dn}}) will use the distinguished name of the user and will work in most cases.
default: (member={{dn}})
order: 26
groupSearchScope:
type: String
title: Group Search Scope
hint: How far from the Group Search Base to search for groups. sub (default) will search the entire subtree. base, will only search the Group Search Base dn. one, will search the Group Search Base dn and one additional level.
default: sub
order: 27
groupDnProperty:
type: String
title: Group DN Property
hint: The property of user object to use in {{dn}} interpolation of Group Search Filter.
default: dn
order: 28
groupNameField:
type: String
title: Group Name Field
hint: The field that contains the name of the LDAP group to match on, usually "name" or "cn".
default: name
order: 29

@ -1,3 +1,5 @@
const bcrypt = require('bcryptjs-then')
/* global WIKI */
// ------------------------------------
@ -28,6 +30,9 @@ module.exports = {
done(null, user)
}
} else {
// Fake verify password to mask timing differences
await bcrypt.compare((Math.random() + 1).toString(36), '$2a$12$irXbAcQSY59pcQQfNQpY8uyhfSw48nzDikAmr60drI501nR.PuBx2')
done(new WIKI.Error.AuthLoginFailed(), null)
}
} catch (err) {

@ -18,7 +18,8 @@ module.exports = {
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
passReqToCallback: true,
scope: conf.scope
scope: conf.scope,
state: conf.enableCSRFProtection
}, async (req, accessToken, refreshToken, profile, cb) => {
try {
const user = await WIKI.models.users.processProfile({
@ -30,6 +31,19 @@ module.exports = {
email: _.get(profile, conf.emailClaim)
}
})
if (conf.mapGroups) {
const groups = _.get(profile, conf.groupsClaim)
if (groups && _.isArray(groups)) {
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}
cb(null, user)
} catch (err) {
cb(err, null)

@ -54,19 +54,38 @@ props:
default: email
maxWidth: 500
order: 8
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 9
groupsClaim:
type: String
title: Groups Claim
hint: Field containing the group names
default: groups
maxWidth: 500
order: 10
logoutURL:
type: String
title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 9
order: 11
scope:
type: String
title: Scope
hint: (optional) Application Client permission scopes.
order: 10
order: 12
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
order: 13
enableCSRFProtection:
type: Boolean
default: true
title: Enable CSRF protection
hint: Pass a nonce state parameter during authentication to protect against CSRF attacks.
order: 14

@ -19,24 +19,31 @@ module.exports = {
issuer: conf.issuer,
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
passReqToCallback: true
}, async (req, iss, sub, profile, cb) => {
passReqToCallback: true,
skipUserProfile: conf.skipUserProfile,
acrValues: conf.acrValues
}, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => {
const profile = Object.assign({}, idProfile, uiProfile)
try {
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile: {
...profile,
email: _.get(profile, '_json.' + conf.emailClaim)
email: _.get(profile, '_json.' + conf.emailClaim),
displayName: _.get(profile, '_json.' + conf.displayNameClaim, '')
}
})
if (conf.mapGroups) {
const groups = _.get(profile, '_json.' + conf.groupsClaim)
if (groups) {
const groupIDs = Object.values(WIKI.auth.groups)
.filter(g => groups.includes(g.name))
.map(g => g.id)
for (let groupID of groupIDs) {
await user.$relatedQuery('groups').relate(groupID)
if (groups && _.isArray(groups)) {
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}

@ -37,33 +37,51 @@ props:
title: User Info Endpoint URL
hint: User Info Endpoint URL
order: 5
skipUserProfile:
type: Boolean
default: false
title: Skip User Profile
hint: Skips call to the OIDC UserInfo endpoint
order: 6
issuer:
type: String
title: Issuer
hint: Issuer URL
order: 6
order: 7
emailClaim:
type: String
title: Email Claim
hint: Field containing the email address
default: email
maxWidth: 500
order: 7
order: 8
displayNameClaim:
type: String
title: Display Name Claim
hint: Field containing the user display name
default: displayName
maxWidth: 500
order: 9
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 8
order: 10
groupsClaim:
type: String
title: Groups Claim
hint: Field containing the group names
default: groups
maxWidth: 500
order: 9
order: 11
logoutURL:
type: String
title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 10
order: 12
acrValues:
type: String
title: ACR Values
hint: (optional) Authentication Context Class Reference
order: 13

@ -0,0 +1,17 @@
main: |
<div id="artalk-container"></div>
head: |
<link href="{{server}}/dist/Artalk.css" rel="stylesheet">
<script src="{{server}}/dist/Artalk.js"></script>
body: |
<script>
window.onload = function() {
new Artalk({
el: '#artalk-container',
pageKey: '{{pageId}}',
pageTitle: '',
server: '{{server}}',
site: '{{siteName}}',
});
};
</script>

@ -0,0 +1,23 @@
key: artalk
title: Artalk
description: A light-weight self-hosted comment system.
author: CDN18
logo: https://static.requarks.io/logo/artalk.png
website: https://artalk.js.org
codeTemplate: true
isAvailable: true
props:
server:
type: String
title: Artalk Backend URL
default: ''
hint: 'Publicly accessible URL of your Artalk instance. It should start with http/https and omit the trailing slash. (e.g. https://artalk.example.com)'
maxWidth: 650
order: 1
siteName:
type: String
title: Site Name
default: ''
hint: 'The name of this site configured in the artalk backend. Leave empty to use default site.'
maxWidth: 450
order: 2

@ -126,6 +126,7 @@ module.exports = {
async update ({ id, content, user }) {
const renderedContent = DOMPurify.sanitize(mkdown.render(content))
await WIKI.models.comments.query().findById(id).patch({
content,
render: renderedContent
})
return renderedContent

@ -0,0 +1,6 @@
key: asciidoc
title: Asciidoc
description: Basic Asciidoc editor
contentType: asciidoc
author: dzruyk
props: {}

@ -0,0 +1,20 @@
key: asciidocCore
title: Core
description: Basic Asciidoc Parser
author: dzruyk (Based on asciidoctor.js renderer)
input: asciidoc
output: html
icon: mdi-sitemap
enabledDefault: true
props:
safeMode:
type: String
default: server
title: Safe Mode
hint: Sets the safe mode to use when parsing content to HTML.
order: 1
enum:
- unsafe
- safe
- server
- secure

@ -0,0 +1,26 @@
const asciidoctor = require('asciidoctor')()
const cheerio = require('cheerio')
module.exports = {
async render() {
const html = asciidoctor.convert(this.input, {
standalone: false,
safe: this.config.safeMode,
attributes: {
showtitle: true,
icons: 'font'
}
})
const $ = cheerio.load(html, {
decodeEntities: true
})
$('pre.highlight > code.language-diagram').each((i, elm) => {
const diagramContent = Buffer.from($(elm).html(), 'base64').toString()
$(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
})
return $.html()
}
}

@ -243,6 +243,16 @@ module.exports = {
}
})
// --------------------------------
// Wrap root table nodes
// --------------------------------
$('body').contents().toArray().forEach(item => {
if (item && item.name === 'table' && item.parent.name === 'body') {
$(item).wrap('<div class="table-container"></div>')
}
})
// --------------------------------
// Escape mustache expresions
// --------------------------------

@ -1,5 +1,6 @@
const md = require('markdown-it')
const mdAttrs = require('markdown-it-attrs')
const mdDecorate = require('markdown-it-decorate')
const _ = require('lodash')
const underline = require('./underline')
@ -42,6 +43,7 @@ module.exports = {
mkdown.use(mdAttrs, {
allowedAttributes: ['id', 'class', 'target']
})
mkdown.use(mdDecorate)
for (let child of this.children) {
const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)

@ -24,12 +24,13 @@ katex.__defineMacro('\\tripledash', '{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2m
module.exports = {
init (mdinst, conf) {
const macros = {}
if (conf.useInline) {
mdinst.inline.ruler.after('escape', 'katex_inline', katexInline)
mdinst.renderer.rules.katex_inline = (tokens, idx) => {
try {
return katex.renderToString(tokens[idx].content, {
displayMode: false
displayMode: false, macros
})
} catch (err) {
WIKI.logger.warn(err)
@ -44,7 +45,7 @@ module.exports = {
mdinst.renderer.rules.katex_block = (tokens, idx) => {
try {
return `<p>` + katex.renderToString(tokens[idx].content, {
displayMode: true
displayMode: true, macros
}) + `</p>`
} catch (err) {
WIKI.logger.warn(err)

@ -0,0 +1,8 @@
key: markdownPivotTable
title: Pivot Table
description: Add pivot table support
author: jaeseopark
icon: mdi-table
enabledDefault: false
dependsOn: markdownCore
props: {}

@ -0,0 +1,7 @@
const pivotTable = require('markdown-it-pivot-table')
module.exports = {
init (md) {
md.use(pivotTable)
}
}

@ -135,7 +135,7 @@ module.exports = {
transform: async (page, enc, cb) => {
const pageObject = await WIKI.models.pages.query().findById(page.id)
page.tags = await pageObject.$relatedQuery('tags')
let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}`

@ -1,5 +1,5 @@
const path = require('path')
const sgit = require('simple-git/promise')
const sgit = require('simple-git')
const fs = require('fs-extra')
const _ = require('lodash')
const stream = require('stream')
@ -30,7 +30,7 @@ module.exports = {
WIKI.logger.info('(STORAGE/GIT) Initializing...')
this.repoPath = path.resolve(WIKI.ROOTPATH, this.config.localRepoPath)
await fs.ensureDir(this.repoPath)
this.git = sgit(this.repoPath)
this.git = sgit(this.repoPath, { maxConcurrentProcesses: 1 })
// Set custom binary path
if (!_.isEmpty(this.config.gitBinaryPath)) {
@ -45,9 +45,10 @@ module.exports = {
await this.git.init()
}
// Disable quotePath
// Disable quotePath, color output
// Link https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
await this.git.raw(['config', '--local', 'core.quotepath', false])
await this.git.raw(['config', '--local', 'color.ui', false])
// Set default author
await this.git.raw(['config', '--local', 'user.email', this.config.defaultEmail])
@ -118,7 +119,7 @@ module.exports = {
* SYNC
*/
async sync() {
const currentCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch]), 'latest', {})
const currentCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {})
const rootUser = await WIKI.models.users.getRootUser()
@ -140,15 +141,29 @@ module.exports = {
// Process Changes
if (_.includes(['sync', 'pull'], this.mode)) {
const latestCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch]), 'latest', {})
const latestCommitLog = _.get(await this.git.log(['-n', '1', this.config.branch, '--']), 'latest', {})
const diff = await this.git.diffSummary(['-M', currentCommitLog.hash, latestCommitLog.hash])
if (_.get(diff, 'files', []).length > 0) {
let filesToProcess = []
const filePattern = /(.*?)(?:{(.*?))? => (?:(.*?)})?(.*)/
for (const f of diff.files) {
const fMoved = f.file.split(' => ')
const fName = fMoved.length === 2 ? fMoved[1] : fMoved[0]
const fPath = path.join(this.repoPath, fName)
const fMatch = f.file.match(filePattern)
const fNames = {
old: null,
new: null
}
if (!fMatch) {
fNames.old = f.file
fNames.new = f.file
} else if (!fMatch[2] && !fMatch[3]) {
fNames.old = fMatch[1]
fNames.new = fMatch[4]
} else {
fNames.old = (fMatch[1] + fMatch[2] + fMatch[4]).replace('//', '/')
fNames.new = (fMatch[1] + fMatch[3] + fMatch[4]).replace('//', '/')
}
const fPath = path.join(this.repoPath, fNames.new)
let fStats = { size: 0 }
try {
fStats = await fs.stat(fPath)
@ -165,8 +180,8 @@ module.exports = {
path: fPath,
stats: fStats
},
oldPath: fMoved[0],
relPath: fName
oldPath: fNames.old,
relPath: fNames.new
})
}
await this.processFiles(filesToProcess, rootUser)

@ -29,6 +29,8 @@ block body
comments-enabled=config.features.featurePageComments
effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')
comments-external=comments.codeTemplate
edit-shortcuts=Buffer.from(JSON.stringify(config.editShortcuts)).toString('base64')
filename=pageFilename
)
template(slot='contents')
div!= page.render

@ -11,4 +11,5 @@ block body
:version-id=page.versionId
version-date=page.versionDate
effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64')
)= page.content
)
code(v-pre)= page.content

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