hackmd.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /* eslint-disable no-use-before-define */
  2. const logger = require('@alias/logger')('growi:routes:hackmd');
  3. const path = require('path');
  4. const fs = require('graceful-fs');
  5. const swig = require('swig-templates');
  6. const axios = require('axios');
  7. const ApiResponse = require('../util/apiResponse');
  8. module.exports = function(crowi, app) {
  9. const Page = crowi.models.Page;
  10. const pageEvent = crowi.event('page');
  11. // load GROWI agent script for HackMD
  12. const manifest = require(path.join(crowi.publicDir, 'manifest.json'));
  13. const agentScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-agent.js']);
  14. const stylesScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-styles.js']);
  15. // generate swig template
  16. let agentScriptContentTpl;
  17. let stylesScriptContentTpl;
  18. // init 'saveOnHackmd' event
  19. pageEvent.on('saveOnHackmd', (page) => {
  20. crowi.getIo().sockets.emit('page:editingWithHackmd', { page });
  21. });
  22. /**
  23. * GET /_hackmd/load-agent
  24. *
  25. * loadAgent action
  26. * This should be access from HackMD and send agent script
  27. *
  28. * @param {object} req
  29. * @param {object} res
  30. */
  31. const loadAgent = function(req, res) {
  32. // generate swig template
  33. if (agentScriptContentTpl == null) {
  34. agentScriptContentTpl = swig.compileFile(agentScriptPath);
  35. }
  36. const origin = crowi.appService.getSiteUrl();
  37. // generate definitions to replace
  38. const definitions = {
  39. origin,
  40. };
  41. // inject
  42. const script = agentScriptContentTpl(definitions);
  43. res.set('Content-Type', 'application/javascript');
  44. res.send(script);
  45. };
  46. /**
  47. * GET /_hackmd/load-styles
  48. *
  49. * loadStyles action
  50. * This should be access from HackMD and send script to insert styles
  51. *
  52. * @param {object} req
  53. * @param {object} res
  54. */
  55. const loadStyles = function(req, res) {
  56. // generate swig template
  57. if (stylesScriptContentTpl == null) {
  58. stylesScriptContentTpl = swig.compileFile(stylesScriptPath);
  59. }
  60. const styleFilePath = path.join(crowi.publicDir, manifest['styles/style-hackmd.css']);
  61. const styles = fs
  62. .readFileSync(styleFilePath).toString()
  63. .replace(/\s+/g, ' ');
  64. // generate definitions to replace
  65. const definitions = {
  66. styles: escape(styles),
  67. };
  68. // inject
  69. const script = stylesScriptContentTpl(definitions);
  70. res.set('Content-Type', 'application/javascript');
  71. res.send(script);
  72. };
  73. const validateForApi = async function(req, res, next) {
  74. // validate process.env.HACKMD_URI
  75. const hackmdUri = process.env.HACKMD_URI;
  76. if (hackmdUri == null) {
  77. return res.json(ApiResponse.error('HackMD for GROWI has not been setup'));
  78. }
  79. // validate pageId
  80. const pageId = req.body.pageId;
  81. if (pageId == null) {
  82. return res.json(ApiResponse.error('pageId required'));
  83. }
  84. // validate page
  85. const page = await Page.findOne({ _id: pageId });
  86. if (page == null) {
  87. return res.json(ApiResponse.error(`Page('${pageId}') does not exist`));
  88. }
  89. req.page = page;
  90. next();
  91. };
  92. /**
  93. * POST /_api/hackmd.integrate
  94. *
  95. * Create page on HackMD and start to integrate
  96. * @param {object} req
  97. * @param {object} res
  98. */
  99. const integrate = async function(req, res) {
  100. const hackmdUri = process.env.HACKMD_URI_FOR_SERVER || process.env.HACKMD_URI;
  101. let page = req.page;
  102. const hackmdPageUri = (page.pageIdOnHackmd != null)
  103. ? `${hackmdUri}/${page.pageIdOnHackmd}`
  104. : `${hackmdUri}/new`;
  105. let hackmdResponse;
  106. try {
  107. // check if page is found or created in HackMD
  108. hackmdResponse = await axios.get(hackmdPageUri, {
  109. maxRedirects: 0,
  110. // validate HTTP status is 200 or 302 or 404
  111. validateStatus: (status) => {
  112. return status === 200 || status === 302 || status === 404;
  113. },
  114. });
  115. }
  116. catch (err) {
  117. logger.error(err);
  118. return res.json(ApiResponse.error(err));
  119. }
  120. const { status, headers } = hackmdResponse;
  121. // validate HackMD/CodiMD specific header
  122. if (headers['codimd-version'] == null && headers['hackmd-version'] == null) {
  123. const message = 'Connecting to a non-HackMD server.';
  124. logger.error(message);
  125. return res.json(ApiResponse.error(message));
  126. }
  127. try {
  128. // when page is not found
  129. if (status === 404) {
  130. // reset registered data
  131. page = await Page.registerHackmdPage(page, undefined);
  132. // re-invoke
  133. return integrate(req, res);
  134. }
  135. // when redirect
  136. if (status === 302) {
  137. // extract page id on HackMD
  138. const pathnameOnHackmd = new URL(headers.location, hackmdUri).pathname; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
  139. const pageIdOnHackmd = pathnameOnHackmd.substr(1); // strip the head '/'
  140. page = await Page.registerHackmdPage(page, pageIdOnHackmd);
  141. }
  142. // when page is found
  143. else {
  144. page = await Page.syncRevisionToHackmd(page);
  145. }
  146. const data = {
  147. pageIdOnHackmd: page.pageIdOnHackmd,
  148. revisionIdHackmdSynced: page.revisionHackmdSynced,
  149. hasDraftOnHackmd: page.hasDraftOnHackmd,
  150. };
  151. return res.json(ApiResponse.success(data));
  152. }
  153. catch (err) {
  154. logger.error(err);
  155. return res.json(ApiResponse.error('Integration with HackMD process failed'));
  156. }
  157. };
  158. /**
  159. * POST /_api/hackmd.discard
  160. *
  161. * Create page on HackMD and start to integrate
  162. * @param {object} req
  163. * @param {object} res
  164. */
  165. const discard = async function(req, res) {
  166. let page = req.page;
  167. try {
  168. page = await Page.syncRevisionToHackmd(page);
  169. const data = {
  170. pageIdOnHackmd: page.pageIdOnHackmd,
  171. revisionIdHackmdSynced: page.revisionHackmdSynced,
  172. hasDraftOnHackmd: page.hasDraftOnHackmd,
  173. };
  174. return res.json(ApiResponse.success(data));
  175. }
  176. catch (err) {
  177. logger.error(err);
  178. return res.json(ApiResponse.error('discard process failed'));
  179. }
  180. };
  181. /**
  182. * POST /_api/hackmd.saveOnHackmd
  183. *
  184. * receive when save operation triggered on HackMD
  185. * !! This will be invoked many time from many people !!
  186. *
  187. * @param {object} req
  188. * @param {object} res
  189. */
  190. const saveOnHackmd = async function(req, res) {
  191. const page = req.page;
  192. try {
  193. await Page.updateHasDraftOnHackmd(page, true);
  194. pageEvent.emit('saveOnHackmd', page);
  195. return res.json(ApiResponse.success());
  196. }
  197. catch (err) {
  198. logger.error(err);
  199. return res.json(ApiResponse.error('saveOnHackmd process failed'));
  200. }
  201. };
  202. return {
  203. loadAgent,
  204. loadStyles,
  205. validateForApi,
  206. integrate,
  207. discard,
  208. saveOnHackmd,
  209. };
  210. };