pages.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  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) {
  204. throw new WIKI.Error.PageIllegalPath()
  205. }
  206. // -> Check for page access
  207. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  208. locale: opts.locale,
  209. path: opts.path
  210. })) {
  211. throw new WIKI.Error.PageDeleteForbidden()
  212. }
  213. // -> Check for duplicate
  214. const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
  215. if (dupCheck) {
  216. throw new WIKI.Error.PageDuplicateCreate()
  217. }
  218. // -> Check for empty content
  219. if (!opts.content || _.trim(opts.content).length < 1) {
  220. throw new WIKI.Error.PageEmptyContent()
  221. }
  222. // -> Create page
  223. await WIKI.models.pages.query().insert({
  224. authorId: opts.user.id,
  225. content: opts.content,
  226. creatorId: opts.user.id,
  227. contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
  228. description: opts.description,
  229. editorKey: opts.editor,
  230. hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' }),
  231. isPrivate: opts.isPrivate,
  232. isPublished: opts.isPublished,
  233. localeCode: opts.locale,
  234. path: opts.path,
  235. publishEndDate: opts.publishEndDate || '',
  236. publishStartDate: opts.publishStartDate || '',
  237. title: opts.title,
  238. toc: '[]'
  239. })
  240. const page = await WIKI.models.pages.getPageFromDb({
  241. path: opts.path,
  242. locale: opts.locale,
  243. userId: opts.user.id,
  244. isPrivate: opts.isPrivate
  245. })
  246. // -> Save Tags
  247. if (opts.tags && opts.tags.length > 0) {
  248. await WIKI.models.tags.associateTags({ tags: opts.tags, page })
  249. }
  250. // -> Render page to HTML
  251. await WIKI.models.pages.renderPage(page)
  252. // -> Rebuild page tree
  253. await WIKI.models.pages.rebuildTree()
  254. // -> Add to Search Index
  255. const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
  256. page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
  257. await WIKI.data.searchEngine.created(page)
  258. // -> Add to Storage
  259. if (!opts.skipStorage) {
  260. await WIKI.models.storage.pageEvent({
  261. event: 'created',
  262. page
  263. })
  264. }
  265. // -> Reconnect Links
  266. await WIKI.models.pages.reconnectLinks({
  267. locale: page.localeCode,
  268. path: page.path,
  269. mode: 'create'
  270. })
  271. // -> Get latest updatedAt
  272. page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
  273. return page
  274. }
  275. /**
  276. * Update an Existing Page
  277. *
  278. * @param {Object} opts Page Properties
  279. * @returns {Promise} Promise of the Page Model Instance
  280. */
  281. static async updatePage(opts) {
  282. // -> Fetch original page
  283. const ogPage = await WIKI.models.pages.query().findById(opts.id)
  284. if (!ogPage) {
  285. throw new Error('Invalid Page Id')
  286. }
  287. // -> Check for page access
  288. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  289. locale: opts.locale,
  290. path: opts.path
  291. })) {
  292. throw new WIKI.Error.PageUpdateForbidden()
  293. }
  294. // -> Check for empty content
  295. if (!opts.content || _.trim(opts.content).length < 1) {
  296. throw new WIKI.Error.PageEmptyContent()
  297. }
  298. // -> Create version snapshot
  299. await WIKI.models.pageHistory.addVersion({
  300. ...ogPage,
  301. isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,
  302. action: opts.action ? opts.action : 'updated',
  303. versionDate: ogPage.updatedAt
  304. })
  305. // -> Update page
  306. await WIKI.models.pages.query().patch({
  307. authorId: opts.user.id,
  308. content: opts.content,
  309. description: opts.description,
  310. isPublished: opts.isPublished === true || opts.isPublished === 1,
  311. publishEndDate: opts.publishEndDate || '',
  312. publishStartDate: opts.publishStartDate || '',
  313. title: opts.title
  314. }).where('id', ogPage.id)
  315. let page = await WIKI.models.pages.getPageFromDb(ogPage.id)
  316. // -> Save Tags
  317. await WIKI.models.tags.associateTags({ tags: opts.tags, page })
  318. // -> Render page to HTML
  319. await WIKI.models.pages.renderPage(page)
  320. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  321. // -> Update Search Index
  322. const pageContents = await WIKI.models.pages.query().findById(page.id).select('render')
  323. page.safeContent = WIKI.models.pages.cleanHTML(pageContents.render)
  324. await WIKI.data.searchEngine.updated(page)
  325. // -> Update on Storage
  326. if (!opts.skipStorage) {
  327. await WIKI.models.storage.pageEvent({
  328. event: 'updated',
  329. page
  330. })
  331. }
  332. // -> Perform move?
  333. if ((opts.locale && opts.locale !== page.localeCode) || (opts.path && opts.path !== page.path)) {
  334. await WIKI.models.pages.movePage({
  335. id: page.id,
  336. destinationLocale: opts.locale,
  337. destinationPath: opts.path,
  338. user: opts.user
  339. })
  340. } else {
  341. // -> Update title of page tree entry
  342. await WIKI.models.knex.table('pageTree').where({
  343. pageId: page.id
  344. }).update('title', page.title)
  345. }
  346. // -> Get latest updatedAt
  347. page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
  348. return page
  349. }
  350. /**
  351. * Move a Page
  352. *
  353. * @param {Object} opts Page Properties
  354. * @returns {Promise} Promise with no value
  355. */
  356. static async movePage(opts) {
  357. const page = await WIKI.models.pages.query().findById(opts.id)
  358. if (!page) {
  359. throw new WIKI.Error.PageNotFound()
  360. }
  361. // -> Check for source page access
  362. if (!WIKI.auth.checkAccess(opts.user, ['manage:pages'], {
  363. locale: page.sourceLocale,
  364. path: page.sourcePath
  365. })) {
  366. throw new WIKI.Error.PageMoveForbidden()
  367. }
  368. // -> Check for destination page access
  369. if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
  370. locale: opts.destinationLocale,
  371. path: opts.destinationPath
  372. })) {
  373. throw new WIKI.Error.PageMoveForbidden()
  374. }
  375. // -> Check for existing page at destination path
  376. const destPage = await await WIKI.models.pages.query().findOne({
  377. path: opts.destinationPath,
  378. localeCode: opts.destinationLocale
  379. })
  380. if (destPage) {
  381. throw new WIKI.Error.PagePathCollision()
  382. }
  383. // -> Create version snapshot
  384. await WIKI.models.pageHistory.addVersion({
  385. ...page,
  386. action: 'moved',
  387. versionDate: page.updatedAt
  388. })
  389. const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
  390. // -> Move page
  391. await WIKI.models.pages.query().patch({
  392. path: opts.destinationPath,
  393. localeCode: opts.destinationLocale,
  394. hash: destinationHash
  395. }).findById(page.id)
  396. await WIKI.models.pages.deletePageFromCache(page.hash)
  397. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  398. // -> Rebuild page tree
  399. await WIKI.models.pages.rebuildTree()
  400. // -> Rename in Search Index
  401. await WIKI.data.searchEngine.renamed({
  402. ...page,
  403. destinationPath: opts.destinationPath,
  404. destinationLocaleCode: opts.destinationLocale,
  405. destinationHash
  406. })
  407. // -> Rename in Storage
  408. if (!opts.skipStorage) {
  409. await WIKI.models.storage.pageEvent({
  410. event: 'renamed',
  411. page: {
  412. ...page,
  413. destinationPath: opts.destinationPath,
  414. destinationLocaleCode: opts.destinationLocale,
  415. destinationHash,
  416. moveAuthorId: opts.user.id,
  417. moveAuthorName: opts.user.name,
  418. moveAuthorEmail: opts.user.email
  419. }
  420. })
  421. }
  422. // -> Reconnect Links
  423. await WIKI.models.pages.reconnectLinks({
  424. sourceLocale: page.localeCode,
  425. sourcePath: page.path,
  426. locale: opts.destinationLocale,
  427. path: opts.destinationPath,
  428. mode: 'move'
  429. })
  430. }
  431. /**
  432. * Delete an Existing Page
  433. *
  434. * @param {Object} opts Page Properties
  435. * @returns {Promise} Promise with no value
  436. */
  437. static async deletePage(opts) {
  438. let page
  439. if (_.has(opts, 'id')) {
  440. page = await WIKI.models.pages.query().findById(opts.id)
  441. } else {
  442. page = await await WIKI.models.pages.query().findOne({
  443. path: opts.path,
  444. localeCode: opts.locale
  445. })
  446. }
  447. if (!page) {
  448. throw new Error('Invalid Page Id')
  449. }
  450. // -> Check for page access
  451. if (!WIKI.auth.checkAccess(opts.user, ['delete:pages'], {
  452. locale: page.locale,
  453. path: page.path
  454. })) {
  455. throw new WIKI.Error.PageDeleteForbidden()
  456. }
  457. // -> Create version snapshot
  458. await WIKI.models.pageHistory.addVersion({
  459. ...page,
  460. action: 'deleted',
  461. versionDate: page.updatedAt
  462. })
  463. // -> Delete page
  464. await WIKI.models.pages.query().delete().where('id', page.id)
  465. await WIKI.models.pages.deletePageFromCache(page.hash)
  466. WIKI.events.outbound.emit('deletePageFromCache', page.hash)
  467. // -> Rebuild page tree
  468. await WIKI.models.pages.rebuildTree()
  469. // -> Delete from Search Index
  470. await WIKI.data.searchEngine.deleted(page)
  471. // -> Delete from Storage
  472. if (!opts.skipStorage) {
  473. await WIKI.models.storage.pageEvent({
  474. event: 'deleted',
  475. page
  476. })
  477. }
  478. // -> Reconnect Links
  479. await WIKI.models.pages.reconnectLinks({
  480. locale: page.localeCode,
  481. path: page.path,
  482. mode: 'delete'
  483. })
  484. }
  485. /**
  486. * Reconnect links to new/move/deleted page
  487. *
  488. * @param {Object} opts - Page parameters
  489. * @param {string} opts.path - Page Path
  490. * @param {string} opts.locale - Page Locale Code
  491. * @param {string} [opts.sourcePath] - Previous Page Path (move only)
  492. * @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only)
  493. * @param {string} opts.mode - Page Update mode (create, move, delete)
  494. * @returns {Promise} Promise with no value
  495. */
  496. static async reconnectLinks (opts) {
  497. const pageHref = `/${opts.locale}/${opts.path}`
  498. let replaceArgs = {
  499. from: '',
  500. to: ''
  501. }
  502. switch (opts.mode) {
  503. case 'create':
  504. replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
  505. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  506. break
  507. case 'move':
  508. const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`
  509. replaceArgs.from = `<a href="${prevPageHref}" class="is-internal-link is-invalid-page">`
  510. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  511. break
  512. case 'delete':
  513. replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
  514. replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
  515. break
  516. default:
  517. return false
  518. }
  519. let affectedHashes = []
  520. // -> Perform replace and return affected page hashes (POSTGRES only)
  521. if (WIKI.config.db.type === 'postgres') {
  522. const qryHashes = await WIKI.models.pages.query()
  523. .returning('hash')
  524. .patch({
  525. render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
  526. })
  527. .whereIn('pages.id', function () {
  528. this.select('pageLinks.pageId').from('pageLinks').where({
  529. 'pageLinks.path': opts.path,
  530. 'pageLinks.localeCode': opts.locale
  531. })
  532. })
  533. affectedHashes = qryHashes.map(h => h.hash)
  534. } else {
  535. // -> Perform replace, then query affected page hashes (MYSQL, MARIADB, MSSQL, SQLITE only)
  536. await WIKI.models.pages.query()
  537. .patch({
  538. render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
  539. })
  540. .whereIn('pages.id', function () {
  541. this.select('pageLinks.pageId').from('pageLinks').where({
  542. 'pageLinks.path': opts.path,
  543. 'pageLinks.localeCode': opts.locale
  544. })
  545. })
  546. const qryHashes = await WIKI.models.pages.query()
  547. .column('hash')
  548. .whereIn('pages.id', function () {
  549. this.select('pageLinks.pageId').from('pageLinks').where({
  550. 'pageLinks.path': opts.path,
  551. 'pageLinks.localeCode': opts.locale
  552. })
  553. })
  554. affectedHashes = qryHashes.map(h => h.hash)
  555. }
  556. for (const hash of affectedHashes) {
  557. await WIKI.models.pages.deletePageFromCache(hash)
  558. WIKI.events.outbound.emit('deletePageFromCache', hash)
  559. }
  560. }
  561. /**
  562. * Rebuild page tree for new/updated/deleted page
  563. *
  564. * @returns {Promise} Promise with no value
  565. */
  566. static async rebuildTree() {
  567. const rebuildJob = await WIKI.scheduler.registerJob({
  568. name: 'rebuild-tree',
  569. immediate: true,
  570. worker: true
  571. })
  572. return rebuildJob.finished
  573. }
  574. /**
  575. * Trigger the rendering of a page
  576. *
  577. * @param {Object} page Page Model Instance
  578. * @returns {Promise} Promise with no value
  579. */
  580. static async renderPage(page) {
  581. const renderJob = await WIKI.scheduler.registerJob({
  582. name: 'render-page',
  583. immediate: true,
  584. worker: true
  585. }, page.id)
  586. return renderJob.finished
  587. }
  588. /**
  589. * Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache
  590. *
  591. * @param {Object} opts Page Properties
  592. * @returns {Promise} Promise of the Page Model Instance
  593. */
  594. static async getPage(opts) {
  595. // -> Get from cache first
  596. let page = await WIKI.models.pages.getPageFromCache(opts)
  597. if (!page) {
  598. // -> Get from DB
  599. page = await WIKI.models.pages.getPageFromDb(opts)
  600. if (page) {
  601. if (page.render) {
  602. // -> Save render to cache
  603. await WIKI.models.pages.savePageToCache(page)
  604. } else {
  605. // -> No render? Possible duplicate issue
  606. /* TODO: Detect duplicate and delete */
  607. throw new Error('Error while fetching page. Duplicate entry detected. Reload the page to try again.')
  608. }
  609. }
  610. }
  611. return page
  612. }
  613. /**
  614. * Fetch an Existing Page from the Database
  615. *
  616. * @param {Object} opts Page Properties
  617. * @returns {Promise} Promise of the Page Model Instance
  618. */
  619. static async getPageFromDb(opts) {
  620. const queryModeID = _.isNumber(opts)
  621. try {
  622. return WIKI.models.pages.query()
  623. .column([
  624. 'pages.id',
  625. 'pages.path',
  626. 'pages.hash',
  627. 'pages.title',
  628. 'pages.description',
  629. 'pages.isPrivate',
  630. 'pages.isPublished',
  631. 'pages.privateNS',
  632. 'pages.publishStartDate',
  633. 'pages.publishEndDate',
  634. 'pages.content',
  635. 'pages.render',
  636. 'pages.toc',
  637. 'pages.contentType',
  638. 'pages.createdAt',
  639. 'pages.updatedAt',
  640. 'pages.editorKey',
  641. 'pages.localeCode',
  642. 'pages.authorId',
  643. 'pages.creatorId',
  644. {
  645. authorName: 'author.name',
  646. authorEmail: 'author.email',
  647. creatorName: 'creator.name',
  648. creatorEmail: 'creator.email'
  649. }
  650. ])
  651. .joinRelated('author')
  652. .joinRelated('creator')
  653. .withGraphJoined('tags')
  654. .modifyGraph('tags', builder => {
  655. builder.select('tag', 'title')
  656. })
  657. .where(queryModeID ? {
  658. 'pages.id': opts
  659. } : {
  660. 'pages.path': opts.path,
  661. 'pages.localeCode': opts.locale
  662. })
  663. // .andWhere(builder => {
  664. // if (queryModeID) return
  665. // builder.where({
  666. // 'pages.isPublished': true
  667. // }).orWhere({
  668. // 'pages.isPublished': false,
  669. // 'pages.authorId': opts.userId
  670. // })
  671. // })
  672. // .andWhere(builder => {
  673. // if (queryModeID) return
  674. // if (opts.isPrivate) {
  675. // builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
  676. // } else {
  677. // builder.where({ 'pages.isPrivate': false })
  678. // }
  679. // })
  680. .first()
  681. } catch (err) {
  682. WIKI.logger.warn(err)
  683. throw err
  684. }
  685. }
  686. /**
  687. * Save a Page Model Instance to Cache
  688. *
  689. * @param {Object} page Page Model Instance
  690. * @returns {Promise} Promise with no value
  691. */
  692. static async savePageToCache(page) {
  693. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${page.hash}.bin`)
  694. await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
  695. id: page.id,
  696. authorId: page.authorId,
  697. authorName: page.authorName,
  698. createdAt: page.createdAt,
  699. creatorId: page.creatorId,
  700. creatorName: page.creatorName,
  701. description: page.description,
  702. isPrivate: page.isPrivate === 1 || page.isPrivate === true,
  703. isPublished: page.isPublished === 1 || page.isPublished === true,
  704. publishEndDate: page.publishEndDate,
  705. publishStartDate: page.publishStartDate,
  706. render: page.render,
  707. tags: page.tags.map(t => _.pick(t, ['tag', 'title'])),
  708. title: page.title,
  709. toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
  710. updatedAt: page.updatedAt
  711. }))
  712. }
  713. /**
  714. * Fetch an Existing Page from Cache
  715. *
  716. * @param {Object} opts Page Properties
  717. * @returns {Promise} Promise of the Page Model Instance
  718. */
  719. static async getPageFromCache(opts) {
  720. const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
  721. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${pageHash}.bin`)
  722. try {
  723. const pageBuffer = await fs.readFile(cachePath)
  724. let page = WIKI.models.pages.cacheSchema.decode(pageBuffer)
  725. return {
  726. ...page,
  727. path: opts.path,
  728. localeCode: opts.locale,
  729. isPrivate: opts.isPrivate
  730. }
  731. } catch (err) {
  732. if (err.code === 'ENOENT') {
  733. return false
  734. }
  735. WIKI.logger.error(err)
  736. throw err
  737. }
  738. }
  739. /**
  740. * Delete an Existing Page from Cache
  741. *
  742. * @param {String} page Page Unique Hash
  743. * @returns {Promise} Promise with no value
  744. */
  745. static async deletePageFromCache(hash) {
  746. return fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))
  747. }
  748. /**
  749. * Flush the contents of the Cache
  750. */
  751. static async flushCache() {
  752. return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))
  753. }
  754. /**
  755. * Migrate all pages from a source locale to the target locale
  756. *
  757. * @param {Object} opts Migration properties
  758. * @param {string} opts.sourceLocale Source Locale Code
  759. * @param {string} opts.targetLocale Target Locale Code
  760. * @returns {Promise} Promise with no value
  761. */
  762. static async migrateToLocale({ sourceLocale, targetLocale }) {
  763. return WIKI.models.pages.query()
  764. .patch({
  765. localeCode: targetLocale
  766. })
  767. .where({
  768. localeCode: sourceLocale
  769. })
  770. .whereNotExists(function() {
  771. this.select('id').from('pages AS pagesm').where('pagesm.localeCode', targetLocale).andWhereRaw('pagesm.path = pages.path')
  772. })
  773. }
  774. /**
  775. * Clean raw HTML from content for use in search engines
  776. *
  777. * @param {string} rawHTML Raw HTML
  778. * @returns {string} Cleaned Content Text
  779. */
  780. static cleanHTML(rawHTML = '') {
  781. let data = striptags(rawHTML || '', [], ' ')
  782. .replace(emojiRegex(), '')
  783. // .replace(htmlEntitiesRegex, '')
  784. return he.decode(data)
  785. .replace(punctuationRegex, ' ')
  786. .replace(/(\r\n|\n|\r)/gm, ' ')
  787. .replace(/\s\s+/g, ' ')
  788. .split(' ').filter(w => w.length > 1).join(' ').toLowerCase()
  789. }
  790. /**
  791. * Subscribe to HA propagation events
  792. */
  793. static subscribeToEvents() {
  794. WIKI.events.inbound.on('deletePageFromCache', hash => {
  795. WIKI.models.pages.deletePageFromCache(hash)
  796. })
  797. WIKI.events.inbound.on('flushCache', () => {
  798. WIKI.models.pages.flushCache()
  799. })
  800. }
  801. }