page.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. import { pagePathUtils } from '@growi/core';
  2. import { AllSubscriptionStatusType } from '~/interfaces/subscription';
  3. import Subscription from '~/server/models/subscription';
  4. import loggerFactory from '~/utils/logger';
  5. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  6. const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
  7. const express = require('express');
  8. const { body, query, param } = require('express-validator');
  9. const router = express.Router();
  10. const { convertToNewAffiliationPath, isTopPage } = pagePathUtils;
  11. const ErrorV3 = require('../../models/vo/error-apiv3');
  12. /**
  13. * @swagger
  14. * tags:
  15. * name: Page
  16. */
  17. /**
  18. * @swagger
  19. *
  20. * components:
  21. * schemas:
  22. * Page:
  23. * description: Page
  24. * type: object
  25. * properties:
  26. * _id:
  27. * type: string
  28. * description: page ID
  29. * example: 5e07345972560e001761fa63
  30. * __v:
  31. * type: number
  32. * description: DB record version
  33. * example: 0
  34. * commentCount:
  35. * type: number
  36. * description: count of comments
  37. * example: 3
  38. * createdAt:
  39. * type: string
  40. * description: date created at
  41. * example: 2010-01-01T00:00:00.000Z
  42. * creator:
  43. * $ref: '#/components/schemas/User'
  44. * extended:
  45. * type: object
  46. * description: extend data
  47. * example: {}
  48. * grant:
  49. * type: number
  50. * description: grant
  51. * example: 1
  52. * grantedUsers:
  53. * type: array
  54. * description: granted users
  55. * items:
  56. * type: string
  57. * description: user ID
  58. * example: ["5ae5fccfc5577b0004dbd8ab"]
  59. * lastUpdateUser:
  60. * $ref: '#/components/schemas/User'
  61. * liker:
  62. * type: array
  63. * description: granted users
  64. * items:
  65. * type: string
  66. * description: user ID
  67. * example: []
  68. * path:
  69. * type: string
  70. * description: page 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. * PageInfo:
  109. * description: PageInfo
  110. * type: object
  111. * required:
  112. * - sumOfLikers
  113. * - likerIds
  114. * - sumOfSeenUsers
  115. * - seenUserIds
  116. * properties:
  117. * isLiked:
  118. * type: boolean
  119. * description: Whether the page is liked by the logged in user
  120. * sumOfLikers:
  121. * type: number
  122. * description: Number of users who have liked the page
  123. * likerIds:
  124. * type: array
  125. * items:
  126. * type: string
  127. * description: Ids of users who have liked the page
  128. * example: ["5e07345972560e001761fa63"]
  129. * sumOfSeenUsers:
  130. * type: number
  131. * description: Number of users who have seen the page
  132. * seenUserIds:
  133. * type: array
  134. * items:
  135. * type: string
  136. * description: Ids of users who have seen the page
  137. * example: ["5e07345972560e001761fa63"]
  138. *
  139. * PageParams:
  140. * description: PageParams
  141. * type: object
  142. * required:
  143. * - pageId
  144. * properties:
  145. * pageId:
  146. * type: string
  147. * description: page ID
  148. * example: 5e07345972560e001761fa63
  149. */
  150. module.exports = (crowi) => {
  151. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  152. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  153. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  154. const csrf = require('../../middlewares/csrf')(crowi);
  155. const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
  156. const globalNotificationService = crowi.getGlobalNotificationService();
  157. const socketIoService = crowi.socketIoService;
  158. const { Page, GlobalNotificationSetting, Bookmark } = crowi.models;
  159. const { pageService, exportService } = crowi;
  160. const validator = {
  161. getPage: [
  162. query('id').if(value => value != null).isMongoId(),
  163. query('path').if(value => value != null).isString(),
  164. ],
  165. likes: [
  166. body('pageId').isString(),
  167. body('bool').isBoolean(),
  168. ],
  169. info: [
  170. query('pageId').isMongoId().withMessage('pageId is required'),
  171. ],
  172. isGrantNormalized: [
  173. query('pageId').isMongoId().withMessage('pageId is required'),
  174. ],
  175. applicableGrant: [
  176. query('pageId').isMongoId().withMessage('pageId is required'),
  177. ],
  178. updateGrant: [
  179. param('pageId').isMongoId().withMessage('pageId is required'),
  180. body('grant').isInt().withMessage('grant is required'),
  181. body('grantedGroup').optional().isMongoId().withMessage('grantedGroup must be a mongo id'),
  182. ],
  183. export: [
  184. query('format').isString().isIn(['md', 'pdf']),
  185. query('revisionId').isString(),
  186. ],
  187. archive: [
  188. body('rootPagePath').isString(),
  189. body('isCommentDownload').isBoolean(),
  190. body('isAttachmentFileDownload').isBoolean(),
  191. body('isSubordinatedPageDownload').isBoolean(),
  192. body('fileType').isString().isIn(['pdf', 'markdown']),
  193. body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
  194. body('hierarchyValue').isNumeric(),
  195. ],
  196. exist: [
  197. query('fromPath').isString(),
  198. query('toPath').isString(),
  199. ],
  200. subscribe: [
  201. body('pageId').isString(),
  202. body('status').isIn(AllSubscriptionStatusType),
  203. ],
  204. subscribeStatus: [
  205. query('pageId').isString(),
  206. ],
  207. };
  208. /**
  209. * @swagger
  210. *
  211. * /page:
  212. * get:
  213. * tags: [Page]
  214. * operationId: getPage
  215. * summary: /page
  216. * description: get page by pagePath or pageId
  217. * parameters:
  218. * - name: pageId
  219. * in: query
  220. * description: page id
  221. * schema:
  222. * $ref: '#/components/schemas/Page/properties/_id'
  223. * - name: path
  224. * in: query
  225. * description: page path
  226. * schema:
  227. * $ref: '#/components/schemas/Page/properties/path'
  228. * responses:
  229. * 200:
  230. * description: Page data
  231. * content:
  232. * application/json:
  233. * schema:
  234. * $ref: '#/components/schemas/Page'
  235. */
  236. router.get('/', accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
  237. const { pageId, path } = req.query;
  238. if (pageId == null && path == null) {
  239. return res.apiv3Err(new ErrorV3('Parameter path or pageId is required.', 'invalid-request'));
  240. }
  241. let page;
  242. try {
  243. if (pageId != null) { // prioritized
  244. page = await Page.findByIdAndViewer(pageId, req.user);
  245. }
  246. else {
  247. page = await Page.findByPathAndViewer(path, req.user);
  248. }
  249. }
  250. catch (err) {
  251. logger.error('get-page-failed', err);
  252. return res.apiv3Err(err, 500);
  253. }
  254. if (page == null) {
  255. return res.apiv3Err('Page is not found', 404);
  256. }
  257. try {
  258. page.initLatestRevisionField();
  259. // populate
  260. page = await page.populateDataToShowRevision();
  261. }
  262. catch (err) {
  263. logger.error('populate-page-failed', err);
  264. return res.apiv3Err(err, 500);
  265. }
  266. return res.apiv3({ page });
  267. });
  268. /**
  269. * @swagger
  270. *
  271. * /page/likes:
  272. * put:
  273. * tags: [Page]
  274. * summary: /page/likes
  275. * description: Update liked status
  276. * operationId: updateLikedStatus
  277. * requestBody:
  278. * content:
  279. * application/json:
  280. * schema:
  281. * $ref: '#/components/schemas/LikeParams'
  282. * responses:
  283. * 200:
  284. * description: Succeeded to update liked status.
  285. * content:
  286. * application/json:
  287. * schema:
  288. * $ref: '#/components/schemas/Page'
  289. */
  290. router.put('/likes', accessTokenParser, loginRequiredStrictly, csrf, validator.likes, apiV3FormValidator, async(req, res) => {
  291. const { pageId, bool: isLiked } = req.body;
  292. let page;
  293. try {
  294. page = await Page.findByIdAndViewer(pageId, req.user);
  295. if (page == null) {
  296. return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
  297. }
  298. if (isLiked) {
  299. page = await page.like(req.user);
  300. }
  301. else {
  302. page = await page.unlike(req.user);
  303. }
  304. }
  305. catch (err) {
  306. logger.error('update-like-failed', err);
  307. return res.apiv3Err(err, 500);
  308. }
  309. const result = { page };
  310. result.seenUser = page.seenUsers;
  311. res.apiv3({ result });
  312. if (isLiked) {
  313. const pageEvent = crowi.event('page');
  314. // in-app notification
  315. pageEvent.emit('like', page, req.user);
  316. try {
  317. // global notification
  318. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
  319. }
  320. catch (err) {
  321. logger.error('Like notification failed', err);
  322. }
  323. }
  324. });
  325. /**
  326. * @swagger
  327. *
  328. * /page/info:
  329. * get:
  330. * tags: [Page]
  331. * summary: /page/info
  332. * description: Retrieve current page info
  333. * operationId: getPageInfo
  334. * requestBody:
  335. * content:
  336. * application/json:
  337. * schema:
  338. * $ref: '#/components/schemas/PageParams'
  339. * responses:
  340. * 200:
  341. * description: Successfully retrieved current page info.
  342. * content:
  343. * application/json:
  344. * schema:
  345. * $ref: '#/components/schemas/PageInfo'
  346. * 500:
  347. * description: Internal server error.
  348. */
  349. router.get('/info', certifySharedPage, loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
  350. const { user, isSharedPage } = req;
  351. const { pageId } = req.query;
  352. try {
  353. const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, null, user, true, isSharedPage);
  354. if (pageWithMeta == null) {
  355. return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
  356. }
  357. return res.apiv3(pageWithMeta.meta);
  358. }
  359. catch (err) {
  360. logger.error('get-page-info', err);
  361. return res.apiv3Err(err, 500);
  362. }
  363. });
  364. /**
  365. * @swagger
  366. *
  367. * /page/is-grant-normalized:
  368. * get:
  369. * tags: [Page]
  370. * summary: /page/info
  371. * description: Retrieve current page's isGrantNormalized value
  372. * operationId: getIsGrantNormalized
  373. * parameters:
  374. * - name: pageId
  375. * in: query
  376. * description: page id
  377. * schema:
  378. * $ref: '#/components/schemas/Page/properties/_id'
  379. * responses:
  380. * 200:
  381. * description: Successfully retrieved current isGrantNormalized.
  382. * content:
  383. * application/json:
  384. * schema:
  385. * type: object
  386. * properties:
  387. * isGrantNormalized:
  388. * type: boolean
  389. * 400:
  390. * description: Bad request. Page is unreachable or empty.
  391. * 500:
  392. * description: Internal server error.
  393. */
  394. router.get('/is-grant-normalized', loginRequiredStrictly, validator.isGrantNormalized, apiV3FormValidator, async(req, res) => {
  395. const { pageId } = req.query;
  396. const Page = crowi.model('Page');
  397. const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
  398. if (page == null) {
  399. return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
  400. }
  401. const {
  402. path, grant, grantedUsers, grantedGroup,
  403. } = page;
  404. let isGrantNormalized;
  405. try {
  406. isGrantNormalized = await crowi.pageGrantService.isGrantNormalized(req.user, path, grant, grantedUsers, grantedGroup, false, false);
  407. }
  408. catch (err) {
  409. logger.error('Error occurred while processing isGrantNormalized.', err);
  410. return res.apiv3Err(err, 500);
  411. }
  412. return res.apiv3({ isGrantNormalized });
  413. });
  414. router.get('/applicable-grant', loginRequiredStrictly, validator.applicableGrant, apiV3FormValidator, async(req, res) => {
  415. const { pageId } = req.query;
  416. const Page = crowi.model('Page');
  417. const page = await Page.findByIdAndViewer(pageId, req.user, null);
  418. if (page == null) {
  419. return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
  420. }
  421. let data;
  422. try {
  423. data = await crowi.pageGrantService.calcApplicableGrantData(page, req.user);
  424. }
  425. catch (err) {
  426. logger.error('Error occurred while processing calcApplicableGrantData.', err);
  427. return res.apiv3Err(err, 500);
  428. }
  429. return res.apiv3(data);
  430. });
  431. router.put('/:pageId/grant', loginRequiredStrictly, validator.updateGrant, apiV3FormValidator, async(req, res) => {
  432. const { pageId } = req.params;
  433. const { grant, grantedGroup } = req.body;
  434. const Page = crowi.model('Page');
  435. const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
  436. if (page == null) {
  437. return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
  438. }
  439. let data;
  440. try {
  441. const shouldUseV4Process = false;
  442. const grantData = { grant, grantedGroup };
  443. data = await Page.updateGrant(page, req.user, grantData, shouldUseV4Process);
  444. }
  445. catch (err) {
  446. logger.error('Error occurred while processing calcApplicableGrantData.', err);
  447. return res.apiv3Err(err, 500);
  448. }
  449. return res.apiv3(data);
  450. });
  451. /**
  452. * @swagger
  453. *
  454. * /pages/export:
  455. * get:
  456. * tags: [Export]
  457. * description: return page's markdown
  458. * responses:
  459. * 200:
  460. * description: Return page's markdown
  461. */
  462. router.get('/export/:pageId', loginRequiredStrictly, validator.export, async(req, res) => {
  463. const { pageId } = req.params;
  464. const { format, revisionId = null } = req.query;
  465. let revision;
  466. try {
  467. const Page = crowi.model('Page');
  468. const page = await Page.findByIdAndViewer(pageId, req.user);
  469. if (page == null) {
  470. const isPageExist = await Page.count({ _id: pageId }) > 0;
  471. if (isPageExist) {
  472. // This page exists but req.user has not read permission
  473. return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
  474. }
  475. return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
  476. }
  477. const revisionIdForFind = revisionId || page.revision;
  478. const Revision = crowi.model('Revision');
  479. revision = await Revision.findById(revisionIdForFind);
  480. }
  481. catch (err) {
  482. logger.error('Failed to get page data', err);
  483. return res.apiv3Err(err, 500);
  484. }
  485. const fileName = revision.id;
  486. let stream;
  487. try {
  488. stream = exportService.getReadStreamFromRevision(revision, format);
  489. }
  490. catch (err) {
  491. logger.error('Failed to create readStream', err);
  492. return res.apiv3Err(err, 500);
  493. }
  494. res.set({
  495. 'Content-Disposition': `attachment;filename*=UTF-8''${fileName}.${format}`,
  496. });
  497. return stream.pipe(res);
  498. });
  499. /**
  500. * @swagger
  501. *
  502. * /page/exist-paths:
  503. * get:
  504. * tags: [Page]
  505. * summary: /page/exist-paths
  506. * description: Get already exist paths
  507. * operationId: getAlreadyExistPaths
  508. * parameters:
  509. * - name: fromPath
  510. * in: query
  511. * description: old parent path
  512. * schema:
  513. * type: string
  514. * - name: toPath
  515. * in: query
  516. * description: new parent path
  517. * schema:
  518. * type: string
  519. * responses:
  520. * 200:
  521. * description: Succeeded to retrieve pages.
  522. * content:
  523. * application/json:
  524. * schema:
  525. * properties:
  526. * existPaths:
  527. * type: object
  528. * description: Paths are already exist in DB
  529. * 500:
  530. * description: Internal server error.
  531. */
  532. router.get('/exist-paths', loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
  533. const { fromPath, toPath } = req.query;
  534. try {
  535. const fromPage = await Page.findByPath(fromPath);
  536. const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user);
  537. const toPathDescendantsArray = fromPageDescendants.map((subordinatedPage) => {
  538. return convertToNewAffiliationPath(fromPath, toPath, subordinatedPage.path);
  539. });
  540. const existPages = await Page.findListByPathsArray(toPathDescendantsArray);
  541. const existPaths = existPages.map(page => page.path);
  542. return res.apiv3({ existPaths });
  543. }
  544. catch (err) {
  545. logger.error('Failed to get exist path', err);
  546. return res.apiv3Err(err, 500);
  547. }
  548. });
  549. // TODO GW-2746 bulk export pages
  550. // /**
  551. // * @swagger
  552. // *
  553. // * /page/archive:
  554. // * post:
  555. // * tags: [Page]
  556. // * summary: /page/archive
  557. // * description: create page archive
  558. // * requestBody:
  559. // * content:
  560. // * application/json:
  561. // * schema:
  562. // * properties:
  563. // * rootPagePath:
  564. // * type: string
  565. // * description: path of the root page
  566. // * isCommentDownload:
  567. // * type: boolean
  568. // * description: whether archive data contains comments
  569. // * isAttachmentFileDownload:
  570. // * type: boolean
  571. // * description: whether archive data contains attachments
  572. // * isSubordinatedPageDownload:
  573. // * type: boolean
  574. // * description: whether archive data children pages
  575. // * fileType:
  576. // * type: string
  577. // * description: file type of archive data(.md, .pdf)
  578. // * hierarchyType:
  579. // * type: string
  580. // * description: method of select children pages archive data contains('allSubordinatedPage', 'decideHierarchy')
  581. // * hierarchyValue:
  582. // * type: number
  583. // * description: depth of hierarchy(use when hierarchyType is 'decideHierarchy')
  584. // * responses:
  585. // * 200:
  586. // * description: create page archive
  587. // * content:
  588. // * application/json:
  589. // * schema:
  590. // * $ref: '#/components/schemas/Page'
  591. // */
  592. // router.post('/archive', accessTokenParser, loginRequired, csrf, validator.archive, apiV3FormValidator, async(req, res) => {
  593. // const PageArchive = crowi.model('PageArchive');
  594. // const {
  595. // rootPagePath,
  596. // isCommentDownload,
  597. // isAttachmentFileDownload,
  598. // fileType,
  599. // } = req.body;
  600. // const owner = req.user._id;
  601. // const numOfPages = 1; // TODO 最終的にzipファイルに取り込むページ数を入れる
  602. // const createdPageArchive = PageArchive.create({
  603. // owner,
  604. // fileType,
  605. // rootPagePath,
  606. // numOfPages,
  607. // hasComment: isCommentDownload,
  608. // hasAttachment: isAttachmentFileDownload,
  609. // });
  610. // console.log(createdPageArchive);
  611. // return res.apiv3({ });
  612. // });
  613. // router.get('/count-children-pages', accessTokenParser, loginRequired, async(req, res) => {
  614. // // TO DO implement correct number at another task
  615. // const { pageId } = req.query;
  616. // console.log(pageId);
  617. // const dummy = 6;
  618. // return res.apiv3({ dummy });
  619. // });
  620. /**
  621. * @swagger
  622. *
  623. * /page/subscribe:
  624. * put:
  625. * tags: [Page]
  626. * summary: /page/subscribe
  627. * description: Update subscription status
  628. * operationId: updateSubscriptionStatus
  629. * requestBody:
  630. * content:
  631. * application/json:
  632. * schema:
  633. * properties:
  634. * pageId:
  635. * $ref: '#/components/schemas/Page/properties/_id'
  636. * responses:
  637. * 200:
  638. * description: Succeeded to update subscription status.
  639. * content:
  640. * application/json:
  641. * schema:
  642. * $ref: '#/components/schemas/Page'
  643. * 500:
  644. * description: Internal server error.
  645. */
  646. router.put('/subscribe', accessTokenParser, loginRequiredStrictly, csrf, validator.subscribe, apiV3FormValidator, async(req, res) => {
  647. const { pageId, status } = req.body;
  648. const userId = req.user._id;
  649. try {
  650. const subscription = await Subscription.subscribeByPageId(userId, pageId, status);
  651. return res.apiv3({ subscription });
  652. }
  653. catch (err) {
  654. logger.error('Failed to update subscribe status', err);
  655. return res.apiv3Err(err, 500);
  656. }
  657. });
  658. return router;
  659. };