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.

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