Merge branch 'dev' into master

pull/283/head
Nicolas Giard 7 years ago committed by GitHub
commit 05c32fb1bd

@ -0,0 +1,20 @@
{
"comments": false,
"presets": [
["env", {
"targets": {
"browsers": [
"last 6 Chrome major versions",
"last 6 Firefox major versions",
"last 4 Safari major versions",
"last 4 Edge major versions",
"last 3 iOS major versions",
"last 3 Android major versions",
"last 2 ChromeAndroid major versions",
"Explorer 11"
]
}
}],
"stage-2"
]
}

@ -1,27 +1,14 @@
{
"extends": "standard",
"extends": "requarks",
"env": {
"node": true,
"es6": true,
"jest": true
},
"globals": {
// Client
"document": false,
"navigator": false,
"window": false,
"siteLang": false,
"socket": true,
"wikijs": true,
"FuseBox": false,
// Server
"appconfig": true,
"appdata": true,
"ROOTPATH": true,
"SERVERPATH": true,
"IS_DEBUG": true
},
"rules": {
"space-before-function-paren": 0
"FuseBox": false
}
}

@ -1 +1,2 @@
save-prefix = "~"
save-exact = true
save-prefix = ""

@ -1 +1,2 @@
save-prefix "~"
save-exact true
save-prefix ""

@ -237,11 +237,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- Updated dependencies + snyk policy
[v1.0.11]: https://github.com/Requarks/wiki/releases/tag/v1.0.11
[v1.0.10]: https://github.com/Requarks/wiki/releases/tag/v1.0.10
[v1.0.9]: https://github.com/Requarks/wiki/releases/tag/v1.0.9
[v1.0.8]: https://github.com/Requarks/wiki/releases/tag/v1.0.8
[v1.0.7]: https://github.com/Requarks/wiki/releases/tag/v1.0.7
[v1.0.6]: https://github.com/Requarks/wiki/releases/tag/v1.0.6
[v1.0.5]: https://github.com/Requarks/wiki/releases/tag/v1.0.5
[v1.0.4]: https://github.com/Requarks/wiki/releases/tag/v1.0.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

@ -0,0 +1,8 @@
<svg width="100%" height="100%" viewBox="0 0 159 158" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<path fill="#51b5d7" d="M87.477,3.864c-4.583,-4.583 -12.024,-4.583 -16.608,0l-66.432,66.432c-4.583,4.583 -4.583,12.025 0,16.608l67.352,67.352c4.583,4.583 12.025,4.583 16.608,0l66.432,-66.432c4.583,-4.583 4.583,-12.025 0,-16.608l-67.352,-67.352Zm-14.477,44.282c-3.497,-2.176 -5.826,-6.054 -5.826,-10.472c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,4.418 -2.329,8.296 -5.826,10.472l0,61.157c0.415,0.214 0.818,0.447 1.208,0.699l24.89,-24.89c-0.909,-1.718 -1.424,-3.676 -1.424,-5.753c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,6.803 -5.523,12.326 -12.326,12.326c-1.493,0 -2.924,-0.266 -4.249,-0.753l-25.952,25.951c0.606,1.58 0.938,3.295 0.938,5.088c0,7.857 -6.38,14.236 -14.237,14.236c-7.857,0 -14.237,-6.379 -14.237,-14.236c0,-1.804 0.337,-3.531 0.95,-5.119l-26.466,-26.466c-1.182,0.377 -2.441,0.581 -3.747,0.581c-6.803,0 -12.326,-5.523 -12.326,-12.326c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,2.254 -0.606,4.368 -1.665,6.187l25.157,25.157c0.382,-0.245 0.776,-0.473 1.182,-0.682l0,-61.157Z" />
<path fill="#97cbe1" d="M91.745,39.094l28.297,28.297c-4.527,1.116 -8.072,4.737 -9.079,9.306l-24.963,-24.963l0,-3.588c3.116,-1.939 5.306,-5.231 5.745,-9.052Zm-24.49,0c0.439,3.821 2.629,7.113 5.745,9.052l0,3.588l-24.846,24.846c-0.785,-4.655 -4.183,-8.427 -8.632,-9.753l27.733,-27.733Z" />
<circle fill="#97cbe1" cx="35.84" cy="78.701" r="6.701" />
<circle fill="#97cbe1" cx="123.16" cy="79.778" r="6.701" />
<circle fill="#97cbe1" cx="79.299" cy="37.243" r="6.701" />
<circle fill="#97cbe1" cx="79.633" cy="122.201" r="8.201" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 500 500" preserveAspectRatio="xMinYMin meet">
<path fill="#3B5998" d="M288.714,500l0,-228.073l76.554,0l11.461,-88.885l-88.017,0l0,-56.749c0,-25.735 7.145,-43.271 44.049,-43.271l47.067,-0.022l0,-79.498c-8.141,-1.081 -36.082,-3.502 -68.584,-3.502c-67.862,0 -114.321,41.422 -114.321,117.492l0,65.55l-76.751,0l0,88.885l76.751,0l0,228.071l91.791,0l0,0.002Z" style="fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@ -0,0 +1 @@
<svg width="2500" height="2432" viewBox="0 0 256 249" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><g fill="#161614"><path d="M127.505 0C57.095 0 0 57.085 0 127.505c0 56.336 36.534 104.13 87.196 120.99 6.372 1.18 8.712-2.766 8.712-6.134 0-3.04-.119-13.085-.173-23.739-35.473 7.713-42.958-15.044-42.958-15.044-5.8-14.738-14.157-18.656-14.157-18.656-11.568-7.914.872-7.752.872-7.752 12.804.9 19.546 13.14 19.546 13.14 11.372 19.493 29.828 13.857 37.104 10.6 1.144-8.242 4.449-13.866 8.095-17.05-28.32-3.225-58.092-14.158-58.092-63.014 0-13.92 4.981-25.295 13.138-34.224-1.324-3.212-5.688-16.18 1.235-33.743 0 0 10.707-3.427 35.073 13.07 10.17-2.826 21.078-4.242 31.914-4.29 10.836.048 21.752 1.464 31.942 4.29 24.337-16.497 35.029-13.07 35.029-13.07 6.94 17.563 2.574 30.531 1.25 33.743 8.175 8.929 13.122 20.303 13.122 34.224 0 48.972-29.828 59.756-58.22 62.912 4.573 3.957 8.648 11.717 8.648 23.612 0 17.06-.148 30.791-.148 34.991 0 3.393 2.295 7.369 8.759 6.117 50.634-16.879 87.122-64.656 87.122-120.973C255.009 57.085 197.922 0 127.505 0"/><path d="M47.755 181.634c-.28.633-1.278.823-2.185.389-.925-.416-1.445-1.28-1.145-1.916.275-.652 1.273-.834 2.196-.396.927.415 1.455 1.287 1.134 1.923M54.027 187.23c-.608.564-1.797.302-2.604-.589-.834-.889-.99-2.077-.373-2.65.627-.563 1.78-.3 2.616.59.834.899.996 2.08.36 2.65M58.33 194.39c-.782.543-2.06.034-2.849-1.1-.781-1.133-.781-2.493.017-3.038.792-.545 2.05-.055 2.85 1.07.78 1.153.78 2.513-.019 3.069M65.606 202.683c-.699.77-2.187.564-3.277-.488-1.114-1.028-1.425-2.487-.724-3.258.707-.772 2.204-.555 3.302.488 1.107 1.026 1.445 2.496.7 3.258M75.01 205.483c-.307.998-1.741 1.452-3.185 1.028-1.442-.437-2.386-1.607-2.095-2.616.3-1.005 1.74-1.478 3.195-1.024 1.44.435 2.386 1.596 2.086 2.612M85.714 206.67c.036 1.052-1.189 1.924-2.705 1.943-1.525.033-2.758-.818-2.774-1.852 0-1.062 1.197-1.926 2.721-1.951 1.516-.03 2.758.815 2.758 1.86M96.228 206.267c.182 1.026-.872 2.08-2.377 2.36-1.48.27-2.85-.363-3.039-1.38-.184-1.052.89-2.105 2.367-2.378 1.508-.262 2.857.355 3.049 1.398"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -0,0 +1 @@
<svg width="2443" height="2500" viewBox="0 0 256 262" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"/><path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"/><path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"/><path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"/></svg>

After

Width:  |  Height:  |  Size: 1018 B

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
<path fill="#458BC4" d="M44.804,30.404l-20-27C24.615,3.15,24.308,3.023,24,3.023V46c0.2,0,0.401-0.061,0.573-0.181l20-14
c0.222-0.155,0.37-0.393,0.414-0.659C45.03,30.895,44.964,30.622,44.804,30.404z"/>
<path fill="#43A6DD" d="M23.196,3.405l-20,27c-0.16,0.218-0.227,0.49-0.184,0.756c0.044,0.267,0.192,0.504,0.414,0.659l20,14
C23.599,45.939,23.8,46,24,46V3.023C23.692,3.023,23.385,3.15,23.196,3.405z"/>
</svg>

After

Width:  |  Height:  |  Size: 872 B

@ -0,0 +1,8 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<path d="M32,19c13.089,0,27-3.154,27-9S45.089,1,32,1S5,4.154,5,10S18.911,19,32,19z"/>
<path d="M32,41c13.089,0,27-3.154,27-9V14.436c-1.481,1.493-3.963,2.968-8.022,4.174C45.864,20.128,38.946,21,32,21
s-13.864-0.872-18.978-2.391C8.963,17.403,6.481,15.929,5,14.436V32C5,37.846,18.911,41,32,41z"/>
<path d="M32,63c13.089,0,27-3.154,27-9V36.436c-1.481,1.493-3.963,2.968-8.022,4.174C45.864,42.128,38.946,43,32,43
s-13.864-0.872-18.978-2.391C8.963,39.403,6.481,37.929,5,36.436V54C5,59.846,18.911,63,32,63z"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

@ -0,0 +1 @@
<svg width="2490" height="2500" viewBox="0 0 256 257" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M0 36.357L104.62 22.11l.045 100.914-104.57.595L0 36.358zm104.57 98.293l.08 101.002L.081 221.275l-.006-87.302 104.494.677zm12.682-114.405L255.968 0v121.74l-138.716 1.1V20.246zM256 135.6l-.033 121.191-138.716-19.578-.194-101.84L256 135.6z" fill="#00ADEF"/></svg>

After

Width:  |  Height:  |  Size: 389 B

@ -0,0 +1 @@
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M165.964 15.838c-3.89-11.975-16.752-18.528-28.725-14.636-11.975 3.89-18.528 16.752-14.636 28.725l58.947 181.365c4.048 11.187 16.132 17.473 27.732 14.135 12.1-3.483 19.475-16.334 15.614-28.217L165.964 15.838" fill="#DFA22F"/><path d="M74.626 45.516C70.734 33.542 57.873 26.989 45.9 30.879 33.924 34.77 27.37 47.631 31.263 59.606l58.948 181.366c4.047 11.186 16.132 17.473 27.732 14.132 12.099-3.481 19.474-16.332 15.613-28.217L74.626 45.516" fill="#3CB187"/><path d="M240.162 166.045c11.975-3.89 18.526-16.75 14.636-28.726-3.89-11.973-16.752-18.527-28.725-14.636L44.708 181.632c-11.187 4.046-17.473 16.13-14.135 27.73 3.483 12.099 16.334 19.475 28.217 15.614l181.372-58.93" fill="#CE1E5B"/><path d="M82.508 217.27l43.347-14.084-14.086-43.352-43.35 14.09 14.089 43.347" fill="#392538"/><path d="M173.847 187.591c16.388-5.323 31.62-10.273 43.348-14.084l-14.088-43.36-43.35 14.09 14.09 43.354" fill="#BB242A"/><path d="M210.484 74.706c11.974-3.89 18.527-16.751 14.637-28.727-3.89-11.973-16.752-18.526-28.727-14.636L15.028 90.293C3.842 94.337-2.445 106.422.896 118.022c3.481 12.098 16.332 19.474 28.217 15.613l181.371-58.93" fill="#72C5CD"/><path d="M52.822 125.933c11.805-3.836 27.025-8.782 43.354-14.086-5.323-16.39-10.273-31.622-14.084-43.352l-43.36 14.092 14.09 43.346" fill="#248C73"/><path d="M144.16 96.256l43.356-14.088a546179.21 546179.21 0 0 0-14.089-43.36L130.07 52.9l14.09 43.356" fill="#62803A"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1500 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<path d="M1500,543c0,0 -135.753,54.677 -252.197,101.577c-106.023,42.702 -223.596,47.2 -332.571,12.724c-56.088,-17.745 -118.404,-37.46 -173.999,-55.048c-103.773,-32.831 -216.376,-22.709 -312.631,28.103c0,0 0,0.001 0,0.001c-94.821,50.055 -206.441,57.476 -307.053,20.415c-63.317,-23.322 -121.549,-44.772 -121.549,-44.772l0,417l1500,0l0,-480Z" style="fill:#2d2d2d;fill-opacity:0.282609;"/>
<path d="M1510,580c0,0 -144.155,47.882 -252.311,83.806c-74.651,24.796 -156.199,17.958 -225.679,-18.923c0,0 0,0 0,0c-62.207,-33.021 -133.629,-44.415 -203.023,-32.389c-98.381,17.051 -244.859,42.438 -352.664,61.121c-92.259,15.99 -187.076,8.079 -275.41,-22.977c-93.342,-32.818 -200.913,-70.638 -200.913,-70.638l0,466l1500,0l10,-466Z" style="fill:#2d2d2d;fill-opacity:0.550725;"/>
<path d="M1500,650c0,0 -143.367,28.581 -239.425,47.731c-56.087,11.181 -113.694,12.508 -170.237,3.922c-74.75,-11.351 -183.318,-27.838 -261.719,-39.743c-65.252,-9.909 -131.707,-8.759 -196.577,3.4c-49.655,9.308 -109.704,20.564 -158.992,29.803c-63.125,11.833 -127.839,12.479 -191.188,1.911c-111.875,-18.665 -281.862,-47.024 -281.862,-47.024l0,430l1500,0l0,-430Z" style="fill:#2d2d2d;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1500 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<path d="M1500,543c0,0 -135.753,54.677 -252.197,101.577c-106.023,42.702 -223.596,47.2 -332.571,12.724c-56.088,-17.745 -118.404,-37.46 -173.999,-55.048c-103.773,-32.831 -216.376,-22.709 -312.631,28.103c0,0 0,0.001 0,0.001c-94.821,50.055 -206.441,57.476 -307.053,20.415c-63.317,-23.322 -121.549,-44.772 -121.549,-44.772l0,417l1500,0l0,-480Z" style="fill:#fff;fill-opacity:0.282609;"/>
<path d="M1510,580c0,0 -144.155,47.882 -252.311,83.806c-74.651,24.796 -156.199,17.958 -225.679,-18.923c0,0 0,0 0,0c-62.207,-33.021 -133.629,-44.415 -203.023,-32.389c-98.381,17.051 -244.859,42.438 -352.664,61.121c-92.259,15.99 -187.076,8.079 -275.41,-22.977c-93.342,-32.818 -200.913,-70.638 -200.913,-70.638l0,466l1500,0l10,-466Z" style="fill:#fff;fill-opacity:0.550725;"/>
<path d="M1500,650c0,0 -143.367,28.581 -239.425,47.731c-56.087,11.181 -113.694,12.508 -170.237,3.922c-74.75,-11.351 -183.318,-27.838 -261.719,-39.743c-65.252,-9.909 -131.707,-8.759 -196.577,3.4c-49.655,9.308 -109.704,20.564 -158.992,29.803c-63.125,11.833 -127.839,12.479 -191.188,1.911c-111.875,-18.665 -281.862,-47.024 -281.862,-47.024l0,430l1500,0l0,-430Z" style="fill:#fff;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -1,4 +0,0 @@
'use strict'
require('./scss/configure.scss')
require('./js/configure.js')

@ -1,17 +1,4 @@
'use strict'
let logic = document.documentElement.dataset.logic
switch (logic) {
case 'error':
require('./scss/error.scss')
break
case 'login':
require('./scss/login.scss')
require('./js/login.js')
break
default:
require('./scss/app.scss')
require('./js/app.js')
break
}
require('./scss/app.scss')
require('./js/app.js')

