pages.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. import ErrorV3 from '@growi/core/src/models/vo/error-apiv3';
  2. import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
  3. import { subscribeRuleNames } from '~/interfaces/in-app-notification';
  4. import loggerFactory from '~/utils/logger';
  5. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  6. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  7. import { isV5ConversionError } from '../../models/vo/v5-conversion-error';
  8. const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
  9. const { pathUtils, pagePathUtils } = require('@growi/core');
  10. const express = require('express');
  11. const { body } = require('express-validator');
  12. const { query } = require('express-validator');
  13. const mongoose = require('mongoose');
  14. const { isCreatablePage } = pagePathUtils;
  15. const router = express.Router();
  16. const LIMIT_FOR_LIST = 10;
  17. const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  18. /**
  19. * @swagger
  20. * tags:
  21. * name: Pages
  22. */
  23. /**
  24. * @swagger
  25. *
  26. * components:
  27. * schemas:
  28. * Tags:
  29. * description: Tags
  30. * type: array
  31. * items:
  32. * $ref: '#/components/schemas/Tag/properties/name'
  33. * example: ['daily', 'report', 'tips']
  34. *
  35. * Tag:
  36. * description: Tag
  37. * type: object
  38. * properties:
  39. * _id:
  40. * type: string
  41. * description: tag ID
  42. * example: 5e2d6aede35da4004ef7e0b7
  43. * name:
  44. * type: string
  45. * description: tag name
  46. * example: daily
  47. * count:
  48. * type: number
  49. * description: Count of tagged pages
  50. * example: 3
  51. */
  52. /**
  53. * @swagger
  54. *
  55. * components:
  56. * schemas:
  57. * Page:
  58. * description: Page
  59. * type: object
  60. * properties:
  61. * _id:
  62. * type: string
  63. * description: page ID
  64. * example: 5e07345972560e001761fa63
  65. * __v:
  66. * type: number
  67. * description: DB record version
  68. * example: 0
  69. * commentCount:
  70. * type: number
  71. * description: count of comments
  72. * example: 3
  73. * createdAt:
  74. * type: string
  75. * description: date created at
  76. * example: 2010-01-01T00:00:00.000Z
  77. * creator:
  78. * $ref: '#/components/schemas/User'
  79. * extended:
  80. * type: object
  81. * description: extend data
  82. * example: {}
  83. * grant:
  84. * type: number
  85. * description: grant
  86. * example: 1
  87. * grantedUsers:
  88. * type: array
  89. * description: granted users
  90. * items:
  91. * type: string
  92. * description: user ID
  93. * example: ["5ae5fccfc5577b0004dbd8ab"]
  94. * lastUpdateUser:
  95. * $ref: '#/components/schemas/User'
  96. * liker:
  97. * type: array
  98. * description: granted users
  99. * items:
  100. * type: string
  101. * description: user ID
  102. * example: []
  103. * path:
  104. * type: string
  105. * description: page path
  106. * example: /Sandbox/Math
  107. * revision:
  108. * type: string
  109. * description: revision ID
  110. * example: ["5ae5fccfc5577b0004dbd8ab"]
  111. * seenUsers:
  112. * type: array
  113. * description: granted users
  114. * items:
  115. * type: string
  116. * description: user ID
  117. * example: ["5ae5fccfc5577b0004dbd8ab"]
  118. * status:
  119. * type: string
  120. * description: status
  121. * enum:
  122. * - 'wip'
  123. * - 'published'
  124. * - 'deleted'
  125. * - 'deprecated'
  126. * example: published
  127. * updatedAt:
  128. * type: string
  129. * description: date updated at
  130. * example: 2010-01-01T00:00:00.000Z
  131. */
  132. module.exports = (crowi) => {
  133. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  134. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  135. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  136. const adminRequired = require('../../middlewares/admin-required')(crowi);
  137. const Page = crowi.model('Page');
  138. const User = crowi.model('User');
  139. const PageTagRelation = crowi.model('PageTagRelation');
  140. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  141. const activityEvent = crowi.event('activity');
  142. const globalNotificationService = crowi.getGlobalNotificationService();
  143. const userNotificationService = crowi.getUserNotificationService();
  144. const { serializePageSecurely } = require('../../models/serializers/page-serializer');
  145. const { serializeRevisionSecurely } = require('../../models/serializers/revision-serializer');
  146. const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
  147. const addActivity = generateAddActivityMiddleware(crowi);
  148. const validator = {
  149. createPage: [
  150. body('body').exists()
  151. .withMessage('body is re quired but an empty string is allowed'),
  152. body('path').exists().not().isEmpty({ ignore_whitespace: true })
  153. .withMessage('path is required'),
  154. body('grant').if(value => value != null).isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
  155. body('overwriteScopesOfDescendants').if(value => value != null).isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
  156. body('isSlackEnabled').if(value => value != null).isBoolean().withMessage('isSlackEnabled must be boolean'),
  157. body('slackChannels').if(value => value != null).isString().withMessage('slackChannels must be string'),
  158. body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
  159. body('createFromPageTree').optional().isBoolean().withMessage('createFromPageTree must be boolean'),
  160. ],
  161. renamePage: [
  162. body('pageId').isMongoId().withMessage('pageId is required'),
  163. body('revisionId').optional({ nullable: true }).isMongoId().withMessage('revisionId is required'), // required when v4
  164. body('newPagePath').isLength({ min: 1 }).withMessage('newPagePath is required'),
  165. body('isRecursively').if(value => value != null).isBoolean().withMessage('isRecursively must be boolean'),
  166. body('isRenameRedirect').if(value => value != null).isBoolean().withMessage('isRenameRedirect must be boolean'),
  167. body('updateMetadata').if(value => value != null).isBoolean().withMessage('updateMetadata must be boolean'),
  168. body('isMoveMode').if(value => value != null).isBoolean().withMessage('isMoveMode must be boolean'),
  169. ],
  170. resumeRenamePage: [
  171. body('pageId').isMongoId().withMessage('pageId is required'),
  172. ],
  173. duplicatePage: [
  174. body('pageId').isMongoId().withMessage('pageId is required'),
  175. body('pageNameInput').trim().isLength({ min: 1 }).withMessage('pageNameInput is required'),
  176. body('isRecursively').if(value => value != null).isBoolean().withMessage('isRecursively must be boolean'),
  177. ],
  178. deletePages: [
  179. body('pageIdToRevisionIdMap')
  180. .exists()
  181. .withMessage('The body property "pageIdToRevisionIdMap" must be an json map with pageId as key and revisionId as value.'),
  182. body('isCompletely')
  183. .custom(v => v === 'true' || v === true || v == null)
  184. .withMessage('The body property "isCompletely" must be "true" or true. (Omit param for false)'),
  185. body('isRecursively')
  186. .custom(v => v === 'true' || v === true || v == null)
  187. .withMessage('The body property "isRecursively" must be "true" or true. (Omit param for false)'),
  188. ],
  189. legacyPagesMigration: [
  190. body('convertPath').optional().isString().withMessage('convertPath must be a string'),
  191. body('pageIds').optional().isArray().withMessage('pageIds must be an array'),
  192. body('isRecursively')
  193. .optional()
  194. .custom(v => v === 'true' || v === true || v == null)
  195. .withMessage('The body property "isRecursively" must be "true" or true. (Omit param for false)'),
  196. ],
  197. convertPagesByPath: [
  198. body('convertPath').optional().isString().withMessage('convertPath must be a string'),
  199. ],
  200. };
  201. async function createPageAction({
  202. path, body, user, options,
  203. }) {
  204. const createdPage = await crowi.pageService.create(path, body, user, options);
  205. return createdPage;
  206. }
  207. async function saveTagsAction({ createdPage, pageTags }) {
  208. if (pageTags != null) {
  209. const tagEvent = crowi.event('tag');
  210. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  211. tagEvent.emit('update', createdPage, pageTags);
  212. return PageTagRelation.listTagNamesByPage(createdPage.id);
  213. }
  214. return [];
  215. }
  216. /**
  217. * @swagger
  218. *
  219. * /pages:
  220. * post:
  221. * tags: [Pages]
  222. * operationId: createPage
  223. * description: Create page
  224. * requestBody:
  225. * content:
  226. * application/json:
  227. * schema:
  228. * properties:
  229. * body:
  230. * type: string
  231. * description: Text of page
  232. * path:
  233. * $ref: '#/components/schemas/Page/properties/path'
  234. * grant:
  235. * $ref: '#/components/schemas/Page/properties/grant'
  236. * grantUserGroupId:
  237. * type: string
  238. * description: UserGroup ID
  239. * example: 5ae5fccfc5577b0004dbd8ab
  240. * pageTags:
  241. * type: array
  242. * items:
  243. * $ref: '#/components/schemas/Tag'
  244. * createFromPageTree:
  245. * type: boolean
  246. * description: Whether the page was created from the page tree or not
  247. * required:
  248. * - body
  249. * - path
  250. * responses:
  251. * 201:
  252. * description: Succeeded to create page.
  253. * content:
  254. * application/json:
  255. * schema:
  256. * properties:
  257. * data:
  258. * type: object
  259. * properties:
  260. * page:
  261. * $ref: '#/components/schemas/Page'
  262. * tags:
  263. * type: array
  264. * items:
  265. * $ref: '#/components/schemas/Tags'
  266. * revision:
  267. * $ref: '#/components/schemas/Revision'
  268. * 409:
  269. * description: page path is already existed
  270. */
  271. router.post('/', accessTokenParser, loginRequiredStrictly, addActivity, validator.createPage, apiV3FormValidator, async(req, res) => {
  272. const {
  273. body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags,
  274. } = req.body;
  275. let { path } = req.body;
  276. // check whether path starts slash
  277. path = pathUtils.addHeadingSlash(path);
  278. const options = {};
  279. if (grant != null) {
  280. options.grant = grant;
  281. options.grantUserGroupId = grantUserGroupId;
  282. }
  283. let createdPage;
  284. try {
  285. createdPage = await createPageAction({
  286. path, body, user: req.user, options,
  287. });
  288. }
  289. catch (err) {
  290. logger.error('Error occurred while creating a page.', err);
  291. return res.apiv3Err(err);
  292. }
  293. const savedTags = await saveTagsAction({ createdPage, pageTags });
  294. const result = {
  295. page: serializePageSecurely(createdPage),
  296. tags: savedTags,
  297. revision: serializeRevisionSecurely(createdPage.revision),
  298. };
  299. // update scopes for descendants
  300. if (overwriteScopesOfDescendants) {
  301. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  302. }
  303. const parameters = {
  304. targetModel: SupportedTargetModel.MODEL_PAGE,
  305. target: createdPage,
  306. action: SupportedAction.ACTION_PAGE_CREATE,
  307. };
  308. activityEvent.emit('update', res.locals.activity._id, parameters);
  309. res.apiv3(result, 201);
  310. try {
  311. // global notification
  312. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  313. }
  314. catch (err) {
  315. logger.error('Create grobal notification failed', err);
  316. }
  317. // user notification
  318. if (isSlackEnabled) {
  319. try {
  320. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
  321. results.forEach((result) => {
  322. if (result.status === 'rejected') {
  323. logger.error('Create user notification failed', result.reason);
  324. }
  325. });
  326. }
  327. catch (err) {
  328. logger.error('Create user notification failed', err);
  329. }
  330. }
  331. // create subscription
  332. try {
  333. await crowi.inAppNotificationService.createSubscription(req.user.id, createdPage._id, subscribeRuleNames.PAGE_CREATE);
  334. }
  335. catch (err) {
  336. logger.error('Failed to create subscription document', err);
  337. }
  338. });
  339. /**
  340. * @swagger
  341. *
  342. * /pages/recent:
  343. * get:
  344. * tags: [Pages]
  345. * description: Get recently updated pages
  346. * responses:
  347. * 200:
  348. * description: Return pages recently updated
  349. *
  350. */
  351. router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
  352. const limit = 20;
  353. const offset = parseInt(req.query.offset) || 0;
  354. const skip = offset > 0 ? (offset - 1) * limit : offset;
  355. const queryOptions = {
  356. offset: skip,
  357. limit,
  358. includeTrashed: false,
  359. isRegExpEscapedFromPath: true,
  360. sort: 'updatedAt',
  361. desc: -1,
  362. };
  363. try {
  364. const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
  365. if (result.pages.length > limit) {
  366. result.pages.pop();
  367. }
  368. result.pages.forEach((page) => {
  369. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  370. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  371. }
  372. });
  373. const PageTagRelation = mongoose.model('PageTagRelation');
  374. const ids = result.pages.map((page) => { return page._id });
  375. const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
  376. // { pageId: [{ tag }, ...] }
  377. const relationsMap = new Map();
  378. // increment relationsMap
  379. relations.forEach((relation) => {
  380. const pageId = relation.relatedPage.toString();
  381. if (!relationsMap.has(pageId)) {
  382. relationsMap.set(pageId, []);
  383. }
  384. if (relation.relatedTag != null) {
  385. relationsMap.get(pageId).push(relation.relatedTag);
  386. }
  387. });
  388. // add tags to each page
  389. result.pages.forEach((page) => {
  390. const pageId = page._id.toString();
  391. page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
  392. });
  393. return res.apiv3(result);
  394. }
  395. catch (err) {
  396. logger.error('Failed to get recent pages', err);
  397. return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
  398. }
  399. });
  400. /**
  401. * @swagger
  402. *
  403. *
  404. * /pages/rename:
  405. * post:
  406. * tags: [Pages]
  407. * operationId: renamePage
  408. * description: Rename page
  409. * requestBody:
  410. * content:
  411. * application/json:
  412. * schema:
  413. * properties:
  414. * pageId:
  415. * $ref: '#/components/schemas/Page/properties/_id'
  416. * path:
  417. * $ref: '#/components/schemas/Page/properties/path'
  418. * revisionId:
  419. * type: string
  420. * description: revision ID
  421. * example: 5e07345972560e001761fa63
  422. * newPagePath:
  423. * type: string
  424. * description: new path
  425. * example: /user/alice/new_test
  426. * isRenameRedirect:
  427. * type: boolean
  428. * description: whether redirect page
  429. * updateMetadata:
  430. * type: boolean
  431. * description: whether update meta data
  432. * isRecursively:
  433. * type: boolean
  434. * description: whether rename page with descendants
  435. * required:
  436. * - pageId
  437. * - revisionId
  438. * responses:
  439. * 200:
  440. * description: Succeeded to rename page.
  441. * content:
  442. * application/json:
  443. * schema:
  444. * properties:
  445. * page:
  446. * $ref: '#/components/schemas/Page'
  447. * 401:
  448. * description: page id is invalid
  449. * 409:
  450. * description: page path is already existed
  451. */
  452. router.put('/rename', accessTokenParser, loginRequiredStrictly, validator.renamePage, apiV3FormValidator, async(req, res) => {
  453. const { pageId, revisionId } = req.body;
  454. let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
  455. const options = {
  456. isRecursively: req.body.isRecursively,
  457. createRedirectPage: req.body.isRenameRedirect,
  458. updateMetadata: req.body.updateMetadata,
  459. isMoveMode: req.body.isMoveMode,
  460. };
  461. const activityParameters = {
  462. ip: req.ip,
  463. endpoint: req.originalUrl,
  464. };
  465. if (!isCreatablePage(newPagePath)) {
  466. return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath}'`, 'invalid_path'), 409);
  467. }
  468. // check whether path starts slash
  469. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  470. const isExist = await Page.count({ path: newPagePath }) > 0;
  471. if (isExist) {
  472. // if page found, cannot rename to that path
  473. return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
  474. }
  475. let page;
  476. let renamedPage;
  477. try {
  478. page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  479. options.isRecursively = page.descendantCount > 0;
  480. if (page == null) {
  481. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  482. }
  483. // empty page does not require revisionId validation
  484. if (!page.isEmpty && revisionId == null) {
  485. return res.apiv3Err(new ErrorV3('revisionId must be a mongoId', 'invalid_body'), 400);
  486. }
  487. if (!page.isEmpty && !page.isUpdatable(revisionId)) {
  488. return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
  489. }
  490. renamedPage = await crowi.pageService.renamePage(page, newPagePath, req.user, options, activityParameters);
  491. }
  492. catch (err) {
  493. logger.error(err);
  494. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  495. }
  496. const result = { page: serializePageSecurely(renamedPage ?? page) };
  497. try {
  498. // global notification
  499. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
  500. oldPath: req.body.path,
  501. });
  502. }
  503. catch (err) {
  504. logger.error('Move notification failed', err);
  505. }
  506. return res.apiv3(result);
  507. });
  508. router.post('/resume-rename', accessTokenParser, loginRequiredStrictly, validator.resumeRenamePage, apiV3FormValidator, async(req, res) => {
  509. const { pageId } = req.body;
  510. const { user } = req;
  511. // The user has permission to resume rename operation if page is returned.
  512. const page = await Page.findByIdAndViewer(pageId, user, null, true);
  513. if (page == null) {
  514. const msg = 'The operation is forbidden for this user';
  515. const code = 'forbidden-user';
  516. return res.apiv3Err(new ErrorV3(msg, code), 403);
  517. }
  518. const pageOp = await crowi.pageOperationService.getRenameSubOperationByPageId(page._id);
  519. if (pageOp == null) {
  520. const msg = 'PageOperation document for Rename Sub operation not found.';
  521. const code = 'document_not_found';
  522. return res.apiv3Err(new ErrorV3(msg, code), 404);
  523. }
  524. try {
  525. await crowi.pageService.resumeRenameSubOperation(page, pageOp);
  526. }
  527. catch (err) {
  528. logger.error(err);
  529. return res.apiv3Err(err, 500);
  530. }
  531. return res.apiv3();
  532. });
  533. /**
  534. * @swagger
  535. *
  536. * /pages/empty-trash:
  537. * delete:
  538. * tags: [Pages]
  539. * description: empty trash
  540. * responses:
  541. * 200:
  542. * description: Succeeded to remove all trash pages
  543. */
  544. router.delete('/empty-trash', accessTokenParser, loginRequired, addActivity, apiV3FormValidator, async(req, res) => {
  545. const options = {};
  546. const pagesInTrash = await crowi.pageService.findChildrenByParentPathOrIdAndViewer('/trash', req.user);
  547. const deletablePages = crowi.pageService.filterPagesByCanDeleteCompletely(pagesInTrash, req.user, true);
  548. if (deletablePages.length === 0) {
  549. const msg = 'No pages can be deleted.';
  550. return res.apiv3Err(new ErrorV3(msg), 500);
  551. }
  552. const parameters = { action: SupportedAction.ACTION_PAGE_EMPTY_TRASH };
  553. // when some pages are not deletable
  554. if (deletablePages.length < pagesInTrash.length) {
  555. try {
  556. const options = { isCompletely: true, isRecursively: true };
  557. await crowi.pageService.deleteMultiplePages(deletablePages, req.user, options);
  558. activityEvent.emit('update', res.locals.activity._id, parameters);
  559. return res.apiv3({ deletablePages });
  560. }
  561. catch (err) {
  562. logger.error(err);
  563. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  564. }
  565. }
  566. // when all pages are deletable
  567. else {
  568. try {
  569. const pages = await crowi.pageService.emptyTrashPage(req.user, options);
  570. activityEvent.emit('update', res.locals.activity._id, parameters);
  571. return res.apiv3({ pages });
  572. }
  573. catch (err) {
  574. logger.error(err);
  575. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  576. }
  577. }
  578. });
  579. validator.displayList = [
  580. query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
  581. ];
  582. router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
  583. const { isTrashPage } = pagePathUtils;
  584. const { path } = req.query;
  585. const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
  586. const page = req.query.page || 1;
  587. const offset = (page - 1) * limit;
  588. let includeTrashed = false;
  589. if (isTrashPage(path)) {
  590. includeTrashed = true;
  591. }
  592. const queryOptions = {
  593. offset,
  594. limit,
  595. includeTrashed,
  596. };
  597. try {
  598. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  599. result.pages.forEach((page) => {
  600. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  601. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  602. }
  603. });
  604. return res.apiv3(result);
  605. }
  606. catch (err) {
  607. logger.error('Failed to get Descendants Pages', err);
  608. return res.apiv3Err(err, 500);
  609. }
  610. });
  611. /**
  612. * @swagger
  613. *
  614. *
  615. * /pages/duplicate:
  616. * post:
  617. * tags: [Pages]
  618. * operationId: duplicatePage
  619. * description: Duplicate page
  620. * requestBody:
  621. * content:
  622. * application/json:
  623. * schema:
  624. * properties:
  625. * pageId:
  626. * $ref: '#/components/schemas/Page/properties/_id'
  627. * pageNameInput:
  628. * $ref: '#/components/schemas/Page/properties/path'
  629. * isRecursively:
  630. * type: boolean
  631. * description: whether duplicate page with descendants
  632. * required:
  633. * - pageId
  634. * responses:
  635. * 200:
  636. * description: Succeeded to duplicate page.
  637. * content:
  638. * application/json:
  639. * schema:
  640. * properties:
  641. * page:
  642. * $ref: '#/components/schemas/Page'
  643. *
  644. * 403:
  645. * description: Forbidden to duplicate page.
  646. * 500:
  647. * description: Internal server error.
  648. */
  649. router.post('/duplicate', accessTokenParser, loginRequiredStrictly, addActivity, validator.duplicatePage, apiV3FormValidator, async(req, res) => {
  650. const { pageId, isRecursively } = req.body;
  651. const newPagePath = pathUtils.normalizePath(req.body.pageNameInput);
  652. const isCreatable = isCreatablePage(newPagePath);
  653. if (!isCreatable) {
  654. return res.apiv3Err(new ErrorV3('This page path is invalid', 'invalid_path'), 400);
  655. }
  656. // check page existence
  657. const isExist = (await Page.count({ path: newPagePath })) > 0;
  658. if (isExist) {
  659. return res.apiv3Err(new ErrorV3(`Page exists '${newPagePath})'`, 'already_exists'), 409);
  660. }
  661. const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  662. const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
  663. if (page == null || isEmptyAndNotRecursively) {
  664. res.code = 'Page is not found';
  665. logger.error('Failed to find the pages');
  666. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  667. }
  668. const newParentPage = await crowi.pageService.duplicate(page, newPagePath, req.user, isRecursively);
  669. const result = { page: serializePageSecurely(newParentPage) };
  670. // copy the page since it's used and updated in crowi.pageService.duplicate
  671. const copyPage = { ...page };
  672. copyPage.path = newPagePath;
  673. try {
  674. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, copyPage, req.user);
  675. }
  676. catch (err) {
  677. logger.error('Create grobal notification failed', err);
  678. }
  679. // create subscription (parent page only)
  680. try {
  681. await crowi.inAppNotificationService.createSubscription(req.user.id, newParentPage._id, subscribeRuleNames.PAGE_CREATE);
  682. }
  683. catch (err) {
  684. logger.error('Failed to create subscription document', err);
  685. }
  686. const parameters = {
  687. targetModel: SupportedTargetModel.MODEL_PAGE,
  688. target: page,
  689. action: SupportedAction.ACTION_PAGE_DUPLICATE,
  690. };
  691. activityEvent.emit('update', res.locals.activity._id, parameters, page);
  692. return res.apiv3(result);
  693. });
  694. /**
  695. * @swagger
  696. *
  697. *
  698. * /pages/subordinated-list:
  699. * get:
  700. * tags: [Pages]
  701. * operationId: subordinatedList
  702. * description: Get subordinated pages
  703. * parameters:
  704. * - name: path
  705. * in: query
  706. * description: Parent path of search
  707. * schema:
  708. * type: string
  709. * - name: limit
  710. * in: query
  711. * description: Limit of acquisitions
  712. * schema:
  713. * type: number
  714. * responses:
  715. * 200:
  716. * description: Succeeded to retrieve pages.
  717. * content:
  718. * application/json:
  719. * schema:
  720. * properties:
  721. * subordinatedPaths:
  722. * type: object
  723. * description: descendants page
  724. * 500:
  725. * description: Internal server error.
  726. */
  727. router.get('/subordinated-list', accessTokenParser, loginRequired, async(req, res) => {
  728. const { path } = req.query;
  729. const limit = parseInt(req.query.limit) || LIMIT_FOR_LIST;
  730. try {
  731. const pageData = await Page.findByPath(path, true);
  732. const result = await Page.findManageableListWithDescendants(pageData, req.user, { limit });
  733. return res.apiv3({ subordinatedPages: result });
  734. }
  735. catch (err) {
  736. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  737. }
  738. });
  739. router.post('/delete', accessTokenParser, loginRequiredStrictly, validator.deletePages, apiV3FormValidator, async(req, res) => {
  740. const { pageIdToRevisionIdMap, isCompletely, isRecursively } = req.body;
  741. const pageIds = Object.keys(pageIdToRevisionIdMap);
  742. if (pageIds.length === 0) {
  743. return res.apiv3Err(new ErrorV3('Select pages to delete.', 'no_page_selected'), 400);
  744. }
  745. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  746. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  747. }
  748. let pagesToDelete;
  749. try {
  750. pagesToDelete = await Page.findByIdsAndViewer(pageIds, req.user, null, true);
  751. }
  752. catch (err) {
  753. logger.error('Failed to find pages to delete.', err);
  754. return res.apiv3Err(new ErrorV3('Failed to find pages to delete.'));
  755. }
  756. let pagesCanBeDeleted;
  757. /*
  758. * Delete Completely
  759. */
  760. if (isCompletely) {
  761. pagesCanBeDeleted = crowi.pageService.filterPagesByCanDeleteCompletely(pagesToDelete, req.user, isRecursively);
  762. }
  763. /*
  764. * Trash
  765. */
  766. else {
  767. pagesCanBeDeleted = pagesToDelete.filter(p => p.isEmpty || p.isUpdatable(pageIdToRevisionIdMap[p._id].toString()));
  768. pagesCanBeDeleted = crowi.pageService.filterPagesByCanDelete(pagesToDelete, req.user, isRecursively);
  769. }
  770. if (pagesCanBeDeleted.length === 0) {
  771. const msg = 'No pages can be deleted.';
  772. return res.apiv3Err(new ErrorV3(msg), 500);
  773. }
  774. // run delete
  775. const options = { isCompletely, isRecursively };
  776. crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options);
  777. return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
  778. });
  779. // eslint-disable-next-line max-len
  780. router.post('/convert-pages-by-path', accessTokenParser, loginRequiredStrictly, adminRequired, validator.convertPagesByPath, apiV3FormValidator, async(req, res) => {
  781. const { convertPath } = req.body;
  782. // Convert by path
  783. const normalizedPath = pathUtils.normalizePath(convertPath);
  784. try {
  785. await crowi.pageService.normalizeParentByPath(normalizedPath, req.user);
  786. }
  787. catch (err) {
  788. logger.error(err);
  789. if (isV5ConversionError(err)) {
  790. return res.apiv3Err(new ErrorV3(err.message, err.code), 400);
  791. }
  792. return res.apiv3Err(new ErrorV3('Failed to convert pages.'), 400);
  793. }
  794. return res.apiv3({});
  795. });
  796. // eslint-disable-next-line max-len
  797. router.post('/legacy-pages-migration', accessTokenParser, loginRequired, validator.legacyPagesMigration, apiV3FormValidator, async(req, res) => {
  798. const { pageIds: _pageIds, isRecursively } = req.body;
  799. // Convert by pageIds
  800. const pageIds = _pageIds == null ? [] : _pageIds;
  801. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  802. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  803. }
  804. if (pageIds.length === 0) {
  805. return res.apiv3Err(new ErrorV3('No page is selected.'), 400);
  806. }
  807. try {
  808. if (isRecursively) {
  809. await crowi.pageService.normalizeParentByPageIdsRecursively(pageIds, req.user);
  810. }
  811. else {
  812. await crowi.pageService.normalizeParentByPageIds(pageIds, req.user);
  813. }
  814. }
  815. catch (err) {
  816. return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
  817. }
  818. return res.apiv3({});
  819. });
  820. router.get('/v5-migration-status', accessTokenParser, loginRequired, async(req, res) => {
  821. try {
  822. const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  823. const migratablePagesCount = req.user != null ? await crowi.pageService.countPagesCanNormalizeParentByUser(req.user) : null; // null check since not using loginRequiredStrictly
  824. return res.apiv3({ isV5Compatible, migratablePagesCount });
  825. }
  826. catch (err) {
  827. return res.apiv3Err(new ErrorV3('Failed to obtain migration status'));
  828. }
  829. });
  830. return router;
  831. };