pages.js 28 KB

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