@ -1,28 +1,28 @@
'use strict'
/* global $, siteRoot */
/* global siteConfig */
/* eslint-disable no-new */
import CONSTANTS from './constants'
import Vue from 'vue'
import VueResource from 'vue-resource'
import VueClipboards from 'vue-clipboards'
import VueLodash from 'vue-lodash'
import VeeValidate from 'vee-validate'
import { ApolloClient, createBatchingNetworkInterface } from 'apollo-client'
import store from './store'
import io from 'socket-io-client'
import i18next from 'i18next'
import i18nextXHR from 'i18next-xhr-backend'
import VueI18Next from '@panter/vue-i18next'
import 'jquery-contextmenu'
import 'jquery-simple-upload'
import 'jquery-smooth-scroll'
import 'jquery-sticky'
// ====================================
// Load Modules
// ====================================
import localization from './modules/localization'
// ====================================
// Load Helpers
// ====================================
import helpers from './helpers'
import _ from './helpers/lodash'
// ====================================
// Load Vue Components
@ -36,6 +36,7 @@ import editorFileComponent from './components/editor-file.vue'
import editorVideoComponent from './components/editor-video.vue'
import historyComponent from './components/history.vue'
import loadingSpinnerComponent from './components/loading-spinner.vue'
import loginComponent from './components/login.vue'
import modalCreatePageComponent from './components/modal-create-page.vue'
import modalCreateUserComponent from './components/modal-create-user.vue'
import modalDeletePageComponent from './components/modal-delete-page.vue'
@ -53,19 +54,48 @@ import adminEditUserComponent from './pages/admin-edit-user.component.js'
import adminProfileComponent from './pages/admin-profile.component.js'
import adminSettingsComponent from './pages/admin-settings.component.js'
import adminThemeComponent from './pages/admin-theme.component.js'
import configManagerComponent from './components/config-manager.component.js'
import contentViewComponent from './pages/content-view.component.js'
import editorComponent from './components/editor.component.js'
import sourceViewComponent from './pages/source-view.component.js'
// ====================================
// Initialize Global Vars
// ====================================
window.wiki = null
window.CONSTANTS = CONSTANTS
// ====================================
// Initialize Apollo Client (GraphQL)
// ====================================
window.graphQL = new ApolloClient({
networkInterface: createBatchingNetworkInterface({
uri: window.location.protocol + '//' + window.location.host + siteConfig.path + '/graphql'
}),
connectToDevTools: true
})
// ====================================
// Initialize Vue Modules
// ====================================
Vue.use(VueResource)
Vue.use(VueClipboards)
Vue.use(VueI18Next)
Vue.use(VueLodash, _)
Vue.use(localization.VueI18Next)
Vue.use(helpers)
Vue.use(VeeValidate, {
enableAutoClasses: true,
classNames: {
touched: 'is-touched', // the control has been blurred
untouched: 'is-untouched', // the control hasn't been blurred
valid: 'is-valid', // model is valid
invalid: 'is-invalid', // model is invalid
pristine: 'is-pristine', // control has not been interacted with
dirty: 'is-dirty' // control has been interacted with
}
})
// ====================================
// Register Vue Components
@ -78,6 +108,7 @@ Vue.component('adminSettings', adminSettingsComponent)
Vue.component('adminTheme', adminThemeComponent)
Vue.component('anchor', anchorComponent)
Vue.component('colorPicker', colorPickerComponent)
Vue.component('configManager', configManagerComponent)
Vue.component('contentView', contentViewComponent)
Vue.component('editor', editorComponent)
Vue.component('editorCodeblock', editorCodeblockComponent)
@ -85,6 +116,7 @@ Vue.component('editorFile', editorFileComponent)
Vue.component('editorVideo', editorVideoComponent)
Vue.component('history', historyComponent)
Vue.component('loadingSpinner', loadingSpinnerComponent)
Vue.component('login', loginComponent)
Vue.component('modalCreatePage', modalCreatePageComponent)
Vue.component('modalCreateUser', modalCreateUserComponent)
Vue.component('modalDeletePage', modalDeletePageComponent)
@ -99,52 +131,26 @@ Vue.component('sourceView', sourceViewComponent)
Vue.component('toggle', toggleComponent)
Vue.component('tree', treeComponent)
// ====================================
// Load Localization strings
// ====================================
i18next
.use(i18nextXHR)
.init({
backend: {
loadPath: siteRoot + '/js/i18n/{{lng}}.json'
},
lng: siteLang,
fallbackLng: siteLang
})
$(() => {
document.addEventListener('DOMContentLoaded', ev => {
// ====================================
// Notifications
// ====================================
$(window).bind('beforeunload', () => {
window.addEventListener('beforeunload', () => {
store.dispatch('startLoading')
})
$(document).ajaxSend(() => {
store.dispatch('startLoading')
}).ajaxComplete(() => {
store.dispatch('stopLoading')
})
// ====================================
// Establish WebSocket connection
// ====================================
let socket = io(window.location.origin)
window.socket = socket
// ====================================
// Bootstrap Vue
// ====================================
const i18n = new VueI18Next(i18next)
window.wikijs = new Vue({
const i18n = localization.init()
window.wiki = new Vue({
mixins: [helpers],
components: {},
store,
i18n,
el: '#root',
el: '#app',
methods: {
changeTheme(opts) {
this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}`
@ -153,9 +159,7 @@ $(() => {
}
},
mounted() {
$('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 })
$('#header').sticky({ topSpacing: 0 })
$('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 })
}
})
})

@ -0,0 +1,153 @@
'use strict'
/* global $, siteConfig */
/* eslint-disable no-new */
import Vue from 'vue'
import VueResource from 'vue-resource'
import VueClipboards from 'vue-clipboards'
import VueLodash from 'vue-lodash'
import store from './store'
import i18next from 'i18next'
import i18nextXHR from 'i18next-xhr-backend'
import VueI18Next from '@panter/vue-i18next'
import 'jquery-contextmenu'
import 'jquery-simple-upload'
import 'jquery-smooth-scroll'
import 'jquery-sticky'
// ====================================
// Load Helpers
// ====================================
import helpers from './helpers'
import _ from './helpers/lodash'
// ====================================
// Load Vue Components
// ====================================
import alertComponent from './components/alert.vue'
import anchorComponent from './components/anchor.vue'
import colorPickerComponent from './components/color-picker.vue'
import editorCodeblockComponent from './components/editor-codeblock.vue'
import editorFileComponent from './components/editor-file.vue'
import editorVideoComponent from './components/editor-video.vue'
import historyComponent from './components/history.vue'
import loadingSpinnerComponent from './components/loading-spinner.vue'
import modalCreatePageComponent from './components/modal-create-page.vue'
import modalCreateUserComponent from './components/modal-create-user.vue'
import modalDeleteUserComponent from './components/modal-delete-user.vue'
import modalDiscardPageComponent from './components/modal-discard-page.vue'
import modalMovePageComponent from './components/modal-move-page.vue'
import modalProfile2faComponent from './components/modal-profile-2fa.vue'
import modalUpgradeSystemComponent from './components/modal-upgrade-system.vue'
import pageLoaderComponent from './components/page-loader.vue'
import searchComponent from './components/search.vue'
import toggleComponent from './components/toggle.vue'
import treeComponent from './components/tree.vue'
import adminEditUserComponent from './pages/admin-edit-user.component.js'
import adminProfileComponent from './pages/admin-profile.component.js'
import adminSettingsComponent from './pages/admin-settings.component.js'
import adminThemeComponent from './pages/admin-theme.component.js'
import contentViewComponent from './pages/content-view.component.js'
import editorComponent from './components/editor.component.js'
import sourceViewComponent from './pages/source-view.component.js'
// ====================================
// Initialize Vue Modules
// ====================================
Vue.use(VueResource)
Vue.use(VueClipboards)
Vue.use(VueI18Next)
Vue.use(VueLodash, _)
Vue.use(helpers)
// ====================================
// Register Vue Components
// ====================================
Vue.component('alert', alertComponent)
Vue.component('adminEditUser', adminEditUserComponent)
Vue.component('adminProfile', adminProfileComponent)
Vue.component('adminSettings', adminSettingsComponent)
Vue.component('adminTheme', adminThemeComponent)
Vue.component('anchor', anchorComponent)
Vue.component('colorPicker', colorPickerComponent)
Vue.component('contentView', contentViewComponent)
Vue.component('editor', editorComponent)
Vue.component('editorCodeblock', editorCodeblockComponent)
Vue.component('editorFile', editorFileComponent)
Vue.component('editorVideo', editorVideoComponent)
Vue.component('history', historyComponent)
Vue.component('loadingSpinner', loadingSpinnerComponent)
Vue.component('modalCreatePage', modalCreatePageComponent)
Vue.component('modalCreateUser', modalCreateUserComponent)
Vue.component('modalDeleteUser', modalDeleteUserComponent)
Vue.component('modalDiscardPage', modalDiscardPageComponent)
Vue.component('modalMovePage', modalMovePageComponent)
Vue.component('modalProfile2fa', modalProfile2faComponent)
Vue.component('modalUpgradeSystem', modalUpgradeSystemComponent)
Vue.component('pageLoader', pageLoaderComponent)
Vue.component('search', searchComponent)
Vue.component('sourceView', sourceViewComponent)
Vue.component('toggle', toggleComponent)
Vue.component('tree', treeComponent)
// ====================================
// Load Localization strings
// ====================================
i18next
.use(i18nextXHR)
.init({
backend: {
loadPath: siteConfig.path + '/js/i18n/{{lng}}.json'
},
lng: siteConfig.lang,
fallbackLng: siteConfig.lang
})
$(() => {
// ====================================
// Notifications
// ====================================
$(window).bind('beforeunload', () => {
store.dispatch('startLoading')
})
$(document).ajaxSend(() => {
store.dispatch('startLoading')
}).ajaxComplete(() => {
store.dispatch('stopLoading')
})
// ====================================
// Bootstrap Vue
// ====================================
const i18n = new VueI18Next(i18next)
if (document.querySelector('#root')) {
window.wikijs = new Vue({
mixins: [helpers],
components: {},
store,
i18n,
el: '#root',
methods: {
changeTheme(opts) {
this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}`
this.$refs.header.className = `nav is-${opts.primary}`
this.$refs.footer.className = `footer is-${opts.footer}`
}
},
mounted() {
$('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 })
$('#header').sticky({ topSpacing: 0 })
$('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 })
}
})
}
})

@ -0,0 +1,239 @@
/* global siteConfig */
import axios from 'axios'
export default {
name: 'configManager',
data() {
return {
loading: false,
state: 'welcome',
syscheck: {
ok: false,
error: '',
results: []
},
dbcheck: {
ok: false,
error: ''
},
gitcheck: {
ok: false,
error: ''
},
final: {
ok: false,
error: '',
results: []
},
conf: {
telemetry: true,
upgrade: false,
title: siteConfig.title || 'Wiki',
host: siteConfig.host || 'http://',
port: siteConfig.port || 80,
lang: siteConfig.lang || 'en',
public: (siteConfig.public === true),
pathData: './data',
pathRepo: './repo',
gitUseRemote: (siteConfig.git !== false),
gitUrl: '',
gitBranch: 'master',
gitAuthType: 'ssh',
gitAuthSSHKey: '',
gitAuthUser: '',
gitAuthPass: '',
gitAuthSSL: true,
gitShowUserEmail: true,
gitServerEmail: '',
adminEmail: '',
adminPassword: '',
adminPasswordConfirm: ''
},
considerations: {
https: false,
port: false,
localhost: false
}
}
},
computed: {
currentProgress: function () {
let perc = '0%'
switch (this.state) {
case 'welcome':
perc = '0%'
break
case 'syscheck':
perc = (this.syscheck.ok) ? '15%' : '5%'
break
case 'general':
perc = '25%'
break
case 'considerations':
perc = '30%'
break
case 'git':
perc = '50%'
break
case 'gitcheck':
perc = (this.gitcheck.ok) ? '70%' : '55%'
break
case 'admin':
perc = '75%'
break
}
return perc
}
},
mounted: function () {
/* if (appconfig.paths) {
this.conf.pathData = appconfig.paths.data || './data'
this.conf.pathRepo = appconfig.paths.repo || './repo'
}
if (appconfig.git !== false && _.isPlainObject(appconfig.git)) {
this.conf.gitUrl = appconfig.git.url || ''
this.conf.gitBranch = appconfig.git.branch || 'master'
this.conf.gitShowUserEmail = (appconfig.git.showUserEmail !== false)
this.conf.gitServerEmail = appconfig.git.serverEmail || ''
if (_.isPlainObject(appconfig.git.auth)) {
this.conf.gitAuthType = appconfig.git.auth.type || 'ssh'
this.conf.gitAuthSSHKey = appconfig.git.auth.privateKey || ''
this.conf.gitAuthUser = appconfig.git.auth.username || ''
this.conf.gitAuthPass = appconfig.git.auth.password || ''
this.conf.gitAuthSSL = (appconfig.git.auth.sslVerify !== false)
}
} */
},
methods: {
proceedToWelcome: function (ev) {
this.state = 'welcome'
this.loading = false
},
proceedToSyscheck: function (ev) {
let self = this
this.state = 'syscheck'
this.loading = true
self.syscheck = {
ok: false,
error: '',
results: []
}
this.$helpers._.delay(() => {
axios.post('/syscheck').then(resp => {
if (resp.data.ok === true) {
self.syscheck.ok = true
self.syscheck.results = resp.data.results
} else {
self.syscheck.ok = false
self.syscheck.error = resp.data.error
}
self.loading = false
self.$nextTick()
}).catch(err => {
window.alert(err.message)
})
}, 1000)
},
proceedToGeneral: function (ev) {
let self = this
self.state = 'general'
self.loading = false
self.$nextTick(() => {
self.$validator.validateAll('general')
})
},
proceedToConsiderations: function (ev) {
this.considerations = {
https: !this.$helpers._.startsWith(this.conf.host, 'https'),
port: false, // TODO
localhost: this.$helpers._.includes(this.conf.host, 'localhost')
}
this.state = 'considerations'
this.loading = false
},
proceedToGit: function (ev) {
let self = this
self.state = 'git'
self.loading = false
self.$nextTick(() => {
self.$validator.validateAll('git')
})
},
proceedToGitCheck: function (ev) {
let self = this
this.state = 'gitcheck'
this.loading = true
self.gitcheck = {
ok: false,
results: [],
error: ''
}
this.$helpers._.delay(() => {
axios.post('/gitcheck', self.conf).then(resp => {
if (resp.data.ok === true) {
self.gitcheck.ok = true
self.gitcheck.results = resp.data.results
} else {
self.gitcheck.ok = false
self.gitcheck.error = resp.data.error
}
self.loading = false
self.$nextTick()
}).catch(err => {
window.alert(err.message)
})
}, 1000)
},
proceedToAdmin: function (ev) {
let self = this
self.state = 'admin'
self.loading = false
self.$nextTick(() => {
self.$validator.validateAll('admin')
})
},
proceedToFinal: function (ev) {
let self = this
self.state = 'final'
self.loading = true
self.final = {
ok: false,
error: '',
results: []
}
this.$helpers._.delay(() => {
axios.post('/finalize', self.conf).then(resp => {
if (resp.data.ok === true) {
self.final.ok = true
self.final.results = resp.data.results
} else {
self.final.ok = false
self.final.error = resp.data.error
}
self.loading = false
self.$nextTick()
}).catch(err => {
window.alert(err.message)
})
}, 1000)
},
finish: function (ev) {
let self = this
self.state = 'restart'
this.$helpers._.delay(() => {
axios.post('/restart', {}).then(resp => {
this.$helpers._.delay(() => {
window.location.assign(self.conf.host)
}, 30000)
}).catch(err => {
window.alert(err.message)
})
}, 1000)
}
}
}

@ -34,9 +34,9 @@
<script>
const videoRules = {
'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/, 'i'),
'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i')
'youtube': new RegExp('/(?:(?:youtu\\.be\\/|v\\/|vi\\/|u\\/\\w\\/|embed\\/)|(?:(?:watch)?\\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/', 'i'),
'vimeo': new RegExp('/vimeo.com\\/(?:channels\\/(?:\\w+\\/)?|groups\\/(?:[^/]*)\\/videos\\/|album\\/(?:\\d+)\\/video\\/|)(\\d+)(?:$|\\/|\\?)/', 'i'),
'dailymotion': new RegExp('/(?:dailymotion\\.com(?:\\/embed)?(?:\\/video|\\/hub)|dai\\.ly)\\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/', 'i')
}
export default {

@ -0,0 +1,70 @@
<template lang="pug">
.login(:class='{ "is-error": error }')
.login-container(:class='{ "is-expanded": strategies.length > 1 }')
.login-error(v-if='error')
strong
i.icon-warning-outline
| {{ error.title }}
span {{ error.message }}
.login-providers(v-show='strategies.length > 1')
button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
em(v-html='strategy.icon')
span {{ strategy.title }}
.login-frame
h1 {{ siteTitle }}
h2 {{ $t('auth:loginrequired') }}
form(method='post', action='/login')
input#login-user(type='text', name='email', :placeholder='$t("auth:fields.emailuser")')
input#login-pass(type='password', name='password', :placeholder='$t("auth:fields.password")')
button.button.is-light-blue.is-fullwidth(type='submit')
span {{ $t('auth:actions.login') }}
.login-copyright
span {{ $t('footer.poweredby') }}
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
</template>
<script>
export default {
name: 'login',
data() {
return {
error: false,
strategies: [],
selectedStrategy: 'local'
}
},
computed: {
siteTitle() {
return siteConfig.title
}
},
methods: {
selectStrategy(key, useForm) {
this.selectedStrategy = key
if (!useForm) {
window.location.assign(siteConfig.path + '/login/' + key)
}
},
refreshStrategies() {
graphQL.query({
query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
variables: {
mode: 'active'
}
}).then(resp => {
if (resp.data.authentication) {
this.strategies = resp.data.authentication
} else {
throw new Error('No authentication providers available!')
}
}).catch(err => {
console.error(err)
})
}
},
mounted() {
this.refreshStrategies()
}
}
</script>

@ -5,20 +5,19 @@
span {{ msg }}
</template>
<script>
export default {
name: 'page-loader',
props: ['text'],
data () {
return {}
},
computed: {
msg () { return this.$store.state.pageLoader.msg },
isShown () { return this.$store.state.pageLoader.shown }
},
mounted() {
this.$store.commit('pageLoader/msgChange', this.text)
}
<script type='js'>
export default {
name: 'page-loader',
props: ['text'],
data () {
return {}
},
computed: {
msg () { return this.$store.state.pageLoader.msg },
isShown () { return this.$store.state.pageLoader.shown }
},
mounted() {
this.$store.commit('pageLoader/msgChange', this.text)
}
}
</script>

@ -0,0 +1,22 @@
import gql from 'graphql-tag'
export default {
GQL_QUERY_AUTHENTICATION: gql`
query($mode: String!) {
authentication(mode:$mode) {
key
useForm
title
icon
}
}
`,
GQL_QUERY_TRANSLATIONS: gql`
query($locale: String!, $namespace: String!) {
translations(locale:$locale, namespace:$namespace) {
key
value
}
}
`
}

@ -0,0 +1,5 @@
import GRAPHQL from './graphql'
export default {
GRAPHQL
}

@ -1,6 +1,7 @@
'use strict'
const helpers = {
_: require('./lodash'),
common: require('./common'),
form: require('./form'),
pages: require('./pages')

@ -1,7 +0,0 @@
'use strict'
/* global $ */
$(() => {
$('#login-user').focus()
})

@ -0,0 +1,52 @@
import i18next from 'i18next'
import i18nextXHR from 'i18next-xhr-backend'
import i18nextCache from 'i18next-localstorage-cache'
import VueI18Next from '@panter/vue-i18next'
import loSet from 'lodash/set'
/* global siteConfig, graphQL, CONSTANTS */
module.exports = {
VueI18Next,
init() {
i18next
.use(i18nextXHR)
.use(i18nextCache)
.init({
backend: {
loadPath: '{{lng}}/{{ns}}',
parse: (data) => data,
ajax: (url, opts, cb, data) => {
let langParams = url.split('/')
graphQL.query({
query: CONSTANTS.GRAPHQL.GQL_QUERY_TRANSLATIONS,
variables: {
locale: langParams[0],
namespace: langParams[1]
}
}).then(resp => {
let ns = {}
if (resp.data.translations.length > 0) {
resp.data.translations.forEach(entry => {
loSet(ns, entry.key, entry.value)
})
}
return cb(ns, {status: '200'})
}).catch(err => {
console.error(err)
return cb(null, {status: '404'})
})
}
},
cache: {
enabled: true,
expiration: 60 * 60 * 1000
},
defaultNS: 'common',
lng: siteConfig.lang,
fallbackLng: siteConfig.lang,
ns: ['common', 'admin', 'auth']
})
return new VueI18Next(i18next)
}
}

@ -15,6 +15,7 @@ $primary: 'indigo';
@import 'components/button';
@import 'components/collapsable-nav';
@import 'components/color-picker';
@import 'components/config-manager';
@import 'components/footer';
@import 'components/form';
@import 'components/grid';
@ -43,6 +44,7 @@ $primary: 'indigo';
@import 'layout/_loader';
@import 'layout/_rtl';
@import 'pages/_welcome';
@import 'pages/login';
@import 'pages/welcome';
@import 'base/print';

@ -11,10 +11,15 @@ html {
display: none;
}
#root {
#app {
padding-bottom: 67px;
position: relative;
min-height: 100%;
&.is-fullscreen {
width: 100vw;
height: 100vh;
}
}
body {

@ -4,7 +4,7 @@
border: 1px solid mc('orange','700');
border-radius: 3px;
display: inline-flex;
height: 30px;
height: 40px;
align-items: center;
padding: 0 15px;
font-size: 13px;
@ -61,7 +61,11 @@
background-color: mc($color,'800');
color: #FFF;
animation: none;
}
}
&:focus {
box-shadow: inset 0 0 0 3px rgba(255,255,255, .4);
}
}
}
@ -74,7 +78,13 @@
&.is-featured {
animation: btnInvertedPulse .6s ease alternate infinite;
}
}
&.is-fullwidth {
width: 100%;
text-align: center;
justify-content: center;
}
&.is-disabled, &:disabled {
background-color: mc('grey', '300');
@ -87,7 +97,11 @@
background-color: mc('grey', '300') !important;
color: mc('grey', '500') !important;
}
}
}
&.is-small {
height: 30px;
}
}

@ -0,0 +1,61 @@
.config-manager {
.welcome {
text-align: center;
padding: 50px 0 15px 0;
color: mc('grey', '700');
h2 {
margin: 0;
}
}
i.icon-loader {
display: inline-block;
color: mc('indigo', '500')
}
i.icon-check {
color: mc('green', '500')
}
i.icon-square-cross {
color: mc('red', '500')
}
i.icon-warning-outline {
color: mc('orange', '500')
}
.tst-welcome-leave-active, .tst-welcome-enter-active {
transition: all .5s;
overflow-y: hidden;
}
.tst-welcome-leave, .tst-welcome-enter-to {
opacity: 1;
max-height: 200px;
}
.tst-welcome-leave-to, .tst-welcome-enter {
opacity: 0;
max-height: 0;
padding-top: 0;
}
.progress-bar {
width: 150px;
height: 10px;
background-color: mc('indigo', '50');
border:1px solid mc('indigo', '100');
border-radius: 3px;
position: absolute;
left: 15px;
top: 21px;
padding: 1px;
> div {
width: 5px;
height: 6px;
background-color: mc('indigo', '200');
border-radius: 2px;
transition: all 1s ease;
}
}
}

@ -1,13 +0,0 @@
@charset "utf-8";
$primary: 'indigo';
@import "base/variables";
@import "base/colors";
@import "base/reset";
@import "base/mixins";
@import "base/fonts";
@import "base/base";
@import "libs/animate";
@import 'pages/login';

