pull/7305/merge
Timo Kruth 1 year ago committed by GitHub
commit e5311c6642
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -28,7 +28,7 @@ It is also always helpful to have some context for your pull request. What was t
Use the feature request board to submit new ideas and vote on which ideas should be integrated first.
:triangular_flag_on_post: [https://wiki.js.org/feedback/](https://wiki.js.org/feedback/)
:triangular_flag_on_post: [https://js.wiki/feedback/](https://js.wiki/feedback/)
*Do not use GitHub issues to submit new feature ideas, as it will closed and you'll be asked to use the feature request board above. GitHub Issues are limited to bugs / issues / help*.

@ -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@v4
- 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@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
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@v5
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@v4
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@v4
- name: Set Test Variables
run: |
@ -129,7 +129,7 @@ jobs:
docker: armv7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- 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@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
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@v4
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@v5
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@v4
with:
node-version: 12.x
node-version: 18.x
- name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v4
with:
name: drop
path: drop
@ -202,17 +203,25 @@ jobs:
- name: Extract Build
run: |
mkdir -p win
tar -xzf $env:GITHUB_WORKSPACE\drop\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\win --exclude=node_modules
tar -xzf $env:GITHUB_WORKSPACE\drop\wiki-js.tar.gz -C $env:GITHUB_WORKSPACE\win
Copy-Item win\node_modules\extract-files\package.json patch-extractfile.json -Force
Remove-Item -Path win\node_modules -Force -Recurse
- name: Install Dependencies
run: yarn --production --frozen-lockfile --non-interactive
run: |
yarn --production --frozen-lockfile --non-interactive
yarn patch-package
working-directory: win
- name: Fix patched packages
run: |
Copy-Item patch-extractfile.json win\node_modules\extract-files\package.json -Force
- name: Create Bundle
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@v4
with:
name: drop-win
path: wiki-js-windows.tar.gz
@ -232,13 +241,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -273,13 +282,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -319,13 +328,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@v4
with:
name: drop
path: drop
- name: Download Windows Build
uses: actions/download-artifact@v2.1.0
uses: actions/download-artifact@v4
with:
name: drop-win
path: drop-win
@ -339,17 +348,18 @@ 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
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
@ -359,29 +369,46 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
build-do-image:
name: Build DigitalOcean Image
runs-on: ubuntu-latest
needs: [release]
steps:
- uses: actions/checkout@v2
- name: Set Version Variables
run: |
echo "Using TAG mode: $GITHUB_REF_NAME"
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Install Packer
run: |
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install packer
- name: Build Droplet Image
env:
DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}
WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }}
working-directory: dev/packer
run: |
packer build digitalocean.json
- name: Notify Telegram Channel
uses: appleboy/telegram-action@v0.1.1
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
format: markdown
disable_web_page_preview: true
message: |
Wiki.js *${{ github.ref_name }}* has been released!
See [release notes](https://github.com/requarks/wiki/releases) for details.
- name: Notify Discord Channel
uses: sebastianpopp/discord-action@v2.0
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details.
# build-do-image:
# name: Build DigitalOcean Image
# runs-on: ubuntu-latest
# needs: [release]
# steps:
# - uses: actions/checkout@v4
# - name: Set Version Variables
# run: |
# echo "Using TAG mode: $GITHUB_REF_NAME"
# echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
# - name: Install Packer
# run: |
# curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
# sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
# sudo apt-get update && sudo apt-get install packer
# - name: Build Droplet Image
# env:
# DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}
# WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }}
# working-directory: dev/packer
# run: |
# packer build digitalocean.json

@ -1 +1 @@
v12.16.3
v18.17.1

@ -8,7 +8,7 @@
"vue"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": [
"server/locales"

@ -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)
@ -12,9 +15,10 @@
[![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)
[![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack)
[![Twitter Follow](https://img.shields.io/badge/follow-%40requarks-blue.svg?style=flat&logo=twitter)](https://twitter.com/requarks)
[![Follow on Twitter](https://img.shields.io/badge/twitter-%40requarks-blue.svg?style=flat&logo=twitter&logoColor=white)](https://twitter.com/requarks)
[![Follow on Telegram](https://img.shields.io/badge/telegram-%40wiki__js-blue.svg?style=flat&logo=telegram)](https://t.me/wiki_js)
[![Chat on Discord](https://img.shields.io/badge/discord-join-8D96F6.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/rcxt9QS2jd)
[![Reddit](https://img.shields.io/badge/reddit-%2Fr%2Fwikijs-orange?logo=reddit&logoColor=white)](https://www.reddit.com/r/wikijs/)
[![Subscribe to Newsletter](https://img.shields.io/badge/newsletter-subscribe-yellow.svg?style=flat&logo=mailchimp)](https://blog.js.wiki/subscribe)
##### A modern, lightweight and powerful wiki app built on NodeJS
@ -39,7 +43,7 @@
<div align="center">
Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://wiki.js.org/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`).
Wiki.js is an open source project that has been made possible due to the generous contributions by community [backers](https://js.wiki/about). If you are interested in supporting this project, please consider [becoming a sponsor](https://github.com/users/NGPixel/sponsorship), [becoming a patron](https://www.patreon.com/requarks), donating to our [OpenCollective](https://opencollective.com/wikijs), via [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FLV5X255Z9CJU&source=url) or via Ethereum (`0xe1d55c19ae86f6bcbfb17e7f06ace96bdbb22cb5`).
[![Become a Sponsor](https://img.shields.io/badge/donate-github-ea4aaa.svg?style=popout&logo=github)](https://github.com/users/NGPixel/sponsorship)
[![Become a Patron](https://img.shields.io/badge/donate-patreon-orange.svg?style=popout&logo=patreon)](https://www.patreon.com/requarks)
@ -80,11 +84,6 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<img src="https://cdn.js.wiki/images/sponsors/stellarhosted.png">
</a>
</td>
<td align="center" valign="middle" width="444">
<a href="https://www.hostwiki.com/" target="_blank">
<img src="https://cdn.js.wiki/images/sponsors/hostwiki.png">
</a>
</td>
</tr>
</tbody>
</table>
@ -94,6 +93,11 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<table>
<tbody>
<tr>
<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" width="148">
<a href="https://github.com/alexksso" target="_blank">
Alexander Casassovici<br />(@alexksso)
@ -114,6 +118,13 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<img src="https://static.requarks.io/sponsors/gigabitelabs-148x129.png">
</a>
</td>
<td align="center" valign="middle" width="148">
<a href="https://www.hostwiki.com/" target="_blank">
<img src="https://cdn.js.wiki/images/sponsors/hostwiki.png">
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle" width="148">
<a href="https://github.com/JayDaley" target="_blank">
Jay Daley<br />(@JayDaley)
@ -124,11 +135,26 @@ 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">
<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="http://www.taicep.org/" target="_blank">
<img src="https://avatars.githubusercontent.com/u/160072306?v=4">
</a>
</td>
<td align="center" valign="middle" colspan="1">
<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>
@ -137,22 +163,27 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<img width="441" height="1" />
- Akira Suenami ([@a-suenami](https://github.com/a-suenami))
- Armin Reiter ([@arminreiter](https://github.com/arminreiter))
- Arnaud Marchand ([@snuids](https://github.com/snuids))
- Brian Douglass ([@bhdouglass](https://github.com/bhdouglass))
- Bryon Vandiver ([@asterick](https://github.com/asterick))
- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer))
- Charlie Schliesser ([@charlie-s](https://github.com/charlie-s))
- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))
- Cole Manning ([@RVRX](https://github.com/RVRX))
- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))
- Daniel Horner ([@danhorner](https://github.com/danhorner))
- 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))
@ -162,32 +193,42 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
- Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))
- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))
- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
</td><td>
<img width="441" height="1" />
- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))
- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
- M. Scott Ford ([@mscottford](https://github.com/mscottford))
- Nick Halase ([@nhalase](https://github.com/nhalase))
- Nick Price ([@DominoTree](https://github.com/DominoTree))
- Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
- Noel Cower ([@nilium](https://github.com/nilium))
- Oleksandr Koltsov ([@crambo](https://github.com/crambo))
- 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))
- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))
- Sean Coffey ([@seanecoffey](https://github.com/seanecoffey))
- Simon Ott ([@ottsimon](https://github.com/ottsimon))
- Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro))
- Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu))
- Tim Elmer ([@tim-elmer](https://github.com/tim-elmer))
- Tyler Denman ([@tylerguy](https://github.com/tylerguy))
- Victor Bilgin ([@vbilgin](https://github.com/vbilgin))
- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions))
- YazMogg35 ([@YazMogg35](https://github.com/YazMogg35))
- Yu Yongwoo ([@uyu423](https://github.com/uyu423))
- ameyrakheja ([@ameyrakheja](https://github.com/ameyrakheja))
- aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
- aytaa ([@aytaa](https://github.com/aytaa))
- cesar ([@cesarnr21](https://github.com/cesarnr21))
- chaee ([@chaee](https://github.com/chaee))
- lwileczek ([@lwileczek](https://github.com/lwileczek))
- 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))
@ -338,6 +379,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>
@ -350,15 +408,21 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
<table><tbody><tr><td>
<img width="441" height="1" />
- Aeternum
- 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
@ -368,26 +432,34 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Hope
- Ian
- Imari Childress
- Iskander Callos
</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
- 张白驹
@ -407,9 +479,6 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
<h2 align="center">Special Thanks</h2>
![Algolia](https://js.wiki/legacy/logo_algolia.png)
[Algolia](https://www.algolia.com/) for providing access to their incredible search engine.
![Browserstack](https://js.wiki/legacy/logo_browserstack.png)
[Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools.
@ -417,16 +486,22 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
[Cloudflare](https://www.cloudflare.com/) for providing their great CDN, SSL and advanced networking services.
![DigitalOcean](https://js.wiki/legacy/logo_digitalocean.png)
[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site.
[DigitalOcean](https://m.do.co/c/5f7445bfa4d0) for providing hosting of the Wiki.js documentation site and APIs.
![Icons8](https://static.requarks.io/logo/icons8-text-h40.png)
[Icons8](https://icons8.com/) for providing beautiful icon sets.
[Icons8](https://icons8.com/) for providing access to their beautiful icon sets.
![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 landings and blog websites.
[Netlify](https://www.netlify.com) for providing hosting for our website.
![ngrok](https://static.requarks.io/logo/ngrok-h40.png)
[ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services.

@ -13,11 +13,11 @@ If you find such vulnerability, it's important to disclose it in a quick and sec
## Reporting a Vulnerability
**DO NOT CREATE AN ISSUE ON GITHUB** to report a potential vulnerability / security problem. Instead, choose one of these options:
**DO NOT CREATE A GITHUB ISSUE / DISCUSSION** to report a potential vulnerability / security problem. Instead, choose one of these options:
### A) Disclose on Huntr.dev
### A) Submit a Vulnerability Report *(recommended)*
Disclose the vulnerability on [Huntr.dev](https://huntr.dev/bounties/disclose) for the repository `https://github.com/Requarks/wiki`.
Fill in the form on https://github.com/requarks/wiki/security/advisories/new
### B) Send an email

@ -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'
@ -164,6 +173,89 @@
//- disabled
//- )
v-card.mt-5.animated.fadeInUp.wait-p6s
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title.subtitle-1 URL Handling
v-card-text
v-text-field(
outlined
:label='$t(`admin:general.pageExtensions`)'
v-model='config.pageExtensions'
prepend-icon='mdi-format-text-wrapping-overflow'
:hint='$t(`admin:general.pageExtensionsHint`)'
persistent-hint
)
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>
@ -197,12 +289,21 @@ export default {
analyticsId: '',
company: '',
contentLicense: '',
footerOverride: '',
logoUrl: '',
featureAnalytics: false,
featurePageRatings: false,
featurePageComments: false,
featurePersonalWikis: false,
featureTinyPNG: false
featureTinyPNG: false,
pageExtensions: '',
editFab: false,
editMenuBar: false,
editMenuBtn: false,
editMenuExternalBtn: false,
editMenuExternalName: '',
editMenuExternalIcon: '',
editMenuExternalUrl: ''
},
metaRobots: [
{ text: 'Index', value: 'index' },
@ -217,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 [
@ -247,33 +349,51 @@ export default {
await this.$apollo.mutate({
mutation: gql`
mutation (
$host: String!
$title: String!
$description: String!
$robots: [String]!
$analyticsService: String!
$analyticsId: String!
$company: String!
$contentLicense: String!
$logoUrl: String!
$featurePageRatings: Boolean!
$featurePageComments: Boolean!
$featurePersonalWikis: Boolean!
$host: String
$title: String
$description: String
$robots: [String]
$analyticsService: String
$analyticsId: String
$company: String
$contentLicense: String
$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(
host: $host,
title: $title,
description: $description,
robots: $robots,
analyticsService: $analyticsService,
analyticsId: $analyticsId,
company: $company,
contentLicense: $contentLicense,
logoUrl: $logoUrl,
featurePageRatings: $featurePageRatings,
featurePageComments: $featurePageComments,
host: $host
title: $title
description: $description
robots: $robots
analyticsService: $analyticsService
analyticsId: $analyticsId
company: $company
contentLicense: $contentLicense
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
@ -294,10 +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')
@ -311,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)
@ -346,10 +476,19 @@ export default {
analyticsId
company
contentLicense
footerOverride
logoUrl
pageExtensions
featurePageRatings
featurePageComments
featurePersonalWikis
editFab
editMenuBar
editMenuBtn
editMenuExternalBtn
editMenuExternalName
editMenuExternalIcon
editMenuExternalUrl
}
}
}

@ -214,8 +214,8 @@ export default {
return {
roles: [
{ text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },
{ text: 'Create Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
{ text: 'Edit + Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
{ text: 'Create + Edit Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
{ text: 'Rename / Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
{ text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },
{ text: 'View Pages Source', value: 'read:source', icon: 'mdi-code-tags' },
{ text: 'View Pages History', value: 'read:history', icon: 'mdi-history' },

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

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

@ -265,7 +265,7 @@ export default {
securityOpenRedirect: true,
securityIframe: true,
securityReferrerPolicy: true,
securityTrustProxy: true,
securityTrustProxy: false,
securitySRI: true,
securityHSTS: false,
securityHSTSDuration: 0,

@ -55,15 +55,24 @@
v-toolbar(color='primary', dark, dense, flat)
v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}
v-card-text
v-select(
:items='tocPositions'
outlined
prepend-icon='mdi-border-vertical'
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.'
)
v-range-slider(
prepend-icon='mdi-menu-open'
:label='$t(`admin:theme.tocHeadingLevels`)'
label='Heading Levels in ToC'
v-model='tocRange'
:min='1'
:max='6'
:tick-labels='["H1", "H2", "H3", "H4", "H5", "H6"]'
hint='Select which levels of the toc are displayed.'
)
.text-caption {{$t('admin:theme.tocHeadingLevelsHint')}}
v-flex(lg6 xs12)
v-card.animated.fadeInUp.wait-p2s
@ -129,6 +138,7 @@ export default {
max: 2
},
iconset: '',
tocPosition: 'left',
injectCSS: '',
injectHead: '',
injectBody: ''
@ -170,6 +180,13 @@ export default {
width: 100
}
]
},
tocPositions () {
return [
{ text: 'Left (default)', value: 'left' },
{ text: 'Right', value: 'right' },
{ text: 'Hidden', value: 'off' }
]
}
},
watch: {
@ -196,6 +213,7 @@ export default {
iconset: this.config.iconset,
darkMode: this.darkMode,
tocDepth: this.config.tocDepth,
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' },

@ -105,6 +105,9 @@ export default {
} else {
this.searchIsLoading = true
}
},
results() {
this.cursor = 0
}
},
mounted() {
@ -153,6 +156,9 @@ export default {
skip() {
return !this.search || this.search.length < 2
},
result() {
this.pagination = 1
},
update: (data) => _.get(data, 'pages.search', {}),
watchLoading (isLoading) {
this.searchIsLoading = isLoading

@ -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,12 +195,12 @@ 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 mdEmoji from 'markdown-it-emoji'
import mdDecorate from 'markdown-it-decorate'
import { full as mdEmoji } from 'markdown-it-emoji'
import mdTaskLists from 'markdown-it-task-lists'
import mdExpandTabs from 'markdown-it-expand-tabs'
import mdAbbr from 'markdown-it-abbr'
@ -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>

@ -92,7 +92,7 @@ v-dialog(
)
v-tab-item(transition='fade-transition', reverse-transition='fade-transition')
v-card-text
.overline {{$t('editor:props.tocTitle')}}
.overline.pb-5 {{$t('editor:props.tocTitle')}}
v-switch(
:label='$t(`editor:props.tocUseDefault`)'
v-model='useDefaultTocDepth'

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

@ -98,6 +98,7 @@
:search='innerSearch'
:loading='isLoading'
:options.sync='pagination'
@page-count='pageTotal = $event'
hide-default-footer
ref='dude'
)
@ -183,6 +184,7 @@ export default {
sortDesc: [false]
},
pages: [],
pageTotal: 0,
isLoading: true,
scrollStyle: {
vuescroll: {},
@ -214,9 +216,6 @@ export default {
tagsSelected () {
return _.filter(this.tags, t => _.includes(this.selection, t.tag))
},
pageTotal () {
return Math.ceil(this.pages.length / this.pagination.itemsPerPage)
},
orderByItems () {
return [
{ text: this.$t('tags:orderByField.creationDate'), value: 'createdAt' },

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

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

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

@ -8,6 +8,7 @@ query {
min
max
}
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

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

@ -1,20 +1,15 @@
<template lang="pug">
.page-toc-item
template(v-if='level >= min')
v-list-item(@click='click(item.anchor)', v-if='(item.children.length === 0 && max > level) || max > level',
:key='item.anchor', :class='isNestedLevel ? `pl-9` : `pl-6`')
v-icon.pl-0(small, color='grey lighten-1') {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }}
v-list-item(@click='click(item.anchor)', v-if='max > level', :key='item.anchor', :class='[isNestedLevel ? "pl-9" : "pl-6"]')
v-icon.pl-0(small, color='grey lighten-1', v-if='item.children.length > 0') {{ $vuetify.rtl ? `mdi-menu-up` : `mdi-menu-up` }}
v-list-item-title.pl-4(v-bind:class='titleClasses') {{item.title}}
v-list-group(sub-group, v-else, v-bind:class='{"pl-3": isNestedLevel}')
v-list-group(v-else, sub-group, v-bind:class='{"pl-6": isNestedLevel}')
template(v-slot:activator)
v-list-item.pl-0(@click='click(item.anchor)', :key='item.anchor')
v-list-item-title(v-bind:class='titleClasses') {{item.title}}
template(v-if='item.children.length !== 0', v-for='subItem in item.children')
page-toc-item(:item='subItem', :level='level + 1', :min='min', :max='max')
template(v-if='max > level', v-for='subItem in item.children')
page-toc-item(:item='subItem', :level='level + 1', :min='min', :max='max')
template(v-else, v-for='subItem in item.children')
page-toc-item(:item='subItem', :level='level + 1', :min='min', :max='max')
page-toc-item(v-if='item.children.length > 0', v-for='subItem in item.children', :key='subItem.anchor', :item='subItem', :level='level + 1', :min='min', :max='max')
page-toc-item(v-show='max > level && item.children.length > 0', v-for='subItem in item.children', :key='subItem.anchor', :item='subItem', :level='level + 1', :min='min', :max='max')
</template>
<script>

@ -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.py-2(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')
page-toc-item(
@ -67,7 +102,7 @@
:min='tocOptionsDecoded.min'
:max='tocOptionsDecoded.max'
)
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(
@ -87,7 +122,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')}}
@ -123,7 +158,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')}}
@ -140,8 +175,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
@ -156,13 +191,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)
@ -181,8 +216,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'
@ -439,6 +480,14 @@ export default {
type: Boolean,
default: false
},
editShortcuts: {
type: String,
default: ''
},
filename: {
type: String,
default: ''
},
tocOptions: {
type: String,
default: ''
@ -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'),
tocOptionsDecoded () {
return JSON.parse(Buffer.from(this.tocOptions, 'base64').toString())
},
@ -525,7 +576,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)
@ -543,6 +601,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')
},
@ -588,6 +649,8 @@ export default {
this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts)
}
})
window.boot.notify('page-ready')
})
},
methods: {
@ -682,4 +745,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;
}
}

@ -1,20 +1,22 @@
# =========================
# --- BUILD NPM MODULES ---
# =========================
FROM node:16-alpine AS build
FROM node:18-alpine AS build
RUN apk add yarn g++ make cmake python3 --no-cache
WORKDIR /wiki
COPY ./package.json ./package.json
COPY ./patches ./patches
RUN yarn --production --frozen-lockfile --non-interactive --network-timeout 100000
RUN yarn patch-package
# ===============
# --- Release ---
# ===============
FROM node:16-alpine
FROM node:18-alpine
LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \

@ -1,7 +1,7 @@
# ====================
# --- Build Assets ---
# ====================
FROM node:16-alpine AS assets
FROM node:18-alpine AS assets
RUN apk add yarn g++ make cmake python3 --no-cache
@ -9,6 +9,7 @@ WORKDIR /wiki
COPY ./client ./client
COPY ./dev ./dev
COPY ./patches ./patches
COPY ./package.json ./package.json
COPY ./.babelrc ./.babelrc
COPY ./.eslintignore ./.eslintignore
@ -19,11 +20,12 @@ RUN yarn --frozen-lockfile --non-interactive
RUN yarn build
RUN rm -rf /wiki/node_modules
RUN yarn --production --frozen-lockfile --non-interactive
RUN yarn patch-package
# ===============
# --- Release ---
# ===============
FROM node:16-alpine
FROM node:18-alpine
LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \

@ -1,11 +1,11 @@
# -- DEV DOCKERFILE --
# -- DO NOT USE IN PRODUCTION! --
FROM node:14
FROM node:18
LABEL maintainer "requarks.io"
RUN apt-get update && \
apt-get install -y bash curl git python make g++ nano openssh-server gnupg && \
apt-get install -y bash curl git python3 make g++ nano openssh-server gnupg && \
mkdir -p /wiki
WORKDIR /wiki

@ -5,7 +5,7 @@ version: "3"
services:
db:
container_name: wiki-db
image: postgres:9-alpine
image: postgres:15-alpine
environment:
POSTGRES_DB: wiki
POSTGRES_PASSWORD: wikijsrocks

@ -2,7 +2,7 @@ version: "3"
services:
db:
image: postgres:11-alpine
image: postgres:15-alpine
environment:
POSTGRES_DB: wiki
POSTGRES_PASSWORD: wikijsrocks

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

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

@ -14,6 +14,8 @@ spec:
metadata:
labels:
{{- include "wiki.selectorLabels" . | nindent 8 }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
@ -39,8 +41,18 @@ 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 }}
- name: DATABASE_URL
value: {{ .Values.externalPostgresql.databaseURL }}
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ default "1" .Values.externalPostgresql.NODE_TLS_REJECT_UNAUTHORIZED | quote }}
{{- else }}
- name: DB_HOST
value: {{ template "wiki.postgresql.host" . }}
- name: DB_PORT
@ -62,6 +74,7 @@ spec:
name: {{ template "wiki.postgresql.secret" . }}
{{- end }}
key: {{ template "wiki.postgresql.secretKey" . }}
{{- end }}
- name: HA_ACTIVE
value: {{ .Values.replicaCount | int | le 2 | quote }}
{{- with .Values.volumeMounts }}
@ -76,6 +89,8 @@ spec:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

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

@ -32,6 +32,18 @@ readinessProbe:
path: /healthz
port: http
startupProbe:
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 60
httpGet:
path: /healthz
port: http
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
@ -51,6 +63,7 @@ service:
# type: LoadBalancer
# httpsPort: 443
# annotations: {}
# loadBalancerIP: 172.16.0.1
ingress:
enabled: true
@ -91,7 +104,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:
@ -102,6 +115,16 @@ 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
# databaseURL: postgresql://postgres:postgres@postgres:5432/wiki?ssl=true
# # For self signed CAs, like DigitalOcean
# NODE_TLS_REJECT_UNAUTHORIZED: "0"
## Configuration values for the postgresql dependency.
## ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
##

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

@ -52,7 +52,7 @@ html
script(
crossorigin='anonymous'
src='https://polyfill.io/v3/polyfill.min.js?features=EventSource'
src='https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=EventSource'
)
//- JS

@ -7,11 +7,12 @@
"dev": true,
"scripts": {
"start": "node server",
"dev": "node dev",
"build": "webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "webpack --config dev/webpack/webpack.dev.js",
"dev": "NODE_OPTIONS=--openssl-legacy-provider node dev",
"build": "NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "NODE_OPTIONS=--openssl-legacy-provider webpack --config dev/webpack/webpack.dev.js",
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"cypress:open": "cypress open"
"cypress:open": "cypress open",
"postinstall": "patch-package"
},
"repository": {
"type": "git",
@ -36,9 +37,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 +49,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 +68,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 +95,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,7 +108,8 @@
"markdown-it": "11.0.1",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "3.0.3",
"markdown-it-emoji": "1.4.0",
"markdown-it-decorate": "1.2.2",
"markdown-it-emoji": "3.0.0",
"markdown-it-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "3.0.3",
@ -114,26 +117,27 @@
"markdown-it-mark": "3.0.1",
"markdown-it-mathjax": "2.0.0",
"markdown-it-multimd-table": "4.0.3",
"markdown-it-pivot-table": "1.0.5",
"markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mathjax": "3.1.2",
"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 +145,52 @@
"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",
"patch-package": "8.0.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",
"postinstall-postinstall": "2.1.0",
"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 +218,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 +231,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 +256,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 +283,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 +324,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",

@ -0,0 +1,14 @@
diff --git a/node_modules/extract-files/package.json b/node_modules/extract-files/package.json
index 636fa03..1b75f79 100644
--- a/node_modules/extract-files/package.json
+++ b/node_modules/extract-files/package.json
@@ -34,6 +34,9 @@
"import": "./public/index.mjs",
"require": "./public/index.js"
},
+ "./public/extractFiles": "./public/extractFiles.js",
+ "./public/isExtractableFile": "./public/isExtractableFile.js",
+ "./public/ReactNativeFile": "./public/ReactNativeFile.js",
"./public/": "./public/",
"./package": "./package.json",
"./package.json": "./package.json"

@ -44,7 +44,12 @@ defaults:
title: Wiki.js
company: ''
contentLicense: ''
footerOverride: ''
logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
pageExtensions:
- md
- html
- txt
mail:
host: ''
secure: true
@ -58,6 +63,7 @@ defaults:
tocDepth:
min: 1
max: 2
tocPosition: 'left'
auth:
autoLogin: false
enforce2FA: false
@ -66,6 +72,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
@ -74,7 +88,7 @@ defaults:
securityOpenRedirect: true
securityIframe: true
securityReferrerPolicy: true
securityTrustProxy: true
securityTrustProxy: false
securitySRI: true
securityHSTS: false
securityHSTSDuration: 300
@ -155,8 +169,4 @@ reservedPaths:
- img
- js
- svg
pageExtensions:
- md
- html
- txt
# ---------------------------------

@ -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 */
@ -419,13 +420,14 @@ router.get('/_userav/:uid', async (req, res, next) => {
* View document / asset
*/
router.get('/*', async (req, res, next) => {
const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const stripExt = _.some(WIKI.config.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const pageArgs = pageHelper.parsePath(req.path, { stripExt })
const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
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)
@ -511,10 +513,11 @@ router.get('/*', async (req, res, next) => {
}
// -> Set TOC display options
const tocOptions = _.get(page, 'tocOptions.useDefault', true) ? {
min: page.tocOptions.min,
max: page.tocOptions.max
} : WIKI.config.theming.tocDepth
const tocOptions = _.get(page, 'tocOptions.useDefault', true) ?
WIKI.config.theming.tocDepth : {
min: page.tocOptions.min,
max: page.tocOptions.max
}
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
// -> Convert page TOC
@ -553,6 +556,10 @@ 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,
@ -560,7 +567,8 @@ router.get('/*', async (req, res, next) => {
tocOptions,
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()
}

@ -156,6 +156,9 @@ module.exports = {
} else {
res.cookie('jwt', newToken.token, { expires: DateTime.utc().plus({ days: 365 }).toJSDate() })
}
// Avoid caching this response
res.set('Cache-Control', 'no-store')
} catch (errc) {
WIKI.logger.warn(errc)
return 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'])

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

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

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

@ -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,8 +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,
@ -58,10 +61,18 @@ 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)
}
if (args.hasOwnProperty('pageExtensions')) {
WIKI.config.pageExtensions = _.trim(args.pageExtensions).split(',').map(p => p.trim().toLowerCase()).filter(p => p !== '')
}
WIKI.config.seo = {
description: _.get(args, 'description', WIKI.config.seo.description),
robots: _.get(args, 'robots', WIKI.config.seo.robots),
@ -79,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),
@ -104,7 +125,7 @@ module.exports = {
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', '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.')

@ -25,6 +25,7 @@ module.exports = {
iconset: WIKI.config.theming.iconset,
darkMode: WIKI.config.theming.darkMode,
tocDepth: WIKI.config.theming.tocDepth,
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
@ -46,6 +47,7 @@ module.exports = {
iconset: args.iconset,
darkMode: args.darkMode,
tocDepth: args.tocDepth,
tocPosition: args.tocPosition || 'left',
injectCSS: args.injectCSS || '',
injectHead: args.injectHead || '',
injectBody: args.injectBody || ''

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

@ -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,7 +32,9 @@ type SiteMutation {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean
@ -40,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
@ -73,7 +82,9 @@ type SiteConfig {
analyticsId: String
company: String
contentLicense: String
footerOverride: String
logoUrl: String
pageExtensions: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean
@ -81,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

@ -29,6 +29,7 @@ type ThemingMutation {
iconset: String!
darkMode: Boolean!
tocDepth: RangeInput!
tocPosition: String
injectCSS: String
injectHead: String
injectBody: String
@ -40,9 +41,10 @@ type ThemingMutation {
# -----------------------------------------------
type ThemingConfig {
theme: String
iconset: String
darkMode: Boolean
theme: String!
iconset: String!
darkMode: Boolean!
tocPosition: String
tocDepth: Range
injectCSS: String
injectHead: 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,11 @@
const path = require('path')
const { nanoid } = require('nanoid')
const { DateTime } = require('luxon')
const { gte } = require('semver')
// ----------------------------------------
// Init WIKI instance
// ----------------------------------------
let WIKI = {
IS_DEBUG: process.env.NODE_ENV === 'development',
@ -38,6 +43,9 @@ WIKI.kernel.init()
// Register exit handler
// ----------------------------------------
process.on('SIGTERM', () => {
WIKI.kernel.shutdown()
})
process.on('SIGINT', () => {
WIKI.kernel.shutdown()
})

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

@ -147,6 +147,7 @@ module.exports = class Page extends Model {
isPublished: 'boolean',
publishEndDate: 'string',
publishStartDate: 'string',
contentType: 'string',
render: 'string',
tags: [
{
@ -738,7 +739,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,
@ -758,6 +759,7 @@ module.exports = class Page extends Model {
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
title: destinationTitle,
destinationHash
})
@ -972,9 +974,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.')
}
}
}
@ -1082,6 +1083,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,

@ -866,7 +866,7 @@ module.exports = class User extends Model {
}
const usr = await WIKI.models.users.query().findById(context.req.user.id).select('providerKey')
const provider = _.find(WIKI.auth.strategies, ['key', usr.providerKey])
return provider.logout ? provider.logout(provider.config) : '/'
return provider.logout ? provider.logout(provider.config, context) : '/'
}
static async getGuestUser () {

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

@ -27,5 +27,8 @@ module.exports = {
}
}
))
},
logout (conf) {
return `https://${conf.domain}/v2/logout?${new URLSearchParams({ client_id: conf.clientId, returnTo: WIKI.config.host }).toString()}`
}
}

@ -1,3 +1,4 @@
const _ = require('lodash')
/* global WIKI */
// ------------------------------------
@ -10,15 +11,24 @@ module.exports = {
init (passport, conf) {
passport.use(conf.key,
new CASStrategy({
ssoBaseURL: conf.ssoBaseURL,
serverBaseURL: conf.serverBaseURL,
version: conf.casVersion,
ssoBaseURL: conf.casUrl,
serverBaseURL: conf.baseUrl,
serviceURL: conf.callbackURL,
passReqToCallback: true
}, async (req, profile, cb) => {
try {
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile
profile: {
...profile,
id: _.get(profile.attributes, conf.uniqueIdAttribute, profile.user),
email: _.get(profile.attributes, conf.emailAttribute),
name: _.get(profile.attributes, conf.displayNameAttribute, profile.user),
picture: ''
}
})
cb(null, user)
} catch (err) {
cb(err, null)

@ -6,6 +6,37 @@ logo: https://static.requarks.io/logo/cas.svg
color: green darken-2
website: https://apereo.github.io/cas/
useForm: false
isAvailable: true
props:
ssoBaseURL: String
serverBaseURL: String
baseUrl:
type: String
title: Base URL
hint: 'Base-URL of your WikiJS (for example: https://wiki.example.com)'
order: 1
casUrl:
type: String
title: URL to the CAS Server
hint: 'Base-URL of the CAS server, including context path. (for example: https://login.company.com/cas)'
order: 2
casVersion:
type: String
title: CAS Version
hint: 'The version of CAS to use'
order: 3
enum:
- CAS3.0
- CAS1.0
default: 'CAS3.0'
emailAttribute:
type: String
title: Attribute key which contains the users email
default: email
order: 4
displayNameAttribute:
type: String
title: Attribute key which contains the users display name (leave empty if there is none)
order: 5
uniqueIdAttribute:
type: String
title: Attribute key which contains the unique identifier of a user. (if empty, username will be used)
order: 6

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

@ -21,7 +21,7 @@ module.exports = {
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => {
}, async (req, accessToken, refreshToken, results, profile, cb) => {
let displayName = profile.username
if (_.isString(profile.fullName) && profile.fullName.length > 0) {
displayName = profile.fullName
@ -36,6 +36,7 @@ module.exports = {
picture: ''
}
})
req.session.keycloak_id_token = results.id_token
cb(null, user)
} catch (err) {
cb(err, null)
@ -43,11 +44,22 @@ module.exports = {
})
)
},
logout (conf) {
logout (conf, context) {
if (!conf.logoutUpstream) {
return '/'
} else if (conf.logoutURL && conf.logoutURL.length > 5) {
return `${conf.logoutURL}?redirect_uri=${encodeURIComponent(WIKI.config.host)}`
const idToken = context.req.session.keycloak_id_token
const redirURL = encodeURIComponent(WIKI.config.host)
if (conf.logoutUpstreamRedirectLegacy) {
// keycloak < 18
return `${conf.logoutURL}?redirect_uri=${redirURL}`
} else if (idToken) {
// keycloak 18+
return `${conf.logoutURL}?post_logout_redirect_uri=${redirURL}&id_token_hint=${idToken}`
} else {
// fall back to no redirect if keycloak_id_token isn't available
return conf.logoutURL
}
} else {
WIKI.logger.warn('Keycloak logout URL is not configured!')
return '/'

@ -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
@ -53,4 +57,9 @@ props:
title: Logout Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/logout
order: 9
logoutUpstreamRedirectLegacy:
type: Boolean
title: Legacy Logout Redirect
hint: Pass the legacy 'redirect_uri' parameter to the logout endpoint. Leave disabled for Keycloak 18 and above.
order: 10

@ -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,16 +19,34 @@ 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 && _.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)

@ -37,20 +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: 10
groupsClaim:
type: String
title: Groups Claim
hint: Field containing the group names
default: groups
maxWidth: 500
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: 8
order: 12
acrValues:
type: String
title: ACR Values
hint: (optional) Authentication Context Class Reference
order: 13

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save