pages.js 36 KB

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