page.js 39 KB

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