page.ts 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851
  1. import { pagePathUtils } from '@growi/core';
  2. import mongoose, { QueryCursor } from 'mongoose';
  3. import escapeStringRegexp from 'escape-string-regexp';
  4. import streamToPromise from 'stream-to-promise';
  5. import pathlib from 'path';
  6. import { Readable, Writable } from 'stream';
  7. import { serializePageSecurely } from '../models/serializers/page-serializer';
  8. import { createBatchStream } from '~/server/util/batch-stream';
  9. import loggerFactory from '~/utils/logger';
  10. import {
  11. CreateMethod, generateGrantCondition, PageCreateOptions, PageModel,
  12. } from '~/server/models/page';
  13. import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
  14. import ActivityDefine from '../util/activityDefine';
  15. import { IPage } from '~/interfaces/page';
  16. const debug = require('debug')('growi:services:page');
  17. const logger = loggerFactory('growi:services:page');
  18. const {
  19. isCreatablePage, isDeletablePage, isTrashPage, collectAncestorPaths, isTopPage,
  20. } = pagePathUtils;
  21. const BULK_REINDEX_SIZE = 100;
  22. // TODO: improve type
  23. class PageCursorsForDescendantsFactory {
  24. private user: any; // TODO: Typescriptize model
  25. private rootPage: any; // TODO: wait for mongoose update
  26. private shouldIncludeEmpty: boolean;
  27. private initialCursor: QueryCursor<any>; // TODO: wait for mongoose update
  28. private Page: PageModel;
  29. constructor(user: any, rootPage: any, shouldIncludeEmpty: boolean) {
  30. this.user = user;
  31. this.rootPage = rootPage;
  32. this.shouldIncludeEmpty = shouldIncludeEmpty;
  33. this.Page = mongoose.model('Page') as unknown as PageModel;
  34. }
  35. // prepare initial cursor
  36. private async init() {
  37. const initialCursor = await this.generateCursorToFindChildren(this.rootPage);
  38. this.initialCursor = initialCursor;
  39. }
  40. /**
  41. * Returns Iterable that yields only descendant pages unorderedly
  42. * @returns Promise<AsyncGenerator>
  43. */
  44. async generateIterable(): Promise<AsyncGenerator> {
  45. // initialize cursor
  46. await this.init();
  47. return this.generateOnlyDescendants(this.initialCursor);
  48. }
  49. /**
  50. * Returns Readable that produces only descendant pages unorderedly
  51. * @returns Promise<Readable>
  52. */
  53. async generateReadable(): Promise<Readable> {
  54. return Readable.from(await this.generateIterable());
  55. }
  56. /**
  57. * Generator that unorderedly yields descendant pages
  58. */
  59. private async* generateOnlyDescendants(cursor: QueryCursor<any>) {
  60. for await (const page of cursor) {
  61. const nextCursor = await this.generateCursorToFindChildren(page);
  62. yield* this.generateOnlyDescendants(nextCursor); // recursively yield
  63. yield page;
  64. }
  65. }
  66. private async generateCursorToFindChildren(page: any): Promise<QueryCursor<any>> {
  67. const { PageQueryBuilder } = this.Page;
  68. const builder = new PageQueryBuilder(this.Page.find(), this.shouldIncludeEmpty);
  69. builder.addConditionToFilteringByParentId(page._id);
  70. await this.Page.addConditionToFilteringByViewerToEdit(builder, this.user);
  71. const cursor = builder.query.lean().cursor({ batchSize: BULK_REINDEX_SIZE }) as QueryCursor<any>;
  72. return cursor;
  73. }
  74. }
  75. class PageService {
  76. crowi: any;
  77. pageEvent: any;
  78. tagEvent: any;
  79. constructor(crowi) {
  80. this.crowi = crowi;
  81. this.pageEvent = crowi.event('page');
  82. this.tagEvent = crowi.event('tag');
  83. // init
  84. this.initPageEvent();
  85. }
  86. private initPageEvent() {
  87. // create
  88. this.pageEvent.on('create', this.pageEvent.onCreate);
  89. // createMany
  90. this.pageEvent.on('createMany', this.pageEvent.onCreateMany);
  91. this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
  92. // update
  93. this.pageEvent.on('update', async(page, user) => {
  94. this.pageEvent.onUpdate();
  95. try {
  96. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_UPDATE);
  97. }
  98. catch (err) {
  99. logger.error(err);
  100. }
  101. });
  102. // rename
  103. this.pageEvent.on('rename', async(page, user) => {
  104. try {
  105. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_RENAME);
  106. }
  107. catch (err) {
  108. logger.error(err);
  109. }
  110. });
  111. // delete
  112. this.pageEvent.on('delete', async(page, user) => {
  113. try {
  114. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE);
  115. }
  116. catch (err) {
  117. logger.error(err);
  118. }
  119. });
  120. // delete completely
  121. this.pageEvent.on('deleteCompletely', async(page, user) => {
  122. try {
  123. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE_COMPLETELY);
  124. }
  125. catch (err) {
  126. logger.error(err);
  127. }
  128. });
  129. // likes
  130. this.pageEvent.on('like', async(page, user) => {
  131. try {
  132. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_LIKE);
  133. }
  134. catch (err) {
  135. logger.error(err);
  136. }
  137. });
  138. // bookmark
  139. this.pageEvent.on('bookmark', async(page, user) => {
  140. try {
  141. await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_BOOKMARK);
  142. }
  143. catch (err) {
  144. logger.error(err);
  145. }
  146. });
  147. }
  148. async findPageAndMetaDataByViewer({ pageId, path, user }) {
  149. const Page = this.crowi.model('Page');
  150. let page;
  151. if (pageId != null) { // prioritized
  152. page = await Page.findByIdAndViewer(pageId, user);
  153. }
  154. else {
  155. page = await Page.findByPathAndViewer(path, user);
  156. }
  157. const result: any = {};
  158. if (page == null) {
  159. const isExist = await Page.count({ $or: [{ _id: pageId }, { path }] }) > 0;
  160. result.isForbidden = isExist;
  161. result.isNotFound = !isExist;
  162. result.isCreatable = isCreatablePage(path);
  163. result.isDeletable = false;
  164. result.canDeleteCompletely = false;
  165. result.page = page;
  166. return result;
  167. }
  168. result.page = page;
  169. result.isForbidden = false;
  170. result.isNotFound = false;
  171. result.isCreatable = false;
  172. result.isDeletable = isDeletablePage(path);
  173. result.isDeleted = page.isDeleted();
  174. result.canDeleteCompletely = user != null && user.canDeleteCompletely(page.creator);
  175. return result;
  176. }
  177. private shouldUseV4Process(page): boolean {
  178. const Page = mongoose.model('Page') as unknown as PageModel;
  179. const isPageMigrated = page.parent != null;
  180. const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  181. const isRoot = isTopPage(page.path);
  182. const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
  183. const isTrashed = isTrashPage(page.path);
  184. const shouldUseV4Process = !isRoot && !isPageRestricted && (!isV5Compatible || !isPageMigrated || !isTrashed);
  185. return shouldUseV4Process;
  186. }
  187. private shouldNormalizeParent(page) {
  188. const Page = mongoose.model('Page') as unknown as PageModel;
  189. return page.grant !== Page.GRANT_RESTRICTED && page.grant !== Page.GRANT_SPECIFIED;
  190. }
  191. /**
  192. * Generate read stream to operate descendants of the specified page path
  193. * @param {string} targetPagePath
  194. * @param {User} viewer
  195. */
  196. private async generateReadStreamToOperateOnlyDescendants(targetPagePath, userToOperate) {
  197. const Page = this.crowi.model('Page');
  198. const { PageQueryBuilder } = Page;
  199. const builder = new PageQueryBuilder(Page.find(), true)
  200. .addConditionAsNotMigrated() // to avoid affecting v5 pages
  201. .addConditionToExcludeRedirect()
  202. .addConditionToListOnlyDescendants(targetPagePath);
  203. await Page.addConditionToFilteringByViewerToEdit(builder, userToOperate);
  204. return builder
  205. .query
  206. .lean()
  207. .cursor({ batchSize: BULK_REINDEX_SIZE });
  208. }
  209. async renamePage(page, newPagePath, user, options) {
  210. const Page = this.crowi.model('Page');
  211. if (isTopPage(page.path)) {
  212. throw Error('It is forbidden to rename the top page');
  213. }
  214. // v4 compatible process
  215. const shouldUseV4Process = this.shouldUseV4Process(page);
  216. if (shouldUseV4Process) {
  217. return this.renamePageV4(page, newPagePath, user, options);
  218. }
  219. const updateMetadata = options.updateMetadata || false;
  220. // sanitize path
  221. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  222. // use the parent's grant when target page is an empty page
  223. let grant;
  224. let grantedUserIds;
  225. let grantedGroupId;
  226. if (page.isEmpty) {
  227. const parent = await Page.findOne({ _id: page.parent });
  228. if (parent == null) {
  229. throw Error('parent not found');
  230. }
  231. grant = parent.grant;
  232. grantedUserIds = parent.grantedUsers;
  233. grantedGroupId = parent.grantedGroup;
  234. }
  235. else {
  236. grant = page.grant;
  237. grantedUserIds = page.grantedUsers;
  238. grantedGroupId = page.grantedGroup;
  239. }
  240. /*
  241. * UserGroup & Owner validation
  242. */
  243. if (grant !== Page.GRANT_RESTRICTED) {
  244. let isGrantNormalized = false;
  245. try {
  246. const shouldCheckDescendants = false;
  247. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(newPagePath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
  248. }
  249. catch (err) {
  250. logger.error(`Failed to validate grant of page at "${newPagePath}" when renaming`, err);
  251. throw err;
  252. }
  253. if (!isGrantNormalized) {
  254. throw Error(`This page cannot be renamed to "${newPagePath}" since the selected grant or grantedGroup is not assignable to this page.`);
  255. }
  256. }
  257. // update descendants first
  258. await this.renameDescendantsWithStream(page, newPagePath, user, options, shouldUseV4Process);
  259. /*
  260. * TODO: https://redmine.weseek.co.jp/issues/86577
  261. * bulkWrite PageRedirectDocument if createRedirectPage is true
  262. */
  263. /*
  264. * update target
  265. */
  266. const update: Partial<IPage> = {};
  267. // find or create parent
  268. const newParent = await Page.getParentAndFillAncestors(newPagePath);
  269. // update Page
  270. update.path = newPagePath;
  271. update.parent = newParent._id;
  272. if (updateMetadata) {
  273. update.lastUpdateUser = user;
  274. update.updatedAt = new Date();
  275. }
  276. const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
  277. this.pageEvent.emit('rename', page, user);
  278. return renamedPage;
  279. }
  280. // !!renaming always include descendant pages!!
  281. private async renamePageV4(page, newPagePath, user, options) {
  282. const Page = this.crowi.model('Page');
  283. const Revision = this.crowi.model('Revision');
  284. const updateMetadata = options.updateMetadata || false;
  285. // sanitize path
  286. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  287. // create descendants first
  288. await this.renameDescendantsWithStream(page, newPagePath, user, options);
  289. const update: any = {};
  290. // update Page
  291. update.path = newPagePath;
  292. if (updateMetadata) {
  293. update.lastUpdateUser = user;
  294. update.updatedAt = Date.now();
  295. }
  296. const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
  297. // update Rivisions
  298. await Revision.updateRevisionListByPageId(renamedPage._id, { pageId: renamedPage._id });
  299. /*
  300. * TODO: https://redmine.weseek.co.jp/issues/86577
  301. * bulkWrite PageRedirectDocument if createRedirectPage is true
  302. */
  303. this.pageEvent.emit('rename', page, user);
  304. return renamedPage;
  305. }
  306. private async renameDescendants(pages, user, options, oldPagePathPrefix, newPagePathPrefix, shouldUseV4Process = true) {
  307. // v4 compatible process
  308. if (shouldUseV4Process) {
  309. return this.renameDescendantsV4(pages, user, options, oldPagePathPrefix, newPagePathPrefix);
  310. }
  311. const Page = this.crowi.model('Page');
  312. const { updateMetadata } = options;
  313. const updatePathOperations: any[] = [];
  314. pages.forEach((page) => {
  315. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  316. // increment updatePathOperations
  317. let update;
  318. if (updateMetadata && !page.isEmpty) {
  319. update = {
  320. $set: { path: newPagePath, lastUpdateUser: user._id, updatedAt: new Date() },
  321. };
  322. }
  323. else {
  324. update = {
  325. $set: { path: newPagePath },
  326. };
  327. }
  328. updatePathOperations.push({
  329. updateOne: {
  330. filter: {
  331. _id: page._id,
  332. },
  333. update,
  334. },
  335. });
  336. });
  337. try {
  338. await Page.bulkWrite(updatePathOperations);
  339. }
  340. catch (err) {
  341. if (err.code !== 11000) {
  342. throw new Error(`Failed to rename pages: ${err}`);
  343. }
  344. }
  345. this.pageEvent.emit('updateMany', pages, user);
  346. }
  347. private async renameDescendantsV4(pages, user, options, oldPagePathPrefix, newPagePathPrefix) {
  348. const pageCollection = mongoose.connection.collection('pages');
  349. const { updateMetadata } = options;
  350. const unorderedBulkOp = pageCollection.initializeUnorderedBulkOp();
  351. pages.forEach((page) => {
  352. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  353. if (updateMetadata) {
  354. unorderedBulkOp
  355. .find({ _id: page._id })
  356. .update({ $set: { path: newPagePath, lastUpdateUser: user._id, updatedAt: new Date() } });
  357. }
  358. else {
  359. unorderedBulkOp.find({ _id: page._id }).update({ $set: { path: newPagePath } });
  360. }
  361. });
  362. try {
  363. await unorderedBulkOp.execute();
  364. }
  365. catch (err) {
  366. if (err.code !== 11000) {
  367. throw new Error(`Failed to rename pages: ${err}`);
  368. }
  369. }
  370. this.pageEvent.emit('updateMany', pages, user);
  371. }
  372. private async renameDescendantsWithStream(targetPage, newPagePath, user, options = {}, shouldUseV4Process = true) {
  373. // v4 compatible process
  374. if (shouldUseV4Process) {
  375. return this.renameDescendantsWithStreamV4(targetPage, newPagePath, user, options);
  376. }
  377. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  378. const readStream = await factory.generateReadable();
  379. const newPagePathPrefix = newPagePath;
  380. const pathRegExp = new RegExp(`^${escapeStringRegexp(targetPage.path)}`, 'i');
  381. const renameDescendants = this.renameDescendants.bind(this);
  382. const pageEvent = this.pageEvent;
  383. let count = 0;
  384. const writeStream = new Writable({
  385. objectMode: true,
  386. async write(batch, encoding, callback) {
  387. try {
  388. count += batch.length;
  389. await renameDescendants(
  390. batch, user, options, pathRegExp, newPagePathPrefix, shouldUseV4Process,
  391. );
  392. logger.debug(`Renaming pages progressing: (count=${count})`);
  393. }
  394. catch (err) {
  395. logger.error('Renaming error on add anyway: ', err);
  396. }
  397. callback();
  398. },
  399. async final(callback) {
  400. logger.debug(`Renaming pages has completed: (totalCount=${count})`);
  401. // update path
  402. targetPage.path = newPagePath;
  403. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  404. callback();
  405. },
  406. });
  407. readStream
  408. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  409. .pipe(writeStream);
  410. await streamToPromise(readStream);
  411. }
  412. private async renameDescendantsWithStreamV4(targetPage, newPagePath, user, options = {}) {
  413. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  414. const newPagePathPrefix = newPagePath;
  415. const pathRegExp = new RegExp(`^${escapeStringRegexp(targetPage.path)}`, 'i');
  416. const renameDescendants = this.renameDescendants.bind(this);
  417. const pageEvent = this.pageEvent;
  418. let count = 0;
  419. const writeStream = new Writable({
  420. objectMode: true,
  421. async write(batch, encoding, callback) {
  422. try {
  423. count += batch.length;
  424. await renameDescendants(batch, user, options, pathRegExp, newPagePathPrefix);
  425. logger.debug(`Reverting pages progressing: (count=${count})`);
  426. }
  427. catch (err) {
  428. logger.error('revertPages error on add anyway: ', err);
  429. }
  430. callback();
  431. },
  432. final(callback) {
  433. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  434. // update path
  435. targetPage.path = newPagePath;
  436. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  437. callback();
  438. },
  439. });
  440. readStream
  441. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  442. .pipe(writeStream);
  443. await streamToPromise(readStream);
  444. }
  445. /*
  446. * Duplicate
  447. */
  448. async duplicate(page, newPagePath, user, isRecursively) {
  449. const Page = mongoose.model('Page') as unknown as PageModel;
  450. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  451. // v4 compatible process
  452. const shouldUseV4Process = this.shouldUseV4Process(page);
  453. if (shouldUseV4Process) {
  454. return this.duplicateV4(page, newPagePath, user, isRecursively);
  455. }
  456. // use the parent's grant when target page is an empty page
  457. let grant;
  458. let grantedUserIds;
  459. let grantedGroupId;
  460. if (page.isEmpty) {
  461. const parent = await Page.findOne({ _id: page.parent });
  462. if (parent == null) {
  463. throw Error('parent not found');
  464. }
  465. grant = parent.grant;
  466. grantedUserIds = parent.grantedUsers;
  467. grantedGroupId = parent.grantedGroup;
  468. }
  469. else {
  470. grant = page.grant;
  471. grantedUserIds = page.grantedUsers;
  472. grantedGroupId = page.grantedGroup;
  473. }
  474. /*
  475. * UserGroup & Owner validation
  476. */
  477. if (grant !== Page.GRANT_RESTRICTED) {
  478. let isGrantNormalized = false;
  479. try {
  480. const shouldCheckDescendants = false;
  481. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(newPagePath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
  482. }
  483. catch (err) {
  484. logger.error(`Failed to validate grant of page at "${newPagePath}" when duplicating`, err);
  485. throw err;
  486. }
  487. if (!isGrantNormalized) {
  488. throw Error(`This page cannot be duplicated to "${newPagePath}" since the selected grant or grantedGroup is not assignable to this page.`);
  489. }
  490. }
  491. // populate
  492. await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
  493. // create option
  494. const options: PageCreateOptions = {
  495. grant: page.grant,
  496. grantUserGroupId: page.grantedGroup,
  497. };
  498. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  499. const createdPage = await (Page.create as CreateMethod)(
  500. newPagePath, page.revision.body, user, options,
  501. );
  502. if (isRecursively) {
  503. this.duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process);
  504. }
  505. // take over tags
  506. const originTags = await page.findRelatedTagsById();
  507. let savedTags = [];
  508. if (originTags.length !== 0) {
  509. await PageTagRelation.updatePageTags(createdPage._id, originTags);
  510. savedTags = await PageTagRelation.listTagNamesByPage(createdPage._id);
  511. this.tagEvent.emit('update', createdPage, savedTags);
  512. }
  513. const result = serializePageSecurely(createdPage);
  514. result.tags = savedTags;
  515. return result;
  516. }
  517. async duplicateV4(page, newPagePath, user, isRecursively) {
  518. const Page = this.crowi.model('Page');
  519. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  520. // populate
  521. await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
  522. // create option
  523. const options: any = { page };
  524. options.grant = page.grant;
  525. options.grantUserGroupId = page.grantedGroup;
  526. options.grantedUserIds = page.grantedUsers;
  527. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  528. const createdPage = await Page.create(
  529. newPagePath, page.revision.body, user, options,
  530. );
  531. if (isRecursively) {
  532. this.duplicateDescendantsWithStream(page, newPagePath, user);
  533. }
  534. // take over tags
  535. const originTags = await page.findRelatedTagsById();
  536. let savedTags = [];
  537. if (originTags != null) {
  538. await PageTagRelation.updatePageTags(createdPage.id, originTags);
  539. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  540. this.tagEvent.emit('update', createdPage, savedTags);
  541. }
  542. const result = serializePageSecurely(createdPage);
  543. result.tags = savedTags;
  544. return result;
  545. }
  546. /**
  547. * Receive the object with oldPageId and newPageId and duplicate the tags from oldPage to newPage
  548. * @param {Object} pageIdMapping e.g. key: oldPageId, value: newPageId
  549. */
  550. private async duplicateTags(pageIdMapping) {
  551. const PageTagRelation = mongoose.model('PageTagRelation');
  552. // convert pageId from string to ObjectId
  553. const pageIds = Object.keys(pageIdMapping);
  554. const stage = { $or: pageIds.map((pageId) => { return { relatedPage: new mongoose.Types.ObjectId(pageId) } }) };
  555. const pagesAssociatedWithTag = await PageTagRelation.aggregate([
  556. {
  557. $match: stage,
  558. },
  559. {
  560. $group: {
  561. _id: '$relatedTag',
  562. relatedPages: { $push: '$relatedPage' },
  563. },
  564. },
  565. ]);
  566. const newPageTagRelation: any[] = [];
  567. pagesAssociatedWithTag.forEach(({ _id, relatedPages }) => {
  568. // relatedPages
  569. relatedPages.forEach((pageId) => {
  570. newPageTagRelation.push({
  571. relatedPage: pageIdMapping[pageId], // newPageId
  572. relatedTag: _id,
  573. });
  574. });
  575. });
  576. return PageTagRelation.insertMany(newPageTagRelation, { ordered: false });
  577. }
  578. private async duplicateDescendants(pages, user, oldPagePathPrefix, newPagePathPrefix, shouldUseV4Process = true) {
  579. if (shouldUseV4Process) {
  580. return this.duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix);
  581. }
  582. const Page = this.crowi.model('Page');
  583. const Revision = this.crowi.model('Revision');
  584. const pageIds = pages.map(page => page._id);
  585. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  586. // Mapping to set to the body of the new revision
  587. const pageIdRevisionMapping = {};
  588. revisions.forEach((revision) => {
  589. pageIdRevisionMapping[revision.pageId] = revision;
  590. });
  591. // key: oldPageId, value: newPageId
  592. const pageIdMapping = {};
  593. const newPages: any[] = [];
  594. const newRevisions: any[] = [];
  595. // no need to save parent here
  596. pages.forEach((page) => {
  597. const newPageId = new mongoose.Types.ObjectId();
  598. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  599. const revisionId = new mongoose.Types.ObjectId();
  600. pageIdMapping[page._id] = newPageId;
  601. let newPage;
  602. if (page.isEmpty) {
  603. newPage = {
  604. _id: newPageId,
  605. path: newPagePath,
  606. isEmpty: true,
  607. };
  608. }
  609. else {
  610. newPage = {
  611. _id: newPageId,
  612. path: newPagePath,
  613. creator: user._id,
  614. grant: page.grant,
  615. grantedGroup: page.grantedGroup,
  616. grantedUsers: page.grantedUsers,
  617. lastUpdateUser: user._id,
  618. revision: revisionId,
  619. };
  620. }
  621. newPages.push(newPage);
  622. newRevisions.push({
  623. _id: revisionId, path: newPagePath, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
  624. });
  625. });
  626. await Page.insertMany(newPages, { ordered: false });
  627. await Revision.insertMany(newRevisions, { ordered: false });
  628. await this.duplicateTags(pageIdMapping);
  629. }
  630. private async duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix) {
  631. const Page = this.crowi.model('Page');
  632. const Revision = this.crowi.model('Revision');
  633. const pageIds = pages.map(page => page._id);
  634. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  635. // Mapping to set to the body of the new revision
  636. const pageIdRevisionMapping = {};
  637. revisions.forEach((revision) => {
  638. pageIdRevisionMapping[revision.pageId] = revision;
  639. });
  640. // key: oldPageId, value: newPageId
  641. const pageIdMapping = {};
  642. const newPages: any[] = [];
  643. const newRevisions: any[] = [];
  644. pages.forEach((page) => {
  645. const newPageId = new mongoose.Types.ObjectId();
  646. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  647. const revisionId = new mongoose.Types.ObjectId();
  648. pageIdMapping[page._id] = newPageId;
  649. newPages.push({
  650. _id: newPageId,
  651. path: newPagePath,
  652. creator: user._id,
  653. grant: page.grant,
  654. grantedGroup: page.grantedGroup,
  655. grantedUsers: page.grantedUsers,
  656. lastUpdateUser: user._id,
  657. revision: revisionId,
  658. });
  659. newRevisions.push({
  660. _id: revisionId, path: newPagePath, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
  661. });
  662. });
  663. await Page.insertMany(newPages, { ordered: false });
  664. await Revision.insertMany(newRevisions, { ordered: false });
  665. await this.duplicateTags(pageIdMapping);
  666. }
  667. private async duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process = true) {
  668. if (shouldUseV4Process) {
  669. return this.duplicateDescendantsWithStreamV4(page, newPagePath, user);
  670. }
  671. const iterableFactory = new PageCursorsForDescendantsFactory(user, page, true);
  672. const readStream = await iterableFactory.generateReadable();
  673. const newPagePathPrefix = newPagePath;
  674. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  675. const duplicateDescendants = this.duplicateDescendants.bind(this);
  676. const shouldNormalizeParent = this.shouldNormalizeParent.bind(this);
  677. const normalizeParentRecursively = this.normalizeParentRecursively.bind(this);
  678. const pageEvent = this.pageEvent;
  679. let count = 0;
  680. const writeStream = new Writable({
  681. objectMode: true,
  682. async write(batch, encoding, callback) {
  683. try {
  684. count += batch.length;
  685. await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, shouldUseV4Process);
  686. logger.debug(`Adding pages progressing: (count=${count})`);
  687. }
  688. catch (err) {
  689. logger.error('addAllPages error on add anyway: ', err);
  690. }
  691. callback();
  692. },
  693. async final(callback) {
  694. const Page = mongoose.model('Page') as unknown as PageModel;
  695. // normalize parent of descendant pages
  696. const shouldNormalize = shouldNormalizeParent(page);
  697. if (shouldNormalize) {
  698. try {
  699. const escapedPath = escapeStringRegexp(newPagePath);
  700. const regexps = [new RegExp(`^${escapedPath}`, 'i')];
  701. await normalizeParentRecursively(null, regexps);
  702. logger.info(`Successfully normalized duplicated descendant pages under "${newPagePath}"`);
  703. }
  704. catch (err) {
  705. logger.error('Failed to normalize descendants afrer duplicate:', err);
  706. throw err;
  707. }
  708. }
  709. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  710. // update path
  711. page.path = newPagePath;
  712. pageEvent.emit('syncDescendantsUpdate', page, user);
  713. callback();
  714. },
  715. });
  716. readStream
  717. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  718. .pipe(writeStream);
  719. }
  720. private async duplicateDescendantsWithStreamV4(page, newPagePath, user) {
  721. const readStream = await this.generateReadStreamToOperateOnlyDescendants(page.path, user);
  722. const newPagePathPrefix = newPagePath;
  723. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  724. const duplicateDescendants = this.duplicateDescendants.bind(this);
  725. const pageEvent = this.pageEvent;
  726. let count = 0;
  727. const writeStream = new Writable({
  728. objectMode: true,
  729. async write(batch, encoding, callback) {
  730. try {
  731. count += batch.length;
  732. await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix);
  733. logger.debug(`Adding pages progressing: (count=${count})`);
  734. }
  735. catch (err) {
  736. logger.error('addAllPages error on add anyway: ', err);
  737. }
  738. callback();
  739. },
  740. final(callback) {
  741. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  742. // update path
  743. page.path = newPagePath;
  744. pageEvent.emit('syncDescendantsUpdate', page, user);
  745. callback();
  746. },
  747. });
  748. readStream
  749. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  750. .pipe(writeStream);
  751. }
  752. /*
  753. * Delete
  754. */
  755. async deletePage(page, user, options = {}, isRecursively = false) {
  756. const Page = mongoose.model('Page') as PageModel;
  757. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  758. const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
  759. // v4 compatible process
  760. const shouldUseV4Process = this.shouldUseV4Process(page);
  761. if (shouldUseV4Process) {
  762. return this.deletePageV4(page, user, options, isRecursively);
  763. }
  764. const newPath = Page.getDeletedPageName(page.path);
  765. const isTrashed = isTrashPage(page.path);
  766. if (isTrashed) {
  767. throw new Error('This method does NOT support deleting trashed pages.');
  768. }
  769. if (!Page.isDeletableName(page.path)) {
  770. throw new Error('Page is not deletable.');
  771. }
  772. if (isRecursively) {
  773. this.deleteDescendantsWithStream(page, user, shouldUseV4Process); // use the same process in both version v4 and v5
  774. }
  775. else {
  776. // replace with an empty page
  777. const shouldReplace = await Page.exists({ parent: page._id });
  778. if (shouldReplace) {
  779. await Page.replaceTargetWithEmptyPage(page);
  780. }
  781. }
  782. let deletedPage;
  783. // update Revisions
  784. if (page.isEmpty) {
  785. await Page.remove({ _id: page._id });
  786. }
  787. else {
  788. await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
  789. deletedPage = await Page.findByIdAndUpdate(page._id, {
  790. $set: {
  791. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, // set parent as null
  792. },
  793. }, { new: true });
  794. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
  795. /*
  796. * TODO: https://redmine.weseek.co.jp/issues/86577
  797. * bulkWrite PageRedirect documents
  798. */
  799. // const body = `redirect ${newPath}`;
  800. // await Page.create(page.path, body, user, { redirectTo: newPath });
  801. this.pageEvent.emit('delete', page, user);
  802. this.pageEvent.emit('create', deletedPage, user);
  803. }
  804. return deletedPage;
  805. }
  806. private async deletePageV4(page, user, options = {}, isRecursively = false) {
  807. const Page = mongoose.model('Page') as PageModel;
  808. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  809. const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
  810. const newPath = Page.getDeletedPageName(page.path);
  811. const isTrashed = isTrashPage(page.path);
  812. if (isTrashed) {
  813. throw new Error('This method does NOT support deleting trashed pages.');
  814. }
  815. if (!Page.isDeletableName(page.path)) {
  816. throw new Error('Page is not deletable.');
  817. }
  818. if (isRecursively) {
  819. this.deleteDescendantsWithStream(page, user);
  820. }
  821. // update Revisions
  822. await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
  823. const deletedPage = await Page.findByIdAndUpdate(page._id, {
  824. $set: {
  825. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
  826. },
  827. }, { new: true });
  828. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
  829. /*
  830. * TODO: https://redmine.weseek.co.jp/issues/86577
  831. * bulkWrite PageRedirect documents
  832. */
  833. // const body = `redirect ${newPath}`;
  834. // await Page.create(page.path, body, user, { redirectTo: newPath });
  835. this.pageEvent.emit('delete', page, user);
  836. this.pageEvent.emit('create', deletedPage, user);
  837. return deletedPage;
  838. }
  839. private async deleteDescendants(pages, user) {
  840. const Page = mongoose.model('Page') as PageModel;
  841. const deletePageOperations: any[] = [];
  842. pages.forEach((page) => {
  843. const newPath = Page.getDeletedPageName(page.path);
  844. let operation;
  845. // if empty, delete completely
  846. if (page.isEmpty) {
  847. operation = {
  848. deleteOne: {
  849. filter: { _id: page._id },
  850. },
  851. };
  852. }
  853. // if not empty, set parent to null and update to trash
  854. else {
  855. operation = {
  856. updateOne: {
  857. filter: { _id: page._id },
  858. update: {
  859. $set: {
  860. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, // set parent as null
  861. },
  862. },
  863. },
  864. };
  865. }
  866. deletePageOperations.push(operation);
  867. });
  868. try {
  869. await Page.bulkWrite(deletePageOperations);
  870. }
  871. catch (err) {
  872. if (err.code !== 11000) {
  873. throw new Error(`Failed to delete pages: ${err}`);
  874. }
  875. }
  876. finally {
  877. this.pageEvent.emit('syncDescendantsDelete', pages, user);
  878. }
  879. }
  880. /**
  881. * Create delete stream
  882. */
  883. private async deleteDescendantsWithStream(targetPage, user, shouldUseV4Process = true) {
  884. let readStream;
  885. if (shouldUseV4Process) {
  886. readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  887. }
  888. else {
  889. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  890. readStream = await factory.generateReadable();
  891. }
  892. const deleteDescendants = this.deleteDescendants.bind(this);
  893. let count = 0;
  894. const writeStream = new Writable({
  895. objectMode: true,
  896. async write(batch, encoding, callback) {
  897. try {
  898. count += batch.length;
  899. await deleteDescendants(batch, user);
  900. logger.debug(`Reverting pages progressing: (count=${count})`);
  901. }
  902. catch (err) {
  903. logger.error('revertPages error on add anyway: ', err);
  904. }
  905. callback();
  906. },
  907. final(callback) {
  908. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  909. callback();
  910. },
  911. });
  912. readStream
  913. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  914. .pipe(writeStream);
  915. }
  916. private async deleteCompletelyOperation(pageIds, pagePaths) {
  917. // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
  918. const Bookmark = this.crowi.model('Bookmark');
  919. const Comment = this.crowi.model('Comment');
  920. const Page = this.crowi.model('Page');
  921. const PageTagRelation = this.crowi.model('PageTagRelation');
  922. const ShareLink = this.crowi.model('ShareLink');
  923. const Revision = this.crowi.model('Revision');
  924. const Attachment = this.crowi.model('Attachment');
  925. const { attachmentService } = this.crowi;
  926. const attachments = await Attachment.find({ page: { $in: pageIds } });
  927. /*
  928. * TODO: https://redmine.weseek.co.jp/issues/86577
  929. * deleteMany related PageRedirect documents
  930. */
  931. // const pages = await Page.find({ redirectTo: { $ne: null } });
  932. // const redirectToPagePathMapping = {};
  933. // pages.forEach((page) => {
  934. // redirectToPagePathMapping[page.redirectTo] = page.path;
  935. // });
  936. // const redirectedFromPagePaths: any[] = [];
  937. // pagePaths.forEach((pagePath) => {
  938. // redirectedFromPagePaths.push(...this.prepareShoudDeletePagesByRedirectTo(pagePath, redirectToPagePathMapping));
  939. // });
  940. return Promise.all([
  941. Bookmark.deleteMany({ page: { $in: pageIds } }),
  942. Comment.deleteMany({ page: { $in: pageIds } }),
  943. PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
  944. ShareLink.deleteMany({ relatedPage: { $in: pageIds } }),
  945. Revision.deleteMany({ path: { $in: pagePaths } }),
  946. Page.deleteMany({ $or: [{ path: { $in: pagePaths } }, { _id: { $in: pageIds } }] }),
  947. // TODO: https://redmine.weseek.co.jp/issues/86577
  948. // Page.deleteMany({ $or: [{ path: { $in: pagePaths } }, { path: { $in: redirectedFromPagePaths } }, { _id: { $in: pageIds } }] }),
  949. attachmentService.removeAllAttachments(attachments),
  950. ]);
  951. }
  952. // delete multiple pages
  953. private async deleteMultipleCompletely(pages, user, options = {}) {
  954. const ids = pages.map(page => (page._id));
  955. const paths = pages.map(page => (page.path));
  956. logger.debug('Deleting completely', paths);
  957. await this.deleteCompletelyOperation(ids, paths);
  958. this.pageEvent.emit('syncDescendantsDelete', pages, user); // update as renamed page
  959. return;
  960. }
  961. async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false) {
  962. const Page = mongoose.model('Page') as PageModel;
  963. if (isTopPage(page.path)) {
  964. throw Error('It is forbidden to delete the top page');
  965. }
  966. // v4 compatible process
  967. const shouldUseV4Process = this.shouldUseV4Process(page);
  968. if (shouldUseV4Process) {
  969. return this.deleteCompletelyV4(page, user, options, isRecursively, preventEmitting);
  970. }
  971. const ids = [page._id];
  972. const paths = [page.path];
  973. logger.debug('Deleting completely', paths);
  974. // replace with an empty page
  975. const shouldReplace = !isRecursively && !isTrashPage(page.path) && await Page.exists({ parent: page._id });
  976. if (shouldReplace) {
  977. await Page.replaceTargetWithEmptyPage(page);
  978. }
  979. await this.deleteCompletelyOperation(ids, paths);
  980. if (isRecursively) {
  981. this.deleteCompletelyDescendantsWithStream(page, user, options, shouldUseV4Process);
  982. }
  983. if (!page.isEmpty && !preventEmitting) {
  984. this.pageEvent.emit('deleteCompletely', page, user);
  985. }
  986. return;
  987. }
  988. private async deleteCompletelyV4(page, user, options = {}, isRecursively = false, preventEmitting = false) {
  989. const ids = [page._id];
  990. const paths = [page.path];
  991. logger.debug('Deleting completely', paths);
  992. await this.deleteCompletelyOperation(ids, paths);
  993. if (isRecursively) {
  994. this.deleteCompletelyDescendantsWithStream(page, user, options);
  995. }
  996. if (!page.isEmpty && !preventEmitting) {
  997. this.pageEvent.emit('deleteCompletely', page, user);
  998. }
  999. return;
  1000. }
  1001. async emptyTrashPage(user, options = {}) {
  1002. return this.deleteCompletelyDescendantsWithStream({ path: '/trash' }, user, options);
  1003. }
  1004. /**
  1005. * Create delete completely stream
  1006. */
  1007. private async deleteCompletelyDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true) {
  1008. let readStream;
  1009. if (shouldUseV4Process) { // pages don't have parents
  1010. readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1011. }
  1012. else {
  1013. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  1014. readStream = await factory.generateReadable();
  1015. }
  1016. const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
  1017. let count = 0;
  1018. const writeStream = new Writable({
  1019. objectMode: true,
  1020. async write(batch, encoding, callback) {
  1021. try {
  1022. count += batch.length;
  1023. await deleteMultipleCompletely(batch, user, options);
  1024. logger.debug(`Adding pages progressing: (count=${count})`);
  1025. }
  1026. catch (err) {
  1027. logger.error('addAllPages error on add anyway: ', err);
  1028. }
  1029. callback();
  1030. },
  1031. final(callback) {
  1032. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1033. callback();
  1034. },
  1035. });
  1036. readStream
  1037. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1038. .pipe(writeStream);
  1039. }
  1040. private async revertDeletedDescendants(pages, user) {
  1041. const Page = this.crowi.model('Page');
  1042. const pageCollection = mongoose.connection.collection('pages');
  1043. const revisionCollection = mongoose.connection.collection('revisions');
  1044. const removePageBulkOp = pageCollection.initializeUnorderedBulkOp();
  1045. const revertPageBulkOp = pageCollection.initializeUnorderedBulkOp();
  1046. const revertRevisionBulkOp = revisionCollection.initializeUnorderedBulkOp();
  1047. // e.g. key: '/test'
  1048. const pathToPageMapping = {};
  1049. const toPaths = pages.map(page => Page.getRevertDeletedPageName(page.path));
  1050. const toPages = await Page.find({ path: { $in: toPaths } });
  1051. toPages.forEach((toPage) => {
  1052. pathToPageMapping[toPage.path] = toPage;
  1053. });
  1054. pages.forEach((page) => {
  1055. // e.g. page.path = /trash/test, toPath = /test
  1056. const toPath = Page.getRevertDeletedPageName(page.path);
  1057. if (pathToPageMapping[toPath] != null) {
  1058. // When the page is deleted, it will always be created with "redirectTo" in the path of the original page.
  1059. // So, it's ok to delete the page
  1060. // However, If a page exists that is not "redirectTo", something is wrong. (Data correction is needed).
  1061. if (pathToPageMapping[toPath].redirectTo === page.path) {
  1062. removePageBulkOp.find({ path: toPath }).delete();
  1063. }
  1064. }
  1065. revertPageBulkOp.find({ _id: page._id }).update({
  1066. $set: {
  1067. path: toPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  1068. },
  1069. });
  1070. revertRevisionBulkOp.find({ path: page.path }).update({ $set: { path: toPath } });
  1071. });
  1072. try {
  1073. await removePageBulkOp.execute();
  1074. await revertPageBulkOp.execute();
  1075. await revertRevisionBulkOp.execute();
  1076. }
  1077. catch (err) {
  1078. if (err.code !== 11000) {
  1079. throw new Error(`Failed to revert pages: ${err}`);
  1080. }
  1081. }
  1082. }
  1083. async revertDeletedPage(page, user, options = {}, isRecursively = false) {
  1084. const Page = this.crowi.model('Page');
  1085. const PageTagRelation = this.crowi.model('PageTagRelation');
  1086. const Revision = this.crowi.model('Revision');
  1087. const newPath = Page.getRevertDeletedPageName(page.path);
  1088. const originPage = await Page.findByPath(newPath);
  1089. if (originPage != null) {
  1090. // When the page is deleted, it will always be created with "redirectTo" in the path of the original page.
  1091. // So, it's ok to delete the page
  1092. // However, If a page exists that is not "redirectTo", something is wrong. (Data correction is needed).
  1093. if (originPage.redirectTo !== page.path) {
  1094. throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
  1095. }
  1096. await this.deleteCompletely(originPage, user, options, false, true);
  1097. this.pageEvent.emit('revert', page, user);
  1098. }
  1099. if (isRecursively) {
  1100. this.revertDeletedDescendantsWithStream(page, user, options);
  1101. }
  1102. page.status = Page.STATUS_PUBLISHED;
  1103. page.lastUpdateUser = user;
  1104. debug('Revert deleted the page', page, newPath);
  1105. const updatedPage = await Page.findByIdAndUpdate(page._id, {
  1106. $set: {
  1107. path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  1108. },
  1109. }, { new: true });
  1110. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
  1111. await Revision.updateMany({ path: page.path }, { $set: { path: newPath } });
  1112. return updatedPage;
  1113. }
  1114. /**
  1115. * Create revert stream
  1116. */
  1117. private async revertDeletedDescendantsWithStream(targetPage, user, options = {}) {
  1118. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1119. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  1120. let count = 0;
  1121. const writeStream = new Writable({
  1122. objectMode: true,
  1123. async write(batch, encoding, callback) {
  1124. try {
  1125. count += batch.length;
  1126. revertDeletedDescendants(batch, user);
  1127. logger.debug(`Reverting pages progressing: (count=${count})`);
  1128. }
  1129. catch (err) {
  1130. logger.error('revertPages error on add anyway: ', err);
  1131. }
  1132. callback();
  1133. },
  1134. final(callback) {
  1135. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  1136. callback();
  1137. },
  1138. });
  1139. readStream
  1140. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1141. .pipe(writeStream);
  1142. }
  1143. async handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroupId, user) {
  1144. const Page = this.crowi.model('Page');
  1145. const pages = await Page.find({ grantedGroup: { $in: groupsToDelete } });
  1146. switch (action) {
  1147. case 'public':
  1148. await Page.publicizePages(pages);
  1149. break;
  1150. case 'delete':
  1151. return this.deleteMultipleCompletely(pages, user);
  1152. case 'transfer':
  1153. await Page.transferPagesToGroup(pages, transferToUserGroupId);
  1154. break;
  1155. default:
  1156. throw new Error('Unknown action for private pages');
  1157. }
  1158. }
  1159. async shortBodiesMapByPageIds(pageIds: string[] = [], user) {
  1160. const Page = mongoose.model('Page');
  1161. const MAX_LENGTH = 350;
  1162. // aggregation options
  1163. const viewerCondition = await generateGrantCondition(user, null);
  1164. const filterByIds = {
  1165. _id: { $in: pageIds.map(id => new mongoose.Types.ObjectId(id)) },
  1166. };
  1167. let pages;
  1168. try {
  1169. pages = await Page
  1170. .aggregate([
  1171. // filter by pageIds
  1172. {
  1173. $match: filterByIds,
  1174. },
  1175. // filter by viewer
  1176. viewerCondition,
  1177. // lookup: https://docs.mongodb.com/v4.4/reference/operator/aggregation/lookup/
  1178. {
  1179. $lookup: {
  1180. from: 'revisions',
  1181. let: { localRevision: '$revision' },
  1182. pipeline: [
  1183. {
  1184. $match: {
  1185. $expr: {
  1186. $eq: ['$_id', '$$localRevision'],
  1187. },
  1188. },
  1189. },
  1190. {
  1191. $project: {
  1192. // What is $substrCP?
  1193. // see: https://stackoverflow.com/questions/43556024/mongodb-error-substrbytes-invalid-range-ending-index-is-in-the-middle-of-a-ut/43556249
  1194. revision: { $substrCP: ['$body', 0, MAX_LENGTH] },
  1195. },
  1196. },
  1197. ],
  1198. as: 'revisionData',
  1199. },
  1200. },
  1201. // projection
  1202. {
  1203. $project: {
  1204. _id: 1,
  1205. revisionData: 1,
  1206. },
  1207. },
  1208. ]).exec();
  1209. }
  1210. catch (err) {
  1211. logger.error('Error occurred while generating shortBodiesMap');
  1212. throw err;
  1213. }
  1214. const shortBodiesMap = {};
  1215. pages.forEach((page) => {
  1216. shortBodiesMap[page._id] = page.revisionData?.[0]?.revision;
  1217. });
  1218. return shortBodiesMap;
  1219. }
  1220. private async createAndSendNotifications(page, user, action) {
  1221. const { activityService, inAppNotificationService } = this.crowi;
  1222. const snapshot = stringifySnapshot(page);
  1223. // Create activity
  1224. const parameters = {
  1225. user: user._id,
  1226. targetModel: ActivityDefine.MODEL_PAGE,
  1227. target: page,
  1228. action,
  1229. };
  1230. const activity = await activityService.createByParameters(parameters);
  1231. // Get user to be notified
  1232. const targetUsers = await activity.getNotificationTargetUsers();
  1233. // Create and send notifications
  1234. await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
  1235. await inAppNotificationService.emitSocketIo(targetUsers);
  1236. }
  1237. async v5MigrationByPageIds(pageIds) {
  1238. const Page = mongoose.model('Page');
  1239. if (pageIds == null || pageIds.length === 0) {
  1240. logger.error('pageIds is null or 0 length.');
  1241. return;
  1242. }
  1243. // generate regexps
  1244. const regexps = await this._generateRegExpsByPageIds(pageIds);
  1245. // migrate recursively
  1246. try {
  1247. await this.normalizeParentRecursively(null, regexps);
  1248. }
  1249. catch (err) {
  1250. logger.error('V5 initial miration failed.', err);
  1251. // socket.emit('v5InitialMirationFailed', { error: err.message }); TODO: use socket to tell user
  1252. throw err;
  1253. }
  1254. }
  1255. async _isPagePathIndexUnique() {
  1256. const Page = this.crowi.model('Page');
  1257. const now = (new Date()).toString();
  1258. const path = `growi_check_is_path_index_unique_${now}`;
  1259. let isUnique = false;
  1260. try {
  1261. await Page.insertMany([
  1262. { path },
  1263. { path },
  1264. ]);
  1265. }
  1266. catch (err) {
  1267. if (err?.code === 11000) { // Error code 11000 indicates the index is unique
  1268. isUnique = true;
  1269. logger.info('Page path index is unique.');
  1270. }
  1271. else {
  1272. throw err;
  1273. }
  1274. }
  1275. finally {
  1276. await Page.deleteMany({ path: { $regex: new RegExp('growi_check_is_path_index_unique', 'g') } });
  1277. }
  1278. return isUnique;
  1279. }
  1280. // TODO: use socket to send status to the client
  1281. async v5InitialMigration(grant) {
  1282. // const socket = this.crowi.socketIoService.getAdminSocket();
  1283. let isUnique;
  1284. try {
  1285. isUnique = await this._isPagePathIndexUnique();
  1286. }
  1287. catch (err) {
  1288. logger.error('Failed to check path index status', err);
  1289. throw err;
  1290. }
  1291. // drop unique index first
  1292. if (isUnique) {
  1293. try {
  1294. await this._v5NormalizeIndex();
  1295. }
  1296. catch (err) {
  1297. logger.error('V5 index normalization failed.', err);
  1298. // socket.emit('v5IndexNormalizationFailed', { error: err.message });
  1299. throw err;
  1300. }
  1301. }
  1302. // then migrate
  1303. try {
  1304. await this.normalizeParentRecursively(grant, null, true);
  1305. }
  1306. catch (err) {
  1307. logger.error('V5 initial miration failed.', err);
  1308. // socket.emit('v5InitialMirationFailed', { error: err.message });
  1309. throw err;
  1310. }
  1311. // update descendantCount of all public pages
  1312. try {
  1313. await this.updateDescendantCountOfSelfAndDescendants('/');
  1314. logger.info('Successfully updated all descendantCount of public pages.');
  1315. }
  1316. catch (err) {
  1317. logger.error('Failed updating descendantCount of public pages.', err);
  1318. throw err;
  1319. }
  1320. await this._setIsV5CompatibleTrue();
  1321. }
  1322. /*
  1323. * returns an array of js RegExp instance instead of RE2 instance for mongo filter
  1324. */
  1325. private async _generateRegExpsByPageIds(pageIds) {
  1326. const Page = mongoose.model('Page') as unknown as PageModel;
  1327. let result;
  1328. try {
  1329. result = await Page.findListByPageIds(pageIds, null, false);
  1330. }
  1331. catch (err) {
  1332. logger.error('Failed to find pages by ids', err);
  1333. throw err;
  1334. }
  1335. const { pages } = result;
  1336. const regexps = pages.map(page => new RegExp(`^${page.path}`));
  1337. return regexps;
  1338. }
  1339. private async _setIsV5CompatibleTrue() {
  1340. try {
  1341. await this.crowi.configManager.updateConfigsInTheSameNamespace('crowi', {
  1342. 'app:isV5Compatible': true,
  1343. });
  1344. logger.info('Successfully migrated all public pages.');
  1345. }
  1346. catch (err) {
  1347. logger.warn('Failed to update app:isV5Compatible to true.');
  1348. throw err;
  1349. }
  1350. }
  1351. // TODO: use websocket to show progress
  1352. async normalizeParentRecursively(grant, regexps, publicOnly = false): Promise<void> {
  1353. const BATCH_SIZE = 100;
  1354. const PAGES_LIMIT = 1000;
  1355. const Page = this.crowi.model('Page');
  1356. const { PageQueryBuilder } = Page;
  1357. // generate filter
  1358. let filter: any = {
  1359. parent: null,
  1360. path: { $ne: '/' },
  1361. };
  1362. if (grant != null) {
  1363. filter = {
  1364. ...filter,
  1365. grant,
  1366. };
  1367. }
  1368. if (regexps != null && regexps.length !== 0) {
  1369. filter = {
  1370. ...filter,
  1371. path: {
  1372. $in: regexps,
  1373. },
  1374. };
  1375. }
  1376. const total = await Page.countDocuments(filter);
  1377. let baseAggregation = Page
  1378. .aggregate([
  1379. {
  1380. $match: filter,
  1381. },
  1382. {
  1383. $project: { // minimize data to fetch
  1384. _id: 1,
  1385. path: 1,
  1386. },
  1387. },
  1388. ]);
  1389. // limit pages to get
  1390. if (total > PAGES_LIMIT) {
  1391. baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
  1392. }
  1393. const pagesStream = await baseAggregation.cursor({ batchSize: BATCH_SIZE });
  1394. // use batch stream
  1395. const batchStream = createBatchStream(BATCH_SIZE);
  1396. let countPages = 0;
  1397. let shouldContinue = true;
  1398. // migrate all siblings for each page
  1399. const migratePagesStream = new Writable({
  1400. objectMode: true,
  1401. async write(pages, encoding, callback) {
  1402. // make list to create empty pages
  1403. const parentPathsSet = new Set(pages.map(page => pathlib.dirname(page.path)));
  1404. const parentPaths = Array.from(parentPathsSet);
  1405. // fill parents with empty pages
  1406. await Page.createEmptyPagesByPaths(parentPaths, publicOnly);
  1407. // find parents again
  1408. const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }), true);
  1409. const parents = await builder
  1410. .addConditionToListByPathsArray(parentPaths)
  1411. .query
  1412. .lean()
  1413. .exec();
  1414. // bulkWrite to update parent
  1415. const updateManyOperations = parents.map((parent) => {
  1416. const parentId = parent._id;
  1417. // modify to adjust for RegExp
  1418. let parentPath = parent.path === '/' ? '' : parent.path;
  1419. parentPath = escapeStringRegexp(parentPath);
  1420. const filter: any = {
  1421. // regexr.com/6889f
  1422. // ex. /parent/any_child OR /any_level1
  1423. path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'i') },
  1424. };
  1425. if (grant != null) {
  1426. filter.grant = grant;
  1427. }
  1428. return {
  1429. updateMany: {
  1430. filter,
  1431. update: {
  1432. parent: parentId,
  1433. },
  1434. },
  1435. };
  1436. });
  1437. try {
  1438. const res = await Page.bulkWrite(updateManyOperations);
  1439. countPages += res.result.nModified;
  1440. logger.info(`Page migration processing: (count=${countPages})`);
  1441. // throw
  1442. if (res.result.writeErrors.length > 0) {
  1443. logger.error('Failed to migrate some pages', res.result.writeErrors);
  1444. throw Error('Failed to migrate some pages');
  1445. }
  1446. // finish migration
  1447. if (res.result.nModified === 0 && res.result.nMatched === 0) {
  1448. shouldContinue = false;
  1449. logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
  1450. }
  1451. }
  1452. catch (err) {
  1453. logger.error('Failed to update page.parent.', err);
  1454. throw err;
  1455. }
  1456. callback();
  1457. },
  1458. final(callback) {
  1459. callback();
  1460. },
  1461. });
  1462. pagesStream
  1463. .pipe(batchStream)
  1464. .pipe(migratePagesStream);
  1465. await streamToPromise(migratePagesStream);
  1466. if (await Page.exists(filter) && shouldContinue) {
  1467. return this.normalizeParentRecursively(grant, regexps, publicOnly);
  1468. }
  1469. }
  1470. private async _v5NormalizeIndex() {
  1471. const collection = mongoose.connection.collection('pages');
  1472. try {
  1473. // drop pages.path_1 indexes
  1474. await collection.dropIndex('path_1');
  1475. logger.info('Succeeded to drop unique indexes from pages.path.');
  1476. }
  1477. catch (err) {
  1478. logger.warn('Failed to drop unique indexes from pages.path.', err);
  1479. throw err;
  1480. }
  1481. try {
  1482. // create indexes without
  1483. await collection.createIndex({ path: 1 }, { unique: false });
  1484. logger.info('Succeeded to create non-unique indexes on pages.path.');
  1485. }
  1486. catch (err) {
  1487. logger.warn('Failed to create non-unique indexes on pages.path.', err);
  1488. throw err;
  1489. }
  1490. }
  1491. async v5MigratablePrivatePagesCount(user) {
  1492. if (user == null) {
  1493. throw Error('user is required');
  1494. }
  1495. const Page = this.crowi.model('Page');
  1496. return Page.count({ parent: null, creator: user, grant: { $ne: Page.GRANT_PUBLIC } });
  1497. }
  1498. /**
  1499. * update descendantCount of the following pages
  1500. * - page that has the same path as the provided path
  1501. * - pages that are descendants of the above page
  1502. */
  1503. async updateDescendantCountOfSelfAndDescendants(path = '/') {
  1504. const BATCH_SIZE = 200;
  1505. const Page = this.crowi.model('Page');
  1506. const aggregateCondition = Page.getAggrConditionForPageWithProvidedPathAndDescendants(path);
  1507. const aggregatedPages = await Page.aggregate(aggregateCondition).cursor({ batchSize: BATCH_SIZE });
  1508. const recountWriteStream = new Writable({
  1509. objectMode: true,
  1510. async write(pageDocuments, encoding, callback) {
  1511. for (const document of pageDocuments) {
  1512. // eslint-disable-next-line no-await-in-loop
  1513. await Page.recountDescendantCountOfSelfAndDescendants(document._id);
  1514. }
  1515. callback();
  1516. },
  1517. final(callback) {
  1518. callback();
  1519. },
  1520. });
  1521. aggregatedPages
  1522. .pipe(createBatchStream(BATCH_SIZE))
  1523. .pipe(recountWriteStream);
  1524. await streamToPromise(recountWriteStream);
  1525. }
  1526. // update descendantCount of all pages that are ancestors of a provided path by count
  1527. async updateDescendantCountOfAncestors(path = '/', count = 0) {
  1528. const Page = this.crowi.model('Page');
  1529. const ancestors = collectAncestorPaths(path);
  1530. await Page.incrementDescendantCountOfPaths(ancestors, count);
  1531. }
  1532. }
  1533. export default PageService;