@ -1,306 +1,264 @@
body {
padding: 0;
margin: 0;
font-family: $core-font-standard;
font-size: 14px;
}
a {
color: #FFF;
transition: color 0.4s ease;
text-decoration: none;
&:hover {
color: mc('orange','600');
text-decoration: underline;
}
}
#bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: #000;
> div {
background-size: cover;
background-position: center center;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0;
visibility: hidden;
transition: opacity 3s ease, visibility 3s;
animation: bg 30s linear infinite;
&:nth-child(1) {
animation-delay: 10s;
}
&:nth-child(2) {
animation-delay: 20s;
}
}
}
#root {
position: fixed;
top: 15vh;
left: 10vw;
z-index: 2;
color: #FFF;
display: flex;
flex-direction: column;
h1 {
font-size: 4rem;
font-weight: bold;
color: #FFF;
padding: 0;
margin: 0;
animation: headerIntro 3s ease;
}
h2 {
font-size: 1.5rem;
font-weight: normal;
color: rgba(255,255,255,0.7);
padding: 0;
margin: 0 0 25px 0;
animation: headerIntro 3s ease;
}
h3 {
font-size: 1.25rem;
font-weight: normal;
color: #FB8C00;
padding: 0;
margin: 0;
animation: shake 1s ease;
> .fa {
margin-right: 7px;
}
}
h4 {
font-size: 0.8rem;
font-weight: normal;
color: rgba(255,255,255,0.7);
padding: 0;
margin: 0 0 15px 0;
animation: fadeIn 3s ease;
}
form {
display: flex;
flex-direction: column;
}
input[type=text], input[type=password] {
width: 350px;
max-width: 80vw;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 3px;
background-color: rgba(0,0,0,0.2);
padding: 0 15px;
height: 40px;
margin: 0 0 10px 0;
color: #FFF;
font-weight: bold;
font-size: 14px;
transition: all 0.4s ease;
&:focus {
outline: none;
border-color: mc('orange','600');
}
}
button {
background-color: mc('orange','600');
color: #FFF;
border: 1px solid lighten(mc('orange','600'), 10%);
border-radius: 3px;
height: 40px;
width: 125px;
padding: 0;
font-weight: bold;
margin: 15px 0 0 0;
transition: all 0.4s ease;
cursor: pointer;
span {
font-weight: bold;
}
&:focus {
outline: none;
border-color: #FFF;
}
&:hover {
background-color: darken(mc('orange','600'), 10%);
}
}
#social {
margin-top: 25px;
> span {
display: block;
font-weight: bold;
color: rgba(255,255,255,0.7);
}
button {
margin-right: 5px;
width: auto;
padding: 0 15px;
> i {
margin-right: 10px;
font-size: 16px;
}
&.ms {
background-color: mc('blue','600');
border-color: lighten(mc('blue','600'), 10%);
&:focus {
border-color: #FFF;
}
&:hover {
background-color: darken(mc('blue','600'), 10%);
}
}
&.google {
background-color: mc('light-blue','600');
border-color: lighten(mc('light-blue','600'), 10%);
&:focus {
border-color: #FFF;
}
&:hover {
background-color: darken(mc('light-blue','600'), 10%);
}
}
&.facebook {
background-color: mc('indigo','600');
border-color: lighten(mc('indigo','600'), 10%);
&:focus {
border-color: #FFF;
}
&:hover {
background-color: darken(mc('indigo','600'), 10%);
}
}
&.github {
background-color: mc('blue-grey','700');
border-color: lighten(mc('blue-grey','700'), 10%);
&:focus {
border-color: #FFF;
}
&:hover {
background-color: darken(mc('blue-grey','700'), 10%);
}
}
&.slack {
background-color: mc('purple','700');
border-color: lighten(mc('purple','700'), 10%);
&:focus {
border-color: #FFF;
}
&:hover {
background-color: darken(mc('purple','700'), 10%);
}
}
}
}
}
#copyright {
display: flex;
align-items: center;
justify-content: flex-start;
position: absolute;
left: 10vw;
bottom: 10vh;
z-index: 2;
color: rgba(255,255,255,0.5);
font-weight: bold;
.icon {
font-size: 1.2rem;
margin: 0 8px;
}
a {
opacity: 0.75;
}
}
@include keyframes(bg) {
0% {
@include prefix(transform, scale(1,1));
visibility: visible;
opacity: 0;
},
5% {
opacity: 0.5;
},
33% {
opacity: 0.5;
},
38% {
@include prefix(transform, scale(1.2, 1.2));
opacity: 0;
},
39% {
visibility: hidden;
}
100% {
visibility: hidden;
opacity: 0;
}
}
@include keyframes(headerIntro) {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
.login {
background-image: linear-gradient(to right, mc('blue', '400'), mc('blue', '600'));
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
&.is-error {
background-image: linear-gradient(to right, mc('red', '400'), mc('red', '600'));
}
&::before {
content: '';
position: absolute;
background-image: url('../svg/login-bg.svg');
background-position: center bottom;
background-size: cover;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
&-container {
position: relative;
display: flex;
width: 400px;
align-items: stretch;
box-shadow: 0 14px 28px rgba(0,0,0,0.2);
border-radius: 6px;
&.is-expanded {
width: 650px;
.login-frame {
border-radius: 0 6px 6px 0;
border-left: none;
}
}
@include until($tablet) {
width: 350px;
&.is-expanded {
width: 400px;
}
}
}
&-error {
position: absolute;
bottom: 105%;
width: 100%;
min-height: 50px;
background-image: radial-gradient(ellipse at top left, rgba(255,255,255,.9) 0%,rgba(255,255,255,.8) 100%);
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
border-radius: 6px;
color: mc('red', '800');
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
strong {
font-weight: 600;
text-transform: uppercase;
display: block;
padding: 0 1rem 0 0;
border-right: 1px solid mc('red', '200');
}
span {
padding: 0 0 0 1rem;
display: block;
}
}
&-providers {
display: flex;
flex-direction: column;
width: 250px;
border-right: none;
border-radius: 6px 0 0 6px;
z-index: 1;
overflow: hidden;
@include until($tablet) {
width: 50px;
}
button {
flex: 1 1;
padding: 5px 15px;
border: none;
color: #FFF;
background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
border-top: 1px solid rgba(mc('light-blue', '900'), .5);
font-family: $core-font-standard;
font-weight: 600;
text-align: left;
min-height: 40px;
display: flex;
justify-content: flex-start;
align-items: center;
transition: all .4s ease;
&:focus {
outline: none;
}
@include until($tablet) {
justify-content: center;
}
&:hover {
background-color: mc('light-blue', '900');
}
&:first-child {
border-top: none;
&.is-active {
border-top: 1px solid rgba(255,255,255, .5);
}
}
&.is-active {
background-image: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,.77) 100%);
color: mc('light-blue', '700');
cursor: default;
&:hover {
background-color: transparent;
}
svg path {
fill: mc('light-blue', '800');
}
}
i {
margin-right: 10px;
font-size: 16px;
@include until($tablet) {
margin-right: 0;
font-size: 20px;
}
}
svg {
margin-right: 10px;
width: auto;
height: 20px;
max-width: 18px;
max-height: 20px;
path {
fill: #FFF;
}
@include until($tablet) {
margin-right: 0;
font-size: 20px;
}
}
span {
font-weight: 600;
@include until($tablet) {
display: none;
}
}
}
}
&-frame {
background-image: radial-gradient(circle at top center, rgba(255,255,255,1) 5%,rgba(255,255,255,.6) 100%);
border: 1px solid rgba(255,255,255, .5);
border-radius: 6px;
width: 400px;
padding: 1rem;
color: mc('grey', '700');
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
@include until($tablet) {
width: 350px;
}
h1 {
font-size: 2rem;
font-weight: 600;
color: mc('light-blue', '700');
text-shadow: 1px 1px 0 #FFF;
padding: 0;
margin: 0;
}
h2 {
font-size: 1.5rem;
font-weight: 300;
color: mc('grey', '700');
text-shadow: 1px 1px 0 #FFF;
padding: 0;
margin: 0 0 25px 0;
}
form {
display: flex;
flex-direction: column;
}
input[type=text], input[type=password] {
width: 100%;
border: 1px solid #FFF;
border-radius: 3px;
background-color: rgba(255,255,255,.9);
box-shadow: inset 0 0 0 3px rgba(255,255,255, .25);
padding: 0 15px;
height: 40px;
margin: 0 0 10px 0;
color: mc('grey', '700');
font-weight: 600;
font-size: .8rem;
transition: all 0.4s ease;
text-align: center;
&:focus {
outline: none;
border-color: mc('light-blue','500');
background-color: rgba(255,255,255,1);
box-shadow: inset 0 0 0 3px rgba(mc('light-blue','500'), .25);
color: mc('light-blue', '800');
}
}
}
&-copyright {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
bottom: 10vh;
width: 100%;
z-index: 2;
color: mc('grey', '500');
font-weight: 400;
a {
font-weight: 600;
color: mc('light-blue', '500');
margin-left: .25rem;
}
}
}

@ -5,23 +5,9 @@
# https://docs.requarks.io/wiki/install
# ---------------------------------------------------------------------
# Title of this site
# Port the main server should listen to
# ---------------------------------------------------------------------
title: Wiki
# ---------------------------------------------------------------------
# Full public path to the site, without the trailing slash
# ---------------------------------------------------------------------
# INCLUDE CLIENT PORT IF NOT 80/443!
host: http://localhost
# ---------------------------------------------------------------------
# Port the main server should listen to (80 by default)
# ---------------------------------------------------------------------
# To use process.env.PORT, comment the line below:
port: 80
# ---------------------------------------------------------------------
@ -33,140 +19,38 @@ paths:
data: ./data
# ---------------------------------------------------------------------
# Upload Limits
# ---------------------------------------------------------------------
# In megabytes (MB)
uploads:
maxImageFileSize: 3
maxOtherFileSize: 100
# ---------------------------------------------------------------------
# Site Language
# ---------------------------------------------------------------------
# Possible values: en, de, es, fa, fr, ja, ko, nl, pt, ru, sr, tr or zh
lang: en
# Enable for right to left languages (e.g. arabic):
langRtl: false
# ---------------------------------------------------------------------
# Site Authentication
# ---------------------------------------------------------------------
public: false
auth:
defaultReadAccess: false
local:
enabled: true
google:
enabled: true
clientId: GOOGLE_CLIENT_ID
clientSecret: GOOGLE_CLIENT_SECRET
microsoft:
enabled: true
clientId: MS_APP_ID
clientSecret: MS_APP_SECRET
facebook:
enabled: false
clientId: FACEBOOK_APP_ID
clientSecret: FACEBOOK_APP_SECRET
github:
enabled: false
clientId: GITHUB_CLIENT_ID
clientSecret: GITHUB_CLIENT_SECRET
slack:
enabled: false
clientId: 'SLACK_CLIENT_ID'
clientSecret: 'SLACK_CLIENT_SECRET'
ldap:
enabled: false
url: ldap://serverhost:389
bindDn: cn='root'
bindCredentials: BIND_PASSWORD
searchBase: o=users,o=example.com
searchFilter: (uid={{username}})
tlsEnabled: false
tlsCertPath: C:\example\root_ca_cert.crt
azure:
enabled: false
clientId: APP_ID
clientSecret: APP_SECRET_KEY
resource: '00000002-0000-0000-c000-000000000000'
tenant: 'YOUR_TENANT.onmicrosoft.com'
# ---------------------------------------------------------------------
# Secret key to use when encrypting sessions
# ---------------------------------------------------------------------
# Use a long and unique random string (256-bit keys are perfect!)
sessionSecret: 1234567890abcdefghijklmnopqrstuvxyz
# ---------------------------------------------------------------------
# Database Connection String
# ---------------------------------------------------------------------
# You can also use an ENV variable by using $ENV_VAR_NAME as the value
db: mongodb://localhost:27017/wiki
# ---------------------------------------------------------------------
# Git Connection Info
# Database
# ---------------------------------------------------------------------
git:
url: https://github.com/Organization/Repo
branch: master
auth:
# Type: basic or ssh
type: ssh
# Only for Basic authentication:
username: marty
password: MartyMcFly88
# Only for SSH authentication:
privateKey: /etc/wiki/keys/git.pem
sslVerify: true
# Default email to use as commit author
serverEmail: marty@example.com
# Whether to use user email as author in commits
showUserEmail: true
db:
host: localhost
port: 5432
user: wikijs
pass: wikijsrocks
db: wiki
# ---------------------------------------------------------------------
# Features
# Redis
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
linebreaks: true
mathjax: true
redis:
host: localhost
port: 6379
db: 0
password: null
# ---------------------------------------------------------------------
# External Logging
# Background Workers
# ---------------------------------------------------------------------
# Leave 0 for auto based on CPU cores
externalLogging:
bugsnag: false
loggly: false
papertrail: false
rollbar: false
sentry: false
workers: 0
# ---------------------------------------------------------------------
# Color Theme
# High Availability
# ---------------------------------------------------------------------
# Read the docs BEFORE changing these settings!
theme:
primary: indigo
alt: blue-grey
viewSource: all # all | write | false
footer: blue-grey
code:
dark: true
colorize: true
ha:
nodeuid: primary
readonly: false

