page.js 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397
  1. // disable no-return-await for model functions
  2. /* eslint-disable no-return-await */
  3. /* eslint-disable no-use-before-define */
  4. const logger = require('@alias/logger')('growi:models:page');
  5. const debug = require('debug')('growi:models:page');
  6. const nodePath = require('path');
  7. const urljoin = require('url-join');
  8. const mongoose = require('mongoose');
  9. const mongoosePaginate = require('mongoose-paginate-v2');
  10. const uniqueValidator = require('mongoose-unique-validator');
  11. const differenceInYears = require('date-fns/differenceInYears');
  12. const { pathUtils } = require('growi-commons');
  13. const templateChecker = require('@commons/util/template-checker');
  14. const { isTopPage } = require('@commons/util/path-utils');
  15. const escapeStringRegexp = require('escape-string-regexp');
  16. const ObjectId = mongoose.Schema.Types.ObjectId;
  17. /*
  18. * define schema
  19. */
  20. const GRANT_PUBLIC = 1;
  21. const GRANT_RESTRICTED = 2;
  22. const GRANT_SPECIFIED = 3;
  23. const GRANT_OWNER = 4;
  24. const GRANT_USER_GROUP = 5;
  25. const PAGE_GRANT_ERROR = 1;
  26. const STATUS_PUBLISHED = 'published';
  27. const STATUS_DELETED = 'deleted';
  28. const pageSchema = new mongoose.Schema({
  29. path: {
  30. type: String, required: true, index: true, unique: true,
  31. },
  32. revision: { type: ObjectId, ref: 'Revision' },
  33. redirectTo: { type: String, index: true },
  34. status: { type: String, default: STATUS_PUBLISHED, index: true },
  35. grant: { type: Number, default: GRANT_PUBLIC, index: true },
  36. grantedUsers: [{ type: ObjectId, ref: 'User' }],
  37. grantedGroup: { type: ObjectId, ref: 'UserGroup', index: true },
  38. creator: { type: ObjectId, ref: 'User', index: true },
  39. lastUpdateUser: { type: ObjectId, ref: 'User' },
  40. liker: [{ type: ObjectId, ref: 'User' }],
  41. seenUsers: [{ type: ObjectId, ref: 'User' }],
  42. commentCount: { type: Number, default: 0 },
  43. extended: {
  44. type: String,
  45. default: '{}',
  46. get(data) {
  47. try {
  48. return JSON.parse(data);
  49. }
  50. catch (e) {
  51. return data;
  52. }
  53. },
  54. set(data) {
  55. return JSON.stringify(data);
  56. },
  57. },
  58. pageIdOnHackmd: String,
  59. revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
  60. hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
  61. createdAt: { type: Date, default: Date.now },
  62. updatedAt: { type: Date, default: Date.now },
  63. }, {
  64. toJSON: { getters: true },
  65. toObject: { getters: true },
  66. });
  67. // apply plugins
  68. pageSchema.plugin(mongoosePaginate);
  69. pageSchema.plugin(uniqueValidator);
  70. /**
  71. * return an array of ancestors paths that is extracted from specified pagePath
  72. * e.g.
  73. * when `pagePath` is `/foo/bar/baz`,
  74. * this method returns [`/foo/bar/baz`, `/foo/bar`, `/foo`, `/`]
  75. *
  76. * @param {string} pagePath
  77. * @return {string[]} ancestors paths
  78. */
  79. const extractToAncestorsPaths = (pagePath) => {
  80. const ancestorsPaths = [];
  81. let parentPath;
  82. while (parentPath !== '/') {
  83. parentPath = nodePath.dirname(parentPath || pagePath);
  84. ancestorsPaths.push(parentPath);
  85. }
  86. return ancestorsPaths;
  87. };
  88. const addSlashOfEnd = (path) => {
  89. let returnPath = path;
  90. if (!path.match(/\/$/)) {
  91. returnPath += '/';
  92. }
  93. return returnPath;
  94. };
  95. /**
  96. * populate page (Query or Document) to show revision
  97. * @param {any} page Query or Document
  98. * @param {string} userPublicFields string to set to select
  99. */
  100. /* eslint-disable object-curly-newline, object-property-newline */
  101. const populateDataToShowRevision = (page, userPublicFields, imagePopulation) => {
  102. return page
  103. .populate([
  104. { path: 'lastUpdateUser', model: 'User', select: userPublicFields, populate: imagePopulation },
  105. { path: 'creator', model: 'User', select: userPublicFields, populate: imagePopulation },
  106. { path: 'grantedGroup', model: 'UserGroup' },
  107. { path: 'revision', model: 'Revision', populate: {
  108. path: 'author', model: 'User', select: userPublicFields, populate: imagePopulation,
  109. } },
  110. ]);
  111. };
  112. /* eslint-enable object-curly-newline, object-property-newline */
  113. class PageQueryBuilder {
  114. constructor(query) {
  115. this.query = query;
  116. }
  117. addConditionToExcludeTrashed() {
  118. this.query = this.query
  119. .and({
  120. $or: [
  121. { status: null },
  122. { status: STATUS_PUBLISHED },
  123. ],
  124. });
  125. return this;
  126. }
  127. addConditionToExcludeRedirect() {
  128. this.query = this.query.and({ redirectTo: null });
  129. return this;
  130. }
  131. /**
  132. * generate the query to find the page that is match with `path` and its descendants
  133. */
  134. addConditionToListWithDescendants(path, option) {
  135. // ignore other pages than descendants
  136. // eslint-disable-next-line no-param-reassign
  137. path = addSlashOfEnd(path);
  138. this.addConditionToListByStartWith(path, option);
  139. return this;
  140. }
  141. /**
  142. * generate the query to find pages that start with `path`
  143. *
  144. * In normal case, returns '{path}/*' and '{path}' self.
  145. * If top page, return without doing anything.
  146. *
  147. * *option*
  148. * - isRegExpEscapedFromPath -- if true, the regex strings included in `path` is escaped (default: false)
  149. */
  150. addConditionToListByStartWith(path, option) {
  151. // No request is set for the top page
  152. if (isTopPage(path)) {
  153. return this;
  154. }
  155. const pathCondition = [];
  156. const isRegExpEscapedFromPath = option.isRegExpEscapedFromPath || false;
  157. /*
  158. * 1. add condition for finding the page completely match with `path` w/o last slash
  159. */
  160. let pathSlashOmitted = path;
  161. if (path.match(/\/$/)) {
  162. pathSlashOmitted = path.substr(0, path.length - 1);
  163. pathCondition.push({ path: pathSlashOmitted });
  164. }
  165. /*
  166. * 2. add decendants
  167. */
  168. const pattern = (isRegExpEscapedFromPath)
  169. ? escapeStringRegexp(path) // escape
  170. : pathSlashOmitted;
  171. let queryReg;
  172. try {
  173. queryReg = new RegExp(`^${pattern}`);
  174. }
  175. // if regular expression is invalid
  176. catch (e) {
  177. // force to escape
  178. queryReg = new RegExp(`^${escapeStringRegexp(pattern)}`);
  179. }
  180. pathCondition.push({ path: queryReg });
  181. this.query = this.query
  182. .and({
  183. $or: pathCondition,
  184. });
  185. return this;
  186. }
  187. addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false) {
  188. const grantConditions = [
  189. { grant: null },
  190. { grant: GRANT_PUBLIC },
  191. ];
  192. if (showAnyoneKnowsLink) {
  193. grantConditions.push({ grant: GRANT_RESTRICTED });
  194. }
  195. if (showPagesRestrictedByOwner) {
  196. grantConditions.push(
  197. { grant: GRANT_SPECIFIED },
  198. { grant: GRANT_OWNER },
  199. );
  200. }
  201. else if (user != null) {
  202. grantConditions.push(
  203. { grant: GRANT_SPECIFIED, grantedUsers: user._id },
  204. { grant: GRANT_OWNER, grantedUsers: user._id },
  205. );
  206. }
  207. if (showPagesRestrictedByGroup) {
  208. grantConditions.push(
  209. { grant: GRANT_USER_GROUP },
  210. );
  211. }
  212. else if (userGroups != null && userGroups.length > 0) {
  213. grantConditions.push(
  214. { grant: GRANT_USER_GROUP, grantedGroup: { $in: userGroups } },
  215. );
  216. }
  217. this.query = this.query
  218. .and({
  219. $or: grantConditions,
  220. });
  221. return this;
  222. }
  223. addConditionToPagenate(offset, limit, sortOpt) {
  224. this.query = this.query
  225. .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
  226. return this;
  227. }
  228. populateDataToList(userPublicFields, imagePopulation) {
  229. this.query = this.query
  230. .populate({
  231. path: 'lastUpdateUser',
  232. select: userPublicFields,
  233. populate: imagePopulation,
  234. });
  235. return this;
  236. }
  237. populateDataToShowRevision(userPublicFields, imagePopulation) {
  238. this.query = populateDataToShowRevision(this.query, userPublicFields, imagePopulation);
  239. return this;
  240. }
  241. }
  242. module.exports = function(crowi) {
  243. let pageEvent;
  244. // init event
  245. if (crowi != null) {
  246. pageEvent = crowi.event('page');
  247. pageEvent.on('create', pageEvent.onCreate);
  248. pageEvent.on('update', pageEvent.onUpdate);
  249. }
  250. function validateCrowi() {
  251. if (crowi == null) {
  252. throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
  253. }
  254. }
  255. pageSchema.methods.isDeleted = function() {
  256. return (this.status === STATUS_DELETED) || checkIfTrashed(this.path);
  257. };
  258. pageSchema.methods.isPublic = function() {
  259. if (!this.grant || this.grant === GRANT_PUBLIC) {
  260. return true;
  261. }
  262. return false;
  263. };
  264. pageSchema.methods.isTopPage = function() {
  265. return isTopPage(this.path);
  266. };
  267. pageSchema.methods.isTemplate = function() {
  268. return templateChecker(this.path);
  269. };
  270. pageSchema.methods.isLatestRevision = function() {
  271. // populate されていなくて判断できない
  272. if (!this.latestRevision || !this.revision) {
  273. return true;
  274. }
  275. // comparing ObjectId with string
  276. // eslint-disable-next-line eqeqeq
  277. return (this.latestRevision == this.revision._id.toString());
  278. };
  279. pageSchema.methods.findRelatedTagsById = async function() {
  280. const PageTagRelation = mongoose.model('PageTagRelation');
  281. const relations = await PageTagRelation.find({ relatedPage: this._id }).populate('relatedTag');
  282. return relations.map((relation) => { return relation.relatedTag.name });
  283. };
  284. pageSchema.methods.isUpdatable = function(previousRevision) {
  285. const revision = this.latestRevision || this.revision;
  286. // comparing ObjectId with string
  287. // eslint-disable-next-line eqeqeq
  288. if (revision != previousRevision) {
  289. return false;
  290. }
  291. return true;
  292. };
  293. pageSchema.methods.isLiked = function(userData) {
  294. return this.liker.some((likedUserId) => {
  295. return likedUserId.toString() === userData._id.toString();
  296. });
  297. };
  298. pageSchema.methods.like = function(userData) {
  299. const self = this;
  300. return new Promise(((resolve, reject) => {
  301. const added = self.liker.addToSet(userData._id);
  302. if (added.length > 0) {
  303. self.save((err, data) => {
  304. if (err) {
  305. return reject(err);
  306. }
  307. logger.debug('liker updated!', added);
  308. return resolve(data);
  309. });
  310. }
  311. else {
  312. logger.debug('liker not updated');
  313. return reject(self);
  314. }
  315. }));
  316. };
  317. pageSchema.methods.unlike = function(userData, callback) {
  318. const self = this;
  319. return new Promise(((resolve, reject) => {
  320. const beforeCount = self.liker.length;
  321. self.liker.pull(userData._id);
  322. if (self.liker.length !== beforeCount) {
  323. self.save((err, data) => {
  324. if (err) {
  325. return reject(err);
  326. }
  327. return resolve(data);
  328. });
  329. }
  330. else {
  331. logger.debug('liker not updated');
  332. return reject(self);
  333. }
  334. }));
  335. };
  336. pageSchema.methods.isSeenUser = function(userData) {
  337. return this.seenUsers.includes(userData._id);
  338. };
  339. pageSchema.methods.seen = async function(userData) {
  340. if (this.isSeenUser(userData)) {
  341. debug('seenUsers not updated');
  342. return this;
  343. }
  344. if (!userData || !userData._id) {
  345. throw new Error('User data is not valid');
  346. }
  347. const added = this.seenUsers.addToSet(userData);
  348. const saved = await this.save();
  349. debug('seenUsers updated!', added);
  350. return saved;
  351. };
  352. pageSchema.methods.getSlackChannel = function() {
  353. const extended = this.get('extended');
  354. if (!extended) {
  355. return '';
  356. }
  357. return extended.slack || '';
  358. };
  359. pageSchema.methods.updateSlackChannel = function(slackChannel) {
  360. const extended = this.extended;
  361. extended.slack = slackChannel;
  362. return this.updateExtended(extended);
  363. };
  364. pageSchema.methods.updateExtended = function(extended) {
  365. const page = this;
  366. page.extended = extended;
  367. return new Promise(((resolve, reject) => {
  368. return page.save((err, doc) => {
  369. if (err) {
  370. return reject(err);
  371. }
  372. return resolve(doc);
  373. });
  374. }));
  375. };
  376. pageSchema.methods.initLatestRevisionField = async function(revisionId) {
  377. this.latestRevision = this.revision;
  378. if (revisionId != null) {
  379. this.revision = revisionId;
  380. }
  381. };
  382. pageSchema.methods.populateDataToShowRevision = async function() {
  383. validateCrowi();
  384. const User = crowi.model('User');
  385. return populateDataToShowRevision(this, User.USER_PUBLIC_FIELDS, User.IMAGE_POPULATION)
  386. .execPopulate();
  387. };
  388. pageSchema.methods.populateDataToMakePresentation = async function(revisionId) {
  389. this.latestRevision = this.revision;
  390. if (revisionId != null) {
  391. this.revision = revisionId;
  392. }
  393. return this.populate('revision').execPopulate();
  394. };
  395. pageSchema.methods.applyScope = function(user, grant, grantUserGroupId) {
  396. // reset
  397. this.grantedUsers = [];
  398. this.grantedGroup = null;
  399. this.grant = grant || GRANT_PUBLIC;
  400. if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP) {
  401. this.grantedUsers.push(user._id);
  402. }
  403. if (grant === GRANT_USER_GROUP) {
  404. this.grantedGroup = grantUserGroupId;
  405. }
  406. };
  407. pageSchema.methods.getContentAge = function() {
  408. return differenceInYears(new Date(), this.updatedAt);
  409. };
  410. pageSchema.statics.updateCommentCount = function(pageId) {
  411. validateCrowi();
  412. const self = this;
  413. const Comment = crowi.model('Comment');
  414. return Comment.countCommentByPageId(pageId)
  415. .then((count) => {
  416. self.update({ _id: pageId }, { commentCount: count }, {}, (err, data) => {
  417. if (err) {
  418. debug('Update commentCount Error', err);
  419. throw err;
  420. }
  421. return data;
  422. });
  423. });
  424. };
  425. pageSchema.statics.getGrantLabels = function() {
  426. const grantLabels = {};
  427. grantLabels[GRANT_PUBLIC] = 'Public'; // 公開
  428. grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
  429. // grantLabels[GRANT_SPECIFIED] = 'Specified users only'; // 特定ユーザーのみ
  430. grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
  431. grantLabels[GRANT_OWNER] = 'Just me'; // 自分のみ
  432. return grantLabels;
  433. };
  434. pageSchema.statics.getUserPagePath = function(user) {
  435. return `/user/${user.username}`;
  436. };
  437. pageSchema.statics.getDeletedPageName = function(path) {
  438. if (path.match('/')) {
  439. // eslint-disable-next-line no-param-reassign
  440. path = path.substr(1);
  441. }
  442. return `/trash/${path}`;
  443. };
  444. pageSchema.statics.getRevertDeletedPageName = function(path) {
  445. return path.replace('/trash', '');
  446. };
  447. pageSchema.statics.isDeletableName = function(path) {
  448. const notDeletable = [
  449. /^\/user\/[^/]+$/, // user page
  450. ];
  451. for (let i = 0; i < notDeletable.length; i++) {
  452. const pattern = notDeletable[i];
  453. if (path.match(pattern)) {
  454. return false;
  455. }
  456. }
  457. return true;
  458. };
  459. pageSchema.statics.isCreatableName = function(name) {
  460. const forbiddenPages = [
  461. /\^|\$|\*|\+|#|%/,
  462. /^\/-\/.*/,
  463. /^\/_r\/.*/,
  464. /^\/_apix?(\/.*)?/,
  465. /^\/?https?:\/\/.+$/, // avoid miss in renaming
  466. /\/{2,}/, // avoid miss in renaming
  467. /\s+\/\s+/, // avoid miss in renaming
  468. /.+\/edit$/,
  469. /.+\.md$/,
  470. /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags)(\/.*|$)/,
  471. ];
  472. let isCreatable = true;
  473. forbiddenPages.forEach((page) => {
  474. const pageNameReg = new RegExp(page);
  475. if (name.match(pageNameReg)) {
  476. isCreatable = false;
  477. }
  478. });
  479. return isCreatable;
  480. };
  481. pageSchema.statics.fixToCreatableName = function(path) {
  482. return path
  483. .replace(/\/\//g, '/');
  484. };
  485. pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
  486. this.update({ _id: pageId }, { revision: revisionId }, {}, (err, data) => {
  487. cb(err, data);
  488. });
  489. };
  490. /**
  491. * return whether the user is accessible to the page
  492. * @param {string} id ObjectId
  493. * @param {User} user
  494. */
  495. pageSchema.statics.isAccessiblePageByViewer = async function(id, user) {
  496. const baseQuery = this.count({ _id: id });
  497. let userGroups = [];
  498. if (user != null) {
  499. validateCrowi();
  500. const UserGroupRelation = crowi.model('UserGroupRelation');
  501. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  502. }
  503. const queryBuilder = new PageQueryBuilder(baseQuery);
  504. queryBuilder.addConditionToFilteringByViewer(user, userGroups, true);
  505. const count = await queryBuilder.query.exec();
  506. return count > 0;
  507. };
  508. /**
  509. * @param {string} id ObjectId
  510. * @param {User} user User instance
  511. * @param {UserGroup[]} userGroups List of UserGroup instances
  512. */
  513. pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups) {
  514. const baseQuery = this.findOne({ _id: id });
  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, true);
  523. return await queryBuilder.query.exec();
  524. };
  525. // find page by path
  526. pageSchema.statics.findByPath = function(path) {
  527. if (path == null) {
  528. return null;
  529. }
  530. return this.findOne({ path });
  531. };
  532. /**
  533. * @param {string} path Page path
  534. * @param {User} user User instance
  535. * @param {UserGroup[]} userGroups List of UserGroup instances
  536. */
  537. pageSchema.statics.findByPathAndViewer = async function(path, user, userGroups) {
  538. if (path == null) {
  539. throw new Error('path is required.');
  540. }
  541. const baseQuery = this.findOne({ path });
  542. let relatedUserGroups = userGroups;
  543. if (user != null && relatedUserGroups == null) {
  544. validateCrowi();
  545. const UserGroupRelation = crowi.model('UserGroupRelation');
  546. relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  547. }
  548. const queryBuilder = new PageQueryBuilder(baseQuery);
  549. queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
  550. return await queryBuilder.query.exec();
  551. };
  552. /**
  553. * @param {string} path Page path
  554. * @param {User} user User instance
  555. * @param {UserGroup[]} userGroups List of UserGroup instances
  556. */
  557. pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups) {
  558. if (path == null) {
  559. throw new Error('path is required.');
  560. }
  561. if (path === '/') {
  562. return null;
  563. }
  564. const ancestorsPaths = extractToAncestorsPaths(path);
  565. // pick the longest one
  566. const baseQuery = this.findOne({ path: { $in: ancestorsPaths } }).sort({ path: -1 });
  567. let relatedUserGroups = userGroups;
  568. if (user != null && relatedUserGroups == null) {
  569. validateCrowi();
  570. const UserGroupRelation = crowi.model('UserGroupRelation');
  571. relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  572. }
  573. const queryBuilder = new PageQueryBuilder(baseQuery);
  574. queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
  575. return await queryBuilder.query.exec();
  576. };
  577. pageSchema.statics.findByRedirectTo = function(path) {
  578. return this.findOne({ redirectTo: path });
  579. };
  580. /**
  581. * find pages that is match with `path` and its descendants
  582. */
  583. pageSchema.statics.findListWithDescendants = async function(path, user, option) {
  584. const builder = new PageQueryBuilder(this.find());
  585. builder.addConditionToListWithDescendants(path, option);
  586. return await findListFromBuilderAndViewer(builder, user, false, option);
  587. };
  588. /**
  589. * find pages that start with `path`
  590. */
  591. pageSchema.statics.findListByStartWith = async function(path, user, option) {
  592. const builder = new PageQueryBuilder(this.find());
  593. builder.addConditionToListByStartWith(path, option);
  594. return await findListFromBuilderAndViewer(builder, user, false, option);
  595. };
  596. /**
  597. * find pages that is created by targetUser
  598. *
  599. * @param {User} targetUser
  600. * @param {User} currentUser
  601. * @param {any} option
  602. */
  603. pageSchema.statics.findListByCreator = async function(targetUser, currentUser, option) {
  604. const opt = Object.assign({ sort: 'createdAt', desc: -1 }, option);
  605. const builder = new PageQueryBuilder(this.find({ creator: targetUser._id }));
  606. let showAnyoneKnowsLink = null;
  607. if (targetUser != null && currentUser != null) {
  608. showAnyoneKnowsLink = targetUser._id.equals(currentUser._id);
  609. }
  610. return await findListFromBuilderAndViewer(builder, currentUser, showAnyoneKnowsLink, opt);
  611. };
  612. pageSchema.statics.findListByPageIds = async function(ids, option) {
  613. const User = crowi.model('User');
  614. const opt = Object.assign({}, option);
  615. const builder = new PageQueryBuilder(this.find({ _id: { $in: ids } }));
  616. builder.addConditionToExcludeRedirect();
  617. builder.addConditionToPagenate(opt.offset, opt.limit);
  618. // count
  619. const totalCount = await builder.query.exec('count');
  620. // find
  621. builder.populateDataToList(User.USER_PUBLIC_FIELDS, User.IMAGE_POPULATION);
  622. const pages = await builder.query.exec('find');
  623. const result = {
  624. pages, totalCount, offset: opt.offset, limit: opt.limit,
  625. };
  626. return result;
  627. };
  628. /**
  629. * find pages by PageQueryBuilder
  630. * @param {PageQueryBuilder} builder
  631. * @param {User} user
  632. * @param {boolean} showAnyoneKnowsLink
  633. * @param {any} option
  634. */
  635. async function findListFromBuilderAndViewer(builder, user, showAnyoneKnowsLink, option) {
  636. validateCrowi();
  637. const User = crowi.model('User');
  638. const opt = Object.assign({ sort: 'updatedAt', desc: -1 }, option);
  639. const sortOpt = {};
  640. sortOpt[opt.sort] = opt.desc;
  641. // exclude trashed pages
  642. if (!opt.includeTrashed) {
  643. builder.addConditionToExcludeTrashed();
  644. }
  645. // exclude redirect pages
  646. if (!opt.includeRedirect) {
  647. builder.addConditionToExcludeRedirect();
  648. }
  649. // add grant conditions
  650. await addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink);
  651. // count
  652. const totalCount = await builder.query.exec('count');
  653. // find
  654. builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
  655. builder.populateDataToList(User.USER_PUBLIC_FIELDS, User.IMAGE_POPULATION);
  656. const pages = await builder.query.exec('find');
  657. const result = {
  658. pages, totalCount, offset: opt.offset, limit: opt.limit,
  659. };
  660. return result;
  661. }
  662. /**
  663. * Add condition that filter pages by viewer
  664. * by considering Config
  665. *
  666. * @param {PageQueryBuilder} builder
  667. * @param {User} user
  668. * @param {boolean} showAnyoneKnowsLink
  669. */
  670. async function addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink) {
  671. validateCrowi();
  672. // determine User condition
  673. const hidePagesRestrictedByOwner = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
  674. const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
  675. // determine UserGroup condition
  676. let userGroups = null;
  677. if (user != null) {
  678. const UserGroupRelation = crowi.model('UserGroupRelation');
  679. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  680. }
  681. return builder.addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
  682. }
  683. /**
  684. * Add condition that filter pages by viewer
  685. * by considering Config
  686. *
  687. * @param {PageQueryBuilder} builder
  688. * @param {User} user
  689. * @param {boolean} showAnyoneKnowsLink
  690. */
  691. async function addConditionToFilteringByViewerToEdit(builder, user) {
  692. validateCrowi();
  693. // determine UserGroup condition
  694. let userGroups = null;
  695. if (user != null) {
  696. const UserGroupRelation = crowi.model('UserGroupRelation');
  697. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  698. }
  699. return builder.addConditionToFilteringByViewer(user, userGroups, false, false, false);
  700. }
  701. /**
  702. * export addConditionToFilteringByViewerForList as static method
  703. */
  704. pageSchema.statics.addConditionToFilteringByViewerForList = addConditionToFilteringByViewerForList;
  705. /**
  706. * Throw error for growi-lsx-plugin (v1.x)
  707. */
  708. pageSchema.statics.generateQueryToListByStartWith = function(path, user, option) {
  709. const dummyQuery = this.find();
  710. dummyQuery.exec = async() => {
  711. throw new Error('Plugin version mismatch. Upgrade growi-lsx-plugin to v2.0.0 or above.');
  712. };
  713. return dummyQuery;
  714. };
  715. pageSchema.statics.generateQueryToListWithDescendants = pageSchema.statics.generateQueryToListByStartWith;
  716. /**
  717. * find all templates applicable to the new page
  718. */
  719. pageSchema.statics.findTemplate = async function(path) {
  720. const templatePath = nodePath.posix.dirname(path);
  721. const pathList = generatePathsOnTree(path, []);
  722. const regexpList = pathList.map((path) => {
  723. const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
  724. return new RegExp(`^${escapeStringRegexp(pathWithTrailingSlash)}_{1,2}template$`);
  725. });
  726. const templatePages = await this.find({ path: { $in: regexpList } })
  727. .populate({ path: 'revision', model: 'Revision' })
  728. .exec();
  729. return fetchTemplate(templatePages, templatePath);
  730. };
  731. const generatePathsOnTree = (path, pathList) => {
  732. pathList.push(path);
  733. if (path === '/') {
  734. return pathList;
  735. }
  736. const newPath = nodePath.posix.dirname(path);
  737. return generatePathsOnTree(newPath, pathList);
  738. };
  739. const assignTemplateByType = (templates, path, type) => {
  740. const targetTemplatePath = urljoin(path, `${type}template`);
  741. return templates.find((template) => {
  742. return (template.path === targetTemplatePath);
  743. });
  744. };
  745. const assignDecendantsTemplate = (decendantsTemplates, path) => {
  746. const decendantsTemplate = assignTemplateByType(decendantsTemplates, path, '__');
  747. if (decendantsTemplate) {
  748. return decendantsTemplate;
  749. }
  750. if (path === '/') {
  751. return;
  752. }
  753. const newPath = nodePath.posix.dirname(path);
  754. return assignDecendantsTemplate(decendantsTemplates, newPath);
  755. };
  756. const fetchTemplate = async(templates, templatePath) => {
  757. let templateBody;
  758. let templateTags;
  759. /**
  760. * get children template
  761. * __tempate: applicable only to immediate decendants
  762. */
  763. const childrenTemplate = assignTemplateByType(templates, templatePath, '_');
  764. /**
  765. * get decendants templates
  766. * _tempate: applicable to all pages under
  767. */
  768. const decendantsTemplate = assignDecendantsTemplate(templates, templatePath);
  769. if (childrenTemplate) {
  770. templateBody = childrenTemplate.revision.body;
  771. templateTags = await childrenTemplate.findRelatedTagsById();
  772. }
  773. else if (decendantsTemplate) {
  774. templateBody = decendantsTemplate.revision.body;
  775. templateTags = await decendantsTemplate.findRelatedTagsById();
  776. }
  777. return { templateBody, templateTags };
  778. };
  779. async function pushRevision(pageData, newRevision, user) {
  780. await newRevision.save();
  781. debug('Successfully saved new revision', newRevision);
  782. pageData.revision = newRevision;
  783. pageData.lastUpdateUser = user;
  784. pageData.updatedAt = Date.now();
  785. return pageData.save();
  786. }
  787. async function validateAppliedScope(user, grant, grantUserGroupId) {
  788. if (grant === GRANT_USER_GROUP && grantUserGroupId == null) {
  789. throw new Error('grant userGroupId is not specified');
  790. }
  791. if (grant === GRANT_USER_GROUP) {
  792. const UserGroupRelation = crowi.model('UserGroupRelation');
  793. const count = await UserGroupRelation.countByGroupIdAndUser(grantUserGroupId, user);
  794. if (count === 0) {
  795. throw new Error('no relations were exist for group and user.');
  796. }
  797. }
  798. }
  799. pageSchema.statics.create = async function(path, body, user, options = {}) {
  800. validateCrowi();
  801. const Page = this;
  802. const Revision = crowi.model('Revision');
  803. const format = options.format || 'markdown';
  804. const redirectTo = options.redirectTo || null;
  805. const grantUserGroupId = options.grantUserGroupId || null;
  806. const socketClientId = options.socketClientId || null;
  807. // sanitize path
  808. path = crowi.xss.process(path); // eslint-disable-line no-param-reassign
  809. let grant = options.grant;
  810. // force public
  811. if (isTopPage(path)) {
  812. grant = GRANT_PUBLIC;
  813. }
  814. const isExist = await this.count({ path });
  815. if (isExist) {
  816. throw new Error('Cannot create new page to existed path');
  817. }
  818. const page = new Page();
  819. page.path = path;
  820. page.creator = user;
  821. page.lastUpdateUser = user;
  822. page.redirectTo = redirectTo;
  823. page.status = STATUS_PUBLISHED;
  824. await validateAppliedScope(user, grant, grantUserGroupId);
  825. page.applyScope(user, grant, grantUserGroupId);
  826. let savedPage = await page.save();
  827. const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
  828. const revision = await pushRevision(savedPage, newRevision, user);
  829. savedPage = await this.findByPath(revision.path);
  830. await savedPage.populateDataToShowRevision();
  831. if (socketClientId != null) {
  832. pageEvent.emit('create', savedPage, user, socketClientId);
  833. }
  834. return savedPage;
  835. };
  836. pageSchema.statics.updatePage = async function(pageData, body, previousBody, user, options = {}) {
  837. validateCrowi();
  838. const Revision = crowi.model('Revision');
  839. const grant = options.grant || pageData.grant; // use the previous data if absence
  840. const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
  841. const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
  842. const socketClientId = options.socketClientId || null;
  843. await validateAppliedScope(user, grant, grantUserGroupId);
  844. pageData.applyScope(user, grant, grantUserGroupId);
  845. // update existing page
  846. let savedPage = await pageData.save();
  847. const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
  848. const revision = await pushRevision(savedPage, newRevision, user);
  849. savedPage = await this.findByPath(revision.path);
  850. await savedPage.populateDataToShowRevision();
  851. if (isSyncRevisionToHackmd) {
  852. savedPage = await this.syncRevisionToHackmd(savedPage);
  853. }
  854. if (socketClientId != null) {
  855. pageEvent.emit('update', savedPage, user, socketClientId);
  856. }
  857. return savedPage;
  858. };
  859. pageSchema.statics.applyScopesToDescendantsAsyncronously = async function(parentPage, user) {
  860. const builder = new PageQueryBuilder(this.find());
  861. builder.addConditionToListWithDescendants(parentPage.path);
  862. builder.addConditionToExcludeRedirect();
  863. // add grant conditions
  864. await addConditionToFilteringByViewerToEdit(builder, user);
  865. // get all pages that the specified user can update
  866. const pages = await builder.query.exec();
  867. for (const page of pages) {
  868. // skip parentPage
  869. if (page.id === parentPage.id) {
  870. continue;
  871. }
  872. page.applyScope(user, parentPage.grant, parentPage.grantedGroup);
  873. page.save();
  874. }
  875. };
  876. pageSchema.statics.deletePage = async function(pageData, user, options = {}) {
  877. const newPath = this.getDeletedPageName(pageData.path);
  878. const isTrashed = checkIfTrashed(pageData.path);
  879. if (isTrashed) {
  880. throw new Error('This method does NOT support deleting trashed pages.');
  881. }
  882. const socketClientId = options.socketClientId || null;
  883. if (this.isDeletableName(pageData.path)) {
  884. pageData.status = STATUS_DELETED;
  885. const updatedPageData = await this.rename(pageData, newPath, user, { socketClientId, createRedirectPage: true });
  886. return updatedPageData;
  887. }
  888. return Promise.reject(new Error('Page is not deletable.'));
  889. };
  890. const checkIfTrashed = (path) => {
  891. return (path.search(/^\/trash/) !== -1);
  892. };
  893. pageSchema.statics.deletePageRecursively = async function(targetPage, user, options = {}) {
  894. const isTrashed = checkIfTrashed(targetPage.path);
  895. if (isTrashed) {
  896. throw new Error('This method does NOT supports deleting trashed pages.');
  897. }
  898. const findOpts = { includeRedirect: true };
  899. const result = await this.findListWithDescendants(targetPage.path, user, findOpts);
  900. const pages = result.pages;
  901. let updatedPage = null;
  902. await Promise.all(pages.map((page) => {
  903. const isParent = (page.path === targetPage.path);
  904. const p = this.deletePage(page, user, options);
  905. if (isParent) {
  906. updatedPage = p;
  907. }
  908. return p;
  909. }));
  910. return updatedPage;
  911. };
  912. pageSchema.statics.revertDeletedPage = async function(page, user, options = {}) {
  913. const newPath = this.getRevertDeletedPageName(page.path);
  914. const originPage = await this.findByPath(newPath);
  915. if (originPage != null) {
  916. // 削除時、元ページの path には必ず redirectTo 付きで、ページが作成される。
  917. // そのため、そいつは削除してOK
  918. // が、redirectTo ではないページが存在している場合それは何かがおかしい。(データ補正が必要)
  919. if (originPage.redirectTo !== page.path) {
  920. throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
  921. }
  922. await this.completelyDeletePage(originPage, options);
  923. }
  924. page.status = STATUS_PUBLISHED;
  925. page.lastUpdateUser = user;
  926. debug('Revert deleted the page', page, newPath);
  927. const updatedPage = await this.rename(page, newPath, user, {});
  928. return updatedPage;
  929. };
  930. pageSchema.statics.revertDeletedPageRecursively = async function(targetPage, user, options = {}) {
  931. const findOpts = { includeRedirect: true, includeTrashed: true };
  932. const result = await this.findListWithDescendants(targetPage.path, user, findOpts);
  933. const pages = result.pages;
  934. let updatedPage = null;
  935. await Promise.all(pages.map((page) => {
  936. const isParent = (page.path === targetPage.path);
  937. const p = this.revertDeletedPage(page, user, options);
  938. if (isParent) {
  939. updatedPage = p;
  940. }
  941. return p;
  942. }));
  943. return updatedPage;
  944. };
  945. /**
  946. * This is danger.
  947. */
  948. pageSchema.statics.completelyDeletePage = async function(pageData, user, options = {}) {
  949. validateCrowi();
  950. // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
  951. const Bookmark = crowi.model('Bookmark');
  952. const Attachment = crowi.model('Attachment');
  953. const Comment = crowi.model('Comment');
  954. const PageTagRelation = crowi.model('PageTagRelation');
  955. const Revision = crowi.model('Revision');
  956. const pageId = pageData._id;
  957. const socketClientId = options.socketClientId || null;
  958. debug('Completely delete', pageData.path);
  959. await Bookmark.removeBookmarksByPageId(pageId);
  960. await Attachment.removeAttachmentsByPageId(pageId);
  961. await Comment.removeCommentsByPageId(pageId);
  962. await PageTagRelation.remove({ relatedPage: pageId });
  963. await Revision.removeRevisionsByPath(pageData.path);
  964. await this.findByIdAndRemove(pageId);
  965. await this.removeRedirectOriginPageByPath(pageData.path);
  966. if (socketClientId != null) {
  967. pageEvent.emit('delete', pageData, user, socketClientId); // update as renamed page
  968. }
  969. return pageData;
  970. };
  971. /**
  972. * Delete Bookmarks, Attachments, Revisions, Pages and emit delete
  973. */
  974. pageSchema.statics.completelyDeletePageRecursively = async function(pageData, user, options = {}) {
  975. const path = pageData.path;
  976. const findOpts = { includeRedirect: true, includeTrashed: true };
  977. const result = await this.findListWithDescendants(path, user, findOpts);
  978. const pages = result.pages;
  979. await Promise.all(pages.map((page) => {
  980. return this.completelyDeletePage(page, user, options);
  981. }));
  982. return pageData;
  983. };
  984. pageSchema.statics.removeByPath = function(path) {
  985. if (path == null) {
  986. throw new Error('path is required');
  987. }
  988. return this.findOneAndRemove({ path }).exec();
  989. };
  990. /**
  991. * remove the page that is redirecting to specified `pagePath` recursively
  992. * ex: when
  993. * '/page1' redirects to '/page2' and
  994. * '/page2' redirects to '/page3'
  995. * and given '/page3',
  996. * '/page1' and '/page2' will be removed
  997. *
  998. * @param {string} pagePath
  999. */
  1000. pageSchema.statics.removeRedirectOriginPageByPath = async function(pagePath) {
  1001. const redirectPage = await this.findByRedirectTo(pagePath);
  1002. if (redirectPage == null) {
  1003. return;
  1004. }
  1005. // remove
  1006. await this.findByIdAndRemove(redirectPage.id);
  1007. // remove recursive
  1008. await this.removeRedirectOriginPageByPath(redirectPage.path);
  1009. };
  1010. pageSchema.statics.rename = async function(pageData, newPagePath, user, options) {
  1011. validateCrowi();
  1012. const Page = this;
  1013. const Revision = crowi.model('Revision');
  1014. const path = pageData.path;
  1015. const createRedirectPage = options.createRedirectPage || false;
  1016. const updateMetadata = options.updateMetadata || false;
  1017. const socketClientId = options.socketClientId || null;
  1018. // sanitize path
  1019. newPagePath = crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  1020. // update Page
  1021. pageData.path = newPagePath;
  1022. if (updateMetadata) {
  1023. pageData.lastUpdateUser = user;
  1024. pageData.updatedAt = Date.now();
  1025. }
  1026. const updatedPageData = await pageData.save();
  1027. // update Rivisions
  1028. await Revision.updateRevisionListByPath(path, { path: newPagePath }, {});
  1029. if (createRedirectPage) {
  1030. const body = `redirect ${newPagePath}`;
  1031. await Page.create(path, body, user, { redirectTo: newPagePath });
  1032. }
  1033. pageEvent.emit('delete', pageData, user, socketClientId);
  1034. pageEvent.emit('create', updatedPageData, user, socketClientId);
  1035. return updatedPageData;
  1036. };
  1037. pageSchema.statics.renameRecursively = async function(pageData, newPagePathPrefix, user, options) {
  1038. validateCrowi();
  1039. const path = pageData.path;
  1040. const pathRegExp = new RegExp(`^${escapeStringRegexp(path)}`, 'i');
  1041. // sanitize path
  1042. newPagePathPrefix = crowi.xss.process(newPagePathPrefix); // eslint-disable-line no-param-reassign
  1043. const result = await this.findListWithDescendants(path, user, options);
  1044. await Promise.all(result.pages.map((page) => {
  1045. const newPagePath = page.path.replace(pathRegExp, newPagePathPrefix);
  1046. return this.rename(page, newPagePath, user, options);
  1047. }));
  1048. pageData.path = newPagePathPrefix;
  1049. return pageData;
  1050. };
  1051. pageSchema.statics.handlePrivatePagesForDeletedGroup = async function(deletedGroup, action, transferToUserGroupId) {
  1052. const Page = mongoose.model('Page');
  1053. const pages = await this.find({ grantedGroup: deletedGroup });
  1054. switch (action) {
  1055. case 'public':
  1056. await Promise.all(pages.map((page) => {
  1057. return Page.publicizePage(page);
  1058. }));
  1059. break;
  1060. case 'delete':
  1061. await Promise.all(pages.map((page) => {
  1062. return Page.completelyDeletePage(page);
  1063. }));
  1064. break;
  1065. case 'transfer':
  1066. await Promise.all(pages.map((page) => {
  1067. return Page.transferPageToGroup(page, transferToUserGroupId);
  1068. }));
  1069. break;
  1070. default:
  1071. throw new Error('Unknown action for private pages');
  1072. }
  1073. };
  1074. pageSchema.statics.publicizePage = async function(page) {
  1075. page.grantedGroup = null;
  1076. page.grant = GRANT_PUBLIC;
  1077. await page.save();
  1078. };
  1079. pageSchema.statics.transferPageToGroup = async function(page, transferToUserGroupId) {
  1080. const UserGroup = mongoose.model('UserGroup');
  1081. // check page existence
  1082. const isExist = await UserGroup.count({ _id: transferToUserGroupId }) > 0;
  1083. if (isExist) {
  1084. page.grantedGroup = transferToUserGroupId;
  1085. await page.save();
  1086. }
  1087. else {
  1088. throw new Error('Cannot find the group to which private pages belong to. _id: ', transferToUserGroupId);
  1089. }
  1090. };
  1091. /**
  1092. * associate GROWI page and HackMD page
  1093. * @param {Page} pageData
  1094. * @param {string} pageIdOnHackmd
  1095. */
  1096. pageSchema.statics.registerHackmdPage = function(pageData, pageIdOnHackmd) {
  1097. pageData.pageIdOnHackmd = pageIdOnHackmd;
  1098. return this.syncRevisionToHackmd(pageData);
  1099. };
  1100. /**
  1101. * update revisionHackmdSynced
  1102. * @param {Page} pageData
  1103. * @param {bool} isSave whether save or not
  1104. */
  1105. pageSchema.statics.syncRevisionToHackmd = function(pageData, isSave = true) {
  1106. pageData.revisionHackmdSynced = pageData.revision;
  1107. pageData.hasDraftOnHackmd = false;
  1108. let returnData = pageData;
  1109. if (isSave) {
  1110. returnData = pageData.save();
  1111. }
  1112. return returnData;
  1113. };
  1114. /**
  1115. * update hasDraftOnHackmd
  1116. * !! This will be invoked many time from many people !!
  1117. *
  1118. * @param {Page} pageData
  1119. * @param {Boolean} newValue
  1120. */
  1121. pageSchema.statics.updateHasDraftOnHackmd = async function(pageData, newValue) {
  1122. if (pageData.hasDraftOnHackmd === newValue) {
  1123. // do nothing when hasDraftOnHackmd equals to newValue
  1124. return;
  1125. }
  1126. pageData.hasDraftOnHackmd = newValue;
  1127. return pageData.save();
  1128. };
  1129. pageSchema.statics.getHistories = function() {
  1130. // TODO
  1131. };
  1132. /**
  1133. * return path that added slash to the end for specified path
  1134. */
  1135. pageSchema.statics.addSlashOfEnd = function(path) {
  1136. return addSlashOfEnd(path);
  1137. };
  1138. pageSchema.statics.GRANT_PUBLIC = GRANT_PUBLIC;
  1139. pageSchema.statics.GRANT_RESTRICTED = GRANT_RESTRICTED;
  1140. pageSchema.statics.GRANT_SPECIFIED = GRANT_SPECIFIED;
  1141. pageSchema.statics.GRANT_OWNER = GRANT_OWNER;
  1142. pageSchema.statics.GRANT_USER_GROUP = GRANT_USER_GROUP;
  1143. pageSchema.statics.PAGE_GRANT_ERROR = PAGE_GRANT_ERROR;
  1144. pageSchema.statics.PageQueryBuilder = PageQueryBuilder;
  1145. return mongoose.model('Page', pageSchema);
  1146. };