wikijs-fork/server/models/pages.js

969 lines
27 KiB
JavaScript
Raw Normal View History

2018-05-20 22:50:51 +00:00
const Model = require('objection').Model
const _ = require('lodash')
const JSBinType = require('js-binary').Type
const pageHelper = require('../helpers/page')
const path = require('path')
const fs = require('fs-extra')
2019-02-25 04:48:28 +00:00
const yaml = require('js-yaml')
const striptags = require('striptags')
const emojiRegex = require('emoji-regex')
const he = require('he')
2020-06-20 05:11:05 +00:00
const CleanCSS = require('clean-css')
2018-05-20 22:50:51 +00:00
/* global WIKI */
2019-02-25 04:48:28 +00:00
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,
2019-02-25 04:48:28 +00:00
markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/
}
const punctuationRegex = /[!,:;/\\_+\-=()&#@<>$~%^*[\]{}"'|]+|(\.\s)|(\s\.)/ig
// const htmlEntitiesRegex = /(&#[0-9]{3};)|(&#x[a-zA-Z0-9]{2};)/ig
2018-05-20 22:50:51 +00:00
/**
* Pages model
*/
module.exports = class Page extends Model {
static get tableName() { return 'pages' }
static get jsonSchema () {
return {
type: 'object',
required: ['path', 'title'],
properties: {
id: {type: 'integer'},
path: {type: 'string'},
hash: {type: 'string'},
2018-05-20 22:50:51 +00:00
title: {type: 'string'},
description: {type: 'string'},
isPublished: {type: 'boolean'},
privateNS: {type: 'string'},
2018-05-20 22:50:51 +00:00
publishStartDate: {type: 'string'},
publishEndDate: {type: 'string'},
content: {type: 'string'},
contentType: {type: 'string'},
2018-05-20 22:50:51 +00:00
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
}
}
}
2020-06-20 20:39:36 +00:00
static get jsonAttributes() {
return ['extra']
}
2018-05-20 22:50:51 +00:00
static get relationMappings() {
return {
tags: {
relation: Model.ManyToManyRelation,
modelClass: require('./tags'),
join: {
from: 'pages.id',
through: {
from: 'pageTags.pageId',
to: 'pageTags.tagId'
},
to: 'tags.id'
}
},
links: {
2019-09-03 02:18:59 +00:00
relation: Model.HasManyRelation,
modelClass: require('./pageLinks'),
join: {
from: 'pages.id',
2019-09-03 02:18:59 +00:00
to: 'pageLinks.pageId'
}
},
2018-05-20 22:50:51 +00:00
author: {
relation: Model.BelongsToOneRelation,
modelClass: require('./users'),
join: {
from: 'pages.authorId',
to: 'users.id'
}
},
creator: {
relation: Model.BelongsToOneRelation,
modelClass: require('./users'),
join: {
from: 'pages.creatorId',
to: 'users.id'
}
},
2018-07-22 04:29:39 +00:00
editor: {
relation: Model.BelongsToOneRelation,
modelClass: require('./editors'),
join: {
from: 'pages.editorKey',
to: 'editors.key'
}
},
2018-05-20 22:50:51 +00:00
locale: {
relation: Model.BelongsToOneRelation,
modelClass: require('./locales'),
join: {
2018-07-22 04:29:39 +00:00
from: 'pages.localeCode',
2018-05-20 22:50:51 +00:00
to: 'locales.code'
}
}
}
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString()
}
$beforeInsert() {
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}
/**
* Cache Schema
*/
static get cacheSchema() {
return new JSBinType({
2019-01-26 23:35:56 +00:00
id: 'uint',
authorId: 'uint',
authorName: 'string',
createdAt: 'string',
creatorId: 'uint',
creatorName: 'string',
description: 'string',
isPrivate: 'boolean',
isPublished: 'boolean',
publishEndDate: 'string',
publishStartDate: 'string',
render: 'string',
tags: [
{
tag: 'string',
title: 'string'
}
],
2020-06-20 20:39:36 +00:00
extra: {
js: 'string',
css: 'string'
},
title: 'string',
toc: 'string',
updatedAt: 'string'
})
}
/**
* Inject page metadata into contents
*
* @returns {string} Page Contents with Injected Metadata
*/
injectMetadata () {
return pageHelper.injectPageMetadata(this)
}
/**
* Get the page's file extension based on content type
*
* @returns {string} File Extension
*/
getFileExtension() {
2019-09-27 18:17:12 +00:00
return pageHelper.getFileExtension(this.contentType)
}
2019-02-25 04:48:28 +00:00
/**
* Parse injected page metadata from raw content
*
* @param {String} raw Raw file contents
* @param {String} contentType Content Type
* @returns {Object} Parsed Page Metadata with Raw Content
2019-02-25 04:48:28 +00:00
*/
static parseMetadata (raw, contentType) {
let result
switch (contentType) {
case 'markdown':
result = frontmatterRegex.markdown.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
} else {
// Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw)
if (result[2]) {
return {
title: result[2],
description: result[4],
content: result[5]
}
}
}
break
case 'html':
result = frontmatterRegex.html.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
}
break
}
return {
content: raw
}
}
/**
* Create a New Page
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async createPage(opts) {
2019-10-13 23:59:50 +00:00
// -> Validate path
if (opts.path.indexOf('.') >= 0 || opts.path.indexOf(' ') >= 0 || opts.path.indexOf('\\') >= 0 || opts.path.indexOf('//') >= 0) {
throw new WIKI.Error.PageIllegalPath()
}
// -> Remove trailing slash
2020-04-25 06:58:00 +00:00
if (opts.path.endsWith('/')) {
opts.path = opts.path.slice(0, -1)
}
// -> Remove starting slash
if (opts.path.startsWith('/')) {
opts.path = opts.path.slice(1)
}
2019-10-13 23:59:50 +00:00
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.locale,
path: opts.path
})) {
throw new WIKI.Error.PageDeleteForbidden()
}
// -> Check for duplicate
2019-06-29 20:45:27 +00:00
const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
if (dupCheck) {
throw new WIKI.Error.PageDuplicateCreate()
}
2019-10-13 23:59:50 +00:00
// -> Check for empty content
2019-08-27 03:19:39 +00:00
if (!opts.content || _.trim(opts.content).length < 1) {
throw new WIKI.Error.PageEmptyContent()
}
2020-06-20 20:39:36 +00:00
// -> Format CSS Scripts
2020-06-20 05:11:05 +00:00
let scriptCss = ''
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 = ''
}
}
// -> Format JS Scripts
let scriptJs = ''
if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
locale: opts.locale,
path: opts.path
})) {
scriptJs = opts.scriptJs || ''
}
2019-10-13 23:59:50 +00:00
// -> Create page
await WIKI.models.pages.query().insert({
2019-10-13 23:59:50 +00:00
authorId: opts.user.id,
content: opts.content,
2019-10-13 23:59:50 +00:00
creatorId: opts.user.id,
2018-08-20 05:02:57 +00:00
contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
description: opts.description,
editorKey: opts.editor,
hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),
isPrivate: opts.isPrivate,
isPublished: opts.isPublished,
localeCode: opts.locale,
path: opts.path,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
title: opts.title,
2020-06-20 05:11:05 +00:00
toc: '[]',
extra: JSON.stringify({
js: scriptJs,
css: scriptCss
})
})
2018-09-16 04:35:03 +00:00
const page = await WIKI.models.pages.getPageFromDb({
path: opts.path,
locale: opts.locale,
2019-10-13 23:59:50 +00:00
userId: opts.user.id,
2018-09-16 04:35:03 +00:00
isPrivate: opts.isPrivate
})
// -> Save Tags
2019-10-07 04:06:47 +00:00
if (opts.tags && opts.tags.length > 0) {
await WIKI.models.tags.associateTags({ tags: opts.tags, page })
}
// -> Render page to HTML
2018-09-10 00:33:10 +00:00
await WIKI.models.pages.renderPage(page)
2019-10-14 02:35:42 +00:00
// -> Rebuild page tree
await WIKI.models.pages.rebuildTree()
// -> Add to Search Index
const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
await WIKI.data.searchEngine.created(page)
// -> Add to Storage
2019-02-25 04:48:28 +00:00
if (!opts.skipStorage) {
await WIKI.models.storage.pageEvent({
event: 'created',
page
})
}
// -> Reconnect Links
await WIKI.models.pages.reconnectLinks({
locale: page.localeCode,
path: page.path,
mode: 'create'
})
2020-03-21 23:18:08 +00:00
// -> Get latest updatedAt
page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
return page
}
/**
* Update an Existing Page
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async updatePage(opts) {
2019-10-13 23:59:50 +00:00
// -> Fetch original page
const ogPage = await WIKI.models.pages.query().findById(opts.id)
if (!ogPage) {
throw new Error('Invalid Page Id')
}
2019-08-27 03:19:39 +00:00
2019-10-13 23:59:50 +00:00
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.locale,
path: opts.path
})) {
throw new WIKI.Error.PageUpdateForbidden()
}
// -> Check for empty content
2019-08-27 03:19:39 +00:00
if (!opts.content || _.trim(opts.content).length < 1) {
throw new WIKI.Error.PageEmptyContent()
}
2019-10-13 23:59:50 +00:00
// -> Create version snapshot
2019-01-26 23:35:56 +00:00
await WIKI.models.pageHistory.addVersion({
...ogPage,
2019-02-22 22:05:18 +00:00
isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,
2020-03-01 04:40:07 +00:00
action: opts.action ? opts.action : 'updated',
versionDate: ogPage.updatedAt
2019-01-26 23:35:56 +00:00
})
2019-10-13 23:59:50 +00:00
2020-06-20 05:11:05 +00:00
// -> Format Extra Properties
if (!_.isPlainObject(ogPage.extra)) {
ogPage.extra = {}
}
2020-06-20 20:39:36 +00:00
// -> Format CSS Scripts
2020-06-20 05:11:05 +00:00
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 = ''
}
}
// -> 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 || ''
}
2019-10-13 23:59:50 +00:00
// -> Update page
await WIKI.models.pages.query().patch({
2019-10-13 23:59:50 +00:00
authorId: opts.user.id,
content: opts.content,
description: opts.description,
2019-02-25 04:48:28 +00:00
isPublished: opts.isPublished === true || opts.isPublished === 1,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
2020-06-20 05:11:05 +00:00
title: opts.title,
extra: JSON.stringify({
...ogPage.extra,
js: scriptJs,
css: scriptCss
})
}).where('id', ogPage.id)
2020-03-21 23:18:08 +00:00
let page = await WIKI.models.pages.getPageFromDb(ogPage.id)
// -> Save Tags
await WIKI.models.tags.associateTags({ tags: opts.tags, page })
// -> Render page to HTML
2018-09-10 00:33:10 +00:00
await WIKI.models.pages.renderPage(page)
2020-04-20 02:41:19 +00:00
WIKI.events.outbound.emit('deletePageFromCache', page.hash)
// -> Update Search Index
const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
await WIKI.data.searchEngine.updated(page)
// -> Update on Storage
2019-02-25 04:48:28 +00:00
if (!opts.skipStorage) {
await WIKI.models.storage.pageEvent({
event: 'updated',
page
})
}
2019-10-13 23:59:50 +00:00
// -> Perform move?
if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {
2019-10-13 23:59:50 +00:00
await WIKI.models.pages.movePage({
id: page.id,
destinationLocale: opts.locale,
destinationPath: opts.path,
user: opts.user
})
2019-10-14 02:35:42 +00:00
} else {
// -> Update title of page tree entry
await WIKI.models.knex.table('pageTree').where({
pageId: page.id
}).update('title', page.title)
2019-10-13 23:59:50 +00:00
}
2020-03-21 23:18:08 +00:00
// -> Get latest updatedAt
page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
return page
}
2019-10-13 23:59:50 +00:00
/**
* Move a Page
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise with no value
*/
static async movePage(opts) {
const page = await WIKI.models.pages.query().findById(opts.id)
if (!page) {
throw new WIKI.Error.PageNotFound()
}
// -> Validate path
if (opts.destinationPath.indexOf('.') >= 0 || opts.destinationPath.indexOf(' ') >= 0 || opts.destinationPath.indexOf('\\') >= 0 || opts.destinationPath.indexOf('//') >= 0) {
throw new WIKI.Error.PageIllegalPath()
}
// -> Remove trailing slash
2020-04-25 06:58:00 +00:00
if (opts.destinationPath.endsWith('/')) {
opts.destinationPath = opts.destinationPath.slice(0, -1)
}
// -> Remove starting slash
if (opts.destinationPath.startsWith('/')) {
opts.destinationPath = opts.destinationPath.slice(1)
}
2019-10-13 23:59:50 +00:00
// -> Check for source page access
if (!WIKI.auth.checkAccess(opts.user, ['manage:pages'], {
locale: page.localeCode,
path: page.path
2019-10-13 23:59:50 +00:00
})) {
throw new WIKI.Error.PageMoveForbidden()
}
// -> Check for destination page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.destinationLocale,
path: opts.destinationPath
})) {
throw new WIKI.Error.PageMoveForbidden()
}
// -> Check for existing page at destination path
const destPage = await await WIKI.models.pages.query().findOne({
path: opts.destinationPath,
localeCode: opts.destinationLocale
})
if (destPage) {
throw new WIKI.Error.PagePathCollision()
}
// -> Create version snapshot
await WIKI.models.pageHistory.addVersion({
...page,
2020-03-01 04:40:07 +00:00
action: 'moved',
versionDate: page.updatedAt
2019-10-13 23:59:50 +00:00
})
const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
// -> Move page
await WIKI.models.pages.query().patch({
path: opts.destinationPath,
localeCode: opts.destinationLocale,
hash: destinationHash
}).findById(page.id)
2020-04-20 00:26:26 +00:00
await WIKI.models.pages.deletePageFromCache(page.hash)
WIKI.events.outbound.emit('deletePageFromCache', page.hash)
2019-10-13 23:59:50 +00:00
2019-10-14 02:35:42 +00:00
// -> Rebuild page tree
await WIKI.models.pages.rebuildTree()
2019-10-13 23:59:50 +00:00
// -> Rename in Search Index
await WIKI.data.searchEngine.renamed({
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
destinationHash
})
// -> Rename in Storage
if (!opts.skipStorage) {
await WIKI.models.storage.pageEvent({
event: 'renamed',
page: {
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
destinationHash,
moveAuthorId: opts.user.id,
moveAuthorName: opts.user.name,
moveAuthorEmail: opts.user.email
}
})
}
// -> Reconnect Links
await WIKI.models.pages.reconnectLinks({
sourceLocale: page.localeCode,
sourcePath: page.path,
locale: opts.destinationLocale,
path: opts.destinationPath,
mode: 'move'
})
}
/**
* Delete an Existing Page
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise with no value
*/
2019-01-26 23:35:56 +00:00
static async deletePage(opts) {
2019-02-25 04:48:28 +00:00
let page
if (_.has(opts, 'id')) {
page = await WIKI.models.pages.query().findById(opts.id)
} else {
page = await await WIKI.models.pages.query().findOne({
path: opts.path,
localeCode: opts.locale
})
}
2019-01-26 23:35:56 +00:00
if (!page) {
throw new Error('Invalid Page Id')
}
2019-10-13 23:59:50 +00:00
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['delete:pages'], {
locale: page.locale,
path: page.path
})) {
throw new WIKI.Error.PageDeleteForbidden()
}
// -> Create version snapshot
2019-01-26 23:35:56 +00:00
await WIKI.models.pageHistory.addVersion({
...page,
2020-03-01 04:40:07 +00:00
action: 'deleted',
versionDate: page.updatedAt
2019-01-26 23:35:56 +00:00
})
2019-10-13 23:59:50 +00:00
// -> Delete page
2019-01-26 23:35:56 +00:00
await WIKI.models.pages.query().delete().where('id', page.id)
2020-04-20 00:26:26 +00:00
await WIKI.models.pages.deletePageFromCache(page.hash)
WIKI.events.outbound.emit('deletePageFromCache', page.hash)
2019-10-14 02:35:42 +00:00
// -> Rebuild page tree
await WIKI.models.pages.rebuildTree()
// -> Delete from Search Index
await WIKI.data.searchEngine.deleted(page)
// -> Delete from Storage
2019-02-25 04:48:28 +00:00
if (!opts.skipStorage) {
await WIKI.models.storage.pageEvent({
event: 'deleted',
page
})
}
// -> Reconnect Links
await WIKI.models.pages.reconnectLinks({
locale: page.localeCode,
path: page.path,
mode: 'delete'
})
}
/**
2019-10-14 02:35:42 +00:00
* Reconnect links to new/move/deleted page
*
* @param {Object} opts - Page parameters
* @param {string} opts.path - Page Path
* @param {string} opts.locale - Page Locale Code
* @param {string} [opts.sourcePath] - Previous Page Path (move only)
* @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only)
2019-10-14 02:35:42 +00:00
* @param {string} opts.mode - Page Update mode (create, move, delete)
* @returns {Promise} Promise with no value
*/
static async reconnectLinks (opts) {
const pageHref = `/${opts.locale}/${opts.path}`
let replaceArgs = {
from: '',
to: ''
}
switch (opts.mode) {
case 'create':
replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
break
case 'move':
const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`
replaceArgs.from = `<a href="${prevPageHref}" class="is-internal-link is-invalid-page">`
replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
break
case 'delete':
replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
break
default:
return false
}
let affectedHashes = []
// -> Perform replace and return affected page hashes (POSTGRES only)
if (WIKI.config.db.type === 'postgres') {
2020-01-26 18:11:33 +00:00
const qryHashes = await WIKI.models.pages.query()
.returning('hash')
.patch({
render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
})
.whereIn('pages.id', function () {
this.select('pageLinks.pageId').from('pageLinks').where({
'pageLinks.path': opts.path,
'pageLinks.localeCode': opts.locale
})
})
2020-01-26 18:11:33 +00:00
affectedHashes = qryHashes.map(h => h.hash)
} else {
// -> Perform replace, then query affected page hashes (MYSQL, MARIADB, MSSQL, SQLITE only)
await WIKI.models.pages.query()
.patch({
render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
})
.whereIn('pages.id', function () {
this.select('pageLinks.pageId').from('pageLinks').where({
'pageLinks.path': opts.path,
'pageLinks.localeCode': opts.locale
})
})
2020-01-26 18:11:33 +00:00
const qryHashes = await WIKI.models.pages.query()
.column('hash')
.whereIn('pages.id', function () {
this.select('pageLinks.pageId').from('pageLinks').where({
'pageLinks.path': opts.path,
'pageLinks.localeCode': opts.locale
})
})
2020-01-26 18:11:33 +00:00
affectedHashes = qryHashes.map(h => h.hash)
}
for (const hash of affectedHashes) {
2020-04-20 00:26:26 +00:00
await WIKI.models.pages.deletePageFromCache(hash)
WIKI.events.outbound.emit('deletePageFromCache', hash)
}
2019-01-26 23:35:56 +00:00
}
2019-10-14 02:35:42 +00:00
/**
* Rebuild page tree for new/updated/deleted page
*
* @returns {Promise} Promise with no value
*/
static async rebuildTree() {
const rebuildJob = await WIKI.scheduler.registerJob({
name: 'rebuild-tree',
immediate: true,
worker: true
})
return rebuildJob.finished
}
/**
* Trigger the rendering of a page
*
* @param {Object} page Page Model Instance
* @returns {Promise} Promise with no value
*/
2018-09-10 00:33:10 +00:00
static async renderPage(page) {
const renderJob = await WIKI.scheduler.registerJob({
name: 'render-page',
immediate: true,
worker: true
}, page.id)
return renderJob.finished
}
/**
* Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async getPage(opts) {
// -> Get from cache first
let page = await WIKI.models.pages.getPageFromCache(opts)
if (!page) {
// -> Get from DB
page = await WIKI.models.pages.getPageFromDb(opts)
2018-09-16 22:36:15 +00:00
if (page) {
if (page.render) {
// -> Save render to cache
await WIKI.models.pages.savePageToCache(page)
} else {
// -> No render? Possible duplicate issue
/* TODO: Detect duplicate and delete */
throw new Error('Error while fetching page. Duplicate entry detected. Reload the page to try again.')
}
2018-09-16 22:36:15 +00:00
}
}
return page
}
/**
* Fetch an Existing Page from the Database
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async getPageFromDb(opts) {
const queryModeID = _.isNumber(opts)
try {
return WIKI.models.pages.query()
.column([
'pages.id',
'pages.path',
'pages.hash',
'pages.title',
'pages.description',
'pages.isPrivate',
'pages.isPublished',
'pages.privateNS',
'pages.publishStartDate',
'pages.publishEndDate',
'pages.content',
'pages.render',
'pages.toc',
'pages.contentType',
'pages.createdAt',
'pages.updatedAt',
'pages.editorKey',
'pages.localeCode',
'pages.authorId',
'pages.creatorId',
2020-06-20 20:39:36 +00:00
'pages.extra',
{
authorName: 'author.name',
authorEmail: 'author.email',
creatorName: 'creator.name',
creatorEmail: 'creator.email'
}
])
2020-01-25 00:20:53 +00:00
.joinRelated('author')
.joinRelated('creator')
2020-01-26 04:29:46 +00:00
.withGraphJoined('tags')
.modifyGraph('tags', builder => {
builder.select('tag', 'title')
})
.where(queryModeID ? {
'pages.id': opts
} : {
'pages.path': opts.path,
'pages.localeCode': opts.locale
})
// .andWhere(builder => {
// if (queryModeID) return
// builder.where({
// 'pages.isPublished': true
// }).orWhere({
// 'pages.isPublished': false,
// 'pages.authorId': opts.userId
// })
// })
// .andWhere(builder => {
// if (queryModeID) return
// if (opts.isPrivate) {
// builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
// } else {
// builder.where({ 'pages.isPrivate': false })
// }
// })
.first()
} catch (err) {
WIKI.logger.warn(err)
throw err
}
}
/**
* Save a Page Model Instance to Cache
*
* @param {Object} page Page Model Instance
* @returns {Promise} Promise with no value
*/
static async savePageToCache(page) {
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${page.hash}.bin`)
await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
2019-01-26 23:35:56 +00:00
id: page.id,
authorId: page.authorId,
authorName: page.authorName,
createdAt: page.createdAt,
creatorId: page.creatorId,
creatorName: page.creatorName,
description: page.description,
2020-06-20 20:39:36 +00:00
extra: {
css: _.get(page, 'extra.css', ''),
js: _.get(page, 'extra.js', '')
2020-06-20 20:39:36 +00:00
},
2018-12-02 04:03:14 +00:00
isPrivate: page.isPrivate === 1 || page.isPrivate === true,
isPublished: page.isPublished === 1 || page.isPublished === true,
publishEndDate: page.publishEndDate,
publishStartDate: page.publishStartDate,
render: page.render,
tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
title: page.title,
2018-12-02 04:03:14 +00:00
toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
updatedAt: page.updatedAt
}))
}
/**
* Fetch an Existing Page from Cache
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async getPageFromCache(opts) {
const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${pageHash}.bin`)
try {
const pageBuffer = await fs.readFile(cachePath)
let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)
return {
...page,
path: opts.path,
localeCode: opts.locale,
isPrivate: opts.isPrivate
}
} catch (err) {
if (err.code === 'ENOENT') {
return false
}
WIKI.logger.error(err)
throw err
}
}
2019-01-26 23:35:56 +00:00
/**
* Delete an Existing Page from Cache
*
2020-04-20 02:41:19 +00:00
* @param {String} page Page Unique Hash
* @returns {Promise} Promise with no value
*/
2020-04-20 02:41:19 +00:00
static async deletePageFromCache(hash) {
return fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))
2019-01-26 23:35:56 +00:00
}
/**
* Flush the contents of the Cache
*/
2019-07-06 21:06:42 +00:00
static async flushCache() {
return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))
2019-07-06 21:06:42 +00:00
}
/**
* Migrate all pages from a source locale to the target locale
*
* @param {Object} opts Migration properties
* @param {string} opts.sourceLocale Source Locale Code
* @param {string} opts.targetLocale Target Locale Code
* @returns {Promise} Promise with no value
*/
static async migrateToLocale({ sourceLocale, targetLocale }) {
return WIKI.models.pages.query()
.patch({
localeCode: targetLocale
})
.where({
localeCode: sourceLocale
})
.whereNotExists(function() {
this.select('id').from('pages AS pagesm').where('pagesm.localeCode', targetLocale).andWhereRaw('pagesm.path = pages.path')
})
}
/**
* Clean raw HTML from content for use in search engines
*
* @param {string} rawHTML Raw HTML
* @returns {string} Cleaned Content Text
*/
static cleanHTML(rawHTML = '') {
let data = striptags(rawHTML || '', [], ' ')
.replace(emojiRegex(), '')
// .replace(htmlEntitiesRegex, '')
return he.decode(data)
.replace(punctuationRegex, ' ')
.replace(/(\r\n|\n|\r)/gm, ' ')
.replace(/\s\s+/g, ' ')
.split(' ').filter(w => w.length > 1).join(' ').toLowerCase()
}
2020-04-20 00:26:26 +00:00
/**
* Subscribe to HA propagation events
*/
static subscribeToEvents() {
WIKI.events.inbound.on('deletePageFromCache', hash => {
WIKI.models.pages.deletePageFromCache(hash)
})
WIKI.events.inbound.on('flushCache', () => {
WIKI.models.pages.flushCache()
})
}
2018-05-20 22:50:51 +00:00
}