obsolete-page.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. import { templateChecker, pagePathUtils } from '@growi/core';
  2. import loggerFactory from '~/utils/logger';
  3. // disable no-return-await for model functions
  4. /* eslint-disable no-return-await */
  5. /* eslint-disable no-use-before-define */
  6. const debug = require('debug')('growi:models:page');
  7. const nodePath = require('path');
  8. const urljoin = require('url-join');
  9. const mongoose = require('mongoose');
  10. const differenceInYears = require('date-fns/differenceInYears');
  11. const { pathUtils } = require('growi-commons');
  12. const escapeStringRegexp = require('escape-string-regexp');
  13. const { isTopPage, isTrashPage } = pagePathUtils;
  14. const { checkTemplatePath } = templateChecker;
  15. const logger = loggerFactory('growi:models:page');
  16. const GRANT_PUBLIC = 1;
  17. const GRANT_RESTRICTED = 2;
  18. const GRANT_SPECIFIED = 3;
  19. const GRANT_OWNER = 4;
  20. const GRANT_USER_GROUP = 5;
  21. const PAGE_GRANT_ERROR = 1;
  22. const STATUS_PUBLISHED = 'published';
  23. const STATUS_DELETED = 'deleted';
  24. // schema definition has moved to page.ts
  25. const pageSchema = {
  26. statics: {},
  27. methods: {},
  28. };
  29. /**
  30. * return an array of ancestors paths that is extracted from specified pagePath
  31. * e.g.
  32. * when `pagePath` is `/foo/bar/baz`,
  33. * this method returns [`/foo/bar/baz`, `/foo/bar`, `/foo`, `/`]
  34. *
  35. * @param {string} pagePath
  36. * @return {string[]} ancestors paths
  37. */
  38. const extractToAncestorsPaths = (pagePath) => {
  39. const ancestorsPaths = [];
  40. let parentPath;
  41. while (parentPath !== '/') {
  42. parentPath = nodePath.dirname(parentPath || pagePath);
  43. ancestorsPaths.push(parentPath);
  44. }
  45. return ancestorsPaths;
  46. };
  47. /**
  48. * populate page (Query or Document) to show revision
  49. * @param {any} page Query or Document
  50. * @param {string} userPublicFields string to set to select
  51. */
  52. /* eslint-disable object-curly-newline, object-property-newline */
  53. const populateDataToShowRevision = (page, userPublicFields) => {
  54. return page
  55. .populate([
  56. { path: 'lastUpdateUser', model: 'User', select: userPublicFields },
  57. { path: 'creator', model: 'User', select: userPublicFields },
  58. { path: 'deleteUser', model: 'User', select: userPublicFields },
  59. { path: 'grantedGroup', model: 'UserGroup' },
  60. { path: 'revision', model: 'Revision', populate: {
  61. path: 'author', model: 'User', select: userPublicFields,
  62. } },
  63. ]);
  64. };
  65. /* eslint-enable object-curly-newline, object-property-newline */
  66. export class PageQueryBuilder {
  67. constructor(query) {
  68. this.query = query;
  69. }
  70. addConditionToExcludeTrashed() {
  71. this.query = this.query
  72. .and({
  73. $or: [
  74. { status: null },
  75. { status: STATUS_PUBLISHED },
  76. ],
  77. });
  78. return this;
  79. }
  80. addConditionToExcludeRedirect() {
  81. this.query = this.query.and({ redirectTo: null });
  82. return this;
  83. }
  84. /**
  85. * generate the query to find the pages '{path}/*' and '{path}' self.
  86. * If top page, return without doing anything.
  87. */
  88. addConditionToListWithDescendants(path, option) {
  89. // No request is set for the top page
  90. if (isTopPage(path)) {
  91. return this;
  92. }
  93. const pathNormalized = pathUtils.normalizePath(path);
  94. const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
  95. const startsPattern = escapeStringRegexp(pathWithTrailingSlash);
  96. this.query = this.query
  97. .and({
  98. $or: [
  99. { path: pathNormalized },
  100. { path: new RegExp(`^${startsPattern}`) },
  101. ],
  102. });
  103. return this;
  104. }
  105. /**
  106. * generate the query to find the pages '{path}/*' (exclude '{path}' self).
  107. * If top page, return without doing anything.
  108. */
  109. addConditionToListOnlyDescendants(path, option) {
  110. // No request is set for the top page
  111. if (isTopPage(path)) {
  112. return this;
  113. }
  114. const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
  115. const startsPattern = escapeStringRegexp(pathWithTrailingSlash);
  116. this.query = this.query
  117. .and({ path: new RegExp(`^${startsPattern}`) });
  118. return this;
  119. }
  120. /**
  121. * generate the query to find pages that start with `path`
  122. *
  123. * In normal case, returns '{path}/*' and '{path}' self.
  124. * If top page, return without doing anything.
  125. *
  126. * *option*
  127. * Left for backward compatibility
  128. */
  129. addConditionToListByStartWith(path, option) {
  130. // No request is set for the top page
  131. if (isTopPage(path)) {
  132. return this;
  133. }
  134. const startsPattern = escapeStringRegexp(path);
  135. this.query = this.query
  136. .and({ path: new RegExp(`^${startsPattern}`) });
  137. return this;
  138. }
  139. addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false) {
  140. const grantConditions = [
  141. { grant: null },
  142. { grant: GRANT_PUBLIC },
  143. ];
  144. if (showAnyoneKnowsLink) {
  145. grantConditions.push({ grant: GRANT_RESTRICTED });
  146. }
  147. if (showPagesRestrictedByOwner) {
  148. grantConditions.push(
  149. { grant: GRANT_SPECIFIED },
  150. { grant: GRANT_OWNER },
  151. );
  152. }
  153. else if (user != null) {
  154. grantConditions.push(
  155. { grant: GRANT_SPECIFIED, grantedUsers: user._id },
  156. { grant: GRANT_OWNER, grantedUsers: user._id },
  157. );
  158. }
  159. if (showPagesRestrictedByGroup) {
  160. grantConditions.push(
  161. { grant: GRANT_USER_GROUP },
  162. );
  163. }
  164. else if (userGroups != null && userGroups.length > 0) {
  165. grantConditions.push(
  166. { grant: GRANT_USER_GROUP, grantedGroup: { $in: userGroups } },
  167. );
  168. }
  169. this.query = this.query
  170. .and({
  171. $or: grantConditions,
  172. });
  173. return this;
  174. }
  175. addConditionToPagenate(offset, limit, sortOpt) {
  176. this.query = this.query
  177. .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
  178. return this;
  179. }
  180. addConditionAsNonRootPage() {
  181. this.query = this.query.and({ path: { $ne: '/' } });
  182. return this;
  183. }
  184. addConditionAsNotMigrated() {
  185. this.query = this.query
  186. .and({ parent: null });
  187. return this;
  188. }
  189. addConditionAsMigrated() {
  190. this.query = this.query
  191. .and(
  192. {
  193. $or: [
  194. { parent: { $ne: null } },
  195. { path: '/' },
  196. ],
  197. },
  198. );
  199. return this;
  200. }
  201. /*
  202. * Add this condition when get any ancestor pages including the target's parent
  203. */
  204. addConditionToSortPagesByDescPath() {
  205. this.query = this.query.sort('-path');
  206. return this;
  207. }
  208. addConditionToSortPagesByAscPath() {
  209. this.query = this.query.sort('path');
  210. return this;
  211. }
  212. addConditionToMinimizeDataForRendering() {
  213. this.query = this.query.select('_id path isEmpty grant revision');
  214. return this;
  215. }
  216. addConditionToListByPathsArray(paths) {
  217. this.query = this.query
  218. .and({
  219. path: {
  220. $in: paths,
  221. },
  222. });
  223. return this;
  224. }
  225. addConditionToListByPageIdsArray(pageIds) {
  226. this.query = this.query
  227. .and({
  228. _id: {
  229. $in: pageIds,
  230. },
  231. });
  232. return this;
  233. }
  234. populateDataToList(userPublicFields) {
  235. this.query = this.query
  236. .populate({
  237. path: 'lastUpdateUser',
  238. select: userPublicFields,
  239. });
  240. return this;
  241. }
  242. populateDataToShowRevision(userPublicFields) {
  243. this.query = populateDataToShowRevision(this.query, userPublicFields);
  244. return this;
  245. }
  246. }
  247. export const getPageSchema = (crowi) => {
  248. let pageEvent;
  249. // init event
  250. if (crowi != null) {
  251. pageEvent = crowi.event('page');
  252. pageEvent.on('create', pageEvent.onCreate);
  253. pageEvent.on('update', pageEvent.onUpdate);
  254. pageEvent.on('createMany', pageEvent.onCreateMany);
  255. pageEvent.on('addSeenUsers', pageEvent.onAddSeenUsers);
  256. }
  257. function validateCrowi() {
  258. if (crowi == null) {
  259. throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
  260. }
  261. }
  262. pageSchema.methods.isDeleted = function() {
  263. return (this.status === STATUS_DELETED) || isTrashPage(this.path);
  264. };
  265. pageSchema.methods.isPublic = function() {
  266. if (!this.grant || this.grant === GRANT_PUBLIC) {
  267. return true;
  268. }
  269. return false;
  270. };
  271. pageSchema.methods.isTopPage = function() {
  272. return isTopPage(this.path);
  273. };
  274. pageSchema.methods.isTemplate = function() {
  275. return checkTemplatePath(this.path);
  276. };
  277. pageSchema.methods.isLatestRevision = function() {
  278. // populate されていなくて判断できない
  279. if (!this.latestRevision || !this.revision) {
  280. return true;
  281. }
  282. // comparing ObjectId with string
  283. // eslint-disable-next-line eqeqeq
  284. return (this.latestRevision == this.revision._id.toString());
  285. };
  286. pageSchema.methods.findRelatedTagsById = async function() {
  287. const PageTagRelation = mongoose.model('PageTagRelation');
  288. const relations = await PageTagRelation.find({ relatedPage: this._id }).populate('relatedTag');
  289. return relations.map((relation) => { return relation.relatedTag.name });
  290. };
  291. pageSchema.methods.isUpdatable = function(previousRevision) {
  292. const revision = this.latestRevision || this.revision;
  293. // comparing ObjectId with string
  294. // eslint-disable-next-line eqeqeq
  295. if (revision != previousRevision) {
  296. return false;
  297. }
  298. return true;
  299. };
  300. pageSchema.methods.isLiked = function(user) {
  301. if (user == null || user._id == null) {
  302. return false;
  303. }
  304. return this.liker.some((likedUserId) => {
  305. return likedUserId.toString() === user._id.toString();
  306. });
  307. };
  308. pageSchema.methods.like = function(userData) {
  309. const self = this;
  310. return new Promise(((resolve, reject) => {
  311. const added = self.liker.addToSet(userData._id);
  312. if (added.length > 0) {
  313. self.save((err, data) => {
  314. if (err) {
  315. return reject(err);
  316. }
  317. logger.debug('liker updated!', added);
  318. return resolve(data);
  319. });
  320. }
  321. else {
  322. logger.debug('liker not updated');
  323. return reject(new Error('Already liked'));
  324. }
  325. }));
  326. };
  327. pageSchema.methods.unlike = function(userData, callback) {
  328. const self = this;
  329. return new Promise(((resolve, reject) => {
  330. const beforeCount = self.liker.length;
  331. self.liker.pull(userData._id);
  332. if (self.liker.length !== beforeCount) {
  333. self.save((err, data) => {
  334. if (err) {
  335. return reject(err);
  336. }
  337. return resolve(data);
  338. });
  339. }
  340. else {
  341. logger.debug('liker not updated');
  342. return reject(new Error('Already unliked'));
  343. }
  344. }));
  345. };
  346. pageSchema.methods.isSeenUser = function(userData) {
  347. return this.seenUsers.includes(userData._id);
  348. };
  349. pageSchema.methods.seen = async function(userData) {
  350. if (this.isSeenUser(userData)) {
  351. debug('seenUsers not updated');
  352. return this;
  353. }
  354. if (!userData || !userData._id) {
  355. throw new Error('User data is not valid');
  356. }
  357. const added = this.seenUsers.addToSet(userData._id);
  358. const saved = await this.save();
  359. debug('seenUsers updated!', added);
  360. pageEvent.emit('addSeenUsers', saved);
  361. return saved;
  362. };
  363. pageSchema.methods.updateSlackChannels = function(slackChannels) {
  364. this.slackChannels = slackChannels;
  365. return this.save();
  366. };
  367. pageSchema.methods.initLatestRevisionField = async function(revisionId) {
  368. this.latestRevision = this.revision;
  369. if (revisionId != null) {
  370. this.revision = revisionId;
  371. }
  372. };
  373. pageSchema.methods.populateDataToShowRevision = async function() {
  374. validateCrowi();
  375. const User = crowi.model('User');
  376. return populateDataToShowRevision(this, User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
  377. };
  378. pageSchema.methods.populateDataToMakePresentation = async function(revisionId) {
  379. this.latestRevision = this.revision;
  380. if (revisionId != null) {
  381. this.revision = revisionId;
  382. }
  383. return this.populate('revision');
  384. };
  385. pageSchema.methods.applyScope = function(user, grant, grantUserGroupId) {
  386. // reset
  387. this.grantedUsers = [];
  388. this.grantedGroup = null;
  389. this.grant = grant || GRANT_PUBLIC;
  390. if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP) {
  391. this.grantedUsers.push(user._id);
  392. }
  393. if (grant === GRANT_USER_GROUP) {
  394. this.grantedGroup = grantUserGroupId;
  395. }
  396. };
  397. pageSchema.methods.getContentAge = function() {
  398. return differenceInYears(new Date(), this.updatedAt);
  399. };
  400. pageSchema.statics.updateCommentCount = function(pageId) {
  401. validateCrowi();
  402. const self = this;
  403. const Comment = crowi.model('Comment');
  404. return Comment.countCommentByPageId(pageId)
  405. .then((count) => {
  406. self.update({ _id: pageId }, { commentCount: count }, {}, (err, data) => {
  407. if (err) {
  408. debug('Update commentCount Error', err);
  409. throw err;
  410. }
  411. return data;
  412. });
  413. });
  414. };
  415. pageSchema.statics.getGrantLabels = function() {
  416. const grantLabels = {};
  417. grantLabels[GRANT_PUBLIC] = 'Public'; // 公開
  418. grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
  419. // grantLabels[GRANT_SPECIFIED] = 'Specified users only'; // 特定ユーザーのみ
  420. grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
  421. grantLabels[GRANT_OWNER] = 'Only me'; // 自分のみ
  422. return grantLabels;
  423. };
  424. pageSchema.statics.getUserPagePath = function(user) {
  425. return `/user/${user.username}`;
  426. };
  427. pageSchema.statics.getDeletedPageName = function(path) {
  428. if (path.match('/')) {
  429. // eslint-disable-next-line no-param-reassign
  430. path = path.substr(1);
  431. }
  432. return `/trash/${path}`;
  433. };
  434. pageSchema.statics.getRevertDeletedPageName = function(path) {
  435. return path.replace('/trash', '');
  436. };
  437. pageSchema.statics.isDeletableName = function(path) {
  438. const notDeletable = [
  439. /^\/user\/[^/]+$/, // user page
  440. ];
  441. for (let i = 0; i < notDeletable.length; i++) {
  442. const pattern = notDeletable[i];
  443. if (path.match(pattern)) {
  444. return false;
  445. }
  446. }
  447. return true;
  448. };
  449. pageSchema.statics.fixToCreatableName = function(path) {
  450. return path
  451. .replace(/\/\//g, '/');
  452. };
  453. pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
  454. this.update({ _id: pageId }, { revision: revisionId }, {}, (err, data) => {
  455. cb(err, data);
  456. });
  457. };
  458. /**
  459. * return whether the user is accessible to the page
  460. * @param {string} id ObjectId
  461. * @param {User} user
  462. */
  463. pageSchema.statics.isAccessiblePageByViewer = async function(id, user) {
  464. const baseQuery = this.count({ _id: id });
  465. let userGroups = [];
  466. if (user != null) {
  467. validateCrowi();
  468. const UserGroupRelation = crowi.model('UserGroupRelation');
  469. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  470. }
  471. const queryBuilder = new PageQueryBuilder(baseQuery);
  472. queryBuilder.addConditionToFilteringByViewer(user, userGroups, true);
  473. const count = await queryBuilder.query.exec();
  474. return count > 0;
  475. };
  476. /**
  477. * @param {string} id ObjectId
  478. * @param {User} user User instance
  479. * @param {UserGroup[]} userGroups List of UserGroup instances
  480. */
  481. pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups) {
  482. const baseQuery = this.findOne({ _id: id });
  483. let relatedUserGroups = userGroups;
  484. if (user != null && relatedUserGroups == null) {
  485. validateCrowi();
  486. const UserGroupRelation = crowi.model('UserGroupRelation');
  487. relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  488. }
  489. const queryBuilder = new PageQueryBuilder(baseQuery);
  490. queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
  491. return await queryBuilder.query.exec();
  492. };
  493. // find page by path
  494. pageSchema.statics.findByPath = function(path) {
  495. if (path == null) {
  496. return null;
  497. }
  498. return this.findOne({ path });
  499. };
  500. /**
  501. * @param {string} path Page path
  502. * @param {User} user User instance
  503. * @param {UserGroup[]} userGroups List of UserGroup instances
  504. */
  505. pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups) {
  506. if (path == null) {
  507. throw new Error('path is required.');
  508. }
  509. if (path === '/') {
  510. return null;
  511. }
  512. const ancestorsPaths = extractToAncestorsPaths(path);
  513. // pick the longest one
  514. const baseQuery = this.findOne({ path: { $in: ancestorsPaths } }).sort({ path: -1 });
  515. let relatedUserGroups = userGroups;
  516. if (user != null && relatedUserGroups == null) {
  517. validateCrowi();
  518. const UserGroupRelation = crowi.model('UserGroupRelation');
  519. relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  520. }
  521. const queryBuilder = new PageQueryBuilder(baseQuery);
  522. queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
  523. return await queryBuilder.query.exec();
  524. };
  525. pageSchema.statics.findByRedirectTo = function(path) {
  526. return this.findOne({ redirectTo: path });
  527. };
  528. /**
  529. * find pages that is match with `path` and its descendants
  530. */
  531. pageSchema.statics.findListWithDescendants = async function(path, user, option = {}) {
  532. const builder = new PageQueryBuilder(this.find());
  533. builder.addConditionToListWithDescendants(path, option);
  534. return await findListFromBuilderAndViewer(builder, user, false, option);
  535. };
  536. /**
  537. * find pages that is match with `path` and its descendants whitch user is able to manage
  538. */
  539. pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}) {
  540. if (user == null) {
  541. return null;
  542. }
  543. const builder = new PageQueryBuilder(this.find());
  544. builder.addConditionToListWithDescendants(page.path, option);
  545. builder.addConditionToExcludeRedirect();
  546. // add grant conditions
  547. await addConditionToFilteringByViewerToEdit(builder, user);
  548. const { pages } = await findListFromBuilderAndViewer(builder, user, false, option);
  549. // add page if 'grant' is GRANT_RESTRICTED
  550. // because addConditionToListWithDescendants excludes GRANT_RESTRICTED pages
  551. if (page.grant === GRANT_RESTRICTED) {
  552. pages.push(page);
  553. }
  554. return pages;
  555. };
  556. /**
  557. * find pages that start with `path`
  558. */
  559. pageSchema.statics.findListByStartWith = async function(path, user, option) {
  560. const builder = new PageQueryBuilder(this.find());
  561. builder.addConditionToListByStartWith(path, option);
  562. return await findListFromBuilderAndViewer(builder, user, false, option);
  563. };
  564. /**
  565. * find pages that is created by targetUser
  566. *
  567. * @param {User} targetUser
  568. * @param {User} currentUser
  569. * @param {any} option
  570. */
  571. pageSchema.statics.findListByCreator = async function(targetUser, currentUser, option) {
  572. const opt = Object.assign({ sort: 'createdAt', desc: -1 }, option);
  573. const builder = new PageQueryBuilder(this.find({ creator: targetUser._id }));
  574. let showAnyoneKnowsLink = null;
  575. if (targetUser != null && currentUser != null) {
  576. showAnyoneKnowsLink = targetUser._id.equals(currentUser._id);
  577. }
  578. return await findListFromBuilderAndViewer(builder, currentUser, showAnyoneKnowsLink, opt);
  579. };
  580. pageSchema.statics.findListByPageIds = async function(ids, option, excludeRedirect = true) {
  581. const User = crowi.model('User');
  582. const opt = Object.assign({}, option);
  583. const builder = new PageQueryBuilder(this.find({ _id: { $in: ids } }));
  584. if (excludeRedirect) {
  585. builder.addConditionToExcludeRedirect();
  586. }
  587. builder.addConditionToPagenate(opt.offset, opt.limit);
  588. // count
  589. const totalCount = await builder.query.exec('count');
  590. // find
  591. builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
  592. const pages = await builder.query.clone().exec('find');
  593. const result = {
  594. pages, totalCount, offset: opt.offset, limit: opt.limit,
  595. };
  596. return result;
  597. };
  598. /**
  599. * find pages by PageQueryBuilder
  600. * @param {PageQueryBuilder} builder
  601. * @param {User} user
  602. * @param {boolean} showAnyoneKnowsLink
  603. * @param {any} option
  604. */
  605. async function findListFromBuilderAndViewer(builder, user, showAnyoneKnowsLink, option) {
  606. validateCrowi();
  607. const User = crowi.model('User');
  608. const opt = Object.assign({ sort: 'updatedAt', desc: -1 }, option);
  609. const sortOpt = {};
  610. sortOpt[opt.sort] = opt.desc;
  611. // exclude trashed pages
  612. if (!opt.includeTrashed) {
  613. builder.addConditionToExcludeTrashed();
  614. }
  615. // exclude redirect pages
  616. if (!opt.includeRedirect) {
  617. builder.addConditionToExcludeRedirect();
  618. }
  619. // add grant conditions
  620. await addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink);
  621. // count
  622. const totalCount = await builder.query.exec('count');
  623. // find
  624. builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
  625. builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
  626. const pages = await builder.query.lean().clone().exec('find');
  627. const result = {
  628. pages, totalCount, offset: opt.offset, limit: opt.limit,
  629. };
  630. return result;
  631. }
  632. /**
  633. * Add condition that filter pages by viewer
  634. * by considering Config
  635. *
  636. * @param {PageQueryBuilder} builder
  637. * @param {User} user
  638. * @param {boolean} showAnyoneKnowsLink
  639. */
  640. async function addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink) {
  641. validateCrowi();
  642. // determine User condition
  643. const hidePagesRestrictedByOwner = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
  644. const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
  645. // determine UserGroup condition
  646. let userGroups = null;
  647. if (user != null) {
  648. const UserGroupRelation = crowi.model('UserGroupRelation');
  649. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  650. }
  651. return builder.addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
  652. }
  653. /**
  654. * Add condition that filter pages by viewer
  655. * by considering Config
  656. *
  657. * @param {PageQueryBuilder} builder
  658. * @param {User} user
  659. * @param {boolean} showAnyoneKnowsLink
  660. */
  661. async function addConditionToFilteringByViewerToEdit(builder, user) {
  662. validateCrowi();
  663. // determine UserGroup condition
  664. let userGroups = null;
  665. if (user != null) {
  666. const UserGroupRelation = crowi.model('UserGroupRelation');
  667. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  668. }
  669. return builder.addConditionToFilteringByViewer(user, userGroups, false, false, false);
  670. }
  671. /**
  672. * export addConditionToFilteringByViewerForList as static method
  673. */
  674. pageSchema.statics.addConditionToFilteringByViewerForList = addConditionToFilteringByViewerForList;
  675. /**
  676. * export addConditionToFilteringByViewerToEdit as static method
  677. */
  678. pageSchema.statics.addConditionToFilteringByViewerToEdit = addConditionToFilteringByViewerToEdit;
  679. /**
  680. * Throw error for growi-lsx-plugin (v1.x)
  681. */
  682. pageSchema.statics.generateQueryToListByStartWith = function(path, user, option) {
  683. const dummyQuery = this.find();
  684. dummyQuery.exec = async() => {
  685. throw new Error('Plugin version mismatch. Upgrade growi-lsx-plugin to v2.0.0 or above.');
  686. };
  687. return dummyQuery;
  688. };
  689. pageSchema.statics.generateQueryToListWithDescendants = pageSchema.statics.generateQueryToListByStartWith;
  690. /**
  691. * find all templates applicable to the new page
  692. */
  693. pageSchema.statics.findTemplate = async function(path) {
  694. const templatePath = nodePath.posix.dirname(path);
  695. const pathList = generatePathsOnTree(path, []);
  696. const regexpList = pathList.map((path) => {
  697. const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
  698. return new RegExp(`^${escapeStringRegexp(pathWithTrailingSlash)}_{1,2}template$`);
  699. });
  700. const templatePages = await this.find({ path: { $in: regexpList } })
  701. .populate({ path: 'revision', model: 'Revision' })
  702. .exec();
  703. return fetchTemplate(templatePages, templatePath);
  704. };
  705. const generatePathsOnTree = (path, pathList) => {
  706. pathList.push(path);
  707. if (path === '/') {
  708. return pathList;
  709. }
  710. const newPath = nodePath.posix.dirname(path);
  711. return generatePathsOnTree(newPath, pathList);
  712. };
  713. const assignTemplateByType = (templates, path, type) => {
  714. const targetTemplatePath = urljoin(path, `${type}template`);
  715. return templates.find((template) => {
  716. return (template.path === targetTemplatePath);
  717. });
  718. };
  719. const assignDecendantsTemplate = (decendantsTemplates, path) => {
  720. const decendantsTemplate = assignTemplateByType(decendantsTemplates, path, '__');
  721. if (decendantsTemplate) {
  722. return decendantsTemplate;
  723. }
  724. if (path === '/') {
  725. return;
  726. }
  727. const newPath = nodePath.posix.dirname(path);
  728. return assignDecendantsTemplate(decendantsTemplates, newPath);
  729. };
  730. const fetchTemplate = async(templates, templatePath) => {
  731. let templateBody;
  732. let templateTags;
  733. /**
  734. * get children template
  735. * __tempate: applicable only to immediate decendants
  736. */
  737. const childrenTemplate = assignTemplateByType(templates, templatePath, '_');
  738. /**
  739. * get decendants templates
  740. * _tempate: applicable to all pages under
  741. */
  742. const decendantsTemplate = assignDecendantsTemplate(templates, templatePath);
  743. if (childrenTemplate) {
  744. templateBody = childrenTemplate.revision.body;
  745. templateTags = await childrenTemplate.findRelatedTagsById();
  746. }
  747. else if (decendantsTemplate) {
  748. templateBody = decendantsTemplate.revision.body;
  749. templateTags = await decendantsTemplate.findRelatedTagsById();
  750. }
  751. return { templateBody, templateTags };
  752. };
  753. async function pushRevision(pageData, newRevision, user) {
  754. await newRevision.save();
  755. debug('Successfully saved new revision', newRevision);
  756. pageData.revision = newRevision;
  757. pageData.lastUpdateUser = user;
  758. pageData.updatedAt = Date.now();
  759. return pageData.save();
  760. }
  761. async function validateAppliedScope(user, grant, grantUserGroupId) {
  762. if (grant === GRANT_USER_GROUP && grantUserGroupId == null) {
  763. throw new Error('grant userGroupId is not specified');
  764. }
  765. if (grant === GRANT_USER_GROUP) {
  766. const UserGroupRelation = crowi.model('UserGroupRelation');
  767. const count = await UserGroupRelation.countByGroupIdAndUser(grantUserGroupId, user);
  768. if (count === 0) {
  769. throw new Error('no relations were exist for group and user.');
  770. }
  771. }
  772. }
  773. pageSchema.statics.create = async function(path, body, user, options = {}) {
  774. validateCrowi();
  775. const Page = this;
  776. const Revision = crowi.model('Revision');
  777. const {
  778. format = 'markdown', redirectTo, grantUserGroupId, parentId,
  779. } = options;
  780. // sanitize path
  781. path = crowi.xss.process(path); // eslint-disable-line no-param-reassign
  782. let grant = options.grant;
  783. // force public
  784. if (isTopPage(path)) {
  785. grant = GRANT_PUBLIC;
  786. }
  787. const isExist = await this.count({ path, isEmpty: false }); // not validate empty page
  788. if (isExist) {
  789. throw new Error('Cannot create new page to existed path');
  790. }
  791. /*
  792. * update empty page if exists, if not, create a new page
  793. */
  794. let page;
  795. const emptyPage = await Page.findOne({ path, isEmpty: true });
  796. if (emptyPage != null) {
  797. page = emptyPage;
  798. page.isEmpty = false;
  799. }
  800. else {
  801. page = new Page();
  802. }
  803. const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  804. let parent = parentId;
  805. if (isV5Compatible && parent == null && !isTopPage(path)) {
  806. parent = await Page.getParentIdAndFillAncestors(path);
  807. }
  808. page.path = path;
  809. page.creator = user;
  810. page.lastUpdateUser = user;
  811. page.redirectTo = redirectTo;
  812. page.status = STATUS_PUBLISHED;
  813. page.parent = parent;
  814. await validateAppliedScope(user, grant, grantUserGroupId);
  815. page.applyScope(user, grant, grantUserGroupId);
  816. let savedPage = await page.save();
  817. const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
  818. const revision = await pushRevision(savedPage, newRevision, user);
  819. savedPage = await this.findByPath(revision.path);
  820. await savedPage.populateDataToShowRevision();
  821. pageEvent.emit('create', savedPage, user);
  822. return savedPage;
  823. };
  824. pageSchema.statics.updatePage = async function(pageData, body, previousBody, user, options = {}) {
  825. validateCrowi();
  826. const Revision = crowi.model('Revision');
  827. const grant = options.grant || pageData.grant; // use the previous data if absence
  828. const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
  829. const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
  830. await validateAppliedScope(user, grant, grantUserGroupId);
  831. pageData.applyScope(user, grant, grantUserGroupId);
  832. // update existing page
  833. let savedPage = await pageData.save();
  834. const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
  835. const revision = await pushRevision(savedPage, newRevision, user);
  836. savedPage = await this.findByPath(revision.path);
  837. await savedPage.populateDataToShowRevision();
  838. if (isSyncRevisionToHackmd) {
  839. savedPage = await this.syncRevisionToHackmd(savedPage);
  840. }
  841. pageEvent.emit('update', savedPage, user);
  842. return savedPage;
  843. };
  844. pageSchema.statics.applyScopesToDescendantsAsyncronously = async function(parentPage, user) {
  845. const builder = new PageQueryBuilder(this.find());
  846. builder.addConditionToListWithDescendants(parentPage.path);
  847. builder.addConditionToExcludeRedirect();
  848. // add grant conditions
  849. await addConditionToFilteringByViewerToEdit(builder, user);
  850. // get all pages that the specified user can update
  851. const pages = await builder.query.exec();
  852. for (const page of pages) {
  853. // skip parentPage
  854. if (page.id === parentPage.id) {
  855. continue;
  856. }
  857. page.applyScope(user, parentPage.grant, parentPage.grantedGroup);
  858. page.save();
  859. }
  860. };
  861. pageSchema.statics.removeByPath = function(path) {
  862. if (path == null) {
  863. throw new Error('path is required');
  864. }
  865. return this.findOneAndRemove({ path }).exec();
  866. };
  867. /**
  868. * remove the page that is redirecting to specified `pagePath` recursively
  869. * ex: when
  870. * '/page1' redirects to '/page2' and
  871. * '/page2' redirects to '/page3'
  872. * and given '/page3',
  873. * '/page1' and '/page2' will be removed
  874. *
  875. * @param {string} pagePath
  876. */
  877. pageSchema.statics.removeRedirectOriginPageByPath = async function(pagePath) {
  878. const redirectPage = await this.findByRedirectTo(pagePath);
  879. if (redirectPage == null) {
  880. return;
  881. }
  882. // remove
  883. await this.findByIdAndRemove(redirectPage.id);
  884. // remove recursive
  885. await this.removeRedirectOriginPageByPath(redirectPage.path);
  886. };
  887. pageSchema.statics.findListByPathsArray = async function(paths) {
  888. const queryBuilder = new PageQueryBuilder(this.find());
  889. queryBuilder.addConditionToListByPathsArray(paths);
  890. return await queryBuilder.query.exec();
  891. };
  892. pageSchema.statics.publicizePage = async function(page) {
  893. page.grantedGroup = null;
  894. page.grant = GRANT_PUBLIC;
  895. await page.save();
  896. };
  897. pageSchema.statics.transferPageToGroup = async function(page, transferToUserGroupId) {
  898. const UserGroup = mongoose.model('UserGroup');
  899. // check page existence
  900. const isExist = await UserGroup.count({ _id: transferToUserGroupId }) > 0;
  901. if (isExist) {
  902. page.grantedGroup = transferToUserGroupId;
  903. await page.save();
  904. }
  905. else {
  906. throw new Error('Cannot find the group to which private pages belong to. _id: ', transferToUserGroupId);
  907. }
  908. };
  909. /**
  910. * associate GROWI page and HackMD page
  911. * @param {Page} pageData
  912. * @param {string} pageIdOnHackmd
  913. */
  914. pageSchema.statics.registerHackmdPage = function(pageData, pageIdOnHackmd) {
  915. pageData.pageIdOnHackmd = pageIdOnHackmd;
  916. return this.syncRevisionToHackmd(pageData);
  917. };
  918. /**
  919. * update revisionHackmdSynced
  920. * @param {Page} pageData
  921. * @param {bool} isSave whether save or not
  922. */
  923. pageSchema.statics.syncRevisionToHackmd = function(pageData, isSave = true) {
  924. pageData.revisionHackmdSynced = pageData.revision;
  925. pageData.hasDraftOnHackmd = false;
  926. let returnData = pageData;
  927. if (isSave) {
  928. returnData = pageData.save();
  929. }
  930. return returnData;
  931. };
  932. /**
  933. * update hasDraftOnHackmd
  934. * !! This will be invoked many time from many people !!
  935. *
  936. * @param {Page} pageData
  937. * @param {Boolean} newValue
  938. */
  939. pageSchema.statics.updateHasDraftOnHackmd = async function(pageData, newValue) {
  940. if (pageData.hasDraftOnHackmd === newValue) {
  941. // do nothing when hasDraftOnHackmd equals to newValue
  942. return;
  943. }
  944. pageData.hasDraftOnHackmd = newValue;
  945. return pageData.save();
  946. };
  947. pageSchema.statics.getHistories = function() {
  948. // TODO
  949. };
  950. pageSchema.statics.STATUS_PUBLISHED = STATUS_PUBLISHED;
  951. pageSchema.statics.STATUS_DELETED = STATUS_DELETED;
  952. pageSchema.statics.GRANT_PUBLIC = GRANT_PUBLIC;
  953. pageSchema.statics.GRANT_RESTRICTED = GRANT_RESTRICTED;
  954. pageSchema.statics.GRANT_SPECIFIED = GRANT_SPECIFIED;
  955. pageSchema.statics.GRANT_OWNER = GRANT_OWNER;
  956. pageSchema.statics.GRANT_USER_GROUP = GRANT_USER_GROUP;
  957. pageSchema.statics.PAGE_GRANT_ERROR = PAGE_GRANT_ERROR;
  958. pageSchema.statics.PageQueryBuilder = PageQueryBuilder;
  959. return pageSchema;
  960. };