page.ts 63 KB

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