@ -1,14 +0,0 @@
![Wiki.js](https://raw.githubusercontent.com/Requarks/wiki-site/1.0/assets/images/logo.png)
# Wiki.js
[![npm](https://img.shields.io/npm/v/wiki.js.svg?style=flat-square)](https://github.com/Requarks)
[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat-square&maxAge=3600)](https://github.com/Requarks/wiki/releases)
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat-square)](https://github.com/requarks/wiki/blob/master/LICENSE)
This npm package is an installer for Wiki.js.
For information about Wiki.js, including detailed installation steps, read the following links:
- [Official Website](https://wiki.js.org/)
- [Installation Guide](https://wiki.js.org/get-started.html)
- [GitHub Repository](https://github.com/Requarks/wiki)

@ -1,25 +0,0 @@
'use strict'
// =====================================================
// Wiki.js
// Installation Script
// =====================================================
const path = require('path')
const spawn = require('child_process').spawn
const installDir = path.resolve(__dirname, '../..')
const cmd = (process.platform !== 'win32')
? 'curl -s -S -o- https://wiki.js.org/install.sh | bash'
: `PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://wiki.js.org/install.ps1'))"`
console.info(`Executing installation script for ${process.platform} platform...`)
let inst = spawn(cmd, [], {
cwd: installDir,
env: process.env,
shell: true,
stdio: 'inherit',
detached: true
})
inst.unref()

@ -1,32 +0,0 @@
{
"name": "wiki.js",
"version": "1.0.5-rev.1",
"description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
"main": "install.js",
"scripts": {
"test": "exit 1",
"postinstall": "node install.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Requarks/wiki.git"
},
"keywords": [
"wiki",
"wikis",
"wikijs",
"wiki.js",
"wiki-js",
"docs",
"documentation",
"markdown",
"guides"
],
"author": "Nicolas Giard",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/Requarks/wiki/issues"
},
"homepage": "https://github.com/Requarks/wiki#readme",
"dependencies": {}
}

@ -1,6 +1,6 @@
{
"name": "wiki",
"version": "1.0.11",
"version": "2.0.0",
"description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
"main": "wiki.js",
"scripts": {
@ -9,7 +9,6 @@
"restart": "node wiki restart",
"build": "node tools/fuse",
"dev": "node tools/fuse -d",
"dev-configure": "node tools/fuse -c",
"test": "jest",
"postinstall": "opencollective postinstall"
},
@ -38,135 +37,143 @@
"node": ">=6.11.1"
},
"dependencies": {
"auto-load": "~3.0.0",
"axios": "~0.16.2",
"bcryptjs-then": "~1.0.1",
"bluebird": "~3.5.0",
"body-parser": "~1.17.2",
"bunyan": "~1.8.12",
"cheerio": "~1.0.0-rc.2",
"child-process-promise": "~2.2.1",
"chokidar": "~1.7.0",
"compression": "~1.7.0",
"connect-flash": "~0.1.1",
"connect-mongo": "~1.3.2",
"cookie-parser": "~1.4.3",
"cron": "~1.2.1",
"diff2html": "~2.3.0",
"execa": "~0.8.0",
"express": "~4.15.4",
"apollo-server-express": "1.1.3",
"auto-load": "3.0.0",
"axios": "0.16.2",
"bcryptjs-then": "1.0.1",
"bluebird": "3.5.1",
"body-parser": "1.18.2",
"bull": "3.3.0",
"bunyan": "1.8.12",
"cheerio": "1.0.0-rc.2",
"child-process-promise": "2.2.1",
"chokidar": "1.7.0",
"compression": "1.7.1",
"connect-flash": "0.1.1",
"connect-redis": "3.3.2",
"cookie-parser": "1.4.3",
"diff2html": "2.3.1",
"dotize": "^0.2.0",
"execa": "0.8.0",
"express": "4.16.1",
"express-brute": "1.0.1",
"express-brute-mongoose": "~0.0.9",
"express-session": "~1.15.5",
"file-type": "~6.1.0",
"filesize.js": "~1.0.2",
"follow-redirects": "~1.2.4",
"fs-extra": "~4.0.1",
"git-wrapper2-promise": "~0.2.9",
"highlight.js": "~9.12.0",
"i18next": "~9.0.0",
"i18next-express-middleware": "~1.0.5",
"i18next-node-fs-backend": "~1.0.0",
"image-size": "~0.6.0",
"jimp": "~0.2.28",
"js-yaml": "~3.9.1",
"jsonwebtoken": "~7.4.3",
"klaw": "~2.1.0",
"levelup": "~1.3.9",
"lodash": "~4.17.4",
"markdown-it": "~8.4.0",
"markdown-it-abbr": "~1.0.4",
"markdown-it-anchor": "~4.0.0",
"markdown-it-attrs": "~1.1.0",
"markdown-it-emoji": "~1.4.0",
"markdown-it-expand-tabs": "~1.0.12",
"express-brute-redis": "0.0.1",
"express-session": "1.15.6",
"file-type": "6.2.0",
"filesize.js": "1.0.2",
"follow-redirects": "1.2.5",
"fs-extra": "4.0.2",
"git-wrapper2-promise": "0.2.9",
"graphql": "0.10.5",
"graphql-tools": "2.2.1",
"highlight.js": "9.12.0",
"i18next": "9.1.0",
"i18next-express-middleware": "1.0.7",
"i18next-localstorage-cache": "1.1.1",
"i18next-node-fs-backend": "1.0.0",
"image-size": "0.6.1",
"ioredis": "3.1.4",
"jimp": "0.2.28",
"js-yaml": "3.10.0",
"jsonwebtoken": "8.0.1",
"klaw": "2.1.0",
"lodash": "4.17.4",
"markdown-it": "8.4.0",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "4.0.0",
"markdown-it-attrs": "1.2.0",
"markdown-it-emoji": "1.4.0",
"markdown-it-expand-tabs": "1.0.12",
"markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "~3.0.1",
"markdown-it-mathjax": "~2.0.0",
"markdown-it-task-lists": "~2.0.1",
"mathjax-node": "~1.2.0",
"memdown": "~1.2.4",
"mime-types": "~2.1.16",
"moment": "~2.18.1",
"moment-timezone": "~0.5.13",
"mongodb": "~2.2.31",
"mongoose": "~4.11.9",
"multer": "~1.3.0",
"node-2fa": "~1.1.2",
"node-graceful": "~0.2.3",
"opencollective": "~1.0.3",
"ora": "~1.3.0",
"passport": "~0.4.0",
"markdown-it-footnote": "3.0.1",
"markdown-it-mathjax": "2.0.0",
"markdown-it-task-lists": "2.0.1",
"mathjax-node": "1.2.1",
"mime-types": "2.1.17",
"moment": "2.18.1",
"moment-timezone": "0.5.13",
"multer": "1.3.0",
"node-2fa": "1.1.2",
"node-graceful": "0.2.3",
"ora": "1.3.0",
"passport": "0.4.0",
"passport-azure-ad-oauth2": "0.0.4",
"passport-facebook": "~2.1.1",
"passport-github2": "~0.1.10",
"passport-google-oauth20": "~1.0.0",
"passport-ldapauth": "~2.0.0",
"passport-local": "~1.0.0",
"passport-facebook": "2.1.1",
"passport-github2": "0.1.11",
"passport-google-oauth20": "1.0.0",
"passport-ldapauth": "2.0.0",
"passport-local": "1.0.0",
"passport-slack": "0.0.7",
"passport-windowslive": "~1.0.2",
"passport.socketio": "~3.7.0",
"pm2": "~2.6.1",
"pug": "~2.0.0-rc.3",
"read-chunk": "~2.1.0",
"remove-markdown": "~0.2.2",
"request": "~2.81.0",
"search-index-adder": "~0.3.9",
"search-index-searcher": "~0.2.10",
"semver": "~5.4.1",
"serve-favicon": "~2.4.3",
"simplemde": "~1.11.2",
"socket.io": "~2.0.2",
"stopword": "~0.1.6",
"stream-to-promise": "~2.2.0",
"tar": "~4.0.1",
"through2": "~2.0.3",
"validator": "~8.1.0",
"validator-as-promised": "~1.0.2",
"winston": "~2.3.1",
"yargs": "~8.0.1"
"passport-windowslive": "1.0.2",
"pg": "6.4.2",
"pg-hstore": "2.3.2",
"pg-promise": "6.10.3",
"pm2": "2.7.1",
"pug": "2.0.0-rc.4",
"qr-image": "3.2.0",
"read-chunk": "2.1.0",
"remove-markdown": "0.2.2",
"request": "2.83.0",
"semver": "5.4.1",
"sequelize": "4.13.5",
"serve-favicon": "2.4.5",
"simplemde": "1.11.2",
"stream-to-promise": "2.2.0",
"tar": "4.0.1",
"through2": "2.0.3",
"validator": "9.0.0",
"validator-as-promised": "1.0.2",
"winston": "2.4.0",
"yargs": "9.0.1"
},
"devDependencies": {
"@glimpse/glimpse": "~0.22.15",
"@panter/vue-i18next": "~0.5.0",
"babel-cli": "~6.26.0",
"babel-jest": "~20.0.3",
"babel-plugin-transform-object-assign": "~6.22.0",
"babel-preset-es2015": "~6.24.1",
"brace": "~0.10.0",
"colors": "~1.1.2",
"consolidate": "~0.14.5",
"eslint": "~4.5.0",
"eslint-config-standard": "~10.2.1",
"eslint-plugin-import": "~2.7.0",
"eslint-plugin-node": "~5.1.0",
"eslint-plugin-promise": "~3.5.0",
"eslint-plugin-standard": "~3.0.1",
"fuse-box": "~2.2.2",
"i18next-xhr-backend": "~1.4.2",
"jest": "~20.0.4",
"jest-junit": "~3.1.0",
"jquery": "~3.2.1",
"jquery-contextmenu": "~2.5.0",
"jquery-simple-upload": "~1.0.0",
"jquery-smooth-scroll": "~2.2.0",
"jquery-sticky": "~1.0.4",
"lodash-cli": "~4.17.4",
"lodash-es": "~4.17.4",
"node-sass": "~4.5.3",
"nodemon": "~1.11.0",
"pug-lint": "~2.4.0",
"twemoji-awesome": "~1.0.6",
"typescript": "~2.5.2",
"uglify-es": "~3.0.28",
"vee-validate": "~2.0.0-rc.14",
"vue": "~2.4.2",
"vue-clipboards": "~1.1.0",
"vue-lodash": "~1.0.3",
"vue-resource": "~1.3.4",
"vue-template-compiler": "~2.4.2",
"vue-template-es2015-compiler": "~1.5.3",
"vuex": "~2.4.0"
"@glimpse/glimpse": "0.22.15",
"@panter/vue-i18next": "0.6.1",
"apollo-client": "^1.9.3",
"autoprefixer": "7.1.5",
"babel-cli": "6.26.0",
"babel-core": "6.26.0",
"babel-jest": "21.2.0",
"babel-preset-env": "1.6.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-stage-2": "6.24.1",
"brace": "0.10.0",
"colors": "1.1.2",
"consolidate": "0.14.5",
"eslint": "4.8.0",
"eslint-config-requarks": "1.0.7",
"eslint-config-standard": "10.2.1",
"eslint-plugin-import": "2.7.0",
"eslint-plugin-node": "5.2.0",
"eslint-plugin-promise": "3.5.0",
"eslint-plugin-standard": "3.0.1",
"fuse-box": "2.3.3",
"graphql-tag": "^2.4.2",
"i18next-xhr-backend": "1.4.3",
"jest": "21.2.1",
"jquery": "3.2.1",
"jquery-contextmenu": "2.6.2",
"jquery-simple-upload": "1.0.0",
"js-cookie": "2.1.4",
"node-sass": "4.5.3",
"nodemon": "1.12.1",
"postcss-selector-parser": "2.2.3",
"pug-lint": "2.5.0",
"twemoji-awesome": "1.0.6",
"typescript": "2.5.3",
"uglify-es": "3.1.3",
"vee-validate": "2.0.0-rc.18",
"vue": "2.4.4",
"vue-clipboards": "1.1.0",
"vue-hot-reload-api": "2.1.1",
"vue-lodash": "1.0.4",
"vue-material": "^0.7.5",
"vue-resource": "1.3.4",
"vue-simple-breakpoints": "1.0.2",
"vue-template-compiler": "2.4.4",
"vue-template-es2015-compiler": "1.5.3",
"vuex": "2.4.1",
"vuex-persistedstate": "2.0.0"
},
"jest": {
"testResultsProcessor": "./node_modules/jest-junit",

@ -1,222 +0,0 @@
// ===========================================
// Wiki.js - Background Agent
// 1.0.0
// Licensed under AGPLv3
// ===========================================
const path = require('path')
const ROOTPATH = process.cwd()
const SERVERPATH = path.join(ROOTPATH, 'server')
global.ROOTPATH = ROOTPATH
global.SERVERPATH = SERVERPATH
const IS_DEBUG = process.env.NODE_ENV === 'development'
let appconf = require('./libs/config')()
global.appconfig = appconf.config
global.appdata = appconf.data
// ----------------------------------------
// Load Winston
// ----------------------------------------
global.winston = require('./libs/logger')(IS_DEBUG, 'AGENT')
// ----------------------------------------
// Load global modules
// ----------------------------------------
global.winston.info('Background Agent is initializing...')
global.db = require('./libs/db').init()
global.upl = require('./libs/uploads-agent').init()
global.git = require('./libs/git').init()
global.entries = require('./libs/entries').init()
global.lang = require('i18next')
global.mark = require('./libs/markdown')
// ----------------------------------------
// Load modules
// ----------------------------------------
const moment = require('moment')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const klaw = require('klaw')
const Cron = require('cron').CronJob
const i18nBackend = require('i18next-node-fs-backend')
const entryHelper = require('./helpers/entry')
// ----------------------------------------
// Localization Engine
// ----------------------------------------
global.lang
.use(i18nBackend)
.init({
load: 'languageOnly',
ns: ['common', 'admin', 'auth', 'errors', 'git'],
defaultNS: 'common',
saveMissing: false,
preload: [appconfig.lang],
lng: appconfig.lang,
fallbackLng: 'en',
backend: {
loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
}
})
// ----------------------------------------
// Start Cron
// ----------------------------------------
let job
let jobIsBusy = false
let jobUplWatchStarted = false
global.db.onReady.then(() => {
return global.db.Entry.remove({})
}).then(() => {
job = new Cron({
cronTime: '0 */5 * * * *',
onTick: () => {
// Make sure we don't start two concurrent jobs
if (jobIsBusy) {
global.winston.warn('Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)')
return
}
global.winston.info('Running all jobs...')
jobIsBusy = true
// Prepare async job collector
let jobs = []
let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
let dataPath = path.resolve(ROOTPATH, appconfig.paths.data)
let uploadsTempPath = path.join(dataPath, 'temp-upload')
// ----------------------------------------
// REGULAR JOBS
// ----------------------------------------
//* ****************************************
// -> Sync with Git remote
//* ****************************************
jobs.push(global.git.resync().then(() => {
// -> Stream all documents
let cacheJobs = []
let jobCbStreamDocsResolve = null
let jobCbStreamDocs = new Promise((resolve, reject) => {
jobCbStreamDocsResolve = resolve
})
klaw(repoPath).on('data', function (item) {
if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path))
let cachePath = entryHelper.getCachePath(entryPath)
// -> Purge outdated cache
cacheJobs.push(
fs.statAsync(cachePath).then((st) => {
return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'
}).catch((err) => {
return (err.code !== 'EEXIST') ? err : 'new'
}).then((fileStatus) => {
// -> Delete expired cache file
if (fileStatus === 'expired') {
return fs.unlinkAsync(cachePath).return(fileStatus)
}
return fileStatus
}).then((fileStatus) => {
// -> Update cache and search index
if (fileStatus !== 'active') {
return global.entries.updateCache(entryPath).then(entry => {
process.send({
action: 'searchAdd',
content: entry
})
return true
})
}
return true
})
)
}
}).on('end', () => {
jobCbStreamDocsResolve(Promise.all(cacheJobs))
})
return jobCbStreamDocs
}))
//* ****************************************
// -> Clear failed temporary upload files
//* ****************************************
jobs.push(
fs.readdirAsync(uploadsTempPath).then((ls) => {
let fifteenAgo = moment().subtract(15, 'minutes')
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } })
}).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {
return Promise.map(arrFiles, (f) => {
if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
return fs.unlinkAsync(path.join(uploadsTempPath, f.filename))
} else {
return true
}
})
})
})
)
// ----------------------------------------
// Run
// ----------------------------------------
Promise.all(jobs).then(() => {
global.winston.info('All jobs completed successfully! Going to sleep for now.')
if (!jobUplWatchStarted) {
jobUplWatchStarted = true
global.upl.initialScan().then(() => {
job.start()
})
}
return true
}).catch((err) => {
global.winston.error('One or more jobs have failed: ', err)
}).finally(() => {
jobIsBusy = false
})
},
start: false,
timeZone: 'UTC',
runOnInit: true
})
})
// ----------------------------------------
// Shutdown gracefully
// ----------------------------------------
process.on('disconnect', () => {
global.winston.warn('Lost connection to main server. Exiting...')
job.stop()
process.exit()
})
process.on('exit', () => {
job.stop()
})

@ -5,67 +5,49 @@
name: Wiki.js
defaults:
config:
title: Wiki
host: http://localhost
port: 80
paths:
repo: ./repo
data: ./data
uploads:
maxImageFileSize: 3,
maxOtherFileSize: 100
lang: en
langRtl: false
public: false
auth:
defaultReadAccess: false
local:
enabled: true
microsoft:
enabled: false
google:
enabled: false
facebook:
enabled: false
github:
enabled: false
slack:
enabled: false
ldap:
enabled: false
azure:
enabled: false
db: mongodb://localhost/wiki
sessionSecret: null
admin: null
git:
url: null
branch: master
auth:
type: basic
username: null
password: null
privateKey: null
sslVerify: true
serverEmail: wiki@example.com
showUserEmail: true
features:
linebreaks: true
mathjax: true
externalLogging:
bugsnap: false
loggly: false
papertrail: false
rollbar: false
sentry: false
theme:
primary: indigo
alt: blue-grey
footer: blue-grey
viewSource: false
code:
dark: true
colorize: true
db:
host: localhost
port: 5432
user: wikijs
pass: wikijsrocks
db: wiki
redis:
host: localhost
port: 6379
db: 0
password: null
workers: 0
ha:
nodeuid: primary
readonly: false
site:
path: ''
lang: en
title: Wiki.js
configNamespaces:
- auth
- features
- git
- logging
- site
- theme
- uploads
queues:
- gitSync
- uplClearTemp
authProviders:
- local
- microsoft
- google
- facebook
- github
- slack
- ldap
- azure
colors:
- red
- pink
@ -108,6 +90,12 @@ langs:
-
id: ko
name: Korean - 한국어
-
id: fa
name: Persian (Fārsi) - فارسی
-
id: pt
name: Portuguese - Português
-
id: ru
name: Russian - Русский

