page.js 40 KB

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