page.js 24 KB

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