@ -0,0 +1,37 @@
'use strict'
/* global wiki */
// ------------------------------------
// Azure AD Account
// ------------------------------------
const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
module.exports = {
key: 'azure',
title: 'Azure Active Directory',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL', 'resource', 'tenant'],
init (passport, conf) {
const jwt = require('jsonwebtoken')
passport.use('azure_ad_oauth2',
new AzureAdOAuth2Strategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
resource: conf.resource,
tenant: conf.tenant
}, (accessToken, refreshToken, params, profile, cb) => {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
wiki.db.User.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,32 @@
'use strict'
/* global wiki */
// ------------------------------------
// Facebook Account
// ------------------------------------
const FacebookStrategy = require('passport-facebook').Strategy
module.exports = {
key: 'facebook',
title: 'Facebook',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('facebook',
new FacebookStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,32 @@
'use strict'
/* global wiki */
// ------------------------------------
// GitHub Account
// ------------------------------------
const GitHubStrategy = require('passport-github2').Strategy
module.exports = {
key: 'github',
title: 'GitHub',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('github',
new GitHubStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,31 @@
'use strict'
/* global wiki */
// ------------------------------------
// Google ID Account
// ------------------------------------
const GoogleStrategy = require('passport-google-oauth20').Strategy
module.exports = {
key: 'google',
title: 'Google ID',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('google',
new GoogleStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,46 @@
'use strict'
/* global wiki */
// ------------------------------------
// LDAP Account
// ------------------------------------
const LdapStrategy = require('passport-ldapauth').Strategy
const fs = require('fs')
module.exports = {
key: 'ldap',
title: 'LDAP / Active Directory',
useForm: true,
props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'],
init (passport, conf) {
passport.use('ldapauth',
new LdapStrategy({
server: {
url: conf.url,
bindDn: conf.bindDn,
bindCredentials: conf.bindCredentials,
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
searchAttributes: ['displayName', 'name', 'cn', 'mail'],
tlsOptions: (conf.tlsEnabled) ? {
ca: [
fs.readFileSync(conf.tlsCertPath)
]
} : {}
},
usernameField: 'email',
passReqToCallback: false
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,38 @@
'use strict'
/* global wiki */
// ------------------------------------
// Local Account
// ------------------------------------
const LocalStrategy = require('passport-local').Strategy
module.exports = {
key: 'local',
title: 'Local',
useForm: true,
props: [],
init (passport, conf) {
passport.use('local',
new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)
})
} else {
return done(new Error('INVALID_LOGIN'), null)
}
}).catch((err) => {
done(err, null)
})
}
))
}
}

@ -0,0 +1,31 @@
'use strict'
/* global wiki */
// ------------------------------------
// Microsoft Account
// ------------------------------------
const WindowsLiveStrategy = require('passport-windowslive').Strategy
module.exports = {
key: 'microsoft',
title: 'Microsoft Account',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('microsoft',
new WindowsLiveStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -0,0 +1,31 @@
'use strict'
/* global wiki */
// ------------------------------------
// Slack Account
// ------------------------------------
const SlackStrategy = require('passport-slack').Strategy
module.exports = {
key: 'slack',
title: 'Slack',
useForm: false,
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('slack',
new SlackStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

@ -1,11 +1,12 @@
'use strict'
const path = require('path')
module.exports = (port, spinner) => {
const path = require('path')
/* global wiki */
const ROOTPATH = process.cwd()
const SERVERPATH = path.join(ROOTPATH, 'server')
const IS_DEBUG = process.env.NODE_ENV === 'development'
module.exports = () => {
wiki.config.site = {
path: '',
title: 'Wiki.js'
}
// ----------------------------------------
// Load modules
@ -26,28 +27,30 @@ module.exports = (port, spinner) => {
// Define Express App
// ----------------------------------------
var app = express()
let app = express()
app.use(compression())
var server
let server
// ----------------------------------------
// Public Assets
// ----------------------------------------
app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(ROOTPATH, 'assets')))
app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(wiki.ROOTPATH, 'assets')))
// ----------------------------------------
// View Engine Setup
// ----------------------------------------
app.set('views', path.join(SERVERPATH, 'views'))
app.set('views', path.join(wiki.SERVERPATH, 'views'))
app.set('view engine', 'pug')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.locals.config = wiki.config
app.locals.data = wiki.data
app.locals._ = require('lodash')
// ----------------------------------------
@ -55,21 +58,8 @@ module.exports = (port, spinner) => {
// ----------------------------------------
app.get('*', (req, res) => {
let langs = []
let conf = {}
try {
langs = yaml.safeLoad(fs.readFileSync(path.join(SERVERPATH, 'app/data.yml'), 'utf8')).langs
conf = yaml.safeLoad(fs.readFileSync(path.join(ROOTPATH, 'config.yml'), 'utf8'))
} catch (err) {
console.error(err)
}
res.render('configure/index', {
langs,
conf,
runmode: {
staticPort: (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER),
staticMongo: (!_.isNil(process.env.WIKI_JS_HEROKU))
}
fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
res.render('configure/index', { packageObj })
})
})
@ -80,15 +70,15 @@ module.exports = (port, spinner) => {
Promise.mapSeries([
() => {
const semver = require('semver')
if (!semver.satisfies(semver.clean(process.version), '>=6.9.0')) {
throw new Error('Node.js version is too old. Minimum is v6.6.0.')
if (!semver.satisfies(semver.clean(process.version), '>=6.11.1')) {
throw new Error('Node.js version is too old. Minimum is 6.11.1.')
}
return 'Node.js ' + process.version + ' detected. Minimum is v6.9.0.'
return 'Node.js ' + process.version + ' detected. Minimum is 6.11.1.'
},
() => {
return Promise.try(() => {
require('crypto')
}).catch(err => { // eslint-disable-line handle-callback-err
}).catch(err => {
throw new Error('Crypto Node.js module is not available.')
}).return('Node.js Crypto module is available.')
},
@ -102,24 +92,24 @@ module.exports = (port, spinner) => {
}
let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi))
if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) {
reject(new Error('Git version is too old. Minimum is v2.7.4.'))
reject(new Error('Git version is too old. Minimum is 2.7.4.'))
}
resolve('Git v' + gitver + ' detected. Minimum is v2.7.4.')
resolve('Git ' + gitver + ' detected. Minimum is 2.7.4.')
})
})
},
() => {
const os = require('os')
if (os.totalmem() < 1000 * 1000 * 768) {
throw new Error('Not enough memory. Minimum is 768 MB.')
if (os.totalmem() < 1000 * 1000 * 512) {
throw new Error('Not enough memory. Minimum is 512 MB.')
}
return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 768 MB.'
return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 512 MB.'
},
() => {
let fs = require('fs')
return Promise.try(() => {
fs.accessSync(path.join(ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
}).catch(err => { // eslint-disable-line handle-callback-err
fs.accessSync(path.join(wiki.ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
}).catch(err => {
throw new Error('config.yml file is not writable by Node.js process or was not created properly.')
}).return('config.yml is writable by the setup process.')
}
@ -130,43 +120,6 @@ module.exports = (port, spinner) => {
})
})
/**
* Check the DB connection
*/
app.post('/dbcheck', (req, res) => {
let mongo = require('mongodb').MongoClient
let mongoURI = cfgHelper.parseConfigValue(req.body.db)
mongo.connect(mongoURI, {
autoReconnect: false,
reconnectTries: 2,
reconnectInterval: 1000,
connectTimeoutMS: 5000,
socketTimeoutMS: 5000
}, (err, db) => {
if (err === null) {
// Try to create a test collection
db.createCollection('test', (err, results) => {
if (err === null) {
// Try to drop test collection
db.dropCollection('test', (err, results) => {
if (err === null) {
res.json({ ok: true })
} else {
res.json({ ok: false, error: 'Unable to delete test collection. Verify permissions. ' + err.message })
}
db.close()
})
} else {
res.json({ ok: false, error: 'Unable to create test collection. Verify permissions. ' + err.message })
db.close()
}
})
} else {
res.json({ ok: false, error: err.message })
}
})
})
/**
* Check the Git connection
*/
@ -174,8 +127,8 @@ module.exports = (port, spinner) => {
const exec = require('execa')
const url = require('url')
const dataDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
const gitDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
const dataDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
const gitDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
let gitRemoteUrl = ''
@ -315,7 +268,7 @@ module.exports = (port, spinner) => {
}
})
}),
fs.readFileAsync(path.join(ROOTPATH, 'config.yml'), 'utf8').then(confRaw => {
fs.readFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8').then(confRaw => {
let conf = yaml.safeLoad(confRaw)
conf.title = req.body.title
conf.host = req.body.host
@ -356,12 +309,12 @@ module.exports = (port, spinner) => {
return crypto.randomBytesAsync(32).then(buf => {
conf.sessionSecret = buf.toString('hex')
confRaw = yaml.safeDump(conf)
return fs.writeFileAsync(path.join(ROOTPATH, 'config.yml'), confRaw)
return fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw)
})
})
).then(() => {
if (process.env.IS_HEROKU) {
return fs.outputJsonAsync(path.join(SERVERPATH, 'app/heroku.json'), { configured: true })
return fs.outputJsonAsync(path.join(wiki.SERVERPATH, 'app/heroku.json'), { configured: true })
} else {
return true
}
@ -377,7 +330,7 @@ module.exports = (port, spinner) => {
*/
app.post('/restart', (req, res) => {
res.status(204).end()
server.destroy(() => {
/* server.destroy(() => {
spinner.text = 'Setup wizard terminated. Restarting in normal mode...'
_.delay(() => {
const exec = require('execa')
@ -386,7 +339,7 @@ module.exports = (port, spinner) => {
process.exit(0)
})
}, 1000)
})
}) */
})
// ----------------------------------------
@ -403,21 +356,20 @@ module.exports = (port, spinner) => {
res.status(err.status || 500)
res.send({
message: err.message,
error: IS_DEBUG ? err : {}
error: wiki.IS_DEBUG ? err : {}
})
spinner.fail(err.message)
process.exit(1)
wiki.logger.error(err.message)
})
// ----------------------------------------
// Start HTTP server
// ----------------------------------------
spinner.text = 'Starting HTTP server...'
wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`)
app.set('port', port)
app.set('port', wiki.config.port)
server = http.createServer(app)
server.listen(port)
server.listen(wiki.config.port)
var openConnections = []
@ -443,10 +395,10 @@ module.exports = (port, spinner) => {
switch (error.code) {
case 'EACCES':
spinner.fail('Listening on port ' + port + ' requires elevated privileges!')
wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
spinner.fail('Port ' + port + ' is already in use!')
wiki.logger.error('Port ' + wiki.config.port + ' is already in use!')
return process.exit(1)
default:
throw error
@ -454,6 +406,6 @@ module.exports = (port, spinner) => {
})
server.on('listening', () => {
spinner.text = 'Browse to http://localhost:' + port + ' to configure Wiki.js!'
wiki.logger.info('HTTP Server: RUNNING')
})
}

@ -1,6 +1,6 @@
'use strict'
/* global db, lang, rights, winston */
/* global wiki */
var express = require('express')
var router = express.Router()
@ -33,14 +33,14 @@ router.post('/profile', (req, res) => {
return res.render('error-forbidden')
}
return db.User.findById(req.user.id).then((usr) => {
return wiki.db.User.findById(req.user.id).then((usr) => {
usr.name = _.trim(req.body.name)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
return wiki.db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
@ -61,9 +61,9 @@ router.get('/stats', (req, res) => {
}
Promise.all([
db.Entry.count(),
db.UplFile.count(),
db.User.count()
wiki.db.Entry.count(),
wiki.db.UplFile.count(),
wiki.db.User.count()
]).spread((totalEntries, totalUploads, totalUsers) => {
return res.render('pages/admin/stats', {
totalEntries, totalUploads, totalUsers, adminTab: 'stats'
@ -78,7 +78,7 @@ router.get('/users', (req, res) => {
return res.render('error-forbidden')
}
db.User.find({})
wiki.db.User.find({})
.select('-password -rights')
.sort('name email')
.exec().then((usrs) => {
@ -95,7 +95,7 @@ router.get('/users/:id', (req, res) => {
return res.render('error-forbidden')
}
db.User.findById(req.params.id)
wiki.db.User.findById(req.params.id)
.select('-password -providerId')
.exec().then((usr) => {
let usrOpts = {
@ -137,12 +137,12 @@ router.post('/users/create', (req, res) => {
return res.status(400).json({ msg: 'Name is missing' })
}
db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
wiki.db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
if (exUsr) {
return res.status(400).json({ msg: 'User already exists!' }) || true
}
let pwdGen = (nUsr.provider === 'local') ? db.User.hashPassword(nUsr.password) : Promise.resolve(true)
let pwdGen = (nUsr.provider === 'local') ? wiki.db.User.hashPassword(nUsr.password) : Promise.resolve(true)
return pwdGen.then(nPwd => {
if (nUsr.provider !== 'local') {
nUsr.password = ''
@ -158,37 +158,37 @@ router.post('/users/create', (req, res) => {
deny: false
}]
return db.User.create(nUsr).then(() => {
return wiki.db.User.create(nUsr).then(() => {
return res.json({ ok: true })
})
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
return res.status(500).json({ msg: err })
})
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
return res.status(500).json({ msg: err })
})
})
router.post('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: lang.t('errors:unauthorized') })
return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
}
if (!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
}
return db.User.findById(req.params.id).then((usr) => {
return wiki.db.User.findById(req.params.id).then((usr) => {
usr.name = _.trim(req.body.name)
usr.rights = JSON.parse(req.body.rights)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error(lang.t('errors:newpasswordtooshort')))
return Promise.reject(new Error(wiki.lang.t('errors:newpasswordtooshort')))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
return wiki.db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
@ -199,7 +199,7 @@ router.post('/users/:id', (req, res) => {
}).then((usr) => {
// Update guest rights for future requests
if (usr.provider === 'local' && usr.email === 'guest') {
rights.guest = usr
wiki.rights.guest = usr
}
return usr
}).then(() => {
@ -214,14 +214,14 @@ router.post('/users/:id', (req, res) => {
*/
router.delete('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: lang.t('errors:unauthorized') })
return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
}
if (!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
}
return db.User.findByIdAndRemove(req.params.id).then(() => {
return wiki.db.User.findByIdAndRemove(req.params.id).then(() => {
return res.json({ ok: true })
}).catch((err) => {
res.status(500).json({ ok: false, msg: err.message })
@ -249,7 +249,7 @@ router.get('/system', (req, res) => {
cwd: process.cwd()
}
fs.readJsonAsync(path.join(ROOTPATH, 'package.json')).then(packageObj => {
fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
axios.get('https://api.github.com/repos/Requarks/wiki/releases/latest').then(resp => {
let sysversion = {
current: 'v' + packageObj.version,
@ -259,7 +259,7 @@ router.get('/system', (req, res) => {
res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion })
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion: { current: 'v' + packageObj.version } })
})
})
@ -287,19 +287,19 @@ router.post('/theme', (req, res) => {
return res.render('error-forbidden')
}
if (!validator.isIn(req.body.primary, appdata.colors)) {
if (!validator.isIn(req.body.primary, wiki.data.colors)) {
return res.status(406).json({ msg: 'Primary color is invalid.' })
} else if (!validator.isIn(req.body.alt, appdata.colors)) {
} else if (!validator.isIn(req.body.alt, wiki.data.colors)) {
return res.status(406).json({ msg: 'Alternate color is invalid.' })
} else if (!validator.isIn(req.body.footer, appdata.colors)) {
} else if (!validator.isIn(req.body.footer, wiki.data.colors)) {
return res.status(406).json({ msg: 'Footer color is invalid.' })
}
appconfig.theme.primary = req.body.primary
appconfig.theme.alt = req.body.alt
appconfig.theme.footer = req.body.footer
appconfig.theme.code.dark = req.body.codedark === 'true'
appconfig.theme.code.colorize = req.body.codecolorize === 'true'
wiki.config.theme.primary = req.body.primary
wiki.config.theme.alt = req.body.alt
wiki.config.theme.footer = req.body.footer
wiki.config.theme.code.dark = req.body.codedark === 'true'
wiki.config.theme.code.colorize = req.body.codecolorize === 'true'
return res.json({ msg: 'OK' })
})

@ -1,19 +1,19 @@
'use strict'
/* global db, lang */
/* global wiki */
const Promise = require('bluebird')
const express = require('express')
const router = express.Router()
const passport = require('passport')
const ExpressBrute = require('express-brute')
const ExpressBruteMongooseStore = require('express-brute-mongoose')
const ExpressBruteRedisStore = require('express-brute-redis')
const moment = require('moment')
const _ = require('lodash')
/**
* Setup Express-Brute
*/
const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
const EBstore = new ExpressBruteRedisStore({
client: wiki.redis
})
const bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5,
minWait: 60 * 1000,
@ -22,8 +22,8 @@ const bruteforce = new ExpressBrute(EBstore, {
failCallback (req, res, next, nextValidRequestDate) {
req.flash('alert', {
class: 'error',
title: lang.t('auth:errors.toomanyattempts'),
message: lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
title: wiki.lang.t('auth:errors.toomanyattempts'),
message: wiki.lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
iconClass: 'fa-times'
})
res.redirect('/login')
@ -35,23 +35,24 @@ const bruteforce = new ExpressBrute(EBstore, {
*/
router.get('/login', function (req, res, next) {
res.render('auth/login', {
usr: res.locals.usr
authStrategies: _.reject(wiki.auth.strategies, { key: 'local' }),
hasMultipleStrategies: Object.keys(wiki.config.auth.strategies).length > 1
})
})
router.post('/login', bruteforce.prevent, function (req, res, next) {
new Promise((resolve, reject) => {
// [1] LOCAL AUTHENTICATION
passport.authenticate('local', function (err, user, info) {
wiki.auth.passport.authenticate('local', function (err, user, info) {
if (err) { return reject(err) }
if (!user) { return reject(new Error('INVALID_LOGIN')) }
resolve(user)
})(req, res, next)
}).catch({ message: 'INVALID_LOGIN' }, err => {
if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) {
if (_.has(wiki.config.auth.strategy, 'ldap')) {
// [2] LDAP AUTHENTICATION
return new Promise((resolve, reject) => {
passport.authenticate('ldapauth', function (err, user, info) {
wiki.auth.passport.authenticate('ldapauth', function (err, user, info) {
if (err) { return reject(err) }
if (info && info.message) { return reject(new Error(info.message)) }
if (!user) { return reject(new Error('INVALID_LOGIN')) }
@ -73,13 +74,13 @@ router.post('/login', bruteforce.prevent, function (req, res, next) {
// LOGIN FAIL
if (err.message === 'INVALID_LOGIN') {
req.flash('alert', {
title: lang.t('auth:errors.invalidlogin'),
message: lang.t('auth:errors.invalidloginmsg')
title: wiki.lang.t('auth:errors.invalidlogin'),
message: wiki.lang.t('auth:errors.invalidloginmsg')
})
return res.redirect('/login')
} else {
req.flash('alert', {
title: lang.t('auth:errors.loginerror'),
title: wiki.lang.t('auth:errors.loginerror'),
message: err.message
})
return res.redirect('/login')
@ -91,19 +92,19 @@ router.post('/login', bruteforce.prevent, function (req, res, next) {
* Social Login
*/
router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }))
router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
router.get('/login/github', passport.authenticate('github', { scope: ['user:email'] }))
router.get('/login/slack', passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
router.get('/login/azure', passport.authenticate('azure_ad_oauth2'))
router.get('/login/ms', wiki.auth.passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
router.get('/login/google', wiki.auth.passport.authenticate('google', { scope: ['profile', 'email'] }))
router.get('/login/facebook', wiki.auth.passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
router.get('/login/github', wiki.auth.passport.authenticate('github', { scope: ['user:email'] }))
router.get('/login/slack', wiki.auth.passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
router.get('/login/azure', wiki.auth.passport.authenticate('azure_ad_oauth2'))
router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/github/callback', passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/slack/callback', passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/azure/callback', passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/ms/callback', wiki.auth.passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/google/callback', wiki.auth.passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/facebook/callback', wiki.auth.passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/github/callback', wiki.auth.passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/slack/callback', wiki.auth.passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/azure/callback', wiki.auth.passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
/**
* Logout

@ -1,6 +1,9 @@
'use strict'
/* global git, lang, lcdata, upl */
/* global wiki */
module.exports = false
return
const express = require('express')
const router = express.Router()
@ -12,7 +15,7 @@ const fs = Promise.promisifyAll(require('fs-extra'))
const path = require('path')
const _ = require('lodash')
const validPathRe = new RegExp('^([a-z0-9/-' + appdata.regex.cjk + appdata.regex.arabic + ']+\\.[a-z0-9]+)$')
const validPathRe = new RegExp('^([a-z0-9/-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\.[a-z0-9]+)$')
const validPathThumbsRe = new RegExp('^([a-z0-9]+\\.png)$')
// ==========================================
@ -28,7 +31,7 @@ router.get('/t/*', (req, res, next) => {
// todo: Authentication-based access
res.sendFile(fileName, {
root: lcdata.getThumbsPath(),
root: wiki.disk.getThumbsPath(),
dotfiles: 'deny'
}, (err) => {
if (err) {
@ -37,12 +40,12 @@ router.get('/t/*', (req, res, next) => {
})
})
router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
router.post('/img', wiki.disk.uploadImgHandler, (req, res, next) => {
let destFolder = _.chain(req.body.folder).trim().toLower().value()
upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
res.json({ ok: false, msg: lang.t('errors:invalidfolder') })
res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') })
return true
}
@ -50,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
let destFilename = ''
let destFilePath = ''
return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
return wiki.disk.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
@ -60,7 +63,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
let mimeInfo = fileType(buf)
if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
return Promise.reject(new Error(lang.t('errors:invalidfiletype')))
return Promise.reject(new Error(wiki.lang.t('errors:invalidfiletype')))
}
return true
}).then(() => {
@ -94,12 +97,12 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
})
})
router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
router.post('/file', wiki.disk.uploadFileHandler, (req, res, next) => {
let destFolder = _.chain(req.body.folder).trim().toLower().value()
upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
res.json({ ok: false, msg: lang.t('errors:invalidfolder') })
res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') })
return true
}
@ -107,7 +110,7 @@ router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
let destFilename = ''
let destFilePath = ''
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
return wiki.disk.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
@ -150,7 +153,7 @@ router.get('/*', (req, res, next) => {
// todo: Authentication-based access
res.sendFile(fileName, {
root: git.getRepoPath() + '/uploads/',
root: wiki.git.getRepoPath() + '/uploads/',
dotfiles: 'deny'
}, (err) => {
if (err) {

@ -1,276 +1,43 @@
'use strict'
// ===========================================
// Wiki.js
// 1.0.0
// Licensed under AGPLv3
// ===========================================
const path = require('path')
const ROOTPATH = process.cwd()
const SERVERPATH = path.join(ROOTPATH, 'server')
global.ROOTPATH = ROOTPATH
global.SERVERPATH = SERVERPATH
const IS_DEBUG = process.env.NODE_ENV === 'development'
const cluster = require('cluster')
let wiki = {
IS_DEBUG: process.env.NODE_ENV === 'development',
IS_MASTER: cluster.isMaster,
ROOTPATH: process.cwd(),
SERVERPATH: path.join(process.cwd(), 'server'),
configSvc: require('./modules/config'),
kernel: require('./modules/kernel')
}
global.wiki = wiki
process.env.VIPS_WARNING = false
// if (IS_DEBUG) {
// if (wiki.IS_DEBUG) {
// require('@glimpse/glimpse').init()
// }
let appconf = require('./libs/config')()
global.appconfig = appconf.config
global.appdata = appconf.data
// ----------------------------------------
// Load Winston
// ----------------------------------------
global.winston = require('./libs/logger')(IS_DEBUG, 'SERVER')
global.winston.info('Wiki.js is initializing...')
// ----------------------------------------
// Load global modules
// ----------------------------------------
global.lcdata = require('./libs/local').init()
global.db = require('./libs/db').init()
global.entries = require('./libs/entries').init()
global.git = require('./libs/git').init(false)
global.lang = require('i18next')
global.mark = require('./libs/markdown')
global.search = require('./libs/search').init()
global.upl = require('./libs/uploads').init()
// ----------------------------------------
// Load modules
// ----------------------------------------
const autoload = require('auto-load')
const bodyParser = require('body-parser')
const compression = require('compression')
const cookieParser = require('cookie-parser')
const express = require('express')
const favicon = require('serve-favicon')
const flash = require('connect-flash')
const fork = require('child_process').fork
const http = require('http')
const i18nBackend = require('i18next-node-fs-backend')
const passport = require('passport')
const passportSocketIo = require('passport.socketio')
const session = require('express-session')
const SessionMongoStore = require('connect-mongo')(session)
const graceful = require('node-graceful')
const socketio = require('socket.io')
var mw = autoload(path.join(SERVERPATH, '/middlewares'))
var ctrl = autoload(path.join(SERVERPATH, '/controllers'))
// ----------------------------------------
// Define Express App
// ----------------------------------------
const app = express()
global.app = app
app.use(compression())
// ----------------------------------------
// Security
// ----------------------------------------
app.use(mw.security)
// ----------------------------------------
// Public Assets
// ----------------------------------------
app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(ROOTPATH, 'assets'), {
index: false,
maxAge: '7d'
}))
wiki.configSvc.init()
// ----------------------------------------
// Passport Authentication
// Init Logger
// ----------------------------------------
require('./libs/auth')(passport)
global.rights = require('./libs/rights')
global.rights.init()
wiki.logger = require('./modules/logger').init()
let sessionStore = new SessionMongoStore({
mongooseConnection: global.db.connection,
touchAfter: 15
})
app.use(cookieParser())
app.use(session({
name: 'wikijs.sid',
store: sessionStore,
secret: appconfig.sessionSecret,
resave: false,
saveUninitialized: false
}))
app.use(flash())
app.use(passport.initialize())
app.use(passport.session())
// ----------------------------------------
// SEO
// ----------------------------------------
app.use(mw.seo)
// ----------------------------------------
// Localization Engine
// ----------------------------------------
global.lang
.use(i18nBackend)
.init({
load: 'languageOnly',
ns: ['common', 'admin', 'auth', 'errors', 'git'],
defaultNS: 'common',
saveMissing: false,
preload: [appconfig.lang],
lng: appconfig.lang,
fallbackLng: 'en',
backend: {
loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
}
})
// Init DB
// ----------------------------------------
// View Engine Setup
// ----------------------------------------
app.set('views', path.join(SERVERPATH, 'views'))
app.set('view engine', 'pug')
app.use(bodyParser.json({ limit: '1mb' }))
app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))
// ----------------------------------------
// View accessible data
// ----------------------------------------
app.locals._ = require('lodash')
app.locals.t = global.lang.t.bind(global.lang)
app.locals.moment = require('moment')
app.locals.moment.locale(appconfig.lang)
app.locals.appconfig = appconfig
app.use(mw.flash)
// ----------------------------------------
// Controllers
// ----------------------------------------
app.use('/', ctrl.auth)
app.use('/uploads', mw.auth, ctrl.uploads)
app.use('/admin', mw.auth, ctrl.admin)
app.use('/', mw.auth, ctrl.pages)
// ----------------------------------------
// Error handling
// ----------------------------------------
app.use(function (req, res, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function (err, req, res, next) {
res.status(err.status || 500)
res.render('error', {
message: err.message,
error: IS_DEBUG ? err : {}
})
})
// ----------------------------------------
// Start HTTP server
// ----------------------------------------
global.winston.info('Starting HTTP/WS server on port ' + appconfig.port + '...')
app.set('port', appconfig.port)
var server = http.createServer(app)
var io = socketio(server)
server.listen(appconfig.port)
server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
global.winston.error('Listening on port ' + appconfig.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
global.winston.error('Port ' + appconfig.port + ' is already in use!')
return process.exit(1)
default:
throw error
}
})
server.on('listening', () => {
global.winston.info('HTTP/WS server started successfully! [RUNNING]')
})
// ----------------------------------------
// WebSocket
// ----------------------------------------
io.use(passportSocketIo.authorize({
key: 'wikijs.sid',
store: sessionStore,
secret: appconfig.sessionSecret,
cookieParser,
success: (data, accept) => {
accept()
},
fail: (data, message, error, accept) => {
accept()
}
}))
io.on('connection', ctrl.ws)
// ----------------------------------------
// Start child processes
// ----------------------------------------
let bgAgent = fork(path.join(SERVERPATH, 'agent.js'))
bgAgent.on('message', m => {
if (!m.action) {
return
}
switch (m.action) {
case 'searchAdd':
global.search.add(m.content)
break
}
})
wiki.db = require('./modules/db').init()
// ----------------------------------------
// Graceful shutdown
// Start Kernel
// ----------------------------------------
graceful.on('exit', () => {
global.winston.info('- SHUTTING DOWN - Terminating Background Agent...')
bgAgent.kill()
global.winston.info('- SHUTTING DOWN - Performing git sync...')
return global.git.resync().then(() => {
global.winston.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.')
process.exit()
})
})
wiki.kernel.init()

@ -1,90 +0,0 @@
'use strict'
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const pm2 = Promise.promisifyAll(require('pm2'))
const ora = require('ora')
const path = require('path')
const ROOTPATH = process.cwd()
module.exports = {
/**
* Detect the most appropriate start mode
*/
startDetect: function () {
if (process.env.WIKI_JS_HEROKU) {
return this.startInHerokuMode()
} else {
return this.startInBackgroundMode()
}
},
/**
* Start in background mode
*/
startInBackgroundMode: function () {
let spinner = ora('Initializing...').start()
return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => {
return pm2.connectAsync().then(() => {
return pm2.startAsync({
name: 'wiki',
script: 'server',
cwd: ROOTPATH,
output: path.join(ROOTPATH, './logs/wiki-output.log'),
error: path.join(ROOTPATH, './logs/wiki-error.log'),
minUptime: 5000,
maxRestarts: 5
}).then(() => {
spinner.succeed('Wiki.js has started successfully.')
}).finally(() => {
pm2.disconnect()
})
})
}).catch(err => {
spinner.fail(err)
process.exit(1)
})
},
/**
* Start in Heroku mode
*/
startInHerokuMode: function () {
console.warn('Incorrect command on Heroku, use instead: node server')
process.exit(1)
},
/**
* Stop Wiki.js process(es)
*/
stop () {
let spinner = ora('Shutting down Wiki.js...').start()
return pm2.connectAsync().then(() => {
return pm2.stopAsync('wiki').then(() => {
spinner.succeed('Wiki.js has stopped successfully.')
}).finally(() => {
pm2.disconnect()
})
}).catch(err => {
spinner.fail(err)
process.exit(1)
})
},
/**
* Restart Wiki.js process(es)
*/
restart: function () {
let self = this
return self.stop().delay(1000).then(() => {
self.startDetect()
})
},
/**
* Start the web-based configuration wizard
*
* @param {Number} port Port to bind the HTTP server on
*/
configure (port) {
port = port || 3000
let spinner = ora('Initializing interactive setup...').start()
require('./configure')(port, spinner)
}
}

@ -1,261 +0,0 @@
'use strict'
/* global appconfig, appdata, db, lang, winston */
const fs = require('fs')
module.exports = function (passport) {
// Serialization user methods
passport.serializeUser(function (user, done) {
done(null, user._id)
})
passport.deserializeUser(function (id, done) {
db.User.findById(id).then((user) => {
if (user) {
done(null, user)
} else {
done(new Error(lang.t('auth:errors:usernotfound')), null)
}
return true
}).catch((err) => {
done(err, null)
})
})
// Local Account
if (appconfig.auth.local && appconfig.auth.local.enabled) {
const LocalStrategy = require('passport-local').Strategy
passport.use('local',
new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)
})
} else {
return done(new Error('INVALID_LOGIN'), null)
}
}).catch((err) => {
done(err, null)
})
}
))
}
// Google ID
if (appconfig.auth.google && appconfig.auth.google.enabled) {
const GoogleStrategy = require('passport-google-oauth20').Strategy
passport.use('google',
new GoogleStrategy({
clientID: appconfig.auth.google.clientId,
clientSecret: appconfig.auth.google.clientSecret,
callbackURL: appconfig.host + '/login/google/callback'
}, (accessToken, refreshToken, profile, cb) => {
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// Microsoft Accounts
if (appconfig.auth.microsoft && appconfig.auth.microsoft.enabled) {
const WindowsLiveStrategy = require('passport-windowslive').Strategy
passport.use('windowslive',
new WindowsLiveStrategy({
clientID: appconfig.auth.microsoft.clientId,
clientSecret: appconfig.auth.microsoft.clientSecret,
callbackURL: appconfig.host + '/login/ms/callback'
}, function (accessToken, refreshToken, profile, cb) {
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// Facebook
if (appconfig.auth.facebook && appconfig.auth.facebook.enabled) {
const FacebookStrategy = require('passport-facebook').Strategy
passport.use('facebook',
new FacebookStrategy({
clientID: appconfig.auth.facebook.clientId,
clientSecret: appconfig.auth.facebook.clientSecret,
callbackURL: appconfig.host + '/login/facebook/callback',
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// GitHub
if (appconfig.auth.github && appconfig.auth.github.enabled) {
const GitHubStrategy = require('passport-github2').Strategy
passport.use('github',
new GitHubStrategy({
clientID: appconfig.auth.github.clientId,
clientSecret: appconfig.auth.github.clientSecret,
callbackURL: appconfig.host + '/login/github/callback',
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// Slack
if (appconfig.auth.slack && appconfig.auth.slack.enabled) {
const SlackStrategy = require('passport-slack').Strategy
passport.use('slack',
new SlackStrategy({
clientID: appconfig.auth.slack.clientId,
clientSecret: appconfig.auth.slack.clientSecret,
callbackURL: appconfig.host + '/login/slack/callback'
}, (accessToken, refreshToken, profile, cb) => {
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// LDAP
if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) {
const LdapStrategy = require('passport-ldapauth').Strategy
passport.use('ldapauth',
new LdapStrategy({
server: {
url: appconfig.auth.ldap.url,
bindDn: appconfig.auth.ldap.bindDn,
bindCredentials: appconfig.auth.ldap.bindCredentials,
searchBase: appconfig.auth.ldap.searchBase,
searchFilter: appconfig.auth.ldap.searchFilter,
searchAttributes: ['displayName', 'name', 'cn', 'mail'],
tlsOptions: (appconfig.auth.ldap.tlsEnabled) ? {
ca: [
fs.readFileSync(appconfig.auth.ldap.tlsCertPath)
]
} : {}
},
usernameField: 'email',
passReqToCallback: false
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// AZURE AD
if (appconfig.auth.azure && appconfig.auth.azure.enabled) {
const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
const jwt = require('jsonwebtoken')
passport.use('azure_ad_oauth2',
new AzureAdOAuth2Strategy({
clientID: appconfig.auth.azure.clientId,
clientSecret: appconfig.auth.azure.clientSecret,
callbackURL: appconfig.host + '/login/azure/callback',
resource: appconfig.auth.azure.resource,
tenant: appconfig.auth.azure.tenant
}, (accessToken, refreshToken, params, profile, cb) => {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
db.User.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
// Create users for first-time
db.onReady.then(() => {
return db.User.findOne({ provider: 'local', email: 'guest' }).then((c) => {
if (c < 1) {
// Create guest account
return db.User.create({
provider: 'local',
email: 'guest',
name: 'Guest',
password: '',
rights: [{
role: 'read',
path: '/',
exact: false,
deny: !appconfig.public
}]
}).then(() => {
winston.info('[AUTH] Guest account created successfully!')
}).catch((err) => {
winston.error('[AUTH] An error occured while creating guest account:')
winston.error(err)
})
}
}).then(() => {
if (process.env.WIKI_JS_HEROKU) {
return db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
if (c < 1) {
// Create root admin account (HEROKU ONLY)
return db.User.create({
provider: 'local',
email: process.env.WIKI_ADMIN_EMAIL,
name: 'Administrator',
password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
rights: [{
role: 'admin',
path: '/',
exact: false,
deny: false
}]
}).then(() => {
winston.info('[AUTH] Root admin account created successfully!')
}).catch((err) => {
winston.error('[AUTH] An error occured while creating root admin account:')
winston.error(err)
})
} else { return true }
})
} else { return true }
})
})
}

@ -1,68 +0,0 @@
'use strict'
const fs = require('fs')
const yaml = require('js-yaml')
const _ = require('lodash')
const path = require('path')
const cfgHelper = require('../helpers/config')
/**
* Load Application Configuration
*
* @param {Object} confPaths Path to the configuration files
* @return {Object} Application Configuration
*/
module.exports = (confPaths) => {
confPaths = _.defaults(confPaths, {
config: path.join(ROOTPATH, 'config.yml'),
data: path.join(SERVERPATH, 'app/data.yml'),
dataRegex: path.join(SERVERPATH, 'app/regex.js')
})
let appconfig = {}
let appdata = {}
try {
appconfig = yaml.safeLoad(
cfgHelper.parseConfigValue(
fs.readFileSync(confPaths.config, 'utf8')
)
)
appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))
appdata.regex = require(confPaths.dataRegex)
} catch (ex) {
console.error(ex)
process.exit(1)
}
// Merge with defaults
appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
// Check port
if (appconfig.port < 1) {
appconfig.port = process.env.PORT || 80
}
// Convert booleans
appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
// List authentication strategies
appconfig.authStrategies = {
list: _.filter(appconfig.auth, ['enabled', true]),
socialEnabled: (_.chain(appconfig.auth).omit(['local', 'ldap']).filter(['enabled', true]).value().length > 0)
}
if (appconfig.authStrategies.list.length < 1) {
console.error(new Error('You must enable at least 1 authentication strategy!'))
process.exit(1)
}
return {
config: appconfig,
data: appdata
}
}

@ -1,63 +0,0 @@
'use strict'
/* global ROOTPATH, appconfig, winston */
const modb = require('mongoose')
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
/**
* MongoDB module
*
* @return {Object} MongoDB wrapper instance
*/
module.exports = {
/**
* Initialize DB
*
* @return {Object} DB instance
*/
init() {
let self = this
let dbModelsPath = path.join(SERVERPATH, 'models')
modb.Promise = require('bluebird')
// Event handlers
modb.connection.on('error', err => {
winston.error('Failed to connect to MongoDB instance.')
return err
})
modb.connection.once('open', function () {
winston.log('Connected to MongoDB instance.')
})
// Store connection handle
self.connection = modb.connection
self.ObjectId = modb.Types.ObjectId
// Load DB Models
fs
.readdirSync(dbModelsPath)
.filter(function (file) {
return (file.indexOf('.') !== 0)
})
.forEach(function (file) {
let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
self[modelName] = require(path.join(dbModelsPath, file))
})
// Connect
self.onReady = modb.connect(appconfig.db, { useMongoClient: true })
return self
}
}

@ -1,77 +0,0 @@
'use strict'
module.exports = (isDebug, processName) => {
let winston = require('winston')
if (typeof processName === 'undefined') {
processName = 'SERVER'
}
// Console
let logger = new (winston.Logger)({
level: (isDebug) ? 'debug' : 'info',
transports: [
new (winston.transports.Console)({
level: (isDebug) ? 'debug' : 'info',
prettyPrint: true,
colorize: true,
silent: false,
timestamp: true
})
]
})
logger.filters.push((level, msg) => {
return '[' + processName + '] ' + msg
})
// External services
if (appconfig.externalLogging.bugsnag) {
const bugsnagTransport = require('./winston-transports/bugsnag')
logger.add(bugsnagTransport, {
level: 'warn',
key: appconfig.externalLogging.bugsnag
})
}
if (appconfig.externalLogging.loggly) {
require('winston-loggly-bulk')
logger.add(winston.transports.Loggly, {
token: appconfig.externalLogging.loggly.token,
subdomain: appconfig.externalLogging.loggly.subdomain,
tags: ['wiki-js'],
level: 'warn',
json: true
})
}
if (appconfig.externalLogging.papertrail) {
require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
logger.add(winston.transports.Papertrail, {
host: appconfig.externalLogging.papertrail.host,
port: appconfig.externalLogging.papertrail.port,
level: 'warn',
program: 'wiki.js'
})
}
if (appconfig.externalLogging.rollbar) {
const rollbarTransport = require('./winston-transports/rollbar')
logger.add(rollbarTransport, {
level: 'warn',
key: appconfig.externalLogging.rollbar
})
}
if (appconfig.externalLogging.sentry) {
const sentryTransport = require('./winston-transports/sentry')
logger.add(sentryTransport, {
level: 'warn',
key: appconfig.externalLogging.sentry
})
}
return logger
}

@ -1,81 +0,0 @@
const bunyan = require('bunyan')
const level = require('levelup')
const down = require('memdown')
const SearchIndexAdder = require('search-index-adder')
const SearchIndexSearcher = require('search-index-searcher')
module.exports = function (givenOptions, moduleReady) {
const optionsLoaded = function (err, SearchIndex) {
const siUtil = require('./siUtil.js')(SearchIndex.options)
if (err) return moduleReady(err)
SearchIndex.close = siUtil.close
SearchIndex.countDocs = siUtil.countDocs
getAdder(SearchIndex, adderLoaded)
}
const adderLoaded = function (err, SearchIndex) {
if (err) return moduleReady(err)
getSearcher(SearchIndex, searcherLoaded)
}
const searcherLoaded = function (err, SearchIndex) {
if (err) return moduleReady(err)
return moduleReady(err, SearchIndex)
}
getOptions(givenOptions, optionsLoaded)
}
const getAdder = function (SearchIndex, done) {
SearchIndexAdder(SearchIndex.options, function (err, searchIndexAdder) {
SearchIndex.add = searchIndexAdder.add
SearchIndex.callbackyAdd = searchIndexAdder.concurrentAdd // deprecated
SearchIndex.concurrentAdd = searchIndexAdder.concurrentAdd
SearchIndex.createWriteStream = searchIndexAdder.createWriteStream
SearchIndex.dbWriteStream = searchIndexAdder.dbWriteStream
SearchIndex.defaultPipeline = searchIndexAdder.defaultPipeline
SearchIndex.del = searchIndexAdder.deleter
SearchIndex.deleteStream = searchIndexAdder.deleteStream
SearchIndex.flush = searchIndexAdder.flush
done(err, SearchIndex)
})
}
const getSearcher = function (SearchIndex, done) {
SearchIndexSearcher(SearchIndex.options, function (err, searchIndexSearcher) {
SearchIndex.availableFields = searchIndexSearcher.availableFields
SearchIndex.buckets = searchIndexSearcher.bucketStream
SearchIndex.categorize = searchIndexSearcher.categoryStream
SearchIndex.dbReadStream = searchIndexSearcher.dbReadStream
SearchIndex.get = searchIndexSearcher.get
SearchIndex.match = searchIndexSearcher.match
SearchIndex.scan = searchIndexSearcher.scan
SearchIndex.search = searchIndexSearcher.search
SearchIndex.totalHits = searchIndexSearcher.totalHits
done(err, SearchIndex)
})
}
const getOptions = function (options, done) {
var SearchIndex = {}
SearchIndex.options = Object.assign({}, {
indexPath: 'si',
keySeparator: '○',
logLevel: 'error'
}, options)
options.log = bunyan.createLogger({
name: 'search-index',
level: options.logLevel
})
if (!options.indexes) {
level(SearchIndex.options.indexPath || 'si', {
valueEncoding: 'json',
db: down
}, function (err, db) {
SearchIndex.options.indexes = db
return done(err, SearchIndex)
})
} else {
return done(null, SearchIndex)
}
}

@ -1,36 +0,0 @@
'use strict'
module.exports = function (siOptions) {
var siUtil = {}
siUtil.countDocs = function (callback) {
var count = 0
const gte = 'DOCUMENT' + siOptions.keySeparator
const lte = 'DOCUMENT' + siOptions.keySeparator + siOptions.keySeparator
siOptions.indexes.createReadStream({gte: gte, lte: lte})
.on('data', function (data) {
count++
})
.on('error', function (err) {
return callback(err, null)
})
.on('end', function () {
return callback(null, count)
})
}
siUtil.close = function (callback) {
siOptions.indexes.close(function (err) {
while (!siOptions.indexes.isClosed()) {
// log not always working here- investigate
if (siOptions.log) siOptions.log.info('closing...')
}
if (siOptions.indexes.isClosed()) {
if (siOptions.log) siOptions.log.info('closed...')
callback(err)
}
})
}
return siUtil
}

@ -13,7 +13,7 @@
"invalidlogin": "不正なログイン",
"invalidloginmsg": "Eメール又はパスワードが無効です。",
"invaliduseremail": "無効なユーザーEメール",
"lognerror": "ログインエラー",
"loginerror": "ログインエラー",
"notyetauthorized": "まだこのサイトにログインする権限がありません。",
"toomanyattempts": "試行回数が多すぎます",
"toomanyattemptsmsg": "短期間に失敗した試行回数が多すぎます。{{time}}にもう一度お試しください。",

@ -0,0 +1,186 @@
/* global wiki */
module.exports = () => {
// ----------------------------------------
// Load global modules
// ----------------------------------------
wiki.auth = require('./modules/auth').init()
wiki.disk = require('./modules/disk').init()
wiki.docs = require('./modules/documents').init()
wiki.git = require('./modules/git').init(false)
wiki.lang = require('./modules/localization').init()
wiki.mark = require('./modules/markdown')
wiki.search = require('./modules/search').init()
wiki.upl = require('./modules/uploads').init()
// ----------------------------------------
// Load modules
// ----------------------------------------
const autoload = require('auto-load')
const bodyParser = require('body-parser')
const compression = require('compression')
const cookieParser = require('cookie-parser')
const express = require('express')
const favicon = require('serve-favicon')
const flash = require('connect-flash')
const http = require('http')
const path = require('path')
const session = require('express-session')
const SessionRedisStore = require('connect-redis')(session)
const graceful = require('node-graceful')
const graphqlApollo = require('apollo-server-express')
const graphqlSchema = require('./modules/graphql')
var mw = autoload(path.join(wiki.SERVERPATH, '/middlewares'))
var ctrl = autoload(path.join(wiki.SERVERPATH, '/controllers'))
// ----------------------------------------
// Define Express App
// ----------------------------------------
const app = express()
wiki.app = app
app.use(compression())
// ----------------------------------------
// Security
// ----------------------------------------
app.use(mw.security)
// ----------------------------------------
// Public Assets
// ----------------------------------------
app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(wiki.ROOTPATH, 'assets'), {
index: false,
maxAge: '7d'
}))
// ----------------------------------------
// Passport Authentication
// ----------------------------------------
let sessionStore = new SessionRedisStore({
client: wiki.redis
})
app.use(cookieParser())
app.use(session({
name: 'wikijs.sid',
store: sessionStore,
secret: wiki.config.site.sessionSecret,
resave: false,
saveUninitialized: false
}))
app.use(flash())
app.use(wiki.auth.passport.initialize())
app.use(wiki.auth.passport.session())
// ----------------------------------------
// SEO
// ----------------------------------------
app.use(mw.seo)
// ----------------------------------------
// View Engine Setup
// ----------------------------------------
app.set('views', path.join(wiki.SERVERPATH, 'views'))
app.set('view engine', 'pug')
app.use(bodyParser.json({ limit: '1mb' }))
app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))
// ----------------------------------------
// View accessible data
// ----------------------------------------
app.locals.basedir = wiki.ROOTPATH
app.locals._ = require('lodash')
app.locals.t = wiki.lang.engine.t.bind(wiki.lang)
app.locals.moment = require('moment')
app.locals.moment.locale(wiki.config.site.lang)
app.locals.config = wiki.config
app.use(mw.flash)
// ----------------------------------------
// Controllers
// ----------------------------------------
app.use('/', ctrl.auth)
app.use('/graphql', graphqlApollo.graphqlExpress({ schema: graphqlSchema }))
app.use('/graphiql', graphqlApollo.graphiqlExpress({ endpointURL: '/graphql' }))
// app.use('/uploads', mw.auth, ctrl.uploads)
app.use('/admin', mw.auth, ctrl.admin)
app.use('/', mw.auth, ctrl.pages)
// ----------------------------------------
// Error handling
// ----------------------------------------
app.use(function (req, res, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function (err, req, res, next) {
res.status(err.status || 500)
res.render('error', {
message: err.message,
error: wiki.IS_DEBUG ? err : {}
})
})
// ----------------------------------------
// Start HTTP server
// ----------------------------------------
wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`)
app.set('port', wiki.config.port)
let server = http.createServer(app)
server.listen(wiki.config.port)
server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!')
return process.exit(1)
case 'EADDRINUSE':
wiki.logger.error('Port ' + wiki.config.port + ' is already in use!')
return process.exit(1)
default:
throw error
}
})
server.on('listening', () => {
wiki.logger.info('HTTP Server: RUNNING')
})
// ----------------------------------------
// Graceful shutdown
// ----------------------------------------
graceful.on('exit', () => {
wiki.logger.info('- SHUTTING DOWN - Performing git sync...')
return global.git.resync().then(() => {
wiki.logger.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.')
process.exit()
})
})
return true
}

@ -9,7 +9,7 @@
* @return {any} void
*/
module.exports = (req, res, next) => {
res.locals.appflash = req.flash('alert')
res.locals.flash = req.flash('alert')
next()
}

@ -1,7 +1,5 @@
'use strict'
/* global app */
/**
* Security Middleware
*
@ -12,7 +10,7 @@
*/
module.exports = function (req, res, next) {
// -> Disable X-Powered-By
app.disable('x-powered-by')
req.app.disable('x-powered-by')
// -> Disable Frame Embedding
res.set('X-Frame-Options', 'deny')

@ -0,0 +1,16 @@
/**
* Associate DB Model relations
*/
module.exports = db => {
db.User.belongsToMany(db.Group, { through: 'userGroups' })
db.Group.belongsToMany(db.User, { through: 'userGroups' })
db.Group.hasMany(db.Right)
db.Right.belongsTo(db.Group)
db.Document.belongsToMany(db.Tag, { through: 'documentTags' })
db.Document.hasMany(db.Comment)
db.Tag.belongsToMany(db.Document, { through: 'documentTags' })
db.File.belongsTo(db.Folder)
db.Folder.hasMany(db.File)
db.Comment.belongsTo(db.Document)
db.Comment.belongsTo(db.User, { as: 'author' })
}

@ -1,20 +0,0 @@
'use strict'
const Mongoose = require('mongoose')
/**
* BruteForce schema
*
* @type {<Mongoose.Schema>}
*/
var bruteForceSchema = Mongoose.Schema({
_id: { type: String, index: 1 },
data: {
count: Number,
lastRequest: Date,
firstRequest: Date
},
expires: { type: Date, index: { expires: '1d' } }
})
module.exports = Mongoose.model('Bruteforce', bruteForceSchema)

@ -0,0 +1,16 @@
/**
* Comment schema
*/
module.exports = (sequelize, DataTypes) => {
let commentSchema = sequelize.define('comment', {
content: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true,
version: true
})
return commentSchema
}

@ -0,0 +1,64 @@
/**
* Document schema
*/
module.exports = (sequelize, DataTypes) => {
let documentSchema = sequelize.define('setting', {
path: {
type: DataTypes.STRING,
allowNull: false
},
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 255]
}
},
subtitle: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: ''
},
parentPath: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: ''
},
parentTitle: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: ''
},
isDirectory: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
isEntry: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
isDraft: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
searchContent: {
type: DataTypes.TEXT,
allowNull: true,
defaultValue: ''
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['path']
}
]
})
return documentSchema
}

