|
@@ -13,6 +13,8 @@ const TurndownService = require('turndown')
|
|
|
const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm
|
|
|
const cheerio = require('cheerio')
|
|
|
|
|
|
+const pageRegex = /^[a-zA0-90-9-_/]*$/
|
|
|
+
|
|
|
const frontmatterRegex = {
|
|
|
html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/,
|
|
|
legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
|
|
@@ -52,7 +54,7 @@ module.exports = class Page extends Model {
|
|
|
}
|
|
|
|
|
|
static get jsonAttributes() {
|
|
|
- return ['extra']
|
|
|
+ return ['config', 'historyData', 'relations', 'scripts', 'toc']
|
|
|
}
|
|
|
|
|
|
static get relationMappings() {
|
|
@@ -231,11 +233,6 @@ module.exports = class Page extends Model {
|
|
|
throw new WIKI.Error.Custom('InvalidSiteId', 'Site ID is invalid.')
|
|
|
}
|
|
|
|
|
|
- // -> Validate path
|
|
|
- if (opts.path.includes('.') || opts.path.includes(' ') || opts.path.includes('\\') || opts.path.includes('//')) {
|
|
|
- throw new WIKI.Error.PageIllegalPath()
|
|
|
- }
|
|
|
-
|
|
|
// -> Remove trailing slash
|
|
|
if (opts.path.endsWith('/')) {
|
|
|
opts.path = opts.path.slice(0, -1)
|
|
@@ -246,6 +243,14 @@ module.exports = class Page extends Model {
|
|
|
opts.path = opts.path.slice(1)
|
|
|
}
|
|
|
|
|
|
+ // -> Validate path
|
|
|
+ if (!pageRegex.test(opts.path)) {
|
|
|
+ throw new Error('ERR_INVALID_PATH')
|
|
|
+ }
|
|
|
+
|
|
|
+ opts.path = opts.path.toLowerCase()
|
|
|
+ const dotPath = opts.path.replaceAll('/', '.').replaceAll('-', '_')
|
|
|
+
|
|
|
// -> Check for page access
|
|
|
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
|
|
|
locale: opts.locale,
|
|
@@ -279,41 +284,52 @@ module.exports = class Page extends Model {
|
|
|
}
|
|
|
|
|
|
// -> Format JS Scripts
|
|
|
- let scriptJs = ''
|
|
|
+ let scriptJsLoad = ''
|
|
|
+ let scriptJsUnload = ''
|
|
|
if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
|
|
|
locale: opts.locale,
|
|
|
path: opts.path
|
|
|
})) {
|
|
|
- scriptJs = opts.scriptJs || ''
|
|
|
+ scriptJsLoad = opts.scriptJsLoad || ''
|
|
|
+ scriptJsUnload = opts.scriptJsUnload || ''
|
|
|
}
|
|
|
|
|
|
// -> Create page
|
|
|
- await WIKI.db.pages.query().insert({
|
|
|
+ const page = await WIKI.db.pages.query().insert({
|
|
|
authorId: opts.user.id,
|
|
|
content: opts.content,
|
|
|
creatorId: opts.user.id,
|
|
|
- contentType: _.get(WIKI.data.editors[opts.editor], 'contentType', 'text'),
|
|
|
+ config: {
|
|
|
+ allowComments: opts.allowComments ?? true,
|
|
|
+ allowContributions: opts.allowContributions ?? true,
|
|
|
+ allowRatings: opts.allowRatings ?? true,
|
|
|
+ showSidebar: opts.showSidebar ?? true,
|
|
|
+ showTags: opts.showTags ?? true,
|
|
|
+ showToc: opts.showToc ?? true,
|
|
|
+ tocDepth: opts.tocDepth ?? WIKI.sites[opts.siteId].config?.defaults.tocDepth
|
|
|
+ },
|
|
|
+ contentType: WIKI.data.editors[opts.editor]?.contentType ?? 'text',
|
|
|
description: opts.description,
|
|
|
+ dotPath: dotPath,
|
|
|
editor: opts.editor,
|
|
|
hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale }),
|
|
|
- publishState: opts.publishState,
|
|
|
+ icon: opts.icon,
|
|
|
+ isBrowsable: opts.isBrowsable ?? true,
|
|
|
localeCode: opts.locale,
|
|
|
path: opts.path,
|
|
|
+ publishState: opts.publishState,
|
|
|
publishEndDate: opts.publishEndDate?.toISO(),
|
|
|
publishStartDate: opts.publishStartDate?.toISO(),
|
|
|
+ relations: opts.relations ?? [],
|
|
|
siteId: opts.siteId,
|
|
|
title: opts.title,
|
|
|
toc: '[]',
|
|
|
- extra: JSON.stringify({
|
|
|
- js: scriptJs,
|
|
|
+ scripts: JSON.stringify({
|
|
|
+ jsLoad: scriptJsLoad,
|
|
|
+ jsUnload: scriptJsUnload,
|
|
|
css: scriptCss
|
|
|
})
|
|
|
- })
|
|
|
- const page = await WIKI.db.pages.getPageFromDb({
|
|
|
- path: opts.path,
|
|
|
- locale: opts.locale,
|
|
|
- userId: opts.user.id
|
|
|
- })
|
|
|
+ }).returning('*')
|
|
|
|
|
|
// -> Save Tags
|
|
|
if (opts.tags && opts.tags.length > 0) {
|
|
@@ -365,7 +381,7 @@ module.exports = class Page extends Model {
|
|
|
// -> Fetch original page
|
|
|
const ogPage = await WIKI.db.pages.query().findById(opts.id)
|
|
|
if (!ogPage) {
|
|
|
- throw new Error('Invalid Page Id')
|
|
|
+ throw new Error('ERR_PAGE_NOT_FOUND')
|
|
|
}
|
|
|
|
|
|
// -> Check for page access
|
|
@@ -373,70 +389,205 @@ module.exports = class Page extends Model {
|
|
|
locale: ogPage.localeCode,
|
|
|
path: ogPage.path
|
|
|
})) {
|
|
|
- throw new WIKI.Error.PageUpdateForbidden()
|
|
|
+ throw new Error('ERR_PAGE_UPDATE_FORBIDDEN')
|
|
|
}
|
|
|
|
|
|
- // -> Check for empty content
|
|
|
- if (!opts.content || _.trim(opts.content).length < 1) {
|
|
|
- throw new WIKI.Error.PageEmptyContent()
|
|
|
+ const patch = {}
|
|
|
+ const historyData = {
|
|
|
+ action: 'updated',
|
|
|
+ affectedFields: []
|
|
|
}
|
|
|
|
|
|
// -> Create version snapshot
|
|
|
- await WIKI.db.pageHistory.addVersion({
|
|
|
- ...ogPage,
|
|
|
- action: opts.action ? opts.action : 'updated',
|
|
|
- versionDate: ogPage.updatedAt
|
|
|
- })
|
|
|
+ await WIKI.db.pageHistory.addVersion(ogPage)
|
|
|
+
|
|
|
+ // -> Basic fields
|
|
|
+ if ('title' in opts.patch) {
|
|
|
+ patch.title = opts.patch.title.trim()
|
|
|
+ historyData.affectedFields.push('title')
|
|
|
|
|
|
- // -> Format Extra Properties
|
|
|
- if (!_.isPlainObject(ogPage.extra)) {
|
|
|
- ogPage.extra = {}
|
|
|
+ if (patch.title.length < 1) {
|
|
|
+ throw new Error('ERR_PAGE_TITLE_MISSING')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ('description' in opts.patch) {
|
|
|
+ patch.description = opts.patch.description.trim()
|
|
|
+ historyData.affectedFields.push('description')
|
|
|
+ }
|
|
|
+
|
|
|
+ if ('icon' in opts.patch) {
|
|
|
+ patch.icon = opts.patch.icon.trim()
|
|
|
+ historyData.affectedFields.push('icon')
|
|
|
+ }
|
|
|
+
|
|
|
+ if ('content' in opts.patch) {
|
|
|
+ patch.content = opts.patch.content
|
|
|
+ historyData.affectedFields.push('content')
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Publish State
|
|
|
+ if (opts.patch.publishState) {
|
|
|
+ patch.publishState = opts.patch.publishState
|
|
|
+ historyData.affectedFields.push('publishState')
|
|
|
+
|
|
|
+ if (patch.publishState === 'scheduled' && (!opts.patch.publishStartDate || !opts.patch.publishEndDate)) {
|
|
|
+ throw new Error('ERR_PAGE_MISSING_SCHEDULED_DATES')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (opts.patch.publishStartDate) {
|
|
|
+ patch.publishStartDate = opts.patch.publishStartDate
|
|
|
+ historyData.affectedFields.push('publishStartDate')
|
|
|
+ }
|
|
|
+ if (opts.patch.publishEndDate) {
|
|
|
+ patch.publishEndDate = opts.patch.publishEndDate
|
|
|
+ historyData.affectedFields.push('publishEndDate')
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Page Config
|
|
|
+ if ('isBrowsable' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ isBrowsable: opts.patch.isBrowsable
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('isBrowsable')
|
|
|
+ }
|
|
|
+ if ('allowComments' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ allowComments: opts.patch.allowComments
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('allowComments')
|
|
|
+ }
|
|
|
+ if ('allowContributions' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ allowContributions: opts.patch.allowContributions
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('allowContributions')
|
|
|
+ }
|
|
|
+ if ('allowRatings' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ allowRatings: opts.patch.allowRatings
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('allowRatings')
|
|
|
+ }
|
|
|
+ if ('showSidebar' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ showSidebar: opts.patch.showSidebar
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('showSidebar')
|
|
|
+ }
|
|
|
+ if ('showTags' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ showTags: opts.patch.showTags
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('showTags')
|
|
|
+ }
|
|
|
+ if ('showToc' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ showToc: opts.patch.showToc
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('showToc')
|
|
|
+ }
|
|
|
+ if ('tocDepth' in opts.patch) {
|
|
|
+ patch.config = {
|
|
|
+ ...patch.config ?? ogPage.config ?? {},
|
|
|
+ tocDepth: opts.patch.tocDepth
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('tocDepth')
|
|
|
+
|
|
|
+ if (patch.config.tocDepth?.min < 1 || patch.config.tocDepth?.min > 6) {
|
|
|
+ throw new Error('ERR_PAGE_INVALID_TOC_DEPTH')
|
|
|
+ }
|
|
|
+ if (patch.config.tocDepth?.max < 1 || patch.config.tocDepth?.max > 6) {
|
|
|
+ throw new Error('ERR_PAGE_INVALID_TOC_DEPTH')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Relations
|
|
|
+ if ('relations' in opts.patch) {
|
|
|
+ patch.relations = opts.patch.relations.map(r => {
|
|
|
+ if (r.label.length < 1) {
|
|
|
+ throw new Error('ERR_PAGE_RELATION_LABEL_MISSING')
|
|
|
+ } else if (r.label.length > 255) {
|
|
|
+ throw new Error('ERR_PAGE_RELATION_LABEL_TOOLONG')
|
|
|
+ } else if (r.icon.length > 255) {
|
|
|
+ throw new Error('ERR_PAGE_RELATION_ICON_INVALID')
|
|
|
+ } else if (r.target.length > 1024) {
|
|
|
+ throw new Error('ERR_PAGE_RELATION_TARGET_INVALID')
|
|
|
+ }
|
|
|
+ return r
|
|
|
+ })
|
|
|
+ historyData.affectedFields.push('relations')
|
|
|
}
|
|
|
|
|
|
// -> Format CSS Scripts
|
|
|
- let scriptCss = _.get(ogPage, 'extra.css', '')
|
|
|
- if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {
|
|
|
- locale: opts.locale,
|
|
|
- path: opts.path
|
|
|
- })) {
|
|
|
- if (!_.isEmpty(opts.scriptCss)) {
|
|
|
- scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles
|
|
|
- } else {
|
|
|
- scriptCss = ''
|
|
|
+ if (opts.patch.scriptCss) {
|
|
|
+ if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {
|
|
|
+ locale: ogPage.localeCode,
|
|
|
+ path: ogPage.path
|
|
|
+ })) {
|
|
|
+ patch.scripts = {
|
|
|
+ ...patch.scripts ?? ogPage.scripts ?? {},
|
|
|
+ css: !_.isEmpty(opts.patch.scriptCss) ? new CleanCSS({ inline: false }).minify(opts.patch.scriptCss).styles : ''
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('scripts.css')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// -> Format JS Scripts
|
|
|
- let scriptJs = _.get(ogPage, 'extra.js', '')
|
|
|
- if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
|
|
|
- locale: opts.locale,
|
|
|
- path: opts.path
|
|
|
- })) {
|
|
|
- scriptJs = opts.scriptJs || ''
|
|
|
+ if (opts.patch.scriptJsLoad) {
|
|
|
+ if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
|
|
|
+ locale: ogPage.localeCode,
|
|
|
+ path: ogPage.path
|
|
|
+ })) {
|
|
|
+ patch.scripts = {
|
|
|
+ ...patch.scripts ?? ogPage.scripts ?? {},
|
|
|
+ jsLoad: opts.patch.scriptJsLoad ?? ''
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('scripts.jsLoad')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (opts.patch.scriptJsUnload) {
|
|
|
+ if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
|
|
|
+ locale: ogPage.localeCode,
|
|
|
+ path: ogPage.path
|
|
|
+ })) {
|
|
|
+ patch.scripts = {
|
|
|
+ ...patch.scripts ?? ogPage.scripts ?? {},
|
|
|
+ jsUnload: opts.patch.scriptJsUnload ?? ''
|
|
|
+ }
|
|
|
+ historyData.affectedFields.push('scripts.jsUnload')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // -> Tags
|
|
|
+ if ('tags' in opts.patch) {
|
|
|
+ historyData.affectedFields.push('tags')
|
|
|
}
|
|
|
|
|
|
// -> Update page
|
|
|
await WIKI.db.pages.query().patch({
|
|
|
+ ...patch,
|
|
|
authorId: opts.user.id,
|
|
|
- content: opts.content,
|
|
|
- description: opts.description,
|
|
|
- publishState: opts.publishState,
|
|
|
- publishEndDate: opts.publishEndDate?.toISO(),
|
|
|
- publishStartDate: opts.publishStartDate?.toISO(),
|
|
|
- title: opts.title,
|
|
|
- extra: JSON.stringify({
|
|
|
- ...ogPage.extra,
|
|
|
- js: scriptJs,
|
|
|
- css: scriptCss
|
|
|
- })
|
|
|
+ historyData
|
|
|
}).where('id', ogPage.id)
|
|
|
let page = await WIKI.db.pages.getPageFromDb(ogPage.id)
|
|
|
|
|
|
// -> Save Tags
|
|
|
- await WIKI.db.tags.associateTags({ tags: opts.tags, page })
|
|
|
+ if (opts.patch.tags) {
|
|
|
+ await WIKI.db.tags.associateTags({ tags: opts.patch.tags, page })
|
|
|
+ }
|
|
|
|
|
|
// -> Render page to HTML
|
|
|
- await WIKI.db.pages.renderPage(page)
|
|
|
+ if (opts.patch.content) {
|
|
|
+ await WIKI.db.pages.renderPage(page)
|
|
|
+ }
|
|
|
WIKI.events.outbound.emit('deletePageFromCache', page.hash)
|
|
|
|
|
|
// // -> Update Search Index
|
|
@@ -468,11 +619,6 @@ module.exports = class Page extends Model {
|
|
|
destinationPath: opts.path,
|
|
|
user: opts.user
|
|
|
})
|
|
|
- } else {
|
|
|
- // -> Update title of page tree entry
|
|
|
- await WIKI.db.knex.table('pageTree').where({
|
|
|
- pageId: page.id
|
|
|
- }).update('title', page.title)
|
|
|
}
|
|
|
|
|
|
// -> Get latest updatedAt
|
|
@@ -944,6 +1090,8 @@ module.exports = class Page extends Model {
|
|
|
* @returns {Promise} Promise of the Page Model Instance
|
|
|
*/
|
|
|
static async getPage(opts) {
|
|
|
+ return WIKI.db.pages.getPageFromDb(opts)
|
|
|
+
|
|
|
// -> Get from cache first
|
|
|
let page = await WIKI.db.pages.getPageFromCache(opts)
|
|
|
if (!page) {
|
|
@@ -974,26 +1122,7 @@ module.exports = class Page extends Model {
|
|
|
try {
|
|
|
return WIKI.db.pages.query()
|
|
|
.column([
|
|
|
- 'pages.id',
|
|
|
- 'pages.path',
|
|
|
- 'pages.hash',
|
|
|
- 'pages.title',
|
|
|
- 'pages.description',
|
|
|
- 'pages.publishState',
|
|
|
- 'pages.publishStartDate',
|
|
|
- 'pages.publishEndDate',
|
|
|
- 'pages.content',
|
|
|
- 'pages.render',
|
|
|
- 'pages.toc',
|
|
|
- 'pages.contentType',
|
|
|
- 'pages.createdAt',
|
|
|
- 'pages.updatedAt',
|
|
|
- 'pages.editor',
|
|
|
- 'pages.localeCode',
|
|
|
- 'pages.authorId',
|
|
|
- 'pages.creatorId',
|
|
|
- 'pages.siteId',
|
|
|
- 'pages.extra',
|
|
|
+ 'pages.*',
|
|
|
{
|
|
|
authorName: 'author.name',
|
|
|
authorEmail: 'author.email',
|