page.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. import { pagePathUtils } from '@growi/core';
  2. import loggerFactory from '~/utils/logger';
  3. import ActivityDefine from '../util/activityDefine';
  4. import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
  5. const mongoose = require('mongoose');
  6. const escapeStringRegexp = require('escape-string-regexp');
  7. const streamToPromise = require('stream-to-promise');
  8. const logger = loggerFactory('growi:service:page');
  9. const debug = require('debug')('growi:service:page');
  10. const { Writable } = require('stream');
  11. const { createBatchStream } = require('~/server/util/batch-stream');
  12. const { isCreatablePage, isDeletablePage, isTrashPage } = pagePathUtils;
  13. const { serializePageSecurely } = require('../models/serializers/page-serializer');
  14. const BULK_REINDEX_SIZE = 100;
  15. class PageService {
  16. constructor(crowi) {
  17. this.crowi = crowi;
  18. this.pageEvent = crowi.event('page');
  19. this.tagEvent = crowi.event('tag');
  20. // init
  21. this.initPageEvent();
  22. }
  23. initPageEvent() {
  24. // create
  25. this.pageEvent.on('create', this.pageEvent.onCreate);
  26. // createMany
  27. this.pageEvent.on('createMany', this.pageEvent.onCreateMany);
  28. // update
  29. this.pageEvent.on('update', async(page, user) => {
  30. this.pageEvent.onUpdate();
  31. try {
  32. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_UPDATE);
  33. }
  34. catch (err) {
  35. logger.error(err);
  36. }
  37. });
  38. // rename
  39. this.pageEvent.on('rename', async(page, user) => {
  40. try {
  41. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_RENAME);
  42. }
  43. catch (err) {
  44. logger.error(err);
  45. }
  46. });
  47. // delete
  48. this.pageEvent.on('delete', async(page, user) => {
  49. try {
  50. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE);
  51. }
  52. catch (err) {
  53. logger.error(err);
  54. }
  55. });
  56. // delete completely
  57. this.pageEvent.on('deleteCompletely', async(page, user) => {
  58. try {
  59. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE_COMPLETELY);
  60. }
  61. catch (err) {
  62. logger.error(err);
  63. }
  64. });
  65. // likes
  66. this.pageEvent.on('like', async(page, user) => {
  67. try {
  68. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_LIKE);
  69. }
  70. catch (err) {
  71. logger.error(err);
  72. }
  73. });
  74. // bookmark
  75. this.pageEvent.on('bookmark', async(page, user) => {
  76. try {
  77. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_BOOKMARK);
  78. }
  79. catch (err) {
  80. logger.error(err);
  81. }
  82. });
  83. }
  84. canDeleteCompletely(creatorId, operator) {
  85. const pageCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
  86. if (operator.admin) {
  87. return true;
  88. }
  89. if (pageCompleteDeletionAuthority === 'anyOne' || pageCompleteDeletionAuthority == null) {
  90. return true;
  91. }
  92. if (pageCompleteDeletionAuthority === 'adminAndAuthor') {
  93. const operatorId = operator?._id;
  94. return (operatorId != null && operatorId.equals(creatorId));
  95. }
  96. return false;
  97. }
  98. async findPageAndMetaDataByViewer({ pageId, path, user }) {
  99. const Page = this.crowi.model('Page');
  100. let page;
  101. if (pageId != null) { // prioritized
  102. page = await Page.findByIdAndViewer(pageId, user);
  103. }
  104. else {
  105. page = await Page.findByPathAndViewer(path, user);
  106. }
  107. const result = {};
  108. if (page == null) {
  109. const isExist = await Page.count({ $or: [{ _id: pageId }, { path }] }) > 0;
  110. result.isForbidden = isExist;
  111. result.isNotFound = !isExist;
  112. result.isCreatable = isCreatablePage(path);
  113. result.isDeletable = false;
  114. result.canDeleteCompletely = false;
  115. result.page = page;
  116. return result;
  117. }
  118. result.page = page;
  119. result.isForbidden = false;
  120. result.isNotFound = false;
  121. result.isCreatable = false;
  122. result.isDeletable = isDeletablePage(path);
  123. result.isDeleted = page.isDeleted();
  124. result.canDeleteCompletely = user != null && this.canDeleteCompletely(page.creator, user);
  125. return result;
  126. }
  127. /**
  128. * go back by using redirectTo and return the paths
  129. * ex: when
  130. * '/page1' redirects to '/page2' and
  131. * '/page2' redirects to '/page3'
  132. * and given '/page3',
  133. * '/page1' and '/page2' will be return
  134. *
  135. * @param {string} redirectTo
  136. * @param {object} redirectToPagePathMapping
  137. * @param {array} pagePaths
  138. */
  139. prepareShoudDeletePagesByRedirectTo(redirectTo, redirectToPagePathMapping, pagePaths = []) {
  140. const pagePath = redirectToPagePathMapping[redirectTo];
  141. if (pagePath == null) {
  142. return pagePaths;
  143. }
  144. pagePaths.push(pagePath);
  145. return this.prepareShoudDeletePagesByRedirectTo(pagePath, redirectToPagePathMapping, pagePaths);
  146. }
  147. /**
  148. * Generate read stream to operate descendants of the specified page path
  149. * @param {string} targetPagePath
  150. * @param {User} viewer
  151. */
  152. async generateReadStreamToOperateOnlyDescendants(targetPagePath, userToOperate) {
  153. const Page = this.crowi.model('Page');
  154. const { PageQueryBuilder } = Page;
  155. const builder = new PageQueryBuilder(Page.find())
  156. .addConditionToExcludeRedirect()
  157. .addConditionToListOnlyDescendants(targetPagePath);
  158. await Page.addConditionToFilteringByViewerToEdit(builder, userToOperate);
  159. return builder
  160. .query
  161. .lean()
  162. .cursor({ batchSize: BULK_REINDEX_SIZE });
  163. }
  164. async renamePage(page, newPagePath, user, options, isRecursively = false) {
  165. const Page = this.crowi.model('Page');
  166. const Revision = this.crowi.model('Revision');
  167. const path = page.path;
  168. const createRedirectPage = options.createRedirectPage || false;
  169. const updateMetadata = options.updateMetadata || false;
  170. // sanitize path
  171. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  172. // create descendants first
  173. if (isRecursively) {
  174. await this.renameDescendantsWithStream(page, newPagePath, user, options);
  175. }
  176. const update = {};
  177. // update Page
  178. update.path = newPagePath;
  179. if (updateMetadata) {
  180. update.lastUpdateUser = user;
  181. update.updatedAt = Date.now();
  182. }
  183. const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
  184. // update Rivisions
  185. await Revision.updateRevisionListByPath(path, { path: newPagePath }, {});
  186. if (createRedirectPage) {
  187. const body = `redirect ${newPagePath}`;
  188. await Page.create(path, body, user, { redirectTo: newPagePath });
  189. }
  190. this.pageEvent.emit('rename', page, user);
  191. return renamedPage;
  192. }
  193. async renameDescendants(pages, user, options, oldPagePathPrefix, newPagePathPrefix) {
  194. const Page = this.crowi.model('Page');
  195. const pageCollection = mongoose.connection.collection('pages');
  196. const revisionCollection = mongoose.connection.collection('revisions');
  197. const { updateMetadata, createRedirectPage } = options;
  198. const unorderedBulkOp = pageCollection.initializeUnorderedBulkOp();
  199. const createRediectPageBulkOp = pageCollection.initializeUnorderedBulkOp();
  200. const revisionUnorderedBulkOp = revisionCollection.initializeUnorderedBulkOp();
  201. const createRediectRevisionBulkOp = revisionCollection.initializeUnorderedBulkOp();
  202. pages.forEach((page) => {
  203. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  204. const revisionId = new mongoose.Types.ObjectId();
  205. if (updateMetadata) {
  206. unorderedBulkOp
  207. .find({ _id: page._id })
  208. .update({ $set: { path: newPagePath, lastUpdateUser: user._id, updatedAt: new Date() } });
  209. }
  210. else {
  211. unorderedBulkOp.find({ _id: page._id }).update({ $set: { path: newPagePath } });
  212. }
  213. if (createRedirectPage) {
  214. createRediectPageBulkOp.insert({
  215. path: page.path, revision: revisionId, creator: user._id, lastUpdateUser: user._id, status: Page.STATUS_PUBLISHED, redirectTo: newPagePath,
  216. });
  217. createRediectRevisionBulkOp.insert({
  218. _id: revisionId, path: page.path, body: `redirect ${newPagePath}`, author: user._id, format: 'markdown',
  219. });
  220. }
  221. revisionUnorderedBulkOp.find({ path: page.path }).update({ $set: { path: newPagePath } }, { multi: true });
  222. });
  223. try {
  224. await unorderedBulkOp.execute();
  225. await revisionUnorderedBulkOp.execute();
  226. // Execute after unorderedBulkOp to prevent duplication
  227. if (createRedirectPage) {
  228. await createRediectPageBulkOp.execute();
  229. await createRediectRevisionBulkOp.execute();
  230. }
  231. }
  232. catch (err) {
  233. if (err.code !== 11000) {
  234. throw new Error('Failed to rename pages: ', err);
  235. }
  236. }
  237. this.pageEvent.emit('updateMany', pages, user);
  238. }
  239. /**
  240. * Create rename stream
  241. */
  242. async renameDescendantsWithStream(targetPage, newPagePath, user, options = {}) {
  243. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  244. const newPagePathPrefix = newPagePath;
  245. const pathRegExp = new RegExp(`^${escapeStringRegexp(targetPage.path)}`, 'i');
  246. const renameDescendants = this.renameDescendants.bind(this);
  247. const pageEvent = this.pageEvent;
  248. let count = 0;
  249. const writeStream = new Writable({
  250. objectMode: true,
  251. async write(batch, encoding, callback) {
  252. try {
  253. count += batch.length;
  254. await renameDescendants(batch, user, options, pathRegExp, newPagePathPrefix);
  255. logger.debug(`Reverting pages progressing: (count=${count})`);
  256. }
  257. catch (err) {
  258. logger.error('revertPages error on add anyway: ', err);
  259. }
  260. callback();
  261. },
  262. final(callback) {
  263. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  264. // update path
  265. targetPage.path = newPagePath;
  266. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  267. callback();
  268. },
  269. });
  270. readStream
  271. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  272. .pipe(writeStream);
  273. await streamToPromise(readStream);
  274. }
  275. async deleteCompletelyOperation(pageIds, pagePaths) {
  276. // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
  277. const Bookmark = this.crowi.model('Bookmark');
  278. const Comment = this.crowi.model('Comment');
  279. const Page = this.crowi.model('Page');
  280. const PageTagRelation = this.crowi.model('PageTagRelation');
  281. const ShareLink = this.crowi.model('ShareLink');
  282. const Revision = this.crowi.model('Revision');
  283. const Attachment = this.crowi.model('Attachment');
  284. const { attachmentService } = this.crowi;
  285. const attachments = await Attachment.find({ page: { $in: pageIds } });
  286. const pages = await Page.find({ redirectTo: { $ne: null } });
  287. const redirectToPagePathMapping = {};
  288. pages.forEach((page) => {
  289. redirectToPagePathMapping[page.redirectTo] = page.path;
  290. });
  291. const redirectedFromPagePaths = [];
  292. pagePaths.forEach((pagePath) => {
  293. redirectedFromPagePaths.push(...this.prepareShoudDeletePagesByRedirectTo(pagePath, redirectToPagePathMapping));
  294. });
  295. return Promise.all([
  296. Bookmark.deleteMany({ page: { $in: pageIds } }),
  297. Comment.deleteMany({ page: { $in: pageIds } }),
  298. PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
  299. ShareLink.deleteMany({ relatedPage: { $in: pageIds } }),
  300. Revision.deleteMany({ path: { $in: pagePaths } }),
  301. Page.deleteMany({ $or: [{ path: { $in: pagePaths } }, { path: { $in: redirectedFromPagePaths } }, { _id: { $in: pageIds } }] }),
  302. attachmentService.removeAllAttachments(attachments),
  303. ]);
  304. }
  305. async duplicate(page, newPagePath, user, isRecursively) {
  306. const Page = this.crowi.model('Page');
  307. const PageTagRelation = mongoose.model('PageTagRelation');
  308. // populate
  309. await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
  310. // create option
  311. const options = { page };
  312. options.grant = page.grant;
  313. options.grantUserGroupId = page.grantedGroup;
  314. options.grantedUsers = page.grantedUsers;
  315. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  316. const createdPage = await Page.create(
  317. newPagePath, page.revision.body, user, options,
  318. );
  319. if (isRecursively) {
  320. this.duplicateDescendantsWithStream(page, newPagePath, user);
  321. }
  322. // take over tags
  323. const originTags = await page.findRelatedTagsById();
  324. let savedTags = [];
  325. if (originTags != null) {
  326. await PageTagRelation.updatePageTags(createdPage.id, originTags);
  327. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  328. this.tagEvent.emit('update', createdPage, savedTags);
  329. }
  330. const result = serializePageSecurely(createdPage);
  331. result.tags = savedTags;
  332. return result;
  333. }
  334. /**
  335. * Receive the object with oldPageId and newPageId and duplicate the tags from oldPage to newPage
  336. * @param {Object} pageIdMapping e.g. key: oldPageId, value: newPageId
  337. */
  338. async duplicateTags(pageIdMapping) {
  339. const PageTagRelation = mongoose.model('PageTagRelation');
  340. // convert pageId from string to ObjectId
  341. const pageIds = Object.keys(pageIdMapping);
  342. const stage = { $or: pageIds.map((pageId) => { return { relatedPage: mongoose.Types.ObjectId(pageId) } }) };
  343. const pagesAssociatedWithTag = await PageTagRelation.aggregate([
  344. {
  345. $match: stage,
  346. },
  347. {
  348. $group: {
  349. _id: '$relatedTag',
  350. relatedPages: { $push: '$relatedPage' },
  351. },
  352. },
  353. ]);
  354. const newPageTagRelation = [];
  355. pagesAssociatedWithTag.forEach(({ _id, relatedPages }) => {
  356. // relatedPages
  357. relatedPages.forEach((pageId) => {
  358. newPageTagRelation.push({
  359. relatedPage: pageIdMapping[pageId], // newPageId
  360. relatedTag: _id,
  361. });
  362. });
  363. });
  364. return PageTagRelation.insertMany(newPageTagRelation, { ordered: false });
  365. }
  366. async duplicateDescendants(pages, user, oldPagePathPrefix, newPagePathPrefix) {
  367. const Page = this.crowi.model('Page');
  368. const Revision = this.crowi.model('Revision');
  369. const paths = pages.map(page => (page.path));
  370. const revisions = await Revision.find({ path: { $in: paths } });
  371. // Mapping to set to the body of the new revision
  372. const pathRevisionMapping = {};
  373. revisions.forEach((revision) => {
  374. pathRevisionMapping[revision.path] = revision;
  375. });
  376. // key: oldPageId, value: newPageId
  377. const pageIdMapping = {};
  378. const newPages = [];
  379. const newRevisions = [];
  380. pages.forEach((page) => {
  381. const newPageId = new mongoose.Types.ObjectId();
  382. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  383. const revisionId = new mongoose.Types.ObjectId();
  384. pageIdMapping[page._id] = newPageId;
  385. newPages.push({
  386. _id: newPageId,
  387. path: newPagePath,
  388. creator: user._id,
  389. grant: page.grant,
  390. grantedGroup: page.grantedGroup,
  391. grantedUsers: page.grantedUsers,
  392. lastUpdateUser: user._id,
  393. redirectTo: null,
  394. revision: revisionId,
  395. });
  396. newRevisions.push({
  397. _id: revisionId, path: newPagePath, body: pathRevisionMapping[page.path].body, author: user._id, format: 'markdown',
  398. });
  399. });
  400. await Page.insertMany(newPages, { ordered: false });
  401. await Revision.insertMany(newRevisions, { ordered: false });
  402. await this.duplicateTags(pageIdMapping);
  403. }
  404. async duplicateDescendantsWithStream(page, newPagePath, user) {
  405. const readStream = await this.generateReadStreamToOperateOnlyDescendants(page.path, user);
  406. const newPagePathPrefix = newPagePath;
  407. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  408. const duplicateDescendants = this.duplicateDescendants.bind(this);
  409. const pageEvent = this.pageEvent;
  410. let count = 0;
  411. const writeStream = new Writable({
  412. objectMode: true,
  413. async write(batch, encoding, callback) {
  414. try {
  415. count += batch.length;
  416. await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix);
  417. logger.debug(`Adding pages progressing: (count=${count})`);
  418. }
  419. catch (err) {
  420. logger.error('addAllPages error on add anyway: ', err);
  421. }
  422. callback();
  423. },
  424. final(callback) {
  425. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  426. // update path
  427. page.path = newPagePath;
  428. pageEvent.emit('syncDescendantsUpdate', page, user);
  429. callback();
  430. },
  431. });
  432. readStream
  433. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  434. .pipe(writeStream);
  435. }
  436. async deletePage(page, user, options = {}, isRecursively = false) {
  437. const Page = this.crowi.model('Page');
  438. const PageTagRelation = this.crowi.model('PageTagRelation');
  439. const Revision = this.crowi.model('Revision');
  440. const newPath = Page.getDeletedPageName(page.path);
  441. const isTrashed = isTrashPage(page.path);
  442. if (isTrashed) {
  443. throw new Error('This method does NOT support deleting trashed pages.');
  444. }
  445. if (!Page.isDeletableName(page.path)) {
  446. throw new Error('Page is not deletable.');
  447. }
  448. if (isRecursively) {
  449. this.deleteDescendantsWithStream(page, user, options);
  450. }
  451. // update Rivisions
  452. await Revision.updateRevisionListByPath(page.path, { path: newPath }, {});
  453. const deletedPage = await Page.findByIdAndUpdate(page._id, {
  454. $set: {
  455. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
  456. },
  457. }, { new: true });
  458. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
  459. const body = `redirect ${newPath}`;
  460. await Page.create(page.path, body, user, { redirectTo: newPath });
  461. this.pageEvent.emit('delete', page, user);
  462. this.pageEvent.emit('create', deletedPage, user);
  463. return deletedPage;
  464. }
  465. async deleteDescendants(pages, user) {
  466. const Page = this.crowi.model('Page');
  467. const pageCollection = mongoose.connection.collection('pages');
  468. const revisionCollection = mongoose.connection.collection('revisions');
  469. const deletePageBulkOp = pageCollection.initializeUnorderedBulkOp();
  470. const updateRevisionListOp = revisionCollection.initializeUnorderedBulkOp();
  471. const createRediectRevisionBulkOp = revisionCollection.initializeUnorderedBulkOp();
  472. const newPagesForRedirect = [];
  473. pages.forEach((page) => {
  474. const newPath = Page.getDeletedPageName(page.path);
  475. const revisionId = new mongoose.Types.ObjectId();
  476. const body = `redirect ${newPath}`;
  477. deletePageBulkOp.find({ _id: page._id }).update({
  478. $set: {
  479. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
  480. },
  481. });
  482. updateRevisionListOp.find({ path: page.path }).update({ $set: { path: newPath } });
  483. createRediectRevisionBulkOp.insert({
  484. _id: revisionId, path: page.path, body, author: user._id, format: 'markdown',
  485. });
  486. newPagesForRedirect.push({
  487. path: page.path,
  488. creator: user._id,
  489. grant: page.grant,
  490. grantedGroup: page.grantedGroup,
  491. grantedUsers: page.grantedUsers,
  492. lastUpdateUser: user._id,
  493. redirectTo: newPath,
  494. revision: revisionId,
  495. });
  496. });
  497. try {
  498. await deletePageBulkOp.execute();
  499. await updateRevisionListOp.execute();
  500. await createRediectRevisionBulkOp.execute();
  501. await Page.insertMany(newPagesForRedirect, { ordered: false });
  502. }
  503. catch (err) {
  504. if (err.code !== 11000) {
  505. throw new Error('Failed to revert pages: ', err);
  506. }
  507. }
  508. finally {
  509. this.pageEvent.emit('syncDescendantsDelete', pages, user);
  510. }
  511. }
  512. /**
  513. * Create delete stream
  514. */
  515. async deleteDescendantsWithStream(targetPage, user, options = {}) {
  516. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  517. const deleteDescendants = this.deleteDescendants.bind(this);
  518. let count = 0;
  519. const writeStream = new Writable({
  520. objectMode: true,
  521. async write(batch, encoding, callback) {
  522. try {
  523. count += batch.length;
  524. deleteDescendants(batch, user);
  525. logger.debug(`Reverting pages progressing: (count=${count})`);
  526. }
  527. catch (err) {
  528. logger.error('revertPages error on add anyway: ', err);
  529. }
  530. callback();
  531. },
  532. final(callback) {
  533. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  534. callback();
  535. },
  536. });
  537. readStream
  538. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  539. .pipe(writeStream);
  540. }
  541. // delete multiple pages
  542. async deleteMultipleCompletely(pages, user, options = {}) {
  543. const ids = pages.map(page => (page._id));
  544. const paths = pages.map(page => (page.path));
  545. logger.debug('Deleting completely', paths);
  546. await this.deleteCompletelyOperation(ids, paths);
  547. this.pageEvent.emit('syncDescendantsDelete', pages, user); // update as renamed page
  548. return;
  549. }
  550. async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false) {
  551. const ids = [page._id];
  552. const paths = [page.path];
  553. logger.debug('Deleting completely', paths);
  554. await this.deleteCompletelyOperation(ids, paths);
  555. if (isRecursively) {
  556. this.deleteCompletelyDescendantsWithStream(page, user, options);
  557. }
  558. if (!preventEmitting) {
  559. this.pageEvent.emit('deleteCompletely', page, user);
  560. }
  561. return;
  562. }
  563. /**
  564. * Create delete completely stream
  565. */
  566. async deleteCompletelyDescendantsWithStream(targetPage, user, options = {}) {
  567. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  568. const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
  569. let count = 0;
  570. const writeStream = new Writable({
  571. objectMode: true,
  572. async write(batch, encoding, callback) {
  573. try {
  574. count += batch.length;
  575. await deleteMultipleCompletely(batch, user, options);
  576. logger.debug(`Adding pages progressing: (count=${count})`);
  577. }
  578. catch (err) {
  579. logger.error('addAllPages error on add anyway: ', err);
  580. }
  581. callback();
  582. },
  583. final(callback) {
  584. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  585. callback();
  586. },
  587. });
  588. readStream
  589. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  590. .pipe(writeStream);
  591. }
  592. async revertDeletedDescendants(pages, user) {
  593. const Page = this.crowi.model('Page');
  594. const pageCollection = mongoose.connection.collection('pages');
  595. const revisionCollection = mongoose.connection.collection('revisions');
  596. const removePageBulkOp = pageCollection.initializeUnorderedBulkOp();
  597. const revertPageBulkOp = pageCollection.initializeUnorderedBulkOp();
  598. const revertRevisionBulkOp = revisionCollection.initializeUnorderedBulkOp();
  599. // e.g. key: '/test'
  600. const pathToPageMapping = {};
  601. const toPaths = pages.map(page => Page.getRevertDeletedPageName(page.path));
  602. const toPages = await Page.find({ path: { $in: toPaths } });
  603. toPages.forEach((toPage) => {
  604. pathToPageMapping[toPage.path] = toPage;
  605. });
  606. pages.forEach((page) => {
  607. // e.g. page.path = /trash/test, toPath = /test
  608. const toPath = Page.getRevertDeletedPageName(page.path);
  609. if (pathToPageMapping[toPath] != null) {
  610. // When the page is deleted, it will always be created with "redirectTo" in the path of the original page.
  611. // So, it's ok to delete the page
  612. // However, If a page exists that is not "redirectTo", something is wrong. (Data correction is needed).
  613. if (pathToPageMapping[toPath].redirectTo === page.path) {
  614. removePageBulkOp.find({ path: toPath }).delete();
  615. }
  616. }
  617. revertPageBulkOp.find({ _id: page._id }).update({
  618. $set: {
  619. path: toPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  620. },
  621. });
  622. revertRevisionBulkOp.find({ path: page.path }).update({ $set: { path: toPath } }, { multi: true });
  623. });
  624. try {
  625. await removePageBulkOp.execute();
  626. await revertPageBulkOp.execute();
  627. await revertRevisionBulkOp.execute();
  628. }
  629. catch (err) {
  630. if (err.code !== 11000) {
  631. throw new Error('Failed to revert pages: ', err);
  632. }
  633. }
  634. }
  635. async revertDeletedPage(page, user, options = {}, isRecursively = false) {
  636. const Page = this.crowi.model('Page');
  637. const PageTagRelation = this.crowi.model('PageTagRelation');
  638. const Revision = this.crowi.model('Revision');
  639. const newPath = Page.getRevertDeletedPageName(page.path);
  640. const originPage = await Page.findByPath(newPath);
  641. if (originPage != null) {
  642. // When the page is deleted, it will always be created with "redirectTo" in the path of the original page.
  643. // So, it's ok to delete the page
  644. // However, If a page exists that is not "redirectTo", something is wrong. (Data correction is needed).
  645. if (originPage.redirectTo !== page.path) {
  646. throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
  647. }
  648. await this.deleteCompletely(originPage, user, options, false, true);
  649. this.pageEvent.emit('revert', page, user);
  650. }
  651. if (isRecursively) {
  652. this.revertDeletedDescendantsWithStream(page, user, options);
  653. }
  654. page.status = Page.STATUS_PUBLISHED;
  655. page.lastUpdateUser = user;
  656. debug('Revert deleted the page', page, newPath);
  657. const updatedPage = await Page.findByIdAndUpdate(page._id, {
  658. $set: {
  659. path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  660. },
  661. }, { new: true });
  662. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
  663. await Revision.updateMany({ path: page.path }, { $set: { path: newPath } });
  664. return updatedPage;
  665. }
  666. /**
  667. * Create revert stream
  668. */
  669. async revertDeletedDescendantsWithStream(targetPage, user, options = {}) {
  670. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  671. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  672. let count = 0;
  673. const writeStream = new Writable({
  674. objectMode: true,
  675. async write(batch, encoding, callback) {
  676. try {
  677. count += batch.length;
  678. revertDeletedDescendants(batch, user);
  679. logger.debug(`Reverting pages progressing: (count=${count})`);
  680. }
  681. catch (err) {
  682. logger.error('revertPages error on add anyway: ', err);
  683. }
  684. callback();
  685. },
  686. final(callback) {
  687. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  688. callback();
  689. },
  690. });
  691. readStream
  692. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  693. .pipe(writeStream);
  694. }
  695. async handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId, user) {
  696. const Page = this.crowi.model('Page');
  697. const pages = await Page.find({ grantedGroup: deletedGroup });
  698. switch (action) {
  699. case 'public':
  700. await Promise.all(pages.map((page) => {
  701. return Page.publicizePage(page);
  702. }));
  703. break;
  704. case 'delete':
  705. return this.deleteMultipleCompletely(pages, user);
  706. case 'transfer':
  707. await Promise.all(pages.map((page) => {
  708. return Page.transferPageToGroup(page, transferToUserGroupId);
  709. }));
  710. break;
  711. default:
  712. throw new Error('Unknown action for private pages');
  713. }
  714. }
  715. validateCrowi() {
  716. if (this.crowi == null) {
  717. throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
  718. }
  719. }
  720. createAndSendNotifications = async function(page, user, action) {
  721. const { activityService, inAppNotificationService } = this.crowi;
  722. const snapshot = stringifySnapshot(page);
  723. // Create activity
  724. const parameters = {
  725. user: user._id,
  726. targetModel: ActivityDefine.MODEL_PAGE,
  727. target: page,
  728. action,
  729. };
  730. const activity = await activityService.createByParameters(parameters);
  731. // Get user to be notified
  732. const targetUsers = await activity.getNotificationTargetUsers();
  733. // Create and send notifications
  734. await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
  735. await inAppNotificationService.emitSocketIo(targetUsers);
  736. };
  737. }
  738. module.exports = PageService;