@ -1,42 +0,0 @@
'use strict'
const Mongoose = require('mongoose')
/**
* Entry schema
*
* @type {<Mongoose.Schema>}
*/
var entrySchema = Mongoose.Schema({
_id: String,
title: {
type: String,
required: true,
minlength: 2
},
subtitle: {
type: String,
default: ''
},
parentTitle: {
type: String,
default: ''
},
parentPath: {
type: String,
default: ''
},
isDirectory: {
type: Boolean,
default: false
},
isEntry: {
type: Boolean,
default: false
}
}, {
timestamps: {}
})
module.exports = Mongoose.model('Entry', entrySchema)

@ -0,0 +1,42 @@
/**
* File schema
*/
module.exports = (sequelize, DataTypes) => {
let fileSchema = sequelize.define('file', {
category: {
type: DataTypes.ENUM('binary', 'image'),
allowNull: false,
defaultValue: 'binary'
},
mime: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'application/octet-stream'
},
extra: {
type: DataTypes.JSONB,
allowNull: true
},
filename: {
type: DataTypes.STRING,
allowNull: false
},
basename: {
type: DataTypes.STRING,
allowNull: false
},
filesize: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
isInt: true,
min: 0
}
}
}, {
timestamps: true,
version: true
})
return fileSchema
}

