index.ts 141 KB

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