page.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import { body } from 'express-validator';
  2. import mongoose from 'mongoose';
  3. import loggerFactory from '~/utils/logger';
  4. import { PathAlreadyExistsError } from '../models/errors';
  5. import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
  6. import PageTagRelation from '../models/page-tag-relation';
  7. import UpdatePost from '../models/update-post';
  8. /**
  9. * @swagger
  10. * tags:
  11. * name: Pages
  12. */
  13. /* eslint-disable no-use-before-define */
  14. /** @param {import('~/server/crowi').default} crowi Crowi instance */
  15. module.exports = (crowi, app) => {
  16. const logger = loggerFactory('growi:routes:page');
  17. const { pagePathUtils } = require('@growi/core/dist/utils');
  18. /** @type {import('../models/page').PageModel} */
  19. const Page = crowi.model('Page');
  20. const PageRedirect = mongoose.model('PageRedirect');
  21. const ApiResponse = require('../util/apiResponse');
  22. const globalNotificationService = crowi.getGlobalNotificationService();
  23. const actions = {};
  24. // async function showPageForPresentation(req, res, next) {
  25. // const id = req.params.id;
  26. // const { revisionId } = req.query;
  27. // let page = await Page.findByIdAndViewer(id, req.user, null, true, true);
  28. // if (page == null) {
  29. // next();
  30. // }
  31. // // empty page
  32. // if (page.isEmpty) {
  33. // // redirect to page (path) url
  34. // const url = new URL('https://dummy.origin');
  35. // url.pathname = page.path;
  36. // Object.entries(req.query).forEach(([key, value], i) => {
  37. // url.searchParams.append(key, value);
  38. // });
  39. // return res.safeRedirect(urljoin(url.pathname, url.search));
  40. // }
  41. // const renderVars = {};
  42. // // populate
  43. // page = await page.populateDataToMakePresentation(revisionId);
  44. // if (page != null) {
  45. // addRenderVarsForPresentation(renderVars, page);
  46. // }
  47. // return res.render('page_presentation', renderVars);
  48. // }
  49. /**
  50. * switch action
  51. * - presentation mode
  52. * - by behaviorType
  53. */
  54. // actions.showPage = async function(req, res, next) {
  55. // // presentation mode
  56. // if (req.query.presentation) {
  57. // return showPageForPresentation(req, res, next);
  58. // }
  59. // // delegate to showPageForGrowiBehavior
  60. // return showPageForGrowiBehavior(req, res, next);
  61. // };
  62. const api = {};
  63. const validator = {};
  64. actions.api = api;
  65. actions.validator = validator; /**
  66. * @swagger
  67. *
  68. * components:
  69. * schemas:
  70. * PageTagsData:
  71. * type: object
  72. * properties:
  73. * tags:
  74. * type: array
  75. * items:
  76. * type: string
  77. * description: Array of tag names associated with the page
  78. * example: ["javascript", "tutorial", "backend"]
  79. *
  80. * responses:
  81. * PageTagsSuccess:
  82. * description: Successfully retrieved page tags
  83. * content:
  84. * application/json:
  85. * schema:
  86. * allOf:
  87. * - $ref: '#/components/schemas/ApiResponseSuccess'
  88. * - $ref: '#/components/schemas/PageTagsData'
  89. *
  90. * /pages.getPageTag:
  91. * get:
  92. * tags: [Pages]
  93. * operationId: getPageTag
  94. * summary: Get page tags
  95. * description: Retrieve all tags associated with a specific page
  96. * parameters:
  97. * - in: query
  98. * name: pageId
  99. * required: true
  100. * description: Unique identifier of the page
  101. * schema:
  102. * type: string
  103. * format: ObjectId
  104. * example: "507f1f77bcf86cd799439011"
  105. * responses:
  106. * 200:
  107. * $ref: '#/components/responses/PageTagsSuccess'
  108. * 400:
  109. * $ref: '#/components/responses/BadRequest'
  110. * 403:
  111. * $ref: '#/components/responses/Forbidden'
  112. * 404:
  113. * $ref: '#/components/responses/NotFound'
  114. * 500:
  115. * $ref: '#/components/responses/InternalServerError'
  116. */
  117. /**
  118. * @api {get} /pages.getPageTag get page tags
  119. * @apiName GetPageTag
  120. * @apiGroup Page
  121. *
  122. * @apiParam {String} pageId
  123. */
  124. api.getPageTag = async (req, res) => {
  125. const result = {};
  126. try {
  127. result.tags = await PageTagRelation.listTagNamesByPage(req.query.pageId);
  128. } catch (err) {
  129. return res.json(ApiResponse.error(err));
  130. }
  131. return res.json(ApiResponse.success(result));
  132. };
  133. /**
  134. * @swagger
  135. *
  136. * components:
  137. * schemas:
  138. * UpdatePostData:
  139. * type: object
  140. * properties:
  141. * updatePost:
  142. * type: array
  143. * items:
  144. * type: string
  145. * description: Array of channel names for notifications
  146. * example: ["general", "development", "notifications"]
  147. *
  148. * responses:
  149. * UpdatePostSuccess:
  150. * description: Successfully retrieved UpdatePost settings
  151. * content:
  152. * application/json:
  153. * schema:
  154. * allOf:
  155. * - $ref: '#/components/schemas/ApiResponseSuccess'
  156. * - $ref: '#/components/schemas/UpdatePostData'
  157. *
  158. * /pages.updatePost:
  159. * get:
  160. * tags: [Pages]
  161. * operationId: getUpdatePost
  162. * summary: Get UpdatePost settings
  163. * description: Retrieve UpdatePost notification settings for a specific path
  164. * parameters:
  165. * - in: query
  166. * name: path
  167. * required: true
  168. * description: Page path to get UpdatePost settings for
  169. * schema:
  170. * type: string
  171. * example: "/user/example"
  172. * examples:
  173. * userPage:
  174. * value: "/user/john"
  175. * description: User page path
  176. * projectPage:
  177. * value: "/project/myproject"
  178. * description: Project page path
  179. * responses:
  180. * 200:
  181. * $ref: '#/components/responses/UpdatePostSuccess'
  182. * 400:
  183. * $ref: '#/components/responses/BadRequest'
  184. * 403:
  185. * $ref: '#/components/responses/Forbidden'
  186. * 500:
  187. * $ref: '#/components/responses/InternalServerError'
  188. */
  189. /**
  190. * @api {get} /pages.updatePost
  191. * @apiName Get UpdatePost setting list
  192. * @apiGroup Page
  193. *
  194. * @apiParam {String} path
  195. */
  196. api.getUpdatePost = (req, res) => {
  197. const path = req.query.path;
  198. if (!path) {
  199. return res.json(ApiResponse.error({}));
  200. }
  201. UpdatePost.findSettingsByPath(path)
  202. .then((data) => {
  203. // eslint-disable-next-line no-param-reassign
  204. data = data.map((e) => {
  205. return e.channel;
  206. });
  207. logger.debug('Found updatePost data', data);
  208. const result = { updatePost: data };
  209. return res.json(ApiResponse.success(result));
  210. })
  211. .catch((err) => {
  212. logger.debug('Error occured while get setting', err);
  213. return res.json(ApiResponse.error({}));
  214. });
  215. };
  216. validator.remove = [
  217. body('completely')
  218. .custom((v) => v === 'true' || v === true || v == null)
  219. .withMessage(
  220. 'The body property "completely" must be "true" or true. (Omit param for false)',
  221. ),
  222. body('recursively')
  223. .custom((v) => v === 'true' || v === true || v == null)
  224. .withMessage(
  225. 'The body property "recursively" must be "true" or true. (Omit param for false)',
  226. ),
  227. ];
  228. /**
  229. * @swagger
  230. *
  231. * components:
  232. * schemas:
  233. * PageRemoveData:
  234. * type: object
  235. * required:
  236. * - path
  237. * properties:
  238. * path:
  239. * type: string
  240. * description: Path of the deleted page
  241. * example: "/user/example"
  242. * isRecursively:
  243. * type: boolean
  244. * description: Whether deletion was recursive
  245. * example: true
  246. * isCompletely:
  247. * type: boolean
  248. * description: Whether deletion was complete
  249. * example: false
  250. *
  251. * responses:
  252. * PageRemoveSuccess:
  253. * description: Page successfully deleted
  254. * content:
  255. * application/json:
  256. * schema:
  257. * allOf:
  258. * - $ref: '#/components/schemas/ApiResponseSuccess'
  259. * - $ref: '#/components/schemas/PageRemoveData'
  260. *
  261. * /pages.remove:
  262. * post:
  263. * tags: [Pages]
  264. * operationId: removePage
  265. * summary: Remove page
  266. * description: Delete a page either softly or completely, with optional recursive deletion
  267. * requestBody:
  268. * required: true
  269. * content:
  270. * application/json:
  271. * schema:
  272. * type: object
  273. * required:
  274. * - page_id
  275. * properties:
  276. * page_id:
  277. * type: string
  278. * format: ObjectId
  279. * description: Unique identifier of the page to delete
  280. * example: "507f1f77bcf86cd799439011"
  281. * revision_id:
  282. * type: string
  283. * format: ObjectId
  284. * description: Revision ID for conflict detection
  285. * example: "507f1f77bcf86cd799439012"
  286. * completely:
  287. * type: boolean
  288. * description: Whether to delete the page completely (true) or soft delete (false)
  289. * default: false
  290. * example: false
  291. * recursively:
  292. * type: boolean
  293. * description: Whether to delete child pages recursively
  294. * default: false
  295. * example: true
  296. * examples:
  297. * softDelete:
  298. * summary: Soft delete single page
  299. * value:
  300. * page_id: "507f1f77bcf86cd799439011"
  301. * revision_id: "507f1f77bcf86cd799439012"
  302. * recursiveDelete:
  303. * summary: Recursive soft delete
  304. * value:
  305. * page_id: "507f1f77bcf86cd799439011"
  306. * recursively: true
  307. * completeDelete:
  308. * summary: Complete deletion
  309. * value:
  310. * page_id: "507f1f77bcf86cd799439011"
  311. * completely: true
  312. * responses:
  313. * 200:
  314. * $ref: '#/components/responses/PageRemoveSuccess'
  315. * 400:
  316. * $ref: '#/components/responses/BadRequest'
  317. * 403:
  318. * $ref: '#/components/responses/Forbidden'
  319. * 404:
  320. * $ref: '#/components/responses/NotFound'
  321. * 409:
  322. * $ref: '#/components/responses/Conflict'
  323. * 500:
  324. * $ref: '#/components/responses/InternalServerError'
  325. */
  326. api.remove = async (req, res) => {
  327. const pageId = req.body.page_id;
  328. const previousRevision = req.body.revision_id || null;
  329. const { recursively: isRecursively, completely: isCompletely } = req.body;
  330. const options = {};
  331. const activityParameters = {
  332. ip: req.ip,
  333. endpoint: req.originalUrl,
  334. };
  335. /** @type {import('../models/page').PageDocument | undefined} */
  336. const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  337. if (page == null) {
  338. return res.json(
  339. ApiResponse.error(
  340. `Page '${pageId}' is not found or forbidden`,
  341. 'notfound_or_forbidden',
  342. ),
  343. );
  344. }
  345. if (page.isEmpty && !isRecursively) {
  346. return res.json(
  347. ApiResponse.error(
  348. 'Empty pages cannot be single deleted',
  349. 'single_deletion_empty_pages',
  350. ),
  351. );
  352. }
  353. const creatorId = await crowi.pageService.getCreatorIdForCanDelete(page);
  354. logger.debug('Delete page', page._id, page.path);
  355. try {
  356. if (isCompletely) {
  357. const userRelatedGroups =
  358. await crowi.pageGrantService.getUserRelatedGroups(req.user);
  359. const canDeleteCompletely = crowi.pageService.canDeleteCompletely(
  360. page,
  361. creatorId,
  362. req.user,
  363. isRecursively,
  364. userRelatedGroups,
  365. );
  366. if (!canDeleteCompletely) {
  367. return res.json(
  368. ApiResponse.error(
  369. 'You cannot delete this page completely',
  370. 'complete_deletion_not_allowed_for_user',
  371. ),
  372. );
  373. }
  374. if (pagePathUtils.isUsersHomepage(page.path)) {
  375. if (!crowi.pageService.canDeleteUserHomepageByConfig()) {
  376. return res.json(
  377. ApiResponse.error('Could not delete user homepage'),
  378. );
  379. }
  380. if (
  381. !(await crowi.pageService.isUsersHomepageOwnerAbsent(page.path))
  382. ) {
  383. return res.json(
  384. ApiResponse.error('Could not delete user homepage'),
  385. );
  386. }
  387. }
  388. await crowi.pageService.deleteCompletely(
  389. page,
  390. req.user,
  391. options,
  392. isRecursively,
  393. false,
  394. activityParameters,
  395. );
  396. } else {
  397. // behave like not found
  398. const notRecursivelyAndEmpty = page.isEmpty && !isRecursively;
  399. if (notRecursivelyAndEmpty) {
  400. return res.json(
  401. ApiResponse.error(`Page '${pageId}' is not found.`, 'notfound'),
  402. );
  403. }
  404. if (!page.isEmpty && !page.isUpdatable(previousRevision)) {
  405. return res.json(
  406. ApiResponse.error(
  407. "Someone could update this page, so couldn't delete.",
  408. 'outdated',
  409. ),
  410. );
  411. }
  412. if (
  413. !crowi.pageService.canDelete(page, creatorId, req.user, isRecursively)
  414. ) {
  415. return res.json(
  416. ApiResponse.error('You cannot delete this page', 'user_not_admin'),
  417. );
  418. }
  419. if (pagePathUtils.isUsersHomepage(page.path)) {
  420. if (!crowi.pageService.canDeleteUserHomepageByConfig()) {
  421. return res.json(
  422. ApiResponse.error('Could not delete user homepage'),
  423. );
  424. }
  425. if (
  426. !(await crowi.pageService.isUsersHomepageOwnerAbsent(page.path))
  427. ) {
  428. return res.json(
  429. ApiResponse.error('Could not delete user homepage'),
  430. );
  431. }
  432. }
  433. await crowi.pageService.deletePage(
  434. page,
  435. req.user,
  436. options,
  437. isRecursively,
  438. activityParameters,
  439. );
  440. }
  441. } catch (err) {
  442. logger.error('Error occured while get setting', err);
  443. return res.json(ApiResponse.error('Failed to delete page.', err.message));
  444. }
  445. logger.debug('Page deleted', page.path);
  446. const result = {};
  447. result.path = page.path;
  448. result.isRecursively = isRecursively;
  449. result.isCompletely = isCompletely;
  450. res.json(ApiResponse.success(result));
  451. try {
  452. // global notification
  453. await globalNotificationService.fire(
  454. GlobalNotificationSettingEvent.PAGE_DELETE,
  455. page,
  456. req.user,
  457. );
  458. } catch (err) {
  459. logger.error('Delete notification failed', err);
  460. }
  461. };
  462. validator.revertRemove = [
  463. body('recursively')
  464. .optional()
  465. .custom((v) => v === 'true' || v === true || v == null)
  466. .withMessage(
  467. 'The body property "recursively" must be "true" or true. (Omit param for false)',
  468. ),
  469. ];
  470. /**
  471. * @swagger
  472. *
  473. * components:
  474. * schemas:
  475. * PageRevertData:
  476. * type: object
  477. * properties:
  478. * page:
  479. * type: object
  480. * description: Restored page object
  481. * properties:
  482. * _id:
  483. * type: string
  484. * format: ObjectId
  485. * example: "507f1f77bcf86cd799439011"
  486. * path:
  487. * type: string
  488. * example: "/user/example"
  489. * title:
  490. * type: string
  491. * example: "Example Page"
  492. * status:
  493. * type: string
  494. * example: "published"
  495. *
  496. * responses:
  497. * PageRevertSuccess:
  498. * description: Page successfully restored
  499. * content:
  500. * application/json:
  501. * schema:
  502. * allOf:
  503. * - $ref: '#/components/schemas/ApiResponseSuccess'
  504. * - $ref: '#/components/schemas/PageRevertData'
  505. *
  506. * /pages.revertRemove:
  507. * post:
  508. * tags: [Pages]
  509. * operationId: revertRemovePage
  510. * summary: Revert removed page
  511. * description: Restore a previously deleted (soft-deleted) page
  512. * requestBody:
  513. * required: true
  514. * content:
  515. * application/json:
  516. * schema:
  517. * type: object
  518. * required:
  519. * - page_id
  520. * properties:
  521. * page_id:
  522. * type: string
  523. * format: ObjectId
  524. * description: Unique identifier of the page to restore
  525. * example: "507f1f77bcf86cd799439011"
  526. * recursively:
  527. * type: boolean
  528. * description: Whether to restore child pages recursively
  529. * default: false
  530. * example: true
  531. * examples:
  532. * singleRevert:
  533. * summary: Revert single page
  534. * value:
  535. * page_id: "507f1f77bcf86cd799439011"
  536. * recursiveRevert:
  537. * summary: Revert page and children
  538. * value:
  539. * page_id: "507f1f77bcf86cd799439011"
  540. * recursively: true
  541. * responses:
  542. * 200:
  543. * $ref: '#/components/responses/PageRevertSuccess'
  544. * 400:
  545. * $ref: '#/components/responses/BadRequest'
  546. * 403:
  547. * $ref: '#/components/responses/Forbidden'
  548. * 404:
  549. * $ref: '#/components/responses/NotFound'
  550. * 409:
  551. * $ref: '#/components/responses/Conflict'
  552. * 500:
  553. * $ref: '#/components/responses/InternalServerError'
  554. */
  555. api.revertRemove = async (req, res, options) => {
  556. const pageId = req.body.page_id;
  557. // get recursively flag
  558. const isRecursively = req.body.recursively;
  559. const activityParameters = {
  560. ip: req.ip,
  561. endpoint: req.originalUrl,
  562. };
  563. let page;
  564. try {
  565. page = await Page.findByIdAndViewer(pageId, req.user);
  566. if (page == null) {
  567. throw new Error(
  568. `Page '${pageId}' is not found or forbidden`,
  569. 'notfound_or_forbidden',
  570. );
  571. }
  572. page = await crowi.pageService.revertDeletedPage(
  573. page,
  574. req.user,
  575. {},
  576. isRecursively,
  577. activityParameters,
  578. );
  579. } catch (err) {
  580. if (err instanceof PathAlreadyExistsError) {
  581. logger.error('Path already exists', err);
  582. return res.json(
  583. ApiResponse.error(err, 'already_exists', err.targetPath),
  584. );
  585. }
  586. logger.error('Error occured while get setting', err);
  587. return res.json(ApiResponse.error(err));
  588. }
  589. const result = {};
  590. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  591. return res.json(ApiResponse.success(result));
  592. };
  593. /**
  594. * @swagger
  595. *
  596. * components:
  597. * schemas:
  598. * PageUnlinkData:
  599. * type: object
  600. * properties:
  601. * path:
  602. * type: string
  603. * description: Path for which redirects were removed
  604. * example: "/user/example"
  605. *
  606. * responses:
  607. * PageUnlinkSuccess:
  608. * description: Successfully removed page redirects
  609. * content:
  610. * application/json:
  611. * schema:
  612. * allOf:
  613. * - $ref: '#/components/schemas/ApiResponseSuccess'
  614. * - $ref: '#/components/schemas/PageUnlinkData'
  615. *
  616. * /pages.unlink:
  617. * post:
  618. * tags: [Pages]
  619. * operationId: unlinkPage
  620. * summary: Remove page redirects
  621. * description: Remove all redirect entries that point to the specified page path
  622. * requestBody:
  623. * required: true
  624. * content:
  625. * application/json:
  626. * schema:
  627. * type: object
  628. * required:
  629. * - path
  630. * properties:
  631. * path:
  632. * type: string
  633. * description: Target path to remove redirects for
  634. * example: "/user/example"
  635. * examples:
  636. * unlinkPage:
  637. * summary: Remove redirects to a page
  638. * value:
  639. * path: "/user/example"
  640. * responses:
  641. * 200:
  642. * $ref: '#/components/responses/PageUnlinkSuccess'
  643. * 400:
  644. * $ref: '#/components/responses/BadRequest'
  645. * 403:
  646. * $ref: '#/components/responses/Forbidden'
  647. * 500:
  648. * $ref: '#/components/responses/InternalServerError'
  649. */
  650. api.unlink = async (req, res) => {
  651. const path = req.body.path;
  652. try {
  653. await PageRedirect.removePageRedirectsByToPath(path);
  654. logger.debug('Redirect Page deleted', path);
  655. } catch (err) {
  656. logger.error('Error occured while get setting', err);
  657. return res.json(ApiResponse.error('Failed to delete redirect page.'));
  658. }
  659. const result = { path };
  660. return res.json(ApiResponse.success(result));
  661. };
  662. return actions;
  663. };