@ -0,0 +1,22 @@
/**
* Folder schema
*/
module.exports = (sequelize, DataTypes) => {
let folderSchema = sequelize.define('folder', {
name: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['name']
}
]
})
return folderSchema
}

@ -0,0 +1,16 @@
/**
* Group schema
*/
module.exports = (sequelize, DataTypes) => {
let groupSchema = sequelize.define('group', {
name: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true,
version: true
})
return groupSchema
}

@ -0,0 +1,36 @@
/**
* Right schema
*/
module.exports = (sequelize, DataTypes) => {
let rightSchema = sequelize.define('right', {
path: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.ENUM('read', 'write', 'manage'),
allowNull: false,
defaultValue: 'read'
},
exact: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
allow: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
}
}, {
timestamps: true,
version: true,
indexes: [
{
fields: ['path']
}
]
})
return rightSchema
}

@ -1,22 +1,26 @@
'use strict'
const Mongoose = require('mongoose')
/**
* Settings schema
*
* @type {<Mongoose.Schema>}
*/
var settingSchema = Mongoose.Schema({
key: {
type: String,
required: true,
index: true
},
value: {
type: String,
required: true
}
}, { timestamps: {} })
module.exports = (sequelize, DataTypes) => {
let settingSchema = sequelize.define('setting', {
key: {
type: DataTypes.STRING,
allowNull: false
},
config: {
type: DataTypes.JSONB,
allowNull: false
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['key']
}
]
})
module.exports = Mongoose.model('Setting', settingSchema)
return settingSchema
}

@ -0,0 +1,22 @@
/**
* Tags schema
*/
module.exports = (sequelize, DataTypes) => {
let tagSchema = sequelize.define('tag', {
key: {
type: DataTypes.STRING,
allowNull: false
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['key']
}
]
})
return tagSchema
}

@ -1,46 +0,0 @@
'use strict'
const Mongoose = require('mongoose')
/**
* Upload File schema
*
* @type {<Mongoose.Schema>}
*/
var uplFileSchema = Mongoose.Schema({
_id: String,
category: {
type: String,
required: true,
default: 'binary'
},
mime: {
type: String,
required: true,
default: 'application/octet-stream'
},
extra: {
type: Object
},
folder: {
type: String,
ref: 'UplFolder'
},
filename: {
type: String,
required: true
},
basename: {
type: String,
required: true
},
filesize: {
type: Number,
required: true
}
}, { timestamps: {} })
module.exports = Mongoose.model('UplFile', uplFileSchema)

@ -1,21 +0,0 @@
'use strict'
const Mongoose = require('mongoose')
/**
* Upload Folder schema
*
* @type {<Mongoose.Schema>}
*/
var uplFolderSchema = Mongoose.Schema({
_id: String,
name: {
type: String,
index: true
}
}, { timestamps: {} })
module.exports = Mongoose.model('UplFolder', uplFolderSchema)

@ -1,109 +1,120 @@
'use strict'
/* global wiki */
/* global db, lang */
const Mongoose = require('mongoose')
const Promise = require('bluebird')
const bcrypt = require('bcryptjs-then')
const _ = require('lodash')
/**
* Users schema
*
* @type {<Mongoose.Schema>}
*/
var userSchema = Mongoose.Schema({
email: {
type: String,
required: true,
index: true
},
provider: {
type: String,
required: true
},
providerId: {
type: String
},
password: {
type: String
},
name: {
type: String
},
rights: [{
role: String,
path: String,
exact: Boolean,
deny: Boolean
}]
}, { timestamps: {} })
module.exports = (sequelize, DataTypes) => {
let userSchema = sequelize.define('user', {
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
provider: {
type: DataTypes.STRING,
allowNull: false
},
providerId: {
type: DataTypes.STRING,
allowNull: true
},
password: {
type: DataTypes.STRING,
allowNull: true
},
name: {
type: DataTypes.STRING,
allowNull: true
},
role: {
type: DataTypes.ENUM('admin', 'user', 'guest'),
allowNull: false
},
tfaIsActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
tfaSecret: {
type: DataTypes.STRING,
allowNull: true
}
}, {
timestamps: true,
version: true,
indexes: [
{
unique: true,
fields: ['provider', 'email']
}
]
})
userSchema.statics.processProfile = (profile) => {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
let e = _.find(profile.emails, ['primary', true])
primaryEmail = (e) ? e.value : _.first(profile.emails).value
} else if (_.isString(profile.email) && profile.email.length > 5) {
primaryEmail = profile.email
} else if (_.isString(profile.mail) && profile.mail.length > 5) {
primaryEmail = profile.mail
} else if (profile.user && profile.user.email && profile.user.email.length > 5) {
primaryEmail = profile.user.email
} else {
return Promise.reject(new Error(lang.t('auth:errors.invaliduseremail')))
userSchema.prototype.validatePassword = function (rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
return (isValid) ? true : Promise.reject(new Error(wiki.lang.t('auth:errors:invalidlogin')))
})
}
profile.provider = _.lowerCase(profile.provider)
primaryEmail = _.toLower(primaryEmail)
userSchema.processProfile = (profile) => {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
let e = _.find(profile.emails, ['primary', true])
primaryEmail = (e) ? e.value : _.first(profile.emails).value
} else if (_.isString(profile.email) && profile.email.length > 5) {
primaryEmail = profile.email
} else if (_.isString(profile.mail) && profile.mail.length > 5) {
primaryEmail = profile.mail
} else if (profile.user && profile.user.email && profile.user.email.length > 5) {
primaryEmail = profile.user.email
} else {
return Promise.reject(new Error(wiki.lang.t('auth:errors.invaliduseremail')))
}
return db.User.findOneAndUpdate({
email: primaryEmail,
provider: profile.provider
}, {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || profile.cn || _.split(primaryEmail, '@')[0]
}, {
new: true
}).then((user) => {
// Handle unregistered accounts
if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
let nUsr = {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
password: '',
name: profile.displayName || profile.name || profile.cn,
rights: [{
role: 'read',
path: '/',
exact: false,
deny: false
}]
profile.provider = _.lowerCase(profile.provider)
primaryEmail = _.toLower(primaryEmail)
return wiki.db.User.findOneAndUpdate({
email: primaryEmail,
provider: profile.provider
}, {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
}, {
new: true
}).then((user) => {
// Handle unregistered accounts
if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
let nUsr = {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
password: '',
name: profile.displayName || profile.name || profile.cn,
rights: [{
role: 'read',
path: '/',
exact: false,
deny: false
}]
}
return wiki.db.User.create(nUsr)
}
return db.User.create(nUsr)
}
return user || Promise.reject(new Error(lang.t('auth:errors:notyetauthorized')))
})
}
return user || Promise.reject(new Error(wiki.lang.t('auth:errors:notyetauthorized')))
})
}
userSchema.statics.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd)
}
userSchema.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd)
}
userSchema.methods.validatePassword = function (rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
return (isValid) ? true : Promise.reject(new Error(lang.t('auth:errors:invalidlogin')))
})
return userSchema
}
module.exports = Mongoose.model('User', userSchema)

