page.ts 66 KB

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