You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

975 lines
28 KiB

6 years ago
6 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. const Model = require('objection').Model
  2. const _ = require('lodash')
  3. const JSBinType = require('js-binary').Type
  4. const pageHelper = require('../helpers/page')
  5. const path = require('path')
  6. const fs = require('fs-extra')
  7. const yaml = require('js-yaml')
  8. const striptags = require('striptags')
  9. const emojiRegex = require('emoji-regex')
  10. const he = require('he')
  11. const CleanCSS = require('clean-css')
  12. /* global WIKI */
  13. const frontmatterRegex = {
  14. html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/,
  15. legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
  16. markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/
  17. }
  18. const punctuationRegex = /[!,:;/\\_+\-=()&#@<>$~%^*[\]{}"'|]+|(\.\s)|(\s\.)/ig
  19. // const htmlEntitiesRegex = /(&#[0-9]{3};)|(&#x[a-zA-Z0-9]{2};)/ig
  20. /**
  21. * Pages model
  22. */
  23. module.exports = class Page extends Model {
  24. static get tableName() { return 'pages' }
  25. static get jsonSchema () {
  26. return {
  27. type: 'object',
  28. required: ['path', 'title'],
  29. properties: {
  30. id: {type: 'integer'},
  31. path: {type: 'string'},
  32. hash: {type: 'string'},
  33. title: {type: 'string'},
  34. description: {type: 'string'},
  35. isPublished: {type: 'boolean'},
  36. privateNS: {type: 'string'},
  37. publishStartDate: {type: 'string'},
  38. publishEndDate: {type: 'string'},
  39. content: {type: 'string'},
  40. contentType: {type: 'string'},
  41. createdAt: {type: 'string'},
  42. updatedAt: {type: 'string'}
  43. }
  44. }
  45. }
  46. static get jsonAttributes() {
  47. return ['extra']
  48. }
  49. static get relationMappings() {
  50. return {
  51. tags: {
  52. relation: Model.ManyToManyRelation,
  53. modelClass: require('./tags'),
  54. join: {
  55. from: 'pages.id',
  56. through: {
  57. from: 'pageTags.pageId',
  58. to: 'pageTags.tagId'
  59. },
  60. to: 'tags.id'
  61. }
  62. },
  63. links: {
  64. relation: Model.HasManyRelation,
  65. modelClass: require('./pageLinks'),
  66. join: {
  67. from: 'pages.id',
  68. to: 'pageLinks.pageId'
  69. }
  70. },
  71. author: {
  72. relation: Model.BelongsToOneRelation,
  73. modelClass: require('./users'),
  74. join: {
  75. from: 'pages.authorId',
  76. to: 'users.id'
  77. }
  78. },
  79. creator: {
  80. relation: Model.BelongsToOneRelation,
  81. modelClass: require('./users'),
  82. join: {
  83. from: 'pages.creatorId',
  84. to: 'users.id'
  85. }
  86. },
  87. editor: {
  88. relation: Model.BelongsToOneRelation,
  89. modelClass: require('./editors'),
  90. join: {
  91. from: 'pages.editorKey',
  92. to: 'editors.key'
  93. }
  94. },
  95. locale: {
  96. relation: Model.BelongsToOneRelation,
  97. modelClass: require('./locales'),
  98. join: {
  99. from: 'pages.localeCode',
  100. to: 'locales.code'
  101. }
  102. }
  103. }
  104. }
  105. $beforeUpdate() {
  106. this.updatedAt = new Date().toISOString()
  107. }
  108. $beforeInsert() {
  109. this.createdAt = new Date().toISOString()
  110. this.updatedAt = new Date().toISOString()
  111. }
  112. /**
  113. * Cache Schema
  114. */
  115. static get cacheSchema() {
  116. return new JSBinType({
  117. id: 'uint',
  118. authorId: 'uint',
  119. authorName: 'string',
  120. createdAt: 'string',
  121. creatorId: 'uint',
  122. creatorName: 'string',
  123. description: 'string',
  124. isPrivate: 'boolean',
  125. isPublished: 'boolean',
  126. publishEndDate: 'string',
  127. publishStartDate: 'string',
  128. render: 'string',
  129. tags: [
  130. {
  131. tag: 'string',
  132. title: 'string'
  133. }
  134. ],
  135. extra: {
  136. js: 'string',
  137. css: 'string'
  138. },
  139. title: 'string',
  140. toc: 'string',
  141. updatedAt: 'string'
  142. })
  143. }
  144. /**
  145. * Inject page metadata into contents
  146. *
  147. * @returns {string} Page Contents with Injected Metadata
  148. */
  149. injectMetadata () {
  150. return pageHelper.injectPageMetadata(this)
  151. }
  152. /**
  153. * Get the page's file extension based on content type
  154. *
  155. * @returns {string} File Extension
  156. */
  157. getFileExtension() {
  158. return pageHelper.getFileExtension(this.contentType)
  159. }
  160. /**
  161. * Parse injected page metadata from raw content
  162. *
  163. * @param {String} raw Raw file contents
  164. * @param {String} contentType Content Type
  165. * @returns {Object} Parsed Page Metadata with Raw Content
  166. */
  167. static parseMetadata (raw, contentType) {
  168. let result
  169. switch (contentType) {
  170. case 'markdown':
  171. result = frontmatterRegex.markdown.exec(raw)
  172. if (result[2]) {
  173. return {
  174. ...yaml.safeLoad(result[2]),
  175. content: result[3]
  176. }
  177. } else {
  178. // Attempt legacy v1 format
  179. result = frontmatterRegex.legacy.exec(raw)
  180. if (result[2]) {
  181. return {
  182. title: result[2],
  183. description: result[4],
  184. content: result[5]
  185. }
  186. }
  187. }
  188. break
  189. case 'html':
  190. result = frontmatterRegex.html.exec(raw)
  191. if (result[2]) {
  192. return {
  193. ...yaml.safeLoad(result[2]),
  194. content: result[3]
  195. }
  196. }
  197. break
  198. }
  199. return {
  200. content: raw
  201. }
  202. }
  203. /**
  204. * Create a New Page
  205. *
  206. * @param {Object} opts Page Properties
  207. * @returns {Promise} Promise of the Page Model Instance
  208. */
  209. static async createPage(opts) {
  210. // -> Validate path
  211. if (opts.path.indexOf('.') >= 0 || opts.path.indexOf(' ') >= 0 || opts.path.indexOf('\\') >= 0 || opts.path.indexOf('//') >= 0) {
  212. throw new WIKI.Error.PageIllegalPath()
  213. }
  214. // -> Remove trailing slash
  215. if (opts.path.endsWith('/')) {
  216. opts.path = opts.path.slice(0, -1)
  217. }
  218. // -> Remove starting slash
  219. if (opts.path.startsWith('/')) {
  220. opts.path = opts.path.slice(1)
  221. }
  222. // -> Check for page access
  223. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  224. locale: opts.locale,
  225. path: opts.path
  226. })) {
  227. throw new WIKI.Error.PageDeleteForbidden()
  228. }
  229. // -> Check for duplicate
  230. const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
  231. if (dupCheck) {
  232. throw new WIKI.Error.PageDuplicateCreate()
  233. }
  234. // -> Check for empty content
  235. if (!opts.content || _.trim(opts.content).length < 1) {
  236. throw new WIKI.Error.PageEmptyContent()
  237. }
  238. // -> Format CSS Scripts
  239. let scriptCss = ''
  240. if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {
  241. locale: opts.locale,
  242. path: opts.path
  243. })) {
  244. if (!_.isEmpty(opts.scriptCss)) {
  245. scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles
  246. } else {
  247. scriptCss = ''
  248. }
  249. }
  250. // -> Format JS Scripts
  251. let scriptJs = ''
  252. if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
  253. locale: opts.locale,
  254. path: opts.path
  255. })) {
  256. scriptJs = opts.scriptJs || ''
  257. }
  258. // -> Create page
  259. await WIKI.models.pages.query().insert({
  260. authorId: opts.user.id,
  261. content: opts.content,
  262. creatorId: opts.user.id,
  263. contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
  264. description: opts.description,
  265. editorKey: opts.editor,
  266. hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),
  267. isPrivate: opts.isPrivate,
  268. isPublished: opts.isPublished,
  269. localeCode: opts.locale,
  270. path: opts.path,
  271. publishEndDate: opts.publishEndDate || '',
  272. publishStartDate: opts.publishStartDate || '',
  273. title: opts.title,
  274. toc: '[]',
  275. extra: JSON.stringify({
  276. js: scriptJs,
  277. css: scriptCss
  278. })
  279. })
  280. const page = await WIKI.models.pages.getPageFromDb({
  281. path: opts.path,
  282. locale: opts.locale,
  283. userId: opts.user.id,
  284. isPrivate: opts.isPrivate
  285. })
  286. // -> Save Tags
  287. if (opts.tags && opts.tags.length > 0) {
  288. await WIKI.models.tags.associateTags({ tags: opts.tags, page })
  289. }
  290. // -> Render page to HTML
  291. await WIKI.models.pages.renderPage(page)
  292. // -> Rebuild page tree
  293. await WIKI.models.pages.rebuildTree()
  294. // -> Add to Search Index
  295. const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
  296. page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
  297. await WIKI.data.searchEngine.created(page)
  298. // -> Add to Storage
  299. if (!opts.skipStorage) {
  300. await WIKI.models.storage.pageEvent({
  301. event: 'created',
  302. page
  303. })
  304. }
  305. // -> Reconnect Links
  306. await WIKI.models.pages.reconnectLinks({
  307. locale: page.localeCode,
  308. path: page.path,
  309. mode: 'create'
  310. })
  311. // -> Get latest updatedAt
  312. page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
  313. return page
  314. }
  315. /**
  316. * Update an Existing Page
  317. *
  318. * @param {Object} opts Page Properties
  319. * @returns {Promise} Promise of the Page Model Instance
  320. */
  321. static async updatePage(opts) {
  322. // -> Fetch original page
  323. const ogPage = await WIKI.models.pages.query().findById(opts.id)
  324. if (!ogPage) {
  325. throw new Error('Invalid Page Id')
  326. }
  327. // -> Check for page access
  328. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  329. locale: opts.locale,
  330. path: opts.path
  331. })) {
  332. throw new WIKI.Error.PageUpdateForbidden()
  333. }
  334. // -> Check for empty content
  335. if (!opts.content || _.trim(opts.content).length < 1) {
  336. throw new WIKI.Error.PageEmptyContent()
  337. }
  338. // -> Create version snapshot
  339. await WIKI.models.pageHistory.addVersion({
  340. ...ogPage,
  341. isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,
  342. action: opts.action ? opts.action : 'updated',
  343. versionDate: ogPage.updatedAt
  344. })
  345. // -> Format Extra Properties
  346. if (!_.isPlainObject(ogPage.extra)) {
  347. ogPage.extra = {}
  348. }
  349. // -> Format CSS Scripts
  350. let scriptCss = _.get(ogPage, 'extra.css', '')
  351. if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {
  352. locale: opts.locale,
  353. path: opts.path
  354. })) {
  355. if (!_.isEmpty(opts.scriptCss)) {
  356. scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles
  357. } else {
  358. scriptCss = ''
  359. }
  360. }
  361. // -> Format JS Scripts
  362. let scriptJs = _.get(ogPage, 'extra.js', '')
  363. if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
  364. locale: opts.locale,
  365. path: opts.path
  366. })) {
  367. scriptJs = opts.scriptJs || ''
  368. }
  369. // -> Update page
  370. await WIKI.models.pages.query().patch({
  371. authorId: opts.user.id,
  372. content: opts.content,
  373. description: opts.description,
  374. isPublished: opts.isPublished === true || opts.isPublished === 1,
  375. publishEndDate: opts.publishEndDate || '',
  376. publishStartDate: opts.publishStartDate || '',
  377. title: opts.title,
  378. extra: JSON.stringify({
  379. ...ogPage.extra,
  380. js: scriptJs,
  381. css: scriptCss
  382. })
  383. }).where('id', ogPage.id)
  384. let page = await WIKI.models.pages.getPageFromDb(ogPage.id)
  385. // -> Save Tags
  386. await WIKI.models.tags.associateTags({ tags: opts.tags, page })
  387. // -> Render page to HTML
  388. await WIKI.models.pages.renderPage(page)
  389. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  390. // -> Update Search Index
  391. const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
  392. page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
  393. await WIKI.data.searchEngine.updated(page)
  394. // -> Update on Storage
  395. if (!opts.skipStorage) {
  396. await WIKI.models.storage.pageEvent({
  397. event: 'updated',
  398. page
  399. })
  400. }
  401. // -> Perform move?
  402. if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {
  403. await WIKI.models.pages.movePage({
  404. id: page.id,
  405. destinationLocale: opts.locale,
  406. destinationPath: opts.path,
  407. user: opts.user
  408. })
  409. } else {
  410. // -> Update title of page tree entry
  411. await WIKI.models.knex.table('pageTree').where({
  412. pageId: page.id
  413. }).update('title', page.title)
  414. }
  415. // -> Get latest updatedAt
  416. page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
  417. return page
  418. }
  419. /**
  420. * Move a Page
  421. *
  422. * @param {Object} opts Page Properties
  423. * @returns {Promise} Promise with no value
  424. */
  425. static async movePage(opts) {
  426. const page = await WIKI.models.pages.query().findById(opts.id)
  427. if (!page) {
  428. throw new WIKI.Error.PageNotFound()
  429. }
  430. // -> Validate path
  431. if (opts.destinationPath.indexOf('.') >= 0 || opts.destinationPath.indexOf(' ') >= 0 || opts.destinationPath.indexOf('\\') >= 0 || opts.destinationPath.indexOf('//') >= 0) {
  432. throw new WIKI.Error.PageIllegalPath()
  433. }
  434. // -> Remove trailing slash
  435. if (opts.destinationPath.endsWith('/')) {
  436. opts.destinationPath = opts.destinationPath.slice(0, -1)
  437. }
  438. // -> Remove starting slash
  439. if (opts.destinationPath.startsWith('/')) {
  440. opts.destinationPath = opts.destinationPath.slice(1)
  441. }
  442. // -> Check for source page access
  443. if (!WIKI.auth.checkAccess(opts.user, ['manage:pages'], {
  444. locale: page.localeCode,
  445. path: page.path
  446. })) {
  447. throw new WIKI.Error.PageMoveForbidden()
  448. }
  449. // -> Check for destination page access
  450. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  451. locale: opts.destinationLocale,
  452. path: opts.destinationPath
  453. })) {
  454. throw new WIKI.Error.PageMoveForbidden()
  455. }
  456. // -> Check for existing page at destination path
  457. const destPage = await await WIKI.models.pages.query().findOne({
  458. path: opts.destinationPath,
  459. localeCode: opts.destinationLocale
  460. })
  461. if (destPage) {
  462. throw new WIKI.Error.PagePathCollision()
  463. }
  464. // -> Create version snapshot
  465. await WIKI.models.pageHistory.addVersion({
  466. ...page,
  467. action: 'moved',
  468. versionDate: page.updatedAt
  469. })
  470. const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
  471. // -> Move page
  472. await WIKI.models.pages.query().patch({
  473. path: opts.destinationPath,
  474. localeCode: opts.destinationLocale,
  475. hash: destinationHash
  476. }).findById(page.id)
  477. await WIKI.models.pages.deletePageFromCache(page.hash)
  478. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  479. // -> Rebuild page tree
  480. await WIKI.models.pages.rebuildTree()
  481. // -> Rename in Search Index
  482. await WIKI.data.searchEngine.renamed({
  483. ...page,
  484. destinationPath: opts.destinationPath,
  485. destinationLocaleCode: opts.destinationLocale,
  486. destinationHash
  487. })
  488. // -> Rename in Storage
  489. if (!opts.skipStorage) {
  490. await WIKI.models.storage.pageEvent({
  491. event: 'renamed',
  492. page: {
  493. ...page,
  494. destinationPath: opts.destinationPath,
  495. destinationLocaleCode: opts.destinationLocale,
  496. destinationHash,
  497. moveAuthorId: opts.user.id,
  498. moveAuthorName: opts.user.name,
  499. moveAuthorEmail: opts.user.email
  500. }
  501. })
  502. }
  503. // -> Reconnect Links : Changing old links to the new path
  504. await WIKI.models.pages.reconnectLinks({
  505. sourceLocale: page.localeCode,
  506. sourcePath: page.path,
  507. locale: opts.destinationLocale,
  508. path: opts.destinationPath,
  509. mode: 'move'
  510. })
  511. // -> Reconnect Links : Validate invalid links to the new path
  512. await WIKI.models.pages.reconnectLinks({
  513. locale: opts.destinationLocale,
  514. path: opts.destinationPath,
  515. mode: 'create'
  516. })
  517. }
  518. /**
  519. * Delete an Existing Page
  520. *
  521. * @param {Object} opts Page Properties
  522. * @returns {Promise} Promise with no value
  523. */
  524. static async deletePage(opts) {
  525. let page
  526. if (_.has(opts, 'id')) {
  527. page = await WIKI.models.pages.query().findById(opts.id)
  528. } else {
  529. page = await await WIKI.models.pages.query().findOne({
  530. path: opts.path,
  531. localeCode: opts.locale
  532. })
  533. }
  534. if (!page) {
  535. throw new Error('Invalid Page Id')
  536. }
  537. // -> Check for page access
  538. if (!WIKI.auth.checkAccess(opts.user, ['delete:pages'], {
  539. locale: page.locale,
  540. path: page.path
  541. })) {
  542. throw new WIKI.Error.PageDeleteForbidden()
  543. }
  544. // -> Create version snapshot
  545. await WIKI.models.pageHistory.addVersion({
  546. ...page,
  547. action: 'deleted',
  548. versionDate: page.updatedAt
  549. })
  550. // -> Delete page
  551. await WIKI.models.pages.query().delete().where('id', page.id)
  552. await WIKI.models.pages.deletePageFromCache(page.hash)
  553. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  554. // -> Rebuild page tree
  555. await WIKI.models.pages.rebuildTree()
  556. // -> Delete from Search Index
  557. await WIKI.data.searchEngine.deleted(page)
  558. // -> Delete from Storage
  559. if (!opts.skipStorage) {
  560. await WIKI.models.storage.pageEvent({
  561. event: 'deleted',
  562. page
  563. })
  564. }
  565. // -> Reconnect Links
  566. await WIKI.models.pages.reconnectLinks({
  567. locale: page.localeCode,
  568. path: page.path,
  569. mode: 'delete'
  570. })
  571. }
  572. /**
  573. * Reconnect links to new/move/deleted page
  574. *
  575. * @param {Object} opts - Page parameters
  576. * @param {string} opts.path - Page Path
  577. * @param {string} opts.locale - Page Locale Code
  578. * @param {string} [opts.sourcePath] - Previous Page Path (move only)
  579. * @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only)
  580. * @param {string} opts.mode - Page Update mode (create, move, delete)
  581. * @returns {Promise} Promise with no value
  582. */
  583. static async reconnectLinks (opts) {
  584. const pageHref = `/${opts.locale}/${opts.path}`
  585. let replaceArgs = {
  586. from: '',
  587. to: ''
  588. }
  589. switch (opts.mode) {
  590. case 'create':
  591. replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
  592. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  593. break
  594. case 'move':
  595. const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`
  596. replaceArgs.from = `<a href="${prevPageHref}" class="is-internal-link is-valid-page">`
  597. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  598. break
  599. case 'delete':
  600. replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  601. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
  602. break
  603. default:
  604. return false
  605. }
  606. let affectedHashes = []
  607. // -> Perform replace and return affected page hashes (POSTGRES only)
  608. if (WIKI.config.db.type === 'postgres') {
  609. const qryHashes = await WIKI.models.pages.query()
  610. .returning('hash')
  611. .patch({
  612. render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
  613. })
  614. .whereIn('pages.id', function () {
  615. this.select('pageLinks.pageId').from('pageLinks').where({
  616. 'pageLinks.path': opts.path,
  617. 'pageLinks.localeCode': opts.locale
  618. })
  619. })
  620. affectedHashes = qryHashes.map(h => h.hash)
  621. } else {
  622. // -> Perform replace, then query affected page hashes (MYSQL, MARIADB, MSSQL, SQLITE only)
  623. await WIKI.models.pages.query()
  624. .patch({
  625. render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
  626. })
  627. .whereIn('pages.id', function () {
  628. this.select('pageLinks.pageId').from('pageLinks').where({
  629. 'pageLinks.path': opts.path,
  630. 'pageLinks.localeCode': opts.locale
  631. })
  632. })
  633. const qryHashes = await WIKI.models.pages.query()
  634. .column('hash')
  635. .whereIn('pages.id', function () {
  636. this.select('pageLinks.pageId').from('pageLinks').where({
  637. 'pageLinks.path': opts.path,
  638. 'pageLinks.localeCode': opts.locale
  639. })
  640. })
  641. affectedHashes = qryHashes.map(h => h.hash)
  642. }
  643. for (const hash of affectedHashes) {
  644. await WIKI.models.pages.deletePageFromCache(hash)
  645. WIKI.events.outbound.emit('deletePageFromCache', hash)
  646. }
  647. }
  648. /**
  649. * Rebuild page tree for new/updated/deleted page
  650. *
  651. * @returns {Promise} Promise with no value
  652. */
  653. static async rebuildTree() {
  654. const rebuildJob = await WIKI.scheduler.registerJob({
  655. name: 'rebuild-tree',
  656. immediate: true,
  657. worker: true
  658. })
  659. return rebuildJob.finished
  660. }
  661. /**
  662. * Trigger the rendering of a page
  663. *
  664. * @param {Object} page Page Model Instance
  665. * @returns {Promise} Promise with no value
  666. */
  667. static async renderPage(page) {
  668. const renderJob = await WIKI.scheduler.registerJob({
  669. name: 'render-page',
  670. immediate: true,
  671. worker: true
  672. }, page.id)
  673. return renderJob.finished
  674. }
  675. /**
  676. * Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache
  677. *
  678. * @param {Object} opts Page Properties
  679. * @returns {Promise} Promise of the Page Model Instance
  680. */
  681. static async getPage(opts) {
  682. // -> Get from cache first
  683. let page = await WIKI.models.pages.getPageFromCache(opts)
  684. if (!page) {
  685. // -> Get from DB
  686. page = await WIKI.models.pages.getPageFromDb(opts)
  687. if (page) {
  688. if (page.render) {
  689. // -> Save render to cache
  690. await WIKI.models.pages.savePageToCache(page)
  691. } else {
  692. // -> No render? Possible duplicate issue
  693. /* TODO: Detect duplicate and delete */
  694. throw new Error('Error while fetching page. Duplicate entry detected. Reload the page to try again.')
  695. }
  696. }
  697. }
  698. return page
  699. }
  700. /**
  701. * Fetch an Existing Page from the Database
  702. *
  703. * @param {Object} opts Page Properties
  704. * @returns {Promise} Promise of the Page Model Instance
  705. */
  706. static async getPageFromDb(opts) {
  707. const queryModeID = _.isNumber(opts)
  708. try {
  709. return WIKI.models.pages.query()
  710. .column([
  711. 'pages.id',
  712. 'pages.path',
  713. 'pages.hash',
  714. 'pages.title',
  715. 'pages.description',
  716. 'pages.isPrivate',
  717. 'pages.isPublished',
  718. 'pages.privateNS',
  719. 'pages.publishStartDate',
  720. 'pages.publishEndDate',
  721. 'pages.content',
  722. 'pages.render',
  723. 'pages.toc',
  724. 'pages.contentType',
  725. 'pages.createdAt',
  726. 'pages.updatedAt',
  727. 'pages.editorKey',
  728. 'pages.localeCode',
  729. 'pages.authorId',
  730. 'pages.creatorId',
  731. 'pages.extra',
  732. {
  733. authorName: 'author.name',
  734. authorEmail: 'author.email',
  735. creatorName: 'creator.name',
  736. creatorEmail: 'creator.email'
  737. }
  738. ])
  739. .joinRelated('author')
  740. .joinRelated('creator')
  741. .withGraphJoined('tags')
  742. .modifyGraph('tags', builder => {
  743. builder.select('tag', 'title')
  744. })
  745. .where(queryModeID ? {
  746. 'pages.id': opts
  747. } : {
  748. 'pages.path': opts.path,
  749. 'pages.localeCode': opts.locale
  750. })
  751. // .andWhere(builder => {
  752. // if (queryModeID) return
  753. // builder.where({
  754. // 'pages.isPublished': true
  755. // }).orWhere({
  756. // 'pages.isPublished': false,
  757. // 'pages.authorId': opts.userId
  758. // })
  759. // })
  760. // .andWhere(builder => {
  761. // if (queryModeID) return
  762. // if (opts.isPrivate) {
  763. // builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
  764. // } else {
  765. // builder.where({ 'pages.isPrivate': false })
  766. // }
  767. // })
  768. .first()
  769. } catch (err) {
  770. WIKI.logger.warn(err)
  771. throw err
  772. }
  773. }
  774. /**
  775. * Save a Page Model Instance to Cache
  776. *
  777. * @param {Object} page Page Model Instance
  778. * @returns {Promise} Promise with no value
  779. */
  780. static async savePageToCache(page) {
  781. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${page.hash}.bin`)
  782. await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
  783. id: page.id,
  784. authorId: page.authorId,
  785. authorName: page.authorName,
  786. createdAt: page.createdAt,
  787. creatorId: page.creatorId,
  788. creatorName: page.creatorName,
  789. description: page.description,
  790. extra: {
  791. css: _.get(page, 'extra.css', ''),
  792. js: _.get(page, 'extra.js', '')
  793. },
  794. isPrivate: page.isPrivate === 1 || page.isPrivate === true,
  795. isPublished: page.isPublished === 1 || page.isPublished === true,
  796. publishEndDate: page.publishEndDate,
  797. publishStartDate: page.publishStartDate,
  798. render: page.render,
  799. tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
  800. title: page.title,
  801. toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
  802. updatedAt: page.updatedAt
  803. }))
  804. }
  805. /**
  806. * Fetch an Existing Page from Cache
  807. *
  808. * @param {Object} opts Page Properties
  809. * @returns {Promise} Promise of the Page Model Instance
  810. */
  811. static async getPageFromCache(opts) {
  812. const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
  813. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${pageHash}.bin`)
  814. try {
  815. const pageBuffer = await fs.readFile(cachePath)
  816. let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)
  817. return {
  818. ...page,
  819. path: opts.path,
  820. localeCode: opts.locale,
  821. isPrivate: opts.isPrivate
  822. }
  823. } catch (err) {
  824. if (err.code === 'ENOENT') {
  825. return false
  826. }
  827. WIKI.logger.error(err)
  828. throw err
  829. }
  830. }
  831. /**
  832. * Delete an Existing Page from Cache
  833. *
  834. * @param {String} page Page Unique Hash
  835. * @returns {Promise} Promise with no value
  836. */
  837. static async deletePageFromCache(hash) {
  838. return fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))
  839. }
  840. /**
  841. * Flush the contents of the Cache
  842. */
  843. static async flushCache() {
  844. return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))
  845. }
  846. /**
  847. * Migrate all pages from a source locale to the target locale
  848. *
  849. * @param {Object} opts Migration properties
  850. * @param {string} opts.sourceLocale Source Locale Code
  851. * @param {string} opts.targetLocale Target Locale Code
  852. * @returns {Promise} Promise with no value
  853. */
  854. static async migrateToLocale({ sourceLocale, targetLocale }) {
  855. return WIKI.models.pages.query()
  856. .patch({
  857. localeCode: targetLocale
  858. })
  859. .where({
  860. localeCode: sourceLocale
  861. })
  862. .whereNotExists(function() {
  863. this.select('id').from('pages AS pagesm').where('pagesm.localeCode', targetLocale).andWhereRaw('pagesm.path = pages.path')
  864. })
  865. }
  866. /**
  867. * Clean raw HTML from content for use in search engines
  868. *
  869. * @param {string} rawHTML Raw HTML
  870. * @returns {string} Cleaned Content Text
  871. */
  872. static cleanHTML(rawHTML = '') {
  873. let data = striptags(rawHTML || '', [], ' ')
  874. .replace(emojiRegex(), '')
  875. // .replace(htmlEntitiesRegex, '')
  876. return he.decode(data)
  877. .replace(punctuationRegex, ' ')
  878. .replace(/(\r\n|\n|\r)/gm, ' ')
  879. .replace(/\s\s+/g, ' ')
  880. .split(' ').filter(w => w.length > 1).join(' ').toLowerCase()
  881. }
  882. /**
  883. * Subscribe to HA propagation events
  884. */
  885. static subscribeToEvents() {
  886. WIKI.events.inbound.on('deletePageFromCache', hash => {
  887. WIKI.models.pages.deletePageFromCache(hash)
  888. })
  889. WIKI.events.inbound.on('flushCache', () => {
  890. WIKI.models.pages.flushCache()
  891. })
  892. }
  893. }