@ -0,0 +1,106 @@
/* global wiki */
const _ = require('lodash')
const passport = require('passport')
const fs = require('fs-extra')
const path = require('path')
module.exports = {
strategies: {},
init() {
this.passport = passport
// Serialization user methods
passport.serializeUser(function (user, done) {
done(null, user._id)
})
passport.deserializeUser(function (id, done) {
wiki.db.User.findById(id).then((user) => {
if (user) {
done(null, user)
} else {
done(new Error(wiki.lang.t('auth:errors:usernotfound')), null)
}
return true
}).catch((err) => {
done(err, null)
})
})
// Load authentication strategies
wiki.config.auth.strategies.local = {}
_.forOwn(wiki.config.auth.strategies, (strategyConfig, strategyKey) => {
strategyConfig.callbackURL = `${wiki.config.site.host}${wiki.config.site.path}/login/${strategyKey}/callback`
let strategy = require(`../authentication/${strategyKey}`)
strategy.init(passport, strategyConfig)
fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${strategyKey}.svg`), 'utf8').then(iconData => {
strategy.icon = iconData
}).catch(err => {
if (err.code === 'ENOENT') {
strategy.icon = '[missing icon]'
} else {
wiki.logger.error(err)
}
})
this.strategies[strategy.key] = strategy
wiki.logger.info(`Authentication Provider ${strategyKey}: OK`)
})
// Create Guest account for first-time
wiki.db.User.findOne({
where: {
provider: 'local',
email: 'guest@example.com'
}
}).then((c) => {
if (c < 1) {
return wiki.db.User.create({
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
role: 'guest'
}).then(() => {
wiki.logger.info('[AUTH] Guest account created successfully!')
return true
}).catch((err) => {
wiki.logger.error('[AUTH] An error occured while creating guest account:')
wiki.logger.error(err)
return err
})
}
})
// .then(() => {
// if (process.env.WIKI_JS_HEROKU) {
// return wiki.db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
// if (c < 1) {
// // Create root admin account (HEROKU ONLY)
// return wiki.db.User.create({
// provider: 'local',
// email: process.env.WIKI_ADMIN_EMAIL,
// name: 'Administrator',
// password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
// role: 'admin'
// }).then(() => {
// wiki.logger.info('[AUTH] Root admin account created successfully!')
// return true
// }).catch((err) => {
// wiki.logger.error('[AUTH] An error occured while creating root admin account:')
// wiki.logger.error(err)
// return err
// })
// } else { return true }
// })
// } else { return true }
// })
return this
}
}

@ -0,0 +1,85 @@
/* global wiki */
const fs = require('fs')
const yaml = require('js-yaml')
const _ = require('lodash')
const path = require('path')
const cfgHelper = require('../helpers/config')
module.exports = {
/**
* Load root config from disk
*/
init() {
let confPaths = {
config: path.join(wiki.ROOTPATH, 'config.yml'),
data: path.join(wiki.SERVERPATH, 'app/data.yml'),
dataRegex: path.join(wiki.SERVERPATH, 'app/regex.js')
}
let appconfig = {}
let appdata = {}
try {
appconfig = yaml.safeLoad(
cfgHelper.parseConfigValue(
fs.readFileSync(confPaths.config, 'utf8')
)
)
appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))
appdata.regex = require(confPaths.dataRegex)
} catch (ex) {
console.error(ex)
process.exit(1)
}
// Merge with defaults
appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
// Check port
if (appconfig.port < 1) {
appconfig.port = process.env.PORT || 80
}
// Convert booleans
appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
// List authentication strategies
wiki.config = appconfig
wiki.data = appdata
},
/**
* Load config from DB
*
* @param {Array} subsets Array of subsets to load
* @returns Promise
*/
loadFromDb(subsets) {
if (!_.isArray(subsets) || subsets.length === 0) {
subsets = wiki.data.configNamespaces
}
return wiki.db.Setting.findAll({
attributes: ['key', 'config'],
where: {
key: {
$in: subsets
}
}
}).then(results => {
if (_.isArray(results) && results.length === subsets.length) {
results.forEach(result => {
wiki.config[result.key] = result.config
})
return true
} else {
wiki.logger.warn('DB Configuration is empty or incomplete.')
return false
}
})
}
}

@ -0,0 +1,137 @@
'use strict'
/* global wiki */
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const Promise = require('bluebird')
const Sequelize = require('sequelize')
const Op = Sequelize.Op
const operatorsAliases = {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$not: Op.not,
$in: Op.in,
$notIn: Op.notIn,
$is: Op.is,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$regexp: Op.regexp,
$notRegexp: Op.notRegexp,
$iRegexp: Op.iRegexp,
$notIRegexp: Op.notIRegexp,
$between: Op.between,
$notBetween: Op.notBetween,
$overlap: Op.overlap,
$contains: Op.contains,
$contained: Op.contained,
$adjacent: Op.adjacent,
$strictLeft: Op.strictLeft,
$strictRight: Op.strictRight,
$noExtendRight: Op.noExtendRight,
$noExtendLeft: Op.noExtendLeft,
$and: Op.and,
$or: Op.or,
$any: Op.any,
$all: Op.all,
$values: Op.values,
$col: Op.col
}
/**
* PostgreSQL DB module
*/
module.exports = {
Sequelize,
Op: Sequelize.Op,
/**
* Initialize DB
*
* @return {Object} DB instance
*/
init() {
let self = this
let dbModelsPath = path.join(wiki.SERVERPATH, 'models')
// Define Sequelize instance
self.inst = new self.Sequelize(wiki.config.db.db, wiki.config.db.user, wiki.config.db.pass, {
host: wiki.config.db.host,
port: wiki.config.db.port,
dialect: 'postgres',
pool: {
max: 10,
min: 0,
idle: 10000
},
logging: log => { wiki.logger.log('verbose', log) },
operatorsAliases
})
// Attempt to connect and authenticate to DB
self.inst.authenticate().then(() => {
wiki.logger.info('Database (PostgreSQL) connection: OK')
}).catch(err => {
wiki.logger.error('Failed to connect to PostgreSQL instance.')
return err
})
// Load DB Models
fs
.readdirSync(dbModelsPath)
.filter(file => {
return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0)
})
.forEach(file => {
let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
self[modelName] = self.inst.import(path.join(dbModelsPath, file))
})
// Associate DB Models
require(path.join(dbModelsPath, '_relations.js'))(self)
// Set init tasks
let initTasks = {
// -> Sync DB Schemas
syncSchemas() {
return self.inst.sync({
force: false,
logging: log => { wiki.logger.log('verbose', log) }
})
},
// -> Set Connection App Name
setAppName() {
return self.inst.query(`set application_name = 'Wiki.js'`, { raw: true })
}
}
let initTasksQueue = (wiki.IS_MASTER) ? [
initTasks.syncSchemas,
initTasks.setAppName
] : [
initTasks.setAppName
]
// Perform init tasks
self.onReady = Promise.each(initTasksQueue, t => t()).return(true)
return self
}
}

@ -1,6 +1,6 @@
'use strict'
/* global lang, winston */
/* global wiki */
const path = require('path')
const Promise = require('bluebird')
@ -10,7 +10,7 @@ const os = require('os')
const _ = require('lodash')
/**
* Local Data Storage
* Local Disk Storage
*/
module.exports = {
@ -21,29 +21,26 @@ module.exports = {
/**
* Initialize Local Data Storage model
*
* @return {Object} Local Data Storage model instance
*/
init () {
this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
this.createBaseDirectories(appconfig)
this.initMulter(appconfig)
this.createBaseDirectories()
// this.initMulter()
return this
},
/**
* Init Multer upload handlers
*
* @param {Object} appconfig The application config
* @return {boolean} Void
*/
initMulter (appconfig) {
initMulter () {
let maxFileSizes = {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
// img: wiki.config.uploads.maxImageFileSize * 1024 * 1024,
// file: wiki.config.uploads.maxOtherFileSize * 1024 * 1024
img: 3 * 1024 * 1024,
file: 10 * 1024 * 1024
}
// -> IMAGES
@ -51,7 +48,7 @@ module.exports = {
this.uploadImgHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
@ -76,7 +73,7 @@ module.exports = {
this.uploadFileHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
@ -95,35 +92,30 @@ module.exports = {
/**
* Creates a base directories (Synchronous).
*
* @param {Object} appconfig The application config
* @return {Void} Void
*/
createBaseDirectories (appconfig) {
winston.info('Checking data directories...')
createBaseDirectories () {
try {
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
fs.emptyDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
fs.emptyDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './cache'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './thumbs'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'))
if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '755')
fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'), '755')
}
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo))
fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'))
if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'), '755')
fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'), '755')
}
} catch (err) {
winston.error(err)
wiki.logger.error(err)
}
winston.info('Data and Repository directories are OK.')
wiki.logger.info('Disk Data Paths: OK')
},
/**
@ -154,7 +146,7 @@ module.exports = {
*/
validateUploadsFilename (f, fld, isImage) {
let fObj = path.parse(f)
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g'), '')
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']', 'g'), '')
let fext = _.toLower(fObj.ext)
if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
@ -165,7 +157,7 @@ module.exports = {
let fpath = path.resolve(this._uploadsPath, fld, f)
return fs.statAsync(fpath).then((s) => {
throw new Error(lang.t('errors:fileexists', { path: f }))
throw new Error(wiki.lang.t('errors:fileexists', { path: f }))
}).catch((err) => {
if (err.code === 'ENOENT') {
return f

@ -1,6 +1,6 @@
'use strict'
/* global db, git, lang, mark, rights, search, winston */
/* global wiki */
const Promise = require('bluebird')
const path = require('path')
@ -10,7 +10,7 @@ const _ = require('lodash')
const entryHelper = require('../helpers/entry')
/**
* Entries Model
* Documents Model
*/
module.exports = {
@ -25,10 +25,10 @@ module.exports = {
init() {
let self = this
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')
appdata.repoPath = self._repoPath
appdata.cachePath = self._cachePath
self._repoPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo)
self._cachePath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'cache')
wiki.data.repoPath = self._repoPath
wiki.data.cachePath = self._cachePath
return self
},

@ -1,6 +1,6 @@
'use strict'
/* global lang, winston */
/* global wiki */
const Git = require('git-wrapper2-promise')
const Promise = require('bluebird')
@ -43,21 +43,19 @@ module.exports = {
// -> Build repository path
if (_.isEmpty(appconfig.paths.repo)) {
self._repo.path = path.join(ROOTPATH, 'repo')
if (_.isEmpty(wiki.config.paths.repo)) {
self._repo.path = path.join(wiki.ROOTPATH, 'repo')
} else {
self._repo.path = appconfig.paths.repo
self._repo.path = wiki.config.paths.repo
}
// -> Initialize repository
self.onReady = self._initRepo(appconfig)
self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve()
if (appconfig.git) {
// Set repo branch
self._repo.branch = appconfig.git.branch || 'master'
// Define signature
self._signature.email = appconfig.git.serverEmail || 'wiki@example.com'
if (wiki.config.git) {
self._repo.branch = wiki.config.git.branch || 'master'
self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com'
}
return self
@ -66,19 +64,17 @@ module.exports = {
/**
* Initialize Git repository
*
* @param {Object} appconfig The application config
* @param {Object} wiki.config The application config
* @return {Object} Promise
*/
_initRepo(appconfig) {
_initRepo() {
let self = this
winston.info('Checking Git repository...')
// -> Check if path is accessible
return fs.mkdirAsync(self._repo.path).catch((err) => {
if (err.code !== 'EEXIST') {
winston.error('Invalid Git repository path or missing permissions.')
wiki.logger.error('Invalid Git repository path or missing permissions.')
}
}).then(() => {
self._git = new Git({ 'git-dir': self._repo.path })
@ -92,28 +88,28 @@ module.exports = {
self._repo.exists = false
})
}).then(() => {
if (appconfig.git === false) {
winston.info('Remote Git syncing is disabled. Not recommended!')
if (wiki.config.git === false) {
wiki.logger.warn('Remote Git syncing is disabled. Not recommended!')
return Promise.resolve(true)
}
// Initialize remote
let urlObj = URL.parse(appconfig.git.url)
if (appconfig.git.auth.type !== 'ssh') {
urlObj.auth = appconfig.git.auth.username + ':' + appconfig.git.auth.password
let urlObj = URL.parse(wiki.config.git.url)
if (wiki.config.git.auth.type !== 'ssh') {
urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password
}
self._url = URL.format(urlObj)
let gitConfigs = [
() => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
() => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
() => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(appconfig.git.auth.sslVerify)]) }
() => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) }
]
if (appconfig.git.auth.type === 'ssh') {
if (wiki.config.git.auth.type === 'ssh') {
gitConfigs.push(() => {
return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + appconfig.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
})
}
@ -126,14 +122,14 @@ module.exports = {
return self._git.exec('remote', ['set-url', 'origin', self._url])
}
}).catch(err => {
winston.error(err)
wiki.logger.error(err)
})
})
}).catch((err) => {
winston.error('Git remote error!')
wiki.logger.error('Git remote error!')
throw err
}).then(() => {
winston.info('Git repository is OK.')
wiki.logger.info('Git Repository: OK')
return true
})
},
@ -144,7 +140,7 @@ module.exports = {
* @return {String} The repo path.
*/
getRepoPath() {
return this._repo.path || path.join(ROOTPATH, 'repo')
return this._repo.path || path.join(wiki.ROOTPATH, 'repo')
},
/**
@ -157,18 +153,18 @@ module.exports = {
// Is git remote disabled?
if (appconfig.git === false) {
if (wiki.config.git === false) {
return Promise.resolve(true)
}
// Fetch
winston.info('Performing pull from remote Git repository...')
wiki.logger.info('Performing pull from remote Git repository...')
return self._git.pull('origin', self._repo.branch).then((cProc) => {
winston.info('Git Pull completed.')
wiki.logger.info('Git Pull completed.')
})
.catch((err) => {
winston.error('Unable to fetch from git origin!')
wiki.logger.error('Unable to fetch from git origin!')
throw err
})
.then(() => {
@ -178,19 +174,19 @@ module.exports = {
let out = cProc.stdout.toString()
if (_.includes(out, 'commit')) {
winston.info('Performing push to remote Git repository...')
wiki.logger.info('Performing push to remote Git repository...')
return self._git.push('origin', self._repo.branch).then(() => {
return winston.info('Git Push completed.')
return wiki.logger.info('Git Push completed.')
})
} else {
winston.info('Git Push skipped. Repository is already in sync.')
wiki.logger.info('Git Push skipped. Repository is already in sync.')
}
return true
})
})
.catch((err) => {
winston.error('Unable to push changes to remote Git repository!')
wiki.logger.error('Unable to push changes to remote Git repository!')
throw err
})
},
@ -210,7 +206,7 @@ module.exports = {
let out = cProc.stdout.toString()
return _.includes(out, gitFilePath)
}).then((isTracked) => {
commitMsg = (isTracked) ? lang.t('git:updated', { path: gitFilePath }) : lang.t('git:added', { path: gitFilePath })
commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath })
return self._git.add(gitFilePath)
}).then(() => {
let commitUsr = securityHelper.sanitizeCommitUser(author)
@ -245,29 +241,6 @@ module.exports = {
})
},
/**
* Delete a document.
*
* @param {String} entryPath The entry path
* @return {Promise<Boolean>} Resolve on success
*/
deleteDocument(entryPath, author) {
let self = this
let gitFilePath = entryPath + '.md'
return this._git.exec('rm', [gitFilePath]).then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'fatal')) {
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
throw new Error(errorMsg)
}
let commitUsr = securityHelper.sanitizeCommitUser(author)
return self._git.exec('commit', ['-m', lang.t('git:deleted', { path: gitFilePath }), '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
},
/**
* Commits uploads changes.
*

@ -0,0 +1,43 @@
'use strict'
/* global wiki */
const gqlTools = require('graphql-tools')
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8')
const DateScalar = require('../schemas/scalar-date')
const AuthenticationResolvers = require('../schemas/resolvers-authentication')
const CommentResolvers = require('../schemas/resolvers-comment')
const DocumentResolvers = require('../schemas/resolvers-document')
const FileResolvers = require('../schemas/resolvers-file')
const FolderResolvers = require('../schemas/resolvers-folder')
const GroupResolvers = require('../schemas/resolvers-group')
const SettingResolvers = require('../schemas/resolvers-setting')
const TagResolvers = require('../schemas/resolvers-tag')
const TranslationResolvers = require('../schemas/resolvers-translation')
const UserResolvers = require('../schemas/resolvers-user')
const resolvers = _.merge(
AuthenticationResolvers,
CommentResolvers,
DocumentResolvers,
FileResolvers,
FolderResolvers,
GroupResolvers,
SettingResolvers,
TagResolvers,
TranslationResolvers,
UserResolvers,
DateScalar
)
const Schema = gqlTools.makeExecutableSchema({
typeDefs,
resolvers
})
module.exports = Schema

@ -0,0 +1,91 @@
const cluster = require('cluster')
const Promise = require('bluebird')
const _ = require('lodash')
/* global wiki */
module.exports = {
numWorkers: 1,
workers: [],
init() {
if (cluster.isMaster) {
wiki.logger.info('=======================================')
wiki.logger.info('= Wiki.js =============================')
wiki.logger.info('=======================================')
wiki.redis = require('./redis').init()
wiki.queue = require('./queue').init()
this.setWorkerLimit()
this.bootMaster()
} else {
this.bootWorker()
}
},
/**
* Pre-Master Boot Sequence
*/
preBootMaster() {
return Promise.mapSeries([
() => { return wiki.db.onReady },
() => { return wiki.configSvc.loadFromDb() },
() => { return wiki.queue.clean() }
], fn => { return fn() })
},
/**
* Boot Master Process
*/
bootMaster() {
this.preBootMaster().then(sequenceResults => {
if (_.every(sequenceResults, rs => rs === true)) {
this.postBootMaster()
} else {
wiki.logger.info('Starting configuration manager...')
require('../configure')()
}
return true
}).catch(err => {
wiki.logger.error(err)
process.exit(1)
})
},
/**
* Post-Master Boot Sequence
*/
postBootMaster() {
require('../master')().then(() => {
_.times(this.numWorker, this.spawnWorker)
wiki.queue.uplClearTemp.add({}, {
repeat: { cron: '*/15 * * * *' }
})
})
cluster.on('exit', (worker, code, signal) => {
wiki.logger.info(`Background Worker #${worker.id} was terminated.`)
})
},
/**
* Boot Worker Process
*/
bootWorker() {
wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`)
require('../worker')
},
/**
* Spawn new Worker process
*/
spawnWorker() {
this.workers.push(cluster.fork())
},
/**
* Set Worker count based on config + system capabilities
*/
setWorkerLimit() {
const numCPUs = require('os').cpus().length
this.numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs
if (this.numWorkers > numCPUs) {
this.numWorkers = numCPUs
}
}
}

@ -0,0 +1,52 @@
const _ = require('lodash')
const dotize = require('dotize')
const i18nBackend = require('i18next-node-fs-backend')
const i18next = require('i18next')
const path = require('path')
const Promise = require('bluebird')
/* global wiki */
module.exports = {
engine: null,
namespaces: ['common', 'admin', 'auth', 'errors', 'git'],
init() {
this.engine = i18next
this.engine.use(i18nBackend).init({
load: 'languageOnly',
ns: this.namespaces,
defaultNS: 'common',
saveMissing: false,
preload: [wiki.config.site.lang],
lng: wiki.config.site.lang,
fallbackLng: 'en',
backend: {
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
}
})
return this
},
getByNamespace(locale, namespace) {
if (this.engine.hasResourceBundle(locale, namespace)) {
let data = this.engine.getResourceBundle(locale, namespace)
return _.map(dotize.convert(data), (value, key) => {
return {
key,
value
}
})
} else {
throw new Error('Invalid locale or namespace')
}
},
loadLocale(locale) {
return Promise.fromCallback(cb => {
return this.engine.loadLanguages(locale, cb)
})
},
setCurrentLocale(locale) {
return Promise.fromCallback(cb => {
return this.engine.changeLanguage(locale, cb)
})
}
}

@ -0,0 +1,80 @@
'use strict'
/* global wiki */
const cluster = require('cluster')
module.exports = {
init() {
let winston = require('winston')
// Console
let logger = new (winston.Logger)({
level: (wiki.IS_DEBUG) ? 'debug' : 'info',
transports: [
new (winston.transports.Console)({
level: (wiki.IS_DEBUG) ? 'debug' : 'info',
prettyPrint: true,
colorize: true,
silent: false,
timestamp: true
})
]
})
logger.filters.push((level, msg) => {
let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}`
return '[' + processName + '] ' + msg
})
// External services
// if (wiki.config.externalLogging.bugsnag) {
// const bugsnagTransport = require('./winston-transports/bugsnag')
// logger.add(bugsnagTransport, {
// level: 'warn',
// key: wiki.config.externalLogging.bugsnag
// })
// }
// if (wiki.config.externalLogging.loggly) {
// require('winston-loggly-bulk')
// logger.add(winston.transports.Loggly, {
// token: wiki.config.externalLogging.loggly.token,
// subdomain: wiki.config.externalLogging.loggly.subdomain,
// tags: ['wiki-js'],
// level: 'warn',
// json: true
// })
// }
// if (wiki.config.externalLogging.papertrail) {
// require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
// logger.add(winston.transports.Papertrail, {
// host: wiki.config.externalLogging.papertrail.host,
// port: wiki.config.externalLogging.papertrail.port,
// level: 'warn',
// program: 'wiki.js'
// })
// }
// if (wiki.config.externalLogging.rollbar) {
// const rollbarTransport = require('./winston-transports/rollbar')
// logger.add(rollbarTransport, {
// level: 'warn',
// key: wiki.config.externalLogging.rollbar
// })
// }
// if (wiki.config.externalLogging.sentry) {
// const sentryTransport = require('./winston-transports/sentry')
// logger.add(sentryTransport, {
// level: 'warn',
// key: wiki.config.externalLogging.sentry
// })
// }
return logger
}
}

@ -1,6 +1,6 @@
'use strict'
/* global winston */
/* global wiki */
const Promise = require('bluebird')
const md = require('markdown-it')
@ -23,11 +23,12 @@ const mdRemove = require('remove-markdown')
var mkdown = md({
html: true,
breaks: appconfig.features.linebreaks,
// breaks: wiki.config.features.linebreaks,
breaks: true,
linkify: true,
typography: true,
highlight(str, lang) {
if (appconfig.theme.code.colorize && lang && hljs.getLanguage(lang)) {
if (wiki.config.theme.code.colorize && lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
} catch (err) {
@ -57,7 +58,8 @@ var mkdown = md({
})
.use(mdAttrs)
if (appconfig.features.mathjax) {
// if (wiki.config.features.mathjax) {
if (true) {
mkdown.use(mdMathjax)
}
@ -94,7 +96,7 @@ const videoRules = [
// Regex
const textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g')
const textRegex = new RegExp('\\b[a-z0-9-.,' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\b', 'g')
const mathRegex = [
{
format: 'TeX',
@ -301,7 +303,7 @@ const parseContent = (content) => {
// Mathjax Post-processor
if (appconfig.features.mathjax) {
if (wiki.config.features.mathjax) {
return processMathjax(cr.html())
} else {
return Promise.resolve(cr.html())
@ -339,7 +341,7 @@ const processMathjax = (content) => {
resolve(result.svg)
} else {
resolve(currentMatch[0])
winston.warn(result.errors.join(', '))
wiki.logger.warn(result.errors.join(', '))
}
})
})

@ -0,0 +1,37 @@
'use strict'
/* global wiki */
const Bull = require('bull')
const Promise = require('bluebird')
module.exports = {
init() {
wiki.data.queues.forEach(queueName => {
this[queueName] = new Bull(queueName, {
prefix: `q-${wiki.config.ha.nodeuid}`,
redis: wiki.config.redis
})
})
return this
},
clean() {
return Promise.each(wiki.data.queues, queueName => {
return new Promise((resolve, reject) => {
let keyStream = wiki.redis.scanStream({
match: `q-${wiki.config.ha.nodeuid}:${queueName}:*`
})
keyStream.on('data', resultKeys => {
if (resultKeys.length > 0) {
wiki.redis.del(resultKeys)
}
})
keyStream.on('end', resolve)
})
}).then(() => {
wiki.logger.info('Purging old queue jobs: OK')
}).return(true).catch(err => {
wiki.logger.error(err)
})
}
}

@ -0,0 +1,33 @@
'use strict'
/* global wiki */
const Redis = require('ioredis')
const { isPlainObject } = require('lodash')
/**
* Redis module
*
* @return {Object} Redis client wrapper instance
*/
module.exports = {
/**
* Initialize Redis client
*
* @return {Object} Redis client instance
*/
init() {
if (isPlainObject(wiki.config.redis)) {
let red = new Redis(wiki.config.redis)
red.on('ready', () => {
wiki.logger.info('Redis connection: OK')
})
return red
} else {
wiki.logger.error('Invalid Redis configuration!')
process.exit(1)
}
}
}

@ -1,6 +1,6 @@
'use strict'
/* global db */
/* global wiki */
const _ = require('lodash')
@ -32,8 +32,8 @@ module.exports = {
init () {
let self = this
db.onReady.then(() => {
db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => {
wiki.db.onReady.then(() => {
wiki.db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => {
if (u) {
self.guest = u
}

@ -1,13 +1,13 @@
'use strict'
/* global winston */
/* global wiki */
const Promise = require('bluebird')
const _ = require('lodash')
const searchIndex = require('./search-index')
// const searchIndex = require('./search-index')
const stopWord = require('stopword')
const streamToPromise = require('stream-to-promise')
const searchAllowedChars = new RegExp('[^a-z0-9' + appdata.regex.cjk + appdata.regex.arabic + ' ]', 'g')
const searchAllowedChars = new RegExp('[^a-z0-9' + wiki.data.regex.cjk + wiki.data.regex.arabic + ' ]', 'g')
module.exports = {
@ -22,24 +22,24 @@ module.exports = {
init () {
let self = this
self._isReady = new Promise((resolve, reject) => {
searchIndex({
/*searchIndex({
deletable: true,
fieldedSearch: true,
indexPath: 'wiki',
logLevel: 'error',
stopwords: _.get(stopWord, appconfig.lang, [])
stopwords: _.get(stopWord, wiki.config.lang, [])
}, (err, si) => {
if (err) {
winston.error('Failed to initialize search index.', err)
wiki.logger.error('Failed to initialize search index.', err)
reject(err)
} else {
self._si = Promise.promisifyAll(si)
self._si.flushAsync().then(() => {
winston.info('Search index flushed and ready.')
wiki.logger.info('Search index flushed and ready.')
resolve(true)
})
}
})
}) */
})
return self
@ -95,13 +95,13 @@ module.exports = {
parent: content.parent || '',
content: content.text || ''
}]).then(() => {
winston.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
wiki.logger.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
return true
}).catch((err) => {
winston.error(err)
wiki.logger.error(err)
})
}).catch((err) => {
winston.error(err)
wiki.logger.error(err)
})
})
},
@ -131,7 +131,7 @@ module.exports = {
if (err.type === 'NotFoundError') {
return true
} else {
winston.error(err)
wiki.logger.error(err)
}
})
})
@ -204,7 +204,7 @@ module.exports = {
suggest: []
}
} else {
winston.error(err)
wiki.logger.error(err)
}
})
}

@ -1,6 +1,6 @@
'use strict'
/* global db, git, lang, upl */
/* global wiki */
const path = require('path')
const Promise = require('bluebird')
@ -32,8 +32,8 @@ module.exports = {
init () {
let self = this
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
self._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
self._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
return self
},
@ -59,16 +59,16 @@ module.exports = {
self._watcher.on('add', (p) => {
let pInfo = self.parseUploadsRelPath(p)
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
return wiki.db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
}).then(() => {
return git.commitUploads(lang.t('git:uploaded', { path: p }))
return wiki.git.commitUploads(wiki.lang.t('git:uploaded', { path: p }))
})
})
// -> Remove upload file
self._watcher.on('unlink', (p) => {
return git.commitUploads(lang.t('git:deleted', { path: p }))
return wiki.git.commitUploads(wiki.lang.t('git:deleted', { path: p }))
})
},
@ -91,8 +91,8 @@ module.exports = {
// Add folders to DB
return db.UplFolder.remove({}).then(() => {
return db.UplFolder.insertMany(_.map(folderNames, (f) => {
return wiki.db.UplFolder.remove({}).then(() => {
return wiki.db.UplFolder.insertMany(_.map(folderNames, (f) => {
return {
_id: 'f:' + f,
name: f
@ -107,7 +107,7 @@ module.exports = {
let fldPath = path.join(self._uploadsPath, fldName)
return fs.readdirAsync(fldPath).then((fList) => {
return Promise.map(fList, (f) => {
return upl.processFile(fldName, f).then((mData) => {
return wiki.upl.processFile(fldName, f).then((mData) => {
if (mData) {
allFiles.push(mData)
}
@ -118,9 +118,9 @@ module.exports = {
}, {concurrency: 1}).finally(() => {
// Add files to DB
return db.UplFile.remove({}).then(() => {
return wiki.db.UplFile.remove({}).then(() => {
if (_.isArray(allFiles) && allFiles.length > 0) {
return db.UplFile.insertMany(allFiles)
return wiki.db.UplFile.insertMany(allFiles)
} else {
return true
}
@ -131,7 +131,7 @@ module.exports = {
}).then(() => {
// Watch for new changes
return upl.watch()
return wiki.upl.watch()
})
},

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

Loading…
Cancel
Save