page.ts 147 KB

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