page.js 15 KB

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