pages.js 20 KB

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