common.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. const express = require('express')
  2. const router = express.Router()
  3. const pageHelper = require('../helpers/page')
  4. const _ = require('lodash')
  5. const CleanCSS = require('clean-css')
  6. const moment = require('moment')
  7. const path = require('path')
  8. /* global WIKI */
  9. const tmplCreateRegex = /^[0-9]+(,[0-9]+)?$/
  10. const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
  11. /**
  12. * Robots.txt
  13. */
  14. router.get('/robots.txt', (req, res, next) => {
  15. res.type('text/plain')
  16. if (_.includes(WIKI.config.seo.robots, 'noindex')) {
  17. res.send('User-agent: *\nDisallow: /')
  18. } else {
  19. res.status(200).end()
  20. }
  21. })
  22. /**
  23. * Health Endpoint
  24. */
  25. router.get('/healthz', (req, res, next) => {
  26. if (WIKI.models.knex.client.pool.numFree() < 1 && WIKI.models.knex.client.pool.numUsed() < 1) {
  27. res.status(503).json({ ok: false }).end()
  28. } else {
  29. res.status(200).json({ ok: true }).end()
  30. }
  31. })
  32. /**
  33. * Site Asset
  34. */
  35. router.get('/_site/:siteId?/:resource', async (req, res, next) => {
  36. const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.models.sites.getSiteByHostname({ hostname: req.hostname })
  37. if (!site) {
  38. return res.status(404).send('Site Not Found')
  39. }
  40. switch (req.params.resource) {
  41. case 'logo': {
  42. if (site.config.assets.logo) {
  43. res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
  44. } else {
  45. res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
  46. }
  47. break
  48. }
  49. case 'favicon': {
  50. if (site.config.assets.favicon) {
  51. res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
  52. } else {
  53. res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
  54. }
  55. break
  56. }
  57. case 'loginbg': {
  58. if (site.config.assets.loginBg) {
  59. res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
  60. } else {
  61. res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
  62. }
  63. break
  64. }
  65. default: {
  66. return res.status(404).send('Invalid Site Resource')
  67. }
  68. }
  69. })
  70. /**
  71. * New v3 vue app
  72. */
  73. router.get([
  74. '/_admin',
  75. '/_admin/*',
  76. '/_profile',
  77. '/_profile/*',
  78. '/_error',
  79. '/_error/*',
  80. '/_welcome'
  81. ], (req, res, next) => {
  82. res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
  83. })
  84. // router.get(['/_admin', '/_admin/*'], (req, res, next) => {
  85. // if (!WIKI.auth.checkAccess(req.user, [
  86. // 'manage:system',
  87. // 'write:users',
  88. // 'manage:users',
  89. // 'write:groups',
  90. // 'manage:groups',
  91. // 'manage:navigation',
  92. // 'manage:theme',
  93. // 'manage:api'
  94. // ])) {
  95. // _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  96. // return res.status(403).render('unauthorized', { action: 'view' })
  97. // }
  98. // _.set(res.locals, 'pageMeta.title', 'Admin')
  99. // res.render('admin')
  100. // })
  101. /**
  102. * Download Page / Version
  103. */
  104. router.get(['/d', '/d/*'], async (req, res, next) => {
  105. const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
  106. const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
  107. const page = await WIKI.models.pages.getPageFromDb({
  108. path: pageArgs.path,
  109. locale: pageArgs.locale,
  110. userId: req.user.id,
  111. isPrivate: false
  112. })
  113. pageArgs.tags = _.get(page, 'tags', [])
  114. if (versionId > 0) {
  115. if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
  116. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  117. return res.render('unauthorized', { action: 'downloadVersion' })
  118. }
  119. } else {
  120. if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
  121. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  122. return res.render('unauthorized', { action: 'download' })
  123. }
  124. }
  125. if (page) {
  126. const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
  127. res.attachment(fileName)
  128. if (versionId > 0) {
  129. const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })
  130. res.send(pageHelper.injectPageMetadata(pageVersion))
  131. } else {
  132. res.send(pageHelper.injectPageMetadata(page))
  133. }
  134. } else {
  135. res.status(404).end()
  136. }
  137. })
  138. /**
  139. * Create/Edit document
  140. */
  141. router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
  142. const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
  143. if (pageArgs.path === '') {
  144. return res.redirect(`/_edit/home`)
  145. }
  146. if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
  147. return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
  148. }
  149. req.i18n.changeLanguage(pageArgs.locale)
  150. // -> Set Editor Lang
  151. _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
  152. _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
  153. // -> Check for reserved path
  154. if (pageHelper.isReservedPath(pageArgs.path)) {
  155. return next(new Error('Cannot create this page because it starts with a system reserved path.'))
  156. }
  157. // -> Get page data from DB
  158. let page = await WIKI.models.pages.getPageFromDb({
  159. path: pageArgs.path,
  160. locale: pageArgs.locale,
  161. userId: req.user.id,
  162. isPrivate: false
  163. })
  164. pageArgs.tags = _.get(page, 'tags', [])
  165. // -> Effective Permissions
  166. const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
  167. const injectCode = {
  168. css: WIKI.config.theming.injectCSS,
  169. head: WIKI.config.theming.injectHead,
  170. body: WIKI.config.theming.injectBody
  171. }
  172. if (page) {
  173. // -> EDIT MODE
  174. if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
  175. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  176. return res.render('unauthorized', { action: 'edit' })
  177. }
  178. // -> Get page tags
  179. await page.$relatedQuery('tags')
  180. page.tags = _.map(page.tags, 'tag')
  181. // Handle missing extra field
  182. page.extra = page.extra || { css: '', js: '' }
  183. // -> Beautify Script CSS
  184. if (!_.isEmpty(page.extra.css)) {
  185. page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
  186. }
  187. _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
  188. _.set(res.locals, 'pageMeta.description', page.description)
  189. page.mode = 'update'
  190. page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
  191. page.content = Buffer.from(page.content).toString('base64')
  192. } else {
  193. // -> CREATE MODE
  194. if (!effectivePermissions.pages.write) {
  195. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  196. return res.render('unauthorized', { action: 'create' })
  197. }
  198. _.set(res.locals, 'pageMeta.title', `New Page`)
  199. page = {
  200. path: pageArgs.path,
  201. localeCode: pageArgs.locale,
  202. editorKey: null,
  203. mode: 'create',
  204. content: null,
  205. title: null,
  206. description: null,
  207. updatedAt: new Date().toISOString(),
  208. extra: {
  209. css: '',
  210. js: ''
  211. }
  212. }
  213. // -> From Template
  214. if (req.query.from && tmplCreateRegex.test(req.query.from)) {
  215. let tmplPageId = 0
  216. let tmplVersionId = 0
  217. if (req.query.from.indexOf(',')) {
  218. const q = req.query.from.split(',')
  219. tmplPageId = _.toSafeInteger(q[0])
  220. tmplVersionId = _.toSafeInteger(q[1])
  221. } else {
  222. tmplPageId = _.toSafeInteger(req.query.from)
  223. }
  224. if (tmplVersionId > 0) {
  225. // -> From Page Version
  226. const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: tmplPageId, versionId: tmplVersionId })
  227. if (!pageVersion) {
  228. _.set(res.locals, 'pageMeta.title', 'Page Not Found')
  229. return res.status(404).render('notfound', { action: 'template' })
  230. }
  231. if (!WIKI.auth.checkAccess(req.user, ['read:history'], { path: pageVersion.path, locale: pageVersion.locale })) {
  232. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  233. return res.render('unauthorized', { action: 'sourceVersion' })
  234. }
  235. page.content = Buffer.from(pageVersion.content).toString('base64')
  236. page.editorKey = pageVersion.editor
  237. page.title = pageVersion.title
  238. page.description = pageVersion.description
  239. } else {
  240. // -> From Page Live
  241. const pageOriginal = await WIKI.models.pages.query().findById(tmplPageId)
  242. if (!pageOriginal) {
  243. _.set(res.locals, 'pageMeta.title', 'Page Not Found')
  244. return res.status(404).render('notfound', { action: 'template' })
  245. }
  246. if (!WIKI.auth.checkAccess(req.user, ['read:source'], { path: pageOriginal.path, locale: pageOriginal.locale })) {
  247. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  248. return res.render('unauthorized', { action: 'source' })
  249. }
  250. page.content = Buffer.from(pageOriginal.content).toString('base64')
  251. page.editorKey = pageOriginal.editorKey
  252. page.title = pageOriginal.title
  253. page.description = pageOriginal.description
  254. }
  255. }
  256. }
  257. res.render('editor', { page, injectCode, effectivePermissions })
  258. })
  259. /**
  260. * History
  261. */
  262. router.get(['/h', '/h/*'], async (req, res, next) => {
  263. const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
  264. if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
  265. return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
  266. }
  267. req.i18n.changeLanguage(pageArgs.locale)
  268. _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
  269. _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
  270. const page = await WIKI.models.pages.getPageFromDb({
  271. path: pageArgs.path,
  272. locale: pageArgs.locale,
  273. userId: req.user.id,
  274. isPrivate: false
  275. })
  276. if (!page) {
  277. _.set(res.locals, 'pageMeta.title', 'Page Not Found')
  278. return res.status(404).render('notfound', { action: 'history' })
  279. }
  280. pageArgs.tags = _.get(page, 'tags', [])
  281. const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
  282. if (!effectivePermissions.history.read) {
  283. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  284. return res.render('unauthorized', { action: 'history' })
  285. }
  286. if (page) {
  287. _.set(res.locals, 'pageMeta.title', page.title)
  288. _.set(res.locals, 'pageMeta.description', page.description)
  289. res.render('history', { page, effectivePermissions })
  290. } else {
  291. res.redirect(`/${pageArgs.path}`)
  292. }
  293. })
  294. /**
  295. * Page ID redirection
  296. */
  297. router.get(['/i', '/i/:id'], async (req, res, next) => {
  298. const pageId = _.toSafeInteger(req.params.id)
  299. if (pageId <= 0) {
  300. return res.redirect('/')
  301. }
  302. const page = await WIKI.models.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
  303. if (!page) {
  304. _.set(res.locals, 'pageMeta.title', 'Page Not Found')
  305. return res.status(404).render('notfound', { action: 'view' })
  306. }
  307. if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
  308. locale: page.localeCode,
  309. path: page.path,
  310. private: page.isPrivate,
  311. privateNS: page.privateNS,
  312. explicitLocale: false,
  313. tags: page.tags
  314. })) {
  315. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  316. return res.render('unauthorized', { action: 'view' })
  317. }
  318. if (WIKI.config.lang.namespacing) {
  319. return res.redirect(`/${page.localeCode}/${page.path}`)
  320. } else {
  321. return res.redirect(`/${page.path}`)
  322. }
  323. })
  324. /**
  325. * Source
  326. */
  327. router.get(['/s', '/s/*'], async (req, res, next) => {
  328. const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
  329. const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
  330. const page = await WIKI.models.pages.getPageFromDb({
  331. path: pageArgs.path,
  332. locale: pageArgs.locale,
  333. userId: req.user.id,
  334. isPrivate: false
  335. })
  336. pageArgs.tags = _.get(page, 'tags', [])
  337. if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
  338. return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
  339. }
  340. // -> Effective Permissions
  341. const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
  342. _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
  343. _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
  344. if (versionId > 0) {
  345. if (!effectivePermissions.history.read) {
  346. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  347. return res.render('unauthorized', { action: 'sourceVersion' })
  348. }
  349. } else {
  350. if (!effectivePermissions.source.read) {
  351. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  352. return res.render('unauthorized', { action: 'source' })
  353. }
  354. }
  355. if (page) {
  356. if (versionId > 0) {
  357. const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })
  358. _.set(res.locals, 'pageMeta.title', pageVersion.title)
  359. _.set(res.locals, 'pageMeta.description', pageVersion.description)
  360. res.render('source', {
  361. page: {
  362. ...page,
  363. ...pageVersion
  364. },
  365. effectivePermissions
  366. })
  367. } else {
  368. _.set(res.locals, 'pageMeta.title', page.title)
  369. _.set(res.locals, 'pageMeta.description', page.description)
  370. res.render('source', { page, effectivePermissions })
  371. }
  372. } else {
  373. res.redirect(`/${pageArgs.path}`)
  374. }
  375. })
  376. /**
  377. * Tags
  378. */
  379. router.get(['/t', '/t/*'], (req, res, next) => {
  380. _.set(res.locals, 'pageMeta.title', 'Tags')
  381. res.render('tags')
  382. })
  383. /**
  384. * User Avatar
  385. */
  386. router.get('/_userav/:uid', async (req, res, next) => {
  387. if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
  388. return res.sendStatus(403)
  389. }
  390. const av = await WIKI.models.users.getUserAvatarData(req.params.uid)
  391. if (av) {
  392. res.set('Content-Type', 'image/jpeg')
  393. res.send(av)
  394. }
  395. return res.sendStatus(404)
  396. })
  397. /**
  398. * View document / asset
  399. */
  400. router.get('/*', async (req, res, next) => {
  401. const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
  402. const pageArgs = pageHelper.parsePath(req.path, { stripExt })
  403. const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
  404. if (isPage) {
  405. if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
  406. return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
  407. }
  408. req.i18n.changeLanguage(pageArgs.locale)
  409. try {
  410. // -> Get Page from cache
  411. const page = await WIKI.models.pages.getPage({
  412. path: pageArgs.path,
  413. locale: pageArgs.locale,
  414. userId: req.user.id
  415. })
  416. pageArgs.tags = _.get(page, 'tags', [])
  417. // -> Effective Permissions
  418. const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
  419. // -> Check User Access
  420. if (!effectivePermissions.pages.read) {
  421. if (req.user.id === WIKI.auth.guest.id) {
  422. res.cookie('loginRedirect', req.path, {
  423. maxAge: 15 * 60 * 1000
  424. })
  425. }
  426. if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
  427. return res.redirect('/login')
  428. }
  429. return res.redirect(`/_error/unauthorized?from=${req.path}`)
  430. }
  431. _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
  432. _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
  433. if (page) {
  434. _.set(res.locals, 'pageMeta.title', page.title)
  435. _.set(res.locals, 'pageMeta.description', page.description)
  436. // -> Check Publishing State
  437. let pageIsPublished = page.isPublished
  438. if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
  439. pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
  440. }
  441. if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
  442. pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
  443. }
  444. if (!pageIsPublished && !effectivePermissions.pages.write) {
  445. _.set(res.locals, 'pageMeta.title', 'Unauthorized')
  446. return res.status(403).render('unauthorized', {
  447. action: 'view'
  448. })
  449. }
  450. // -> Build sidebar navigation
  451. let sdi = 1
  452. const sidebar = (await WIKI.models.navigation.getTree({ cache: true, locale: pageArgs.locale, groups: req.user.groups })).map(n => ({
  453. i: `sdi-${sdi++}`,
  454. k: n.kind,
  455. l: n.label,
  456. c: n.icon,
  457. y: n.targetType,
  458. t: n.target
  459. }))
  460. // -> Build theme code injection
  461. const injectCode = {
  462. css: WIKI.config.theming.injectCSS,
  463. head: WIKI.config.theming.injectHead,
  464. body: WIKI.config.theming.injectBody
  465. }
  466. // Handle missing extra field
  467. page.extra = page.extra || { css: '', js: '' }
  468. if (!_.isEmpty(page.extra.css)) {
  469. injectCode.css = `${injectCode.css}\n${page.extra.css}`
  470. }
  471. if (!_.isEmpty(page.extra.js)) {
  472. injectCode.body = `${injectCode.body}\n${page.extra.js}`
  473. }
  474. // -> Convert page TOC
  475. if (!_.isString(page.toc)) {
  476. page.toc = JSON.stringify(page.toc)
  477. }
  478. // -> Inject comments variables
  479. const commentTmpl = {
  480. codeTemplate: WIKI.data.commentProvider.codeTemplate,
  481. head: WIKI.data.commentProvider.head,
  482. body: WIKI.data.commentProvider.body,
  483. main: WIKI.data.commentProvider.main
  484. }
  485. if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
  486. [
  487. { key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
  488. { key: 'pageId', value: page.id }
  489. ].forEach((cfg) => {
  490. commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
  491. commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
  492. commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
  493. })
  494. }
  495. // -> Render view
  496. res.render('page', {
  497. page,
  498. sidebar,
  499. injectCode,
  500. comments: commentTmpl,
  501. effectivePermissions
  502. })
  503. } else if (pageArgs.path === 'home') {
  504. res.redirect('/_welcome')
  505. } else {
  506. _.set(res.locals, 'pageMeta.title', 'Page Not Found')
  507. if (effectivePermissions.pages.write) {
  508. res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
  509. } else {
  510. res.status(404).render('notfound', { action: 'view' })
  511. }
  512. }
  513. } catch (err) {
  514. next(err)
  515. }
  516. } else {
  517. if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
  518. return res.sendStatus(403)
  519. }
  520. await WIKI.models.assets.getAsset(pageArgs.path, res)
  521. }
  522. })
  523. module.exports = router