pages.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  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').isBoolean().withMessage('isRecursively is required'),
  185. ],
  186. };
  187. async function createPageAction({
  188. path, body, user, options,
  189. }) {
  190. const createdPage = await Page.create(path, body, user, options);
  191. return createdPage;
  192. }
  193. async function saveTagsAction({ createdPage, pageTags }) {
  194. if (pageTags != null) {
  195. const tagEvent = crowi.event('tag');
  196. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  197. tagEvent.emit('update', createdPage, pageTags);
  198. return PageTagRelation.listTagNamesByPage(createdPage.id);
  199. }
  200. return [];
  201. }
  202. /**
  203. * @swagger
  204. *
  205. * /pages:
  206. * post:
  207. * tags: [Pages]
  208. * operationId: createPage
  209. * description: Create page
  210. * requestBody:
  211. * content:
  212. * application/json:
  213. * schema:
  214. * properties:
  215. * body:
  216. * type: string
  217. * description: Text of page
  218. * path:
  219. * $ref: '#/components/schemas/Page/properties/path'
  220. * grant:
  221. * $ref: '#/components/schemas/Page/properties/grant'
  222. * grantUserGroupId:
  223. * type: string
  224. * description: UserGroup ID
  225. * example: 5ae5fccfc5577b0004dbd8ab
  226. * pageTags:
  227. * type: array
  228. * items:
  229. * $ref: '#/components/schemas/Tag'
  230. * createFromPageTree:
  231. * type: boolean
  232. * description: Whether the page was created from the page tree or not
  233. * required:
  234. * - body
  235. * - path
  236. * responses:
  237. * 201:
  238. * description: Succeeded to create page.
  239. * content:
  240. * application/json:
  241. * schema:
  242. * properties:
  243. * data:
  244. * type: object
  245. * properties:
  246. * page:
  247. * $ref: '#/components/schemas/Page'
  248. * tags:
  249. * type: array
  250. * items:
  251. * $ref: '#/components/schemas/Tags'
  252. * revision:
  253. * $ref: '#/components/schemas/Revision'
  254. * 409:
  255. * description: page path is already existed
  256. */
  257. router.post('/', accessTokenParser, loginRequiredStrictly, csrf, validator.createPage, apiV3FormValidator, async(req, res) => {
  258. const {
  259. body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, pageTags,
  260. } = req.body;
  261. let { path } = req.body;
  262. // check whether path starts slash
  263. path = pathUtils.addHeadingSlash(path);
  264. const options = {};
  265. if (grant != null) {
  266. options.grant = grant;
  267. options.grantUserGroupId = grantUserGroupId;
  268. }
  269. let createdPage;
  270. try {
  271. createdPage = await createPageAction({
  272. path, body, user: req.user, options,
  273. });
  274. }
  275. catch (err) {
  276. logger.error('Error occurred while creating a page.', err);
  277. return res.apiv3Err(err);
  278. }
  279. const savedTags = await saveTagsAction({ createdPage, pageTags });
  280. const result = {
  281. page: serializePageSecurely(createdPage),
  282. tags: savedTags,
  283. revision: serializeRevisionSecurely(createdPage.revision),
  284. };
  285. // update scopes for descendants
  286. if (overwriteScopesOfDescendants) {
  287. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  288. }
  289. res.apiv3(result, 201);
  290. try {
  291. // global notification
  292. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  293. }
  294. catch (err) {
  295. logger.error('Create grobal notification failed', err);
  296. }
  297. // user notification
  298. if (isSlackEnabled) {
  299. try {
  300. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
  301. results.forEach((result) => {
  302. if (result.status === 'rejected') {
  303. logger.error('Create user notification failed', result.reason);
  304. }
  305. });
  306. }
  307. catch (err) {
  308. logger.error('Create user notification failed', err);
  309. }
  310. }
  311. // create subscription
  312. try {
  313. await crowi.inAppNotificationService.createSubscription(req.user.id, createdPage._id, subscribeRuleNames.PAGE_CREATE);
  314. }
  315. catch (err) {
  316. logger.error('Failed to create subscription document', err);
  317. }
  318. });
  319. /**
  320. * @swagger
  321. *
  322. * /pages/recent:
  323. * get:
  324. * tags: [Pages]
  325. * description: Get recently updated pages
  326. * responses:
  327. * 200:
  328. * description: Return pages recently updated
  329. *
  330. */
  331. router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
  332. const limit = 20;
  333. const offset = parseInt(req.query.offset) || 0;
  334. const queryOptions = {
  335. offset,
  336. limit,
  337. includeTrashed: false,
  338. isRegExpEscapedFromPath: true,
  339. sort: 'updatedAt',
  340. desc: -1,
  341. };
  342. try {
  343. const result = await Page.findListWithDescendants('/', req.user, queryOptions);
  344. if (result.pages.length > limit) {
  345. result.pages.pop();
  346. }
  347. result.pages.forEach((page) => {
  348. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  349. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  350. }
  351. });
  352. const PageTagRelation = mongoose.model('PageTagRelation');
  353. const ids = result.pages.map((page) => { return page._id });
  354. const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
  355. // { pageId: [{ tag }, ...] }
  356. const relationsMap = new Map();
  357. // increment relationsMap
  358. relations.forEach((relation) => {
  359. const pageId = relation.relatedPage.toString();
  360. if (!relationsMap.has(pageId)) {
  361. relationsMap.set(pageId, []);
  362. }
  363. if (relation.relatedTag != null) {
  364. relationsMap.get(pageId).push(relation.relatedTag);
  365. }
  366. });
  367. // add tags to each page
  368. result.pages.forEach((page) => {
  369. const pageId = page._id.toString();
  370. page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
  371. });
  372. return res.apiv3(result);
  373. }
  374. catch (err) {
  375. logger.error('Failed to get recent pages', err);
  376. return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
  377. }
  378. });
  379. /**
  380. * @swagger
  381. *
  382. *
  383. * /pages/rename:
  384. * post:
  385. * tags: [Pages]
  386. * operationId: renamePage
  387. * description: Rename page
  388. * requestBody:
  389. * content:
  390. * application/json:
  391. * schema:
  392. * properties:
  393. * pageId:
  394. * $ref: '#/components/schemas/Page/properties/_id'
  395. * path:
  396. * $ref: '#/components/schemas/Page/properties/path'
  397. * revisionId:
  398. * type: string
  399. * description: revision ID
  400. * example: 5e07345972560e001761fa63
  401. * newPagePath:
  402. * type: string
  403. * description: new path
  404. * example: /user/alice/new_test
  405. * isRenameRedirect:
  406. * type: boolean
  407. * description: whether redirect page
  408. * updateMetadata:
  409. * type: boolean
  410. * description: whether update meta data
  411. * isRecursively:
  412. * type: boolean
  413. * description: whether rename page with descendants
  414. * required:
  415. * - pageId
  416. * - revisionId
  417. * responses:
  418. * 200:
  419. * description: Succeeded to rename page.
  420. * content:
  421. * application/json:
  422. * schema:
  423. * properties:
  424. * page:
  425. * $ref: '#/components/schemas/Page'
  426. * 401:
  427. * description: page id is invalid
  428. * 409:
  429. * description: page path is already existed
  430. */
  431. router.put('/rename', accessTokenParser, loginRequiredStrictly, csrf, validator.renamePage, apiV3FormValidator, async(req, res) => {
  432. const { pageId, revisionId } = req.body;
  433. let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
  434. const options = {
  435. isRecursively: req.body.isRecursively,
  436. createRedirectPage: req.body.isRenameRedirect,
  437. updateMetadata: req.body.updateMetadata,
  438. isMoveMode: req.body.isMoveMode,
  439. };
  440. if (!isCreatablePage(newPagePath)) {
  441. return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath}'`, 'invalid_path'), 409);
  442. }
  443. // check whether path starts slash
  444. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  445. const isExist = await Page.count({ path: newPagePath }) > 0;
  446. if (isExist) {
  447. // if page found, cannot rename to that path
  448. return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
  449. }
  450. let page;
  451. let renamedPage;
  452. try {
  453. page = await Page.findByIdAndViewerToEdit(pageId, req.user, true);
  454. if (page == null) {
  455. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  456. }
  457. // empty page does not require revisionId validation
  458. if (!page.isEmpty && revisionId == null) {
  459. return res.apiv3Err(new ErrorV3('revisionId must be a mongoId', 'invalid_body'), 400);
  460. }
  461. if (!page.isEmpty && !page.isUpdatable(revisionId)) {
  462. return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
  463. }
  464. renamedPage = await crowi.pageService.renamePage(page, newPagePath, req.user, options);
  465. }
  466. catch (err) {
  467. logger.error(err);
  468. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  469. }
  470. const result = { page: serializePageSecurely(renamedPage ?? page) };
  471. try {
  472. // global notification
  473. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
  474. oldPath: req.body.path,
  475. });
  476. }
  477. catch (err) {
  478. logger.error('Move notification failed', err);
  479. }
  480. return res.apiv3(result);
  481. });
  482. /**
  483. * @swagger
  484. *
  485. * /pages/empty-trash:
  486. * delete:
  487. * tags: [Pages]
  488. * description: empty trash
  489. * responses:
  490. * 200:
  491. * description: Succeeded to remove all trash pages
  492. */
  493. router.delete('/empty-trash', accessTokenParser, loginRequired, adminRequired, csrf, apiV3FormValidator, async(req, res) => {
  494. const options = {};
  495. try {
  496. const pages = await crowi.pageService.emptyTrashPage(req.user, options);
  497. return res.apiv3({ pages });
  498. }
  499. catch (err) {
  500. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  501. }
  502. });
  503. validator.displayList = [
  504. query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
  505. ];
  506. router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
  507. const { isTrashPage } = pagePathUtils;
  508. const { path } = req.query;
  509. const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
  510. const page = req.query.page || 1;
  511. const offset = (page - 1) * limit;
  512. let includeTrashed = false;
  513. if (isTrashPage(path)) {
  514. includeTrashed = true;
  515. }
  516. const queryOptions = {
  517. offset,
  518. limit,
  519. includeTrashed,
  520. };
  521. try {
  522. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  523. result.pages.forEach((page) => {
  524. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  525. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  526. }
  527. });
  528. return res.apiv3(result);
  529. }
  530. catch (err) {
  531. logger.error('Failed to get Descendants Pages', err);
  532. return res.apiv3Err(err, 500);
  533. }
  534. });
  535. /**
  536. * @swagger
  537. *
  538. *
  539. * /pages/duplicate:
  540. * post:
  541. * tags: [Pages]
  542. * operationId: duplicatePage
  543. * description: Duplicate page
  544. * requestBody:
  545. * content:
  546. * application/json:
  547. * schema:
  548. * properties:
  549. * pageId:
  550. * $ref: '#/components/schemas/Page/properties/_id'
  551. * pageNameInput:
  552. * $ref: '#/components/schemas/Page/properties/path'
  553. * isRecursively:
  554. * type: boolean
  555. * description: whether duplicate page with descendants
  556. * required:
  557. * - pageId
  558. * responses:
  559. * 200:
  560. * description: Succeeded to duplicate page.
  561. * content:
  562. * application/json:
  563. * schema:
  564. * properties:
  565. * page:
  566. * $ref: '#/components/schemas/Page'
  567. *
  568. * 403:
  569. * description: Forbidden to duplicate page.
  570. * 500:
  571. * description: Internal server error.
  572. */
  573. router.post('/duplicate', accessTokenParser, loginRequiredStrictly, csrf, validator.duplicatePage, apiV3FormValidator, async(req, res) => {
  574. const { pageId, isRecursively } = req.body;
  575. const newPagePath = pathUtils.normalizePath(req.body.pageNameInput);
  576. const isCreatable = isCreatablePage(newPagePath);
  577. if (!isCreatable) {
  578. return res.apiv3Err(new ErrorV3('This page path is invalid', 'invalid_path'), 400);
  579. }
  580. // check page existence
  581. const isExist = (await Page.count({ path: newPagePath })) > 0;
  582. if (isExist) {
  583. return res.apiv3Err(new ErrorV3(`Page exists '${newPagePath})'`, 'already_exists'), 409);
  584. }
  585. const page = await Page.findByIdAndViewerToEdit(pageId, req.user, true);
  586. const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
  587. if (page == null || isEmptyAndNotRecursively) {
  588. res.code = 'Page is not found';
  589. logger.error('Failed to find the pages');
  590. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  591. }
  592. const newParentPage = await crowi.pageService.duplicate(page, newPagePath, req.user, isRecursively);
  593. const result = { page: serializePageSecurely(newParentPage) };
  594. // copy the page since it's used and updated in crowi.pageService.duplicate
  595. const copyPage = { ...page };
  596. copyPage.path = newPagePath;
  597. try {
  598. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, copyPage, req.user);
  599. }
  600. catch (err) {
  601. logger.error('Create grobal notification failed', err);
  602. }
  603. // create subscription (parent page only)
  604. try {
  605. await crowi.inAppNotificationService.createSubscription(req.user.id, newParentPage._id, subscribeRuleNames.PAGE_CREATE);
  606. }
  607. catch (err) {
  608. logger.error('Failed to create subscription document', err);
  609. }
  610. return res.apiv3(result);
  611. });
  612. /**
  613. * @swagger
  614. *
  615. *
  616. * /pages/subordinated-list:
  617. * get:
  618. * tags: [Pages]
  619. * operationId: subordinatedList
  620. * description: Get subordinated pages
  621. * parameters:
  622. * - name: path
  623. * in: query
  624. * description: Parent path of search
  625. * schema:
  626. * type: string
  627. * - name: limit
  628. * in: query
  629. * description: Limit of acquisitions
  630. * schema:
  631. * type: number
  632. * responses:
  633. * 200:
  634. * description: Succeeded to retrieve pages.
  635. * content:
  636. * application/json:
  637. * schema:
  638. * properties:
  639. * subordinatedPaths:
  640. * type: object
  641. * description: descendants page
  642. * 500:
  643. * description: Internal server error.
  644. */
  645. router.get('/subordinated-list', accessTokenParser, loginRequired, async(req, res) => {
  646. const { path } = req.query;
  647. const limit = parseInt(req.query.limit) || LIMIT_FOR_LIST;
  648. try {
  649. const pageData = await Page.findByPath(path, true);
  650. const result = await Page.findManageableListWithDescendants(pageData, req.user, { limit });
  651. return res.apiv3({ subordinatedPages: result });
  652. }
  653. catch (err) {
  654. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  655. }
  656. });
  657. router.post('/delete', accessTokenParser, loginRequiredStrictly, csrf, validator.deletePages, apiV3FormValidator, async(req, res) => {
  658. const { pageIdToRevisionIdMap, isCompletely, isRecursively } = req.body;
  659. const pageIds = Object.keys(pageIdToRevisionIdMap);
  660. if (pageIds.length === 0) {
  661. return res.apiv3Err(new ErrorV3('Select pages to delete.', 'no_page_selected'), 400);
  662. }
  663. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  664. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  665. }
  666. let pagesToDelete;
  667. try {
  668. pagesToDelete = await Page.findByPageIdsToEdit(pageIds, req.user, true);
  669. }
  670. catch (err) {
  671. logger.error('Failed to find pages to delete.', err);
  672. return res.apiv3Err(new ErrorV3('Failed to find pages to delete.'));
  673. }
  674. let pagesCanBeDeleted;
  675. /*
  676. * Delete Completely
  677. */
  678. if (isCompletely) {
  679. pagesCanBeDeleted = crowi.pageService.filterPagesByCanDeleteCompletely(pagesToDelete, req.user);
  680. }
  681. /*
  682. * Trash
  683. */
  684. else {
  685. pagesCanBeDeleted = pagesToDelete.filter(p => p.isEmpty || p.isUpdatable(pageIdToRevisionIdMap[p._id].toString()));
  686. }
  687. if (pagesCanBeDeleted.length === 0) {
  688. const msg = 'No pages can be deleted.';
  689. return res.apiv3Err(new ErrorV3(msg), 500);
  690. }
  691. // run delete
  692. const options = { isCompletely, isRecursively };
  693. crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options);
  694. return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
  695. });
  696. router.post('/v5-schema-migration', accessTokenParser, loginRequired, adminRequired, csrf, async(req, res) => {
  697. const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  698. try {
  699. if (!isV5Compatible) {
  700. // this method throws and emit socketIo event when error occurs
  701. crowi.pageService.normalizeAllPublicPages(); // not await
  702. }
  703. }
  704. catch (err) {
  705. return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
  706. }
  707. return res.apiv3({ isV5Compatible });
  708. });
  709. // eslint-disable-next-line max-len
  710. router.post('/legacy-pages-migration', accessTokenParser, loginRequired, csrf, validator.legacyPagesMigration, apiV3FormValidator, async(req, res) => {
  711. const { pageIds: _pageIds, isRecursively } = req.body;
  712. const pageIds = _pageIds == null ? [] : _pageIds;
  713. if (pageIds.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  714. return res.apiv3Err(new ErrorV3(`The maximum number of pages you can select is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`, 'exceeded_maximum_number'), 400);
  715. }
  716. try {
  717. await crowi.pageService.normalizeParentByPageIds(pageIds, req.user, isRecursively);
  718. }
  719. catch (err) {
  720. return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
  721. }
  722. return res.apiv3({});
  723. });
  724. router.get('/v5-migration-status', accessTokenParser, loginRequired, async(req, res) => {
  725. try {
  726. const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  727. const migratablePagesCount = req.user != null ? await crowi.pageService.countPagesCanNormalizeParentByUser(req.user) : null; // null check since not using loginRequiredStrictly
  728. return res.apiv3({ isV5Compatible, migratablePagesCount });
  729. }
  730. catch (err) {
  731. return res.apiv3Err(new ErrorV3('Failed to obtain migration status'));
  732. }
  733. });
  734. return router;
  735. };