pages.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
  2. import { subscribeRuleNames } from '~/interfaces/in-app-notification';
  3. import loggerFactory from '~/utils/logger';
  4. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  5. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  6. import { isV5ConversionError } from '../../models/vo/v5-conversion-error';
  7. import { ErrorV3 } from '@growi/core';
  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 = { overwriteScopesOfDescendants };
  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. const parameters = {
  300. targetModel: SupportedTargetModel.MODEL_PAGE,
  301. target: createdPage,
  302. action: SupportedAction.ACTION_PAGE_CREATE,
  303. };
  304. activityEvent.emit('update', res.locals.activity._id, parameters);
  305. res.apiv3(result, 201);
  306. try {
  307. // global notification
  308. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  309. }
  310. catch (err) {
  311. logger.error('Create grobal notification failed', err);
  312. }
  313. // user notification
  314. if (isSlackEnabled) {
  315. try {
  316. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
  317. results.forEach((result) => {
  318. if (result.status === 'rejected') {
  319. logger.error('Create user notification failed', result.reason);
  320. }
  321. });
  322. }
  323. catch (err) {
  324. logger.error('Create user notification failed', err);
  325. }
  326. }
  327. // create subscription
  328. try {
  329. await crowi.inAppNotificationService.createSubscription(req.user.id, createdPage._id, subscribeRuleNames.PAGE_CREATE);
  330. }
  331. catch (err) {
  332. logger.error('Failed to create subscription document', err);
  333. }
  334. });
  335. /**
  336. * @swagger
  337. *
  338. * /pages/recent:
  339. * get:
  340. * tags: [Pages]
  341. * description: Get recently updated pages
  342. * responses:
  343. * 200:
  344. * description: Return pages recently updated
  345. *
  346. */
  347. router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
  348. const limit = 20;
  349. const offset = parseInt(req.query.offset) || 0;
  350. const skip = offset > 0 ? (offset - 1) * limit : offset;
  351. const queryOptions = {
  352. offset: skip,
  353. limit,
  354. includeTrashed: false,
  355. isRegExpEscapedFromPath: true,
  356. sort: 'updatedAt',
  357. desc: -1,
  358. };
  359. try {
  360. const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
  361. if (result.pages.length > limit) {
  362. result.pages.pop();
  363. }
  364. result.pages.forEach((page) => {
  365. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  366. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  367. }
  368. });
  369. const PageTagRelation = mongoose.model('PageTagRelation');
  370. const ids = result.pages.map((page) => { return page._id });
  371. const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
  372. // { pageId: [{ tag }, ...] }
  373. const relationsMap = new Map();
  374. // increment relationsMap
  375. relations.forEach((relation) => {
  376. const pageId = relation.relatedPage.toString();
  377. if (!relationsMap.has(pageId)) {
  378. relationsMap.set(pageId, []);
  379. }
  380. if (relation.relatedTag != null) {
  381. relationsMap.get(pageId).push(relation.relatedTag);
  382. }
  383. });
  384. // add tags to each page
  385. result.pages.forEach((page) => {
  386. const pageId = page._id.toString();
  387. page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
  388. });
  389. return res.apiv3(result);
  390. }
  391. catch (err) {
  392. logger.error('Failed to get recent pages', err);
  393. return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
  394. }
  395. });
  396. /**
  397. * @swagger
  398. *
  399. *
  400. * /pages/rename:
  401. * post:
  402. * tags: [Pages]
  403. * operationId: renamePage
  404. * description: Rename page
  405. * requestBody:
  406. * content:
  407. * application/json:
  408. * schema:
  409. * properties:
  410. * pageId:
  411. * $ref: '#/components/schemas/Page/properties/_id'
  412. * path:
  413. * $ref: '#/components/schemas/Page/properties/path'
  414. * revisionId:
  415. * type: string
  416. * description: revision ID
  417. * example: 5e07345972560e001761fa63
  418. * newPagePath:
  419. * type: string
  420. * description: new path
  421. * example: /user/alice/new_test
  422. * isRenameRedirect:
  423. * type: boolean
  424. * description: whether redirect page
  425. * updateMetadata:
  426. * type: boolean
  427. * description: whether update meta data
  428. * isRecursively:
  429. * type: boolean
  430. * description: whether rename page with descendants
  431. * required:
  432. * - pageId
  433. * - revisionId
  434. * responses:
  435. * 200:
  436. * description: Succeeded to rename page.
  437. * content:
  438. * application/json:
  439. * schema:
  440. * properties:
  441. * page:
  442. * $ref: '#/components/schemas/Page'
  443. * 401:
  444. * description: page id is invalid
  445. * 409:
  446. * description: page path is already existed
  447. */
  448. router.put('/rename', accessTokenParser, loginRequiredStrictly, validator.renamePage, apiV3FormValidator, async(req, res) => {
  449. const { pageId, revisionId } = req.body;
  450. let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
  451. const options = {
  452. isRecursively: req.body.isRecursively,
  453. createRedirectPage: req.body.isRenameRedirect,
  454. updateMetadata: req.body.updateMetadata,
  455. isMoveMode: req.body.isMoveMode,
  456. };
  457. const activityParameters = {
  458. ip: req.ip,
  459. endpoint: req.originalUrl,
  460. };
  461. if (!isCreatablePage(newPagePath)) {
  462. return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath}'`, 'invalid_path'), 409);
  463. }
  464. // check whether path starts slash
  465. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  466. const isExist = await Page.count({ path: newPagePath }) > 0;
  467. if (isExist) {
  468. // if page found, cannot rename to that path
  469. return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
  470. }
  471. let page;
  472. let renamedPage;
  473. try {
  474. page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  475. options.isRecursively = page.descendantCount > 0;
  476. if (page == null) {
  477. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  478. }
  479. // empty page does not require revisionId validation
  480. if (!page.isEmpty && revisionId == null) {
  481. return res.apiv3Err(new ErrorV3('revisionId must be a mongoId', 'invalid_body'), 400);
  482. }
  483. if (!page.isEmpty && !page.isUpdatable(revisionId)) {
  484. return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
  485. }
  486. renamedPage = await crowi.pageService.renamePage(page, newPagePath, req.user, options, activityParameters);
  487. // Respond before sending notification
  488. const result = { page: serializePageSecurely(renamedPage ?? page) };
  489. res.apiv3(result);
  490. }
  491. catch (err) {
  492. logger.error(err);
  493. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  494. }
  495. try {
  496. // global notification
  497. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, renamedPage, req.user, {
  498. oldPath: page.path,
  499. });
  500. }
  501. catch (err) {
  502. logger.error('Move notification failed', err);
  503. }
  504. });
  505. router.post('/resume-rename', accessTokenParser, loginRequiredStrictly, validator.resumeRenamePage, apiV3FormValidator, async(req, res) => {
  506. const { pageId } = req.body;
  507. const { user } = req;
  508. // The user has permission to resume rename operation if page is returned.
  509. const page = await Page.findByIdAndViewer(pageId, user, null, true);
  510. if (page == null) {
  511. const msg = 'The operation is forbidden for this user';
  512. const code = 'forbidden-user';
  513. return res.apiv3Err(new ErrorV3(msg, code), 403);
  514. }
  515. const pageOp = await crowi.pageOperationService.getRenameSubOperationByPageId(page._id);
  516. if (pageOp == null) {
  517. const msg = 'PageOperation document for Rename Sub operation not found.';
  518. const code = 'document_not_found';
  519. return res.apiv3Err(new ErrorV3(msg, code), 404);
  520. }
  521. try {
  522. await crowi.pageService.resumeRenameSubOperation(page, pageOp);
  523. }
  524. catch (err) {
  525. logger.error(err);
  526. return res.apiv3Err(err, 500);
  527. }
  528. return res.apiv3();
  529. });
  530. /**
  531. * @swagger
  532. *
  533. * /pages/empty-trash:
  534. * delete:
  535. * tags: [Pages]
  536. * description: empty trash
  537. * responses:
  538. * 200:
  539. * description: Succeeded to remove all trash pages
  540. */
  541. router.delete('/empty-trash', accessTokenParser, loginRequired, addActivity, apiV3FormValidator, async(req, res) => {
  542. const options = {};
  543. const pagesInTrash = await crowi.pageService.findChildrenByParentPathOrIdAndViewer('/trash', req.user);
  544. const deletablePages = crowi.pageService.filterPagesByCanDeleteCompletely(pagesInTrash, req.user, true);
  545. if (deletablePages.length === 0) {
  546. const msg = 'No pages can be deleted.';
  547. return res.apiv3Err(new ErrorV3(msg), 500);
  548. }
  549. const parameters = { action: SupportedAction.ACTION_PAGE_EMPTY_TRASH };
  550. // when some pages are not deletable
  551. if (deletablePages.length < pagesInTrash.length) {
  552. try {
  553. const options = { isCompletely: true, isRecursively: true };
  554. await crowi.pageService.deleteMultiplePages(deletablePages, req.user, options);
  555. activityEvent.emit('update', res.locals.activity._id, parameters);
  556. return res.apiv3({ deletablePages });
  557. }
  558. catch (err) {
  559. logger.error(err);
  560. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  561. }
  562. }
  563. // when all pages are deletable
  564. else {
  565. try {
  566. const activityParameters = {
  567. ip: req.ip,
  568. endpoint: req.originalUrl,
  569. };
  570. const pages = await crowi.pageService.emptyTrashPage(req.user, options, activityParameters);
  571. activityEvent.emit('update', res.locals.activity._id, parameters);
  572. return res.apiv3({ pages });
  573. }
  574. catch (err) {
  575. logger.error(err);
  576. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  577. }
  578. }
  579. });
  580. validator.displayList = [
  581. query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
  582. ];
  583. router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
  584. const { isTrashPage } = pagePathUtils;
  585. const { path } = req.query;
  586. const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
  587. const page = req.query.page || 1;
  588. const offset = (page - 1) * limit;
  589. let includeTrashed = false;
  590. if (isTrashPage(path)) {
  591. includeTrashed = true;
  592. }
  593. const queryOptions = {
  594. offset,
  595. limit,
  596. includeTrashed,
  597. };
  598. try {
  599. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  600. result.pages.forEach((page) => {
  601. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  602. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  603. }
  604. });
  605. return res.apiv3(result);
  606. }
  607. catch (err) {
  608. logger.error('Failed to get Descendants Pages', err);
  609. return res.apiv3Err(err, 500);
  610. }
  611. });
  612. /**
  613. * @swagger
  614. *
  615. *
  616. * /pages/duplicate:
  617. * post:
  618. * tags: [Pages]
  619. * operationId: duplicatePage
  620. * description: Duplicate page
  621. * requestBody:
  622. * content:
  623. * application/json:
  624. * schema:
  625. * properties:
  626. * pageId:
  627. * $ref: '#/components/schemas/Page/properties/_id'
  628. * pageNameInput:
  629. * $ref: '#/components/schemas/Page/properties/path'
  630. * isRecursively:
  631. * type: boolean
  632. * description: whether duplicate page with descendants
  633. * required:
  634. * - pageId
  635. * responses:
  636. * 200:
  637. * description: Succeeded to duplicate page.
  638. * content:
  639. * application/json:
  640. * schema:
  641. * properties:
  642. * page:
  643. * $ref: '#/components/schemas/Page'
  644. *
  645. * 403:
  646. * description: Forbidden to duplicate page.
  647. * 500:
  648. * description: Internal server error.
  649. */
  650. router.post('/duplicate', accessTokenParser, loginRequiredStrictly, addActivity, validator.duplicatePage, apiV3FormValidator, async(req, res) => {
  651. const { pageId, isRecursively } = req.body;
  652. const newPagePath = pathUtils.normalizePath(req.body.pageNameInput);
  653. const isCreatable = isCreatablePage(newPagePath);
  654. if (!isCreatable) {
  655. return res.apiv3Err(new ErrorV3('This page path is invalid', 'invalid_path'), 400);
  656. }
  657. // check page existence
  658. const isExist = (await Page.count({ path: newPagePath })) > 0;
  659. if (isExist) {
  660. return res.apiv3Err(new ErrorV3(`Page exists '${newPagePath})'`, 'already_exists'), 409);
  661. }
  662. const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  663. const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
  664. if (page == null || isEmptyAndNotRecursively) {
  665. res.code = 'Page is not found';
  666. logger.error('Failed to find the pages');
  667. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  668. }
  669. const newParentPage = await crowi.pageService.duplicate(page, newPagePath, req.user, isRecursively);
  670. const result = { page: serializePageSecurely(newParentPage) };
  671. // copy the page since it's used and updated in crowi.pageService.duplicate
  672. const copyPage = { ...page };
  673. copyPage.path = newPagePath;
  674. try {
  675. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, copyPage, req.user);
  676. }
  677. catch (err) {
  678. logger.error('Create grobal notification failed', err);
  679. }
  680. // create subscription (parent page only)
  681. try {
  682. await crowi.inAppNotificationService.createSubscription(req.user.id, newParentPage._id, subscribeRuleNames.PAGE_CREATE);
  683. }
  684. catch (err) {
  685. logger.error('Failed to create subscription document', err);
  686. }
  687. const parameters = {
  688. targetModel: SupportedTargetModel.MODEL_PAGE,
  689. target: page,
  690. action: SupportedAction.ACTION_PAGE_DUPLICATE,
  691. };
  692. activityEvent.emit('update', res.locals.activity._id, parameters, page);
  693. return res.apiv3(result);
  694. });
  695. /**
  696. * @swagger
  697. *
  698. *
  699. * /pages/subordinated-list:
  700. * get:
  701. * tags: [Pages]
  702. * operationId: subordinatedList
  703. * description: Get subordinated pages
  704. * parameters:
  705. * - name: path
  706. * in: query
  707. * description: Parent path of search
  708. * schema:
  709. * type: string
  710. * - name: limit
  711. * in: query
  712. * description: Limit of acquisitions
  713. * schema:
  714. * type: number
  715. * responses:
  716. * 200:
  717. * description: Succeeded to retrieve pages.
  718. * content:
  719. * application/json:
  720. * schema:
  721. * properties:
  722. * subordinatedPaths:
  723. * type: object
  724. * description: descendants page
  725. * 500:
  726. * description: Internal server error.
  727. */
  728. router.get('/subordinated-list', accessTokenParser, loginRequired, async(req, res) => {
  729. const { path } = req.query;
  730. const limit = parseInt(req.query.limit) || LIMIT_FOR_LIST;
  731. try {
  732. const pageData = await Page.findByPath(path, true);
  733. const result = await Page.findManageableListWithDescendants(pageData, req.user, { limit });
  734. return res.apiv3({ subordinatedPages: result });
  735. }
  736. catch (err) {
  737. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  738. }
  739. });
  740. router.post('/delete', accessTokenParser, loginRequiredStrictly, validator.deletePages, apiV3FormValidator, async(req, res) => {
  741. const { pageIdToRevisionIdMap, isCompletely, isRecursively } = req.body;
  742. const pageIds = Object.keys(pageIdToRevisionIdMap);
  743. if (pageIds.length === 0) {
  744. return res.apiv3Err(new ErrorV3('Select pages to delete.', 'no_page_selected'), 400);
  745. }
  746. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  747. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  748. }
  749. let pagesToDelete;
  750. try {
  751. pagesToDelete = await Page.findByIdsAndViewer(pageIds, req.user, null, true);
  752. }
  753. catch (err) {
  754. logger.error('Failed to find pages to delete.', err);
  755. return res.apiv3Err(new ErrorV3('Failed to find pages to delete.'));
  756. }
  757. let pagesCanBeDeleted;
  758. /*
  759. * Delete Completely
  760. */
  761. if (isCompletely) {
  762. pagesCanBeDeleted = crowi.pageService.filterPagesByCanDeleteCompletely(pagesToDelete, req.user, isRecursively);
  763. }
  764. /*
  765. * Trash
  766. */
  767. else {
  768. pagesCanBeDeleted = pagesToDelete.filter(p => p.isEmpty || p.isUpdatable(pageIdToRevisionIdMap[p._id].toString()));
  769. pagesCanBeDeleted = crowi.pageService.filterPagesByCanDelete(pagesToDelete, req.user, isRecursively);
  770. }
  771. if (pagesCanBeDeleted.length === 0) {
  772. const msg = 'No pages can be deleted.';
  773. return res.apiv3Err(new ErrorV3(msg), 500);
  774. }
  775. // run delete
  776. const options = { isCompletely, isRecursively };
  777. crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options);
  778. return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
  779. });
  780. // eslint-disable-next-line max-len
  781. router.post('/convert-pages-by-path', accessTokenParser, loginRequiredStrictly, adminRequired, validator.convertPagesByPath, apiV3FormValidator, async(req, res) => {
  782. const { convertPath } = req.body;
  783. // Convert by path
  784. const normalizedPath = pathUtils.normalizePath(convertPath);
  785. try {
  786. await crowi.pageService.normalizeParentByPath(normalizedPath, req.user);
  787. }
  788. catch (err) {
  789. logger.error(err);
  790. if (isV5ConversionError(err)) {
  791. return res.apiv3Err(new ErrorV3(err.message, err.code), 400);
  792. }
  793. return res.apiv3Err(new ErrorV3('Failed to convert pages.'), 400);
  794. }
  795. return res.apiv3({});
  796. });
  797. // eslint-disable-next-line max-len
  798. router.post('/legacy-pages-migration', accessTokenParser, loginRequired, validator.legacyPagesMigration, apiV3FormValidator, async(req, res) => {
  799. const { pageIds: _pageIds, isRecursively } = req.body;
  800. // Convert by pageIds
  801. const pageIds = _pageIds == null ? [] : _pageIds;
  802. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  803. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  804. }
  805. if (pageIds.length === 0) {
  806. return res.apiv3Err(new ErrorV3('No page is selected.'), 400);
  807. }
  808. try {
  809. if (isRecursively) {
  810. await crowi.pageService.normalizeParentByPageIdsRecursively(pageIds, req.user);
  811. }
  812. else {
  813. await crowi.pageService.normalizeParentByPageIds(pageIds, req.user);
  814. }
  815. }
  816. catch (err) {
  817. return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
  818. }
  819. return res.apiv3({});
  820. });
  821. router.get('/v5-migration-status', accessTokenParser, loginRequired, async(req, res) => {
  822. try {
  823. const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  824. const migratablePagesCount = req.user != null ? await crowi.pageService.countPagesCanNormalizeParentByUser(req.user) : null; // null check since not using loginRequiredStrictly
  825. return res.apiv3({ isV5Compatible, migratablePagesCount });
  826. }
  827. catch (err) {
  828. return res.apiv3Err(new ErrorV3('Failed to obtain migration status'));
  829. }
  830. });
  831. return router;
  832. };