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. 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*. *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 - name: Help / Questions
url: https://github.com/Requarks/wiki/discussions/categories/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 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 - name: Errors / Bug Reports
url: https://github.com/Requarks/wiki/discussions/categories/error-bug-report 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. 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 packages: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set Build Variables - name: Set Build Variables
run: | run: |
@ -42,20 +42,20 @@ jobs:
cat package.json cat package.json
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images - name: Build and push Docker images
uses: docker/build-push-action@v2.9.0 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: dev/build/Dockerfile 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 - find _dist/wiki/ -printf "%P\n" | tar -czf wiki-js.tar.gz --no-recursion -C _dist/wiki/ -T -
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1 uses: actions/upload-artifact@v4
with: with:
name: drop name: drop
path: wiki-js.tar.gz path: wiki-js.tar.gz
@ -92,7 +92,7 @@ jobs:
dbtype: [postgres, mysql, mariadb, mssql, sqlite] dbtype: [postgres, mysql, mariadb, mssql, sqlite]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set Test Variables - name: Set Test Variables
run: | run: |
@ -129,7 +129,7 @@ jobs:
docker: armv7 docker: armv7
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set Version Variables - name: Set Version Variables
run: | run: |
@ -142,26 +142,26 @@ jobs:
fi fi
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Download a Build Artifact - name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0 uses: actions/download-artifact@v4
with: with:
name: drop name: drop
path: drop path: drop
@ -172,11 +172,12 @@ jobs:
tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules tar -xzf $GITHUB_WORKSPACE/drop/wiki-js.tar.gz -C $GITHUB_WORKSPACE/build --exclude=node_modules
- name: Build and push Docker images - name: Build and push Docker images
uses: docker/build-push-action@v2.9.0 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: dev/build-arm/Dockerfile file: dev/build-arm/Dockerfile
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
provenance: false
push: true push: true
tags: | tags: |
requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }} requarks/wiki:canary-${{ matrix.docker }}-${{ env.REL_VERSION_STRICT }}
@ -189,12 +190,12 @@ jobs:
steps: steps:
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v4
with: with:
node-version: 12.x node-version: 18.x
- name: Download a Build Artifact - name: Download a Build Artifact
uses: actions/download-artifact@v2.1.0 uses: actions/download-artifact@v4
with: with:
name: drop name: drop
path: drop path: drop
@ -202,17 +203,25 @@ jobs:
- name: Extract Build - name: Extract Build
run: | run: |
mkdir -p win 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 - name: Install Dependencies
run: yarn --production --frozen-lockfile --non-interactive run: |
yarn --production --frozen-lockfile --non-interactive
yarn patch-package
working-directory: win working-directory: win
- name: Fix patched packages
run: |
Copy-Item patch-extractfile.json win\node_modules\extract-files\package.json -Force
- name: Create Bundle - name: Create Bundle
run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win . run: tar -czf wiki-js-windows.tar.gz -C $env:GITHUB_WORKSPACE\win .
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1 uses: actions/upload-artifact@v4
with: with:
name: drop-win name: drop-win
path: wiki-js-windows.tar.gz path: wiki-js-windows.tar.gz
@ -232,13 +241,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -273,13 +282,13 @@ jobs:
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -319,13 +328,13 @@ jobs:
docker manifest push -p ghcr.io/requarks/wiki:latest docker manifest push -p ghcr.io/requarks/wiki:latest
- name: Download Linux Build - name: Download Linux Build
uses: actions/download-artifact@v2.1.0 uses: actions/download-artifact@v4
with: with:
name: drop name: drop
path: drop path: drop
- name: Download Windows Build - name: Download Windows Build
uses: actions/download-artifact@v2.1.0 uses: actions/download-artifact@v4
with: with:
name: drop-win name: drop-win
path: drop-win path: drop-win
@ -339,17 +348,18 @@ jobs:
writeToFile: false writeToFile: false
- name: Update GitHub Release - name: Update GitHub Release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1.12.0
with: with:
allowUpdates: true allowUpdates: true
draft: false draft: false
makeLatest: true
name: ${{ github.ref_name }} name: ${{ github.ref_name }}
body: ${{ steps.changelog.outputs.changes }} body: ${{ steps.changelog.outputs.changes }}
token: ${{ github.token }} token: ${{ github.token }}
artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz' artifacts: 'drop/wiki-js.tar.gz,drop-win/wiki-js-windows.tar.gz'
- name: Notify Slack Releases Channel - name: Notify Slack Releases Channel
uses: slackapi/slack-github-action@v1.18.0 uses: slackapi/slack-github-action@v1.26.0
with: with:
payload: | payload: |
{ {
@ -359,29 +369,46 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
build-do-image: - name: Notify Telegram Channel
name: Build DigitalOcean Image uses: appleboy/telegram-action@v0.1.1
runs-on: ubuntu-latest with:
needs: [release] to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
steps: format: markdown
- uses: actions/checkout@v2 disable_web_page_preview: true
message: |
- name: Set Version Variables Wiki.js *${{ github.ref_name }}* has been released!
run: | See [release notes](https://github.com/requarks/wiki/releases) for details.
echo "Using TAG mode: $GITHUB_REF_NAME"
echo "REL_VERSION_STRICT=${GITHUB_REF_NAME#?}" >> $GITHUB_ENV - name: Notify Discord Channel
uses: sebastianpopp/discord-action@v2.0
- name: Install Packer with:
run: | webhook: ${{ secrets.DISCORD_WEBHOOK }}
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - message: Wiki.js ${{ github.ref_name }} has been released! See https://github.com/requarks/wiki/releases for details.
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 # build-do-image:
# name: Build DigitalOcean Image
- name: Build Droplet Image # runs-on: ubuntu-latest
env: # needs: [release]
DIGITALOCEAN_API_TOKEN: ${{ secrets.DO_TOKEN }}
WIKI_APP_VERSION: ${{ env.REL_VERSION_STRICT }} # steps:
working-directory: dev/packer # - uses: actions/checkout@v4
run: |
packer build digitalocean.json # - 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" "vue"
], ],
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"server/locales" "server/locales"

@ -1,6 +1,9 @@
<div align="center"> <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) [![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) [![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) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ngpixel?logo=github&color=ea4aaa)](https://github.com/users/NGPixel/sponsorship)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/wikijs?label=backers&color=218bff&logo=opencollective&logoColor=white)](https://opencollective.com/wikijs)
[![Chat on Slack](https://img.shields.io/badge/slack-requarks-CC2B5E.svg?style=flat&logo=slack)](https://wiki.requarks.io/slack) [![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/) [![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 ##### A modern, lightweight and powerful wiki app built on NodeJS
@ -39,7 +43,7 @@
<div align="center"> <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 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) [![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"> <img src="https://cdn.js.wiki/images/sponsors/stellarhosted.png">
</a> </a>
</td> </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> </tr>
</tbody> </tbody>
</table> </table>
@ -94,6 +93,11 @@ Support this project by becoming a sponsor. Your name will show up in the Contri
<table> <table>
<tbody> <tbody>
<tr> <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"> <td align="center" valign="middle" width="148">
<a href="https://github.com/alexksso" target="_blank"> <a href="https://github.com/alexksso" target="_blank">
Alexander Casassovici<br />(@alexksso) 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"> <img src="https://static.requarks.io/sponsors/gigabitelabs-148x129.png">
</a> </a>
</td> </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"> <td align="center" valign="middle" width="148">
<a href="https://github.com/JayDaley" target="_blank"> <a href="https://github.com/JayDaley" target="_blank">
Jay Daley<br />(@JayDaley) 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) Oleksii<br />(@idokka)
</a> </a>
</td> </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"> <a href="https://github.com/sponsors/NGPixel" target="_blank">
<img src="https://static.requarks.io/sponsors/become-148x72.png"> <img src="https://static.requarks.io/sponsors/become-148x72.png">
</a> </a>
</td>--> </td>
</tr> </tr>
</tbody> </tbody>
</table> </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" /> <img width="441" height="1" />
- Akira Suenami ([@a-suenami](https://github.com/a-suenami)) - Akira Suenami ([@a-suenami](https://github.com/a-suenami))
- Armin Reiter ([@arminreiter](https://github.com/arminreiter))
- Arnaud Marchand ([@snuids](https://github.com/snuids)) - Arnaud Marchand ([@snuids](https://github.com/snuids))
- Brian Douglass ([@bhdouglass](https://github.com/bhdouglass)) - Brian Douglass ([@bhdouglass](https://github.com/bhdouglass))
- Bryon Vandiver ([@asterick](https://github.com/asterick)) - Bryon Vandiver ([@asterick](https://github.com/asterick))
- Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer)) - Cameron Steele ([@ATechAdventurer](https://github.com/ATechAdventurer))
- Charlie Schliesser ([@charlie-s](https://github.com/charlie-s)) - Charlie Schliesser ([@charlie-s](https://github.com/charlie-s))
- Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC)) - Cloud Data Hosting LLC ([@CloudDataHostingLLC](https://github.com/CloudDataHostingLLC))
- Cole Manning ([@RVRX](https://github.com/RVRX))
- CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin)) - CrazyMarvin ([@CrazyMarvin](https://github.com/CrazyMarvin))
- Daniel Horner ([@danhorner](https://github.com/danhorner))
- David Christian Holin ([@SirGibihm](https://github.com/SirGibihm)) - David Christian Holin ([@SirGibihm](https://github.com/SirGibihm))
- Dragan Espenschied ([@despens](https://github.com/despens)) - Dragan Espenschied ([@despens](https://github.com/despens))
- Elijah Zobenko ([@he110](https://github.com/he110)) - Elijah Zobenko ([@he110](https://github.com/he110))
- Emerson-Perna ([@Emerson-Perna](https://github.com/Emerson-Perna))
- Ernie ([@iamernie](https://github.com/iamernie)) - Ernie ([@iamernie](https://github.com/iamernie))
- Fabio Ferrari ([@devxops](https://github.com/devxops)) - Fabio Ferrari ([@devxops](https://github.com/devxops))
- Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa)) - Finsa S.p.A. ([@finsaspa](https://github.com/finsaspa))
- Florian Moss ([@florianmoss](https://github.com/florianmoss)) - Florian Moss ([@florianmoss](https://github.com/florianmoss))
- GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen)) - GoodCorporateCitizen ([@GoodCorporateCitizen](https://github.com/GoodCorporateCitizen))
- HeavenBay ([@HeavenBay](https://github.com/heavenbay)) - HeavenBay ([@HeavenBay](https://github.com/heavenbay))
- HikaruEgashira ([@HikaruEgashira](https://github.com/HikaruEgashira))
- Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy)) - Ian Hyzy ([@ianhyzy](https://github.com/ianhyzy))
- Jaimyn Mayer ([@jabelone](https://github.com/jabelone)) - Jaimyn Mayer ([@jabelone](https://github.com/jabelone))
- Jay Lee ([@polyglotm](https://github.com/polyglotm)) - 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)) - Marcilio Leite Neto ([@marclneto](https://github.com/marclneto))
- Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson)) - Mattias Johnson ([@mattiasJohnson](https://github.com/mattiasJohnson))
- Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy)) - Max Ricketts-Uy ([@MaxRickettsUy](https://github.com/MaxRickettsUy))
- Mickael Asseline ([@PAPAMICA](https://github.com/PAPAMICA))
- Mitchell Rowton ([@mrowton](https://github.com/mrowton))
</td><td> </td><td>
<img width="441" height="1" /> <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)) - M. Scott Ford ([@mscottford](https://github.com/mscottford))
- Nick Halase ([@nhalase](https://github.com/nhalase)) - Nick Halase ([@nhalase](https://github.com/nhalase))
- Nick Price ([@DominoTree](https://github.com/DominoTree)) - Nick Price ([@DominoTree](https://github.com/DominoTree))
- Nina Reynolds ([@cutecycle](https://github.com/cutecycle)) - Nina Reynolds ([@cutecycle](https://github.com/cutecycle))
- Noel Cower ([@nilium](https://github.com/nilium)) - Noel Cower ([@nilium](https://github.com/nilium))
- Oleksandr Koltsov ([@crambo](https://github.com/crambo)) - Oleksandr Koltsov ([@crambo](https://github.com/crambo))
- Phi Zeroth ([@phizeroth](https://github.com/phizeroth))
- Philipp Schmitt ([@pschmitt](https://github.com/pschmitt)) - Philipp Schmitt ([@pschmitt](https://github.com/pschmitt))
- Robert Lanzke ([@winkelement](https://github.com/winkelement)) - Robert Lanzke ([@winkelement](https://github.com/winkelement))
- Ruizhe Li ([@liruizhe1995](https://github.com/liruizhe1995))
- Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth)) - Sam Martin ([@ABitMoreDepth](https://github.com/ABitMoreDepth))
- Sean Coffey ([@seanecoffey](https://github.com/seanecoffey)) - Sean Coffey ([@seanecoffey](https://github.com/seanecoffey))
- Simon Ott ([@ottsimon](https://github.com/ottsimon))
- Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro)) - Stephan Kristyn ([@stevek-pro](https://github.com/stevek-pro))
- Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu)) - Theodore Chu ([@TheodoreChu](https://github.com/TheodoreChu))
- Tim Elmer ([@tim-elmer](https://github.com/tim-elmer))
- Tyler Denman ([@tylerguy](https://github.com/tylerguy)) - Tyler Denman ([@tylerguy](https://github.com/tylerguy))
- Victor Bilgin ([@vbilgin](https://github.com/vbilgin)) - Victor Bilgin ([@vbilgin](https://github.com/vbilgin))
- VMO Solutions ([@vmosolutions](https://github.com/vmosolutions)) - 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)) - aniketpanjwani ([@aniketpanjwani](https://github.com/aniketpanjwani))
- aytaa ([@aytaa](https://github.com/aytaa)) - aytaa ([@aytaa](https://github.com/aytaa))
- cesar ([@cesarnr21](https://github.com/cesarnr21))
- chaee ([@chaee](https://github.com/chaee)) - chaee ([@chaee](https://github.com/chaee))
- lwileczek ([@lwileczek](https://github.com/lwileczek))
- magicpotato ([@fortheday](https://github.com/fortheday)) - magicpotato ([@fortheday](https://github.com/fortheday))
- motoacs ([@motoacs](https://github.com/motoacs)) - motoacs ([@motoacs](https://github.com/motoacs))
- muzian666 ([@muzian666](https://github.com/muzian666))
- rburckner ([@rburckner](https://github.com/rburckner)) - rburckner ([@rburckner](https://github.com/rburckner))
- scorpion ([@scorpion](https://github.com/scorpion)) - scorpion ([@scorpion](https://github.com/scorpion))
- valantien ([@valantien](https://github.com/valantien)) - 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> <a href="https://opencollective.com/wikijs/sponsor/39/website" target="_blank"><img src="https://opencollective.com/wikijs/sponsor/39/avatar.svg"></a>
</td> </td>
</tr> </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> </tbody>
</table> </table>
</div> </div>
@ -350,15 +408,21 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
<table><tbody><tr><td> <table><tbody><tr><td>
<img width="441" height="1" /> <img width="441" height="1" />
- Aeternum
- Al Romano - Al Romano
- Alex Balabanov - Alex Balabanov
- Alex Milanov
- Alex Zen - Alex Zen
- Arti Zirk - Arti Zirk
- Ave
- Brandon Curtis - Brandon Curtis
- Damien Hottelier
- Daniel T. Holtzclaw
- Dave 'Sri' Seah - Dave 'Sri' Seah
- djagoo - djagoo
- dz - dz
- Douglas Lassance - Douglas Lassance
- Ergoflix
- Ernie Reid - Ernie Reid
- Etienne - Etienne
- Flemis Jurgenheimer - Flemis Jurgenheimer
@ -368,26 +432,34 @@ Thank you to all our patrons! 🙏 [[Become a patron](https://www.patreon.com/re
- Hope - Hope
- Ian - Ian
- Imari Childress - Imari Childress
- Iskander Callos
</td><td> </td><td>
<img width="441" height="1" /> <img width="441" height="1" />
- Iskander Callos
- Josh Stewart - Josh Stewart
- Justin Dunsworth - Justin Dunsworth
- Keir - Keir
- Loïc CRAMPON - Loïc CRAMPON
- Ludgeir Ibanez - Ludgeir Ibanez
- Lyn Matten
- Mads Rosendahl
- Mark Mansur - Mark Mansur
- Matt Gedigian - Matt Gedigian
- Mike Ditton
- Nate Figz - Nate Figz
- Patryk - Patryk
- Paul O'Fallon
- Philipp Schürch - Philipp Schürch
- Tracey Duffy - Tracey Duffy
- Quaxim
- Richeir - Richeir
- Sergio Navarro Fernández
- Shad Narcher - Shad Narcher
- ShadowVoyd
- SmartNET.works - SmartNET.works
- Stepan Sokolovskyi - Stepan Sokolovskyi
- Zach Crawford
- Zach Maynard - Zach Maynard
- 张白驹 - 张白驹
@ -407,9 +479,6 @@ This project exists thanks to all the people who contribute. [[Contribute]](http
<h2 align="center">Special Thanks</h2> <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://js.wiki/legacy/logo_browserstack.png)
[Browserstack](https://www.browserstack.com/) for providing access to their great cross-browser testing tools. [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. [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://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://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://static.requarks.io/logo/lokalise-text-h40.png)
[Lokalise](https://lokalise.com/) for providing access to their great localization tool. [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://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://static.requarks.io/logo/ngrok-h40.png)
[ngrok](https://ngrok.com) for providing access to their great HTTP tunneling services. [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 ## 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 ### B) Send an email

@ -65,15 +65,6 @@
v-list-item(to='/comments') v-list-item(to='/comments')
v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
v-list-item-title {{ $t('admin:comments.title') }} 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(to='/rendering', color='primary')
v-list-item-avatar(size='24', tile): v-icon mdi-cogs v-list-item-avatar(size='24', tile): v-icon mdi-cogs
v-list-item-title {{ $t('admin:rendering.title') }} 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(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
v-list-item-title {{ $t('admin:utilities.title') }} 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( v-list-group(
to='/dev' to='/dev'
no-action no-action

@ -184,7 +184,7 @@
v-list-item-title Netlify 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-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-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 v-icon(color='grey') mdi-earth
</template> </template>

@ -82,6 +82,15 @@
:return-object='false' :return-object='false'
:hint='$t(`admin:general.contentLicenseHint`)' :hint='$t(`admin:general.contentLicenseHint`)'
persistent-hint 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 v-divider
.overline.grey--text.pa-4 SEO .overline.grey--text.pa-4 SEO
@ -144,7 +153,7 @@
//- ) //- )
//- v-divider.mt-3 //- v-divider.mt-3
v-switch( v-switch.mt-0(
inset inset
label='Comments' label='Comments'
color='indigo' color='indigo'
@ -164,6 +173,89 @@
//- disabled //- 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') component(:is='activeModal')
</template> </template>
@ -197,12 +289,21 @@ export default {
analyticsId: '', analyticsId: '',
company: '', company: '',
contentLicense: '', contentLicense: '',
footerOverride: '',
logoUrl: '', logoUrl: '',
featureAnalytics: false, featureAnalytics: false,
featurePageRatings: false, featurePageRatings: false,
featurePageComments: false, featurePageComments: false,
featurePersonalWikis: false, featurePersonalWikis: false,
featureTinyPNG: false featureTinyPNG: false,
pageExtensions: '',
editFab: false,
editMenuBar: false,
editMenuBtn: false,
editMenuExternalBtn: false,
editMenuExternalName: '',
editMenuExternalIcon: '',
editMenuExternalUrl: ''
}, },
metaRobots: [ metaRobots: [
{ text: 'Index', value: 'index' }, { text: 'Index', value: 'index' },
@ -217,6 +318,7 @@ export default {
logoUrl: sync('site/logoUrl'), logoUrl: sync('site/logoUrl'),
company: sync('site/company'), company: sync('site/company'),
contentLicense: sync('site/contentLicense'), contentLicense: sync('site/contentLicense'),
footerOverride: sync('site/footerOverride'),
activeModal: sync('editor/activeModal'), activeModal: sync('editor/activeModal'),
contentLicenses () { contentLicenses () {
return [ return [
@ -247,33 +349,51 @@ export default {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation ( mutation (
$host: String! $host: String
$title: String! $title: String
$description: String! $description: String
$robots: [String]! $robots: [String]
$analyticsService: String! $analyticsService: String
$analyticsId: String! $analyticsId: String
$company: String! $company: String
$contentLicense: String! $contentLicense: String
$logoUrl: String! $footerOverride: String
$featurePageRatings: Boolean! $logoUrl: String
$featurePageComments: Boolean! $pageExtensions: String
$featurePersonalWikis: Boolean! $featurePageRatings: Boolean
$featurePageComments: Boolean
$featurePersonalWikis: Boolean
$editFab: Boolean
$editMenuBar: Boolean
$editMenuBtn: Boolean
$editMenuExternalBtn: Boolean
$editMenuExternalName: String
$editMenuExternalIcon: String
$editMenuExternalUrl: String
) { ) {
site { site {
updateConfig( updateConfig(
host: $host, host: $host
title: $title, title: $title
description: $description, description: $description
robots: $robots, robots: $robots
analyticsService: $analyticsService, analyticsService: $analyticsService
analyticsId: $analyticsId, analyticsId: $analyticsId
company: $company, company: $company
contentLicense: $contentLicense, contentLicense: $contentLicense
logoUrl: $logoUrl, footerOverride: $footerOverride
featurePageRatings: $featurePageRatings, logoUrl: $logoUrl
featurePageComments: $featurePageComments, pageExtensions: $pageExtensions
featurePageRatings: $featurePageRatings
featurePageComments: $featurePageComments
featurePersonalWikis: $featurePersonalWikis featurePersonalWikis: $featurePersonalWikis
editFab: $editFab
editMenuBar: $editMenuBar
editMenuBtn: $editMenuBtn
editMenuExternalBtn: $editMenuExternalBtn
editMenuExternalName: $editMenuExternalName
editMenuExternalIcon: $editMenuExternalIcon
editMenuExternalUrl: $editMenuExternalUrl
) { ) {
responseResult { responseResult {
succeeded succeeded
@ -294,10 +414,19 @@ export default {
analyticsId: _.get(this.config, 'analyticsId', ''), analyticsId: _.get(this.config, 'analyticsId', ''),
company: _.get(this.config, 'company', ''), company: _.get(this.config, 'company', ''),
contentLicense: _.get(this.config, 'contentLicense', ''), contentLicense: _.get(this.config, 'contentLicense', ''),
footerOverride: _.get(this.config, 'footerOverride', ''),
logoUrl: _.get(this.config, 'logoUrl', ''), logoUrl: _.get(this.config, 'logoUrl', ''),
pageExtensions: _.get(this.config, 'pageExtensions', ''),
featurePageRatings: _.get(this.config, 'featurePageRatings', false), featurePageRatings: _.get(this.config, 'featurePageRatings', false),
featurePageComments: _.get(this.config, 'featurePageComments', 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) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
@ -311,6 +440,7 @@ export default {
this.siteTitle = this.config.title this.siteTitle = this.config.title
this.company = this.config.company this.company = this.config.company
this.contentLicense = this.config.contentLicense this.contentLicense = this.config.contentLicense
this.footerOverride = this.config.footerOverride
this.logoUrl = this.config.logoUrl this.logoUrl = this.config.logoUrl
} catch (err) { } catch (err) {
this.$store.commit('pushGraphError', err) this.$store.commit('pushGraphError', err)
@ -346,10 +476,19 @@ export default {
analyticsId analyticsId
company company
contentLicense contentLicense
footerOverride
logoUrl logoUrl
pageExtensions
featurePageRatings featurePageRatings
featurePageComments featurePageComments
featurePersonalWikis featurePersonalWikis
editFab
editMenuBar
editMenuBtn
editMenuExternalBtn
editMenuExternalName
editMenuExternalIcon
editMenuExternalUrl
} }
} }
} }

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

@ -57,6 +57,16 @@
:hint='$t(`admin:mail.smtpPortHint`)' :hint='$t(`admin:mail.smtpPortHint`)'
style='max-width: 300px;' 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-switch(
v-model='config.secure' v-model='config.secure'
:label='$t(`admin:mail.smtpTLS`)' :label='$t(`admin:mail.smtpTLS`)'
@ -169,6 +179,7 @@ export default {
senderEmail: '', senderEmail: '',
host: '', host: '',
port: 0, port: 0,
name: '',
secure: false, secure: false,
verifySSL: false, verifySSL: false,
user: '', user: '',
@ -192,6 +203,7 @@ export default {
senderEmail: this.config.senderEmail || '', senderEmail: this.config.senderEmail || '',
host: this.config.host || '', host: this.config.host || '',
port: _.toSafeInteger(this.config.port) || 0, port: _.toSafeInteger(this.config.port) || 0,
name: this.config.name || '',
secure: this.config.secure || false, secure: this.config.secure || false,
verifySSL: this.config.verifySSL || false, verifySSL: this.config.verifySSL || false,
user: this.config.user || '', user: this.config.user || '',

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

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

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

@ -499,9 +499,9 @@ export default {
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' }, { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' }, { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' }, { 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-03:00) Stanley', value: 'Atlantic/Stanley' },
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' }, { 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-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' }, { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' }, { text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },

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

@ -77,6 +77,7 @@ export default {
editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'), editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'),
editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'), editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.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'), editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'), editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.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 codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/
const codeBlockEndMatch = /^`{3}$/ 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 firstLine = cm.getLine(start.line)
const lastLineNo = cm.lastLine() const lastLineNo = cm.lastLine()
let end let end
@ -59,4 +65,4 @@ CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
from: CodeMirror.Pos(start.line, firstLine.length), from: CodeMirror.Pos(start.line, firstLine.length),
to: CodeMirror.Pos(end, cm.getLine(end).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')}} span {{$t('editor:markup.insertAssets')}}
v-tooltip(right, color='teal') v-tooltip(right, color='teal')
template(v-slot:activator='{ on }') 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-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').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-icon mdi-chart-multiline v-icon mdi-chart-multiline
span {{$t('editor:markup.insertDiagram')}} 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') template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer v-spacer
v-tooltip(right, color='teal') v-tooltip(right, color='teal')
template(v-slot:activator='{ on }') 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 v-icon mdi-arrow-expand-all
span {{$t('editor:markup.distractionFreeMode')}} span {{$t('editor:markup.distractionFreeMode')}}
v-tooltip(right, color='teal') v-tooltip(right, color='teal')
template(v-slot:activator='{ on }') 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 v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle
span {{$t('editor:markup.markdownFormattingHelp')}} span {{$t('editor:markup.markdownFormattingHelp')}}
.editor-markdown-editor .editor-markdown-editor
@ -220,12 +195,12 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/fold/foldcode.js' import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css' import 'codemirror/addon/fold/foldgutter.css'
import './markdown/fold'
// Markdown-it // Markdown-it
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import mdAttrs from 'markdown-it-attrs' 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 mdTaskLists from 'markdown-it-task-lists'
import mdExpandTabs from 'markdown-it-expand-tabs' import mdExpandTabs from 'markdown-it-expand-tabs'
import mdAbbr from 'markdown-it-abbr' import mdAbbr from 'markdown-it-abbr'
@ -250,6 +225,7 @@ import mermaid from 'mermaid'
// Helpers // Helpers
import katexHelper from './common/katex' import katexHelper from './common/katex'
import tabsetHelper from './markdown/tabset' import tabsetHelper from './markdown/tabset'
import cmFold from './common/cmFold'
// ======================================== // ========================================
// INIT // INIT
@ -288,6 +264,7 @@ const md = new MarkdownIt({
.use(mdAttrs, { .use(mdAttrs, {
allowedAttributes: ['id', 'class', 'target'] allowedAttributes: ['id', 'class', 'target']
}) })
.use(mdDecorate)
.use(underline) .use(underline)
.use(mdEmoji) .use(mdEmoji)
.use(mdTaskLists, { label: false, labelAfter: false }) .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.heading_open = injectLineNumbers
md.renderer.rules.blockquote_open = injectLineNumbers md.renderer.rules.blockquote_open = injectLineNumbers
cmFold.register('markdown')
// ======================================== // ========================================
// PLANTUML // PLANTUML
// ======================================== // ========================================
@ -346,11 +324,12 @@ plantuml.init(md, {})
// KATEX // KATEX
// ======================================== // ========================================
const macros = {}
md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline) md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline)
md.renderer.rules.katex_inline = (tokens, idx) => { md.renderer.rules.katex_inline = (tokens, idx) => {
try { try {
return katex.renderToString(tokens[idx].content, { return katex.renderToString(tokens[idx].content, {
displayMode: false displayMode: false, macros
}) })
} catch (err) { } catch (err) {
console.warn(err) console.warn(err)
@ -363,7 +342,7 @@ md.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, {
md.renderer.rules.katex_block = (tokens, idx) => { md.renderer.rules.katex_block = (tokens, idx) => {
try { try {
return `<p>` + katex.renderToString(tokens[idx].content, { return `<p>` + katex.renderToString(tokens[idx].content, {
displayMode: true displayMode: true, macros
}) + `</p>` }) + `</p>`
} catch (err) { } catch (err) {
console.warn(err) console.warn(err)

@ -6,57 +6,7 @@
.subtitle-1.white--text {{$t('editor:select.title')}} .subtitle-1.white--text {{$t('editor:select.title')}}
v-container(grid-list-lg, fluid) v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center) v-layout(row, wrap, justify-center)
v-flex(xs4) v-flex(xs6)
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-card.radius-7.animated.fadeInUp.wait-p1s( v-card.radius-7.animated.fadeInUp.wait-p1s(
hover hover
light light
@ -66,28 +16,8 @@
img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;') img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
.body-2.primary--text.mt-2 Markdown .body-2.primary--text.mt-2 Markdown
.caption.grey--text Plain Text Formatting .caption.grey--text Plain Text Formatting
v-flex(xs4) v-flex(xs6)
v-hover v-card.radius-7.animated.fadeInUp.wait-p2s(
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(
hover hover
light light
ripple ripple
@ -96,85 +26,36 @@
img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;') img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
.body-2.mt-2.primary--text Visual Editor .body-2.mt-2.primary--text Visual Editor
.caption.grey--text Rich-text WYSIWYG .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-flex(xs4)
v-hover v-card.radius-7.animated.fadeInUp.wait-p3s(
template(v-slot:default='{ hover }') hover
v-card.radius-7.animated.fadeInUp( light
hover ripple
light )
ripple v-card-text.text-center(@click='selectEditor("asciidoc")')
) img(src='/_assets/svg/editor-icon-asciidoc.svg', alt='AsciiDoc', style='width: 36px;')
v-card-text.text-center(@click='fromTemplate') .body-2.primary--text.mt-2 AsciiDoc
img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;') .caption.grey--text Plain Text Formatting
.body-2.mt-1.teal--text From Template
.caption.grey--text Use an existing page...
v-flex(xs4) v-flex(xs4)
v-hover v-card.radius-7.animated.fadeInUp.wait-p4s(
template(v-slot:default='{ hover }') hover
v-card.radius-7.teal.animated.fadeInUp.wait-p1s( light
hover ripple
light )
ripple v-card-text.text-center(@click='selectEditor("code")')
) img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
//- v-card-text.text-center(@click='selectEditor("redirect")') .body-2.primary--text.mt-2 Code
v-card-text.text-center(@click='') .caption.grey--text Raw HTML
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-flex(xs4) v-flex(xs4)
v-hover v-card.radius-7.animated.fadeInUp.wait-p5s(
template(v-slot:default='{ hover }') hover
v-card.radius-7.teal.animated.fadeInUp.wait-p2s( light
hover ripple
light )
ripple v-card-text.text-center(@click='fromTemplate')
) img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
v-card-text.text-center(@click='') .body-2.mt-1.teal--text From Template
img(src='/_assets/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;') .caption.grey--text Use an existing page...
.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
page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist) page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
</template> </template>

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

@ -469,9 +469,9 @@ export default {
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' }, { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' }, { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' }, { 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-03:00) Stanley', value: 'Atlantic/Stanley' },
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' }, { 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-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' }, { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' }, { 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.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-4`')
v-card-text v-card-text
pre pre
code slot
slot
nav-footer nav-footer
notify notify

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

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

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

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

@ -8,6 +8,7 @@ query {
min min
max max
} }
tocPosition
injectCSS injectCSS
injectHead injectHead
injectBody injectBody

@ -30,6 +30,12 @@ html {
} }
} }
@media only screen and (min-width:960px) {
.v-application .v-footer {
padding-left: 272px
}
}
#root .v-application { #root .v-application {
.overline { .overline {
line-height: 1rem; 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 manage: false
} }
}, },
commentsCount: 0 commentsCount: 0,
editShortcuts: {
editFab: false,
editMenuBar: false,
editMenuBtn: false,
editMenuExternalBtn: false,
editMenuExternalName: '',
editMenuExternalIcon: '',
editMenuExternalUrl: ''
}
} }
export default { export default {

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

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

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

@ -49,15 +49,50 @@
status-indicator.ml-3(negative, pulse) status-indicator.ml-3(negative, pulse)
v-divider v-divider
v-container.grey.pa-0(fluid, :class='$vuetify.theme.dark ? `darken-4-l3` : `lighten-4`') 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-row.page-header-section(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`') v-col.page-col-content.is-page-header(
.headline.grey--text(:class='$vuetify.theme.dark ? `text--lighten-2` : `text--darken-3`') {{title}} :offset-xl='tocPosition === `left` ? 2 : 0'
.caption.grey--text.text--darken-1 {{description}} :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-divider
v-container.pl-5.pt-4(fluid, grid-list-xl) v-container.pl-5.pt-4(fluid, grid-list-xl)
v-layout(row) v-layout(row)
v-flex.page-col-sd(lg3, xl2, v-if='$vuetify.breakpoint.lgAndUp') v-flex.page-col-sd(
v-card.mb-5(v-if='tocDecoded.length') 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')}} .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` : ``') v-list.py-2(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``')
page-toc-item( page-toc-item(
@ -67,7 +102,7 @@
:min='tocOptionsDecoded.min' :min='tocOptionsDecoded.min'
:max='tocOptionsDecoded.max' :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 .pa-5
.overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}} .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}
v-chip.mr-1.mb-1( 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-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 .pa-5
.overline.pb-2.blue-grey--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : `text--darken-2`') .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')}} 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 v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-1` : `blue-grey darken-2`', dense) mdi-comment-plus
span {{$t('common:comments.newComment')}} span {{$t('common:comments.newComment')}}
v-card.mb-5 v-card.page-author-card.mb-5
.pa-5 .pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') .overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span {{$t('common:page.lastEditedBy')}} span {{$t('common:page.lastEditedBy')}}
@ -140,8 +175,8 @@
) )
v-icon(color='indigo', dense) mdi-history v-icon(color='indigo', dense) mdi-history
span {{$t('common:header.history')}} span {{$t('common:header.history')}}
.body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }} .page-author-card-name.body-2.grey--text(:class='$vuetify.theme.dark ? `` : `text--darken-3`') {{ authorName }}
.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }} .page-author-card-date.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
//- v-card.mb-5 //- v-card.mb-5
//- .pa-5 //- .pa-5
@ -156,13 +191,13 @@
//- ) //- )
//- .caption.grey--text 5 votes //- .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-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-d3` : `grey lighten-3`', flat, dense)
v-spacer v-spacer
v-tooltip(bottom) //- v-tooltip(bottom)
template(v-slot:activator='{ on }') //- template(v-slot:activator='{ on }')
v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark //- v-btn(icon, tile, v-on='on', :aria-label='$t(`common:page.bookmark`)'): v-icon(color='grey') mdi-bookmark
span {{$t('common:page.bookmark')}} //- span {{$t('common:page.bookmark')}}
v-menu(offset-y, bottom, min-width='300') v-menu(offset-y, bottom, min-width='300')
template(v-slot:activator='{ on: menu }') template(v-slot:activator='{ on: menu }')
v-tooltip(bottom) v-tooltip(bottom)
@ -181,8 +216,14 @@
span {{$t('common:page.printFormat')}} span {{$t('common:page.printFormat')}}
v-spacer v-spacer
v-flex.page-col-content(xs12, lg9, xl10) v-flex.page-col-content(
v-tooltip(:right='$vuetify.rtl', :left='!$vuetify.rtl', v-if='hasAnyPagePermissions') 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 }') template(v-slot:activator='{ on: onEditActivator }')
v-speed-dial( v-speed-dial(
v-model='pageEditFab' v-model='pageEditFab'
@ -439,6 +480,14 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
editShortcuts: {
type: String,
default: ''
},
filename: {
type: String,
default: ''
},
tocOptions: { tocOptions: {
type: String, type: String,
default: '' default: ''
@ -480,6 +529,7 @@ export default {
isAuthenticated: get('user/authenticated'), isAuthenticated: get('user/authenticated'),
commentsCount: get('page/commentsCount'), commentsCount: get('page/commentsCount'),
commentsPerms: get('page/effectivePermissions@comments'), commentsPerms: get('page/effectivePermissions@comments'),
editShortcutsObj: get('page/editShortcuts'),
rating: { rating: {
get () { get () {
return 3.5 return 3.5
@ -511,6 +561,7 @@ export default {
tocDecoded () { tocDecoded () {
return JSON.parse(Buffer.from(this.toc, 'base64').toString()) return JSON.parse(Buffer.from(this.toc, 'base64').toString())
}, },
tocPosition: get('site/tocPosition'),
tocOptionsDecoded () { tocOptionsDecoded () {
return JSON.parse(Buffer.from(this.tocOptions, 'base64').toString()) return JSON.parse(Buffer.from(this.tocOptions, 'base64').toString())
}, },
@ -525,7 +576,14 @@ export default {
return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission || return this.hasAdminPermission || this.hasWritePagesPermission || this.hasManagePagesPermission ||
this.hasDeletePagesPermission || this.hasReadSourcePermission || this.hasReadHistoryPermission 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() { created() {
this.$store.set('page/authorId', this.authorId) this.$store.set('page/authorId', this.authorId)
@ -543,6 +601,9 @@ export default {
if (this.effectivePermissions) { if (this.effectivePermissions) {
this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString())) 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') this.$store.set('page/mode', 'view')
}, },
@ -588,6 +649,8 @@ export default {
this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts) this.$vuetify.goTo(decodeURIComponent(ev.currentTarget.hash), this.scrollOpts)
} }
}) })
window.boot.notify('page-ready')
}) })
}, },
methods: { methods: {
@ -682,4 +745,54 @@ export default {
display: none; 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> </style>

@ -282,7 +282,7 @@
content: "\F02FC"; content: "\F02FC";
} }
code { code:not([class^="language-"]) {
background-color: mc('blue', '50'); background-color: mc('blue', '50');
color: mc('blue', '800'); color: mc('blue', '800');
} }
@ -302,7 +302,7 @@
content: "\F0026"; content: "\F0026";
} }
code { code:not([class^="language-"]) {
background-color: mc('orange', '50'); background-color: mc('orange', '50');
color: mc('orange', '800'); color: mc('orange', '800');
} }
@ -323,7 +323,7 @@
content: "\F0159"; content: "\F0159";
} }
code { code:not([class^="language-"]) {
background-color: mc('red', '50'); background-color: mc('red', '50');
color: mc('red', '800'); color: mc('red', '800');
} }
@ -343,7 +343,7 @@
content: "\F0E1E"; content: "\F0E1E";
} }
code { code:not([class^="language-"]) {
background-color: mc('green', '50'); background-color: mc('green', '50');
color: mc('green', '800'); 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 // LISTS
// --------------------------------- // ---------------------------------
@ -543,6 +685,27 @@
display:inline-block; display:inline-block;
vertical-align:top; vertical-align:top;
padding-top:0; 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 { table {
margin: .5rem 1.75rem; margin: .5rem 0;
border-spacing: 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 { th {
padding: .75rem; padding: .75rem;
border-bottom: 2px solid mc('grey', '500'); border-bottom: 2px solid mc('grey', '500');
color: mc('grey', '600'); 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 { td {
@ -677,7 +867,56 @@
tr { tr {
td { 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'); border: 1px solid mc('blue-grey', '100');
box-shadow: inset -1px -1px 0 0 #FFF, inset 1px 0 0 #FFF; box-shadow: inset -1px -1px 0 0 #FFF, inset 1px 0 0 #FFF;
padding: .5rem .75rem; padding: .5rem .75rem;
border-radius: 0 !important;
@at-root .theme--dark & { @at-root .theme--dark & {
border-color: mc('grey', '700'); 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 // IMAGES
// --------------------------------- // ---------------------------------
@ -1036,4 +1282,8 @@
.comments-container { .comments-container {
display: none; display: none;
} }
.page-edit-shortcuts {
display: none;
}
} }

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

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

@ -1,11 +1,11 @@
# -- DEV DOCKERFILE -- # -- DEV DOCKERFILE --
# -- DO NOT USE IN PRODUCTION! -- # -- DO NOT USE IN PRODUCTION! --
FROM node:14 FROM node:18
LABEL maintainer "requarks.io" LABEL maintainer "requarks.io"
RUN apt-get update && \ 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 mkdir -p /wiki
WORKDIR /wiki WORKDIR /wiki

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

@ -2,7 +2,7 @@ version: "3"
services: services:
db: db:
image: postgres:11-alpine image: postgres:15-alpine
environment: environment:
POSTGRES_DB: wiki POSTGRES_DB: wiki
POSTGRES_PASSWORD: wikijsrocks 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.enabled` | Enable sideloading of locale files from git | `false` |
| `sideload.repoURL` | Git repository URL containing locale files | `https://github.com/Requarks/wiki-localization` | | `sideload.repoURL` | Git repository URL containing locale files | `https://github.com/Requarks/wiki-localization` |
| `sideload.env` | Environment variables for sideload Container | `{}` | | `sideload.env` | Environment variables for sideload Container | `{}` |
| `nodeExtraCaCerts` | Trusted certificates path | `nil` |
| `postgresql.enabled` | Deploy postgres server (see below) | `true` | | `postgresql.enabled` | Deploy postgres server (see below) | `true` |
| `postgresql.postgresqlDatabase` | Postgres database name | `wiki` | | `postgresql.postgresqlDatabase` | Postgres database name | `wiki` |
| `postgresql.postgresqlUser` | Postgres username | `postgres` | | `postgresql.postgresqlUser` | Postgres username | `postgres` |
@ -175,3 +176,38 @@ See the [Configuration](#configuration) section to configure the PVC or to disab
## Ingress ## 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. 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 }} {{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }} {{- range $host := .Values.ingress.hosts }}
{{- range .paths }} {{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- else if contains "NodePort" .Values.service.type }} {{- else if contains "NodePort" .Values.service.type }}

@ -14,6 +14,8 @@ spec:
metadata: metadata:
labels: labels:
{{- include "wiki.selectorLabels" . | nindent 8 }} {{- include "wiki.selectorLabels" . | nindent 8 }}
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
spec: spec:
{{- with .Values.imagePullSecrets }} {{- with .Values.imagePullSecrets }}
imagePullSecrets: imagePullSecrets:
@ -39,8 +41,18 @@ spec:
image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}" image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}"
imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }} imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }}
env: env:
{{- if .Values.nodeExtraCaCerts }}
- name: NODE_EXTRA_CA_CERTS
value: {{ .Values.nodeExtraCaCerts }}
{{- end }}
- name: DB_TYPE - name: DB_TYPE
value: postgres 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 - name: DB_HOST
value: {{ template "wiki.postgresql.host" . }} value: {{ template "wiki.postgresql.host" . }}
- name: DB_PORT - name: DB_PORT
@ -62,6 +74,7 @@ spec:
name: {{ template "wiki.postgresql.secret" . }} name: {{ template "wiki.postgresql.secret" . }}
{{- end }} {{- end }}
key: {{ template "wiki.postgresql.secretKey" . }} key: {{ template "wiki.postgresql.secretKey" . }}
{{- end }}
- name: HA_ACTIVE - name: HA_ACTIVE
value: {{ .Values.replicaCount | int | le 2 | quote }} value: {{ .Values.replicaCount | int | le 2 | quote }}
{{- with .Values.volumeMounts }} {{- with .Values.volumeMounts }}
@ -76,6 +89,8 @@ spec:
{{- toYaml .Values.livenessProbe | nindent 12 }} {{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe: readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }} {{- toYaml .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}

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

@ -32,6 +32,18 @@ readinessProbe:
path: /healthz path: /healthz
port: http port: http
startupProbe:
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 60
httpGet:
path: /healthz
port: http
podAnnotations: {}
podSecurityContext: {} podSecurityContext: {}
# fsGroup: 2000 # fsGroup: 2000
@ -51,6 +63,7 @@ service:
# type: LoadBalancer # type: LoadBalancer
# httpsPort: 443 # httpsPort: 443
# annotations: {} # annotations: {}
# loadBalancerIP: 172.16.0.1
ingress: ingress:
enabled: true enabled: true
@ -91,7 +104,7 @@ volumeMounts: []
volumes: [] 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: sideload:
enabled: false enabled: false
# Git-Repo containing all locales.json-files you need: # Git-Repo containing all locales.json-files you need:
@ -102,6 +115,16 @@ sideload:
# - name: HTTPS_PROXY # - name: HTTPS_PROXY
# value: http://my.proxy.com:3128 # 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. ## Configuration values for the postgresql dependency.
## ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md ## ref: https://github.com/kubernetes/charts/blob/master/stable/postgresql/README.md
## ##

@ -78,7 +78,8 @@
"scripts/013-docker-dns.sh", "scripts/013-docker-dns.sh",
"scripts/014-ufw-docker.sh", "scripts/014-ufw-docker.sh",
"scripts/020-application-tag.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 fi
chmod 1777 /tmp chmod 1777 /tmp
export DEBIAN_FRONTEND=noninteractive
apt-get -y update 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/* rm -rf /tmp/* /var/tmp/*
history -c history -c
cat /dev/null > /root/.bash_history cat /dev/null > /root/.bash_history
unset HISTFILE unset HISTFILE
apt-get -y autoremove
apt-get -y autoclean
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; 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/log/*.gz /var/log/*.[0-9] /var/log/*-????????
rm -rf /var/lib/cloud/instances/* 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( script(
crossorigin='anonymous' 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 //- JS

@ -7,11 +7,12 @@
"dev": true, "dev": true,
"scripts": { "scripts": {
"start": "node server", "start": "node server",
"dev": "node dev", "dev": "NODE_OPTIONS=--openssl-legacy-provider node dev",
"build": "webpack --profile --config dev/webpack/webpack.prod.js", "build": "NODE_OPTIONS=--openssl-legacy-provider webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "webpack --config dev/webpack/webpack.dev.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", "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"cypress:open": "cypress open" "cypress:open": "cypress open",
"postinstall": "patch-package"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -36,9 +37,9 @@
"node": ">=10.12" "node": ">=10.12"
}, },
"dependencies": { "dependencies": {
"@azure/storage-blob": "12.9.0", "@azure/storage-blob": "12.12.0",
"@exlinc/keycloak-passport": "1.0.2", "@exlinc/keycloak-passport": "1.0.2",
"@joplin/turndown-plugin-gfm": "1.0.43", "@joplin/turndown-plugin-gfm": "1.0.45",
"@root/csr": "0.8.1", "@root/csr": "0.8.1",
"@root/keypairs": "0.10.3", "@root/keypairs": "0.10.3",
"@root/pem": "1.0.4", "@root/pem": "1.0.4",
@ -48,17 +49,18 @@
"apollo-fetch": "0.7.0", "apollo-fetch": "0.7.0",
"apollo-server": "2.25.2", "apollo-server": "2.25.2",
"apollo-server-express": "2.25.2", "apollo-server-express": "2.25.2",
"asciidoctor": "2.2.6",
"auto-load": "3.0.4", "auto-load": "3.0.4",
"aws-sdk": "2.1125.0", "aws-sdk": "2.1309.0",
"azure-search-client": "3.1.5", "azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1", "bcryptjs-then": "1.0.1",
"bluebird": "3.7.2", "bluebird": "3.7.2",
"body-parser": "1.20.0", "body-parser": "1.20.1",
"chalk": "4.1.0", "chalk": "4.1.0",
"cheerio": "1.0.0-rc.5", "cheerio": "1.0.0-rc.5",
"chokidar": "3.5.3", "chokidar": "3.5.3",
"chromium-pickle-js": "0.2.0", "chromium-pickle-js": "0.2.0",
"clean-css": "4.2.3", "clean-css": "5.3.2",
"command-exists": "1.2.9", "command-exists": "1.2.9",
"compression": "1.7.4", "compression": "1.7.4",
"connect-session-knex": "2.0.0", "connect-session-knex": "2.0.0",
@ -66,18 +68,18 @@
"cors": "2.8.5", "cors": "2.8.5",
"cuint": "0.2.2", "cuint": "0.2.2",
"custom-error-instance": "2.1.2", "custom-error-instance": "2.1.2",
"dependency-graph": "0.9.0", "dependency-graph": "0.11.0",
"diff": "4.0.2", "diff": "4.0.2",
"diff2html": "3.1.14", "diff2html": "3.1.14",
"dompurify": "2.2.7", "dompurify": "2.4.3",
"dotize": "0.3.0", "dotize": "0.3.0",
"elasticsearch6": "npm:@elastic/elasticsearch@6", "elasticsearch6": "npm:@elastic/elasticsearch@6",
"elasticsearch7": "npm:@elastic/elasticsearch@7", "elasticsearch7": "npm:@elastic/elasticsearch@7",
"emoji-regex": "9.2.2", "emoji-regex": "10.2.1",
"eventemitter2": "6.4.5", "eventemitter2": "6.4.9",
"express": "4.18.1", "express": "4.18.2",
"express-brute": "1.0.1", "express-brute": "1.0.1",
"express-session": "1.17.2", "express-session": "1.17.3",
"file-type": "15.0.1", "file-type": "15.0.1",
"filesize": "6.1.0", "filesize": "6.1.0",
"fs-extra": "9.0.1", "fs-extra": "9.0.1",
@ -93,11 +95,11 @@
"i18next-express-middleware": "2.0.0", "i18next-express-middleware": "2.0.0",
"i18next-node-fs-backend": "2.1.3", "i18next-node-fs-backend": "2.1.3",
"image-size": "0.9.2", "image-size": "0.9.2",
"js-base64": "3.7.2", "js-base64": "3.7.4",
"js-binary": "1.2.0", "js-binary": "1.2.0",
"js-yaml": "3.14.0", "js-yaml": "3.14.0",
"jsdom": "16.4.0", "jsdom": "16.4.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"katex": "0.12.0", "katex": "0.12.0",
"klaw": "3.0.0", "klaw": "3.0.0",
"knex": "0.21.7", "knex": "0.21.7",
@ -106,7 +108,8 @@
"markdown-it": "11.0.1", "markdown-it": "11.0.1",
"markdown-it-abbr": "1.0.4", "markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "3.0.3", "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-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6", "markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "3.0.3", "markdown-it-footnote": "3.0.3",
@ -114,26 +117,27 @@
"markdown-it-mark": "3.0.1", "markdown-it-mark": "3.0.1",
"markdown-it-mathjax": "2.0.0", "markdown-it-mathjax": "2.0.0",
"markdown-it-multimd-table": "4.0.3", "markdown-it-multimd-table": "4.0.3",
"markdown-it-pivot-table": "1.0.5",
"markdown-it-sub": "1.0.0", "markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0", "markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1", "markdown-it-task-lists": "2.1.1",
"mathjax": "3.1.2", "mathjax": "3.2.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"moment": "2.29.3", "moment": "2.29.4",
"moment-timezone": "0.5.31", "moment-timezone": "0.5.40",
"mongodb": "3.6.5", "mongodb": "3.6.5",
"ms": "2.1.3", "ms": "2.1.3",
"mssql": "6.2.3", "mssql": "6.2.3",
"multer": "1.4.4", "multer": "1.4.4",
"mysql2": "2.3.3", "mysql2": "3.1.0",
"nanoid": "3.2.0", "nanoid": "3.2.0",
"node-2fa": "1.1.2", "node-2fa": "1.1.2",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "6.7.4", "nodemailer": "6.9.1",
"objection": "2.2.18", "objection": "2.2.18",
"passport": "0.4.1", "passport": "0.4.1",
"passport-auth0": "1.4.2", "passport-auth0": "1.4.3",
"passport-azure-ad": "4.3.1", "passport-azure-ad": "4.3.4",
"passport-cas": "0.1.1", "passport-cas": "0.1.1",
"passport-discord": "0.1.4", "passport-discord": "0.1.4",
"passport-dropbox-oauth2": "1.1.0", "passport-dropbox-oauth2": "1.1.0",
@ -141,50 +145,52 @@
"passport-github2": "0.1.12", "passport-github2": "0.1.12",
"passport-gitlab2": "5.0.0", "passport-gitlab2": "5.0.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.1",
"passport-ldapauth": "3.0.1", "passport-ldapauth": "3.0.1",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"passport-microsoft": "0.1.0", "passport-microsoft": "0.1.0",
"passport-oauth2": "1.6.1", "passport-oauth2": "1.6.1",
"passport-okta-oauth": "0.0.1", "passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.0.2", "passport-openidconnect": "0.1.1",
"passport-saml": "3.2.1", "passport-saml": "3.2.4",
"passport-slack-oauth2": "1.1.1", "passport-slack-oauth2": "1.1.1",
"passport-twitch-strategy": "2.2.0", "passport-twitch-strategy": "2.2.0",
"patch-package": "8.0.0",
"pem-jwk": "2.0.0", "pem-jwk": "2.0.0",
"pg": "8.4.1", "pg": "8.9.0",
"pg-hstore": "2.3.4", "pg-hstore": "2.3.4",
"pg-pubsub": "0.5.0", "pg-pubsub": "0.5.0",
"pg-query-stream": "3.3.1", "pg-query-stream": "4.3.0",
"pg-tsquery": "8.1.0", "pg-tsquery": "8.4.1",
"postinstall-postinstall": "2.1.0",
"pug": "3.0.2", "pug": "3.0.2",
"punycode": "2.1.1", "punycode": "2.3.0",
"qr-image": "3.2.0", "qr-image": "3.2.0",
"raven": "2.6.4", "raven": "2.6.4",
"remove-markdown": "0.3.0", "remove-markdown": "0.5.0",
"request": "2.88.2", "request": "2.88.2",
"request-promise": "4.2.6", "request-promise": "4.2.6",
"safe-regex": "2.1.1", "safe-regex": "2.1.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"scim-query-filter-parser": "2.0.4", "scim-query-filter-parser": "2.0.4",
"semver": "7.3.7", "semver": "7.3.8",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"simple-git": "2.21.0", "simple-git": "3.16.0",
"solr-node": "1.2.1", "solr-node": "1.2.1",
"sqlite3": "5.0.6", "sqlite3": "5.1.4",
"ssh2": "1.5.0", "ssh2": "1.11.0",
"ssh2-promise": "1.0.2", "ssh2-promise": "1.0.3",
"striptags": "3.2.0", "striptags": "3.2.0",
"subscriptions-transport-ws": "0.9.18", "subscriptions-transport-ws": "0.9.18",
"tar-fs": "2.1.1", "tar-fs": "2.1.1",
"turndown": "7.1.1", "turndown": "7.1.1",
"twemoji": "13.1.0", "twemoji": "14.0.2",
"uslug": "1.0.4", "uslug": "1.0.4",
"uuid": "8.3.2", "uuid": "9.0.0",
"validate.js": "0.13.1", "validate.js": "0.13.1",
"winston": "3.3.3", "winston": "3.8.2",
"xss": "1.0.11", "xss": "1.0.14",
"yargs": "16.1.0" "yargs": "17.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.12.1", "@babel/cli": "^7.12.1",
@ -212,7 +218,7 @@
"apollo-link-batch-http": "1.2.14", "apollo-link-batch-http": "1.2.14",
"apollo-link-error": "1.1.13", "apollo-link-error": "1.1.13",
"apollo-link-http": "1.5.17", "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-link-ws": "1.0.20",
"apollo-utilities": "1.3.4", "apollo-utilities": "1.3.4",
"autoprefixer": "9.8.6", "autoprefixer": "9.8.6",
@ -225,11 +231,12 @@
"babel-plugin-transform-imports": "2.0.0", "babel-plugin-transform-imports": "2.0.0",
"cache-loader": "4.1.0", "cache-loader": "4.1.0",
"canvas-confetti": "1.3.1", "canvas-confetti": "1.3.1",
"cash-dom": "8.1.1", "cash-dom": "8.1.3",
"chart.js": "2.9.4", "chart.js": "2.9.4",
"clean-webpack-plugin": "3.0.0", "clean-webpack-plugin": "3.0.0",
"clipboard": "2.0.10", "clipboard": "2.0.11",
"codemirror": "5.58.2", "codemirror": "5.58.2",
"codemirror-asciidoc": "1.0.4",
"copy-webpack-plugin": "6.2.1", "copy-webpack-plugin": "6.2.1",
"core-js": "3.6.5", "core-js": "3.6.5",
"css-loader": "4.3.0", "css-loader": "4.3.0",
@ -249,7 +256,7 @@
"eslint-plugin-vue": "7.1.0", "eslint-plugin-vue": "7.1.0",
"file-loader": "6.1.1", "file-loader": "6.1.1",
"filepond": "4.21.1", "filepond": "4.21.1",
"filepond-plugin-file-validate-type": "1.2.7", "filepond-plugin-file-validate-type": "1.2.8",
"filesize.js": "2.0.0", "filesize.js": "2.0.0",
"graphql-persisted-document-loader": "2.0.0", "graphql-persisted-document-loader": "2.0.0",
"graphql-tag": "2.11.0", "graphql-tag": "2.11.0",
@ -276,7 +283,7 @@
"postcss-import": "12.0.1", "postcss-import": "12.0.1",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0", "postcss-preset-env": "6.7.0",
"postcss-selector-parser": "6.0.10", "postcss-selector-parser": "6.0.11",
"prismjs": "1.22.0", "prismjs": "1.22.0",
"pug-lint": "2.6.0", "pug-lint": "2.6.0",
"pug-loader": "2.4.0", "pug-loader": "2.4.0",
@ -317,7 +324,7 @@
"webpack-bundle-analyzer": "3.9.0", "webpack-bundle-analyzer": "3.9.0",
"webpack-cli": "3.3.12", "webpack-cli": "3.3.12",
"webpack-dev-middleware": "3.7.2", "webpack-dev-middleware": "3.7.2",
"webpack-hot-middleware": "2.25.1", "webpack-hot-middleware": "2.25.3",
"webpack-merge": "5.2.0", "webpack-merge": "5.2.0",
"webpack-modernizr-loader": "5.0.0", "webpack-modernizr-loader": "5.0.0",
"webpack-subresource-integrity": "1.5.1", "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 title: Wiki.js
company: '' company: ''
contentLicense: '' contentLicense: ''
footerOverride: ''
logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
pageExtensions:
- md
- html
- txt
mail: mail:
host: '' host: ''
secure: true secure: true
@ -58,6 +63,7 @@ defaults:
tocDepth: tocDepth:
min: 1 min: 1
max: 2 max: 2
tocPosition: 'left'
auth: auth:
autoLogin: false autoLogin: false
enforce2FA: false enforce2FA: false
@ -66,6 +72,14 @@ defaults:
audience: 'urn:wiki.js' audience: 'urn:wiki.js'
tokenExpiration: '30m' tokenExpiration: '30m'
tokenRenewal: '14d' 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: features:
featurePageRatings: true featurePageRatings: true
featurePageComments: true featurePageComments: true
@ -74,7 +88,7 @@ defaults:
securityOpenRedirect: true securityOpenRedirect: true
securityIframe: true securityIframe: true
securityReferrerPolicy: true securityReferrerPolicy: true
securityTrustProxy: true securityTrustProxy: false
securitySRI: true securitySRI: true
securityHSTS: false securityHSTS: false
securityHSTSDuration: 300 securityHSTSDuration: 300
@ -155,8 +169,4 @@ reservedPaths:
- img - img
- js - js
- svg - svg
pageExtensions:
- md
- html
- txt
# --------------------------------- # ---------------------------------

@ -4,6 +4,7 @@ const pageHelper = require('../helpers/page')
const _ = require('lodash') const _ = require('lodash')
const CleanCSS = require('clean-css') const CleanCSS = require('clean-css')
const moment = require('moment') const moment = require('moment')
const qs = require('querystring')
/* global WIKI */ /* global WIKI */
@ -419,13 +420,14 @@ router.get('/_userav/:uid', async (req, res, next) => {
* View document / asset * View document / asset
*/ */
router.get('/*', async (req, res, next) => { 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 pageArgs = pageHelper.parsePath(req.path, { stripExt })
const isPage = (stripExt || pageArgs.path.indexOf('.') === -1) const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
if (isPage) { if (isPage) {
if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { 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) req.i18n.changeLanguage(pageArgs.locale)
@ -511,10 +513,11 @@ router.get('/*', async (req, res, next) => {
} }
// -> Set TOC display options // -> Set TOC display options
const tocOptions = _.get(page, 'tocOptions.useDefault', true) ? { const tocOptions = _.get(page, 'tocOptions.useDefault', true) ?
min: page.tocOptions.min, WIKI.config.theming.tocDepth : {
max: page.tocOptions.max min: page.tocOptions.min,
} : WIKI.config.theming.tocDepth max: page.tocOptions.max
}
if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) { if (req.query.legacy || req.get('user-agent').indexOf('Trident') >= 0) {
// -> Convert page TOC // -> 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 // -> Render view
res.render('page', { res.render('page', {
page, page,
@ -560,7 +567,8 @@ router.get('/*', async (req, res, next) => {
tocOptions, tocOptions,
injectCode, injectCode,
comments: commentTmpl, comments: commentTmpl,
effectivePermissions effectivePermissions,
pageFilename
}) })
} }
} else if (pageArgs.path === 'home') { } else if (pageArgs.path === 'home') {

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

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

@ -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.payload = certResp
WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain WIKI.config.letsencrypt.domain = WIKI.config.ssl.domain
await WIKI.configSvc.saveToDb(['letsencrypt']) await WIKI.configSvc.saveToDb(['letsencrypt'])

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

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

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

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

@ -159,7 +159,33 @@ module.exports = {
return { return {
...page, ...page,
locale: page.localeCode, 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 { } else {
throw new WIKI.Error.PageViewForbidden() throw new WIKI.Error.PageViewForbidden()

@ -17,8 +17,11 @@ module.exports = {
title: WIKI.config.title, title: WIKI.config.title,
company: WIKI.config.company, company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense, contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl, logoUrl: WIKI.config.logoUrl,
pageExtensions: WIKI.config.pageExtensions.join(', '),
...WIKI.config.seo, ...WIKI.config.seo,
...WIKI.config.editShortcuts,
...WIKI.config.features, ...WIKI.config.features,
...WIKI.config.security, ...WIKI.config.security,
authAutoLogin: WIKI.config.auth.autoLogin, authAutoLogin: WIKI.config.auth.autoLogin,
@ -58,10 +61,18 @@ module.exports = {
WIKI.config.contentLicense = args.contentLicense WIKI.config.contentLicense = args.contentLicense
} }
if (args.hasOwnProperty('footerOverride')) {
WIKI.config.footerOverride = args.footerOverride
}
if (args.hasOwnProperty('logoUrl')) { if (args.hasOwnProperty('logoUrl')) {
WIKI.config.logoUrl = _.trim(args.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 = { WIKI.config.seo = {
description: _.get(args, 'description', WIKI.config.seo.description), description: _.get(args, 'description', WIKI.config.seo.description),
robots: _.get(args, 'robots', WIKI.config.seo.robots), robots: _.get(args, 'robots', WIKI.config.seo.robots),
@ -79,6 +90,16 @@ module.exports = {
tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal) 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 = { WIKI.config.features = {
featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings), featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),
featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments), featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),
@ -104,7 +125,7 @@ module.exports = {
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload) 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) { if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy') WIKI.app.enable('trust proxy')

@ -90,7 +90,10 @@ module.exports = {
if (process.env.UPGRADE_COMPANION) { if (process.env.UPGRADE_COMPANION) {
await request({ await request({
method: 'POST', 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 { return {
responseResult: graphHelper.generateSuccess('Upgrade has started.') responseResult: graphHelper.generateSuccess('Upgrade has started.')

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

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

@ -46,6 +46,11 @@ type PageQuery {
id: Int! id: Int!
): Page @auth(requires: ["read:pages", "manage:system"]) ): 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"]) tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
searchTags( searchTags(

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

@ -29,6 +29,7 @@ type ThemingMutation {
iconset: String! iconset: String!
darkMode: Boolean! darkMode: Boolean!
tocDepth: RangeInput! tocDepth: RangeInput!
tocPosition: String
injectCSS: String injectCSS: String
injectHead: String injectHead: String
injectBody: String injectBody: String
@ -40,9 +41,10 @@ type ThemingMutation {
# ----------------------------------------------- # -----------------------------------------------
type ThemingConfig { type ThemingConfig {
theme: String theme: String!
iconset: String iconset: String!
darkMode: Boolean darkMode: Boolean!
tocPosition: String
tocDepth: Range tocDepth: Range
injectCSS: String injectCSS: String
injectHead: String injectHead: String

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

@ -6,6 +6,11 @@
const path = require('path') const path = require('path')
const { nanoid } = require('nanoid') const { nanoid } = require('nanoid')
const { DateTime } = require('luxon') const { DateTime } = require('luxon')
const { gte } = require('semver')
// ----------------------------------------
// Init WIKI instance
// ----------------------------------------
let WIKI = { let WIKI = {
IS_DEBUG: process.env.NODE_ENV === 'development', IS_DEBUG: process.env.NODE_ENV === 'development',
@ -38,6 +43,9 @@ WIKI.kernel.init()
// Register exit handler // Register exit handler
// ---------------------------------------- // ----------------------------------------
process.on('SIGTERM', () => {
WIKI.kernel.shutdown()
})
process.on('SIGINT', () => { process.on('SIGINT', () => {
WIKI.kernel.shutdown() WIKI.kernel.shutdown()
}) })

@ -149,10 +149,12 @@ module.exports = async () => {
title: WIKI.config.title, title: WIKI.config.title,
theme: WIKI.config.theming.theme, theme: WIKI.config.theming.theme,
darkMode: WIKI.config.theming.darkMode, darkMode: WIKI.config.theming.darkMode,
tocPosition: WIKI.config.theming.tocPosition || 'left',
lang: WIKI.config.lang.code, lang: WIKI.config.lang.code,
rtl: WIKI.config.lang.rtl, rtl: WIKI.config.lang.rtl,
company: WIKI.config.company, company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense, contentLicense: WIKI.config.contentLicense,
footerOverride: WIKI.config.footerOverride,
logoUrl: WIKI.config.logoUrl logoUrl: WIKI.config.logoUrl
} }
res.locals.langs = await WIKI.models.locales.getNavLocales({ cache: true }) 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) { for (const strategy of dbStrategies) {
let newProps = false
const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey]) const strategyDef = _.find(WIKI.data.authentication, ['key', strategy.strategyKey])
if (!strategyDef) { if (!strategyDef) {
await WIKI.models.authentication.query().delete().where('key', strategy.key) 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) => { strategy.config = _.transform(strategyDef.props, (result, value, key) => {
if (!_.has(result, key)) { if (!_.has(result, key)) {
_.set(result, key, value.default) _.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 return result
}, strategy.config) }, strategy.config)
@ -111,6 +114,12 @@ module.exports = class Authentication extends Model {
displayName: strategyDef.title displayName: strategyDef.title
}).where('key', strategy.key) }).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 ]`) WIKI.logger.info(`Loaded ${WIKI.data.authentication.length} authentication strategies: [ OK ]`)

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

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

@ -147,6 +147,7 @@ module.exports = class Page extends Model {
isPublished: 'boolean', isPublished: 'boolean',
publishEndDate: 'string', publishEndDate: 'string',
publishStartDate: 'string', publishStartDate: 'string',
contentType: 'string',
render: 'string', render: 'string',
tags: [ 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' : '' }) const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
// -> Move page // -> 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({ await WIKI.models.pages.query().patch({
path: opts.destinationPath, path: opts.destinationPath,
localeCode: opts.destinationLocale, localeCode: opts.destinationLocale,
@ -758,6 +759,7 @@ module.exports = class Page extends Model {
...page, ...page,
destinationPath: opts.destinationPath, destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale, destinationLocaleCode: opts.destinationLocale,
title: destinationTitle,
destinationHash destinationHash
}) })
@ -972,9 +974,8 @@ module.exports = class Page extends Model {
// -> Save render to cache // -> Save render to cache
await WIKI.models.pages.savePageToCache(page) await WIKI.models.pages.savePageToCache(page)
} else { } else {
// -> No render? Possible duplicate issue // -> No render? Last page render failed...
/* TODO: Detect duplicate and delete */ throw new Error('Page has no rendered version. Looks like the Last page render failed. Try to edit the page and save it again.')
throw new Error('Error while fetching page. Duplicate entry detected. Reload the page to try again.')
} }
} }
} }
@ -1082,6 +1083,7 @@ module.exports = class Page extends Model {
isPublished: page.isPublished === 1 || page.isPublished === true, isPublished: page.isPublished === 1 || page.isPublished === true,
publishEndDate: page.publishEndDate, publishEndDate: page.publishEndDate,
publishStartDate: page.publishStartDate, publishStartDate: page.publishStartDate,
contentType: page.contentType,
render: page.render, render: page.render,
tags: page.tags.map(t => _.pick(t, ['tag', 'title'])), tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
title: page.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 usr = await WIKI.models.users.query().findById(context.req.user.id).select('providerKey')
const provider = _.find(WIKI.auth.strategies, ['key', usr.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 () { static async getGuestUser () {

@ -9,5 +9,5 @@ props:
propertyTrackingId: propertyTrackingId:
type: String type: String
title: Property Tracking ID title: Property Tracking ID
hint: UA-XXXXXXX-X hint: G-XXXXXXXXXX
order: 1 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 */ /* global WIKI */
// ------------------------------------ // ------------------------------------
@ -10,15 +11,24 @@ module.exports = {
init (passport, conf) { init (passport, conf) {
passport.use(conf.key, passport.use(conf.key,
new CASStrategy({ new CASStrategy({
ssoBaseURL: conf.ssoBaseURL, version: conf.casVersion,
serverBaseURL: conf.serverBaseURL, ssoBaseURL: conf.casUrl,
serverBaseURL: conf.baseUrl,
serviceURL: conf.callbackURL,
passReqToCallback: true passReqToCallback: true
}, async (req, profile, cb) => { }, async (req, profile, cb) => {
try { try {
const user = await WIKI.models.users.processProfile({ const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy, 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) cb(null, user)
} catch (err) { } catch (err) {
cb(err, null) cb(err, null)

@ -6,6 +6,37 @@ logo: https://static.requarks.io/logo/cas.svg
color: green darken-2 color: green darken-2
website: https://apereo.github.io/cas/ website: https://apereo.github.io/cas/
useForm: false useForm: false
isAvailable: true
props: props:
ssoBaseURL: String baseUrl:
serverBaseURL: String 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, clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL, callbackURL: conf.callbackURL,
baseURL: conf.baseUrl, baseURL: conf.baseUrl,
authorizationURL: conf.authorizationURL || (conf.baseUrl + '/oauth/authorize'),
tokenURL: conf.tokenURL || (conf.baseUrl + '/oauth/token'),
scope: ['read_user'], scope: ['read_user'],
passReqToCallback: true passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => { }, 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). 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 default: https://gitlab.com
order: 3 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, clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL, callbackURL: conf.callbackURL,
passReqToCallback: true passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, cb) => { }, async (req, accessToken, refreshToken, results, profile, cb) => {
let displayName = profile.username let displayName = profile.username
if (_.isString(profile.fullName) && profile.fullName.length > 0) { if (_.isString(profile.fullName) && profile.fullName.length > 0) {
displayName = profile.fullName displayName = profile.fullName
@ -36,6 +36,7 @@ module.exports = {
picture: '' picture: ''
} }
}) })
req.session.keycloak_id_token = results.id_token
cb(null, user) cb(null, user)
} catch (err) { } catch (err) {
cb(err, null) cb(err, null)
@ -43,11 +44,22 @@ module.exports = {
}) })
) )
}, },
logout (conf) { logout (conf, context) {
if (!conf.logoutUpstream) { if (!conf.logoutUpstream) {
return '/' return '/'
} else if (conf.logoutURL && conf.logoutURL.length > 5) { } 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 { } else {
WIKI.logger.warn('Keycloak logout URL is not configured!') WIKI.logger.warn('Keycloak logout URL is not configured!')
return '/' return '/'

@ -7,6 +7,10 @@ color: blue-grey darken-2
website: https://www.keycloak.org/ website: https://www.keycloak.org/
useForm: false useForm: false
isAvailable: true isAvailable: true
scopes:
- openid
- profile
- email
props: props:
host: host:
type: String type: String
@ -53,4 +57,9 @@ props:
title: Logout Endpoint URL title: Logout Endpoint URL
hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/logout hint: e.g. https://KEYCLOAK-HOST/auth/realms/YOUR-REALM/protocol/openid-connect/logout
order: 9 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, searchBase: conf.searchBase,
searchFilter: conf.searchFilter, searchFilter: conf.searchFilter,
tlsOptions: getTlsOptions(conf), tlsOptions: getTlsOptions(conf),
...conf.mapGroups && {
groupSearchBase: conf.groupSearchBase,
groupSearchFilter: conf.groupSearchFilter,
groupSearchScope: conf.groupSearchScope,
groupDnProperty: conf.groupDnProperty,
groupSearchAttributes: [conf.groupNameField]
},
includeRaw: true includeRaw: true
}, },
usernameField: 'email', usernameField: 'email',
@ -40,6 +47,21 @@ module.exports = {
picture: _.get(profile, `_raw.${conf.mappingPicture}`, '') 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) cb(null, user)
} catch (err) { } catch (err) {
if (WIKI.config.flags.ldapdebug) { if (WIKI.config.flags.ldapdebug) {
@ -59,7 +81,7 @@ function getTlsOptions(conf) {
if (!conf.tlsCertPath) { if (!conf.tlsCertPath) {
return { return {
rejectUnauthorized: conf.verifyTLSCertificate, rejectUnauthorized: conf.verifyTLSCertificate
} }
} }

@ -83,3 +83,39 @@ props:
hint: The field storing the user avatar picture. Usually "jpegPhoto" or "thumbnailPhoto". hint: The field storing the user avatar picture. Usually "jpegPhoto" or "thumbnailPhoto".
maxWidth: 500 maxWidth: 500
order: 23 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 */ /* global WIKI */
// ------------------------------------ // ------------------------------------
@ -28,6 +30,9 @@ module.exports = {
done(null, user) done(null, user)
} }
} else { } 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) done(new WIKI.Error.AuthLoginFailed(), null)
} }
} catch (err) { } catch (err) {

@ -18,7 +18,8 @@ module.exports = {
userInfoURL: conf.userInfoURL, userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL, callbackURL: conf.callbackURL,
passReqToCallback: true, passReqToCallback: true,
scope: conf.scope scope: conf.scope,
state: conf.enableCSRFProtection
}, async (req, accessToken, refreshToken, profile, cb) => { }, async (req, accessToken, refreshToken, profile, cb) => {
try { try {
const user = await WIKI.models.users.processProfile({ const user = await WIKI.models.users.processProfile({
@ -30,6 +31,19 @@ module.exports = {
email: _.get(profile, conf.emailClaim) 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) cb(null, user)
} catch (err) { } catch (err) {
cb(err, null) cb(err, null)

@ -54,19 +54,38 @@ props:
default: email default: email
maxWidth: 500 maxWidth: 500
order: 8 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: logoutURL:
type: String type: String
title: Logout URL title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 9 order: 11
scope: scope:
type: String type: String
title: Scope title: Scope
hint: (optional) Application Client permission scopes. hint: (optional) Application Client permission scopes.
order: 10 order: 12
useQueryStringForAccessToken: useQueryStringForAccessToken:
type: Boolean type: Boolean
default: false default: false
title: Pass access token via GET query string to User Info Endpoint 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. 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, issuer: conf.issuer,
userInfoURL: conf.userInfoURL, userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL, callbackURL: conf.callbackURL,
passReqToCallback: true passReqToCallback: true,
}, async (req, iss, sub, profile, cb) => { skipUserProfile: conf.skipUserProfile,
acrValues: conf.acrValues
}, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => {
const profile = Object.assign({}, idProfile, uiProfile)
try { try {
const user = await WIKI.models.users.processProfile({ const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy, providerKey: req.params.strategy,
profile: { profile: {
...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) cb(null, user)
} catch (err) { } catch (err) {
cb(err, null) cb(err, null)

@ -37,20 +37,51 @@ props:
title: User Info Endpoint URL title: User Info Endpoint URL
hint: User Info Endpoint URL hint: User Info Endpoint URL
order: 5 order: 5
skipUserProfile:
type: Boolean
default: false
title: Skip User Profile
hint: Skips call to the OIDC UserInfo endpoint
order: 6
issuer: issuer:
type: String type: String
title: Issuer title: Issuer
hint: Issuer URL hint: Issuer URL
order: 6 order: 7
emailClaim: emailClaim:
type: String type: String
title: Email Claim title: Email Claim
hint: Field containing the email address hint: Field containing the email address
default: email default: email
maxWidth: 500 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: logoutURL:
type: String type: String
title: Logout URL title: Logout URL
hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process.
order: 8 order: 12
acrValues:
type: String
title: ACR Values
hint: (optional) Authentication Context Class Reference
order: 13

@ -14,14 +14,14 @@ module.exports = {
callbackUrl: conf.callbackURL, callbackUrl: conf.callbackURL,
entryPoint: conf.entryPoint, entryPoint: conf.entryPoint,
issuer: conf.issuer, issuer: conf.issuer,
cert: _.split(conf.cert || '', '|'), cert: (conf.cert || '').split('|'),
signatureAlgorithm: conf.signatureAlgorithm, signatureAlgorithm: conf.signatureAlgorithm,
digestAlgorithm: conf.digestAlgorithm, digestAlgorithm: conf.digestAlgorithm,
identifierFormat: conf.identifierFormat, identifierFormat: conf.identifierFormat,
wantAssertionsSigned: conf.wantAssertionsSigned, wantAssertionsSigned: conf.wantAssertionsSigned,
acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs), acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),
disableRequestedAuthnContext: conf.disableRequestedAuthnContext, disableRequestedAuthnContext: conf.disableRequestedAuthnContext,
authnContext: _.split(conf.authnContext, '|'), authnContext: (conf.authnContext || '').split('|'),
racComparison: conf.racComparison, racComparison: conf.racComparison,
forceAuthn: conf.forceAuthn, forceAuthn: conf.forceAuthn,
passive: conf.passive, passive: conf.passive,
@ -56,6 +56,26 @@ module.exports = {
picture: _.get(profile, conf.mappingPicture, '') picture: _.get(profile, conf.mappingPicture, '')
} }
}) })
// map users provider groups to wiki groups with the same name, and remove any groups that don't match
// Code copied from the LDAP implementation with a slight variation on the field we extract the value from
// In SAML v2 groups come in profile.attributes and can be 1 string or an array of strings
if (conf.mapGroups) {
const maybeArrayOfGroups = _.get(profile.attributes, conf.mappingGroups)
const groups = (maybeArrayOfGroups && !_.isArray(maybeArrayOfGroups)) ? [maybeArrayOfGroups] : maybeArrayOfGroups
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) cb(null, user)
} catch (err) { } catch (err) {
cb(err, null) cb(err, null)

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

Loading…
Cancel
Save