page.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423
  1. import { pagePathUtils } from '@growi/core';
  2. import { body } from 'express-validator';
  3. import mongoose from 'mongoose';
  4. import urljoin from 'url-join';
  5. import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
  6. import loggerFactory from '~/utils/logger';
  7. import { PathAlreadyExistsError } from '../models/errors';
  8. import UpdatePost from '../models/update-post';
  9. const { isCreatablePage, isTopPage, isUsersHomePage } = pagePathUtils;
  10. const { serializePageSecurely } = require('../models/serializers/page-serializer');
  11. const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
  12. const { serializeUserSecurely } = require('../models/serializers/user-serializer');
  13. /**
  14. * @swagger
  15. * tags:
  16. * name: Pages
  17. */
  18. /**
  19. * @swagger
  20. *
  21. * components:
  22. * schemas:
  23. * Page:
  24. * description: Page
  25. * type: object
  26. * properties:
  27. * _id:
  28. * type: string
  29. * description: page ID
  30. * example: 5e07345972560e001761fa63
  31. * __v:
  32. * type: number
  33. * description: DB record version
  34. * example: 0
  35. * commentCount:
  36. * type: number
  37. * description: count of comments
  38. * example: 3
  39. * createdAt:
  40. * type: string
  41. * description: date created at
  42. * example: 2010-01-01T00:00:00.000Z
  43. * creator:
  44. * $ref: '#/components/schemas/User'
  45. * extended:
  46. * type: object
  47. * description: extend data
  48. * example: {}
  49. * grant:
  50. * type: number
  51. * description: grant
  52. * example: 1
  53. * grantedUsers:
  54. * type: array
  55. * description: granted users
  56. * items:
  57. * type: string
  58. * description: user ID
  59. * example: ["5ae5fccfc5577b0004dbd8ab"]
  60. * lastUpdateUser:
  61. * $ref: '#/components/schemas/User'
  62. * liker:
  63. * type: array
  64. * description: granted users
  65. * items:
  66. * type: string
  67. * description: user ID
  68. * example: []
  69. * path:
  70. * type: string
  71. * description: page path
  72. * example: /
  73. * revision:
  74. * $ref: '#/components/schemas/Revision'
  75. * status:
  76. * type: string
  77. * description: status
  78. * enum:
  79. * - 'wip'
  80. * - 'published'
  81. * - 'deleted'
  82. * - 'deprecated'
  83. * example: published
  84. * updatedAt:
  85. * type: string
  86. * description: date updated at
  87. * example: 2010-01-01T00:00:00.000Z
  88. *
  89. * UpdatePost:
  90. * description: UpdatePost
  91. * type: object
  92. * properties:
  93. * _id:
  94. * type: string
  95. * description: update post ID
  96. * example: 5e0734e472560e001761fa68
  97. * __v:
  98. * type: number
  99. * description: DB record version
  100. * example: 0
  101. * pathPattern:
  102. * type: string
  103. * description: path pattern
  104. * example: /test
  105. * patternPrefix:
  106. * type: string
  107. * description: patternPrefix prefix
  108. * example: /
  109. * patternPrefix2:
  110. * type: string
  111. * description: path
  112. * example: test
  113. * channel:
  114. * type: string
  115. * description: channel
  116. * example: general
  117. * provider:
  118. * type: string
  119. * description: provider
  120. * enum:
  121. * - slack
  122. * example: slack
  123. * creator:
  124. * $ref: '#/components/schemas/User'
  125. * createdAt:
  126. * type: string
  127. * description: date created at
  128. * example: 2010-01-01T00:00:00.000Z
  129. */
  130. /* eslint-disable no-use-before-define */
  131. module.exports = function(crowi, app) {
  132. const debug = require('debug')('growi:routes:page');
  133. const logger = loggerFactory('growi:routes:page');
  134. const swig = require('swig-templates');
  135. const { pathUtils } = require('@growi/core');
  136. const Page = crowi.model('Page');
  137. const User = crowi.model('User');
  138. const PageTagRelation = crowi.model('PageTagRelation');
  139. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  140. const ShareLink = crowi.model('ShareLink');
  141. const PageRedirect = mongoose.model('PageRedirect');
  142. const { PageQueryBuilder } = Page;
  143. const ApiResponse = require('../util/apiResponse');
  144. const getToday = require('../util/getToday');
  145. const { configManager, xssService } = crowi;
  146. const globalNotificationService = crowi.getGlobalNotificationService();
  147. const userNotificationService = crowi.getUserNotificationService();
  148. const activityEvent = crowi.event('activity');
  149. const XssOption = require('~/services/xss/xssOption');
  150. const Xss = require('~/services/xss/index');
  151. const initializedConfig = {
  152. isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
  153. tagWhiteList: xssService.getTagWhiteList(),
  154. attrWhiteList: xssService.getAttrWhiteList(),
  155. };
  156. const xssOption = new XssOption(initializedConfig);
  157. const xss = new Xss(xssOption);
  158. const actions = {};
  159. function getPathFromRequest(req) {
  160. return pathUtils.normalizePath(req.pagePath || req.params[0] || '');
  161. }
  162. function generatePager(offset, limit, totalCount) {
  163. let prev = null;
  164. if (offset > 0) {
  165. prev = offset - limit;
  166. if (prev < 0) {
  167. prev = 0;
  168. }
  169. }
  170. let next = offset + limit;
  171. if (totalCount < next) {
  172. next = null;
  173. }
  174. return {
  175. prev,
  176. next,
  177. offset,
  178. };
  179. }
  180. function addRenderVarsForPage(renderVars, page) {
  181. renderVars.page = page;
  182. renderVars.revision = page.revision;
  183. renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
  184. renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
  185. renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
  186. if (page.creator != null) {
  187. renderVars.page.creator = renderVars.page.creator.toObject();
  188. }
  189. if (page.revision.author != null) {
  190. renderVars.revision.author = renderVars.revision.author.toObject();
  191. }
  192. if (page.deleteUser != null) {
  193. renderVars.page.deleteUser = renderVars.page.deleteUser.toObject();
  194. }
  195. }
  196. function addRenderVarsForPresentation(renderVars, page) {
  197. // sanitize page.revision.body
  198. if (crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention')) {
  199. const preventXssRevision = xss.process(page.revision.body);
  200. page.revision.body = preventXssRevision;
  201. }
  202. renderVars.page = page;
  203. renderVars.revision = page.revision;
  204. }
  205. async function addRenderVarsForUserPage(renderVars, page) {
  206. const userData = await User.findUserByUsername(User.getUsernameByPath(page.path));
  207. if (userData != null) {
  208. renderVars.pageUser = serializeUserSecurely(userData);
  209. }
  210. }
  211. function addRenderVarsForScope(renderVars, page) {
  212. renderVars.grant = page.grant;
  213. renderVars.grantedGroupId = page.grantedGroup ? page.grantedGroup.id : null;
  214. renderVars.grantedGroupName = page.grantedGroup ? page.grantedGroup.name : null;
  215. }
  216. async function addRenderVarsForDescendants(renderVars, path, requestUser, offset, limit, isRegExpEscapedFromPath) {
  217. const SEENER_THRESHOLD = 10;
  218. const queryOptions = {
  219. offset,
  220. limit,
  221. includeTrashed: path.startsWith('/trash/'),
  222. isRegExpEscapedFromPath,
  223. };
  224. const result = await Page.findListWithDescendants(path, requestUser, queryOptions);
  225. if (result.pages.length > limit) {
  226. result.pages.pop();
  227. }
  228. renderVars.viewConfig = {
  229. seener_threshold: SEENER_THRESHOLD,
  230. };
  231. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  232. renderVars.pages = result.pages;
  233. }
  234. async function addRenderVarsForPageTree(renderVars, pathOrId, user) {
  235. const { targetAndAncestors, rootPage } = await Page.findTargetAndAncestorsByPathOrId(pathOrId, user);
  236. if (targetAndAncestors.length === 0 && pathOrId.includes('/') && !isTopPage(pathOrId)) {
  237. throw new Error('Ancestors must have at least one page.');
  238. }
  239. renderVars.targetAndAncestors = { targetAndAncestors, rootPage };
  240. }
  241. async function addRenderVarsWhenNotFound(renderVars, pathOrId) {
  242. if (pathOrId == null) {
  243. return;
  244. }
  245. renderVars.notFoundTargetPathOrId = pathOrId;
  246. const isPath = pathOrId.includes('/');
  247. renderVars.isNotFoundPermalink = !isPath && !await Page.exists({ _id: pathOrId });
  248. }
  249. async function addRenderVarsWhenEmptyPage(renderVars, isEmpty, pageId) {
  250. if (!isEmpty) return;
  251. renderVars.pageId = pageId;
  252. renderVars.isEmpty = isEmpty;
  253. }
  254. function replacePlaceholdersOfTemplate(template, req) {
  255. if (req.user == null) {
  256. return '';
  257. }
  258. const definitions = {
  259. pagepath: getPathFromRequest(req),
  260. username: req.user.name,
  261. today: getToday(),
  262. };
  263. const compiledTemplate = swig.compile(template);
  264. return compiledTemplate(definitions);
  265. }
  266. async function _notFound(req, res) {
  267. const path = getPathFromRequest(req);
  268. const pathOrId = req.params.id || path;
  269. let view;
  270. const renderVars = { path };
  271. if (!isCreatablePage(path)) {
  272. view = 'layout-growi/not_creatable';
  273. }
  274. else if (req.isForbidden) {
  275. view = 'layout-growi/forbidden';
  276. }
  277. else {
  278. view = 'layout-growi/not_found';
  279. // retrieve templates
  280. if (req.user != null) {
  281. const template = await Page.findTemplate(path);
  282. if (template.templateBody) {
  283. const body = replacePlaceholdersOfTemplate(template.templateBody, req);
  284. const tags = template.templateTags;
  285. renderVars.template = body;
  286. renderVars.templateTags = tags;
  287. }
  288. }
  289. // add scope variables by ancestor page
  290. const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
  291. if (ancestor != null) {
  292. await ancestor.populate('grantedGroup');
  293. addRenderVarsForScope(renderVars, ancestor);
  294. }
  295. }
  296. const limit = 50;
  297. const offset = parseInt(req.query.offset) || 0;
  298. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  299. await addRenderVarsForPageTree(renderVars, pathOrId, req.user);
  300. await addRenderVarsWhenNotFound(renderVars, pathOrId);
  301. await addRenderVarsWhenEmptyPage(renderVars, req.isEmpty, req.pageId);
  302. return res.render(view, renderVars);
  303. }
  304. async function showPageForPresentation(req, res, next) {
  305. const id = req.params.id;
  306. const { revisionId } = req.query;
  307. let page = await Page.findByIdAndViewer(id, req.user, null, true, true);
  308. if (page == null) {
  309. next();
  310. }
  311. // empty page
  312. if (page.isEmpty) {
  313. // redirect to page (path) url
  314. const url = new URL('https://dummy.origin');
  315. url.pathname = page.path;
  316. Object.entries(req.query).forEach(([key, value], i) => {
  317. url.searchParams.append(key, value);
  318. });
  319. return res.safeRedirect(urljoin(url.pathname, url.search));
  320. }
  321. const renderVars = {};
  322. // populate
  323. page = await page.populateDataToMakePresentation(revisionId);
  324. if (page != null) {
  325. addRenderVarsForPresentation(renderVars, page);
  326. }
  327. return res.render('page_presentation', renderVars);
  328. }
  329. async function showTopPage(req, res, next) {
  330. const portalPath = req.path;
  331. const revisionId = req.query.revision;
  332. const view = 'layout-growi/page_list';
  333. const renderVars = { path: portalPath };
  334. let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
  335. portalPage.initLatestRevisionField(revisionId);
  336. // add user to seen users
  337. if (req.user != null) {
  338. portalPage = await portalPage.seen(req.user);
  339. }
  340. // populate
  341. portalPage = await portalPage.populateDataToShowRevision();
  342. addRenderVarsForPage(renderVars, portalPage);
  343. const sharelinksNumber = await ShareLink.countDocuments({ relatedPage: portalPage._id });
  344. renderVars.sharelinksNumber = sharelinksNumber;
  345. const limit = 50;
  346. const offset = parseInt(req.query.offset) || 0;
  347. await addRenderVarsForDescendants(renderVars, portalPath, req.user, offset, limit);
  348. await addRenderVarsForPageTree(renderVars, portalPath, req.user);
  349. const parameters = { action: SupportedAction.ACTION_PAGE_VIEW };
  350. activityEvent.emit('update', res.locals.activity._id, parameters);
  351. return res.render(view, renderVars);
  352. }
  353. async function showPageForGrowiBehavior(req, res, next) {
  354. const id = req.params.id;
  355. const revisionId = req.query.revision;
  356. let page = await Page.findByIdAndViewer(id, req.user, null, true, true);
  357. if (page == null) {
  358. // check the page is forbidden or just does not exist.
  359. req.isForbidden = await Page.count({ _id: id }) > 0;
  360. return _notFound(req, res);
  361. }
  362. // empty page
  363. if (page.isEmpty) {
  364. req.pageId = page._id;
  365. req.pagePath = page.path;
  366. req.isEmpty = page.isEmpty;
  367. return _notFound(req, res);
  368. }
  369. const { path } = page; // this must exist
  370. logger.debug('Page is found when processing pageShowForGrowiBehavior', page._id, path);
  371. const limit = 50;
  372. const offset = parseInt(req.query.offset) || 0;
  373. const renderVars = {};
  374. let view = 'layout-growi/page';
  375. page.initLatestRevisionField(revisionId);
  376. // add user to seen users
  377. if (req.user != null) {
  378. page = await page.seen(req.user);
  379. }
  380. // populate
  381. page = await page.populateDataToShowRevision();
  382. addRenderVarsForPage(renderVars, page);
  383. addRenderVarsForScope(renderVars, page);
  384. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  385. const sharelinksNumber = await ShareLink.countDocuments({ relatedPage: page._id });
  386. renderVars.sharelinksNumber = sharelinksNumber;
  387. if (isUsersHomePage(path)) {
  388. // change template
  389. view = 'layout-growi/user_page';
  390. await addRenderVarsForUserPage(renderVars, page);
  391. }
  392. await addRenderVarsForPageTree(renderVars, path, req.user);
  393. const parameters = { action: SupportedAction.ACTION_PAGE_VIEW };
  394. activityEvent.emit('update', res.locals.activity._id, parameters);
  395. return res.render(view, renderVars);
  396. }
  397. actions.showTopPage = function(req, res) {
  398. return showTopPage(req, res);
  399. };
  400. /**
  401. * Redirect to the page without trailing slash
  402. */
  403. actions.showPageWithEndOfSlash = function(req, res, next) {
  404. return res.redirect(pathUtils.removeTrailingSlash(req.path));
  405. };
  406. /**
  407. * switch action
  408. * - presentation mode
  409. * - by behaviorType
  410. */
  411. actions.showPage = async function(req, res, next) {
  412. // presentation mode
  413. if (req.query.presentation) {
  414. return showPageForPresentation(req, res, next);
  415. }
  416. // delegate to showPageForGrowiBehavior
  417. return showPageForGrowiBehavior(req, res, next);
  418. };
  419. actions.showSharedPage = async function(req, res, next) {
  420. const { linkId } = req.params;
  421. const revisionId = req.query.revision;
  422. const renderVars = {};
  423. const shareLink = await ShareLink.findOne({ _id: linkId }).populate('relatedPage');
  424. if (shareLink == null || shareLink.relatedPage == null || shareLink.relatedPage.isEmpty) {
  425. // page or sharelink are not found (or page is empty: abnormaly)
  426. return res.render('layout-growi/not_found_shared_page');
  427. }
  428. if (crowi.configManager.getConfig('crowi', 'security:disableLinkSharing')) {
  429. return res.render('layout-growi/forbidden');
  430. }
  431. renderVars.sharelink = shareLink;
  432. // check if share link is expired
  433. if (shareLink.isExpired()) {
  434. // page is not found
  435. return res.render('layout-growi/expired_shared_page', renderVars);
  436. }
  437. let page = shareLink.relatedPage;
  438. // presentation mode
  439. if (req.query.presentation) {
  440. page = await page.populateDataToMakePresentation(revisionId);
  441. // populate
  442. addRenderVarsForPage(renderVars, page);
  443. return res.render('page_presentation', renderVars);
  444. }
  445. page.initLatestRevisionField(revisionId);
  446. // populate
  447. page = await page.populateDataToShowRevision();
  448. addRenderVarsForPage(renderVars, page);
  449. addRenderVarsForScope(renderVars, page);
  450. return res.render('layout-growi/shared_page', renderVars);
  451. };
  452. /**
  453. * switch action by behaviorType
  454. */
  455. /* eslint-disable no-else-return */
  456. actions.trashPageShowWrapper = function(req, res) {
  457. // Crowi behavior for '/trash/*'
  458. return actions.deletedPageListShow(req, res);
  459. };
  460. /* eslint-enable no-else-return */
  461. /**
  462. * switch action by behaviorType
  463. */
  464. /* eslint-disable no-else-return */
  465. actions.deletedPageListShowWrapper = function(req, res) {
  466. const path = `/trash${getPathFromRequest(req)}`;
  467. return res.redirect(path);
  468. };
  469. /* eslint-enable no-else-return */
  470. actions.notFound = async function(req, res) {
  471. return _notFound(req, res);
  472. };
  473. actions.deletedPageListShow = async function(req, res) {
  474. // normalizePath makes '/trash/' -> '/trash'
  475. const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
  476. const limit = 50;
  477. const offset = parseInt(req.query.offset) || 0;
  478. const queryOptions = {
  479. offset,
  480. limit,
  481. includeTrashed: true,
  482. };
  483. const renderVars = {
  484. page: null,
  485. path,
  486. pages: [],
  487. };
  488. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  489. if (result.pages.length > limit) {
  490. result.pages.pop();
  491. }
  492. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  493. renderVars.pages = result.pages;
  494. res.render('layout-growi/page_list', renderVars);
  495. };
  496. /**
  497. * redirector
  498. */
  499. async function redirector(req, res, next, path) {
  500. const { redirectFrom } = req.query;
  501. const includeEmpty = true;
  502. const builder = new PageQueryBuilder(Page.find({ path }), includeEmpty);
  503. builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
  504. await Page.addConditionToFilteringByViewerForList(builder, req.user, true);
  505. const pages = await builder.query.lean().clone().exec('find');
  506. const nonEmptyPages = pages.filter(p => !p.isEmpty);
  507. if (nonEmptyPages.length >= 2) {
  508. return res.render('layout-growi/identical-path-page', {
  509. identicalPathPages: nonEmptyPages,
  510. redirectFrom,
  511. path,
  512. });
  513. }
  514. if (nonEmptyPages.length === 1) {
  515. const nonEmptyPage = nonEmptyPages[0];
  516. const url = new URL('https://dummy.origin');
  517. url.pathname = `/${nonEmptyPage._id}`;
  518. Object.entries(req.query).forEach(([key, value], i) => {
  519. url.searchParams.append(key, value);
  520. });
  521. return res.safeRedirect(urljoin(url.pathname, url.search));
  522. }
  523. // Processing of nonEmptyPage is finished by the time this code is read
  524. // If any pages exist then they should be empty
  525. const emptyPage = pages[0];
  526. if (emptyPage != null) {
  527. req.pageId = emptyPage._id;
  528. req.isEmpty = emptyPage.isEmpty;
  529. return _notFound(req, res);
  530. }
  531. // redirect by PageRedirect
  532. const pageRedirect = await PageRedirect.findOne({ fromPath: path });
  533. if (pageRedirect != null) {
  534. return res.safeRedirect(`${encodeURI(pageRedirect.toPath)}?redirectFrom=${encodeURIComponent(path)}`);
  535. }
  536. return _notFound(req, res);
  537. }
  538. actions.redirector = async function(req, res, next) {
  539. const path = getPathFromRequest(req);
  540. const parameters = { action: SupportedAction.ACTION_PAGE_VIEW };
  541. activityEvent.emit('update', res.locals.activity._id, parameters);
  542. return redirector(req, res, next, path);
  543. };
  544. actions.redirectorWithEndOfSlash = async function(req, res, next) {
  545. const _path = getPathFromRequest(req);
  546. const path = pathUtils.removeTrailingSlash(_path);
  547. const parameters = { action: SupportedAction.ACTION_PAGE_VIEW };
  548. activityEvent.emit('update', res.locals.activity._id, parameters);
  549. return redirector(req, res, next, path);
  550. };
  551. const api = {};
  552. const validator = {};
  553. actions.api = api;
  554. actions.validator = validator;
  555. /**
  556. * @swagger
  557. *
  558. * /pages.list:
  559. * get:
  560. * tags: [Pages, CrowiCompatibles]
  561. * operationId: listPages
  562. * summary: /pages.list
  563. * description: Get list of pages
  564. * parameters:
  565. * - in: query
  566. * name: path
  567. * schema:
  568. * $ref: '#/components/schemas/Page/properties/path'
  569. * - in: query
  570. * name: user
  571. * schema:
  572. * $ref: '#/components/schemas/User/properties/username'
  573. * - in: query
  574. * name: limit
  575. * schema:
  576. * $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/limit'
  577. * - in: query
  578. * name: offset
  579. * schema:
  580. * $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/offset'
  581. * responses:
  582. * 200:
  583. * description: Succeeded to get list of pages.
  584. * content:
  585. * application/json:
  586. * schema:
  587. * properties:
  588. * ok:
  589. * $ref: '#/components/schemas/V1Response/properties/ok'
  590. * pages:
  591. * type: array
  592. * items:
  593. * $ref: '#/components/schemas/Page'
  594. * description: page list
  595. * 403:
  596. * $ref: '#/components/responses/403'
  597. * 500:
  598. * $ref: '#/components/responses/500'
  599. */
  600. /**
  601. * @api {get} /pages.list List pages by user
  602. * @apiName ListPage
  603. * @apiGroup Page
  604. *
  605. * @apiParam {String} path
  606. * @apiParam {String} user
  607. */
  608. api.list = async function(req, res) {
  609. const username = req.query.user || null;
  610. const path = req.query.path || null;
  611. const limit = +req.query.limit || 50;
  612. const offset = parseInt(req.query.offset) || 0;
  613. const queryOptions = { offset, limit: limit + 1 };
  614. // Accepts only one of these
  615. if (username === null && path === null) {
  616. return res.json(ApiResponse.error('Parameter user or path is required.'));
  617. }
  618. if (username !== null && path !== null) {
  619. return res.json(ApiResponse.error('Parameter user or path is required.'));
  620. }
  621. try {
  622. let result = null;
  623. if (path == null) {
  624. const user = await User.findUserByUsername(username);
  625. if (user === null) {
  626. throw new Error('The user not found.');
  627. }
  628. result = await Page.findListByCreator(user, req.user, queryOptions);
  629. }
  630. else {
  631. result = await Page.findListByStartWith(path, req.user, queryOptions);
  632. }
  633. if (result.pages.length > limit) {
  634. result.pages.pop();
  635. }
  636. result.pages.forEach((page) => {
  637. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  638. page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
  639. }
  640. });
  641. return res.json(ApiResponse.success(result));
  642. }
  643. catch (err) {
  644. return res.json(ApiResponse.error(err));
  645. }
  646. };
  647. // TODO If everything that depends on this route, delete it too
  648. api.create = async function(req, res) {
  649. const body = req.body.body || null;
  650. let pagePath = req.body.path || null;
  651. const grant = req.body.grant || null;
  652. const grantUserGroupId = req.body.grantUserGroupId || null;
  653. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  654. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  655. const slackChannels = req.body.slackChannels || null;
  656. const pageTags = req.body.pageTags || undefined;
  657. if (body === null || pagePath === null) {
  658. return res.json(ApiResponse.error('Parameters body and path are required.'));
  659. }
  660. // check whether path starts slash
  661. pagePath = pathUtils.addHeadingSlash(pagePath);
  662. // check page existence
  663. const isExist = await Page.count({ path: pagePath }) > 0;
  664. if (isExist) {
  665. return res.json(ApiResponse.error('Page exists', 'already_exists'));
  666. }
  667. const options = {};
  668. if (grant != null) {
  669. options.grant = grant;
  670. options.grantUserGroupId = grantUserGroupId;
  671. }
  672. const createdPage = await crowi.pageService.create(pagePath, body, req.user, options);
  673. let savedTags;
  674. if (pageTags != null) {
  675. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  676. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  677. }
  678. const result = {
  679. page: serializePageSecurely(createdPage),
  680. revision: serializeRevisionSecurely(createdPage.revision),
  681. tags: savedTags,
  682. };
  683. res.json(ApiResponse.success(result));
  684. // update scopes for descendants
  685. if (overwriteScopesOfDescendants) {
  686. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  687. }
  688. // global notification
  689. try {
  690. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  691. }
  692. catch (err) {
  693. logger.error('Create notification failed', err);
  694. }
  695. // user notification
  696. if (isSlackEnabled) {
  697. try {
  698. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
  699. results.forEach((result) => {
  700. if (result.status === 'rejected') {
  701. logger.error('Create user notification failed', result.reason);
  702. }
  703. });
  704. }
  705. catch (err) {
  706. logger.error('Create user notification failed', err);
  707. }
  708. }
  709. };
  710. /**
  711. * @swagger
  712. *
  713. * /pages.update:
  714. * post:
  715. * tags: [Pages, CrowiCompatibles]
  716. * operationId: updatePage
  717. * summary: /pages.update
  718. * description: Update page
  719. * requestBody:
  720. * content:
  721. * application/json:
  722. * schema:
  723. * properties:
  724. * body:
  725. * $ref: '#/components/schemas/Revision/properties/body'
  726. * page_id:
  727. * $ref: '#/components/schemas/Page/properties/_id'
  728. * revision_id:
  729. * $ref: '#/components/schemas/Revision/properties/_id'
  730. * grant:
  731. * $ref: '#/components/schemas/Page/properties/grant'
  732. * required:
  733. * - body
  734. * - page_id
  735. * - revision_id
  736. * responses:
  737. * 200:
  738. * description: Succeeded to update page.
  739. * content:
  740. * application/json:
  741. * schema:
  742. * properties:
  743. * ok:
  744. * $ref: '#/components/schemas/V1Response/properties/ok'
  745. * page:
  746. * $ref: '#/components/schemas/Page'
  747. * revision:
  748. * $ref: '#/components/schemas/Revision'
  749. * 403:
  750. * $ref: '#/components/responses/403'
  751. * 500:
  752. * $ref: '#/components/responses/500'
  753. */
  754. /**
  755. * @api {post} /pages.update Update page
  756. * @apiName UpdatePage
  757. * @apiGroup Page
  758. *
  759. * @apiParam {String} body
  760. * @apiParam {String} page_id
  761. * @apiParam {String} revision_id
  762. * @apiParam {String} grant
  763. *
  764. * In the case of the page exists:
  765. * - If revision_id is specified => update the page,
  766. * - If revision_id is not specified => force update by the new contents.
  767. */
  768. api.update = async function(req, res) {
  769. const pageBody = req.body.body ?? null;
  770. const pageId = req.body.page_id || null;
  771. const revisionId = req.body.revision_id || null;
  772. const grant = req.body.grant || null;
  773. const grantUserGroupId = req.body.grantUserGroupId || null;
  774. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  775. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  776. const slackChannels = req.body.slackChannels || null;
  777. const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd; // cast to boolean
  778. const pageTags = req.body.pageTags || undefined;
  779. if (pageId === null || pageBody === null || revisionId === null) {
  780. return res.json(ApiResponse.error('page_id, body and revision_id are required.'));
  781. }
  782. // check page existence
  783. const isExist = await Page.count({ _id: pageId }) > 0;
  784. if (!isExist) {
  785. return res.json(ApiResponse.error(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  786. }
  787. // check revision
  788. const Revision = crowi.model('Revision');
  789. let page = await Page.findByIdAndViewer(pageId, req.user);
  790. if (page != null && revisionId != null && !page.isUpdatable(revisionId)) {
  791. const latestRevision = await Revision.findById(page.revision).populate('author');
  792. const returnLatestRevision = {
  793. revisionId: latestRevision._id.toString(),
  794. revisionBody: xss.process(latestRevision.body),
  795. createdAt: latestRevision.createdAt,
  796. user: serializeUserSecurely(latestRevision.author),
  797. };
  798. return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'conflict', returnLatestRevision));
  799. }
  800. const options = { isSyncRevisionToHackmd };
  801. if (grant != null) {
  802. options.grant = grant;
  803. options.grantUserGroupId = grantUserGroupId;
  804. }
  805. const previousRevision = await Revision.findById(revisionId);
  806. try {
  807. page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
  808. }
  809. catch (err) {
  810. logger.error('error on _api/pages.update', err);
  811. return res.json(ApiResponse.error(err));
  812. }
  813. let savedTags;
  814. if (pageTags != null) {
  815. const tagEvent = crowi.event('tag');
  816. await PageTagRelation.updatePageTags(pageId, pageTags);
  817. savedTags = await PageTagRelation.listTagNamesByPage(pageId);
  818. tagEvent.emit('update', page, savedTags);
  819. }
  820. const result = {
  821. page: serializePageSecurely(page),
  822. revision: serializeRevisionSecurely(page.revision),
  823. tags: savedTags,
  824. };
  825. res.json(ApiResponse.success(result));
  826. // update scopes for descendants
  827. if (overwriteScopesOfDescendants) {
  828. Page.applyScopesToDescendantsAsyncronously(page, req.user);
  829. }
  830. // global notification
  831. try {
  832. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page, req.user);
  833. }
  834. catch (err) {
  835. logger.error('Edit notification failed', err);
  836. }
  837. // user notification
  838. if (isSlackEnabled) {
  839. try {
  840. const results = await userNotificationService.fire(page, req.user, slackChannels, 'update', { previousRevision });
  841. results.forEach((result) => {
  842. if (result.status === 'rejected') {
  843. logger.error('Create user notification failed', result.reason);
  844. }
  845. });
  846. }
  847. catch (err) {
  848. logger.error('Create user notification failed', err);
  849. }
  850. }
  851. const parameters = {
  852. targetModel: SupportedTargetModel.MODEL_PAGE,
  853. target: page,
  854. action: SupportedAction.ACTION_PAGE_UPDATE,
  855. };
  856. activityEvent.emit('update', res.locals.activity._id, parameters, page);
  857. };
  858. /**
  859. * @swagger
  860. *
  861. * /pages.exist:
  862. * get:
  863. * tags: [Pages]
  864. * operationId: getPageExistence
  865. * summary: /pages.exist
  866. * description: Get page existence
  867. * parameters:
  868. * - in: query
  869. * name: pagePaths
  870. * schema:
  871. * type: string
  872. * description: Page path list in JSON Array format
  873. * example: '["/", "/user/unknown"]'
  874. * responses:
  875. * 200:
  876. * description: Succeeded to get page existence.
  877. * content:
  878. * application/json:
  879. * schema:
  880. * properties:
  881. * ok:
  882. * $ref: '#/components/schemas/V1Response/properties/ok'
  883. * pages:
  884. * type: string
  885. * description: Properties of page path and existence
  886. * example: '{"/": true, "/user/unknown": false}'
  887. * 403:
  888. * $ref: '#/components/responses/403'
  889. * 500:
  890. * $ref: '#/components/responses/500'
  891. */
  892. /**
  893. * @api {get} /pages.exist Get if page exists
  894. * @apiName GetPage
  895. * @apiGroup Page
  896. *
  897. * @apiParam {String} pages (stringified JSON)
  898. */
  899. api.exist = async function(req, res) {
  900. const pagePaths = JSON.parse(req.query.pagePaths || '[]');
  901. const pages = {};
  902. await Promise.all(pagePaths.map(async(path) => {
  903. // check page existence
  904. const isExist = await Page.count({ path }) > 0;
  905. pages[path] = isExist;
  906. return;
  907. }));
  908. const result = { pages };
  909. return res.json(ApiResponse.success(result));
  910. };
  911. /**
  912. * @swagger
  913. *
  914. * /pages.getPageTag:
  915. * get:
  916. * tags: [Pages]
  917. * operationId: getPageTag
  918. * summary: /pages.getPageTag
  919. * description: Get page tag
  920. * parameters:
  921. * - in: query
  922. * name: pageId
  923. * schema:
  924. * $ref: '#/components/schemas/Page/properties/_id'
  925. * responses:
  926. * 200:
  927. * description: Succeeded to get page tags.
  928. * content:
  929. * application/json:
  930. * schema:
  931. * properties:
  932. * ok:
  933. * $ref: '#/components/schemas/V1Response/properties/ok'
  934. * tags:
  935. * $ref: '#/components/schemas/Tags'
  936. * 403:
  937. * $ref: '#/components/responses/403'
  938. * 500:
  939. * $ref: '#/components/responses/500'
  940. */
  941. /**
  942. * @api {get} /pages.getPageTag get page tags
  943. * @apiName GetPageTag
  944. * @apiGroup Page
  945. *
  946. * @apiParam {String} pageId
  947. */
  948. api.getPageTag = async function(req, res) {
  949. const result = {};
  950. try {
  951. result.tags = await PageTagRelation.listTagNamesByPage(req.query.pageId);
  952. }
  953. catch (err) {
  954. return res.json(ApiResponse.error(err));
  955. }
  956. return res.json(ApiResponse.success(result));
  957. };
  958. /**
  959. * @swagger
  960. *
  961. * /pages.updatePost:
  962. * get:
  963. * tags: [Pages, CrowiCompatibles]
  964. * operationId: getUpdatePostPage
  965. * summary: /pages.updatePost
  966. * description: Get UpdatePost setting list
  967. * parameters:
  968. * - in: query
  969. * name: path
  970. * schema:
  971. * $ref: '#/components/schemas/Page/properties/path'
  972. * responses:
  973. * 200:
  974. * description: Succeeded to get UpdatePost setting list.
  975. * content:
  976. * application/json:
  977. * schema:
  978. * properties:
  979. * ok:
  980. * $ref: '#/components/schemas/V1Response/properties/ok'
  981. * updatePost:
  982. * $ref: '#/components/schemas/UpdatePost'
  983. * 403:
  984. * $ref: '#/components/responses/403'
  985. * 500:
  986. * $ref: '#/components/responses/500'
  987. */
  988. /**
  989. * @api {get} /pages.updatePost
  990. * @apiName Get UpdatePost setting list
  991. * @apiGroup Page
  992. *
  993. * @apiParam {String} path
  994. */
  995. api.getUpdatePost = function(req, res) {
  996. const path = req.query.path;
  997. if (!path) {
  998. return res.json(ApiResponse.error({}));
  999. }
  1000. UpdatePost.findSettingsByPath(path)
  1001. .then((data) => {
  1002. // eslint-disable-next-line no-param-reassign
  1003. data = data.map((e) => {
  1004. return e.channel;
  1005. });
  1006. debug('Found updatePost data', data);
  1007. const result = { updatePost: data };
  1008. return res.json(ApiResponse.success(result));
  1009. })
  1010. .catch((err) => {
  1011. debug('Error occured while get setting', err);
  1012. return res.json(ApiResponse.error({}));
  1013. });
  1014. };
  1015. validator.remove = [
  1016. body('completely')
  1017. .custom(v => v === 'true' || v === true || v == null)
  1018. .withMessage('The body property "completely" must be "true" or true. (Omit param for false)'),
  1019. body('recursively')
  1020. .custom(v => v === 'true' || v === true || v == null)
  1021. .withMessage('The body property "recursively" must be "true" or true. (Omit param for false)'),
  1022. ];
  1023. /**
  1024. * @api {post} /pages.remove Remove page
  1025. * @apiName RemovePage
  1026. * @apiGroup Page
  1027. *
  1028. * @apiParam {String} page_id Page Id.
  1029. * @apiParam {String} revision_id
  1030. */
  1031. api.remove = async function(req, res) {
  1032. const pageId = req.body.page_id;
  1033. const previousRevision = req.body.revision_id || null;
  1034. const { recursively: isRecursively, completely: isCompletely } = req.body;
  1035. const options = {};
  1036. const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
  1037. if (page == null) {
  1038. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  1039. }
  1040. let creator;
  1041. if (page.isEmpty) {
  1042. // If empty, the creator is inherited from the closest non-empty ancestor page.
  1043. const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
  1044. creator = notEmptyClosestAncestor.creator;
  1045. }
  1046. else {
  1047. creator = page.creator;
  1048. }
  1049. debug('Delete page', page._id, page.path);
  1050. try {
  1051. if (isCompletely) {
  1052. if (!crowi.pageService.canDeleteCompletely(page.path, creator, req.user, isRecursively)) {
  1053. return res.json(ApiResponse.error('You can not delete this page completely', 'user_not_admin'));
  1054. }
  1055. await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively);
  1056. }
  1057. else {
  1058. // behave like not found
  1059. const notRecursivelyAndEmpty = page.isEmpty && !isRecursively;
  1060. if (notRecursivelyAndEmpty) {
  1061. return res.json(ApiResponse.error(`Page '${pageId}' is not found.`, 'notfound'));
  1062. }
  1063. if (!page.isEmpty && !page.isUpdatable(previousRevision)) {
  1064. return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
  1065. }
  1066. if (!crowi.pageService.canDelete(page.path, creator, req.user, isRecursively)) {
  1067. return res.json(ApiResponse.error('You can not delete this page', 'user_not_admin'));
  1068. }
  1069. await crowi.pageService.deletePage(page, req.user, options, isRecursively);
  1070. }
  1071. }
  1072. catch (err) {
  1073. logger.error('Error occured while get setting', err);
  1074. return res.json(ApiResponse.error('Failed to delete page.', err.message));
  1075. }
  1076. debug('Page deleted', page.path);
  1077. const result = {};
  1078. result.path = page.path;
  1079. result.isRecursively = isRecursively;
  1080. result.isCompletely = isCompletely;
  1081. const parameters = {
  1082. targetModel: SupportedTargetModel.MODEL_PAGE,
  1083. target: page,
  1084. action: isCompletely ? SupportedAction.ACTION_PAGE_DELETE_COMPLETELY : SupportedAction.ACTION_PAGE_DELETE,
  1085. };
  1086. activityEvent.emit('update', res.locals.activity._id, parameters, page);
  1087. res.json(ApiResponse.success(result));
  1088. try {
  1089. // global notification
  1090. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page, req.user);
  1091. }
  1092. catch (err) {
  1093. logger.error('Delete notification failed', err);
  1094. }
  1095. };
  1096. validator.revertRemove = [
  1097. body('recursively')
  1098. .optional()
  1099. .custom(v => v === 'true' || v === true || v == null)
  1100. .withMessage('The body property "recursively" must be "true" or true. (Omit param for false)'),
  1101. ];
  1102. /**
  1103. * @api {post} /pages.revertRemove Revert removed page
  1104. * @apiName RevertRemovePage
  1105. * @apiGroup Page
  1106. *
  1107. * @apiParam {String} page_id Page Id.
  1108. */
  1109. api.revertRemove = async function(req, res, options) {
  1110. const pageId = req.body.page_id;
  1111. // get recursively flag
  1112. const isRecursively = req.body.recursively;
  1113. let page;
  1114. try {
  1115. page = await Page.findByIdAndViewer(pageId, req.user);
  1116. if (page == null) {
  1117. throw new Error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden');
  1118. }
  1119. page = await crowi.pageService.revertDeletedPage(page, req.user, {}, isRecursively);
  1120. }
  1121. catch (err) {
  1122. if (err instanceof PathAlreadyExistsError) {
  1123. logger.error('Path already exists', err);
  1124. return res.json(ApiResponse.error(err, 'already_exists', err.targetPath));
  1125. }
  1126. logger.error('Error occured while get setting', err);
  1127. return res.json(ApiResponse.error(err));
  1128. }
  1129. const result = {};
  1130. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  1131. const parameters = {
  1132. targetModel: SupportedTargetModel.MODEL_PAGE,
  1133. target: page,
  1134. action: SupportedAction.ACTION_PAGE_REVERT,
  1135. };
  1136. activityEvent.emit('update', res.locals.activity._id, parameters, page);
  1137. return res.json(ApiResponse.success(result));
  1138. };
  1139. /**
  1140. * @swagger
  1141. *
  1142. * /pages.duplicate:
  1143. * post:
  1144. * tags: [Pages]
  1145. * operationId: duplicatePage
  1146. * summary: /pages.duplicate
  1147. * description: Duplicate page
  1148. * requestBody:
  1149. * content:
  1150. * application/json:
  1151. * schema:
  1152. * properties:
  1153. * page_id:
  1154. * $ref: '#/components/schemas/Page/properties/_id'
  1155. * new_path:
  1156. * $ref: '#/components/schemas/Page/properties/path'
  1157. * required:
  1158. * - page_id
  1159. * responses:
  1160. * 200:
  1161. * description: Succeeded to duplicate page.
  1162. * content:
  1163. * application/json:
  1164. * schema:
  1165. * properties:
  1166. * ok:
  1167. * $ref: '#/components/schemas/V1Response/properties/ok'
  1168. * page:
  1169. * $ref: '#/components/schemas/Page'
  1170. * tags:
  1171. * $ref: '#/components/schemas/Tags'
  1172. * 403:
  1173. * $ref: '#/components/responses/403'
  1174. * 500:
  1175. * $ref: '#/components/responses/500'
  1176. */
  1177. /**
  1178. * @api {post} /pages.duplicate Duplicate page
  1179. * @apiName DuplicatePage
  1180. * @apiGroup Page
  1181. *
  1182. * @apiParam {String} page_id Page Id.
  1183. * @apiParam {String} new_path New path name.
  1184. */
  1185. api.duplicate = async function(req, res) {
  1186. const pageId = req.body.page_id;
  1187. let newPagePath = pathUtils.normalizePath(req.body.new_path);
  1188. const page = await Page.findByIdAndViewer(pageId, req.user);
  1189. if (page == null) {
  1190. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  1191. }
  1192. // check whether path starts slash
  1193. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  1194. await page.populateDataToShowRevision();
  1195. const originTags = await page.findRelatedTagsById();
  1196. req.body.path = newPagePath;
  1197. req.body.body = page.revision.body;
  1198. req.body.grant = page.grant;
  1199. req.body.grantedUsers = page.grantedUsers;
  1200. req.body.grantUserGroupId = page.grantedGroup;
  1201. req.body.pageTags = originTags;
  1202. return api.create(req, res);
  1203. };
  1204. /**
  1205. * @api {post} /pages.unlink Remove the redirecting page
  1206. * @apiName UnlinkPage
  1207. * @apiGroup Page
  1208. *
  1209. * @apiParam {String} page_id Page Id.
  1210. * @apiParam {String} revision_id
  1211. */
  1212. api.unlink = async function(req, res) {
  1213. const path = req.body.path;
  1214. try {
  1215. await PageRedirect.removePageRedirectByToPath(path);
  1216. logger.debug('Redirect Page deleted', path);
  1217. }
  1218. catch (err) {
  1219. logger.error('Error occured while get setting', err);
  1220. return res.json(ApiResponse.error('Failed to delete redirect page.'));
  1221. }
  1222. const result = { path };
  1223. return res.json(ApiResponse.success(result));
  1224. };
  1225. return actions;
  1226. };