page.ts 117 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634
  1. import pathlib from 'path';
  2. import { Readable, Writable } from 'stream';
  3. import { pagePathUtils, pathUtils } from '@growi/core';
  4. import escapeStringRegexp from 'escape-string-regexp';
  5. import mongoose, { ObjectId, QueryCursor } from 'mongoose';
  6. import streamToPromise from 'stream-to-promise';
  7. import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
  8. import { Ref } from '~/interfaces/common';
  9. import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
  10. import { HasObjectId } from '~/interfaces/has-object-id';
  11. import {
  12. IPage, IPageInfo, IPageInfoForEntity, IPageWithMeta,
  13. } from '~/interfaces/page';
  14. import {
  15. PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
  16. } from '~/interfaces/page-delete-config';
  17. import { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
  18. import { IUserHasId } from '~/interfaces/user';
  19. import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
  20. import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
  21. import {
  22. CreateMethod, PageCreateOptions, PageModel, PageDocument, pushRevision, PageQueryBuilder,
  23. } from '~/server/models/page';
  24. import { createBatchStream } from '~/server/util/batch-stream';
  25. import loggerFactory from '~/utils/logger';
  26. import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
  27. import { ObjectIdLike } from '../interfaces/mongoose-utils';
  28. import { PathAlreadyExistsError } from '../models/errors';
  29. import PageOperation, { PageActionStage, PageActionType, PageOperationDocument } from '../models/page-operation';
  30. import { PageRedirectModel } from '../models/page-redirect';
  31. import { serializePageSecurely } from '../models/serializers/page-serializer';
  32. import Subscription from '../models/subscription';
  33. import { V5ConversionError } from '../models/vo/v5-conversion-error';
  34. const debug = require('debug')('growi:services:page');
  35. const logger = loggerFactory('growi:services:page');
  36. const {
  37. isTrashPage, isTopPage, omitDuplicateAreaPageFromPages,
  38. collectAncestorPaths, isMovablePage, canMoveByPath, hasSlash, generateChildrenRegExp,
  39. } = pagePathUtils;
  40. const { addTrailingSlash } = pathUtils;
  41. const BULK_REINDEX_SIZE = 100;
  42. const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  43. // TODO: improve type
  44. class PageCursorsForDescendantsFactory {
  45. private user: any; // TODO: Typescriptize model
  46. private rootPage: any; // TODO: wait for mongoose update
  47. private shouldIncludeEmpty: boolean;
  48. private initialCursor: QueryCursor<any> | never[]; // TODO: wait for mongoose update
  49. private Page: PageModel;
  50. constructor(user: any, rootPage: any, shouldIncludeEmpty: boolean) {
  51. this.user = user;
  52. this.rootPage = rootPage;
  53. this.shouldIncludeEmpty = shouldIncludeEmpty;
  54. this.Page = mongoose.model('Page') as unknown as PageModel;
  55. }
  56. // prepare initial cursor
  57. private async init() {
  58. const initialCursor = await this.generateCursorToFindChildren(this.rootPage);
  59. this.initialCursor = initialCursor;
  60. }
  61. /**
  62. * Returns Iterable that yields only descendant pages unorderedly
  63. * @returns Promise<AsyncGenerator>
  64. */
  65. async generateIterable(): Promise<AsyncGenerator | never[]> {
  66. // initialize cursor
  67. await this.init();
  68. return this.isNeverArray(this.initialCursor) ? [] : this.generateOnlyDescendants(this.initialCursor);
  69. }
  70. /**
  71. * Returns Readable that produces only descendant pages unorderedly
  72. * @returns Promise<Readable>
  73. */
  74. async generateReadable(): Promise<Readable> {
  75. return Readable.from(await this.generateIterable());
  76. }
  77. /**
  78. * Generator that unorderedly yields descendant pages
  79. */
  80. private async* generateOnlyDescendants(cursor: QueryCursor<any>) {
  81. for await (const page of cursor) {
  82. const nextCursor = await this.generateCursorToFindChildren(page);
  83. if (!this.isNeverArray(nextCursor)) {
  84. yield* this.generateOnlyDescendants(nextCursor); // recursively yield
  85. }
  86. yield page;
  87. }
  88. }
  89. private async generateCursorToFindChildren(page: any): Promise<QueryCursor<any> | never[]> {
  90. if (page == null) {
  91. return [];
  92. }
  93. const { PageQueryBuilder } = this.Page;
  94. const builder = new PageQueryBuilder(this.Page.find(), this.shouldIncludeEmpty);
  95. builder.addConditionToFilteringByParentId(page._id);
  96. const cursor = builder.query.lean().cursor({ batchSize: BULK_REINDEX_SIZE }) as QueryCursor<any>;
  97. return cursor;
  98. }
  99. private isNeverArray(val: QueryCursor<any> | never[]): val is never[] {
  100. return 'length' in val && val.length === 0;
  101. }
  102. }
  103. class PageService {
  104. crowi: any;
  105. pageEvent: any;
  106. tagEvent: any;
  107. constructor(crowi) {
  108. this.crowi = crowi;
  109. this.pageEvent = crowi.event('page');
  110. this.tagEvent = crowi.event('tag');
  111. // init
  112. this.initPageEvent();
  113. }
  114. private initPageEvent() {
  115. // create
  116. this.pageEvent.on('create', this.pageEvent.onCreate);
  117. // createMany
  118. this.pageEvent.on('createMany', this.pageEvent.onCreateMany);
  119. this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
  120. // update
  121. this.pageEvent.on('update', async(page, user) => {
  122. this.pageEvent.onUpdate();
  123. try {
  124. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_UPDATE);
  125. }
  126. catch (err) {
  127. logger.error(err);
  128. }
  129. });
  130. // rename
  131. this.pageEvent.on('rename', async(page, user) => {
  132. try {
  133. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_RENAME);
  134. }
  135. catch (err) {
  136. logger.error(err);
  137. }
  138. });
  139. // duplicate
  140. this.pageEvent.on('duplicate', async(page, user) => {
  141. try {
  142. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DUPLICATE);
  143. }
  144. catch (err) {
  145. logger.error(err);
  146. }
  147. });
  148. // delete
  149. this.pageEvent.on('delete', async(page, user) => {
  150. try {
  151. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE);
  152. }
  153. catch (err) {
  154. logger.error(err);
  155. }
  156. });
  157. // delete completely
  158. this.pageEvent.on('deleteCompletely', async(page, user) => {
  159. try {
  160. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE_COMPLETELY);
  161. }
  162. catch (err) {
  163. logger.error(err);
  164. }
  165. });
  166. // revert
  167. this.pageEvent.on('revert', async(page, user) => {
  168. try {
  169. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_REVERT);
  170. }
  171. catch (err) {
  172. logger.error(err);
  173. }
  174. });
  175. // likes
  176. this.pageEvent.on('like', async(page, user) => {
  177. try {
  178. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_LIKE);
  179. }
  180. catch (err) {
  181. logger.error(err);
  182. }
  183. });
  184. // bookmark
  185. this.pageEvent.on('bookmark', async(page, user) => {
  186. try {
  187. await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_BOOKMARK);
  188. }
  189. catch (err) {
  190. logger.error(err);
  191. }
  192. });
  193. }
  194. canDeleteCompletely(creatorId: ObjectIdLike, operator, isRecursively: boolean): boolean {
  195. const pageCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
  196. const pageRecursiveCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority');
  197. const [singleAuthority, recursiveAuthority] = prepareDeleteConfigValuesForCalc(pageCompleteDeletionAuthority, pageRecursiveCompleteDeletionAuthority);
  198. return this.canDeleteLogic(creatorId, operator, isRecursively, singleAuthority, recursiveAuthority);
  199. }
  200. canDelete(creatorId: ObjectIdLike, operator, isRecursively: boolean): boolean {
  201. const pageDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageDeletionAuthority');
  202. const pageRecursiveDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority');
  203. const [singleAuthority, recursiveAuthority] = prepareDeleteConfigValuesForCalc(pageDeletionAuthority, pageRecursiveDeletionAuthority);
  204. return this.canDeleteLogic(creatorId, operator, isRecursively, singleAuthority, recursiveAuthority);
  205. }
  206. private canDeleteLogic(
  207. creatorId: ObjectIdLike,
  208. operator,
  209. isRecursively: boolean,
  210. authority: IPageDeleteConfigValueToProcessValidation | null,
  211. recursiveAuthority: IPageDeleteConfigValueToProcessValidation | null,
  212. ): boolean {
  213. const isAdmin = operator?.admin ?? false;
  214. const isOperator = operator?._id == null ? false : operator._id.equals(creatorId);
  215. if (isRecursively) {
  216. return this.compareDeleteConfig(isAdmin, isOperator, recursiveAuthority);
  217. }
  218. return this.compareDeleteConfig(isAdmin, isOperator, authority);
  219. }
  220. private compareDeleteConfig(isAdmin: boolean, isOperator: boolean, authority: IPageDeleteConfigValueToProcessValidation | null): boolean {
  221. if (isAdmin) {
  222. return true;
  223. }
  224. if (authority === PageDeleteConfigValue.Anyone || authority == null) {
  225. return true;
  226. }
  227. if (authority === PageDeleteConfigValue.AdminAndAuthor && isOperator) {
  228. return true;
  229. }
  230. return false;
  231. }
  232. filterPagesByCanDeleteCompletely(pages, user, isRecursively: boolean) {
  233. return pages.filter(p => p.isEmpty || this.canDeleteCompletely(p.creator, user, isRecursively));
  234. }
  235. filterPagesByCanDelete(pages, user, isRecursively: boolean) {
  236. return pages.filter(p => p.isEmpty || this.canDelete(p.creator, user, isRecursively));
  237. }
  238. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  239. async findPageAndMetaDataByViewer(pageId: string, path: string, user: IUserHasId, includeEmpty = false, isSharedPage = false): Promise<IPageWithMeta|null> {
  240. const Page = this.crowi.model('Page');
  241. let page: PageModel & PageDocument & HasObjectId;
  242. if (pageId != null) { // prioritized
  243. page = await Page.findByIdAndViewer(pageId, user, null, includeEmpty);
  244. }
  245. else {
  246. page = await Page.findByPathAndViewer(path, user, null, includeEmpty);
  247. }
  248. if (page == null) {
  249. return null;
  250. }
  251. if (isSharedPage) {
  252. return {
  253. data: page,
  254. meta: {
  255. isV5Compatible: isTopPage(page.path) || page.parent != null,
  256. isEmpty: page.isEmpty,
  257. isMovable: false,
  258. isDeletable: false,
  259. isAbleToDeleteCompletely: false,
  260. isRevertible: false,
  261. },
  262. };
  263. }
  264. const isGuestUser = user == null;
  265. const pageInfo = this.constructBasicPageInfo(page, isGuestUser);
  266. const Bookmark = this.crowi.model('Bookmark');
  267. const bookmarkCount = await Bookmark.countByPageId(pageId);
  268. const metadataForGuest = {
  269. ...pageInfo,
  270. bookmarkCount,
  271. };
  272. if (isGuestUser) {
  273. return {
  274. data: page,
  275. meta: metadataForGuest,
  276. };
  277. }
  278. const isBookmarked: boolean = (await Bookmark.findByPageIdAndUserId(pageId, user._id)) != null;
  279. const isLiked: boolean = page.isLiked(user);
  280. const isAbleToDeleteCompletely: boolean = this.canDeleteCompletely((page.creator as IUserHasId)?._id, user, false); // use normal delete config
  281. const subscription = await Subscription.findByUserIdAndTargetId(user._id, pageId);
  282. return {
  283. data: page,
  284. meta: {
  285. ...metadataForGuest,
  286. isAbleToDeleteCompletely,
  287. isBookmarked,
  288. isLiked,
  289. subscriptionStatus: subscription?.status,
  290. },
  291. };
  292. }
  293. private shouldUseV4Process(page): boolean {
  294. const Page = mongoose.model('Page') as unknown as PageModel;
  295. const isTrashPage = page.status === Page.STATUS_DELETED;
  296. const isPageMigrated = page.parent != null;
  297. const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  298. const isRoot = isTopPage(page.path);
  299. const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
  300. const shouldUseV4Process = !isRoot && (!isV5Compatible || !isPageMigrated || isTrashPage || isPageRestricted);
  301. return shouldUseV4Process;
  302. }
  303. private shouldUseV4ProcessForRevert(page): boolean {
  304. const Page = mongoose.model('Page') as unknown as PageModel;
  305. const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  306. const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
  307. const shouldUseV4Process = !isV5Compatible || isPageRestricted;
  308. return shouldUseV4Process;
  309. }
  310. private shouldNormalizeParent(page): boolean {
  311. const Page = mongoose.model('Page') as unknown as PageModel;
  312. return page.grant !== Page.GRANT_RESTRICTED && page.grant !== Page.GRANT_SPECIFIED;
  313. }
  314. /**
  315. * Generate read stream to operate descendants of the specified page path
  316. * @param {string} targetPagePath
  317. * @param {User} viewer
  318. */
  319. private async generateReadStreamToOperateOnlyDescendants(targetPagePath, userToOperate) {
  320. const Page = this.crowi.model('Page');
  321. const { PageQueryBuilder } = Page;
  322. const builder = new PageQueryBuilder(Page.find(), true)
  323. .addConditionAsNotMigrated() // to avoid affecting v5 pages
  324. .addConditionToListOnlyDescendants(targetPagePath);
  325. await Page.addConditionToFilteringByViewerToEdit(builder, userToOperate);
  326. return builder
  327. .query
  328. .lean()
  329. .cursor({ batchSize: BULK_REINDEX_SIZE });
  330. }
  331. async renamePage(page, newPagePath, user, options) {
  332. /*
  333. * Common Operation
  334. */
  335. const Page = mongoose.model('Page') as unknown as PageModel;
  336. const isExist = await Page.exists({ path: newPagePath });
  337. if (isExist) {
  338. throw Error(`Page already exists at ${newPagePath}`);
  339. }
  340. if (isTopPage(page.path)) {
  341. throw Error('It is forbidden to rename the top page');
  342. }
  343. // Separate v4 & v5 process
  344. const shouldUseV4Process = this.shouldUseV4Process(page);
  345. if (shouldUseV4Process) {
  346. return this.renamePageV4(page, newPagePath, user, options);
  347. }
  348. if (options.isMoveMode) {
  349. const fromPath = page.path;
  350. const toPath = newPagePath;
  351. const canMove = canMoveByPath(fromPath, toPath) && await Page.exists({ path: newPagePath });
  352. if (!canMove) {
  353. throw Error('Cannot move to this path.');
  354. }
  355. }
  356. const canOperate = await this.crowi.pageOperationService.canOperate(true, page.path, newPagePath);
  357. if (!canOperate) {
  358. throw Error(`Cannot operate rename to path "${newPagePath}" right now.`);
  359. }
  360. /*
  361. * Resumable Operation
  362. */
  363. let pageOp;
  364. try {
  365. pageOp = await PageOperation.create({
  366. actionType: PageActionType.Rename,
  367. actionStage: PageActionStage.Main,
  368. page,
  369. user,
  370. fromPath: page.path,
  371. toPath: newPagePath,
  372. options,
  373. });
  374. }
  375. catch (err) {
  376. logger.error('Failed to create PageOperation document.', err);
  377. throw err;
  378. }
  379. const renamedPage = await this.renameMainOperation(page, newPagePath, user, options, pageOp._id);
  380. return renamedPage;
  381. }
  382. async renameMainOperation(page, newPagePath: string, user, options, pageOpId: ObjectIdLike) {
  383. const Page = mongoose.model('Page') as unknown as PageModel;
  384. const updateMetadata = options.updateMetadata || false;
  385. // sanitize path
  386. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  387. // UserGroup & Owner validation
  388. // use the parent's grant when target page is an empty page
  389. let grant;
  390. let grantedUserIds;
  391. let grantedGroupId;
  392. if (page.isEmpty) {
  393. const parent = await Page.findOne({ _id: page.parent });
  394. if (parent == null) {
  395. throw Error('parent not found');
  396. }
  397. grant = parent.grant;
  398. grantedUserIds = parent.grantedUsers;
  399. grantedGroupId = parent.grantedGroup;
  400. }
  401. else {
  402. grant = page.grant;
  403. grantedUserIds = page.grantedUsers;
  404. grantedGroupId = page.grantedGroup;
  405. }
  406. if (grant !== Page.GRANT_RESTRICTED) {
  407. let isGrantNormalized = false;
  408. try {
  409. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, newPagePath, grant, grantedUserIds, grantedGroupId, false);
  410. }
  411. catch (err) {
  412. logger.error(`Failed to validate grant of page at "${newPagePath}" when renaming`, err);
  413. throw err;
  414. }
  415. if (!isGrantNormalized) {
  416. throw Error(`This page cannot be renamed to "${newPagePath}" since the selected grant or grantedGroup is not assignable to this page.`);
  417. }
  418. }
  419. // 1. Take target off from tree
  420. await Page.takeOffFromTree(page._id);
  421. // 2. Find new parent
  422. let newParent;
  423. // If renaming to under target, run getParentAndforceCreateEmptyTree to fill new ancestors
  424. if (this.isRenamingToUnderTarget(page.path, newPagePath)) {
  425. newParent = await this.getParentAndforceCreateEmptyTree(page, newPagePath);
  426. }
  427. else {
  428. newParent = await this.getParentAndFillAncestorsByUser(user, newPagePath);
  429. }
  430. // 3. Put back target page to tree (also update the other attrs)
  431. const update: Partial<IPage> = {};
  432. update.path = newPagePath;
  433. update.parent = newParent._id;
  434. if (updateMetadata) {
  435. update.lastUpdateUser = user;
  436. update.updatedAt = new Date();
  437. }
  438. const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
  439. // 5.increase parent's descendantCount.
  440. // see: https://dev.growi.org/62149d019311629d4ecd91cf#Handling%20of%20descendantCount%20in%20case%20of%20unexpected%20process%20interruption
  441. const nToIncreaseForOperationInterruption = 1;
  442. await Page.incrementDescendantCountOfPageIds([newParent._id], nToIncreaseForOperationInterruption);
  443. // create page redirect
  444. if (options.createRedirectPage) {
  445. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  446. await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
  447. }
  448. this.pageEvent.emit('rename', page, user);
  449. // Set to Sub
  450. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
  451. if (pageOp == null) {
  452. throw Error('PageOperation document not found');
  453. }
  454. /*
  455. * Sub Operation
  456. */
  457. this.renameSubOperation(page, newPagePath, user, options, renamedPage, pageOp._id);
  458. return renamedPage;
  459. }
  460. async renameSubOperation(page, newPagePath: string, user, options, renamedPage, pageOpId: ObjectIdLike): Promise<void> {
  461. const Page = mongoose.model('Page') as unknown as PageModel;
  462. const exParentId = page.parent;
  463. const timerObj = this.crowi.pageOperationService.autoUpdateExpiryDate(pageOpId);
  464. try {
  465. // update descendants first
  466. await this.renameDescendantsWithStream(page, newPagePath, user, options, false);
  467. }
  468. catch (err) {
  469. logger.warn(err);
  470. throw Error(err);
  471. }
  472. finally {
  473. this.crowi.pageOperationService.clearAutoUpdateInterval(timerObj);
  474. }
  475. // reduce parent's descendantCount
  476. // see: https://dev.growi.org/62149d019311629d4ecd91cf#Handling%20of%20descendantCount%20in%20case%20of%20unexpected%20process%20interruption
  477. const nToReduceForOperationInterruption = -1;
  478. await Page.incrementDescendantCountOfPageIds([renamedPage.parent], nToReduceForOperationInterruption);
  479. const nToReduce = -1 * ((page.isEmpty ? 0 : 1) + page.descendantCount);
  480. await this.updateDescendantCountOfAncestors(exParentId, nToReduce, true);
  481. // increase ancestore's descendantCount
  482. const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
  483. await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
  484. // Remove leaf empty pages if not moving to under the ex-target position
  485. if (!this.isRenamingToUnderTarget(page.path, newPagePath)) {
  486. // remove empty pages at leaf position
  487. await Page.removeLeafEmptyPagesRecursively(page.parent);
  488. }
  489. await PageOperation.findByIdAndDelete(pageOpId);
  490. }
  491. async resumeRenameSubOperation(renamedPage: PageDocument, pageOp: PageOperationDocument): Promise<void> {
  492. if (pageOp == null) {
  493. throw Error('There is nothing to be processed right now');
  494. }
  495. const isProcessable = pageOp.isProcessable();
  496. if (!isProcessable) {
  497. throw Error('This page operation is currently being processed');
  498. }
  499. if (pageOp.toPath == null) {
  500. throw Error(`Property toPath is missing which is needed to resume rename operation(${pageOp._id})`);
  501. }
  502. const {
  503. page, fromPath, toPath, options, user,
  504. } = pageOp;
  505. await this.renameSubOperation(page, toPath, user, options, renamedPage, pageOp._id);
  506. const ancestorsPaths = this.crowi.pageOperationService.getAncestorsPathsByFromAndToPath(fromPath, toPath);
  507. await this.updateDescendantCountOfPagesWithPaths(ancestorsPaths);
  508. }
  509. private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
  510. const pathToTest = escapeStringRegexp(addTrailingSlash(fromPath));
  511. const pathToBeTested = toPath;
  512. return (new RegExp(`^${pathToTest}`, 'i')).test(pathToBeTested);
  513. }
  514. private async getParentAndforceCreateEmptyTree(originalPage, toPath: string) {
  515. const Page = mongoose.model('Page') as unknown as PageModel;
  516. const fromPath = originalPage.path;
  517. const newParentPath = pathlib.dirname(toPath);
  518. // local util
  519. const collectAncestorPathsUntilFromPath = (path: string, paths: string[] = []): string[] => {
  520. if (path === fromPath) return paths;
  521. const parentPath = pathlib.dirname(path);
  522. paths.push(parentPath);
  523. return collectAncestorPathsUntilFromPath(parentPath, paths);
  524. };
  525. const pathsToInsert = collectAncestorPathsUntilFromPath(toPath);
  526. const originalParent = await Page.findById(originalPage.parent);
  527. if (originalParent == null) {
  528. throw Error('Original parent not found');
  529. }
  530. const insertedPages = await Page.insertMany(pathsToInsert.map((path) => {
  531. return {
  532. path,
  533. isEmpty: true,
  534. };
  535. }));
  536. const pages = [...insertedPages, originalParent];
  537. const ancestorsMap = new Map<string, PageDocument & {_id: any}>(pages.map(p => [p.path, p]));
  538. // bulkWrite to update ancestors
  539. const operations = insertedPages.map((page) => {
  540. const parentPath = pathlib.dirname(page.path);
  541. const op = {
  542. updateOne: {
  543. filter: {
  544. _id: page._id,
  545. },
  546. update: {
  547. $set: {
  548. parent: ancestorsMap.get(parentPath)?._id,
  549. descedantCount: originalParent.descendantCount,
  550. },
  551. },
  552. },
  553. };
  554. return op;
  555. });
  556. await Page.bulkWrite(operations);
  557. const newParent = ancestorsMap.get(newParentPath);
  558. return newParent;
  559. }
  560. private async renamePageV4(page, newPagePath, user, options) {
  561. const Page = this.crowi.model('Page');
  562. const Revision = this.crowi.model('Revision');
  563. const {
  564. isRecursively = false,
  565. createRedirectPage = false,
  566. updateMetadata = false,
  567. } = options;
  568. // sanitize path
  569. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  570. // create descendants first
  571. if (isRecursively) {
  572. await this.renameDescendantsWithStream(page, newPagePath, user, options);
  573. }
  574. const update: any = {};
  575. // update Page
  576. update.path = newPagePath;
  577. if (updateMetadata) {
  578. update.lastUpdateUser = user;
  579. update.updatedAt = Date.now();
  580. }
  581. const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
  582. // update Rivisions
  583. await Revision.updateRevisionListByPageId(renamedPage._id, { pageId: renamedPage._id });
  584. if (createRedirectPage) {
  585. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  586. await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
  587. }
  588. this.pageEvent.emit('rename', page, user);
  589. return renamedPage;
  590. }
  591. private async renameDescendants(pages, user, options, oldPagePathPrefix, newPagePathPrefix, shouldUseV4Process = true) {
  592. // v4 compatible process
  593. if (shouldUseV4Process) {
  594. return this.renameDescendantsV4(pages, user, options, oldPagePathPrefix, newPagePathPrefix);
  595. }
  596. const Page = mongoose.model('Page') as unknown as PageModel;
  597. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  598. const { updateMetadata, createRedirectPage } = options;
  599. const updatePathOperations: any[] = [];
  600. const insertPageRedirectOperations: any[] = [];
  601. pages.forEach((page) => {
  602. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  603. // increment updatePathOperations
  604. let update;
  605. if (!page.isEmpty && updateMetadata) {
  606. update = {
  607. $set: { path: newPagePath, lastUpdateUser: user._id, updatedAt: new Date() },
  608. };
  609. }
  610. else {
  611. update = {
  612. $set: { path: newPagePath },
  613. };
  614. }
  615. if (!page.isEmpty && createRedirectPage) {
  616. // insert PageRedirect
  617. insertPageRedirectOperations.push({
  618. insertOne: {
  619. document: {
  620. fromPath: page.path,
  621. toPath: newPagePath,
  622. },
  623. },
  624. });
  625. }
  626. updatePathOperations.push({
  627. updateOne: {
  628. filter: {
  629. _id: page._id,
  630. },
  631. update,
  632. },
  633. });
  634. });
  635. try {
  636. await Page.bulkWrite(updatePathOperations);
  637. }
  638. catch (err) {
  639. if (err.code !== 11000) {
  640. throw new Error(`Failed to rename pages: ${err}`);
  641. }
  642. }
  643. try {
  644. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  645. }
  646. catch (err) {
  647. if (err.code !== 11000) {
  648. throw Error(`Failed to create PageRedirect documents: ${err}`);
  649. }
  650. }
  651. this.pageEvent.emit('updateMany', pages, user);
  652. }
  653. private async renameDescendantsV4(pages, user, options, oldPagePathPrefix, newPagePathPrefix) {
  654. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  655. const pageCollection = mongoose.connection.collection('pages');
  656. const { updateMetadata, createRedirectPage } = options;
  657. const unorderedBulkOp = pageCollection.initializeUnorderedBulkOp();
  658. const insertPageRedirectOperations: any[] = [];
  659. pages.forEach((page) => {
  660. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  661. if (updateMetadata) {
  662. unorderedBulkOp
  663. .find({ _id: page._id })
  664. .update({ $set: { path: newPagePath, lastUpdateUser: user._id, updatedAt: new Date() } });
  665. }
  666. else {
  667. unorderedBulkOp.find({ _id: page._id }).update({ $set: { path: newPagePath } });
  668. }
  669. // insert PageRedirect
  670. if (!page.isEmpty && createRedirectPage) {
  671. insertPageRedirectOperations.push({
  672. insertOne: {
  673. document: {
  674. fromPath: page.path,
  675. toPath: newPagePath,
  676. },
  677. },
  678. });
  679. }
  680. });
  681. try {
  682. await unorderedBulkOp.execute();
  683. }
  684. catch (err) {
  685. if (err.code !== 11000) {
  686. throw new Error(`Failed to rename pages: ${err}`);
  687. }
  688. }
  689. try {
  690. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  691. }
  692. catch (err) {
  693. if (err.code !== 11000) {
  694. throw Error(`Failed to create PageRedirect documents: ${err}`);
  695. }
  696. }
  697. this.pageEvent.emit('updateMany', pages, user);
  698. }
  699. private async renameDescendantsWithStream(targetPage, newPagePath, user, options = {}, shouldUseV4Process = true) {
  700. // v4 compatible process
  701. if (shouldUseV4Process) {
  702. return this.renameDescendantsWithStreamV4(targetPage, newPagePath, user, options);
  703. }
  704. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  705. const readStream = await factory.generateReadable();
  706. const newPagePathPrefix = newPagePath;
  707. const pathRegExp = new RegExp(`^${escapeStringRegexp(targetPage.path)}`, 'i');
  708. const renameDescendants = this.renameDescendants.bind(this);
  709. const pageEvent = this.pageEvent;
  710. let count = 0;
  711. const writeStream = new Writable({
  712. objectMode: true,
  713. async write(batch, encoding, callback) {
  714. try {
  715. count += batch.length;
  716. await renameDescendants(
  717. batch, user, options, pathRegExp, newPagePathPrefix, shouldUseV4Process,
  718. );
  719. logger.debug(`Renaming pages progressing: (count=${count})`);
  720. }
  721. catch (err) {
  722. logger.error('Renaming error on add anyway: ', err);
  723. }
  724. callback();
  725. },
  726. async final(callback) {
  727. logger.debug(`Renaming pages has completed: (totalCount=${count})`);
  728. // update path
  729. targetPage.path = newPagePath;
  730. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  731. callback();
  732. },
  733. });
  734. readStream
  735. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  736. .pipe(writeStream);
  737. await streamToPromise(writeStream);
  738. }
  739. private async renameDescendantsWithStreamV4(targetPage, newPagePath, user, options = {}) {
  740. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  741. const newPagePathPrefix = newPagePath;
  742. const pathRegExp = new RegExp(`^${escapeStringRegexp(targetPage.path)}`, 'i');
  743. const renameDescendants = this.renameDescendants.bind(this);
  744. const pageEvent = this.pageEvent;
  745. let count = 0;
  746. const writeStream = new Writable({
  747. objectMode: true,
  748. async write(batch, encoding, callback) {
  749. try {
  750. count += batch.length;
  751. await renameDescendants(batch, user, options, pathRegExp, newPagePathPrefix);
  752. logger.debug(`Renaming pages progressing: (count=${count})`);
  753. }
  754. catch (err) {
  755. logger.error('renameDescendants error on add anyway: ', err);
  756. }
  757. callback();
  758. },
  759. final(callback) {
  760. logger.debug(`Renaming pages has completed: (totalCount=${count})`);
  761. // update path
  762. targetPage.path = newPagePath;
  763. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  764. callback();
  765. },
  766. });
  767. readStream
  768. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  769. .pipe(writeStream);
  770. await streamToPromise(writeStream);
  771. }
  772. /*
  773. * Duplicate
  774. */
  775. async duplicate(page, newPagePath, user, isRecursively) {
  776. /*
  777. * Common Operation
  778. */
  779. const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
  780. if (page == null || isEmptyAndNotRecursively) {
  781. throw new Error('Cannot find or duplicate the empty page');
  782. }
  783. const Page = mongoose.model('Page') as unknown as PageModel;
  784. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  785. if (!isRecursively && page.isEmpty) {
  786. throw Error('Page not found.');
  787. }
  788. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  789. // 1. Separate v4 & v5 process
  790. const shouldUseV4Process = this.shouldUseV4Process(page);
  791. if (shouldUseV4Process) {
  792. return this.duplicateV4(page, newPagePath, user, isRecursively);
  793. }
  794. const canOperate = await this.crowi.pageOperationService.canOperate(isRecursively, page.path, newPagePath);
  795. if (!canOperate) {
  796. throw Error(`Cannot operate duplicate to path "${newPagePath}" right now.`);
  797. }
  798. // 2. UserGroup & Owner validation
  799. // use the parent's grant when target page is an empty page
  800. let grant;
  801. let grantedUserIds;
  802. let grantedGroupId;
  803. if (page.isEmpty) {
  804. const parent = await Page.findOne({ _id: page.parent });
  805. if (parent == null) {
  806. throw Error('parent not found');
  807. }
  808. grant = parent.grant;
  809. grantedUserIds = parent.grantedUsers;
  810. grantedGroupId = parent.grantedGroup;
  811. }
  812. else {
  813. grant = page.grant;
  814. grantedUserIds = page.grantedUsers;
  815. grantedGroupId = page.grantedGroup;
  816. }
  817. if (grant !== Page.GRANT_RESTRICTED) {
  818. let isGrantNormalized = false;
  819. try {
  820. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, newPagePath, grant, grantedUserIds, grantedGroupId, false);
  821. }
  822. catch (err) {
  823. logger.error(`Failed to validate grant of page at "${newPagePath}" when duplicating`, err);
  824. throw err;
  825. }
  826. if (!isGrantNormalized) {
  827. throw Error(`This page cannot be duplicated to "${newPagePath}" since the selected grant or grantedGroup is not assignable to this page.`);
  828. }
  829. }
  830. // copy & populate (reason why copy: SubOperation only allows non-populated page document)
  831. const copyPage = { ...page };
  832. // 3. Duplicate target
  833. const options: PageCreateOptions = {
  834. grant: page.grant,
  835. grantUserGroupId: page.grantedGroup,
  836. };
  837. let duplicatedTarget;
  838. if (page.isEmpty) {
  839. const parent = await this.getParentAndFillAncestorsByUser(user, newPagePath);
  840. duplicatedTarget = await Page.createEmptyPage(newPagePath, parent);
  841. }
  842. else {
  843. await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
  844. duplicatedTarget = await (this.create as CreateMethod)(
  845. newPagePath, page.revision.body, user, options,
  846. );
  847. }
  848. this.pageEvent.emit('duplicate', page, user);
  849. // 4. Take over tags
  850. const originTags = await page.findRelatedTagsById();
  851. let savedTags = [];
  852. if (originTags.length !== 0) {
  853. await PageTagRelation.updatePageTags(duplicatedTarget._id, originTags);
  854. savedTags = await PageTagRelation.listTagNamesByPage(duplicatedTarget._id);
  855. this.tagEvent.emit('update', duplicatedTarget, savedTags);
  856. }
  857. if (isRecursively) {
  858. /*
  859. * Resumable Operation
  860. */
  861. let pageOp;
  862. try {
  863. pageOp = await PageOperation.create({
  864. actionType: PageActionType.Duplicate,
  865. actionStage: PageActionStage.Main,
  866. page: copyPage,
  867. user,
  868. fromPath: page.path,
  869. toPath: newPagePath,
  870. });
  871. }
  872. catch (err) {
  873. logger.error('Failed to create PageOperation document.', err);
  874. throw err;
  875. }
  876. this.duplicateRecursivelyMainOperation(page, newPagePath, user, pageOp._id);
  877. }
  878. const result = serializePageSecurely(duplicatedTarget);
  879. result.tags = savedTags;
  880. return result;
  881. }
  882. async duplicateRecursivelyMainOperation(page, newPagePath: string, user, pageOpId: ObjectIdLike): Promise<void> {
  883. const nDuplicatedPages = await this.duplicateDescendantsWithStream(page, newPagePath, user, false);
  884. // normalize parent of descendant pages
  885. const shouldNormalize = this.shouldNormalizeParent(page);
  886. if (shouldNormalize) {
  887. try {
  888. await this.normalizeParentAndDescendantCountOfDescendants(newPagePath, user);
  889. logger.info(`Successfully normalized duplicated descendant pages under "${newPagePath}"`);
  890. }
  891. catch (err) {
  892. logger.error('Failed to normalize descendants afrer duplicate:', err);
  893. throw err;
  894. }
  895. }
  896. // Set to Sub
  897. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
  898. if (pageOp == null) {
  899. throw Error('PageOperation document not found');
  900. }
  901. /*
  902. * Sub Operation
  903. */
  904. await this.duplicateRecursivelySubOperation(newPagePath, nDuplicatedPages, pageOp._id);
  905. }
  906. async duplicateRecursivelySubOperation(newPagePath: string, nDuplicatedPages: number, pageOpId: ObjectIdLike): Promise<void> {
  907. const Page = mongoose.model('Page');
  908. const newTarget = await Page.findOne({ path: newPagePath }); // only one page will be found since duplicating to existing path is forbidden
  909. if (newTarget == null) {
  910. throw Error('No duplicated page found. Something might have gone wrong in duplicateRecursivelyMainOperation.');
  911. }
  912. await this.updateDescendantCountOfAncestors(newTarget._id, nDuplicatedPages, false);
  913. await PageOperation.findByIdAndDelete(pageOpId);
  914. }
  915. async duplicateV4(page, newPagePath, user, isRecursively) {
  916. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  917. // populate
  918. await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
  919. // create option
  920. const options: any = { page };
  921. options.grant = page.grant;
  922. options.grantUserGroupId = page.grantedGroup;
  923. options.grantedUserIds = page.grantedUsers;
  924. newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
  925. const createdPage = await this.crowi.pageService.create(
  926. newPagePath, page.revision.body, user, options,
  927. );
  928. this.pageEvent.emit('duplicate', page, user);
  929. if (isRecursively) {
  930. this.duplicateDescendantsWithStream(page, newPagePath, user);
  931. }
  932. // take over tags
  933. const originTags = await page.findRelatedTagsById();
  934. let savedTags = [];
  935. if (originTags != null) {
  936. await PageTagRelation.updatePageTags(createdPage.id, originTags);
  937. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  938. this.tagEvent.emit('update', createdPage, savedTags);
  939. }
  940. const result = serializePageSecurely(createdPage);
  941. result.tags = savedTags;
  942. return result;
  943. }
  944. /**
  945. * Receive the object with oldPageId and newPageId and duplicate the tags from oldPage to newPage
  946. * @param {Object} pageIdMapping e.g. key: oldPageId, value: newPageId
  947. */
  948. private async duplicateTags(pageIdMapping) {
  949. const PageTagRelation = mongoose.model('PageTagRelation');
  950. // convert pageId from string to ObjectId
  951. const pageIds = Object.keys(pageIdMapping);
  952. const stage = { $or: pageIds.map((pageId) => { return { relatedPage: new mongoose.Types.ObjectId(pageId) } }) };
  953. const pagesAssociatedWithTag = await PageTagRelation.aggregate([
  954. {
  955. $match: stage,
  956. },
  957. {
  958. $group: {
  959. _id: '$relatedTag',
  960. relatedPages: { $push: '$relatedPage' },
  961. },
  962. },
  963. ]);
  964. const newPageTagRelation: any[] = [];
  965. pagesAssociatedWithTag.forEach(({ _id, relatedPages }) => {
  966. // relatedPages
  967. relatedPages.forEach((pageId) => {
  968. newPageTagRelation.push({
  969. relatedPage: pageIdMapping[pageId], // newPageId
  970. relatedTag: _id,
  971. });
  972. });
  973. });
  974. return PageTagRelation.insertMany(newPageTagRelation, { ordered: false });
  975. }
  976. private async duplicateDescendants(pages, user, oldPagePathPrefix, newPagePathPrefix, shouldUseV4Process = true) {
  977. if (shouldUseV4Process) {
  978. return this.duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix);
  979. }
  980. const Page = this.crowi.model('Page');
  981. const Revision = this.crowi.model('Revision');
  982. const pageIds = pages.map(page => page._id);
  983. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  984. // Mapping to set to the body of the new revision
  985. const pageIdRevisionMapping = {};
  986. revisions.forEach((revision) => {
  987. pageIdRevisionMapping[revision.pageId] = revision;
  988. });
  989. // key: oldPageId, value: newPageId
  990. const pageIdMapping = {};
  991. const newPages: any[] = [];
  992. const newRevisions: any[] = [];
  993. // no need to save parent here
  994. pages.forEach((page) => {
  995. const newPageId = new mongoose.Types.ObjectId();
  996. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  997. const revisionId = new mongoose.Types.ObjectId();
  998. pageIdMapping[page._id] = newPageId;
  999. let newPage;
  1000. if (!page.isEmpty) {
  1001. newPage = {
  1002. _id: newPageId,
  1003. path: newPagePath,
  1004. creator: user._id,
  1005. grant: page.grant,
  1006. grantedGroup: page.grantedGroup,
  1007. grantedUsers: page.grantedUsers,
  1008. lastUpdateUser: user._id,
  1009. revision: revisionId,
  1010. };
  1011. newRevisions.push({
  1012. _id: revisionId, pageId: newPageId, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
  1013. });
  1014. }
  1015. newPages.push(newPage);
  1016. });
  1017. await Page.insertMany(newPages, { ordered: false });
  1018. await Revision.insertMany(newRevisions, { ordered: false });
  1019. await this.duplicateTags(pageIdMapping);
  1020. }
  1021. private async duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix) {
  1022. const Page = this.crowi.model('Page');
  1023. const Revision = this.crowi.model('Revision');
  1024. const pageIds = pages.map(page => page._id);
  1025. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  1026. // Mapping to set to the body of the new revision
  1027. const pageIdRevisionMapping = {};
  1028. revisions.forEach((revision) => {
  1029. pageIdRevisionMapping[revision.pageId] = revision;
  1030. });
  1031. // key: oldPageId, value: newPageId
  1032. const pageIdMapping = {};
  1033. const newPages: any[] = [];
  1034. const newRevisions: any[] = [];
  1035. pages.forEach((page) => {
  1036. const newPageId = new mongoose.Types.ObjectId();
  1037. const newPagePath = page.path.replace(oldPagePathPrefix, newPagePathPrefix);
  1038. const revisionId = new mongoose.Types.ObjectId();
  1039. pageIdMapping[page._id] = newPageId;
  1040. newPages.push({
  1041. _id: newPageId,
  1042. path: newPagePath,
  1043. creator: user._id,
  1044. grant: page.grant,
  1045. grantedGroup: page.grantedGroup,
  1046. grantedUsers: page.grantedUsers,
  1047. lastUpdateUser: user._id,
  1048. revision: revisionId,
  1049. });
  1050. newRevisions.push({
  1051. _id: revisionId, pageId: newPageId, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
  1052. });
  1053. });
  1054. await Page.insertMany(newPages, { ordered: false });
  1055. await Revision.insertMany(newRevisions, { ordered: false });
  1056. await this.duplicateTags(pageIdMapping);
  1057. }
  1058. private async duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process = true) {
  1059. if (shouldUseV4Process) {
  1060. return this.duplicateDescendantsWithStreamV4(page, newPagePath, user);
  1061. }
  1062. const iterableFactory = new PageCursorsForDescendantsFactory(user, page, true);
  1063. const readStream = await iterableFactory.generateReadable();
  1064. const newPagePathPrefix = newPagePath;
  1065. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  1066. const duplicateDescendants = this.duplicateDescendants.bind(this);
  1067. const pageEvent = this.pageEvent;
  1068. let count = 0;
  1069. let nNonEmptyDuplicatedPages = 0;
  1070. const writeStream = new Writable({
  1071. objectMode: true,
  1072. async write(batch, encoding, callback) {
  1073. try {
  1074. count += batch.length;
  1075. nNonEmptyDuplicatedPages += batch.filter(page => !page.isEmpty).length;
  1076. await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, shouldUseV4Process);
  1077. logger.debug(`Adding pages progressing: (count=${count})`);
  1078. }
  1079. catch (err) {
  1080. logger.error('addAllPages error on add anyway: ', err);
  1081. }
  1082. callback();
  1083. },
  1084. async final(callback) {
  1085. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1086. // update path
  1087. page.path = newPagePath;
  1088. pageEvent.emit('syncDescendantsUpdate', page, user);
  1089. callback();
  1090. },
  1091. });
  1092. readStream
  1093. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1094. .pipe(writeStream);
  1095. await streamToPromise(writeStream);
  1096. return nNonEmptyDuplicatedPages;
  1097. }
  1098. private async duplicateDescendantsWithStreamV4(page, newPagePath, user) {
  1099. const readStream = await this.generateReadStreamToOperateOnlyDescendants(page.path, user);
  1100. const newPagePathPrefix = newPagePath;
  1101. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  1102. const duplicateDescendants = this.duplicateDescendants.bind(this);
  1103. const pageEvent = this.pageEvent;
  1104. let count = 0;
  1105. const writeStream = new Writable({
  1106. objectMode: true,
  1107. async write(batch, encoding, callback) {
  1108. try {
  1109. count += batch.length;
  1110. await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix);
  1111. logger.debug(`Adding pages progressing: (count=${count})`);
  1112. }
  1113. catch (err) {
  1114. logger.error('addAllPages error on add anyway: ', err);
  1115. }
  1116. callback();
  1117. },
  1118. final(callback) {
  1119. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1120. // update path
  1121. page.path = newPagePath;
  1122. pageEvent.emit('syncDescendantsUpdate', page, user);
  1123. callback();
  1124. },
  1125. });
  1126. readStream
  1127. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1128. .pipe(writeStream);
  1129. await streamToPromise(writeStream);
  1130. return count;
  1131. }
  1132. /*
  1133. * Delete
  1134. */
  1135. async deletePage(page, user, options = {}, isRecursively = false) {
  1136. /*
  1137. * Common Operation
  1138. */
  1139. const Page = mongoose.model('Page') as PageModel;
  1140. // Separate v4 & v5 process
  1141. const shouldUseV4Process = this.shouldUseV4Process(page);
  1142. if (shouldUseV4Process) {
  1143. return this.deletePageV4(page, user, options, isRecursively);
  1144. }
  1145. // Validate
  1146. if (page.isEmpty && !isRecursively) {
  1147. throw Error('Page not found.');
  1148. }
  1149. const isTrashed = isTrashPage(page.path);
  1150. if (isTrashed) {
  1151. throw new Error('This method does NOT support deleting trashed pages.');
  1152. }
  1153. if (!isMovablePage(page.path)) {
  1154. throw new Error('Page is not deletable.');
  1155. }
  1156. const newPath = Page.getDeletedPageName(page.path);
  1157. const canOperate = await this.crowi.pageOperationService.canOperate(isRecursively, page.path, newPath);
  1158. if (!canOperate) {
  1159. throw Error(`Cannot operate delete to path "${newPath}" right now.`);
  1160. }
  1161. // Replace with an empty page
  1162. const isChildrenExist = await Page.exists({ parent: page._id });
  1163. const shouldReplace = !isRecursively && isChildrenExist;
  1164. if (shouldReplace) {
  1165. await Page.replaceTargetWithPage(page, null, true);
  1166. }
  1167. // Delete target
  1168. let deletedPage;
  1169. if (!page.isEmpty) {
  1170. deletedPage = await this.deleteNonEmptyTarget(page, user);
  1171. }
  1172. else { // always recursive
  1173. deletedPage = page;
  1174. await this.deleteEmptyTarget(page);
  1175. }
  1176. // 1. Update descendantCount
  1177. if (isRecursively) {
  1178. const inc = page.isEmpty ? -page.descendantCount : -(page.descendantCount + 1);
  1179. await this.updateDescendantCountOfAncestors(page.parent, inc, true);
  1180. }
  1181. else {
  1182. // update descendantCount of ancestors'
  1183. await this.updateDescendantCountOfAncestors(page.parent, -1, true);
  1184. }
  1185. // 2. Delete leaf empty pages
  1186. await Page.removeLeafEmptyPagesRecursively(page.parent);
  1187. if (isRecursively) {
  1188. let pageOp;
  1189. try {
  1190. pageOp = await PageOperation.create({
  1191. actionType: PageActionType.Delete,
  1192. actionStage: PageActionStage.Main,
  1193. page,
  1194. user,
  1195. fromPath: page.path,
  1196. toPath: newPath,
  1197. });
  1198. }
  1199. catch (err) {
  1200. logger.error('Failed to create PageOperation document.', err);
  1201. throw err;
  1202. }
  1203. /*
  1204. * Resumable Operation
  1205. */
  1206. this.deleteRecursivelyMainOperation(page, user, pageOp._id);
  1207. }
  1208. return deletedPage;
  1209. }
  1210. private async deleteNonEmptyTarget(page, user) {
  1211. const Page = mongoose.model('Page') as unknown as PageModel;
  1212. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  1213. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  1214. const newPath = Page.getDeletedPageName(page.path);
  1215. const deletedPage = await Page.findByIdAndUpdate(page._id, {
  1216. $set: {
  1217. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, descendantCount: 0, // set parent as null
  1218. },
  1219. }, { new: true });
  1220. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
  1221. try {
  1222. await PageRedirect.create({ fromPath: page.path, toPath: newPath });
  1223. }
  1224. catch (err) {
  1225. if (err.code !== 11000) {
  1226. throw err;
  1227. }
  1228. }
  1229. this.pageEvent.emit('delete', page, user);
  1230. this.pageEvent.emit('create', deletedPage, user);
  1231. return deletedPage;
  1232. }
  1233. private async deleteEmptyTarget(page): Promise<void> {
  1234. const Page = mongoose.model('Page') as unknown as PageModel;
  1235. await Page.deleteOne({ _id: page._id, isEmpty: true });
  1236. // update descendantCount of ancestors' before removeLeafEmptyPages
  1237. await this.updateDescendantCountOfAncestors(page._id, -page.descendantCount, false);
  1238. }
  1239. async deleteRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
  1240. await this.deleteDescendantsWithStream(page, user, false);
  1241. await PageOperation.findByIdAndDelete(pageOpId);
  1242. // no sub operation available
  1243. }
  1244. private async deletePageV4(page, user, options = {}, isRecursively = false) {
  1245. const Page = mongoose.model('Page') as PageModel;
  1246. const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
  1247. const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
  1248. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  1249. const newPath = Page.getDeletedPageName(page.path);
  1250. const isTrashed = isTrashPage(page.path);
  1251. if (isTrashed) {
  1252. throw new Error('This method does NOT support deleting trashed pages.');
  1253. }
  1254. if (!isMovablePage(page.path)) {
  1255. throw new Error('Page is not deletable.');
  1256. }
  1257. if (isRecursively) {
  1258. this.deleteDescendantsWithStream(page, user);
  1259. }
  1260. // update Revisions
  1261. await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
  1262. const deletedPage = await Page.findByIdAndUpdate(page._id, {
  1263. $set: {
  1264. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
  1265. },
  1266. }, { new: true });
  1267. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
  1268. try {
  1269. await PageRedirect.create({ fromPath: page.path, toPath: newPath });
  1270. }
  1271. catch (err) {
  1272. if (err.code !== 11000) {
  1273. throw err;
  1274. }
  1275. }
  1276. this.pageEvent.emit('delete', page, user);
  1277. this.pageEvent.emit('create', deletedPage, user);
  1278. return deletedPage;
  1279. }
  1280. private async deleteDescendants(pages, user) {
  1281. const Page = mongoose.model('Page') as unknown as PageModel;
  1282. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  1283. const deletePageOperations: any[] = [];
  1284. const insertPageRedirectOperations: any[] = [];
  1285. pages.forEach((page) => {
  1286. const newPath = Page.getDeletedPageName(page.path);
  1287. let operation;
  1288. // if empty, delete completely
  1289. if (page.isEmpty) {
  1290. operation = {
  1291. deleteOne: {
  1292. filter: { _id: page._id },
  1293. },
  1294. };
  1295. }
  1296. // if not empty, set parent to null and update to trash
  1297. else {
  1298. operation = {
  1299. updateOne: {
  1300. filter: { _id: page._id },
  1301. update: {
  1302. $set: {
  1303. path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, descendantCount: 0, // set parent as null
  1304. },
  1305. },
  1306. },
  1307. };
  1308. insertPageRedirectOperations.push({
  1309. insertOne: {
  1310. document: {
  1311. fromPath: page.path,
  1312. toPath: newPath,
  1313. },
  1314. },
  1315. });
  1316. }
  1317. deletePageOperations.push(operation);
  1318. });
  1319. try {
  1320. await Page.bulkWrite(deletePageOperations);
  1321. }
  1322. catch (err) {
  1323. if (err.code !== 11000) {
  1324. throw new Error(`Failed to delete pages: ${err}`);
  1325. }
  1326. }
  1327. finally {
  1328. this.pageEvent.emit('syncDescendantsDelete', pages, user);
  1329. }
  1330. try {
  1331. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  1332. }
  1333. catch (err) {
  1334. if (err.code !== 11000) {
  1335. throw Error(`Failed to create PageRedirect documents: ${err}`);
  1336. }
  1337. }
  1338. }
  1339. /**
  1340. * Create delete stream and return deleted document count
  1341. */
  1342. private async deleteDescendantsWithStream(targetPage, user, shouldUseV4Process = true): Promise<number> {
  1343. let readStream;
  1344. if (shouldUseV4Process) {
  1345. readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1346. }
  1347. else {
  1348. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  1349. readStream = await factory.generateReadable();
  1350. }
  1351. const deleteDescendants = this.deleteDescendants.bind(this);
  1352. let count = 0;
  1353. let nDeletedNonEmptyPages = 0; // used for updating descendantCount
  1354. const writeStream = new Writable({
  1355. objectMode: true,
  1356. async write(batch, encoding, callback) {
  1357. nDeletedNonEmptyPages += batch.filter(d => !d.isEmpty).length;
  1358. try {
  1359. count += batch.length;
  1360. await deleteDescendants(batch, user);
  1361. logger.debug(`Deleting pages progressing: (count=${count})`);
  1362. }
  1363. catch (err) {
  1364. logger.error('deleteDescendants error on add anyway: ', err);
  1365. }
  1366. callback();
  1367. },
  1368. final(callback) {
  1369. logger.debug(`Deleting pages has completed: (totalCount=${count})`);
  1370. callback();
  1371. },
  1372. });
  1373. readStream
  1374. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1375. .pipe(writeStream);
  1376. await streamToPromise(writeStream);
  1377. return nDeletedNonEmptyPages;
  1378. }
  1379. private async deleteCompletelyOperation(pageIds, pagePaths) {
  1380. // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
  1381. const Bookmark = this.crowi.model('Bookmark');
  1382. const Comment = this.crowi.model('Comment');
  1383. const Page = this.crowi.model('Page');
  1384. const PageTagRelation = this.crowi.model('PageTagRelation');
  1385. const ShareLink = this.crowi.model('ShareLink');
  1386. const Revision = this.crowi.model('Revision');
  1387. const Attachment = this.crowi.model('Attachment');
  1388. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  1389. const { attachmentService } = this.crowi;
  1390. const attachments = await Attachment.find({ page: { $in: pageIds } });
  1391. return Promise.all([
  1392. Bookmark.deleteMany({ page: { $in: pageIds } }),
  1393. Comment.deleteMany({ page: { $in: pageIds } }),
  1394. PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
  1395. ShareLink.deleteMany({ relatedPage: { $in: pageIds } }),
  1396. Revision.deleteMany({ pageId: { $in: pageIds } }),
  1397. Page.deleteMany({ _id: { $in: pageIds } }),
  1398. PageRedirect.deleteMany({ $or: [{ fromPath: { $in: pagePaths } }, { toPath: { $in: pagePaths } }] }),
  1399. attachmentService.removeAllAttachments(attachments),
  1400. ]);
  1401. }
  1402. // delete multiple pages
  1403. private async deleteMultipleCompletely(pages, user, options = {}) {
  1404. const ids = pages.map(page => (page._id));
  1405. const paths = pages.map(page => (page.path));
  1406. logger.debug('Deleting completely', paths);
  1407. await this.deleteCompletelyOperation(ids, paths);
  1408. this.pageEvent.emit('syncDescendantsDelete', pages, user); // update as renamed page
  1409. return;
  1410. }
  1411. async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false) {
  1412. /*
  1413. * Common Operation
  1414. */
  1415. const Page = mongoose.model('Page') as PageModel;
  1416. if (isTopPage(page.path)) {
  1417. throw Error('It is forbidden to delete the top page');
  1418. }
  1419. if (page.isEmpty && !isRecursively) {
  1420. throw Error('Page not found.');
  1421. }
  1422. // v4 compatible process
  1423. const shouldUseV4Process = this.shouldUseV4Process(page);
  1424. if (shouldUseV4Process) {
  1425. return this.deleteCompletelyV4(page, user, options, isRecursively, preventEmitting);
  1426. }
  1427. const canOperate = await this.crowi.pageOperationService.canOperate(isRecursively, page.path, null);
  1428. if (!canOperate) {
  1429. throw Error(`Cannot operate deleteCompletely from path "${page.path}" right now.`);
  1430. }
  1431. const ids = [page._id];
  1432. const paths = [page.path];
  1433. logger.debug('Deleting completely', paths);
  1434. // 1. update descendantCount
  1435. if (isRecursively) {
  1436. const inc = page.isEmpty ? -page.descendantCount : -(page.descendantCount + 1);
  1437. await this.updateDescendantCountOfAncestors(page.parent, inc, true);
  1438. }
  1439. else {
  1440. // replace with an empty page
  1441. const shouldReplace = await Page.exists({ parent: page._id });
  1442. let pageToUpdateDescendantCount = page;
  1443. if (shouldReplace) {
  1444. pageToUpdateDescendantCount = await Page.replaceTargetWithPage(page);
  1445. }
  1446. await this.updateDescendantCountOfAncestors(pageToUpdateDescendantCount.parent, -1, true);
  1447. }
  1448. // 2. then delete target completely
  1449. await this.deleteCompletelyOperation(ids, paths);
  1450. // delete leaf empty pages
  1451. await Page.removeLeafEmptyPagesRecursively(page.parent);
  1452. if (!page.isEmpty && !preventEmitting) {
  1453. this.pageEvent.emit('deleteCompletely', page, user);
  1454. }
  1455. if (isRecursively) {
  1456. let pageOp;
  1457. try {
  1458. pageOp = await PageOperation.create({
  1459. actionType: PageActionType.DeleteCompletely,
  1460. actionStage: PageActionStage.Main,
  1461. page,
  1462. user,
  1463. fromPath: page.path,
  1464. options,
  1465. });
  1466. }
  1467. catch (err) {
  1468. logger.error('Failed to create PageOperation document.', err);
  1469. throw err;
  1470. }
  1471. /*
  1472. * Main Operation
  1473. */
  1474. this.deleteCompletelyRecursivelyMainOperation(page, user, options, pageOp._id);
  1475. }
  1476. return;
  1477. }
  1478. async deleteCompletelyRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike): Promise<void> {
  1479. await this.deleteCompletelyDescendantsWithStream(page, user, options, false);
  1480. await PageOperation.findByIdAndDelete(pageOpId);
  1481. // no sub operation available
  1482. }
  1483. private async deleteCompletelyV4(page, user, options = {}, isRecursively = false, preventEmitting = false) {
  1484. const ids = [page._id];
  1485. const paths = [page.path];
  1486. logger.debug('Deleting completely', paths);
  1487. await this.deleteCompletelyOperation(ids, paths);
  1488. if (isRecursively) {
  1489. this.deleteCompletelyDescendantsWithStream(page, user, options);
  1490. }
  1491. if (!page.isEmpty && !preventEmitting) {
  1492. this.pageEvent.emit('deleteCompletely', page, user);
  1493. }
  1494. return;
  1495. }
  1496. async emptyTrashPage(user, options = {}) {
  1497. return this.deleteCompletelyDescendantsWithStream({ path: '/trash' }, user, options);
  1498. }
  1499. /**
  1500. * Create delete completely stream
  1501. */
  1502. private async deleteCompletelyDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true): Promise<number> {
  1503. let readStream;
  1504. if (shouldUseV4Process) { // pages don't have parents
  1505. readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1506. }
  1507. else {
  1508. const factory = new PageCursorsForDescendantsFactory(user, targetPage, true);
  1509. readStream = await factory.generateReadable();
  1510. }
  1511. let count = 0;
  1512. let nDeletedNonEmptyPages = 0; // used for updating descendantCount
  1513. const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
  1514. const writeStream = new Writable({
  1515. objectMode: true,
  1516. async write(batch, encoding, callback) {
  1517. nDeletedNonEmptyPages += batch.filter(d => !d.isEmpty).length;
  1518. try {
  1519. count += batch.length;
  1520. await deleteMultipleCompletely(batch, user, options);
  1521. logger.debug(`Adding pages progressing: (count=${count})`);
  1522. }
  1523. catch (err) {
  1524. logger.error('addAllPages error on add anyway: ', err);
  1525. }
  1526. callback();
  1527. },
  1528. final(callback) {
  1529. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1530. callback();
  1531. },
  1532. });
  1533. readStream
  1534. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1535. .pipe(writeStream);
  1536. await streamToPromise(writeStream);
  1537. return nDeletedNonEmptyPages;
  1538. }
  1539. // no need to separate Main Sub since it is devided into single page operations
  1540. async deleteMultiplePages(pagesToDelete, user, options): Promise<void> {
  1541. const { isRecursively, isCompletely } = options;
  1542. if (pagesToDelete.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  1543. throw Error(`The maximum number of pages is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
  1544. }
  1545. // omit duplicate paths if isRecursively true, omit empty pages if isRecursively false
  1546. const pages = isRecursively ? omitDuplicateAreaPageFromPages(pagesToDelete) : pagesToDelete.filter(p => !p.isEmpty);
  1547. if (isCompletely) {
  1548. for await (const page of pages) {
  1549. await this.deleteCompletely(page, user, {}, isRecursively);
  1550. }
  1551. }
  1552. else {
  1553. for await (const page of pages) {
  1554. await this.deletePage(page, user, {}, isRecursively);
  1555. }
  1556. }
  1557. }
  1558. // use the same process in both v4 and v5
  1559. private async revertDeletedDescendants(pages, user) {
  1560. const Page = this.crowi.model('Page');
  1561. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  1562. const revertPageOperations: any[] = [];
  1563. const fromPathsToDelete: string[] = [];
  1564. pages.forEach((page) => {
  1565. // e.g. page.path = /trash/test, toPath = /test
  1566. const toPath = Page.getRevertDeletedPageName(page.path);
  1567. revertPageOperations.push({
  1568. updateOne: {
  1569. filter: { _id: page._id },
  1570. update: {
  1571. $set: {
  1572. path: toPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  1573. },
  1574. },
  1575. },
  1576. });
  1577. fromPathsToDelete.push(page.path);
  1578. });
  1579. try {
  1580. await Page.bulkWrite(revertPageOperations);
  1581. await PageRedirect.deleteMany({ fromPath: { $in: fromPathsToDelete } });
  1582. }
  1583. catch (err) {
  1584. if (err.code !== 11000) {
  1585. throw new Error(`Failed to revert pages: ${err}`);
  1586. }
  1587. }
  1588. }
  1589. async revertDeletedPage(page, user, options = {}, isRecursively = false) {
  1590. /*
  1591. * Common Operation
  1592. */
  1593. const Page = this.crowi.model('Page');
  1594. const PageTagRelation = this.crowi.model('PageTagRelation');
  1595. // 1. Separate v4 & v5 process
  1596. const shouldUseV4Process = this.shouldUseV4ProcessForRevert(page);
  1597. if (shouldUseV4Process) {
  1598. return this.revertDeletedPageV4(page, user, options, isRecursively);
  1599. }
  1600. const newPath = Page.getRevertDeletedPageName(page.path);
  1601. const canOperate = await this.crowi.pageOperationService.canOperate(isRecursively, page.path, newPath);
  1602. if (!canOperate) {
  1603. throw Error(`Cannot operate revert from path "${page.path}" right now.`);
  1604. }
  1605. const includeEmpty = true;
  1606. const originPage = await Page.findByPath(newPath, includeEmpty);
  1607. // throw if any page already exists
  1608. if (originPage != null) {
  1609. throw new PathAlreadyExistsError('already_exists', originPage.path);
  1610. }
  1611. // 2. Revert target
  1612. const parent = await this.getParentAndFillAncestorsByUser(user, newPath);
  1613. const updatedPage = await Page.findByIdAndUpdate(page._id, {
  1614. $set: {
  1615. path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null, parent: parent._id, descendantCount: 0,
  1616. },
  1617. }, { new: true });
  1618. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
  1619. this.pageEvent.emit('revert', page, user);
  1620. if (!isRecursively) {
  1621. await this.updateDescendantCountOfAncestors(parent._id, 1, true);
  1622. }
  1623. else {
  1624. let pageOp;
  1625. try {
  1626. pageOp = await PageOperation.create({
  1627. actionType: PageActionType.Revert,
  1628. actionStage: PageActionStage.Main,
  1629. page,
  1630. user,
  1631. fromPath: page.path,
  1632. toPath: newPath,
  1633. options,
  1634. });
  1635. }
  1636. catch (err) {
  1637. logger.error('Failed to create PageOperation document.', err);
  1638. throw err;
  1639. }
  1640. /*
  1641. * Resumable Operation
  1642. */
  1643. this.revertRecursivelyMainOperation(page, user, options, pageOp._id);
  1644. }
  1645. return updatedPage;
  1646. }
  1647. async revertRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike): Promise<void> {
  1648. const Page = mongoose.model('Page') as unknown as PageModel;
  1649. await this.revertDeletedDescendantsWithStream(page, user, options, false);
  1650. const newPath = Page.getRevertDeletedPageName(page.path);
  1651. // normalize parent of descendant pages
  1652. const shouldNormalize = this.shouldNormalizeParent(page);
  1653. if (shouldNormalize) {
  1654. try {
  1655. await this.normalizeParentAndDescendantCountOfDescendants(newPath, user);
  1656. logger.info(`Successfully normalized reverted descendant pages under "${newPath}"`);
  1657. }
  1658. catch (err) {
  1659. logger.error('Failed to normalize descendants afrer revert:', err);
  1660. throw err;
  1661. }
  1662. }
  1663. // Set to Sub
  1664. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
  1665. if (pageOp == null) {
  1666. throw Error('PageOperation document not found');
  1667. }
  1668. /*
  1669. * Sub Operation
  1670. */
  1671. await this.revertRecursivelySubOperation(newPath, pageOp._id);
  1672. }
  1673. async revertRecursivelySubOperation(newPath: string, pageOpId: ObjectIdLike): Promise<void> {
  1674. const Page = mongoose.model('Page') as unknown as PageModel;
  1675. const newTarget = await Page.findOne({ path: newPath }); // only one page will be found since duplicating to existing path is forbidden
  1676. if (newTarget == null) {
  1677. throw Error('No reverted page found. Something might have gone wrong in revertRecursivelyMainOperation.');
  1678. }
  1679. // update descendantCount of ancestors'
  1680. await this.updateDescendantCountOfAncestors(newTarget.parent as ObjectIdLike, newTarget.descendantCount + 1, true);
  1681. await PageOperation.findByIdAndDelete(pageOpId);
  1682. }
  1683. private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {
  1684. const Page = this.crowi.model('Page');
  1685. const PageTagRelation = this.crowi.model('PageTagRelation');
  1686. const newPath = Page.getRevertDeletedPageName(page.path);
  1687. const originPage = await Page.findByPath(newPath);
  1688. if (originPage != null) {
  1689. throw new PathAlreadyExistsError('already_exists', originPage.path);
  1690. }
  1691. if (isRecursively) {
  1692. this.revertDeletedDescendantsWithStream(page, user, options);
  1693. }
  1694. page.status = Page.STATUS_PUBLISHED;
  1695. page.lastUpdateUser = user;
  1696. debug('Revert deleted the page', page, newPath);
  1697. const updatedPage = await Page.findByIdAndUpdate(page._id, {
  1698. $set: {
  1699. path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null,
  1700. },
  1701. }, { new: true });
  1702. await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
  1703. this.pageEvent.emit('revert', page, user);
  1704. return updatedPage;
  1705. }
  1706. /**
  1707. * Create revert stream
  1708. */
  1709. private async revertDeletedDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true): Promise<number> {
  1710. if (shouldUseV4Process) {
  1711. return this.revertDeletedDescendantsWithStreamV4(targetPage, user, options);
  1712. }
  1713. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1714. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  1715. let count = 0;
  1716. const writeStream = new Writable({
  1717. objectMode: true,
  1718. async write(batch, encoding, callback) {
  1719. try {
  1720. count += batch.length;
  1721. await revertDeletedDescendants(batch, user);
  1722. logger.debug(`Reverting pages progressing: (count=${count})`);
  1723. }
  1724. catch (err) {
  1725. logger.error('revertPages error on add anyway: ', err);
  1726. }
  1727. callback();
  1728. },
  1729. async final(callback) {
  1730. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  1731. callback();
  1732. },
  1733. });
  1734. readStream
  1735. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1736. .pipe(writeStream);
  1737. await streamToPromise(writeStream);
  1738. return count;
  1739. }
  1740. private async revertDeletedDescendantsWithStreamV4(targetPage, user, options = {}) {
  1741. const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
  1742. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  1743. let count = 0;
  1744. const writeStream = new Writable({
  1745. objectMode: true,
  1746. async write(batch, encoding, callback) {
  1747. try {
  1748. count += batch.length;
  1749. await revertDeletedDescendants(batch, user);
  1750. logger.debug(`Reverting pages progressing: (count=${count})`);
  1751. }
  1752. catch (err) {
  1753. logger.error('revertPages error on add anyway: ', err);
  1754. }
  1755. callback();
  1756. },
  1757. final(callback) {
  1758. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  1759. callback();
  1760. },
  1761. });
  1762. readStream
  1763. .pipe(createBatchStream(BULK_REINDEX_SIZE))
  1764. .pipe(writeStream);
  1765. await streamToPromise(readStream);
  1766. return count;
  1767. }
  1768. async handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroupId, user) {
  1769. const Page = this.crowi.model('Page');
  1770. const pages = await Page.find({ grantedGroup: { $in: groupsToDelete } });
  1771. switch (action) {
  1772. case 'public':
  1773. await Page.publicizePages(pages);
  1774. break;
  1775. case 'delete':
  1776. return this.deleteMultipleCompletely(pages, user);
  1777. case 'transfer':
  1778. await Page.transferPagesToGroup(pages, transferToUserGroupId);
  1779. break;
  1780. default:
  1781. throw new Error('Unknown action for private pages');
  1782. }
  1783. }
  1784. private extractStringIds(refs: Ref<HasObjectId>[]) {
  1785. return refs.map((ref: Ref<HasObjectId>) => {
  1786. return (typeof ref === 'string') ? ref : ref._id.toString();
  1787. });
  1788. }
  1789. constructBasicPageInfo(page: IPage, isGuestUser?: boolean): IPageInfo | IPageInfoForEntity {
  1790. const isMovable = isGuestUser ? false : isMovablePage(page.path);
  1791. if (page.isEmpty) {
  1792. return {
  1793. isV5Compatible: true,
  1794. isEmpty: true,
  1795. isMovable,
  1796. isDeletable: false,
  1797. isAbleToDeleteCompletely: false,
  1798. isRevertible: false,
  1799. };
  1800. }
  1801. const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
  1802. const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
  1803. return {
  1804. isV5Compatible: isTopPage(page.path) || page.parent != null,
  1805. isEmpty: false,
  1806. sumOfLikers: page.liker.length,
  1807. likerIds: this.extractStringIds(likers),
  1808. seenUserIds: this.extractStringIds(seenUsers),
  1809. sumOfSeenUsers: page.seenUsers.length,
  1810. isMovable,
  1811. isDeletable: isMovable,
  1812. isAbleToDeleteCompletely: false,
  1813. isRevertible: isTrashPage(page.path),
  1814. };
  1815. }
  1816. async shortBodiesMapByPageIds(pageIds: ObjectId[] = [], user): Promise<Record<string, string | null>> {
  1817. const Page = mongoose.model('Page') as unknown as PageModel;
  1818. const MAX_LENGTH = 350;
  1819. // aggregation options
  1820. let userGroups;
  1821. if (user != null && userGroups == null) {
  1822. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // Typescriptize model
  1823. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  1824. }
  1825. const viewerCondition = Page.generateGrantCondition(user, userGroups);
  1826. const filterByIds = {
  1827. _id: { $in: pageIds },
  1828. };
  1829. let pages;
  1830. try {
  1831. pages = await Page
  1832. .aggregate([
  1833. // filter by pageIds
  1834. {
  1835. $match: filterByIds,
  1836. },
  1837. // filter by viewer
  1838. {
  1839. $match: viewerCondition,
  1840. },
  1841. // lookup: https://docs.mongodb.com/v4.4/reference/operator/aggregation/lookup/
  1842. {
  1843. $lookup: {
  1844. from: 'revisions',
  1845. let: { localRevision: '$revision' },
  1846. pipeline: [
  1847. {
  1848. $match: {
  1849. $expr: {
  1850. $eq: ['$_id', '$$localRevision'],
  1851. },
  1852. },
  1853. },
  1854. {
  1855. $project: {
  1856. // What is $substrCP?
  1857. // see: https://stackoverflow.com/questions/43556024/mongodb-error-substrbytes-invalid-range-ending-index-is-in-the-middle-of-a-ut/43556249
  1858. revision: { $substrCP: ['$body', 0, MAX_LENGTH] },
  1859. },
  1860. },
  1861. ],
  1862. as: 'revisionData',
  1863. },
  1864. },
  1865. // projection
  1866. {
  1867. $project: {
  1868. _id: 1,
  1869. revisionData: 1,
  1870. },
  1871. },
  1872. ]).exec();
  1873. }
  1874. catch (err) {
  1875. logger.error('Error occurred while generating shortBodiesMap');
  1876. throw err;
  1877. }
  1878. const shortBodiesMap = {};
  1879. pages.forEach((page) => {
  1880. shortBodiesMap[page._id] = page.revisionData?.[0]?.revision;
  1881. });
  1882. return shortBodiesMap;
  1883. }
  1884. private async createAndSendNotifications(page, user, action) {
  1885. const { activityService, inAppNotificationService } = this.crowi;
  1886. const snapshot = stringifySnapshot(page);
  1887. // Create activity
  1888. const parameters = {
  1889. user: user._id,
  1890. targetModel: SUPPORTED_TARGET_MODEL_TYPE.MODEL_PAGE,
  1891. target: page,
  1892. action,
  1893. };
  1894. const activity = await activityService.createByParameters(parameters);
  1895. // Get user to be notified
  1896. const targetUsers = await activity.getNotificationTargetUsers();
  1897. // Create and send notifications
  1898. await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
  1899. await inAppNotificationService.emitSocketIo(targetUsers);
  1900. }
  1901. async normalizeParentByPath(path: string, user): Promise<void> {
  1902. const Page = mongoose.model('Page') as unknown as PageModel;
  1903. const { PageQueryBuilder } = Page;
  1904. // This validation is not 100% correct since it ignores user to count
  1905. const builder = new PageQueryBuilder(Page.find());
  1906. builder.addConditionAsNotMigrated();
  1907. builder.addConditionToListWithDescendants(path);
  1908. const nEstimatedNormalizationTarget: number = await builder.query.exec('count');
  1909. if (nEstimatedNormalizationTarget === 0) {
  1910. throw Error('No page is available for conversion');
  1911. }
  1912. const pages = await Page.findByPathAndViewer(path, user, null, false);
  1913. if (pages == null || !Array.isArray(pages)) {
  1914. throw Error('Something went wrong while converting pages.');
  1915. }
  1916. if (pages.length === 0) {
  1917. const isForbidden = await Page.count({ path, isEmpty: false }) > 0;
  1918. if (isForbidden) {
  1919. throw new V5ConversionError('It is not allowed to convert this page.', V5ConversionErrCode.FORBIDDEN);
  1920. }
  1921. }
  1922. if (pages.length > 1) {
  1923. throw new V5ConversionError(
  1924. `There are more than two pages at the path "${path}". Please rename or delete the page first.`,
  1925. V5ConversionErrCode.DUPLICATE_PAGES_FOUND,
  1926. );
  1927. }
  1928. let page;
  1929. let systematicallyCreatedPage;
  1930. const shouldCreateNewPage = pages[0] == null;
  1931. if (shouldCreateNewPage) {
  1932. const notEmptyParent = await Page.findNotEmptyParentByPathRecursively(path);
  1933. const options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] | undefined } = {
  1934. grant: notEmptyParent.grant,
  1935. grantUserGroupId: notEmptyParent.grantedGroup,
  1936. grantedUsers: notEmptyParent.grantedUsers,
  1937. };
  1938. systematicallyCreatedPage = await this.forceCreateBySystem(
  1939. path,
  1940. '',
  1941. options,
  1942. );
  1943. page = systematicallyCreatedPage;
  1944. }
  1945. else {
  1946. page = pages[0];
  1947. }
  1948. const grant = page.grant;
  1949. const grantedUserIds = page.grantedUsers;
  1950. const grantedGroupId = page.grantedGroup;
  1951. /*
  1952. * UserGroup & Owner validation
  1953. */
  1954. let isGrantNormalized = false;
  1955. try {
  1956. const shouldCheckDescendants = true;
  1957. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
  1958. }
  1959. catch (err) {
  1960. logger.error(`Failed to validate grant of page at "${path}"`, err);
  1961. throw err;
  1962. }
  1963. if (!isGrantNormalized) {
  1964. throw new V5ConversionError(
  1965. 'This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.',
  1966. V5ConversionErrCode.GRANT_INVALID,
  1967. );
  1968. }
  1969. let pageOp;
  1970. try {
  1971. pageOp = await PageOperation.create({
  1972. actionType: PageActionType.NormalizeParent,
  1973. actionStage: PageActionStage.Main,
  1974. page,
  1975. user,
  1976. fromPath: page.path,
  1977. toPath: page.path,
  1978. });
  1979. }
  1980. catch (err) {
  1981. logger.error('Failed to create PageOperation document.', err);
  1982. throw err;
  1983. }
  1984. this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
  1985. }
  1986. async normalizeParentByPageIdsRecursively(pageIds: ObjectIdLike[], user): Promise<void> {
  1987. const Page = mongoose.model('Page') as unknown as PageModel;
  1988. const pages = await Page.findByIdsAndViewer(pageIds, user, null);
  1989. if (pages == null || pages.length === 0) {
  1990. throw Error('pageIds is null or 0 length.');
  1991. }
  1992. if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  1993. throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
  1994. }
  1995. this.normalizeParentRecursivelyByPages(pages, user);
  1996. return;
  1997. }
  1998. async normalizeParentByPageIds(pageIds: ObjectIdLike[], user): Promise<void> {
  1999. const Page = await mongoose.model('Page') as unknown as PageModel;
  2000. const socket = this.crowi.socketIoService.getDefaultSocket();
  2001. for await (const pageId of pageIds) {
  2002. const page = await Page.findById(pageId);
  2003. if (page == null) {
  2004. continue;
  2005. }
  2006. const errorData: PageMigrationErrorData = { paths: [page.path] };
  2007. try {
  2008. const canOperate = await this.crowi.pageOperationService.canOperate(false, page.path, page.path);
  2009. if (!canOperate) {
  2010. throw Error(`Cannot operate normalizeParent to path "${page.path}" right now.`);
  2011. }
  2012. const normalizedPage = await this.normalizeParentByPage(page, user);
  2013. if (normalizedPage == null) {
  2014. socket.emit(SocketEventName.PageMigrationError, errorData);
  2015. logger.error(`Failed to update descendantCount of page of id: "${pageId}"`);
  2016. }
  2017. }
  2018. catch (err) {
  2019. socket.emit(SocketEventName.PageMigrationError, errorData);
  2020. logger.error('Something went wrong while normalizing parent.', err);
  2021. }
  2022. }
  2023. socket.emit(SocketEventName.PageMigrationSuccess);
  2024. }
  2025. private async normalizeParentByPage(page, user) {
  2026. const Page = mongoose.model('Page') as unknown as PageModel;
  2027. const {
  2028. path, grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
  2029. } = page;
  2030. // check if any page exists at target path already
  2031. const existingPage = await Page.findOne({ path, parent: { $ne: null } });
  2032. if (existingPage != null && !existingPage.isEmpty) {
  2033. throw Error('Page already exists. Please rename the page to continue.');
  2034. }
  2035. /*
  2036. * UserGroup & Owner validation
  2037. */
  2038. if (grant !== Page.GRANT_RESTRICTED) {
  2039. let isGrantNormalized = false;
  2040. try {
  2041. const shouldCheckDescendants = true;
  2042. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
  2043. }
  2044. catch (err) {
  2045. logger.error(`Failed to validate grant of page at "${path}"`, err);
  2046. throw err;
  2047. }
  2048. if (!isGrantNormalized) {
  2049. throw Error('This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.');
  2050. }
  2051. }
  2052. else {
  2053. throw Error('Restricted pages can not be migrated');
  2054. }
  2055. let normalizedPage;
  2056. // replace if empty page exists
  2057. if (existingPage != null && existingPage.isEmpty) {
  2058. // Inherit descendantCount from the empty page
  2059. const updatedPage = await Page.findOneAndUpdate({ _id: page._id }, { descendantCount: existingPage.descendantCount }, { new: true });
  2060. await Page.replaceTargetWithPage(existingPage, updatedPage, true);
  2061. normalizedPage = await Page.findById(page._id);
  2062. }
  2063. else {
  2064. const parent = await this.getParentAndFillAncestorsByUser(user, page.path);
  2065. normalizedPage = await Page.findOneAndUpdate({ _id: page._id }, { parent: parent._id }, { new: true });
  2066. }
  2067. // Update descendantCount
  2068. const inc = 1;
  2069. await this.updateDescendantCountOfAncestors(normalizedPage.parent, inc, true);
  2070. return normalizedPage;
  2071. }
  2072. async normalizeParentRecursivelyByPages(pages, user): Promise<void> {
  2073. /*
  2074. * Main Operation
  2075. */
  2076. const socket = this.crowi.socketIoService.getDefaultSocket();
  2077. const pagesToNormalize = omitDuplicateAreaPageFromPages(pages);
  2078. let normalizablePages;
  2079. let nonNormalizablePages;
  2080. try {
  2081. [normalizablePages, nonNormalizablePages] = await this.crowi.pageGrantService.separateNormalizableAndNotNormalizablePages(user, pagesToNormalize);
  2082. }
  2083. catch (err) {
  2084. socket.emit(SocketEventName.PageMigrationError);
  2085. throw err;
  2086. }
  2087. if (normalizablePages.length === 0) {
  2088. socket.emit(SocketEventName.PageMigrationError);
  2089. return;
  2090. }
  2091. if (nonNormalizablePages.length !== 0) {
  2092. const nonNormalizablePagePaths: string[] = nonNormalizablePages.map(p => p.path);
  2093. socket.emit(SocketEventName.PageMigrationError, { paths: nonNormalizablePagePaths });
  2094. logger.debug('Some pages could not be converted.', nonNormalizablePagePaths);
  2095. }
  2096. /*
  2097. * Main Operation (s)
  2098. */
  2099. const errorPagePaths: string[] = [];
  2100. for await (const page of normalizablePages) {
  2101. const canOperate = await this.crowi.pageOperationService.canOperate(true, page.path, page.path);
  2102. if (!canOperate) {
  2103. errorPagePaths.push(page.path);
  2104. throw Error(`Cannot operate normalizeParentRecursiively to path "${page.path}" right now.`);
  2105. }
  2106. const Page = mongoose.model('Page') as unknown as PageModel;
  2107. const { PageQueryBuilder } = Page;
  2108. const builder = new PageQueryBuilder(Page.findOne());
  2109. builder.addConditionAsOnTree();
  2110. builder.addConditionToListByPathsArray([page.path]);
  2111. const existingPage = await builder.query.exec();
  2112. if (existingPage?.parent != null) {
  2113. errorPagePaths.push(page.path);
  2114. throw Error('This page has already converted.');
  2115. }
  2116. let pageOp;
  2117. try {
  2118. pageOp = await PageOperation.create({
  2119. actionType: PageActionType.NormalizeParent,
  2120. actionStage: PageActionStage.Main,
  2121. page,
  2122. user,
  2123. fromPath: page.path,
  2124. toPath: page.path,
  2125. });
  2126. }
  2127. catch (err) {
  2128. errorPagePaths.push(page.path);
  2129. logger.error('Failed to create PageOperation document.', err);
  2130. throw err;
  2131. }
  2132. try {
  2133. await this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
  2134. }
  2135. catch (err) {
  2136. errorPagePaths.push(page.path);
  2137. logger.err('Failed to run normalizeParentRecursivelyMainOperation.', err);
  2138. throw err;
  2139. }
  2140. }
  2141. if (errorPagePaths.length === 0) {
  2142. socket.emit(SocketEventName.PageMigrationSuccess);
  2143. }
  2144. else {
  2145. socket.emit(SocketEventName.PageMigrationError, { paths: errorPagePaths });
  2146. }
  2147. }
  2148. async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<number> {
  2149. // Save prevDescendantCount for sub-operation
  2150. const Page = mongoose.model('Page') as unknown as PageModel;
  2151. const { PageQueryBuilder } = Page;
  2152. const builder = new PageQueryBuilder(Page.findOne(), true);
  2153. builder.addConditionAsOnTree();
  2154. builder.addConditionToListByPathsArray([page.path]);
  2155. const exPage = await builder.query.exec();
  2156. const options = { prevDescendantCount: exPage?.descendantCount ?? 0 };
  2157. let count: number;
  2158. try {
  2159. count = await this.normalizeParentRecursively([page.path], user);
  2160. }
  2161. catch (err) {
  2162. logger.error('V5 initial miration failed.', err);
  2163. // socket.emit('normalizeParentRecursivelyByPageIds', { error: err.message }); TODO: use socket to tell user
  2164. throw err;
  2165. }
  2166. // Set to Sub
  2167. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
  2168. if (pageOp == null) {
  2169. throw Error('PageOperation document not found');
  2170. }
  2171. await this.normalizeParentRecursivelySubOperation(page, user, pageOp._id, options);
  2172. return count;
  2173. }
  2174. async normalizeParentRecursivelySubOperation(page, user, pageOpId: ObjectIdLike, options: {prevDescendantCount: number}): Promise<void> {
  2175. const Page = mongoose.model('Page') as unknown as PageModel;
  2176. try {
  2177. // update descendantCount of self and descendant pages first
  2178. await this.updateDescendantCountOfSelfAndDescendants(page.path);
  2179. // find pages again to get updated descendantCount
  2180. // then calculate inc
  2181. const pageAfterUpdatingDescendantCount = await Page.findByIdAndViewer(page._id, user);
  2182. if (pageAfterUpdatingDescendantCount == null) {
  2183. throw Error('Page not found after updating descendantCount');
  2184. }
  2185. const { prevDescendantCount } = options;
  2186. const newDescendantCount = pageAfterUpdatingDescendantCount.descendantCount;
  2187. let inc = newDescendantCount - prevDescendantCount;
  2188. const isAlreadyConverted = page.parent != null;
  2189. if (!isAlreadyConverted) {
  2190. inc += 1;
  2191. }
  2192. await this.updateDescendantCountOfAncestors(page._id, inc, false);
  2193. }
  2194. catch (err) {
  2195. logger.error('Failed to update descendantCount after normalizing parent:', err);
  2196. throw Error(`Failed to update descendantCount after normalizing parent: ${err}`);
  2197. }
  2198. await PageOperation.findByIdAndDelete(pageOpId);
  2199. }
  2200. async _isPagePathIndexUnique() {
  2201. const Page = this.crowi.model('Page');
  2202. const now = (new Date()).toString();
  2203. const path = `growi_check_is_path_index_unique_${now}`;
  2204. let isUnique = false;
  2205. try {
  2206. await Page.insertMany([
  2207. { path },
  2208. { path },
  2209. ]);
  2210. }
  2211. catch (err) {
  2212. if (err?.code === 11000) { // Error code 11000 indicates the index is unique
  2213. isUnique = true;
  2214. logger.info('Page path index is unique.');
  2215. }
  2216. else {
  2217. throw err;
  2218. }
  2219. }
  2220. finally {
  2221. await Page.deleteMany({ path: { $regex: new RegExp('growi_check_is_path_index_unique', 'g') } });
  2222. }
  2223. return isUnique;
  2224. }
  2225. async normalizeAllPublicPages() {
  2226. let isUnique;
  2227. try {
  2228. isUnique = await this._isPagePathIndexUnique();
  2229. }
  2230. catch (err) {
  2231. logger.error('Failed to check path index status', err);
  2232. throw err;
  2233. }
  2234. // drop unique index first
  2235. if (isUnique) {
  2236. try {
  2237. await this._v5NormalizeIndex();
  2238. }
  2239. catch (err) {
  2240. logger.error('V5 index normalization failed.', err);
  2241. throw err;
  2242. }
  2243. }
  2244. // then migrate
  2245. try {
  2246. await this.normalizeParentRecursively(['/'], null);
  2247. }
  2248. catch (err) {
  2249. logger.error('V5 initial miration failed.', err);
  2250. throw err;
  2251. }
  2252. // update descendantCount of all public pages
  2253. try {
  2254. await this.updateDescendantCountOfSelfAndDescendants('/');
  2255. logger.info('Successfully updated all descendantCount of public pages.');
  2256. }
  2257. catch (err) {
  2258. logger.error('Failed updating descendantCount of public pages.', err);
  2259. throw err;
  2260. }
  2261. await this._setIsV5CompatibleTrue();
  2262. }
  2263. private async _setIsV5CompatibleTrue() {
  2264. try {
  2265. await this.crowi.configManager.updateConfigsInTheSameNamespace('crowi', {
  2266. 'app:isV5Compatible': true,
  2267. });
  2268. logger.info('Successfully migrated all public pages.');
  2269. }
  2270. catch (err) {
  2271. logger.warn('Failed to update app:isV5Compatible to true.');
  2272. throw err;
  2273. }
  2274. }
  2275. private async normalizeParentAndDescendantCountOfDescendants(path: string, user): Promise<void> {
  2276. await this.normalizeParentRecursively([path], user);
  2277. // update descendantCount of descendant pages
  2278. await this.updateDescendantCountOfSelfAndDescendants(path);
  2279. }
  2280. /**
  2281. * Normalize parent attribute by passing paths and user.
  2282. * @param paths Pages under this paths value will be updated.
  2283. * @param user To be used to filter pages to update. If null, only public pages will be updated.
  2284. * @returns Promise<void>
  2285. */
  2286. async normalizeParentRecursively(paths: string[], user: any | null, shouldEmit = false): Promise<number> {
  2287. const Page = mongoose.model('Page') as unknown as PageModel;
  2288. const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, []));
  2289. // targets' descendants
  2290. const pathAndRegExpsToNormalize: (RegExp | string)[] = paths
  2291. .map(p => new RegExp(`^${escapeStringRegexp(addTrailingSlash(p))}`, 'i'));
  2292. // include targets' path
  2293. pathAndRegExpsToNormalize.push(...paths);
  2294. // determine UserGroup condition
  2295. let userGroups = null;
  2296. if (user != null) {
  2297. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  2298. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  2299. }
  2300. const grantFiltersByUser: { $or: any[] } = Page.generateGrantCondition(user, userGroups);
  2301. return this._normalizeParentRecursively(pathAndRegExpsToNormalize, ancestorPaths, grantFiltersByUser, user, shouldEmit);
  2302. }
  2303. private buildFilterForNormalizeParentRecursively(pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }) {
  2304. const Page = mongoose.model('Page') as unknown as PageModel;
  2305. const andFilter: any = {
  2306. $and: [
  2307. {
  2308. parent: null,
  2309. status: Page.STATUS_PUBLISHED,
  2310. path: { $ne: '/' },
  2311. },
  2312. ],
  2313. };
  2314. const orFilter: any = { $or: [] };
  2315. // specified pathOrRegExps
  2316. if (pathOrRegExps.length > 0) {
  2317. orFilter.$or.push(
  2318. {
  2319. path: { $in: pathOrRegExps },
  2320. },
  2321. );
  2322. }
  2323. // not specified but ancestors of specified pathOrRegExps
  2324. if (publicPathsToNormalize.length > 0) {
  2325. orFilter.$or.push(
  2326. {
  2327. path: { $in: publicPathsToNormalize },
  2328. grant: Page.GRANT_PUBLIC, // use only public pages to complete the tree
  2329. },
  2330. );
  2331. }
  2332. // Merge filters
  2333. const mergedFilter = {
  2334. $and: [
  2335. { $and: [grantFiltersByUser, ...andFilter.$and] },
  2336. { $or: orFilter.$or },
  2337. ],
  2338. };
  2339. return mergedFilter;
  2340. }
  2341. private async _normalizeParentRecursively(
  2342. pathOrRegExps: (RegExp | string)[],
  2343. publicPathsToNormalize: string[],
  2344. grantFiltersByUser: { $or: any[] },
  2345. user,
  2346. shouldEmit = false,
  2347. count = 0,
  2348. skiped = 0,
  2349. isFirst = true,
  2350. ): Promise<number> {
  2351. const BATCH_SIZE = 100;
  2352. const PAGES_LIMIT = 1000;
  2353. const socket = shouldEmit ? this.crowi.socketIoService.getAdminSocket() : null;
  2354. const Page = mongoose.model('Page') as unknown as PageModel;
  2355. const { PageQueryBuilder } = Page;
  2356. // Build filter
  2357. const matchFilter = this.buildFilterForNormalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser);
  2358. let baseAggregation = Page
  2359. .aggregate([
  2360. { $match: matchFilter },
  2361. {
  2362. $project: { // minimize data to fetch
  2363. _id: 1,
  2364. path: 1,
  2365. },
  2366. },
  2367. ]);
  2368. // Limit pages to get
  2369. const total = await Page.countDocuments(matchFilter);
  2370. if (isFirst) {
  2371. socket?.emit(SocketEventName.PMStarted, { total });
  2372. }
  2373. if (total > PAGES_LIMIT) {
  2374. baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
  2375. }
  2376. const pagesStream = await baseAggregation.cursor({ batchSize: BATCH_SIZE });
  2377. const batchStream = createBatchStream(BATCH_SIZE);
  2378. let shouldContinue = true;
  2379. let nextCount = count;
  2380. let nextSkiped = skiped;
  2381. // eslint-disable-next-line max-len
  2382. const buildPipelineToCreateEmptyPagesByUser = this.buildPipelineToCreateEmptyPagesByUser.bind(this);
  2383. const migratePagesStream = new Writable({
  2384. objectMode: true,
  2385. async write(pages, encoding, callback) {
  2386. const parentPaths = Array.from(new Set<string>(pages.map(p => pathlib.dirname(p.path))));
  2387. // 1. Remove unnecessary empty pages & reset parent for pages which had had those empty pages
  2388. const pageIdsToNotDelete = pages.map(p => p._id);
  2389. const emptyPagePathsToDelete = pages.map(p => p.path);
  2390. const builder1 = new PageQueryBuilder(Page.find({ isEmpty: true }, { _id: 1 }), true);
  2391. builder1.addConditionToListByPathsArray(emptyPagePathsToDelete);
  2392. builder1.addConditionToExcludeByPageIdsArray(pageIdsToNotDelete);
  2393. const emptyPagesToDelete = await builder1.query.lean().exec();
  2394. const resetParentOperations = emptyPagesToDelete.map((p) => {
  2395. return {
  2396. updateOne: {
  2397. filter: {
  2398. parent: p._id,
  2399. },
  2400. update: {
  2401. parent: null,
  2402. },
  2403. },
  2404. };
  2405. });
  2406. await Page.bulkWrite(resetParentOperations);
  2407. await Page.removeEmptyPages(pageIdsToNotDelete, emptyPagePathsToDelete);
  2408. // 2. Create lacking parents as empty pages
  2409. const orFilters = [
  2410. { path: '/' },
  2411. { path: { $in: publicPathsToNormalize }, grant: Page.GRANT_PUBLIC, status: Page.STATUS_PUBLISHED },
  2412. { path: { $in: publicPathsToNormalize }, parent: { $ne: null }, status: Page.STATUS_PUBLISHED },
  2413. { path: { $nin: publicPathsToNormalize }, status: Page.STATUS_PUBLISHED },
  2414. ];
  2415. const filterForApplicableAncestors = { $or: orFilters };
  2416. const aggregationPipeline = await buildPipelineToCreateEmptyPagesByUser(user, parentPaths, false, filterForApplicableAncestors);
  2417. await Page.createEmptyPagesByPaths(parentPaths, aggregationPipeline);
  2418. // 3. Find parents
  2419. const addGrantCondition = (builder) => {
  2420. builder.query = builder.query.and(grantFiltersByUser);
  2421. return builder;
  2422. };
  2423. const builder2 = new PageQueryBuilder(Page.find(), true);
  2424. addGrantCondition(builder2);
  2425. const parents = await builder2
  2426. .addConditionToListByPathsArray(parentPaths)
  2427. .addConditionToFilterByApplicableAncestors(publicPathsToNormalize)
  2428. .query
  2429. .lean()
  2430. .exec();
  2431. // Normalize all siblings for each page
  2432. const updateManyOperations = parents.map((parent) => {
  2433. const parentId = parent._id;
  2434. // Build filter
  2435. const parentPathEscaped = escapeStringRegexp(parent.path === '/' ? '' : parent.path); // adjust the path for RegExp
  2436. const filter: any = {
  2437. $and: [
  2438. {
  2439. path: { $regex: new RegExp(`^${parentPathEscaped}(\\/[^/]+)\\/?$`, 'i') }, // see: regexr.com/6889f (e.g. /parent/any_child or /any_level1)
  2440. },
  2441. {
  2442. path: { $in: pathOrRegExps.concat(publicPathsToNormalize) },
  2443. },
  2444. filterForApplicableAncestors,
  2445. grantFiltersByUser,
  2446. ],
  2447. };
  2448. return {
  2449. updateMany: {
  2450. filter,
  2451. update: {
  2452. parent: parentId,
  2453. },
  2454. },
  2455. };
  2456. });
  2457. try {
  2458. const res = await Page.bulkWrite(updateManyOperations);
  2459. nextCount += res.result.nModified;
  2460. nextSkiped += res.result.writeErrors.length;
  2461. logger.info(`Page migration processing: (migratedPages=${res.result.nModified})`);
  2462. socket?.emit(SocketEventName.PMMigrating, { count: nextCount });
  2463. socket?.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
  2464. // Throw if any error is found
  2465. if (res.result.writeErrors.length > 0) {
  2466. logger.error('Failed to migrate some pages', res.result.writeErrors);
  2467. socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
  2468. throw Error('Failed to migrate some pages');
  2469. }
  2470. // Finish migration if no modification occurred
  2471. if (res.result.nModified === 0 && res.result.nMatched === 0) {
  2472. shouldContinue = false;
  2473. logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
  2474. socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
  2475. }
  2476. }
  2477. catch (err) {
  2478. logger.error('Failed to update page.parent.', err);
  2479. throw err;
  2480. }
  2481. callback();
  2482. },
  2483. final(callback) {
  2484. callback();
  2485. },
  2486. });
  2487. pagesStream
  2488. .pipe(batchStream)
  2489. .pipe(migratePagesStream);
  2490. await streamToPromise(migratePagesStream);
  2491. if (await Page.exists(matchFilter) && shouldContinue) {
  2492. return this._normalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser, user, shouldEmit, nextCount, nextSkiped, false);
  2493. }
  2494. // End
  2495. socket?.emit(SocketEventName.PMEnded, { isSucceeded: true });
  2496. return nextCount;
  2497. }
  2498. private async _v5NormalizeIndex() {
  2499. const collection = mongoose.connection.collection('pages');
  2500. try {
  2501. // drop pages.path_1 indexes
  2502. await collection.dropIndex('path_1');
  2503. logger.info('Succeeded to drop unique indexes from pages.path.');
  2504. }
  2505. catch (err) {
  2506. logger.warn('Failed to drop unique indexes from pages.path.', err);
  2507. throw err;
  2508. }
  2509. try {
  2510. // create indexes without
  2511. await collection.createIndex({ path: 1 }, { unique: false });
  2512. logger.info('Succeeded to create non-unique indexes on pages.path.');
  2513. }
  2514. catch (err) {
  2515. logger.warn('Failed to create non-unique indexes on pages.path.', err);
  2516. throw err;
  2517. }
  2518. }
  2519. async countPagesCanNormalizeParentByUser(user): Promise<number> {
  2520. if (user == null) {
  2521. throw Error('user is required');
  2522. }
  2523. const Page = mongoose.model('Page') as unknown as PageModel;
  2524. const { PageQueryBuilder } = Page;
  2525. const builder = new PageQueryBuilder(Page.count(), false);
  2526. await builder.addConditionAsMigratablePages(user);
  2527. const nMigratablePages = await builder.query.exec();
  2528. return nMigratablePages;
  2529. }
  2530. /**
  2531. * update descendantCount of the following pages
  2532. * - page that has the same path as the provided path
  2533. * - pages that are descendants of the above page
  2534. */
  2535. async updateDescendantCountOfSelfAndDescendants(path: string): Promise<void> {
  2536. const BATCH_SIZE = 200;
  2537. const Page = this.crowi.model('Page');
  2538. const { PageQueryBuilder } = Page;
  2539. const builder = new PageQueryBuilder(Page.find(), true);
  2540. builder.addConditionAsOnTree();
  2541. builder.addConditionToListWithDescendants(path);
  2542. builder.addConditionToSortPagesByDescPath();
  2543. const aggregatedPages = await builder.query.lean().cursor({ batchSize: BATCH_SIZE });
  2544. await this.recountAndUpdateDescendantCountOfPages(aggregatedPages);
  2545. }
  2546. /**
  2547. * update descendantCount of the pages sequentially from path containing more `/` to path containing less
  2548. */
  2549. async updateDescendantCountOfPagesWithPaths(paths: string[]): Promise<void> {
  2550. const BATCH_SIZE = 200;
  2551. const Page = this.crowi.model('Page');
  2552. const { PageQueryBuilder } = Page;
  2553. const builder = new PageQueryBuilder(Page.find(), true);
  2554. builder.addConditionToListByPathsArray(paths); // find by paths
  2555. builder.addConditionToSortPagesByDescPath(); // sort in DESC
  2556. const aggregatedPages = await builder.query.lean().cursor({ batchSize: BATCH_SIZE });
  2557. await this.recountAndUpdateDescendantCountOfPages(aggregatedPages);
  2558. }
  2559. /**
  2560. * Recount descendantCount of pages one by one
  2561. */
  2562. async recountAndUpdateDescendantCountOfPages(pageCursor: QueryCursor<any>, batchSize?:number): Promise<void> {
  2563. const Page = this.crowi.model('Page');
  2564. const recountWriteStream = new Writable({
  2565. objectMode: true,
  2566. async write(pageDocuments, encoding, callback) {
  2567. for await (const document of pageDocuments) {
  2568. const descendantCount = await Page.recountDescendantCount(document._id);
  2569. await Page.findByIdAndUpdate(document._id, { descendantCount });
  2570. }
  2571. callback();
  2572. },
  2573. final(callback) {
  2574. callback();
  2575. },
  2576. });
  2577. pageCursor
  2578. .pipe(createBatchStream(batchSize))
  2579. .pipe(recountWriteStream);
  2580. await streamToPromise(recountWriteStream);
  2581. }
  2582. // update descendantCount of all pages that are ancestors of a provided pageId by count
  2583. async updateDescendantCountOfAncestors(pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean): Promise<void> {
  2584. const Page = this.crowi.model('Page');
  2585. const ancestors = await Page.findAncestorsUsingParentRecursively(pageId, shouldIncludeTarget);
  2586. const ancestorPageIds = ancestors.map(p => p._id);
  2587. await Page.incrementDescendantCountOfPageIds(ancestorPageIds, inc);
  2588. const updateDescCountData: UpdateDescCountRawData = Object.fromEntries(ancestors.map(p => [p._id.toString(), p.descendantCount + inc]));
  2589. this.emitUpdateDescCount(updateDescCountData);
  2590. }
  2591. private emitUpdateDescCount(data: UpdateDescCountRawData): void {
  2592. const socket = this.crowi.socketIoService.getDefaultSocket();
  2593. socket.emit(SocketEventName.UpdateDescCount, data);
  2594. }
  2595. /**
  2596. * Build the base aggregation pipeline for fillAncestors--- methods
  2597. * @param onlyMigratedAsExistingPages Determine whether to include non-migrated pages as existing pages. If a page exists,
  2598. * an empty page will not be created at that page's path.
  2599. */
  2600. private buildBasePipelineToCreateEmptyPages(paths: string[], onlyMigratedAsExistingPages = true, andFilter?): any[] {
  2601. const aggregationPipeline: any[] = [];
  2602. const Page = mongoose.model('Page') as unknown as PageModel;
  2603. // -- Filter by paths
  2604. aggregationPipeline.push({ $match: { path: { $in: paths } } });
  2605. // -- Normalized condition
  2606. if (onlyMigratedAsExistingPages) {
  2607. aggregationPipeline.push({
  2608. $match: {
  2609. $or: [
  2610. { grant: Page.GRANT_PUBLIC },
  2611. { parent: { $ne: null } },
  2612. { path: '/' },
  2613. ],
  2614. },
  2615. });
  2616. }
  2617. // -- Add custom pipeline
  2618. if (andFilter != null) {
  2619. aggregationPipeline.push({ $match: andFilter });
  2620. }
  2621. return aggregationPipeline;
  2622. }
  2623. private async buildPipelineToCreateEmptyPagesByUser(user, paths: string[], onlyMigratedAsExistingPages = true, andFilter?): Promise<any[]> {
  2624. const Page = mongoose.model('Page') as unknown as PageModel;
  2625. const pipeline = this.buildBasePipelineToCreateEmptyPages(paths, onlyMigratedAsExistingPages, andFilter);
  2626. let userGroups = null;
  2627. if (user != null) {
  2628. const UserGroupRelation = mongoose.model('UserGroupRelation') as any;
  2629. userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  2630. }
  2631. const grantCondition = Page.generateGrantCondition(user, userGroups);
  2632. pipeline.push({ $match: grantCondition });
  2633. return pipeline;
  2634. }
  2635. private buildPipelineToCreateEmptyPagesBySystem(paths: string[]): any[] {
  2636. return this.buildBasePipelineToCreateEmptyPages(paths);
  2637. }
  2638. private async connectPageTree(path: string): Promise<void> {
  2639. const Page = mongoose.model('Page') as unknown as PageModel;
  2640. const { PageQueryBuilder } = Page;
  2641. const ancestorPaths = collectAncestorPaths(path);
  2642. // Find ancestors
  2643. const builder = new PageQueryBuilder(Page.find(), true);
  2644. builder.addConditionToFilterByApplicableAncestors(ancestorPaths); // avoid including not normalized pages
  2645. const ancestors = await builder
  2646. .addConditionToListByPathsArray(ancestorPaths)
  2647. .addConditionToSortPagesByDescPath()
  2648. .query
  2649. .exec();
  2650. // Update parent attrs
  2651. const ancestorsMap = new Map(); // Map<path, page>
  2652. ancestors.forEach(page => !ancestorsMap.has(page.path) && ancestorsMap.set(page.path, page)); // the earlier element should be the true ancestor
  2653. const nonRootAncestors = ancestors.filter(page => !isTopPage(page.path));
  2654. const operations = nonRootAncestors.map((page) => {
  2655. const parentPath = pathlib.dirname(page.path);
  2656. return {
  2657. updateOne: {
  2658. filter: {
  2659. _id: page._id,
  2660. },
  2661. update: {
  2662. parent: ancestorsMap.get(parentPath)._id,
  2663. },
  2664. },
  2665. };
  2666. });
  2667. await Page.bulkWrite(operations);
  2668. }
  2669. /**
  2670. * Find parent or create parent if not exists.
  2671. * It also updates parent of ancestors
  2672. * @param path string
  2673. * @returns Promise<PageDocument>
  2674. */
  2675. async getParentAndFillAncestorsByUser(user, path: string): Promise<PageDocument> {
  2676. const Page = mongoose.model('Page') as unknown as PageModel;
  2677. // Find parent
  2678. const parent = await Page.findParentByPath(path);
  2679. if (parent != null) {
  2680. return parent;
  2681. }
  2682. const ancestorPaths = collectAncestorPaths(path);
  2683. // Fill ancestors
  2684. const aggregationPipeline: any[] = await this.buildPipelineToCreateEmptyPagesByUser(user, ancestorPaths);
  2685. await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
  2686. // Connect ancestors
  2687. await this.connectPageTree(path);
  2688. // Return the created parent
  2689. const createdParent = await Page.findParentByPath(path);
  2690. if (createdParent == null) {
  2691. throw Error('Failed to find the created parent by getParentAndFillAncestorsByUser');
  2692. }
  2693. return createdParent;
  2694. }
  2695. async getParentAndFillAncestorsBySystem(path: string): Promise<PageDocument> {
  2696. const Page = mongoose.model('Page') as unknown as PageModel;
  2697. // Find parent
  2698. const parent = await Page.findParentByPath(path);
  2699. if (parent != null) {
  2700. return parent;
  2701. }
  2702. // Fill ancestors
  2703. const ancestorPaths = collectAncestorPaths(path);
  2704. const aggregationPipeline: any[] = this.buildPipelineToCreateEmptyPagesBySystem(ancestorPaths);
  2705. await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
  2706. // Connect ancestors
  2707. await this.connectPageTree(path);
  2708. // Return the created parent
  2709. const createdParent = await Page.findParentByPath(path);
  2710. if (createdParent == null) {
  2711. throw Error('Failed to find the created parent by getParentAndFillAncestorsByUser');
  2712. }
  2713. return createdParent;
  2714. }
  2715. // --------- Create ---------
  2716. private async preparePageDocumentToCreate(path: string, shouldNew: boolean): Promise<PageDocument> {
  2717. const Page = mongoose.model('Page') as unknown as PageModel;
  2718. const emptyPage = await Page.findOne({ path, isEmpty: true });
  2719. // Use empty page if exists, if not, create a new page
  2720. let page;
  2721. if (shouldNew) {
  2722. page = new Page();
  2723. }
  2724. else if (emptyPage != null) {
  2725. page = emptyPage;
  2726. const descendantCount = await Page.recountDescendantCount(page._id);
  2727. page.descendantCount = descendantCount;
  2728. page.isEmpty = false;
  2729. }
  2730. else {
  2731. page = new Page();
  2732. }
  2733. return page;
  2734. }
  2735. private setFieldExceptForGrantRevisionParent(
  2736. pageDocument: PageDocument,
  2737. path: string,
  2738. user?,
  2739. ): void {
  2740. const Page = mongoose.model('Page') as unknown as PageModel;
  2741. pageDocument.path = path;
  2742. pageDocument.creator = user;
  2743. pageDocument.lastUpdateUser = user;
  2744. pageDocument.status = Page.STATUS_PUBLISHED;
  2745. }
  2746. private async canProcessCreate(
  2747. path: string,
  2748. grantData: {
  2749. grant: number,
  2750. grantedUserIds?: ObjectIdLike[],
  2751. grantUserGroupId?: ObjectIdLike,
  2752. },
  2753. shouldValidateGrant: boolean,
  2754. user?,
  2755. ): Promise<boolean> {
  2756. const Page = mongoose.model('Page') as unknown as PageModel;
  2757. // Operatability validation
  2758. const canOperate = await this.crowi.pageOperationService.canOperate(false, null, path);
  2759. if (!canOperate) {
  2760. logger.error(`Cannot operate create to path "${path}" right now.`);
  2761. return false;
  2762. }
  2763. // Existance validation
  2764. const isExist = (await Page.count({ path, isEmpty: false })) > 0; // not validate empty page
  2765. if (isExist) {
  2766. logger.error('Cannot create new page to existed path');
  2767. return false;
  2768. }
  2769. // UserGroup & Owner validation
  2770. const { grant, grantedUserIds, grantUserGroupId } = grantData;
  2771. if (shouldValidateGrant) {
  2772. if (user == null) {
  2773. throw Error('user is required to validate grant');
  2774. }
  2775. let isGrantNormalized = false;
  2776. try {
  2777. // It must check descendants as well if emptyTarget is not null
  2778. const isEmptyPageAlreadyExist = await Page.count({ path, isEmpty: true }) > 0;
  2779. const shouldCheckDescendants = isEmptyPageAlreadyExist;
  2780. isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantUserGroupId, shouldCheckDescendants);
  2781. }
  2782. catch (err) {
  2783. logger.error(`Failed to validate grant of page at "${path}" of grant ${grant}:`, err);
  2784. throw err;
  2785. }
  2786. if (!isGrantNormalized) {
  2787. throw Error('The selected grant or grantedGroup is not assignable to this page.');
  2788. }
  2789. }
  2790. return true;
  2791. }
  2792. async create(path: string, body: string, user, options: PageCreateOptions = {}): Promise<PageDocument> {
  2793. const Page = mongoose.model('Page') as unknown as PageModel;
  2794. // Switch method
  2795. const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  2796. if (!isV5Compatible) {
  2797. return Page.createV4(path, body, user, options);
  2798. }
  2799. // Values
  2800. // eslint-disable-next-line no-param-reassign
  2801. path = this.crowi.xss.process(path); // sanitize path
  2802. const {
  2803. format = 'markdown', grantUserGroupId,
  2804. } = options;
  2805. const grant = isTopPage(path) ? Page.GRANT_PUBLIC : options.grant;
  2806. const grantData = {
  2807. grant,
  2808. grantedUserIds: grant === Page.GRANT_OWNER ? [user._id] : undefined,
  2809. grantUserGroupId,
  2810. };
  2811. const isGrantRestricted = grant === Page.GRANT_RESTRICTED;
  2812. // Validate
  2813. const shouldValidateGrant = !isGrantRestricted;
  2814. const canProcessCreate = await this.canProcessCreate(path, grantData, shouldValidateGrant, user);
  2815. if (!canProcessCreate) {
  2816. throw Error('Cannnot process create');
  2817. }
  2818. // Prepare a page document
  2819. const shouldNew = isGrantRestricted;
  2820. const page = await this.preparePageDocumentToCreate(path, shouldNew);
  2821. // Set field
  2822. this.setFieldExceptForGrantRevisionParent(page, path, user);
  2823. // Apply scope
  2824. page.applyScope(user, grant, grantUserGroupId);
  2825. // Set parent
  2826. if (isTopPage(path) || isGrantRestricted) { // set parent to null when GRANT_RESTRICTED
  2827. page.parent = null;
  2828. }
  2829. else {
  2830. const parent = await this.getParentAndFillAncestorsByUser(user, path);
  2831. page.parent = parent._id;
  2832. }
  2833. // Save
  2834. let savedPage = await page.save();
  2835. // Create revision
  2836. const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
  2837. const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
  2838. savedPage = await pushRevision(savedPage, newRevision, user);
  2839. await savedPage.populateDataToShowRevision();
  2840. // Update descendantCount
  2841. await this.updateDescendantCountOfAncestors(savedPage._id, 1, false);
  2842. // Emit create event
  2843. this.pageEvent.emit('create', savedPage, user);
  2844. // Delete PageRedirect if exists
  2845. const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
  2846. try {
  2847. await PageRedirect.deleteOne({ fromPath: path });
  2848. logger.warn(`Deleted page redirect after creating a new page at path "${path}".`);
  2849. }
  2850. catch (err) {
  2851. // no throw
  2852. logger.error('Failed to delete PageRedirect');
  2853. }
  2854. return savedPage;
  2855. }
  2856. private async canProcessForceCreateBySystem(
  2857. path: string,
  2858. grantData: {
  2859. grant: number,
  2860. grantedUserIds?: ObjectIdLike[],
  2861. grantUserGroupId?: ObjectIdLike,
  2862. },
  2863. ): Promise<boolean> {
  2864. return this.canProcessCreate(path, grantData, false);
  2865. }
  2866. /**
  2867. * @private
  2868. * This method receives the same arguments as the PageService.create method does except for the added type '{ grantedUsers?: ObjectIdLike[] }'.
  2869. * This additional value is used to determine the grantedUser of the page to be created by system.
  2870. * This method must not run isGrantNormalized method to validate grant. **If necessary, run it before use this method.**
  2871. * -- Reason 1: This is because it is not expected to use this method when the grant validation is required.
  2872. * -- Reason 2: This is because it is not expected to use this method when the program cannot determine the operator.
  2873. */
  2874. private async forceCreateBySystem(path: string, body: string, options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] }): Promise<PageDocument> {
  2875. const Page = mongoose.model('Page') as unknown as PageModel;
  2876. const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
  2877. if (!isV5Compatible) {
  2878. throw Error('This method is available only when v5 compatible');
  2879. }
  2880. // Values
  2881. // eslint-disable-next-line no-param-reassign
  2882. path = this.crowi.xss.process(path); // sanitize path
  2883. const {
  2884. format = 'markdown', grantUserGroupId, grantedUsers,
  2885. } = options;
  2886. const grant = isTopPage(path) ? Page.GRANT_PUBLIC : options.grant;
  2887. const isGrantRestricted = grant === Page.GRANT_RESTRICTED;
  2888. const isGrantOwner = grant === Page.GRANT_OWNER;
  2889. const grantData = {
  2890. grant,
  2891. grantedUserIds: isGrantOwner ? grantedUsers : undefined,
  2892. grantUserGroupId,
  2893. };
  2894. // Validate
  2895. if (isGrantOwner && grantedUsers?.length !== 1) {
  2896. throw Error('grantedUser must exist when grant is GRANT_OWNER');
  2897. }
  2898. const canProcessForceCreateBySystem = await this.canProcessForceCreateBySystem(path, grantData);
  2899. if (!canProcessForceCreateBySystem) {
  2900. throw Error('Cannnot process forceCreateBySystem');
  2901. }
  2902. // Prepare a page document
  2903. const shouldNew = isGrantRestricted;
  2904. const page = await this.preparePageDocumentToCreate(path, shouldNew);
  2905. // Set field
  2906. this.setFieldExceptForGrantRevisionParent(page, path);
  2907. // Apply scope
  2908. page.applyScope({ _id: grantedUsers?.[0] }, grant, grantUserGroupId);
  2909. // Set parent
  2910. if (isTopPage(path) || isGrantRestricted) { // set parent to null when GRANT_RESTRICTED
  2911. page.parent = null;
  2912. }
  2913. else {
  2914. const parent = await this.getParentAndFillAncestorsBySystem(path);
  2915. page.parent = parent._id;
  2916. }
  2917. // Save
  2918. let savedPage = await page.save();
  2919. // Create revision
  2920. const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
  2921. const dummyUser = { _id: new mongoose.Types.ObjectId() };
  2922. const newRevision = Revision.prepareRevision(savedPage, body, null, dummyUser, { format });
  2923. savedPage = await pushRevision(savedPage, newRevision, dummyUser);
  2924. // Update descendantCount
  2925. await this.updateDescendantCountOfAncestors(savedPage._id, 1, false);
  2926. // Emit create event
  2927. this.pageEvent.emit('create', savedPage, dummyUser);
  2928. return savedPage;
  2929. }
  2930. /*
  2931. * Find all children by parent's path or id. Using id should be prioritized
  2932. */
  2933. async findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups = null): Promise<PageDocument[]> {
  2934. const Page = mongoose.model('Page') as unknown as PageModel;
  2935. let queryBuilder: PageQueryBuilder;
  2936. if (hasSlash(parentPathOrId)) {
  2937. const path = parentPathOrId;
  2938. const regexp = generateChildrenRegExp(path);
  2939. queryBuilder = new PageQueryBuilder(Page.find({ path: { $regex: regexp } }), true);
  2940. }
  2941. else {
  2942. const parentId = parentPathOrId;
  2943. // Use $eq for user-controlled sources. see: https://codeql.github.com/codeql-query-help/javascript/js-sql-injection/#recommendation
  2944. queryBuilder = new PageQueryBuilder(Page.find({ parent: { $eq: parentId } } as any), true); // TODO: improve type
  2945. }
  2946. await queryBuilder.addViewerCondition(user, userGroups);
  2947. const pages = await queryBuilder
  2948. .addConditionToSortPagesByAscPath()
  2949. .query
  2950. .lean()
  2951. .exec();
  2952. await this.injectProcessDataIntoPagesByActionTypes(pages, [PageActionType.Rename]);
  2953. return pages;
  2954. }
  2955. async findAncestorsChildrenByPathAndViewer(path: string, user, userGroups = null): Promise<Record<string, PageDocument[]>> {
  2956. const Page = mongoose.model('Page') as unknown as PageModel;
  2957. const ancestorPaths = isTopPage(path) ? ['/'] : collectAncestorPaths(path); // root path is necessary for rendering
  2958. const regexps = ancestorPaths.map(path => new RegExp(generateChildrenRegExp(path))); // cannot use re2
  2959. // get pages at once
  2960. const queryBuilder = new PageQueryBuilder(Page.find({ path: { $in: regexps } }), true);
  2961. await queryBuilder.addViewerCondition(user, userGroups);
  2962. const pages = await queryBuilder
  2963. .addConditionAsOnTree()
  2964. .addConditionToMinimizeDataForRendering()
  2965. .addConditionToSortPagesByAscPath()
  2966. .query
  2967. .lean()
  2968. .exec();
  2969. this.injectIsTargetIntoPages(pages, path);
  2970. await this.injectProcessDataIntoPagesByActionTypes(pages, [PageActionType.Rename]);
  2971. /*
  2972. * If any non-migrated page is found during creating the pathToChildren map, it will stop incrementing at that moment
  2973. */
  2974. const pathToChildren: Record<string, PageDocument[]> = {};
  2975. const sortedPaths = ancestorPaths.sort((a, b) => a.length - b.length); // sort paths by path.length
  2976. sortedPaths.every((path) => {
  2977. const children = pages.filter(page => pathlib.dirname(page.path) === path);
  2978. if (children.length === 0) {
  2979. return false; // break when children do not exist
  2980. }
  2981. pathToChildren[path] = children;
  2982. return true;
  2983. });
  2984. return pathToChildren;
  2985. }
  2986. private injectIsTargetIntoPages(pages: (PageDocument & {isTarget?: boolean})[], path): void {
  2987. pages.forEach((page) => {
  2988. if (page.path === path) {
  2989. page.isTarget = true;
  2990. }
  2991. });
  2992. }
  2993. /**
  2994. * Inject processData into page docuements
  2995. * The processData is a combination of actionType as a key and information on whether the action is processable as a value.
  2996. */
  2997. private async injectProcessDataIntoPagesByActionTypes(
  2998. pages: (PageDocument & { processData?: IPageOperationProcessData })[],
  2999. actionTypes: PageActionType[],
  3000. ): Promise<void> {
  3001. const pageOperations = await PageOperation.find({ actionType: { $in: actionTypes } });
  3002. if (pageOperations == null || pageOperations.length === 0) {
  3003. return;
  3004. }
  3005. const processInfo: IPageOperationProcessInfo = this.crowi.pageOperationService.generateProcessInfo(pageOperations);
  3006. const operatingPageIds: string[] = Object.keys(processInfo);
  3007. // inject processData into pages
  3008. pages.forEach((page) => {
  3009. const pageId = page._id.toString();
  3010. if (operatingPageIds.includes(pageId)) {
  3011. const processData: IPageOperationProcessData = processInfo[pageId];
  3012. page.processData = processData;
  3013. }
  3014. });
  3015. }
  3016. }
  3017. export default PageService;