page.js 34 KB

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