page.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. const loggerFactory = require('@alias/logger');
  2. const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
  3. const express = require('express');
  4. const { body, query } = require('express-validator');
  5. const router = express.Router();
  6. const ErrorV3 = require('../../models/vo/error-apiv3');
  7. /**
  8. * @swagger
  9. * tags:
  10. * name: Page
  11. */
  12. /**
  13. * @swagger
  14. *
  15. * components:
  16. * schemas:
  17. * Page:
  18. * description: Page
  19. * type: object
  20. * properties:
  21. * _id:
  22. * type: string
  23. * description: page ID
  24. * example: 5e07345972560e001761fa63
  25. * __v:
  26. * type: number
  27. * description: DB record version
  28. * example: 0
  29. * commentCount:
  30. * type: number
  31. * description: count of comments
  32. * example: 3
  33. * createdAt:
  34. * type: string
  35. * description: date created at
  36. * example: 2010-01-01T00:00:00.000Z
  37. * creator:
  38. * $ref: '#/components/schemas/User'
  39. * extended:
  40. * type: object
  41. * description: extend data
  42. * example: {}
  43. * grant:
  44. * type: number
  45. * description: grant
  46. * example: 1
  47. * grantedUsers:
  48. * type: array
  49. * description: granted users
  50. * items:
  51. * type: string
  52. * description: user ID
  53. * example: ["5ae5fccfc5577b0004dbd8ab"]
  54. * lastUpdateUser:
  55. * $ref: '#/components/schemas/User'
  56. * liker:
  57. * type: array
  58. * description: granted users
  59. * items:
  60. * type: string
  61. * description: user ID
  62. * example: []
  63. * path:
  64. * type: string
  65. * description: page path
  66. * example: /
  67. * redirectTo:
  68. * type: string
  69. * description: redirect path
  70. * example: ""
  71. * revision:
  72. * type: string
  73. * description: page revision
  74. * seenUsers:
  75. * type: array
  76. * description: granted users
  77. * items:
  78. * type: string
  79. * description: user ID
  80. * example: ["5ae5fccfc5577b0004dbd8ab"]
  81. * status:
  82. * type: string
  83. * description: status
  84. * enum:
  85. * - 'wip'
  86. * - 'published'
  87. * - 'deleted'
  88. * - 'deprecated'
  89. * example: published
  90. * updatedAt:
  91. * type: string
  92. * description: date updated at
  93. * example: 2010-01-01T00:00:00.000Z
  94. *
  95. * LikeParams:
  96. * description: LikeParams
  97. * type: object
  98. * properties:
  99. * pageId:
  100. * type: string
  101. * description: page ID
  102. * example: 5e07345972560e001761fa63
  103. * bool:
  104. * type: boolean
  105. * description: boolean for like status
  106. */
  107. module.exports = (crowi) => {
  108. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  109. const loginRequired = require('../../middlewares/login-required')(crowi);
  110. const csrf = require('../../middlewares/csrf')(crowi);
  111. const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
  112. const globalNotificationService = crowi.getGlobalNotificationService();
  113. const { Page, GlobalNotificationSetting, User } = crowi.models;
  114. const { exportService } = crowi;
  115. const validator = {
  116. likes: [
  117. body('pageId').isString(),
  118. body('bool').isBoolean(),
  119. ],
  120. likeInfo: [
  121. query('_id').isMongoId(),
  122. query('user._id').isMongoId(),
  123. ],
  124. export: [
  125. query('format').isString().isIn(['md', 'pdf']),
  126. query('revisionId').isString(),
  127. ],
  128. archive: [
  129. body('rootPagePath').isString(),
  130. body('isCommentDownload').isBoolean(),
  131. body('isAttachmentFileDownload').isBoolean(),
  132. body('isSubordinatedPageDownload').isBoolean(),
  133. body('fileType').isString().isIn(['pdf', 'markdown']),
  134. body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
  135. body('hierarchyValue').isNumeric(),
  136. ],
  137. };
  138. /**
  139. * @swagger
  140. *
  141. * /page/likes:
  142. * put:
  143. * tags: [Page]
  144. * summary: /page/likes
  145. * description: Update liked status
  146. * operationId: updateLikedStatus
  147. * requestBody:
  148. * content:
  149. * application/json:
  150. * schema:
  151. * $ref: '#/components/schemas/LikeParams'
  152. * responses:
  153. * 200:
  154. * description: Succeeded to update liked status.
  155. * content:
  156. * application/json:
  157. * schema:
  158. * $ref: '#/components/schemas/Page'
  159. */
  160. router.put('/likes', accessTokenParser, loginRequired, csrf, validator.likes, apiV3FormValidator, async(req, res) => {
  161. const { pageId, bool } = req.body;
  162. let page;
  163. try {
  164. page = await Page.findByIdAndViewer(pageId, req.user);
  165. if (page == null) {
  166. return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
  167. }
  168. if (bool) {
  169. page = await page.like(req.user);
  170. }
  171. else {
  172. page = await page.unlike(req.user);
  173. }
  174. }
  175. catch (err) {
  176. logger.error('update-like-failed', err);
  177. return res.apiv3Err(err, 500);
  178. }
  179. try {
  180. // global notification
  181. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
  182. }
  183. catch (err) {
  184. logger.error('Like notification failed', err);
  185. }
  186. const result = { page };
  187. result.seenUser = page.seenUsers;
  188. return res.apiv3({ result });
  189. });
  190. router.get('/like-info', loginRequired, validator.likeInfo, async(req, res) => {
  191. const pageId = req.query._id;
  192. const userId = req.user._id;
  193. try {
  194. const page = await Page.findById(pageId);
  195. const users = await Page.findById(pageId).populate('liker', User.USER_PUBLIC_FIELDS);
  196. const sumOfLikers = page.liker.length;
  197. const isLiked = page.liker.includes(userId);
  198. return res.apiv3({ users, sumOfLikers, isLiked });
  199. }
  200. catch (err) {
  201. logger.error('error like info', err);
  202. return res.apiv3Err(err, 500);
  203. }
  204. });
  205. /**
  206. * @swagger
  207. *
  208. * /pages/export:
  209. * get:
  210. * tags: [Export]
  211. * description: return page's markdown
  212. * responses:
  213. * 200:
  214. * description: Return page's markdown
  215. */
  216. router.get('/export/:pageId', loginRequired, validator.export, async(req, res) => {
  217. const { pageId } = req.params;
  218. const { format, revisionId = null } = req.query;
  219. let revision;
  220. try {
  221. const Page = crowi.model('Page');
  222. const page = await Page.findByIdAndViewer(pageId, req.user);
  223. if (page == null) {
  224. const isPageExist = await Page.count({ _id: pageId }) > 0;
  225. if (isPageExist) {
  226. // This page exists but req.user has not read permission
  227. return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
  228. }
  229. return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
  230. }
  231. const revisionIdForFind = revisionId || page.revision;
  232. const Revision = crowi.model('Revision');
  233. revision = await Revision.findById(revisionIdForFind);
  234. }
  235. catch (err) {
  236. logger.error('Failed to get page data', err);
  237. return res.apiv3Err(err, 500);
  238. }
  239. const fileName = revision.id;
  240. let stream;
  241. try {
  242. stream = exportService.getReadStreamFromRevision(revision, format);
  243. }
  244. catch (err) {
  245. logger.error('Failed to create readStream', err);
  246. return res.apiv3Err(err, 500);
  247. }
  248. res.set({
  249. 'Content-Disposition': `attachment;filename*=UTF-8''${fileName}.${format}`,
  250. });
  251. return stream.pipe(res);
  252. });
  253. // TODO GW-2746 bulk export pages
  254. // /**
  255. // * @swagger
  256. // *
  257. // * /page/archive:
  258. // * post:
  259. // * tags: [Page]
  260. // * summary: /page/archive
  261. // * description: create page archive
  262. // * requestBody:
  263. // * content:
  264. // * application/json:
  265. // * schema:
  266. // * properties:
  267. // * rootPagePath:
  268. // * type: string
  269. // * description: path of the root page
  270. // * isCommentDownload:
  271. // * type: boolean
  272. // * description: whether archive data contains comments
  273. // * isAttachmentFileDownload:
  274. // * type: boolean
  275. // * description: whether archive data contains attachments
  276. // * isSubordinatedPageDownload:
  277. // * type: boolean
  278. // * description: whether archive data children pages
  279. // * fileType:
  280. // * type: string
  281. // * description: file type of archive data(.md, .pdf)
  282. // * hierarchyType:
  283. // * type: string
  284. // * description: method of select children pages archive data contains('allSubordinatedPage', 'decideHierarchy')
  285. // * hierarchyValue:
  286. // * type: number
  287. // * description: depth of hierarchy(use when hierarchyType is 'decideHierarchy')
  288. // * responses:
  289. // * 200:
  290. // * description: create page archive
  291. // * content:
  292. // * application/json:
  293. // * schema:
  294. // * $ref: '#/components/schemas/Page'
  295. // */
  296. // router.post('/archive', accessTokenParser, loginRequired, csrf, validator.archive, apiV3FormValidator, async(req, res) => {
  297. // const PageArchive = crowi.model('PageArchive');
  298. // const {
  299. // rootPagePath,
  300. // isCommentDownload,
  301. // isAttachmentFileDownload,
  302. // fileType,
  303. // } = req.body;
  304. // const owner = req.user._id;
  305. // const numOfPages = 1; // TODO 最終的にzipファイルに取り込むページ数を入れる
  306. // const createdPageArchive = PageArchive.create({
  307. // owner,
  308. // fileType,
  309. // rootPagePath,
  310. // numOfPages,
  311. // hasComment: isCommentDownload,
  312. // hasAttachment: isAttachmentFileDownload,
  313. // });
  314. // console.log(createdPageArchive);
  315. // return res.apiv3({ });
  316. // });
  317. // router.get('/count-children-pages', accessTokenParser, loginRequired, async(req, res) => {
  318. // // TO DO implement correct number at another task
  319. // const { pageId } = req.query;
  320. // console.log(pageId);
  321. // const dummy = 6;
  322. // return res.apiv3({ dummy });
  323. // });
  324. return router;
  325. };