| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- /* eslint-disable no-use-before-define */
- const logger = require('@alias/logger')('growi:routes:hackmd');
- const path = require('path');
- const fs = require('graceful-fs');
- const swig = require('swig-templates');
- const axios = require('axios');
- const ApiResponse = require('../util/apiResponse');
- /**
- * @swagger
- *
- * components:
- * schemas:
- * Hackmd:
- * description: Hackmd
- * type: object
- * properties:
- * pageIdOnHackmd:
- * type: string
- * description: page ID on HackMD
- * example: qLnodHLxT6C3hVEVczvbDQ
- * revisionIdHackmdSynced:
- * $ref: '#/components/schemas/Revision/properties/_id'
- * hasDraftOnHackmd:
- * type: boolean
- * description: has draft on HackMD
- * example: false
- */
- module.exports = function(crowi, app) {
- const Page = crowi.models.Page;
- const pageEvent = crowi.event('page');
- // load GROWI agent script for HackMD
- const manifest = require(path.join(crowi.publicDir, 'manifest.json'));
- const agentScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-agent.js']);
- const stylesScriptPath = path.join(crowi.publicDir, manifest['js/hackmd-styles.js']);
- // generate swig template
- let agentScriptContentTpl;
- let stylesScriptContentTpl;
- // init 'saveOnHackmd' event
- pageEvent.on('saveOnHackmd', (page) => {
- crowi.getIo().sockets.emit('page:editingWithHackmd', { page });
- });
- /**
- * GET /_hackmd/load-agent
- *
- * loadAgent action
- * This should be access from HackMD and send agent script
- *
- * @param {object} req
- * @param {object} res
- */
- const loadAgent = function(req, res) {
- // generate swig template
- if (agentScriptContentTpl == null) {
- agentScriptContentTpl = swig.compileFile(agentScriptPath);
- }
- const origin = crowi.appService.getSiteUrl();
- // generate definitions to replace
- const definitions = {
- origin,
- };
- // inject
- const script = agentScriptContentTpl(definitions);
- res.set('Content-Type', 'application/javascript');
- res.send(script);
- };
- /**
- * GET /_hackmd/load-styles
- *
- * loadStyles action
- * This should be access from HackMD and send script to insert styles
- *
- * @param {object} req
- * @param {object} res
- */
- const loadStyles = function(req, res) {
- // generate swig template
- if (stylesScriptContentTpl == null) {
- stylesScriptContentTpl = swig.compileFile(stylesScriptPath);
- }
- const styleFilePath = path.join(crowi.publicDir, manifest['styles/style-hackmd.css']);
- const styles = fs
- .readFileSync(styleFilePath).toString()
- .replace(/\s+/g, ' ');
- // generate definitions to replace
- const definitions = {
- styles: escape(styles),
- };
- // inject
- const script = stylesScriptContentTpl(definitions);
- res.set('Content-Type', 'application/javascript');
- res.send(script);
- };
- const validateForApi = async function(req, res, next) {
- // validate process.env.HACKMD_URI
- const hackmdUri = process.env.HACKMD_URI;
- if (hackmdUri == null) {
- return res.json(ApiResponse.error('HackMD for GROWI has not been setup'));
- }
- // validate pageId
- const pageId = req.body.pageId;
- if (pageId == null) {
- return res.json(ApiResponse.error('pageId required'));
- }
- // validate page
- const page = await Page.findOne({ _id: pageId });
- if (page == null) {
- return res.json(ApiResponse.error(`Page('${pageId}') does not exist`));
- }
- req.page = page;
- next();
- };
- /**
- * @swagger
- *
- * /hackmd.integrate:
- * post:
- * tags: [Hackmd]
- * operationId: integrateHackmd
- * summary: /hackmd.integrate
- * description: Integrate hackmd
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * pageId:
- * $ref: '#/components/schemas/Page/properties/_id'
- * page:
- * $ref: '#/components/schemas/Hackmd'
- * responses:
- * 200:
- * description: Succeeded to integrate HackMD.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * pageIdOnHackmd:
- * $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
- * revisionIdHackmdSynced:
- * $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
- * hasDraftOnHackmd:
- * $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * POST /_api/hackmd.integrate
- *
- * Create page on HackMD and start to integrate
- * @param {object} req
- * @param {object} res
- */
- const integrate = async function(req, res) {
- const hackmdUri = process.env.HACKMD_URI_FOR_SERVER || process.env.HACKMD_URI;
- let page = req.page;
- const hackmdPageUri = (page.pageIdOnHackmd != null)
- ? `${hackmdUri}/${page.pageIdOnHackmd}`
- : `${hackmdUri}/new`;
- let hackmdResponse;
- try {
- // check if page is found or created in HackMD
- hackmdResponse = await axios.get(hackmdPageUri, {
- maxRedirects: 0,
- // validate HTTP status is 200 or 302 or 404
- validateStatus: (status) => {
- return status === 200 || status === 302 || status === 404;
- },
- });
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error(err));
- }
- const { status, headers } = hackmdResponse;
- // validate HackMD/CodiMD specific header
- if (headers['codimd-version'] == null && headers['hackmd-version'] == null) {
- const message = 'Connecting to a non-HackMD server.';
- logger.error(message);
- return res.json(ApiResponse.error(message));
- }
- try {
- // when page is not found
- if (status === 404) {
- // reset registered data
- page = await Page.registerHackmdPage(page, undefined);
- // re-invoke
- return integrate(req, res);
- }
- // when redirect
- if (status === 302) {
- // extract page id on HackMD
- const pathnameOnHackmd = new URL(headers.location, hackmdUri).pathname; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
- const pageIdOnHackmd = pathnameOnHackmd.substr(1); // strip the head '/'
- page = await Page.registerHackmdPage(page, pageIdOnHackmd);
- }
- // when page is found
- else {
- page = await Page.syncRevisionToHackmd(page);
- }
- const data = {
- pageIdOnHackmd: page.pageIdOnHackmd,
- revisionIdHackmdSynced: page.revisionHackmdSynced,
- hasDraftOnHackmd: page.hasDraftOnHackmd,
- };
- return res.json(ApiResponse.success(data));
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error('Integration with HackMD process failed'));
- }
- };
- /**
- * @swagger
- *
- * /hackmd.discard:
- * post:
- * tags: [Hackmd]
- * operationId: discardHackmd
- * summary: /hackmd.discard
- * description: Discard hackmd
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * pageId:
- * $ref: '#/components/schemas/Page/properties/_id'
- * page:
- * $ref: '#/components/schemas/Hackmd'
- * responses:
- * 200:
- * description: Succeeded to integrate HackMD.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * pageIdOnHackmd:
- * $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
- * revisionIdHackmdSynced:
- * $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
- * hasDraftOnHackmd:
- * $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * POST /_api/hackmd.discard
- *
- * Create page on HackMD and start to integrate
- * @param {object} req
- * @param {object} res
- */
- const discard = async function(req, res) {
- let page = req.page;
- try {
- page = await Page.syncRevisionToHackmd(page);
- const data = {
- pageIdOnHackmd: page.pageIdOnHackmd,
- revisionIdHackmdSynced: page.revisionHackmdSynced,
- hasDraftOnHackmd: page.hasDraftOnHackmd,
- };
- return res.json(ApiResponse.success(data));
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error('discard process failed'));
- }
- };
- /**
- * @swagger
- *
- * /hackmd.saveOnHackmd:
- * post:
- * tags: [Hackmd]
- * operationId: saveOnHackmd
- * summary: /hackmd.saveOnHackmd
- * description: Receive when save operation triggered on HackMD
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * pageId:
- * $ref: '#/components/schemas/Page/properties/_id'
- * page:
- * $ref: '#/components/schemas/Hackmd'
- * responses:
- * 200:
- * description: Succeeded to receive when save operation triggered on HackMD.
- * content:
- * application/json:
- * schema:
- * properties:
- * ok:
- * $ref: '#/components/schemas/V1Response/properties/ok'
- * 403:
- * $ref: '#/components/responses/403'
- * 500:
- * $ref: '#/components/responses/500'
- */
- /**
- * POST /_api/hackmd.saveOnHackmd
- *
- * receive when save operation triggered on HackMD
- * !! This will be invoked many time from many people !!
- *
- * @param {object} req
- * @param {object} res
- */
- const saveOnHackmd = async function(req, res) {
- const page = req.page;
- try {
- await Page.updateHasDraftOnHackmd(page, true);
- pageEvent.emit('saveOnHackmd', page);
- return res.json(ApiResponse.success());
- }
- catch (err) {
- logger.error(err);
- return res.json(ApiResponse.error('saveOnHackmd process failed'));
- }
- };
- return {
- loadAgent,
- loadStyles,
- validateForApi,
- integrate,
- discard,
- saveOnHackmd,
- };
- };
|