pages.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. const loggerFactory = require('@alias/logger');
  2. const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
  3. const express = require('express');
  4. const pathUtils = require('growi-commons').pathUtils;
  5. const escapeStringRegexp = require('escape-string-regexp');
  6. const { body } = require('express-validator/check');
  7. const { query } = require('express-validator');
  8. const ErrorV3 = require('../../models/vo/error-apiv3');
  9. const router = express.Router();
  10. const LIMIT_FOR_LIST = 10;
  11. /**
  12. * @swagger
  13. * tags:
  14. * name: Pages
  15. */
  16. /**
  17. * @swagger
  18. *
  19. * components:
  20. * schemas:
  21. * Page:
  22. * description: Page
  23. * type: object
  24. * properties:
  25. * _id:
  26. * type: string
  27. * description: page ID
  28. * example: 5e07345972560e001761fa63
  29. * __v:
  30. * type: number
  31. * description: DB record version
  32. * example: 0
  33. * commentCount:
  34. * type: number
  35. * description: count of comments
  36. * example: 3
  37. * createdAt:
  38. * type: string
  39. * description: date created at
  40. * example: 2010-01-01T00:00:00.000Z
  41. * creator:
  42. * $ref: '#/components/schemas/User'
  43. * extended:
  44. * type: object
  45. * description: extend data
  46. * example: {}
  47. * grant:
  48. * type: number
  49. * description: grant
  50. * example: 1
  51. * grantedUsers:
  52. * type: array
  53. * description: granted users
  54. * items:
  55. * type: string
  56. * description: user ID
  57. * example: ["5ae5fccfc5577b0004dbd8ab"]
  58. * lastUpdateUser:
  59. * $ref: '#/components/schemas/User'
  60. * liker:
  61. * type: array
  62. * description: granted users
  63. * items:
  64. * type: string
  65. * description: user ID
  66. * example: []
  67. * path:
  68. * type: string
  69. * description: page path
  70. * example: /
  71. * redirectTo:
  72. * type: string
  73. * description: redirect path
  74. * example: ""
  75. * revision:
  76. * type: string
  77. * description: revision ID
  78. * example: ["5ae5fccfc5577b0004dbd8ab"]
  79. * seenUsers:
  80. * type: array
  81. * description: granted users
  82. * items:
  83. * type: string
  84. * description: user ID
  85. * example: ["5ae5fccfc5577b0004dbd8ab"]
  86. * status:
  87. * type: string
  88. * description: status
  89. * enum:
  90. * - 'wip'
  91. * - 'published'
  92. * - 'deleted'
  93. * - 'deprecated'
  94. * example: published
  95. * updatedAt:
  96. * type: string
  97. * description: date updated at
  98. * example: 2010-01-01T00:00:00.000Z
  99. */
  100. module.exports = (crowi) => {
  101. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  102. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  103. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  104. const adminRequired = require('../../middlewares/admin-required')(crowi);
  105. const csrf = require('../../middlewares/csrf')(crowi);
  106. const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
  107. const Page = crowi.model('Page');
  108. const PageTagRelation = crowi.model('PageTagRelation');
  109. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  110. const globalNotificationService = crowi.getGlobalNotificationService();
  111. const userNotificationService = crowi.getUserNotificationService();
  112. const { serializePageSecurely } = require('../../models/serializers/page-serializer');
  113. const validator = {
  114. createPage: [
  115. body('body').exists().not().isEmpty({ ignore_whitespace: true })
  116. .withMessage('body is required'),
  117. body('path').exists().not().isEmpty({ ignore_whitespace: true })
  118. .withMessage('path is required'),
  119. body('grant').if(value => value != null).isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
  120. body('overwriteScopesOfDescendants').if(value => value != null).isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
  121. body('isSlackEnabled').if(value => value != null).isBoolean().withMessage('isSlackEnabled must be boolean'),
  122. body('slackChannels').if(value => value != null).isString().withMessage('slackChannels must be string'),
  123. body('socketClientId').if(value => value != null).isInt().withMessage('socketClientId must be int'),
  124. body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
  125. ],
  126. renamePage: [
  127. body('pageId').isMongoId().withMessage('pageId is required'),
  128. body('revisionId').isMongoId().withMessage('revisionId is required'),
  129. body('newPagePath').isLength({ min: 1 }).withMessage('newPagePath is required'),
  130. body('isRenameRedirect').if(value => value != null).isBoolean().withMessage('isRenameRedirect must be boolean'),
  131. body('isRemainMetadata').if(value => value != null).isBoolean().withMessage('isRemainMetadata must be boolean'),
  132. body('isRecursively').if(value => value != null).isBoolean().withMessage('isRecursively must be boolean'),
  133. body('socketClientId').if(value => value != null).isInt().withMessage('socketClientId must be int'),
  134. ],
  135. duplicatePage: [
  136. body('pageId').isMongoId().withMessage('pageId is required'),
  137. body('pageNameInput').trim().isLength({ min: 1 }).withMessage('pageNameInput is required'),
  138. body('isRecursively').if(value => value != null).isBoolean().withMessage('isRecursively must be boolean'),
  139. ],
  140. };
  141. async function createPageAction({
  142. path, body, user, options,
  143. }) {
  144. const createdPage = Page.create(path, body, user, options);
  145. return createdPage;
  146. }
  147. async function saveTagsAction({ createdPage, pageTags }) {
  148. if (pageTags != null) {
  149. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  150. return PageTagRelation.listTagNamesByPage(createdPage.id);
  151. }
  152. return [];
  153. }
  154. /**
  155. * @swagger
  156. *
  157. * /pages/create:
  158. * post:
  159. * tags: [Pages]
  160. * operationId: createPage
  161. * description: Create page
  162. * requestBody:
  163. * content:
  164. * application/json:
  165. * schema:
  166. * properties:
  167. * body:
  168. * type: string
  169. * description: Text of page
  170. * path:
  171. * $ref: '#/components/schemas/Page/properties/path'
  172. * grant:
  173. * $ref: '#/components/schemas/Page/properties/grant'
  174. * required:
  175. * - body
  176. * - path
  177. * responses:
  178. * 201:
  179. * description: Succeeded to create page.
  180. * content:
  181. * application/json:
  182. * schema:
  183. * properties:
  184. * page:
  185. * $ref: '#/components/schemas/Page'
  186. * 409:
  187. * description: page path is already existed
  188. */
  189. router.post('/', accessTokenParser, loginRequiredStrictly, csrf, validator.createPage, apiV3FormValidator, async(req, res) => {
  190. const {
  191. body, grant, grantUserGroupId, overwriteScopesOfDescendants, isSlackEnabled, slackChannels, socketClientId, pageTags,
  192. } = req.body;
  193. let { path } = req.body;
  194. // check whether path starts slash
  195. path = pathUtils.addHeadingSlash(path);
  196. // check page existence
  197. const isExist = await Page.count({ path }) > 0;
  198. if (isExist) {
  199. return res.apiv3Err(new ErrorV3('Failed to post page', 'page_exists'), 500);
  200. }
  201. const options = { socketClientId };
  202. if (grant != null) {
  203. options.grant = grant;
  204. options.grantUserGroupId = grantUserGroupId;
  205. }
  206. const createdPage = await createPageAction({
  207. path, body, user: req.user, options,
  208. });
  209. const savedTags = await saveTagsAction({ createdPage, pageTags });
  210. const result = { page: serializePageSecurely(createdPage), tags: savedTags };
  211. // update scopes for descendants
  212. if (overwriteScopesOfDescendants) {
  213. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  214. }
  215. // global notification
  216. if (globalNotificationService != null) {
  217. try {
  218. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  219. }
  220. catch (err) {
  221. logger.error('Create grobal notification failed', err);
  222. }
  223. }
  224. // user notification
  225. if (isSlackEnabled && userNotificationService != null) {
  226. try {
  227. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create', false);
  228. results.forEach((result) => {
  229. if (result.status === 'rejected') {
  230. logger.error('Create user notification failed', result.reason);
  231. }
  232. });
  233. }
  234. catch (err) {
  235. logger.error('Create user notification failed', err);
  236. }
  237. }
  238. return res.apiv3(result, 201);
  239. });
  240. /**
  241. * @swagger
  242. *
  243. * /pages/recent:
  244. * get:
  245. * tags: [Pages]
  246. * description: Get recently updated pages
  247. * responses:
  248. * 200:
  249. * description: Return pages recently updated
  250. *
  251. */
  252. router.get('/recent', loginRequired, async(req, res) => {
  253. const limit = 20;
  254. const offset = parseInt(req.query.offset) || 0;
  255. const queryOptions = {
  256. offset,
  257. limit,
  258. includeTrashed: false,
  259. isRegExpEscapedFromPath: true,
  260. sort: 'updatedAt',
  261. desc: -1,
  262. };
  263. try {
  264. const result = await Page.findListWithDescendants('/', req.user, queryOptions);
  265. if (result.pages.length > limit) {
  266. result.pages.pop();
  267. }
  268. return res.apiv3(result);
  269. }
  270. catch (err) {
  271. logger.error('Failed to get recent pages', err);
  272. return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
  273. }
  274. });
  275. /**
  276. * @swagger
  277. *
  278. *
  279. * /pages/rename:
  280. * post:
  281. * tags: [Pages]
  282. * operationId: renamePage
  283. * description: Rename page
  284. * requestBody:
  285. * content:
  286. * application/json:
  287. * schema:
  288. * properties:
  289. * pageId:
  290. * $ref: '#/components/schemas/Page/properties/_id'
  291. * path:
  292. * $ref: '#/components/schemas/Page/properties/path'
  293. * revisionId:
  294. * type: string
  295. * description: revision ID
  296. * example: 5e07345972560e001761fa63
  297. * newPagePath:
  298. * type: string
  299. * description: new path
  300. * example: /user/alice/new_test
  301. * isRenameRedirect:
  302. * type: boolean
  303. * description: whether redirect page
  304. * isRemainMetadata:
  305. * type: boolean
  306. * description: whether remain meta data
  307. * isRecursively:
  308. * type: boolean
  309. * description: whether rename page with descendants
  310. * required:
  311. * - pageId
  312. * - revisionId
  313. * responses:
  314. * 200:
  315. * description: Succeeded to rename page.
  316. * content:
  317. * application/json:
  318. * schema:
  319. * properties:
  320. * page:
  321. * $ref: '#/components/schemas/Page'
  322. * 401:
  323. * description: page id is invalid
  324. * 409:
  325. * description: page path is already existed
  326. */
  327. router.put('/rename', accessTokenParser, loginRequiredStrictly, csrf, validator.renamePage, apiV3FormValidator, async(req, res) => {
  328. const { pageId, isRecursively, revisionId } = req.body;
  329. let newPagePath = pathUtils.normalizePath(req.body.newPagePath);
  330. const options = {
  331. createRedirectPage: req.body.isRenameRedirect,
  332. updateMetadata: !req.body.isRemainMetadata,
  333. socketClientId: +req.body.socketClientId || undefined,
  334. };
  335. if (!Page.isCreatableName(newPagePath)) {
  336. return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath})'`, 'invalid_path'), 409);
  337. }
  338. // check whether path starts slash
  339. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  340. const isExist = await Page.count({ path: newPagePath }) > 0;
  341. if (isExist) {
  342. // if page found, cannot cannot rename to that path
  343. return res.apiv3Err(new ErrorV3(`${newPagePath} already exists`, 'already_exists'), 409);
  344. }
  345. let page;
  346. try {
  347. page = await Page.findByIdAndViewer(pageId, req.user);
  348. if (page == null) {
  349. return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
  350. }
  351. if (!page.isUpdatable(revisionId)) {
  352. return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
  353. }
  354. if (isRecursively) {
  355. page = await Page.renameRecursively(page, newPagePath, req.user, options);
  356. }
  357. else {
  358. page = await Page.rename(page, newPagePath, req.user, options);
  359. }
  360. }
  361. catch (err) {
  362. logger.error(err);
  363. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  364. }
  365. const result = { page: serializePageSecurely(page) };
  366. try {
  367. // global notification
  368. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
  369. oldPath: req.body.path,
  370. });
  371. }
  372. catch (err) {
  373. logger.error('Move notification failed', err);
  374. }
  375. return res.apiv3(result);
  376. });
  377. /**
  378. * @swagger
  379. *
  380. * /pages/empty-trash:
  381. * delete:
  382. * tags: [Pages]
  383. * description: empty trash
  384. * responses:
  385. * 200:
  386. * description: Succeeded to remove all trash pages
  387. */
  388. router.delete('/empty-trash', loginRequired, adminRequired, csrf, async(req, res) => {
  389. try {
  390. const pages = await Page.completelyDeletePageRecursively({ path: '/trash' }, req.user);
  391. return res.apiv3({ pages });
  392. }
  393. catch (err) {
  394. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  395. }
  396. });
  397. validator.displayList = [
  398. query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
  399. ];
  400. router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
  401. const { isTrashPage } = require('@commons/util/path-utils');
  402. const { path } = req.query;
  403. const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
  404. const page = req.query.page || 1;
  405. const offset = (page - 1) * limit;
  406. let includeTrashed = false;
  407. if (isTrashPage(path)) {
  408. includeTrashed = true;
  409. }
  410. const queryOptions = {
  411. offset,
  412. limit,
  413. includeTrashed,
  414. };
  415. try {
  416. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  417. return res.apiv3(result);
  418. }
  419. catch (err) {
  420. logger.error('Failed to get Descendants Pages', err);
  421. return res.apiv3Err(err, 500);
  422. }
  423. });
  424. async function duplicatePage(page, newPagePath, user) {
  425. // populate
  426. await page.populate({ path: 'revision', model: 'Revision', select: 'body' }).execPopulate();
  427. // create option
  428. const options = { page };
  429. options.grant = page.grant;
  430. options.grantUserGroupId = page.grantedGroup;
  431. options.grantedUsers = page.grantedUsers;
  432. const createdPage = await createPageAction({
  433. path: newPagePath, user, body: page.revision.body, options,
  434. });
  435. const originTags = await page.findRelatedTagsById();
  436. const savedTags = await saveTagsAction({ page, createdPage, pageTags: originTags });
  437. // global notification
  438. if (globalNotificationService != null) {
  439. try {
  440. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, user);
  441. }
  442. catch (err) {
  443. logger.error('Create grobal notification failed', err);
  444. }
  445. }
  446. return { page: serializePageSecurely(createdPage), tags: savedTags };
  447. }
  448. async function duplicatePageRecursively(page, newPagePath, user) {
  449. const newPagePathPrefix = newPagePath;
  450. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  451. const pages = await Page.findManageableListWithDescendants(page, user);
  452. const promise = pages.map(async(page) => {
  453. const newPagePath = page.path.replace(pathRegExp, newPagePathPrefix);
  454. return duplicatePage(page, newPagePath, user);
  455. });
  456. return Promise.allSettled(promise);
  457. }
  458. /**
  459. * @swagger
  460. *
  461. *
  462. * /pages/duplicate:
  463. * post:
  464. * tags: [Pages]
  465. * operationId: duplicatePage
  466. * description: Duplicate page
  467. * requestBody:
  468. * content:
  469. * application/json:
  470. * schema:
  471. * properties:
  472. * pageId:
  473. * $ref: '#/components/schemas/Page/properties/_id'
  474. * pageNameInput:
  475. * $ref: '#/components/schemas/Page/properties/path'
  476. * isRecursively:
  477. * type: boolean
  478. * description: whether duplicate page with descendants
  479. * required:
  480. * - pageId
  481. * responses:
  482. * 200:
  483. * description: Succeeded to duplicate page.
  484. * content:
  485. * application/json:
  486. * schema:
  487. * properties:
  488. * page:
  489. * $ref: '#/components/schemas/Page'
  490. *
  491. * 403:
  492. * description: Forbidden to duplicate page.
  493. * 500:
  494. * description: Internal server error.
  495. */
  496. router.post('/duplicate', accessTokenParser, loginRequiredStrictly, csrf, validator.duplicatePage, apiV3FormValidator, async(req, res) => {
  497. const { pageId, isRecursively } = req.body;
  498. const newPagePath = pathUtils.normalizePath(req.body.pageNameInput);
  499. // check page existence
  500. const isExist = (await Page.count({ path: newPagePath })) > 0;
  501. if (isExist) {
  502. return res.apiv3Err(new ErrorV3(`Page exists '${newPagePath})'`, 'already_exists'), 409);
  503. }
  504. const page = await Page.findByIdAndViewer(pageId, req.user);
  505. // null check
  506. if (page == null) {
  507. res.code = 'Page is not found';
  508. logger.error('Failed to find the pages');
  509. return res.apiv3Err(new ErrorV3('Not Founded the page', 'notfound_or_forbidden'), 404);
  510. }
  511. let result;
  512. if (isRecursively) {
  513. result = await duplicatePageRecursively(page, newPagePath, req.user);
  514. }
  515. else {
  516. result = await duplicatePage(page, newPagePath, req.user);
  517. }
  518. return res.apiv3({ result });
  519. });
  520. /**
  521. * @swagger
  522. *
  523. *
  524. * /pages/subordinated-list:
  525. * get:
  526. * tags: [Pages]
  527. * operationId: subordinatedList
  528. * description: Get subordinated pages
  529. * parameters:
  530. * - name: path
  531. * in: query
  532. * description: Parent path of search
  533. * schema:
  534. * type: string
  535. * - name: limit
  536. * in: query
  537. * description: Limit of acquisitions
  538. * schema:
  539. * type: number
  540. * responses:
  541. * 200:
  542. * description: Succeeded to retrieve pages.
  543. * content:
  544. * application/json:
  545. * schema:
  546. * properties:
  547. * subordinatedPaths:
  548. * type: object
  549. * description: descendants page
  550. * 500:
  551. * description: Internal server error.
  552. */
  553. router.get('/subordinated-list', accessTokenParser, loginRequired, async(req, res) => {
  554. const { path } = req.query;
  555. const limit = parseInt(req.query.limit) || LIMIT_FOR_LIST;
  556. try {
  557. const pageData = await Page.findByPath(path);
  558. const result = await Page.findManageableListWithDescendants(pageData, req.user, { limit });
  559. return res.apiv3({ subordinatedPaths: result });
  560. }
  561. catch (err) {
  562. return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
  563. }
  564. });
  565. return router;
  566. };