page.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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 { convertToNewAffiliationPath } = require('../../../lib/util/path-utils');
  7. const ErrorV3 = require('../../models/vo/error-apiv3');
  8. /**
  9. * @swagger
  10. * tags:
  11. * name: Page
  12. */
  13. /**
  14. * @swagger
  15. *
  16. * components:
  17. * schemas:
  18. * Page:
  19. * description: Page
  20. * type: object
  21. * properties:
  22. * _id:
  23. * type: string
  24. * description: page ID
  25. * example: 5e07345972560e001761fa63
  26. * __v:
  27. * type: number
  28. * description: DB record version
  29. * example: 0
  30. * commentCount:
  31. * type: number
  32. * description: count of comments
  33. * example: 3
  34. * createdAt:
  35. * type: string
  36. * description: date created at
  37. * example: 2010-01-01T00:00:00.000Z
  38. * creator:
  39. * $ref: '#/components/schemas/User'
  40. * extended:
  41. * type: object
  42. * description: extend data
  43. * example: {}
  44. * grant:
  45. * type: number
  46. * description: grant
  47. * example: 1
  48. * grantedUsers:
  49. * type: array
  50. * description: granted users
  51. * items:
  52. * type: string
  53. * description: user ID
  54. * example: ["5ae5fccfc5577b0004dbd8ab"]
  55. * lastUpdateUser:
  56. * $ref: '#/components/schemas/User'
  57. * liker:
  58. * type: array
  59. * description: granted users
  60. * items:
  61. * type: string
  62. * description: user ID
  63. * example: []
  64. * path:
  65. * type: string
  66. * description: page path
  67. * example: /
  68. * redirectTo:
  69. * type: string
  70. * description: redirect path
  71. * example: ""
  72. * revision:
  73. * type: string
  74. * description: page revision
  75. * seenUsers:
  76. * type: array
  77. * description: granted users
  78. * items:
  79. * type: string
  80. * description: user ID
  81. * example: ["5ae5fccfc5577b0004dbd8ab"]
  82. * status:
  83. * type: string
  84. * description: status
  85. * enum:
  86. * - 'wip'
  87. * - 'published'
  88. * - 'deleted'
  89. * - 'deprecated'
  90. * example: published
  91. * updatedAt:
  92. * type: string
  93. * description: date updated at
  94. * example: 2010-01-01T00:00:00.000Z
  95. *
  96. * LikeParams:
  97. * description: LikeParams
  98. * type: object
  99. * properties:
  100. * pageId:
  101. * type: string
  102. * description: page ID
  103. * example: 5e07345972560e001761fa63
  104. * bool:
  105. * type: boolean
  106. * description: boolean for like status
  107. *
  108. * LikeInfo:
  109. * description: LikeInfo
  110. * type: object
  111. * properties:
  112. * sumOfLikers:
  113. * type: number
  114. * description: how many people liked the page
  115. * isLiked:
  116. * type: boolean
  117. * description: Whether the request user liked (will be returned if the user is included in the request)
  118. */
  119. module.exports = (crowi) => {
  120. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  121. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  122. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  123. const csrf = require('../../middlewares/csrf')(crowi);
  124. const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
  125. const globalNotificationService = crowi.getGlobalNotificationService();
  126. const { Page, GlobalNotificationSetting } = crowi.models;
  127. const { exportService } = crowi;
  128. const validator = {
  129. likes: [
  130. body('pageId').isString(),
  131. body('bool').isBoolean(),
  132. ],
  133. likeInfo: [
  134. query('_id').isMongoId(),
  135. ],
  136. export: [
  137. query('format').isString().isIn(['md', 'pdf']),
  138. query('revisionId').isString(),
  139. ],
  140. archive: [
  141. body('rootPagePath').isString(),
  142. body('isCommentDownload').isBoolean(),
  143. body('isAttachmentFileDownload').isBoolean(),
  144. body('isSubordinatedPageDownload').isBoolean(),
  145. body('fileType').isString().isIn(['pdf', 'markdown']),
  146. body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
  147. body('hierarchyValue').isNumeric(),
  148. ],
  149. exist: [
  150. query('fromPath').isString(),
  151. query('toPath').isString(),
  152. ],
  153. };
  154. /**
  155. * @swagger
  156. *
  157. * /page/likes:
  158. * put:
  159. * tags: [Page]
  160. * summary: /page/likes
  161. * description: Update liked status
  162. * operationId: updateLikedStatus
  163. * requestBody:
  164. * content:
  165. * application/json:
  166. * schema:
  167. * $ref: '#/components/schemas/LikeParams'
  168. * responses:
  169. * 200:
  170. * description: Succeeded to update liked status.
  171. * content:
  172. * application/json:
  173. * schema:
  174. * $ref: '#/components/schemas/Page'
  175. */
  176. router.put('/likes', accessTokenParser, loginRequiredStrictly, csrf, validator.likes, apiV3FormValidator, async(req, res) => {
  177. const { pageId, bool } = req.body;
  178. let page;
  179. try {
  180. page = await Page.findByIdAndViewer(pageId, req.user);
  181. if (page == null) {
  182. return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
  183. }
  184. if (bool) {
  185. page = await page.like(req.user);
  186. }
  187. else {
  188. page = await page.unlike(req.user);
  189. }
  190. }
  191. catch (err) {
  192. logger.error('update-like-failed', err);
  193. return res.apiv3Err(err, 500);
  194. }
  195. try {
  196. // global notification
  197. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
  198. }
  199. catch (err) {
  200. logger.error('Like notification failed', err);
  201. }
  202. const result = { page };
  203. result.seenUser = page.seenUsers;
  204. return res.apiv3({ result });
  205. });
  206. /**
  207. * @swagger
  208. *
  209. * /page/like-info:
  210. * get:
  211. * tags: [Page]
  212. * summary: /page/like-info
  213. * description: Get like info
  214. * operationId: getLikeInfo
  215. * parameters:
  216. * - name: _id
  217. * in: query
  218. * description: page id
  219. * schema:
  220. * type: string
  221. * responses:
  222. * 200:
  223. * description: Succeeded to get bookmark info.
  224. * content:
  225. * application/json:
  226. * schema:
  227. * $ref: '#/components/schemas/LikeInfo'
  228. */
  229. router.get('/like-info', loginRequired, validator.likeInfo, apiV3FormValidator, async(req, res) => {
  230. const pageId = req.query._id;
  231. const responsesParams = {};
  232. try {
  233. const page = await Page.findById(pageId);
  234. responsesParams.sumOfLikers = page.liker.length;
  235. // guest user return nothing
  236. if (!req.user) {
  237. return res.apiv3(responsesParams);
  238. }
  239. responsesParams.isLiked = page.liker.includes(req.user._id);
  240. return res.apiv3(responsesParams);
  241. }
  242. catch (err) {
  243. logger.error('get-like-count-failed', err);
  244. return res.apiv3Err(err, 500);
  245. }
  246. });
  247. /**
  248. * @swagger
  249. *
  250. * /pages/export:
  251. * get:
  252. * tags: [Export]
  253. * description: return page's markdown
  254. * responses:
  255. * 200:
  256. * description: Return page's markdown
  257. */
  258. router.get('/export/:pageId', loginRequiredStrictly, validator.export, async(req, res) => {
  259. const { pageId } = req.params;
  260. const { format, revisionId = null } = req.query;
  261. let revision;
  262. try {
  263. const Page = crowi.model('Page');
  264. const page = await Page.findByIdAndViewer(pageId, req.user);
  265. if (page == null) {
  266. const isPageExist = await Page.count({ _id: pageId }) > 0;
  267. if (isPageExist) {
  268. // This page exists but req.user has not read permission
  269. return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
  270. }
  271. return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
  272. }
  273. const revisionIdForFind = revisionId || page.revision;
  274. const Revision = crowi.model('Revision');
  275. revision = await Revision.findById(revisionIdForFind);
  276. }
  277. catch (err) {
  278. logger.error('Failed to get page data', err);
  279. return res.apiv3Err(err, 500);
  280. }
  281. const fileName = revision.id;
  282. let stream;
  283. try {
  284. stream = exportService.getReadStreamFromRevision(revision, format);
  285. }
  286. catch (err) {
  287. logger.error('Failed to create readStream', err);
  288. return res.apiv3Err(err, 500);
  289. }
  290. res.set({
  291. 'Content-Disposition': `attachment;filename*=UTF-8''${fileName}.${format}`,
  292. });
  293. return stream.pipe(res);
  294. });
  295. /**
  296. * @swagger
  297. *
  298. * /page/exist-paths:
  299. * get:
  300. * tags: [Page]
  301. * summary: /page/exist-paths
  302. * description: Get already exist paths
  303. * operationId: getAlreadyExistPaths
  304. * parameters:
  305. * - name: fromPath
  306. * in: query
  307. * description: old parent path
  308. * schema:
  309. * type: string
  310. * - name: toPath
  311. * in: query
  312. * description: new parent path
  313. * schema:
  314. * type: string
  315. * responses:
  316. * 200:
  317. * description: Succeeded to retrieve pages.
  318. * content:
  319. * application/json:
  320. * schema:
  321. * properties:
  322. * existPaths:
  323. * type: object
  324. * description: Paths are already exist in DB
  325. * 500:
  326. * description: Internal server error.
  327. */
  328. router.get('/exist-paths', loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
  329. const { fromPath, toPath } = req.query;
  330. try {
  331. const fromPage = await Page.findByPath(fromPath);
  332. const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user);
  333. const toPathDescendantsArray = fromPageDescendants.map((subordinatedPage) => {
  334. return convertToNewAffiliationPath(fromPath, toPath, subordinatedPage.path);
  335. });
  336. const existPages = await Page.findListByPathsArray(toPathDescendantsArray);
  337. const existPaths = existPages.map(page => page.path);
  338. return res.apiv3({ existPaths });
  339. }
  340. catch (err) {
  341. logger.error('Failed to get exist path', err);
  342. return res.apiv3Err(err, 500);
  343. }
  344. });
  345. // TODO GW-2746 bulk export pages
  346. // /**
  347. // * @swagger
  348. // *
  349. // * /page/archive:
  350. // * post:
  351. // * tags: [Page]
  352. // * summary: /page/archive
  353. // * description: create page archive
  354. // * requestBody:
  355. // * content:
  356. // * application/json:
  357. // * schema:
  358. // * properties:
  359. // * rootPagePath:
  360. // * type: string
  361. // * description: path of the root page
  362. // * isCommentDownload:
  363. // * type: boolean
  364. // * description: whether archive data contains comments
  365. // * isAttachmentFileDownload:
  366. // * type: boolean
  367. // * description: whether archive data contains attachments
  368. // * isSubordinatedPageDownload:
  369. // * type: boolean
  370. // * description: whether archive data children pages
  371. // * fileType:
  372. // * type: string
  373. // * description: file type of archive data(.md, .pdf)
  374. // * hierarchyType:
  375. // * type: string
  376. // * description: method of select children pages archive data contains('allSubordinatedPage', 'decideHierarchy')
  377. // * hierarchyValue:
  378. // * type: number
  379. // * description: depth of hierarchy(use when hierarchyType is 'decideHierarchy')
  380. // * responses:
  381. // * 200:
  382. // * description: create page archive
  383. // * content:
  384. // * application/json:
  385. // * schema:
  386. // * $ref: '#/components/schemas/Page'
  387. // */
  388. // router.post('/archive', accessTokenParser, loginRequired, csrf, validator.archive, apiV3FormValidator, async(req, res) => {
  389. // const PageArchive = crowi.model('PageArchive');
  390. // const {
  391. // rootPagePath,
  392. // isCommentDownload,
  393. // isAttachmentFileDownload,
  394. // fileType,
  395. // } = req.body;
  396. // const owner = req.user._id;
  397. // const numOfPages = 1; // TODO 最終的にzipファイルに取り込むページ数を入れる
  398. // const createdPageArchive = PageArchive.create({
  399. // owner,
  400. // fileType,
  401. // rootPagePath,
  402. // numOfPages,
  403. // hasComment: isCommentDownload,
  404. // hasAttachment: isAttachmentFileDownload,
  405. // });
  406. // console.log(createdPageArchive);
  407. // return res.apiv3({ });
  408. // });
  409. // router.get('/count-children-pages', accessTokenParser, loginRequired, async(req, res) => {
  410. // // TO DO implement correct number at another task
  411. // const { pageId } = req.query;
  412. // console.log(pageId);
  413. // const dummy = 6;
  414. // return res.apiv3({ dummy });
  415. // });
  416. return router;
  417. };