index.ts 157 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566
  1. import {
  2. getIdForRef,
  3. getIdStringForRef,
  4. PageStatus,
  5. YDocStatus,
  6. } from '@growi/core';
  7. import type {
  8. HasObjectId,
  9. IGrantedGroup,
  10. IPage,
  11. IPageInfoBasic,
  12. IPageInfoBasicForEmpty,
  13. IPageInfoBasicForEntity,
  14. IRevisionHasId,
  15. IUser,
  16. IUserHasId,
  17. Ref,
  18. } from '@growi/core/dist/interfaces';
  19. import { PageGrant } from '@growi/core/dist/interfaces';
  20. import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
  21. import escapeStringRegexp from 'escape-string-regexp';
  22. import type EventEmitter from 'events';
  23. import type { Cursor, HydratedDocument } from 'mongoose';
  24. import mongoose from 'mongoose';
  25. import pathlib from 'path';
  26. import { Readable, Writable } from 'stream';
  27. import { pipeline } from 'stream/promises';
  28. import { Comment } from '~/features/comment/server';
  29. import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
  30. import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
  31. import { isAiEnabled } from '~/features/openai/server/services';
  32. import { SupportedAction } from '~/interfaces/activity';
  33. import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
  34. import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
  35. import type { IPageDeleteConfigValueToProcessValidation } from '~/interfaces/page-delete-config';
  36. import {
  37. PageDeleteConfigValue,
  38. PageSingleDeleteCompConfigValue,
  39. } from '~/interfaces/page-delete-config';
  40. import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
  41. import { PageActionStage, PageActionType } from '~/interfaces/page-operation';
  42. import { PageActionOnGroupDelete } from '~/interfaces/user-group';
  43. import {
  44. type PageMigrationErrorData,
  45. SocketEventName,
  46. type UpdateDescCountRawData,
  47. } from '~/interfaces/websocket';
  48. import type { CurrentPageYjsData } from '~/interfaces/yjs';
  49. import type Crowi from '~/server/crowi';
  50. import type { CreateMethod } from '~/server/models/page';
  51. import {
  52. type PageDocument,
  53. type PageModel,
  54. PageQueryBuilder,
  55. pushRevision,
  56. } from '~/server/models/page';
  57. import type { PageTagRelationDocument } from '~/server/models/page-tag-relation';
  58. import PageTagRelation from '~/server/models/page-tag-relation';
  59. import type { UserGroupDocument } from '~/server/models/user-group';
  60. import { createBatchStream } from '~/server/util/batch-stream';
  61. import { collectAncestorPaths } from '~/server/util/collect-ancestor-paths';
  62. import { generalXssFilter } from '~/services/general-xss-filter';
  63. import loggerFactory from '~/utils/logger';
  64. import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
  65. import type { ObjectIdLike } from '../../interfaces/mongoose-utils';
  66. import { Attachment } from '../../models/attachment';
  67. import { PathAlreadyExistsError } from '../../models/errors';
  68. import type { PageOperationDocument } from '../../models/page-operation';
  69. import PageOperation from '../../models/page-operation';
  70. import PageRedirect from '../../models/page-redirect';
  71. import type { IRevisionDocument } from '../../models/revision';
  72. import { Revision } from '../../models/revision';
  73. import { serializePageSecurely } from '../../models/serializers/page-serializer';
  74. import ShareLink from '../../models/share-link';
  75. import Subscription from '../../models/subscription';
  76. import UserGroupRelation from '../../models/user-group-relation';
  77. import { V5ConversionError } from '../../models/vo/v5-conversion-error';
  78. import { divideByType } from '../../util/granted-group';
  79. import { configManager } from '../config-manager';
  80. import type { IPageGrantService } from '../page-grant';
  81. import { preNotifyService } from '../pre-notify';
  82. import { getYjsService } from '../yjs';
  83. import { BULK_REINDEX_SIZE, LIMIT_FOR_MULTIPLE_PAGE_OP } from './consts';
  84. import { onSeen } from './events/seen';
  85. import type { IPageService } from './page-service';
  86. import { shouldUseV4Process } from './should-use-v4-process';
  87. export * from './page-service';
  88. const logger = loggerFactory('growi:services:page');
  89. const {
  90. isTrashPage,
  91. isTopPage,
  92. omitDuplicateAreaPageFromPages,
  93. getUsernameByPath,
  94. isUsersTopPage,
  95. isMovablePage,
  96. isUsersHomepage,
  97. } = pagePathUtils;
  98. const { addTrailingSlash } = pathUtils;
  99. // TODO: improve type
  100. class PageCursorsForDescendantsFactory {
  101. private user: any; // TODO: Typescriptize model
  102. private rootPage: any; // TODO: wait for mongoose update
  103. private shouldIncludeEmpty: boolean;
  104. private initialCursor: Cursor<any> | never[]; // TODO: wait for mongoose update
  105. constructor(user: any, rootPage: any, shouldIncludeEmpty: boolean) {
  106. this.user = user;
  107. this.rootPage = rootPage;
  108. this.shouldIncludeEmpty = shouldIncludeEmpty;
  109. }
  110. // prepare initial cursor
  111. private async init() {
  112. const initialCursor = await this.generateCursorToFindChildren(
  113. this.rootPage,
  114. );
  115. this.initialCursor = initialCursor;
  116. }
  117. /**
  118. * Returns Iterable that yields only descendant pages unorderedly
  119. * @returns Promise<AsyncGenerator>
  120. */
  121. async generateIterable(): Promise<AsyncGenerator | never[]> {
  122. // initialize cursor
  123. await this.init();
  124. return this.isNeverArray(this.initialCursor)
  125. ? []
  126. : this.generateOnlyDescendants(this.initialCursor);
  127. }
  128. /**
  129. * Returns Readable that produces only descendant pages unorderedly
  130. * @returns Promise<Readable>
  131. */
  132. async generateReadable(): Promise<Readable> {
  133. return Readable.from(await this.generateIterable());
  134. }
  135. /**
  136. * Generator that unorderedly yields descendant pages
  137. */
  138. private async *generateOnlyDescendants(cursor: Cursor<any>) {
  139. for await (const page of cursor) {
  140. const nextCursor = await this.generateCursorToFindChildren(page);
  141. if (!this.isNeverArray(nextCursor)) {
  142. yield* this.generateOnlyDescendants(nextCursor); // recursively yield
  143. }
  144. yield page;
  145. }
  146. }
  147. private async generateCursorToFindChildren(
  148. page: any,
  149. ): Promise<Cursor<any> | never[]> {
  150. if (page == null) {
  151. return [];
  152. }
  153. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  154. 'Page',
  155. );
  156. const { PageQueryBuilder } = Page;
  157. const builder = new PageQueryBuilder(Page.find(), this.shouldIncludeEmpty);
  158. builder.addConditionToFilteringByParentId(page._id);
  159. const cursor = builder.query
  160. .lean()
  161. .cursor({ batchSize: BULK_REINDEX_SIZE }) as Cursor<any>;
  162. return cursor;
  163. }
  164. private isNeverArray(val: Cursor<any> | never[]): val is never[] {
  165. return 'length' in val && val.length === 0;
  166. }
  167. }
  168. class PageService implements IPageService {
  169. crowi: Crowi;
  170. pageEvent: EventEmitter & {
  171. onCreate;
  172. onCreateMany;
  173. onAddSeenUsers;
  174. };
  175. tagEvent: any;
  176. activityEvent: any;
  177. pageGrantService: IPageGrantService;
  178. constructor(crowi: Crowi) {
  179. this.crowi = crowi;
  180. this.pageEvent = crowi.events.page;
  181. this.tagEvent = crowi.events.tag;
  182. this.activityEvent = crowi.events.activity;
  183. this.pageGrantService = crowi.pageGrantService;
  184. // init
  185. this.initPageEvent();
  186. this.canDeleteCompletely = this.canDeleteCompletely.bind(this);
  187. this.canDelete = this.canDelete.bind(this);
  188. }
  189. private initPageEvent() {
  190. // create
  191. this.pageEvent.on('create', this.pageEvent.onCreate);
  192. // createMany
  193. this.pageEvent.on('createMany', this.pageEvent.onCreateMany);
  194. this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
  195. // seen - mark page as seen by user
  196. this.pageEvent.on('seen', onSeen);
  197. }
  198. getEventEmitter(): EventEmitter {
  199. return this.pageEvent;
  200. }
  201. /**
  202. * Check if page can be deleted completely.
  203. * Use the following methods before execution of canDeleteCompletely to get params.
  204. * - pageService.getCreatorIdForCanDelete: creatorId
  205. * - pageGrantService.getUserRelatedGroups: userRelatedGroups
  206. * Do NOT make this method async as for now, because canDeleteCompletely is called in /page-listing/info in a for loop,
  207. * and /page-listing/info should not be an execution heavy API.
  208. */
  209. canDeleteCompletely(
  210. page: PageDocument,
  211. creatorId: ObjectIdLike | null,
  212. operator: any | null,
  213. isRecursively: boolean,
  214. userRelatedGroups: PopulatedGrantedGroup[],
  215. ): boolean {
  216. if (operator == null || isTopPage(page.path) || isUsersTopPage(page.path))
  217. return false;
  218. const pageCompleteDeletionAuthority = configManager.getConfig(
  219. 'security:pageCompleteDeletionAuthority',
  220. );
  221. const pageRecursiveCompleteDeletionAuthority = configManager.getConfig(
  222. 'security:pageRecursiveCompleteDeletionAuthority',
  223. );
  224. if (
  225. !this.canDeleteCompletelyAsMultiGroupGrantedPage(
  226. page,
  227. creatorId,
  228. operator,
  229. userRelatedGroups,
  230. )
  231. )
  232. return false;
  233. const [singleAuthority, recursiveAuthority] =
  234. prepareDeleteConfigValuesForCalc(
  235. pageCompleteDeletionAuthority,
  236. pageRecursiveCompleteDeletionAuthority,
  237. );
  238. return this.canDeleteLogic(
  239. creatorId,
  240. operator,
  241. isRecursively,
  242. singleAuthority,
  243. recursiveAuthority,
  244. );
  245. }
  246. /**
  247. * If page is multi-group granted, check if operator is allowed to completely delete the page.
  248. * see: https://dev.growi.org/656745fa52eafe1cf1879508#%E5%AE%8C%E5%85%A8%E3%81%AB%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B%E6%93%8D%E4%BD%9C
  249. * creatorId must be obtained by getCreatorIdForCanDelete
  250. */
  251. canDeleteCompletelyAsMultiGroupGrantedPage(
  252. page: PageDocument,
  253. creatorId: ObjectIdLike | null,
  254. operator: any | null,
  255. userRelatedGroups: PopulatedGrantedGroup[],
  256. ): boolean {
  257. const pageCompleteDeletionAuthority = configManager.getConfig(
  258. 'security:pageCompleteDeletionAuthority',
  259. );
  260. const isAllGroupMembershipRequiredForPageCompleteDeletion =
  261. configManager.getConfig(
  262. 'security:isAllGroupMembershipRequiredForPageCompleteDeletion',
  263. );
  264. const isAdmin = operator?.admin ?? false;
  265. const isAuthor =
  266. operator?._id == null ? false : operator._id.equals(creatorId);
  267. const isAdminOrAuthor = isAdmin || isAuthor;
  268. if (
  269. page.grant === PageGrant.GRANT_USER_GROUP &&
  270. !isAdminOrAuthor &&
  271. pageCompleteDeletionAuthority ===
  272. PageSingleDeleteCompConfigValue.Anyone &&
  273. isAllGroupMembershipRequiredForPageCompleteDeletion
  274. ) {
  275. const userRelatedGrantedGroups =
  276. this.pageGrantService.getUserRelatedGrantedGroupsSyncronously(
  277. userRelatedGroups,
  278. page,
  279. );
  280. if (userRelatedGrantedGroups.length !== page.grantedGroups.length) {
  281. return false;
  282. }
  283. }
  284. return true;
  285. }
  286. // When page is empty, the 'canDelete' judgement should be done using the creator of the closest non-empty ancestor page.
  287. async getCreatorIdForCanDelete(
  288. page: PageDocument,
  289. ): Promise<ObjectIdLike | null> {
  290. if (page.isEmpty) {
  291. const Page = mongoose.model<IPage, PageModel>('Page');
  292. const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(
  293. page.path,
  294. );
  295. return notEmptyClosestAncestor?.creator == null
  296. ? null
  297. : getIdForRef(notEmptyClosestAncestor.creator);
  298. }
  299. return page.creator == null ? null : getIdForRef(page.creator);
  300. }
  301. // Use getCreatorIdForCanDelete before execution of canDelete to get creatorId.
  302. canDelete(
  303. page: PageDocument,
  304. creatorId: ObjectIdLike | null,
  305. operator: any | null,
  306. isRecursively: boolean,
  307. ): boolean {
  308. if (operator == null || isTopPage(page.path) || isUsersTopPage(page.path))
  309. return false;
  310. const pageDeletionAuthority = configManager.getConfig(
  311. 'security:pageDeletionAuthority',
  312. );
  313. const pageRecursiveDeletionAuthority = configManager.getConfig(
  314. 'security:pageRecursiveDeletionAuthority',
  315. );
  316. const [singleAuthority, recursiveAuthority] =
  317. prepareDeleteConfigValuesForCalc(
  318. pageDeletionAuthority,
  319. pageRecursiveDeletionAuthority,
  320. );
  321. return this.canDeleteLogic(
  322. creatorId,
  323. operator,
  324. isRecursively,
  325. singleAuthority,
  326. recursiveAuthority,
  327. );
  328. }
  329. canDeleteUserHomepageByConfig(): boolean {
  330. return (
  331. configManager.getConfig('security:user-homepage-deletion:isEnabled') ??
  332. false
  333. );
  334. }
  335. async isUsersHomepageOwnerAbsent(path: string): Promise<boolean> {
  336. const User = mongoose.model('User');
  337. const username = getUsernameByPath(path);
  338. if (username == null) {
  339. throw new Error('Cannot found username by path');
  340. }
  341. const ownerExists = await User.exists({ username }).exec();
  342. return ownerExists == null;
  343. }
  344. private canDeleteLogic(
  345. creatorId: ObjectIdLike | null,
  346. operator,
  347. isRecursively: boolean,
  348. authority: IPageDeleteConfigValueToProcessValidation | null,
  349. recursiveAuthority: IPageDeleteConfigValueToProcessValidation | null,
  350. ): boolean {
  351. const isAdmin = operator?.admin ?? false;
  352. const isAuthor =
  353. operator?._id == null ? false : operator._id.equals(creatorId);
  354. if (isRecursively) {
  355. return this.compareDeleteConfig(isAdmin, isAuthor, recursiveAuthority);
  356. }
  357. return this.compareDeleteConfig(isAdmin, isAuthor, authority);
  358. }
  359. private compareDeleteConfig(
  360. isAdmin: boolean,
  361. isAuthor: boolean,
  362. authority: IPageDeleteConfigValueToProcessValidation | null,
  363. ): boolean {
  364. if (isAdmin) {
  365. return true;
  366. }
  367. if (authority === PageDeleteConfigValue.Anyone || authority == null) {
  368. return true;
  369. }
  370. if (authority === PageDeleteConfigValue.AdminAndAuthor && isAuthor) {
  371. return true;
  372. }
  373. return false;
  374. }
  375. private async getAbsenseUserHomeList(
  376. pages: PageDocument[],
  377. ): Promise<string[]> {
  378. const userHomepages = pages.filter((p) => isUsersHomepage(p.path));
  379. const User = mongoose.model<IUser>('User');
  380. const usernames = userHomepages
  381. .map((page) => getUsernameByPath(page.path))
  382. // see: https://zenn.dev/kimuson/articles/filter_safety_type_guard
  383. .filter(
  384. (username): username is Exclude<typeof username, null> =>
  385. username !== null,
  386. );
  387. const existingUsernames = await User.distinct<string>('username', {
  388. username: { $in: usernames },
  389. });
  390. return userHomepages
  391. .filter((page) => {
  392. const username = getUsernameByPath(page.path);
  393. if (username == null) {
  394. throw new Error('Cannot found username by path');
  395. }
  396. return !existingUsernames.includes(username);
  397. })
  398. .map((p) => p.path);
  399. }
  400. private async filterPages(
  401. pages: PageDocument[],
  402. user: IUserHasId,
  403. isRecursively: boolean,
  404. canDeleteFunction: (
  405. page: PageDocument,
  406. creatorId: ObjectIdLike | null,
  407. operator: any,
  408. isRecursively: boolean,
  409. userRelatedGroups: PopulatedGrantedGroup[],
  410. ) => boolean,
  411. ): Promise<PageDocument[]> {
  412. const userRelatedGroups =
  413. await this.pageGrantService.getUserRelatedGroups(user);
  414. const filteredPages = pages.filter(async (p) => {
  415. if (p.isEmpty) return true;
  416. const canDelete = canDeleteFunction(
  417. p,
  418. p.creator == null ? null : getIdForRef(p.creator),
  419. user,
  420. isRecursively,
  421. userRelatedGroups,
  422. );
  423. return canDelete;
  424. });
  425. if (!this.canDeleteUserHomepageByConfig()) {
  426. return filteredPages.filter((p) => !isUsersHomepage(p.path));
  427. }
  428. // Confirmation of deletion of user homepages is an asynchronous process,
  429. // so it is processed separately for performance optimization.
  430. const absenseUserHomeList =
  431. await this.getAbsenseUserHomeList(filteredPages);
  432. const excludeActiveUserHomepage = (path: string) => {
  433. if (!isUsersHomepage(path)) {
  434. return true;
  435. }
  436. return absenseUserHomeList.includes(path);
  437. };
  438. return filteredPages.filter((p) => excludeActiveUserHomepage(p.path));
  439. }
  440. async filterPagesByCanDeleteCompletely(
  441. pages: PageDocument[],
  442. user: IUserHasId,
  443. isRecursively: boolean,
  444. ): Promise<PageDocument[]> {
  445. return this.filterPages(
  446. pages,
  447. user,
  448. isRecursively,
  449. this.canDeleteCompletely,
  450. );
  451. }
  452. async filterPagesByCanDelete(
  453. pages: PageDocument[],
  454. user: IUserHasId,
  455. isRecursively: boolean,
  456. ): Promise<PageDocument[]> {
  457. return this.filterPages(pages, user, isRecursively, this.canDelete);
  458. }
  459. private shouldUseV4ProcessForRevert(page): boolean {
  460. const Page = mongoose.model('Page') as unknown as PageModel;
  461. const isV5Compatible = configManager.getConfig('app:isV5Compatible');
  462. const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
  463. const shouldUseV4Process = !isV5Compatible || isPageRestricted;
  464. return shouldUseV4Process;
  465. }
  466. private shouldNormalizeParent(page): boolean {
  467. const Page = mongoose.model('Page') as unknown as PageModel;
  468. return (
  469. page.grant !== Page.GRANT_RESTRICTED &&
  470. page.grant !== Page.GRANT_SPECIFIED
  471. );
  472. }
  473. /**
  474. * Generate read stream to operate descendants of the specified page path
  475. * @param {string} targetPagePath
  476. * @param {User} viewer
  477. */
  478. private async generateReadStreamToOperateOnlyDescendants(
  479. targetPagePath,
  480. userToOperate,
  481. ) {
  482. const Page = mongoose.model<IPage, PageModel>('Page');
  483. const { PageQueryBuilder } = Page;
  484. const builder = new PageQueryBuilder(Page.find(), true)
  485. .addConditionAsRootOrNotOnTree() // to avoid affecting v5 pages
  486. .addConditionToListOnlyDescendants(targetPagePath);
  487. await Page.addConditionToFilteringByViewerToEdit(builder, userToOperate);
  488. return builder.query.lean().cursor({ batchSize: BULK_REINDEX_SIZE });
  489. }
  490. async renamePage(
  491. page: IPage,
  492. newPagePath,
  493. user,
  494. options,
  495. activityParameters,
  496. ): Promise<PageDocument | null> {
  497. /*
  498. * Common Operation
  499. */
  500. const Page = mongoose.model('Page') as unknown as PageModel;
  501. const parameters = {
  502. ip: activityParameters.ip,
  503. endpoint: activityParameters.endpoint,
  504. action:
  505. page.descendantCount > 0
  506. ? SupportedAction.ACTION_PAGE_RECURSIVELY_RENAME
  507. : SupportedAction.ACTION_PAGE_RENAME,
  508. user,
  509. targetModel: 'Page',
  510. target: page,
  511. snapshot: {
  512. username: user.username,
  513. },
  514. };
  515. const activity =
  516. await this.crowi.activityService.createActivity(parameters);
  517. const isExist = await Page.exists({ path: newPagePath, isEmpty: false });
  518. if (isExist) {
  519. throw Error(`Page already exists at ${newPagePath}`);
  520. }
  521. if (isTopPage(page.path)) {
  522. throw Error('It is forbidden to rename the top page');
  523. }
  524. // Separate v4 & v5 process
  525. const isShouldUseV4Process = shouldUseV4Process(page);
  526. if (isShouldUseV4Process) {
  527. return this.renamePageV4(page, newPagePath, user, options);
  528. }
  529. const canOperate = await this.crowi.pageOperationService.canOperate(
  530. true,
  531. page.path,
  532. newPagePath,
  533. );
  534. if (!canOperate) {
  535. throw Error(`Cannot operate rename to path "${newPagePath}" right now.`);
  536. }
  537. /*
  538. * Resumable Operation
  539. */
  540. let pageOp: PageOperationDocument;
  541. try {
  542. pageOp = await PageOperation.create({
  543. actionType: PageActionType.Rename,
  544. actionStage: PageActionStage.Main,
  545. page,
  546. user,
  547. fromPath: page.path,
  548. toPath: newPagePath,
  549. options,
  550. });
  551. } catch (err) {
  552. logger.error('Failed to create PageOperation document.', err);
  553. throw err;
  554. }
  555. let renamedPage: PageDocument | null = null;
  556. try {
  557. renamedPage = await this.renameMainOperation(
  558. page,
  559. newPagePath,
  560. user,
  561. options,
  562. pageOp._id,
  563. activity,
  564. );
  565. } catch (err) {
  566. logger.error('Error occurred while running renameMainOperation', err);
  567. // cleanup
  568. await PageOperation.deleteOne({ _id: pageOp._id });
  569. throw err;
  570. }
  571. if (page.descendantCount < 1) {
  572. const preNotify = preNotifyService.generatePreNotify(activity);
  573. this.activityEvent.emit('updated', activity, page, preNotify);
  574. }
  575. this.disableAncestorPagesTtl(newPagePath);
  576. return renamedPage;
  577. }
  578. async renameMainOperation(
  579. page,
  580. newPagePath: string,
  581. user,
  582. options,
  583. pageOpId: ObjectIdLike,
  584. activity?,
  585. ): Promise<PageDocument | null> {
  586. const Page = mongoose.model<IPage, PageModel>('Page');
  587. const updateMetadata = options.updateMetadata || false;
  588. // sanitize path
  589. const newPagePathSanitized = generalXssFilter.process(newPagePath);
  590. // UserGroup & Owner validation
  591. // use the parent's grant when target page is an empty page
  592. let grant: PageGrant;
  593. let grantedUserIds: ObjectIdLike[] | undefined;
  594. let grantedGroupIds: IGrantedGroup[];
  595. if (page.isEmpty) {
  596. const parent = await Page.findOne({ _id: page.parent });
  597. if (parent == null) {
  598. throw Error('parent not found');
  599. }
  600. grant = parent.grant;
  601. grantedUserIds = parent.grantedUsers.map((u) => getIdForRef(u));
  602. grantedGroupIds = parent.grantedGroups;
  603. } else {
  604. grant = page.grant;
  605. grantedUserIds = page.grantedUsers;
  606. grantedGroupIds = page.grantedGroups;
  607. }
  608. if (grant !== Page.GRANT_RESTRICTED) {
  609. let isGrantNormalized = false;
  610. try {
  611. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  612. user,
  613. newPagePathSanitized,
  614. grant,
  615. grantedUserIds,
  616. grantedGroupIds,
  617. false,
  618. );
  619. } catch (err) {
  620. logger.error(
  621. `Failed to validate grant of page at "${newPagePathSanitized}" when renaming`,
  622. err,
  623. );
  624. throw err;
  625. }
  626. if (!isGrantNormalized) {
  627. throw Error(
  628. `This page cannot be renamed to "${newPagePathSanitized}" since the selected grant or grantedGroup is not assignable to this page.`,
  629. );
  630. }
  631. }
  632. // 1. Take target off from tree
  633. await Page.takeOffFromTree(page._id);
  634. // 2. Find new parent
  635. let newParent: PageDocument | undefined;
  636. // If renaming to under target, run getParentAndforceCreateEmptyTree to fill new ancestors
  637. if (this.isRenamingToUnderTarget(page.path, newPagePathSanitized)) {
  638. newParent = await this.getParentAndforceCreateEmptyTree(
  639. page,
  640. newPagePathSanitized,
  641. );
  642. } else {
  643. newParent = await this.getParentAndFillAncestorsByUser(
  644. user,
  645. newPagePathSanitized,
  646. );
  647. }
  648. // 3. Put back target page to tree (also update the other attrs)
  649. const update: Partial<IPage> = {};
  650. update.path = newPagePathSanitized;
  651. update.parent = newParent?._id;
  652. if (updateMetadata) {
  653. update.lastUpdateUser = user;
  654. update.updatedAt = new Date();
  655. }
  656. const renamedPage = await Page.findByIdAndUpdate(
  657. page._id,
  658. { $set: update },
  659. { new: true },
  660. );
  661. // 5.increase parent's descendantCount.
  662. // see: https://dev.growi.org/62149d019311629d4ecd91cf#Handling%20of%20descendantCount%20in%20case%20of%20unexpected%20process%20interruption
  663. const nToIncreaseForOperationInterruption = 1;
  664. await Page.incrementDescendantCountOfPageIds(
  665. [newParent?._id],
  666. nToIncreaseForOperationInterruption,
  667. );
  668. // create page redirect
  669. if (options.createRedirectPage) {
  670. await PageRedirect.create({
  671. fromPath: page.path,
  672. toPath: newPagePathSanitized,
  673. });
  674. }
  675. this.pageEvent.emit('rename');
  676. // Set to Sub
  677. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(
  678. pageOpId,
  679. PageActionStage.Sub,
  680. );
  681. if (pageOp == null) {
  682. throw Error('PageOperation document not found');
  683. }
  684. /*
  685. * Sub Operation
  686. */
  687. this.renameSubOperation(
  688. page,
  689. newPagePathSanitized,
  690. user,
  691. options,
  692. renamedPage,
  693. pageOp._id,
  694. activity,
  695. );
  696. return renamedPage;
  697. }
  698. async renameSubOperation(
  699. page,
  700. newPagePathSanitized: string,
  701. user,
  702. options,
  703. renamedPage,
  704. pageOpId: ObjectIdLike,
  705. activity?,
  706. ): Promise<void> {
  707. const Page = mongoose.model('Page') as unknown as PageModel;
  708. const exParentId = page.parent;
  709. const timerObj =
  710. this.crowi.pageOperationService.autoUpdateExpiryDate(pageOpId);
  711. try {
  712. // update descendants first
  713. const descendantsSubscribedSets = new Set();
  714. await this.renameDescendantsWithStream(
  715. page,
  716. newPagePathSanitized,
  717. user,
  718. options,
  719. false,
  720. descendantsSubscribedSets,
  721. );
  722. const descendantsSubscribedUsers = Array.from(
  723. descendantsSubscribedSets,
  724. ) as Ref<IUser>[];
  725. const preNotify = preNotifyService.generatePreNotify(
  726. activity,
  727. async () => {
  728. return descendantsSubscribedUsers;
  729. },
  730. );
  731. this.activityEvent.emit('updated', activity, page, preNotify);
  732. } catch (err) {
  733. logger.warn(err);
  734. throw Error(err);
  735. } finally {
  736. this.crowi.pageOperationService.clearAutoUpdateInterval(timerObj);
  737. }
  738. // reduce parent's descendantCount
  739. // see: https://dev.growi.org/62149d019311629d4ecd91cf#Handling%20of%20descendantCount%20in%20case%20of%20unexpected%20process%20interruption
  740. const nToReduceForOperationInterruption = -1;
  741. await Page.incrementDescendantCountOfPageIds(
  742. [renamedPage.parent],
  743. nToReduceForOperationInterruption,
  744. );
  745. const nToReduce = -1 * ((page.isEmpty ? 0 : 1) + page.descendantCount);
  746. await this.updateDescendantCountOfAncestors(exParentId, nToReduce, true);
  747. // increase ancestore's descendantCount
  748. const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
  749. await this.updateDescendantCountOfAncestors(
  750. renamedPage._id,
  751. nToIncrease,
  752. false,
  753. );
  754. // Remove leaf empty pages if not moving to under the ex-target position
  755. if (!this.isRenamingToUnderTarget(page.path, newPagePathSanitized)) {
  756. // remove empty pages at leaf position
  757. await Page.removeLeafEmptyPagesRecursively(page.parent);
  758. }
  759. await PageOperation.findByIdAndDelete(pageOpId);
  760. }
  761. async resumeRenameSubOperation(
  762. renamedPage: PageDocument,
  763. pageOp: PageOperationDocument,
  764. activity?,
  765. ): Promise<void> {
  766. const isProcessable = pageOp.isProcessable();
  767. if (!isProcessable) {
  768. throw Error('This page operation is currently being processed');
  769. }
  770. if (pageOp.toPath == null) {
  771. throw Error(
  772. `Property toPath is missing which is needed to resume rename operation(${pageOp._id})`,
  773. );
  774. }
  775. const { page, fromPath, toPath, options, user } = pageOp;
  776. this.fixPathsAndDescendantCountOfAncestors(
  777. page,
  778. user,
  779. options,
  780. renamedPage,
  781. pageOp._id,
  782. fromPath,
  783. toPath,
  784. activity,
  785. );
  786. }
  787. /**
  788. * Renaming paths and fixing descendantCount of ancestors. It shoud be run synchronously.
  789. * `renameSubOperation` to restart rename operation
  790. * `updateDescendantCountOfPagesWithPaths` to fix descendantCount of ancestors
  791. */
  792. private async fixPathsAndDescendantCountOfAncestors(
  793. page,
  794. user,
  795. options,
  796. renamedPage,
  797. pageOpId,
  798. fromPath,
  799. toPath,
  800. activity?,
  801. ): Promise<void> {
  802. await this.renameSubOperation(
  803. page,
  804. toPath,
  805. user,
  806. options,
  807. renamedPage,
  808. pageOpId,
  809. activity,
  810. );
  811. const ancestorsPaths =
  812. this.crowi.pageOperationService.getAncestorsPathsByFromAndToPath(
  813. fromPath,
  814. toPath,
  815. );
  816. await this.updateDescendantCountOfPagesWithPaths(ancestorsPaths);
  817. }
  818. private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
  819. const pathToTest = escapeStringRegexp(addTrailingSlash(fromPath));
  820. const pathToBeTested = toPath;
  821. return new RegExp(`^${pathToTest}`, 'i').test(pathToBeTested);
  822. }
  823. private async getParentAndforceCreateEmptyTree(originalPage, toPath: string) {
  824. const Page = mongoose.model('Page') as unknown as PageModel;
  825. const fromPath = originalPage.path;
  826. const newParentPath = pathlib.dirname(toPath);
  827. // local util
  828. const collectAncestorPathsUntilFromPath = (
  829. path: string,
  830. paths: string[] = [],
  831. ): string[] => {
  832. if (path === fromPath) return paths;
  833. const parentPath = pathlib.dirname(path);
  834. paths.push(parentPath);
  835. return collectAncestorPathsUntilFromPath(parentPath, paths);
  836. };
  837. const pathsToInsert = collectAncestorPathsUntilFromPath(toPath);
  838. const originalParent = await Page.findById(originalPage.parent);
  839. if (originalParent == null) {
  840. throw Error('Original parent not found');
  841. }
  842. const insertedPages = await Page.insertMany(
  843. pathsToInsert.map((path) => {
  844. return {
  845. path,
  846. isEmpty: true,
  847. };
  848. }),
  849. );
  850. const pages = [...insertedPages, originalParent];
  851. const ancestorsMap = new Map<string, PageDocument & { _id: any }>(
  852. pages.map((p) => [p.path, p]),
  853. );
  854. // bulkWrite to update ancestors
  855. const operations = insertedPages.map((page) => {
  856. const parentPath = pathlib.dirname(page.path);
  857. const op = {
  858. updateOne: {
  859. filter: {
  860. _id: page._id,
  861. },
  862. update: {
  863. $set: {
  864. parent: ancestorsMap.get(parentPath)?._id,
  865. descedantCount: originalParent.descendantCount,
  866. },
  867. },
  868. },
  869. };
  870. return op;
  871. });
  872. await Page.bulkWrite(operations);
  873. const newParent = ancestorsMap.get(newParentPath);
  874. return newParent;
  875. }
  876. private async renamePageV4(page, newPagePath, user, options) {
  877. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  878. 'Page',
  879. );
  880. const {
  881. isRecursively = false,
  882. createRedirectPage = false,
  883. updateMetadata = false,
  884. } = options;
  885. // sanitize path
  886. const newPagePathSanitized = generalXssFilter.process(newPagePath);
  887. // create descendants first
  888. if (isRecursively) {
  889. await this.renameDescendantsWithStream(
  890. page,
  891. newPagePathSanitized,
  892. user,
  893. options,
  894. );
  895. }
  896. const update: any = {};
  897. // update Page
  898. update.path = newPagePathSanitized;
  899. if (updateMetadata) {
  900. update.lastUpdateUser = user;
  901. update.updatedAt = Date.now();
  902. }
  903. const renamedPage = await Page.findByIdAndUpdate(
  904. page._id,
  905. { $set: update },
  906. { new: true },
  907. );
  908. // update Rivisions
  909. if (renamedPage == null) {
  910. throw new Error('Failed to rename page');
  911. }
  912. await Revision.updateRevisionListByPageId(renamedPage._id, {
  913. pageId: renamedPage._id,
  914. });
  915. if (createRedirectPage) {
  916. await PageRedirect.create({
  917. fromPath: page.path,
  918. toPath: newPagePathSanitized,
  919. });
  920. }
  921. this.pageEvent.emit('rename');
  922. return renamedPage;
  923. }
  924. private async renameDescendants(
  925. pages,
  926. user,
  927. options,
  928. oldPagePathPrefix,
  929. newPagePathPrefix,
  930. shouldUseV4Process = true,
  931. ) {
  932. // v4 compatible process
  933. if (shouldUseV4Process) {
  934. return this.renameDescendantsV4(
  935. pages,
  936. user,
  937. options,
  938. oldPagePathPrefix,
  939. newPagePathPrefix,
  940. );
  941. }
  942. const Page = mongoose.model('Page') as unknown as PageModel;
  943. const { updateMetadata, createRedirectPage } = options;
  944. const updatePathOperations: any[] = [];
  945. const insertPageRedirectOperations: any[] = [];
  946. pages.forEach((page) => {
  947. const newPagePath = page.path.replace(
  948. oldPagePathPrefix,
  949. newPagePathPrefix,
  950. );
  951. // increment updatePathOperations
  952. const update =
  953. !page.isEmpty && updateMetadata
  954. ? {
  955. $set: {
  956. path: newPagePath,
  957. lastUpdateUser: user._id,
  958. updatedAt: new Date(),
  959. },
  960. }
  961. : {
  962. $set: { path: newPagePath },
  963. };
  964. if (!page.isEmpty && createRedirectPage) {
  965. // insert PageRedirect
  966. insertPageRedirectOperations.push({
  967. insertOne: {
  968. document: {
  969. fromPath: page.path,
  970. toPath: newPagePath,
  971. },
  972. },
  973. });
  974. }
  975. updatePathOperations.push({
  976. updateOne: {
  977. filter: {
  978. _id: page._id,
  979. },
  980. update,
  981. },
  982. });
  983. });
  984. try {
  985. await Page.bulkWrite(updatePathOperations);
  986. } catch (err) {
  987. if (err.code !== 11000) {
  988. throw new Error(`Failed to rename pages: ${err}`);
  989. }
  990. }
  991. try {
  992. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  993. } catch (err) {
  994. if (err.code !== 11000) {
  995. throw Error(`Failed to create PageRedirect documents: ${err}`);
  996. }
  997. }
  998. this.pageEvent.emit('updateMany', pages, user);
  999. }
  1000. private async renameDescendantsV4(
  1001. pages,
  1002. user,
  1003. options,
  1004. oldPagePathPrefix,
  1005. newPagePathPrefix,
  1006. ) {
  1007. const pageCollection = mongoose.connection.collection('pages');
  1008. const { updateMetadata, createRedirectPage } = options;
  1009. const unorderedBulkOp = pageCollection.initializeUnorderedBulkOp();
  1010. const insertPageRedirectOperations: any[] = [];
  1011. pages.forEach((page) => {
  1012. const newPagePath = page.path.replace(
  1013. oldPagePathPrefix,
  1014. newPagePathPrefix,
  1015. );
  1016. if (updateMetadata) {
  1017. unorderedBulkOp.find({ _id: page._id }).update({
  1018. $set: {
  1019. path: newPagePath,
  1020. lastUpdateUser: user._id,
  1021. updatedAt: new Date(),
  1022. },
  1023. });
  1024. } else {
  1025. unorderedBulkOp
  1026. .find({ _id: page._id })
  1027. .update({ $set: { path: newPagePath } });
  1028. }
  1029. // insert PageRedirect
  1030. if (!page.isEmpty && createRedirectPage) {
  1031. insertPageRedirectOperations.push({
  1032. insertOne: {
  1033. document: {
  1034. fromPath: page.path,
  1035. toPath: newPagePath,
  1036. },
  1037. },
  1038. });
  1039. }
  1040. });
  1041. try {
  1042. await unorderedBulkOp.execute();
  1043. } catch (err) {
  1044. if (err.code !== 11000) {
  1045. throw new Error(`Failed to rename pages: ${err}`);
  1046. }
  1047. }
  1048. try {
  1049. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  1050. } catch (err) {
  1051. if (err.code !== 11000) {
  1052. throw Error(`Failed to create PageRedirect documents: ${err}`);
  1053. }
  1054. }
  1055. this.pageEvent.emit('updateMany', pages, user);
  1056. }
  1057. private async renameDescendantsWithStream(
  1058. targetPage,
  1059. newPagePathSanitized,
  1060. user,
  1061. options = {},
  1062. shouldUseV4Process = true,
  1063. descendantsSubscribedSets?,
  1064. ) {
  1065. // v4 compatible process
  1066. if (shouldUseV4Process) {
  1067. return this.renameDescendantsWithStreamV4(
  1068. targetPage,
  1069. newPagePathSanitized,
  1070. user,
  1071. options,
  1072. );
  1073. }
  1074. const factory = new PageCursorsForDescendantsFactory(
  1075. user,
  1076. targetPage,
  1077. true,
  1078. );
  1079. const readStream = await factory.generateReadable();
  1080. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  1081. const newPagePathPrefix = newPagePathSanitized;
  1082. const pathRegExp = new RegExp(
  1083. `^${escapeStringRegexp(targetPage.path)}`,
  1084. 'i',
  1085. );
  1086. const renameDescendants = this.renameDescendants.bind(this);
  1087. const pageEvent = this.pageEvent;
  1088. let count = 0;
  1089. const writeStream = new Writable({
  1090. objectMode: true,
  1091. async write(batch, encoding, callback) {
  1092. try {
  1093. count += batch.length;
  1094. await renameDescendants(
  1095. batch,
  1096. user,
  1097. options,
  1098. pathRegExp,
  1099. newPagePathPrefix,
  1100. shouldUseV4Process,
  1101. );
  1102. const subscribedUsers = await Subscription.getSubscriptions(batch);
  1103. subscribedUsers.forEach((eachUser) => {
  1104. descendantsSubscribedSets.add(eachUser);
  1105. });
  1106. logger.debug(`Renaming pages progressing: (count=${count})`);
  1107. } catch (err) {
  1108. logger.error('Renaming error on add anyway: ', err);
  1109. }
  1110. callback();
  1111. },
  1112. async final(callback) {
  1113. logger.debug(`Renaming pages has completed: (totalCount=${count})`);
  1114. // update path
  1115. targetPage.path = newPagePathSanitized;
  1116. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  1117. callback();
  1118. },
  1119. });
  1120. await pipeline(readStream, batchStream, writeStream);
  1121. }
  1122. private async renameDescendantsWithStreamV4(
  1123. targetPage,
  1124. newPagePathSanitized,
  1125. user,
  1126. options = {},
  1127. ) {
  1128. const readStream = await this.generateReadStreamToOperateOnlyDescendants(
  1129. targetPage.path,
  1130. user,
  1131. );
  1132. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  1133. const newPagePathPrefix = newPagePathSanitized;
  1134. const pathRegExp = new RegExp(
  1135. `^${escapeStringRegexp(targetPage.path)}`,
  1136. 'i',
  1137. );
  1138. const renameDescendants = this.renameDescendants.bind(this);
  1139. const pageEvent = this.pageEvent;
  1140. let count = 0;
  1141. const writeStream = new Writable({
  1142. objectMode: true,
  1143. async write(batch, encoding, callback) {
  1144. try {
  1145. count += batch.length;
  1146. await renameDescendants(
  1147. batch,
  1148. user,
  1149. options,
  1150. pathRegExp,
  1151. newPagePathPrefix,
  1152. );
  1153. logger.debug(`Renaming pages progressing: (count=${count})`);
  1154. } catch (err) {
  1155. logger.error('renameDescendants error on add anyway: ', err);
  1156. }
  1157. callback();
  1158. },
  1159. final(callback) {
  1160. logger.debug(`Renaming pages has completed: (totalCount=${count})`);
  1161. // update path
  1162. targetPage.path = newPagePathSanitized;
  1163. pageEvent.emit('syncDescendantsUpdate', targetPage, user);
  1164. callback();
  1165. },
  1166. });
  1167. await pipeline(readStream, batchStream, writeStream);
  1168. }
  1169. /*
  1170. * Duplicate
  1171. */
  1172. async duplicate(
  1173. page: PageDocument,
  1174. newPagePath: string,
  1175. user,
  1176. isRecursively: boolean,
  1177. onlyDuplicateUserRelatedResources: boolean,
  1178. ) {
  1179. /*
  1180. * Common Operation
  1181. */
  1182. const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
  1183. if (page == null || isEmptyAndNotRecursively) {
  1184. throw new Error('Cannot find or duplicate the empty page');
  1185. }
  1186. const Page = mongoose.model('Page') as unknown as PageModel;
  1187. if (!isRecursively && page.isEmpty) {
  1188. throw Error('Page not found.');
  1189. }
  1190. const newPagePathSanitized = generalXssFilter.process(newPagePath);
  1191. // 1. Separate v4 & v5 process
  1192. const isShouldUseV4Process = shouldUseV4Process(page);
  1193. if (isShouldUseV4Process) {
  1194. return this.duplicateV4(
  1195. page,
  1196. newPagePathSanitized,
  1197. user,
  1198. isRecursively,
  1199. onlyDuplicateUserRelatedResources,
  1200. );
  1201. }
  1202. const canOperate = await this.crowi.pageOperationService.canOperate(
  1203. isRecursively,
  1204. page.path,
  1205. newPagePathSanitized,
  1206. );
  1207. if (!canOperate) {
  1208. throw Error(
  1209. `Cannot operate duplicate to path "${newPagePathSanitized}" right now.`,
  1210. );
  1211. }
  1212. // 2. UserGroup & Owner validation
  1213. // use the parent's grant when target page is an empty page
  1214. let grant: PageGrant;
  1215. let grantedUserIds: ObjectIdLike[] | undefined;
  1216. let grantedGroupIds: IGrantedGroup[];
  1217. if (page.isEmpty) {
  1218. const parent = await Page.findOne({ _id: page.parent });
  1219. if (parent == null) {
  1220. throw Error('parent not found');
  1221. }
  1222. grant = parent.grant;
  1223. grantedUserIds = parent.grantedUsers.map((u) => getIdForRef(u));
  1224. grantedGroupIds = onlyDuplicateUserRelatedResources
  1225. ? await this.pageGrantService.getUserRelatedGrantedGroups(parent, user)
  1226. : parent.grantedGroups;
  1227. } else {
  1228. grant = page.grant;
  1229. grantedUserIds = page.grantedUsers.map((u) => getIdForRef(u));
  1230. grantedGroupIds = onlyDuplicateUserRelatedResources
  1231. ? await this.pageGrantService.getUserRelatedGrantedGroups(page, user)
  1232. : page.grantedGroups;
  1233. }
  1234. if (grant !== Page.GRANT_RESTRICTED) {
  1235. let isGrantNormalized = false;
  1236. try {
  1237. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  1238. user,
  1239. newPagePathSanitized,
  1240. grant,
  1241. grantedUserIds,
  1242. grantedGroupIds,
  1243. false,
  1244. );
  1245. } catch (err) {
  1246. logger.error(
  1247. `Failed to validate grant of page at "${newPagePathSanitized}" when duplicating`,
  1248. err,
  1249. );
  1250. throw err;
  1251. }
  1252. if (!isGrantNormalized) {
  1253. throw Error(
  1254. `This page cannot be duplicated to "${newPagePathSanitized}" since the selected grant or grantedGroup is not assignable to this page.`,
  1255. );
  1256. }
  1257. }
  1258. // copy & populate (reason why copy: SubOperation only allows non-populated page document)
  1259. const copyPage = { ...page };
  1260. // 3. Duplicate target
  1261. const options: IOptionsForCreate = {
  1262. grant,
  1263. grantUserGroupIds: grantedGroupIds,
  1264. };
  1265. let duplicatedTarget: HydratedDocument<PageDocument>;
  1266. if (page.isEmpty) {
  1267. const parent = await this.getParentAndFillAncestorsByUser(
  1268. user,
  1269. newPagePath,
  1270. );
  1271. duplicatedTarget = await Page.createEmptyPage(newPagePath, parent);
  1272. } else {
  1273. const populatedPage = await page.populate<{
  1274. revision: IRevisionHasId | null;
  1275. }>({ path: 'revision', model: 'Revision', select: 'body' });
  1276. duplicatedTarget = await (this.create as CreateMethod)(
  1277. newPagePath,
  1278. populatedPage?.revision?.body ?? '',
  1279. user,
  1280. options,
  1281. );
  1282. if (isAiEnabled()) {
  1283. const { getOpenaiService } = await import(
  1284. '~/features/openai/server/services/openai'
  1285. );
  1286. const openaiService = getOpenaiService();
  1287. // Do not await because communication with OpenAI takes time
  1288. openaiService?.createVectorStoreFileOnPageCreate([duplicatedTarget]);
  1289. }
  1290. }
  1291. this.pageEvent.emit('duplicate', page, user);
  1292. // 4. Take over tags
  1293. const originTags = await page.findRelatedTagsById();
  1294. let savedTags: PageTagRelationDocument[] = [];
  1295. if (originTags.length !== 0) {
  1296. await PageTagRelation.updatePageTags(duplicatedTarget._id, originTags);
  1297. savedTags = await PageTagRelation.listTagNamesByPage(
  1298. duplicatedTarget._id,
  1299. );
  1300. this.tagEvent.emit('update', duplicatedTarget, savedTags);
  1301. }
  1302. if (isRecursively) {
  1303. /*
  1304. * Resumable Operation
  1305. */
  1306. let pageOp: PageOperationDocument;
  1307. try {
  1308. pageOp = await PageOperation.create({
  1309. actionType: PageActionType.Duplicate,
  1310. actionStage: PageActionStage.Main,
  1311. page: copyPage,
  1312. user,
  1313. fromPath: page.path,
  1314. toPath: newPagePath,
  1315. });
  1316. } catch (err) {
  1317. logger.error('Failed to create PageOperation document.', err);
  1318. throw err;
  1319. }
  1320. (async () => {
  1321. try {
  1322. await this.duplicateRecursivelyMainOperation(
  1323. page,
  1324. newPagePath,
  1325. user,
  1326. pageOp._id,
  1327. onlyDuplicateUserRelatedResources,
  1328. );
  1329. } catch (err) {
  1330. logger.error(
  1331. 'Error occurred while running duplicateRecursivelyMainOperation.',
  1332. err,
  1333. );
  1334. // cleanup
  1335. await PageOperation.deleteOne({ _id: pageOp._id });
  1336. throw err;
  1337. }
  1338. })();
  1339. }
  1340. const result = serializePageSecurely(duplicatedTarget);
  1341. result.tags = savedTags;
  1342. return result;
  1343. }
  1344. async duplicateRecursivelyMainOperation(
  1345. page: PageDocument,
  1346. newPagePath: string,
  1347. user,
  1348. pageOpId: ObjectIdLike,
  1349. onlyDuplicateUserRelatedResources: boolean,
  1350. ): Promise<void> {
  1351. const nDuplicatedPages = await this.duplicateDescendantsWithStream(
  1352. page,
  1353. newPagePath,
  1354. user,
  1355. onlyDuplicateUserRelatedResources,
  1356. false,
  1357. );
  1358. // normalize parent of descendant pages
  1359. const shouldNormalize = this.shouldNormalizeParent(page);
  1360. if (shouldNormalize) {
  1361. try {
  1362. await this.normalizeParentAndDescendantCountOfDescendants(
  1363. newPagePath,
  1364. user,
  1365. true,
  1366. );
  1367. logger.info(
  1368. `Successfully normalized duplicated descendant pages under "${newPagePath}"`,
  1369. );
  1370. } catch (err) {
  1371. logger.error('Failed to normalize descendants afrer duplicate:', err);
  1372. throw err;
  1373. }
  1374. }
  1375. // Set to Sub
  1376. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(
  1377. pageOpId,
  1378. PageActionStage.Sub,
  1379. );
  1380. if (pageOp == null) {
  1381. throw Error('PageOperation document not found');
  1382. }
  1383. /*
  1384. * Sub Operation
  1385. */
  1386. await this.duplicateRecursivelySubOperation(
  1387. newPagePath,
  1388. nDuplicatedPages,
  1389. pageOp._id,
  1390. );
  1391. }
  1392. async duplicateRecursivelySubOperation(
  1393. newPagePath: string,
  1394. nDuplicatedPages: number,
  1395. pageOpId: ObjectIdLike,
  1396. ): Promise<void> {
  1397. const Page = mongoose.model('Page');
  1398. const newTarget = await Page.findOne({ path: newPagePath }); // only one page will be found since duplicating to existing path is forbidden
  1399. if (newTarget == null) {
  1400. throw Error(
  1401. 'No duplicated page found. Something might have gone wrong in duplicateRecursivelyMainOperation.',
  1402. );
  1403. }
  1404. await this.updateDescendantCountOfAncestors(
  1405. newTarget._id,
  1406. nDuplicatedPages,
  1407. false,
  1408. );
  1409. await PageOperation.findByIdAndDelete(pageOpId);
  1410. }
  1411. async duplicateV4(
  1412. page,
  1413. newPagePath,
  1414. user,
  1415. isRecursively,
  1416. onlyDuplicateUserRelatedResources: boolean,
  1417. ) {
  1418. // populate
  1419. await page.populate({
  1420. path: 'revision',
  1421. model: 'Revision',
  1422. select: 'body',
  1423. });
  1424. // create option
  1425. const options: any = { page };
  1426. options.grant = page.grant;
  1427. options.grantUserGroupIds = page.grantedGroups;
  1428. options.grantedUserIds = page.grantedUsers;
  1429. const newPagePathSanitized = generalXssFilter.process(newPagePath);
  1430. const createdPage = await this.create(
  1431. newPagePathSanitized,
  1432. page.revision.body,
  1433. user,
  1434. options,
  1435. );
  1436. this.pageEvent.emit('duplicate', page, user);
  1437. if (isRecursively) {
  1438. this.duplicateDescendantsWithStream(
  1439. page,
  1440. newPagePathSanitized,
  1441. user,
  1442. onlyDuplicateUserRelatedResources,
  1443. );
  1444. }
  1445. // take over tags
  1446. const originTags = await page.findRelatedTagsById();
  1447. let savedTags: PageTagRelationDocument[] = [];
  1448. if (originTags != null) {
  1449. await PageTagRelation.updatePageTags(createdPage.id, originTags);
  1450. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  1451. this.tagEvent.emit('update', createdPage, savedTags);
  1452. }
  1453. const result = serializePageSecurely(createdPage);
  1454. result.tags = savedTags;
  1455. return result;
  1456. }
  1457. /**
  1458. * Receive the object with oldPageId and newPageId and duplicate the tags from oldPage to newPage
  1459. * @param {Object} pageIdMapping e.g. key: oldPageId, value: newPageId
  1460. */
  1461. private async duplicateTags(pageIdMapping) {
  1462. // convert pageId from string to ObjectId
  1463. const pageIds = Object.keys(pageIdMapping);
  1464. const stage = {
  1465. $or: pageIds.map((pageId) => {
  1466. return { relatedPage: new mongoose.Types.ObjectId(pageId) };
  1467. }),
  1468. };
  1469. const pagesAssociatedWithTag = await PageTagRelation.aggregate([
  1470. {
  1471. $match: stage,
  1472. },
  1473. {
  1474. $group: {
  1475. _id: '$relatedTag',
  1476. relatedPages: { $push: '$relatedPage' },
  1477. },
  1478. },
  1479. ]);
  1480. const newPageTagRelation: any[] = [];
  1481. pagesAssociatedWithTag.forEach(({ _id, relatedPages }) => {
  1482. // relatedPages
  1483. relatedPages.forEach((pageId) => {
  1484. newPageTagRelation.push({
  1485. relatedPage: pageIdMapping[pageId], // newPageId
  1486. relatedTag: _id,
  1487. });
  1488. });
  1489. });
  1490. return PageTagRelation.insertMany(newPageTagRelation, { ordered: false });
  1491. }
  1492. private async duplicateDescendants(
  1493. pages,
  1494. user,
  1495. oldPagePathPrefix,
  1496. newPagePathPrefix,
  1497. onlyDuplicateUserRelatedResources: boolean,
  1498. shouldUseV4Process = true,
  1499. ) {
  1500. if (shouldUseV4Process) {
  1501. return this.duplicateDescendantsV4(
  1502. pages,
  1503. user,
  1504. oldPagePathPrefix,
  1505. newPagePathPrefix,
  1506. );
  1507. }
  1508. const Page = mongoose.model<PageDocument, PageModel>('Page');
  1509. const pageIds = pages.map((page) => page._id);
  1510. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  1511. // Mapping to set to the body of the new revision
  1512. const pageIdRevisionMapping: Record<string, IRevisionDocument> = {};
  1513. revisions.forEach((revision) => {
  1514. pageIdRevisionMapping[getIdStringForRef(revision.pageId)] = revision;
  1515. });
  1516. // key: oldPageId, value: newPageId
  1517. const pageIdMapping = {};
  1518. const newPages: any[] = [];
  1519. const newRevisions: any[] = [];
  1520. const userRelatedGroups =
  1521. await this.pageGrantService.getUserRelatedGroups(user);
  1522. // no need to save parent here
  1523. pages.forEach((page) => {
  1524. const newPageId = new mongoose.Types.ObjectId();
  1525. const newPagePath = page.path.replace(
  1526. oldPagePathPrefix,
  1527. newPagePathPrefix,
  1528. );
  1529. const revisionId = new mongoose.Types.ObjectId();
  1530. pageIdMapping[page._id] = newPageId;
  1531. const isDuplicateTarget =
  1532. !page.isEmpty &&
  1533. (!onlyDuplicateUserRelatedResources ||
  1534. this.pageGrantService.isUserGrantedPageAccess(
  1535. page,
  1536. user,
  1537. userRelatedGroups,
  1538. ));
  1539. if (isDuplicateTarget) {
  1540. const grantedGroups = onlyDuplicateUserRelatedResources
  1541. ? this.pageGrantService.getUserRelatedGrantedGroupsSyncronously(
  1542. userRelatedGroups,
  1543. page,
  1544. )
  1545. : page.grantedGroups;
  1546. const newPage = {
  1547. _id: newPageId,
  1548. path: newPagePath,
  1549. creator: user._id,
  1550. grant: page.grant,
  1551. grantedGroups,
  1552. grantedUsers: page.grantedUsers,
  1553. lastUpdateUser: user._id,
  1554. revision: revisionId,
  1555. };
  1556. newRevisions.push({
  1557. _id: revisionId,
  1558. pageId: newPageId,
  1559. body: pageIdRevisionMapping[page._id.toString()].body,
  1560. author: user._id,
  1561. format: 'markdown',
  1562. });
  1563. newPages.push(newPage);
  1564. }
  1565. });
  1566. const duplicatedPages = await Page.insertMany(newPages, { ordered: false });
  1567. const duplicatedPageIds = duplicatedPages.map(
  1568. (duplicatedPage) => duplicatedPage._id,
  1569. );
  1570. await Revision.insertMany(newRevisions, { ordered: false });
  1571. await this.duplicateTags(pageIdMapping);
  1572. const duplicatedPagesWithPopulatedToShowRevision: HydratedDocument<PageDocument>[] =
  1573. await Page.find({
  1574. _id: { $in: duplicatedPageIds },
  1575. grant: PageGrant.GRANT_PUBLIC,
  1576. }).populate('revision');
  1577. if (isAiEnabled()) {
  1578. const { getOpenaiService } = await import(
  1579. '~/features/openai/server/services/openai'
  1580. );
  1581. const openaiService = getOpenaiService();
  1582. // Do not await because communication with OpenAI takes time
  1583. openaiService?.createVectorStoreFileOnPageCreate(
  1584. duplicatedPagesWithPopulatedToShowRevision,
  1585. );
  1586. }
  1587. }
  1588. private async duplicateDescendantsV4(
  1589. pages,
  1590. user,
  1591. oldPagePathPrefix,
  1592. newPagePathPrefix,
  1593. ) {
  1594. const Page = mongoose.model('Page') as unknown as PageModel;
  1595. const pageIds = pages.map((page) => page._id);
  1596. const revisions = await Revision.find({ pageId: { $in: pageIds } });
  1597. // Mapping to set to the body of the new revision
  1598. const pageIdRevisionMapping: Record<string, IRevisionDocument> = {};
  1599. revisions.forEach((revision) => {
  1600. pageIdRevisionMapping[getIdStringForRef(revision.pageId)] = revision;
  1601. });
  1602. // key: oldPageId, value: newPageId
  1603. const pageIdMapping = {};
  1604. const newPages: any[] = [];
  1605. const newRevisions: any[] = [];
  1606. pages.forEach((page) => {
  1607. const newPageId = new mongoose.Types.ObjectId();
  1608. const newPagePath = page.path.replace(
  1609. oldPagePathPrefix,
  1610. newPagePathPrefix,
  1611. );
  1612. const revisionId = new mongoose.Types.ObjectId();
  1613. pageIdMapping[page._id] = newPageId;
  1614. newPages.push({
  1615. _id: newPageId,
  1616. path: newPagePath,
  1617. creator: user._id,
  1618. grant: page.grant,
  1619. grantedGroups: page.grantedGroups,
  1620. grantedUsers: page.grantedUsers,
  1621. lastUpdateUser: user._id,
  1622. revision: revisionId,
  1623. });
  1624. newRevisions.push({
  1625. _id: revisionId,
  1626. pageId: newPageId,
  1627. body: pageIdRevisionMapping[page._id.toString()].body,
  1628. author: user._id,
  1629. format: 'markdown',
  1630. });
  1631. });
  1632. await Page.insertMany(newPages, { ordered: false });
  1633. await Revision.insertMany(newRevisions, { ordered: false });
  1634. await this.duplicateTags(pageIdMapping);
  1635. }
  1636. private async duplicateDescendantsWithStream(
  1637. page,
  1638. newPagePathSanitized,
  1639. user,
  1640. onlyDuplicateUserRelatedResources: boolean,
  1641. shouldUseV4Process = true,
  1642. ) {
  1643. if (shouldUseV4Process) {
  1644. return this.duplicateDescendantsWithStreamV4(
  1645. page,
  1646. newPagePathSanitized,
  1647. user,
  1648. onlyDuplicateUserRelatedResources,
  1649. );
  1650. }
  1651. const iterableFactory = new PageCursorsForDescendantsFactory(
  1652. user,
  1653. page,
  1654. true,
  1655. );
  1656. const readStream = await iterableFactory.generateReadable();
  1657. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  1658. const newPagePathPrefix = newPagePathSanitized;
  1659. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  1660. const duplicateDescendants = this.duplicateDescendants.bind(this);
  1661. const pageEvent = this.pageEvent;
  1662. let count = 0;
  1663. let nNonEmptyDuplicatedPages = 0;
  1664. const writeStream = new Writable({
  1665. objectMode: true,
  1666. async write(batch, encoding, callback) {
  1667. try {
  1668. count += batch.length;
  1669. nNonEmptyDuplicatedPages += batch.filter(
  1670. (page) => !page.isEmpty,
  1671. ).length;
  1672. await duplicateDescendants(
  1673. batch,
  1674. user,
  1675. pathRegExp,
  1676. newPagePathPrefix,
  1677. onlyDuplicateUserRelatedResources,
  1678. shouldUseV4Process,
  1679. );
  1680. logger.debug(`Adding pages progressing: (count=${count})`);
  1681. } catch (err) {
  1682. logger.error('addAllPages error on add anyway: ', err);
  1683. }
  1684. callback();
  1685. },
  1686. async final(callback) {
  1687. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1688. // update path
  1689. page.path = newPagePathSanitized;
  1690. pageEvent.emit('syncDescendantsUpdate', page, user);
  1691. callback();
  1692. },
  1693. });
  1694. await pipeline(readStream, batchStream, writeStream);
  1695. return nNonEmptyDuplicatedPages;
  1696. }
  1697. private async duplicateDescendantsWithStreamV4(
  1698. page,
  1699. newPagePathSanitized,
  1700. user,
  1701. onlyDuplicateUserRelatedResources: boolean,
  1702. ) {
  1703. const readStream = await this.generateReadStreamToOperateOnlyDescendants(
  1704. page.path,
  1705. user,
  1706. );
  1707. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  1708. const newPagePathPrefix = newPagePathSanitized;
  1709. const pathRegExp = new RegExp(`^${escapeStringRegexp(page.path)}`, 'i');
  1710. const duplicateDescendants = this.duplicateDescendants.bind(this);
  1711. const pageEvent = this.pageEvent;
  1712. let count = 0;
  1713. const writeStream = new Writable({
  1714. objectMode: true,
  1715. async write(batch, _encoding, callback) {
  1716. try {
  1717. count += batch.length;
  1718. await duplicateDescendants(
  1719. batch,
  1720. user,
  1721. pathRegExp,
  1722. newPagePathPrefix,
  1723. onlyDuplicateUserRelatedResources,
  1724. );
  1725. logger.debug(`Adding pages progressing: (count=${count})`);
  1726. } catch (err) {
  1727. logger.error('addAllPages error on add anyway: ', err);
  1728. }
  1729. callback();
  1730. },
  1731. final(callback) {
  1732. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  1733. // update path
  1734. page.path = newPagePathSanitized;
  1735. pageEvent.emit('syncDescendantsUpdate', page, user);
  1736. callback();
  1737. },
  1738. });
  1739. await pipeline(readStream, batchStream, writeStream);
  1740. return count;
  1741. }
  1742. /*
  1743. * Delete
  1744. */
  1745. async deletePage(
  1746. page,
  1747. user,
  1748. options = {},
  1749. isRecursively = false,
  1750. activityParameters,
  1751. ) {
  1752. /*
  1753. * Common Operation
  1754. */
  1755. const Page = mongoose.model('Page') as PageModel;
  1756. // Separate v4 & v5 process
  1757. const isShouldUseV4Process = shouldUseV4Process(page);
  1758. if (isShouldUseV4Process) {
  1759. return this.deletePageV4(page, user, options, isRecursively);
  1760. }
  1761. // Validate
  1762. if (page.isEmpty && !isRecursively) {
  1763. throw Error('Page not found.');
  1764. }
  1765. const isTrashed = isTrashPage(page.path);
  1766. if (isTrashed) {
  1767. throw new Error('This method does NOT support deleting trashed pages.');
  1768. }
  1769. if (isTopPage(page.path) || isUsersTopPage(page.path)) {
  1770. throw new Error('Page is not deletable.');
  1771. }
  1772. if (pagePathUtils.isUsersHomepage(page.path)) {
  1773. if (!this.canDeleteUserHomepageByConfig()) {
  1774. throw new Error('User Homepage is not deletable.');
  1775. }
  1776. if (!(await this.isUsersHomepageOwnerAbsent(page.path))) {
  1777. throw new Error('User Homepage is not deletable.');
  1778. }
  1779. }
  1780. const newPath = Page.getDeletedPageName(page.path);
  1781. const canOperate = await this.crowi.pageOperationService.canOperate(
  1782. isRecursively,
  1783. page.path,
  1784. newPath,
  1785. );
  1786. if (!canOperate) {
  1787. throw Error(`Cannot operate delete to path "${newPath}" right now.`);
  1788. }
  1789. // Replace with an empty page
  1790. const isChildrenExist = await Page.exists({ parent: page._id });
  1791. const shouldReplace = !isRecursively && isChildrenExist;
  1792. if (shouldReplace) {
  1793. await Page.replaceTargetWithPage(page, null, true);
  1794. }
  1795. const parameters = {
  1796. ip: activityParameters.ip,
  1797. endpoint: activityParameters.endpoint,
  1798. action:
  1799. page.descendantCount > 0
  1800. ? SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE
  1801. : SupportedAction.ACTION_PAGE_DELETE,
  1802. user,
  1803. target: page,
  1804. targetModel: 'Page',
  1805. snapshot: {
  1806. username: user.username,
  1807. },
  1808. };
  1809. const activity =
  1810. await this.crowi.activityService.createActivity(parameters);
  1811. // Delete target (only updating an existing document's properties )
  1812. let deletedPage: PageDocument | null;
  1813. if (!page.isEmpty) {
  1814. deletedPage = await this.deleteNonEmptyTarget(page, user);
  1815. } else {
  1816. // always recursive
  1817. deletedPage = page;
  1818. await Page.deleteOne({ _id: page._id, isEmpty: true });
  1819. }
  1820. // 1. Update descendantCount
  1821. if (isRecursively) {
  1822. const inc = page.isEmpty
  1823. ? -page.descendantCount
  1824. : -(page.descendantCount + 1);
  1825. await this.updateDescendantCountOfAncestors(page.parent, inc, true);
  1826. } else {
  1827. // update descendantCount of ancestors'
  1828. await this.updateDescendantCountOfAncestors(page.parent, -1, true);
  1829. }
  1830. // 2. Delete leaf empty pages
  1831. await Page.removeLeafEmptyPagesRecursively(page.parent);
  1832. if (isRecursively) {
  1833. let pageOp: PageOperationDocument;
  1834. try {
  1835. pageOp = await PageOperation.create({
  1836. actionType: PageActionType.Delete,
  1837. actionStage: PageActionStage.Main,
  1838. page,
  1839. user,
  1840. fromPath: page.path,
  1841. toPath: newPath,
  1842. });
  1843. } catch (err) {
  1844. logger.error('Failed to create PageOperation document.', err);
  1845. throw err;
  1846. }
  1847. /*
  1848. * Resumable Operation
  1849. */
  1850. (async () => {
  1851. try {
  1852. await this.deleteRecursivelyMainOperation(
  1853. page,
  1854. user,
  1855. pageOp._id,
  1856. activity,
  1857. );
  1858. } catch (err) {
  1859. logger.error(
  1860. 'Error occurred while running deleteRecursivelyMainOperation.',
  1861. err,
  1862. );
  1863. // cleanup
  1864. await PageOperation.deleteOne({ _id: pageOp._id });
  1865. throw err;
  1866. } finally {
  1867. this.pageEvent.emit('syncDescendantsUpdate', deletedPage, user);
  1868. }
  1869. })();
  1870. } else {
  1871. const preNotify = preNotifyService.generatePreNotify(activity);
  1872. this.activityEvent.emit('updated', activity, page, preNotify);
  1873. }
  1874. return deletedPage;
  1875. }
  1876. private async deleteNonEmptyTarget(page, user) {
  1877. const Page = mongoose.model('Page') as unknown as PageModel;
  1878. const newPath = Page.getDeletedPageName(page.path);
  1879. const deletedPage = await Page.findByIdAndUpdate(
  1880. page._id,
  1881. {
  1882. $set: {
  1883. path: newPath,
  1884. status: Page.STATUS_DELETED,
  1885. deleteUser: user._id,
  1886. deletedAt: Date.now(),
  1887. parent: null,
  1888. descendantCount: 0, // set parent as null
  1889. },
  1890. },
  1891. { new: true },
  1892. );
  1893. await PageTagRelation.updateMany(
  1894. { relatedPage: page._id },
  1895. { $set: { isPageTrashed: true } },
  1896. );
  1897. try {
  1898. await PageRedirect.create({ fromPath: page.path, toPath: newPath });
  1899. } catch (err) {
  1900. if (err.code !== 11000) {
  1901. throw err;
  1902. }
  1903. }
  1904. this.pageEvent.emit('delete', page, deletedPage, user);
  1905. return deletedPage;
  1906. }
  1907. async deleteRecursivelyMainOperation(
  1908. page,
  1909. user,
  1910. pageOpId: ObjectIdLike,
  1911. activity?,
  1912. ): Promise<void> {
  1913. const descendantsSubscribedSets = new Set();
  1914. await this.deleteDescendantsWithStream(
  1915. page,
  1916. user,
  1917. false,
  1918. descendantsSubscribedSets,
  1919. );
  1920. const descendantsSubscribedUsers = Array.from(
  1921. descendantsSubscribedSets,
  1922. ) as Ref<IUser>[];
  1923. const preNotify = preNotifyService.generatePreNotify(activity, async () => {
  1924. return descendantsSubscribedUsers;
  1925. });
  1926. this.activityEvent.emit('updated', activity, page, preNotify);
  1927. await PageOperation.findByIdAndDelete(pageOpId);
  1928. // no sub operation available
  1929. }
  1930. private async deletePageV4(
  1931. page: HydratedDocument<PageDocument>,
  1932. user,
  1933. options = {},
  1934. isRecursively = false,
  1935. ) {
  1936. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  1937. 'Page',
  1938. );
  1939. const newPath = Page.getDeletedPageName(page.path);
  1940. const isTrashed = isTrashPage(page.path);
  1941. if (isTrashed) {
  1942. throw new Error('This method does NOT support deleting trashed pages.');
  1943. }
  1944. if (!isMovablePage(page.path)) {
  1945. throw new Error('Page is not deletable.');
  1946. }
  1947. if (isRecursively) {
  1948. this.deleteDescendantsWithStream(page, user);
  1949. }
  1950. // update Revisions
  1951. await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
  1952. const deletedPage = await Page.findByIdAndUpdate(
  1953. page._id,
  1954. {
  1955. $set: {
  1956. path: newPath,
  1957. status: Page.STATUS_DELETED,
  1958. deleteUser: user._id,
  1959. deletedAt: Date.now(),
  1960. },
  1961. },
  1962. { new: true },
  1963. );
  1964. await PageTagRelation.updateMany(
  1965. { relatedPage: page._id },
  1966. { $set: { isPageTrashed: true } },
  1967. );
  1968. try {
  1969. await PageRedirect.create({ fromPath: page.path, toPath: newPath });
  1970. } catch (err) {
  1971. if (err.code !== 11000) {
  1972. throw err;
  1973. }
  1974. }
  1975. this.pageEvent.emit('delete', page, deletedPage, user);
  1976. return deletedPage;
  1977. }
  1978. private async deleteDescendants(pages, user) {
  1979. const Page = mongoose.model('Page') as unknown as PageModel;
  1980. const deletePageOperations: any[] = [];
  1981. const insertPageRedirectOperations: any[] = [];
  1982. pages.forEach((page) => {
  1983. const newPath = Page.getDeletedPageName(page.path);
  1984. const operation = page.isEmpty
  1985. ? {
  1986. deleteOne: {
  1987. filter: { _id: page._id },
  1988. },
  1989. }
  1990. : {
  1991. updateOne: {
  1992. filter: { _id: page._id },
  1993. update: {
  1994. $set: {
  1995. path: newPath,
  1996. status: Page.STATUS_DELETED,
  1997. deleteUser: user._id,
  1998. deletedAt: Date.now(),
  1999. parent: null,
  2000. descendantCount: 0, // set parent as null
  2001. },
  2002. },
  2003. },
  2004. };
  2005. if (!page.isEmpty) {
  2006. insertPageRedirectOperations.push({
  2007. insertOne: {
  2008. document: {
  2009. fromPath: page.path,
  2010. toPath: newPath,
  2011. },
  2012. },
  2013. });
  2014. }
  2015. deletePageOperations.push(operation);
  2016. });
  2017. try {
  2018. await Page.bulkWrite(deletePageOperations);
  2019. } catch (err) {
  2020. if (err.code !== 11000) {
  2021. throw new Error(`Failed to delete pages: ${err}`);
  2022. }
  2023. } finally {
  2024. this.pageEvent.emit('syncDescendantsDelete', pages, user);
  2025. }
  2026. try {
  2027. await PageRedirect.bulkWrite(insertPageRedirectOperations);
  2028. } catch (err) {
  2029. if (err.code !== 11000) {
  2030. throw Error(`Failed to create PageRedirect documents: ${err}`);
  2031. }
  2032. }
  2033. }
  2034. /**
  2035. * Create delete stream and return deleted document count
  2036. */
  2037. private async deleteDescendantsWithStream(
  2038. targetPage,
  2039. user,
  2040. shouldUseV4Process = true,
  2041. descendantsSubscribedSets?,
  2042. ): Promise<number> {
  2043. const readStream = shouldUseV4Process
  2044. ? await this.generateReadStreamToOperateOnlyDescendants(
  2045. targetPage.path,
  2046. user,
  2047. )
  2048. : await new PageCursorsForDescendantsFactory(
  2049. user,
  2050. targetPage,
  2051. true,
  2052. ).generateReadable();
  2053. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  2054. const deleteDescendants = this.deleteDescendants.bind(this);
  2055. let count = 0;
  2056. let nDeletedNonEmptyPages = 0; // used for updating descendantCount
  2057. const writeStream = new Writable({
  2058. objectMode: true,
  2059. async write(batch, encoding, callback) {
  2060. nDeletedNonEmptyPages += batch.filter((d) => !d.isEmpty).length;
  2061. try {
  2062. count += batch.length;
  2063. await deleteDescendants(batch, user);
  2064. const subscribedUsers = await Subscription.getSubscriptions(batch);
  2065. subscribedUsers.forEach((eachUser) => {
  2066. descendantsSubscribedSets.add(eachUser);
  2067. });
  2068. logger.debug(`Deleting pages progressing: (count=${count})`);
  2069. } catch (err) {
  2070. logger.error('deleteDescendants error on add anyway: ', err);
  2071. }
  2072. callback();
  2073. },
  2074. final(callback) {
  2075. logger.debug(`Deleting pages has completed: (totalCount=${count})`);
  2076. callback();
  2077. },
  2078. });
  2079. await pipeline(readStream, batchStream, writeStream);
  2080. return nDeletedNonEmptyPages;
  2081. }
  2082. async deleteCompletelyOperation(pageIds, pagePaths): Promise<void> {
  2083. // Delete Attachments, Revisions, Pages and emit delete
  2084. const Page = mongoose.model('Page') as unknown as PageModel;
  2085. const { attachmentService } = this.crowi;
  2086. const attachments = await Attachment.find({ page: { $in: pageIds } });
  2087. await Promise.all([
  2088. Comment.deleteMany({ page: { $in: pageIds } }),
  2089. PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
  2090. ShareLink.deleteMany({ relatedPage: { $in: pageIds } }),
  2091. Revision.deleteMany({ pageId: { $in: pageIds } }),
  2092. Page.deleteMany({ _id: { $in: pageIds } }),
  2093. PageRedirect.deleteMany({
  2094. $or: [{ fromPath: { $in: pagePaths } }, { toPath: { $in: pagePaths } }],
  2095. }),
  2096. attachmentService.removeAllAttachments(attachments),
  2097. // Leave bookmarks without deleting -- 2024.05.17 Yuki Takei
  2098. ]);
  2099. if (isAiEnabled()) {
  2100. const { getOpenaiService } = await import(
  2101. '~/features/openai/server/services/openai'
  2102. );
  2103. const openaiService = getOpenaiService();
  2104. await openaiService?.deleteVectorStoreFilesByPageIds(pageIds);
  2105. }
  2106. }
  2107. // delete multiple pages
  2108. async deleteMultipleCompletely(pages, user) {
  2109. const ids = pages.map((page) => page._id);
  2110. const paths = pages.map((page) => page.path);
  2111. logger.debug('Deleting completely', paths);
  2112. await this.deleteCompletelyOperation(ids, paths);
  2113. this.pageEvent.emit('syncDescendantsDelete', pages, user); // update as renamed page
  2114. return;
  2115. }
  2116. async deleteCompletely(
  2117. page,
  2118. user,
  2119. options = {},
  2120. isRecursively = false,
  2121. preventEmitting = false,
  2122. activityParameters,
  2123. ) {
  2124. /*
  2125. * Common Operation
  2126. */
  2127. const Page = mongoose.model('Page') as PageModel;
  2128. if (isTopPage(page.path)) {
  2129. throw Error('It is forbidden to delete the top page');
  2130. }
  2131. if (page.isEmpty && !isRecursively) {
  2132. throw Error('Page not found.');
  2133. }
  2134. // v4 compatible process
  2135. const isShouldUseV4Process = shouldUseV4Process(page);
  2136. if (isShouldUseV4Process) {
  2137. return this.deleteCompletelyV4(
  2138. page,
  2139. user,
  2140. options,
  2141. isRecursively,
  2142. preventEmitting,
  2143. );
  2144. }
  2145. const canOperate = await this.crowi.pageOperationService.canOperate(
  2146. isRecursively,
  2147. page.path,
  2148. null,
  2149. );
  2150. if (!canOperate) {
  2151. throw Error(
  2152. `Cannot operate deleteCompletely from path "${page.path}" right now.`,
  2153. );
  2154. }
  2155. const ids = [page._id];
  2156. const paths = [page.path];
  2157. logger.debug('Deleting completely', paths);
  2158. const parameters = {
  2159. ip: activityParameters.ip,
  2160. endpoint: activityParameters.endpoint,
  2161. action:
  2162. page.descendantCount > 0
  2163. ? SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY
  2164. : SupportedAction.ACTION_PAGE_DELETE_COMPLETELY,
  2165. user,
  2166. target: page,
  2167. targetModel: 'Page',
  2168. snapshot: {
  2169. username: user.username,
  2170. },
  2171. };
  2172. const activity =
  2173. await this.crowi.activityService.createActivity(parameters);
  2174. // 1. update descendantCount
  2175. if (isRecursively) {
  2176. const inc = page.isEmpty
  2177. ? -page.descendantCount
  2178. : -(page.descendantCount + 1);
  2179. await this.updateDescendantCountOfAncestors(page.parent, inc, true);
  2180. } else {
  2181. // replace with an empty page
  2182. const shouldReplace = await Page.exists({ parent: page._id });
  2183. let pageToUpdateDescendantCount = page;
  2184. if (shouldReplace) {
  2185. pageToUpdateDescendantCount = await Page.replaceTargetWithPage(page);
  2186. }
  2187. await this.updateDescendantCountOfAncestors(
  2188. pageToUpdateDescendantCount.parent,
  2189. -1,
  2190. true,
  2191. );
  2192. }
  2193. // 2. then delete target completely
  2194. await this.deleteCompletelyOperation(ids, paths);
  2195. // delete leaf empty pages
  2196. await Page.removeLeafEmptyPagesRecursively(page.parent);
  2197. if (!page.isEmpty && !preventEmitting) {
  2198. this.pageEvent.emit('deleteCompletely', page, user);
  2199. }
  2200. if (isRecursively) {
  2201. let pageOp: PageOperationDocument;
  2202. try {
  2203. pageOp = await PageOperation.create({
  2204. actionType: PageActionType.DeleteCompletely,
  2205. actionStage: PageActionStage.Main,
  2206. page,
  2207. user,
  2208. fromPath: page.path,
  2209. options,
  2210. });
  2211. } catch (err) {
  2212. logger.error('Failed to create PageOperation document.', err);
  2213. throw err;
  2214. }
  2215. /*
  2216. * Main Operation
  2217. */
  2218. (async () => {
  2219. try {
  2220. await this.deleteCompletelyRecursivelyMainOperation(
  2221. page,
  2222. user,
  2223. options,
  2224. pageOp._id,
  2225. activity,
  2226. );
  2227. } catch (err) {
  2228. logger.error(
  2229. 'Error occurred while running deleteCompletelyRecursivelyMainOperation.',
  2230. err,
  2231. );
  2232. // cleanup
  2233. await PageOperation.deleteOne({ _id: pageOp._id });
  2234. throw err;
  2235. }
  2236. })();
  2237. } else {
  2238. const preNotify = preNotifyService.generatePreNotify(activity);
  2239. this.activityEvent.emit('updated', activity, page, preNotify);
  2240. }
  2241. return;
  2242. }
  2243. async deleteCompletelyRecursivelyMainOperation(
  2244. page,
  2245. user,
  2246. options,
  2247. pageOpId: ObjectIdLike,
  2248. activity?,
  2249. ): Promise<void> {
  2250. const descendantsSubscribedSets = new Set();
  2251. await this.deleteCompletelyDescendantsWithStream(
  2252. page,
  2253. user,
  2254. options,
  2255. false,
  2256. descendantsSubscribedSets,
  2257. );
  2258. const descendantsSubscribedUsers = Array.from(
  2259. descendantsSubscribedSets,
  2260. ) as Ref<IUser>[];
  2261. const preNotify = preNotifyService.generatePreNotify(activity, async () => {
  2262. return descendantsSubscribedUsers;
  2263. });
  2264. this.activityEvent.emit('updated', activity, page, preNotify);
  2265. await PageOperation.findByIdAndDelete(pageOpId);
  2266. // no sub operation available
  2267. }
  2268. private async deleteCompletelyV4(
  2269. page,
  2270. user,
  2271. options = {},
  2272. isRecursively = false,
  2273. preventEmitting = false,
  2274. ) {
  2275. const ids = [page._id];
  2276. const paths = [page.path];
  2277. logger.debug('Deleting completely', paths);
  2278. await this.deleteCompletelyOperation(ids, paths);
  2279. if (isRecursively) {
  2280. this.deleteCompletelyDescendantsWithStream(page, user, options);
  2281. }
  2282. if (!page.isEmpty && !preventEmitting) {
  2283. this.pageEvent.emit('deleteCompletely', page, user);
  2284. }
  2285. return;
  2286. }
  2287. async emptyTrashPage(user, options = {}, activityParameters) {
  2288. const page = { path: '/trash' };
  2289. const parameters = {
  2290. ...activityParameters,
  2291. action: SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY,
  2292. user,
  2293. targetModel: 'Page',
  2294. snapshot: {
  2295. username: user.username,
  2296. },
  2297. };
  2298. const activity =
  2299. await this.crowi.activityService.createActivity(parameters);
  2300. const descendantsSubscribedSets = new Set();
  2301. const pages = await this.deleteCompletelyDescendantsWithStream(
  2302. page,
  2303. user,
  2304. options,
  2305. true,
  2306. descendantsSubscribedSets,
  2307. );
  2308. const descendantsSubscribedUsers = Array.from(
  2309. descendantsSubscribedSets,
  2310. ) as Ref<IUser>[];
  2311. const preNotify = preNotifyService.generatePreNotify(activity, async () => {
  2312. return descendantsSubscribedUsers;
  2313. });
  2314. this.activityEvent.emit('updated', activity, page, preNotify);
  2315. return pages;
  2316. }
  2317. /**
  2318. * Create delete completely stream
  2319. */
  2320. private async deleteCompletelyDescendantsWithStream(
  2321. targetPage,
  2322. user,
  2323. options = {},
  2324. shouldUseV4Process = true,
  2325. descendantsSubscribedSets?,
  2326. ): Promise<number> {
  2327. const readStream = shouldUseV4Process // pages don't have parents
  2328. ? await this.generateReadStreamToOperateOnlyDescendants(
  2329. targetPage.path,
  2330. user,
  2331. )
  2332. : await new PageCursorsForDescendantsFactory(
  2333. user,
  2334. targetPage,
  2335. true,
  2336. ).generateReadable();
  2337. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  2338. let count = 0;
  2339. let nDeletedNonEmptyPages = 0; // used for updating descendantCount
  2340. const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
  2341. const writeStream = new Writable({
  2342. objectMode: true,
  2343. async write(batch, encoding, callback) {
  2344. nDeletedNonEmptyPages += batch.filter((d) => !d.isEmpty).length;
  2345. try {
  2346. count += batch.length;
  2347. await deleteMultipleCompletely(batch, user);
  2348. const subscribedUsers = await Subscription.getSubscriptions(batch);
  2349. subscribedUsers.forEach((eachUser) => {
  2350. descendantsSubscribedSets.add(eachUser);
  2351. });
  2352. logger.debug(`Adding pages progressing: (count=${count})`);
  2353. } catch (err) {
  2354. logger.error('addAllPages error on add anyway: ', err);
  2355. }
  2356. callback();
  2357. },
  2358. final(callback) {
  2359. logger.debug(`Adding pages has completed: (totalCount=${count})`);
  2360. callback();
  2361. },
  2362. });
  2363. await pipeline(readStream, batchStream, writeStream);
  2364. return nDeletedNonEmptyPages;
  2365. }
  2366. // no need to separate Main Sub since it is devided into single page operations
  2367. async deleteMultiplePages(
  2368. pagesToDelete,
  2369. user,
  2370. options,
  2371. activityParameters,
  2372. ): Promise<void> {
  2373. const { isRecursively, isCompletely } = options;
  2374. if (pagesToDelete.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  2375. throw Error(
  2376. `The maximum number of pages is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`,
  2377. );
  2378. }
  2379. // omit duplicate paths if isRecursively true, omit empty pages if isRecursively false
  2380. const pages = isRecursively
  2381. ? omitDuplicateAreaPageFromPages(pagesToDelete)
  2382. : pagesToDelete.filter((p) => !p.isEmpty);
  2383. if (isCompletely) {
  2384. for await (const page of pages) {
  2385. await this.deleteCompletely(
  2386. page,
  2387. user,
  2388. {},
  2389. isRecursively,
  2390. false,
  2391. activityParameters,
  2392. );
  2393. }
  2394. } else {
  2395. for await (const page of pages) {
  2396. await this.deletePage(
  2397. page,
  2398. user,
  2399. {},
  2400. isRecursively,
  2401. activityParameters,
  2402. );
  2403. }
  2404. }
  2405. }
  2406. // use the same process in both v4 and v5
  2407. private async revertDeletedDescendants(pages, user) {
  2408. const Page = mongoose.model<IPage, PageModel>('Page');
  2409. const revertPageOperations: any[] = [];
  2410. const fromPathsToDelete: string[] = [];
  2411. pages.forEach((page) => {
  2412. // e.g. page.path = /trash/test, toPath = /test
  2413. const toPath = Page.getRevertDeletedPageName(page.path);
  2414. revertPageOperations.push({
  2415. updateOne: {
  2416. filter: { _id: page._id },
  2417. update: {
  2418. $set: {
  2419. path: toPath,
  2420. status: Page.STATUS_PUBLISHED,
  2421. lastUpdateUser: user._id,
  2422. deleteUser: null,
  2423. deletedAt: null,
  2424. },
  2425. },
  2426. },
  2427. });
  2428. fromPathsToDelete.push(page.path);
  2429. });
  2430. try {
  2431. await Page.bulkWrite(revertPageOperations);
  2432. await PageRedirect.deleteMany({ fromPath: { $in: fromPathsToDelete } });
  2433. } catch (err) {
  2434. if (err.code !== 11000) {
  2435. throw new Error(`Failed to revert pages: ${err}`);
  2436. }
  2437. }
  2438. }
  2439. async revertDeletedPage(
  2440. page,
  2441. user,
  2442. options = {},
  2443. isRecursively = false,
  2444. activityParameters?,
  2445. ) {
  2446. /*
  2447. * Common Operation
  2448. */
  2449. const Page = mongoose.model<IPage, PageModel>('Page');
  2450. const parameters = {
  2451. ip: activityParameters.ip,
  2452. endpoint: activityParameters.endpoint,
  2453. action:
  2454. page.descendantCount > 0
  2455. ? SupportedAction.ACTION_PAGE_RECURSIVELY_REVERT
  2456. : SupportedAction.ACTION_PAGE_REVERT,
  2457. user,
  2458. target: page,
  2459. targetModel: 'Page',
  2460. snapshot: {
  2461. username: user.username,
  2462. },
  2463. };
  2464. const activity =
  2465. await this.crowi.activityService.createActivity(parameters);
  2466. // 1. Separate v4 & v5 process
  2467. const shouldUseV4Process = this.shouldUseV4ProcessForRevert(page);
  2468. if (shouldUseV4Process) {
  2469. return this.revertDeletedPageV4(page, user, options, isRecursively);
  2470. }
  2471. const newPath = Page.getRevertDeletedPageName(page.path);
  2472. const canOperate = await this.crowi.pageOperationService.canOperate(
  2473. isRecursively,
  2474. page.path,
  2475. newPath,
  2476. );
  2477. if (!canOperate) {
  2478. throw Error(`Cannot operate revert from path "${page.path}" right now.`);
  2479. }
  2480. const includeEmpty = true;
  2481. const originPage = await Page.findByPath(newPath, includeEmpty);
  2482. // throw if any page already exists when recursively operation
  2483. if (originPage != null && (!originPage.isEmpty || isRecursively)) {
  2484. throw new PathAlreadyExistsError('already_exists', originPage.path);
  2485. }
  2486. // 2. Revert target
  2487. const parent = await this.getParentAndFillAncestorsByUser(user, newPath);
  2488. const shouldReplace = originPage != null && originPage.isEmpty;
  2489. let updatedPage = await Page.findByIdAndUpdate(
  2490. page._id,
  2491. {
  2492. $set: {
  2493. path: newPath,
  2494. status: Page.STATUS_PUBLISHED,
  2495. lastUpdateUser: user._id,
  2496. deleteUser: null,
  2497. deletedAt: null,
  2498. parent: parent._id,
  2499. descendantCount: shouldReplace ? originPage.descendantCount : 0,
  2500. },
  2501. },
  2502. { new: true },
  2503. );
  2504. if (shouldReplace) {
  2505. updatedPage = await Page.replaceTargetWithPage(
  2506. originPage,
  2507. updatedPage,
  2508. true,
  2509. );
  2510. }
  2511. await PageTagRelation.updateMany(
  2512. { relatedPage: page._id },
  2513. { $set: { isPageTrashed: false } },
  2514. );
  2515. this.pageEvent.emit('revert', page, updatedPage, user);
  2516. if (!isRecursively) {
  2517. await this.updateDescendantCountOfAncestors(parent._id, 1, true);
  2518. const preNotify = preNotifyService.generatePreNotify(activity);
  2519. this.activityEvent.emit('updated', activity, page, preNotify);
  2520. } else {
  2521. let pageOp: PageOperationDocument;
  2522. try {
  2523. pageOp = await PageOperation.create({
  2524. actionType: PageActionType.Revert,
  2525. actionStage: PageActionStage.Main,
  2526. page,
  2527. user,
  2528. fromPath: page.path,
  2529. toPath: newPath,
  2530. options,
  2531. });
  2532. } catch (err) {
  2533. logger.error('Failed to create PageOperation document.', err);
  2534. throw err;
  2535. }
  2536. /*
  2537. * Resumable Operation
  2538. */
  2539. (async () => {
  2540. try {
  2541. await this.revertRecursivelyMainOperation(
  2542. page,
  2543. user,
  2544. options,
  2545. pageOp._id,
  2546. activity,
  2547. );
  2548. this.pageEvent.emit('syncDescendantsUpdate', updatedPage, user);
  2549. } catch (err) {
  2550. logger.error(
  2551. 'Error occurred while running revertRecursivelyMainOperation.',
  2552. err,
  2553. );
  2554. // cleanup
  2555. await PageOperation.deleteOne({ _id: pageOp._id });
  2556. throw err;
  2557. }
  2558. })();
  2559. }
  2560. return updatedPage;
  2561. }
  2562. async revertRecursivelyMainOperation(
  2563. page,
  2564. user,
  2565. options,
  2566. pageOpId: ObjectIdLike,
  2567. activity?,
  2568. ): Promise<void> {
  2569. const Page = mongoose.model('Page') as unknown as PageModel;
  2570. const descendantsSubscribedSets = new Set();
  2571. await this.revertDeletedDescendantsWithStream(
  2572. page,
  2573. user,
  2574. options,
  2575. false,
  2576. descendantsSubscribedSets,
  2577. );
  2578. const descendantsSubscribedUsers = Array.from(
  2579. descendantsSubscribedSets,
  2580. ) as Ref<IUser>[];
  2581. const preNotify = preNotifyService.generatePreNotify(activity, async () => {
  2582. return descendantsSubscribedUsers;
  2583. });
  2584. this.activityEvent.emit('updated', activity, page, preNotify);
  2585. const newPath = Page.getRevertDeletedPageName(page.path);
  2586. // normalize parent of descendant pages
  2587. const shouldNormalize = this.shouldNormalizeParent(page);
  2588. if (shouldNormalize) {
  2589. try {
  2590. await this.normalizeParentAndDescendantCountOfDescendants(
  2591. newPath,
  2592. user,
  2593. );
  2594. logger.info(
  2595. `Successfully normalized reverted descendant pages under "${newPath}"`,
  2596. );
  2597. } catch (err) {
  2598. logger.error('Failed to normalize descendants afrer revert:', err);
  2599. throw err;
  2600. }
  2601. }
  2602. // Set to Sub
  2603. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(
  2604. pageOpId,
  2605. PageActionStage.Sub,
  2606. );
  2607. if (pageOp == null) {
  2608. throw Error('PageOperation document not found');
  2609. }
  2610. /*
  2611. * Sub Operation
  2612. */
  2613. await this.revertRecursivelySubOperation(newPath, pageOp._id);
  2614. }
  2615. async revertRecursivelySubOperation(
  2616. newPath: string,
  2617. pageOpId: ObjectIdLike,
  2618. ): Promise<void> {
  2619. const Page = mongoose.model('Page') as unknown as PageModel;
  2620. const newTarget = await Page.findOne({ path: newPath }); // only one page will be found since duplicating to existing path is forbidden
  2621. if (newTarget == null) {
  2622. throw Error(
  2623. 'No reverted page found. Something might have gone wrong in revertRecursivelyMainOperation.',
  2624. );
  2625. }
  2626. // update descendantCount of ancestors'
  2627. await this.updateDescendantCountOfAncestors(
  2628. newTarget.parent as ObjectIdLike,
  2629. newTarget.descendantCount + 1,
  2630. true,
  2631. );
  2632. await PageOperation.findByIdAndDelete(pageOpId);
  2633. }
  2634. private async revertDeletedPageV4(
  2635. page,
  2636. user,
  2637. options = {},
  2638. isRecursively = false,
  2639. ) {
  2640. const Page = mongoose.model<IPage, PageModel>('Page');
  2641. const newPath = Page.getRevertDeletedPageName(page.path);
  2642. const originPage = await Page.findByPath(newPath);
  2643. if (originPage != null) {
  2644. throw new PathAlreadyExistsError('already_exists', originPage.path);
  2645. }
  2646. if (isRecursively) {
  2647. this.revertDeletedDescendantsWithStream(page, user, options);
  2648. }
  2649. page.status = Page.STATUS_PUBLISHED;
  2650. page.lastUpdateUser = user;
  2651. logger.debug('Revert deleted the page', page, newPath);
  2652. const updatedPage = await Page.findByIdAndUpdate(
  2653. page._id,
  2654. {
  2655. $set: {
  2656. path: newPath,
  2657. status: Page.STATUS_PUBLISHED,
  2658. lastUpdateUser: user._id,
  2659. deleteUser: null,
  2660. deletedAt: null,
  2661. },
  2662. },
  2663. { new: true },
  2664. );
  2665. await PageTagRelation.updateMany(
  2666. { relatedPage: page._id },
  2667. { $set: { isPageTrashed: false } },
  2668. );
  2669. this.pageEvent.emit('revert', page, updatedPage, user);
  2670. return updatedPage;
  2671. }
  2672. private async applyScopesToDescendantsWithStream(
  2673. parentPage,
  2674. user,
  2675. isV4 = false,
  2676. ) {
  2677. const Page = mongoose.model<IPage, PageModel>('Page');
  2678. const builder = new Page.PageQueryBuilder(Page.find());
  2679. builder.addConditionToListOnlyDescendants(parentPage.path);
  2680. if (isV4) {
  2681. builder.addConditionAsRootOrNotOnTree();
  2682. } else {
  2683. builder.addConditionAsOnTree();
  2684. }
  2685. // add grant conditions
  2686. await Page.addConditionToFilteringByViewerToEdit(builder, user);
  2687. const grant = parentPage.grant;
  2688. const userRelatedGroups =
  2689. await this.pageGrantService.getUserRelatedGroups(user);
  2690. const userRelatedParentGrantedGroups =
  2691. this.pageGrantService.getUserRelatedGrantedGroupsSyncronously(
  2692. userRelatedGroups,
  2693. parentPage,
  2694. );
  2695. const childPagesReadableStream = builder.query.cursor({
  2696. batchSize: BULK_REINDEX_SIZE,
  2697. });
  2698. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  2699. const childPagesWritable = new Writable({
  2700. objectMode: true,
  2701. write: async (batch, encoding, callback) => {
  2702. await this.updateChildPagesGrant(
  2703. batch,
  2704. grant,
  2705. user,
  2706. userRelatedGroups,
  2707. userRelatedParentGrantedGroups,
  2708. );
  2709. callback();
  2710. },
  2711. });
  2712. await pipeline(childPagesReadableStream, batchStream, childPagesWritable);
  2713. }
  2714. async updateChildPagesGrant(
  2715. pages: PageDocument[],
  2716. grant: PageGrant,
  2717. user,
  2718. userRelatedGroups: PopulatedGrantedGroup[],
  2719. userRelatedParentGrantedGroups: IGrantedGroup[],
  2720. ): Promise<void> {
  2721. const Page = mongoose.model('Page') as unknown as PageModel;
  2722. const operations: any = [];
  2723. pages.forEach((childPage) => {
  2724. let newChildGrantedGroups: IGrantedGroup[] = [];
  2725. if (grant === PageGrant.GRANT_USER_GROUP) {
  2726. newChildGrantedGroups = this.getNewGrantedGroupsSyncronously(
  2727. userRelatedGroups,
  2728. userRelatedParentGrantedGroups,
  2729. childPage,
  2730. );
  2731. }
  2732. const canChangeGrant =
  2733. this.pageGrantService.validateGrantChangeSyncronously(
  2734. userRelatedGroups,
  2735. childPage.grantedGroups,
  2736. grant,
  2737. newChildGrantedGroups,
  2738. );
  2739. if (canChangeGrant) {
  2740. operations.push({
  2741. updateOne: {
  2742. filter: { _id: childPage._id },
  2743. update: {
  2744. $set: {
  2745. grant,
  2746. grantedUsers: grant === PageGrant.GRANT_OWNER ? [user._id] : [],
  2747. grantedGroups: newChildGrantedGroups,
  2748. },
  2749. },
  2750. },
  2751. });
  2752. }
  2753. });
  2754. await Page.bulkWrite(operations);
  2755. }
  2756. /**
  2757. * Create revert stream
  2758. */
  2759. private async revertDeletedDescendantsWithStream(
  2760. targetPage,
  2761. user,
  2762. options = {},
  2763. shouldUseV4Process = true,
  2764. descendantsSubscribedSets?,
  2765. ): Promise<number> {
  2766. if (shouldUseV4Process) {
  2767. return this.revertDeletedDescendantsWithStreamV4(
  2768. targetPage,
  2769. user,
  2770. options,
  2771. );
  2772. }
  2773. const readStream = await this.generateReadStreamToOperateOnlyDescendants(
  2774. targetPage.path,
  2775. user,
  2776. );
  2777. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  2778. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  2779. let count = 0;
  2780. const writeStream = new Writable({
  2781. objectMode: true,
  2782. async write(batch, encoding, callback) {
  2783. try {
  2784. count += batch.length;
  2785. await revertDeletedDescendants(batch, user);
  2786. const subscribedUsers = await Subscription.getSubscriptions(batch);
  2787. subscribedUsers.forEach((eachUser) => {
  2788. descendantsSubscribedSets.add(eachUser);
  2789. });
  2790. logger.debug(`Reverting pages progressing: (count=${count})`);
  2791. } catch (err) {
  2792. logger.error('revertPages error on add anyway: ', err);
  2793. }
  2794. callback();
  2795. },
  2796. async final(callback) {
  2797. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  2798. callback();
  2799. },
  2800. });
  2801. await pipeline(readStream, batchStream, writeStream);
  2802. return count;
  2803. }
  2804. private async revertDeletedDescendantsWithStreamV4(
  2805. targetPage,
  2806. user,
  2807. options = {},
  2808. ) {
  2809. const readStream = await this.generateReadStreamToOperateOnlyDescendants(
  2810. targetPage.path,
  2811. user,
  2812. );
  2813. const batchStream = createBatchStream(BULK_REINDEX_SIZE);
  2814. const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
  2815. let count = 0;
  2816. const writeStream = new Writable({
  2817. objectMode: true,
  2818. async write(batch, encoding, callback) {
  2819. try {
  2820. count += batch.length;
  2821. await revertDeletedDescendants(batch, user);
  2822. logger.debug(`Reverting pages progressing: (count=${count})`);
  2823. } catch (err) {
  2824. logger.error('revertPages error on add anyway: ', err);
  2825. }
  2826. callback();
  2827. },
  2828. final(callback) {
  2829. logger.debug(`Reverting pages has completed: (totalCount=${count})`);
  2830. callback();
  2831. },
  2832. });
  2833. await pipeline(readStream, batchStream, writeStream);
  2834. return count;
  2835. }
  2836. async handlePrivatePagesForGroupsToDelete(
  2837. groupsToDelete: UserGroupDocument[] | ExternalUserGroupDocument[],
  2838. action: PageActionOnGroupDelete,
  2839. transferToUserGroup: IGrantedGroup | undefined,
  2840. user: IUser,
  2841. ): Promise<void> {
  2842. const Page = mongoose.model<IPage, PageModel>('Page');
  2843. const pages = await Page.find({
  2844. grantedGroups: { $elemMatch: { item: { $in: groupsToDelete } } },
  2845. });
  2846. switch (action) {
  2847. case PageActionOnGroupDelete.publicize:
  2848. await Page.removeGroupsToDeleteFromPages(pages, groupsToDelete);
  2849. break;
  2850. case PageActionOnGroupDelete.delete:
  2851. return this.deleteMultipleCompletely(pages, user);
  2852. case PageActionOnGroupDelete.transfer:
  2853. await Page.transferPagesToGroup(pages, transferToUserGroup);
  2854. break;
  2855. default:
  2856. throw new Error('Unknown action for private pages');
  2857. }
  2858. }
  2859. private extractStringIds(refs: Ref<HasObjectId>[]) {
  2860. return refs.map((ref: Ref<HasObjectId>) => {
  2861. return typeof ref === 'string' ? ref : ref._id.toString();
  2862. });
  2863. }
  2864. constructBasicPageInfo(
  2865. page: HydratedDocument<PageDocument>,
  2866. isGuestUser?: boolean,
  2867. ): IPageInfoBasic {
  2868. const isMovable = isGuestUser ? false : isMovablePage(page.path);
  2869. const pageId = page._id.toString();
  2870. if (page.isEmpty) {
  2871. return {
  2872. emptyPageId: pageId,
  2873. isNotFound: false,
  2874. isV5Compatible: true,
  2875. isEmpty: true,
  2876. isMovable,
  2877. isRevertible: false,
  2878. } satisfies IPageInfoBasicForEmpty;
  2879. }
  2880. const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
  2881. const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
  2882. const infoForEntity = {
  2883. isNotFound: false,
  2884. isV5Compatible: isTopPage(page.path) || page.parent != null,
  2885. isEmpty: false,
  2886. sumOfLikers: page.liker.length,
  2887. likerIds: this.extractStringIds(likers),
  2888. seenUserIds: this.extractStringIds(seenUsers),
  2889. sumOfSeenUsers: page.seenUsers.length,
  2890. isMovable,
  2891. isRevertible: isTrashPage(page.path),
  2892. contentAge: page.getContentAge(),
  2893. descendantCount: page.descendantCount,
  2894. commentCount: page.commentCount,
  2895. // biome-ignore lint/style/noNonNullAssertion: the page must have a revision if it is not empty
  2896. latestRevisionId: getIdStringForRef(page.revision!),
  2897. } satisfies IPageInfoBasicForEntity;
  2898. return infoForEntity;
  2899. }
  2900. async shortBodiesMapByPageIds(
  2901. pageIds: ObjectIdLike[] = [],
  2902. user?,
  2903. ): Promise<Record<string, string | null>> {
  2904. const Page = mongoose.model('Page') as unknown as PageModel;
  2905. const MAX_LENGTH = 350;
  2906. // aggregation options
  2907. const userGroups =
  2908. user != null
  2909. ? [
  2910. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  2911. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
  2912. user,
  2913. )),
  2914. ]
  2915. : null;
  2916. const viewerCondition = Page.generateGrantCondition(user, userGroups);
  2917. const filterByIds = {
  2918. _id: { $in: pageIds },
  2919. };
  2920. let pages: Array<{
  2921. _id: string;
  2922. revisionData?: Array<{ revision?: string }>;
  2923. }>;
  2924. try {
  2925. pages = await Page.aggregate([
  2926. // filter by pageIds
  2927. {
  2928. $match: filterByIds,
  2929. },
  2930. // filter by viewer
  2931. {
  2932. $match: viewerCondition,
  2933. },
  2934. // lookup: https://docs.mongodb.com/v4.4/reference/operator/aggregation/lookup/
  2935. {
  2936. $lookup: {
  2937. from: 'revisions',
  2938. let: { localRevision: '$revision' },
  2939. pipeline: [
  2940. {
  2941. $match: {
  2942. $expr: {
  2943. $eq: ['$_id', '$$localRevision'],
  2944. },
  2945. },
  2946. },
  2947. {
  2948. $project: {
  2949. // What is $substrCP?
  2950. // see: https://stackoverflow.com/questions/43556024/mongodb-error-substrbytes-invalid-range-ending-index-is-in-the-middle-of-a-ut/43556249
  2951. revision: { $substrCP: ['$body', 0, MAX_LENGTH] },
  2952. },
  2953. },
  2954. ],
  2955. as: 'revisionData',
  2956. },
  2957. },
  2958. // projection
  2959. {
  2960. $project: {
  2961. _id: 1,
  2962. revisionData: 1,
  2963. },
  2964. },
  2965. ]).exec();
  2966. } catch (err) {
  2967. logger.error('Error occurred while generating shortBodiesMap');
  2968. throw err;
  2969. }
  2970. const shortBodiesMap = {};
  2971. pages.forEach((page) => {
  2972. shortBodiesMap[page._id] = page.revisionData?.[0]?.revision;
  2973. });
  2974. return shortBodiesMap;
  2975. }
  2976. async normalizeParentByPath(path: string, user): Promise<void> {
  2977. const Page = mongoose.model<PageDocument, PageModel>('Page');
  2978. const { PageQueryBuilder } = Page;
  2979. // This validation is not 100% correct since it ignores user to count
  2980. const builder = new PageQueryBuilder(Page.find());
  2981. builder.addConditionAsRootOrNotOnTree();
  2982. builder.addConditionToListWithDescendants(path);
  2983. const nEstimatedNormalizationTarget: number =
  2984. await builder.query.exec('count');
  2985. if (nEstimatedNormalizationTarget === 0) {
  2986. throw Error('No page is available for conversion');
  2987. }
  2988. const pages = await Page.findByPathAndViewer(path, user, null, false);
  2989. if (pages == null || !Array.isArray(pages)) {
  2990. throw Error('Something went wrong while converting pages.');
  2991. }
  2992. if (pages.length === 0) {
  2993. const isForbidden = (await Page.count({ path, isEmpty: false })) > 0;
  2994. if (isForbidden) {
  2995. throw new V5ConversionError(
  2996. 'It is not allowed to convert this page.',
  2997. V5ConversionErrCode.FORBIDDEN,
  2998. );
  2999. }
  3000. }
  3001. if (pages.length > 1) {
  3002. throw new V5ConversionError(
  3003. `There are more than two pages at the path "${path}". Please rename or delete the page first.`,
  3004. V5ConversionErrCode.DUPLICATE_PAGES_FOUND,
  3005. );
  3006. }
  3007. const notEmptyParent =
  3008. pages[0] == null
  3009. ? await Page.findNotEmptyParentByPathRecursively(path)
  3010. : null;
  3011. const page =
  3012. pages[0] ??
  3013. (await this.forceCreateBySystem(path, '', {
  3014. grant: notEmptyParent?.grant,
  3015. grantUserIds: notEmptyParent?.grantedUsers.map((u) => getIdForRef(u)),
  3016. grantUserGroupIds: notEmptyParent?.grantedGroups,
  3017. }));
  3018. const grant = page.grant;
  3019. const grantedUserIds = page.grantedUsers.map((u) => getIdForRef(u));
  3020. const grantedGroupIds = page.grantedGroups;
  3021. /*
  3022. * UserGroup & Owner validation
  3023. */
  3024. let isGrantNormalized = false;
  3025. try {
  3026. const shouldCheckDescendants = true;
  3027. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  3028. user,
  3029. path,
  3030. grant,
  3031. grantedUserIds,
  3032. grantedGroupIds,
  3033. shouldCheckDescendants,
  3034. );
  3035. } catch (err) {
  3036. logger.error(`Failed to validate grant of page at "${path}"`, err);
  3037. throw err;
  3038. }
  3039. if (!isGrantNormalized) {
  3040. throw new V5ConversionError(
  3041. 'This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.',
  3042. V5ConversionErrCode.GRANT_INVALID,
  3043. );
  3044. }
  3045. let pageOp: PageOperationDocument;
  3046. try {
  3047. pageOp = await PageOperation.create({
  3048. actionType: PageActionType.NormalizeParent,
  3049. actionStage: PageActionStage.Main,
  3050. page,
  3051. user,
  3052. fromPath: page.path,
  3053. toPath: page.path,
  3054. });
  3055. } catch (err) {
  3056. logger.error('Failed to create PageOperation document.', err);
  3057. throw err;
  3058. }
  3059. (async () => {
  3060. try {
  3061. await this.normalizeParentRecursivelyMainOperation(
  3062. page,
  3063. user,
  3064. pageOp._id,
  3065. );
  3066. } catch (err) {
  3067. logger.error(
  3068. 'Error occurred while running normalizeParentRecursivelyMainOperation.',
  3069. err,
  3070. );
  3071. // cleanup
  3072. await PageOperation.deleteOne({ _id: pageOp._id });
  3073. throw err;
  3074. }
  3075. })();
  3076. }
  3077. async normalizeParentByPageIdsRecursively(
  3078. pageIds: ObjectIdLike[],
  3079. user,
  3080. ): Promise<void> {
  3081. const Page = mongoose.model('Page') as unknown as PageModel;
  3082. const pages = await Page.findByIdsAndViewer(pageIds, user, null);
  3083. if (pages == null || pages.length === 0) {
  3084. throw Error('pageIds is null or 0 length.');
  3085. }
  3086. if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  3087. throw Error(
  3088. `The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`,
  3089. );
  3090. }
  3091. await this.normalizeParentRecursivelyByPages(pages, user);
  3092. return;
  3093. }
  3094. async normalizeParentByPageIds(pageIds: ObjectIdLike[], user): Promise<void> {
  3095. const Page = (await mongoose.model('Page')) as unknown as PageModel;
  3096. const socket = this.crowi.socketIoService.getDefaultSocket();
  3097. for await (const pageId of pageIds) {
  3098. const page = await Page.findById(pageId);
  3099. if (page == null) {
  3100. continue;
  3101. }
  3102. const errorData: PageMigrationErrorData = { paths: [page.path] };
  3103. try {
  3104. const canOperate = await this.crowi.pageOperationService.canOperate(
  3105. false,
  3106. page.path,
  3107. page.path,
  3108. );
  3109. if (!canOperate) {
  3110. throw Error(
  3111. `Cannot operate normalizeParent to path "${page.path}" right now.`,
  3112. );
  3113. }
  3114. const normalizedPage = await this.normalizeParentByPage(page, user);
  3115. if (normalizedPage == null) {
  3116. socket.emit(SocketEventName.PageMigrationError, errorData);
  3117. logger.error(
  3118. `Failed to update descendantCount of page of id: "${pageId}"`,
  3119. );
  3120. }
  3121. } catch (err) {
  3122. socket.emit(SocketEventName.PageMigrationError, errorData);
  3123. logger.error('Something went wrong while normalizing parent.', err);
  3124. }
  3125. }
  3126. socket.emit(SocketEventName.PageMigrationSuccess);
  3127. }
  3128. private async normalizeParentByPage(page, user) {
  3129. const Page = mongoose.model('Page') as unknown as PageModel;
  3130. const {
  3131. path,
  3132. grant,
  3133. grantedUsers: grantedUserIds,
  3134. grantedGroups: grantedGroupIds,
  3135. } = page;
  3136. // check if any page exists at target path already
  3137. const existingPage = await Page.findOne({ path, parent: { $ne: null } });
  3138. if (existingPage != null && !existingPage.isEmpty) {
  3139. throw Error('Page already exists. Please rename the page to continue.');
  3140. }
  3141. /*
  3142. * UserGroup & Owner validation
  3143. */
  3144. if (grant !== Page.GRANT_RESTRICTED) {
  3145. let isGrantNormalized = false;
  3146. try {
  3147. const shouldCheckDescendants = true;
  3148. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  3149. user,
  3150. path,
  3151. grant,
  3152. grantedUserIds,
  3153. grantedGroupIds,
  3154. shouldCheckDescendants,
  3155. );
  3156. } catch (err) {
  3157. logger.error(`Failed to validate grant of page at "${path}"`, err);
  3158. throw err;
  3159. }
  3160. if (!isGrantNormalized) {
  3161. throw Error(
  3162. 'This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.',
  3163. );
  3164. }
  3165. } else {
  3166. throw Error('Restricted pages can not be migrated');
  3167. }
  3168. let normalizedPage: PageDocument | null;
  3169. // replace if empty page exists
  3170. if (existingPage != null && existingPage.isEmpty) {
  3171. // Inherit descendantCount from the empty page
  3172. const updatedPage = await Page.findOneAndUpdate(
  3173. { _id: page._id },
  3174. { descendantCount: existingPage.descendantCount },
  3175. { new: true },
  3176. );
  3177. await Page.replaceTargetWithPage(existingPage, updatedPage, true);
  3178. normalizedPage = await Page.findById(page._id);
  3179. } else {
  3180. const parent = await this.getParentAndFillAncestorsByUser(
  3181. user,
  3182. page.path,
  3183. );
  3184. normalizedPage = await Page.findOneAndUpdate(
  3185. { _id: page._id },
  3186. { parent: parent._id },
  3187. { new: true },
  3188. );
  3189. }
  3190. // Update descendantCount
  3191. const inc = 1;
  3192. if (normalizedPage != null && normalizedPage.parent != null) {
  3193. await this.updateDescendantCountOfAncestors(
  3194. getIdForRef(normalizedPage.parent),
  3195. inc,
  3196. true,
  3197. );
  3198. }
  3199. return normalizedPage;
  3200. }
  3201. async normalizeParentRecursivelyByPages(pages, user): Promise<void> {
  3202. /*
  3203. * Main Operation
  3204. */
  3205. const socket = this.crowi.socketIoService.getDefaultSocket();
  3206. const pagesToNormalize = omitDuplicateAreaPageFromPages(pages);
  3207. let normalizablePages: PageDocument[];
  3208. let nonNormalizablePages: PageDocument[];
  3209. try {
  3210. [normalizablePages, nonNormalizablePages] =
  3211. await this.pageGrantService.separateNormalizableAndNotNormalizablePages(
  3212. user,
  3213. pagesToNormalize,
  3214. );
  3215. } catch (err) {
  3216. socket.emit(SocketEventName.PageMigrationError);
  3217. throw err;
  3218. }
  3219. if (normalizablePages.length === 0) {
  3220. socket.emit(SocketEventName.PageMigrationError);
  3221. return;
  3222. }
  3223. if (nonNormalizablePages.length !== 0) {
  3224. const nonNormalizablePagePaths: string[] = nonNormalizablePages.map(
  3225. (p) => p.path,
  3226. );
  3227. socket.emit(SocketEventName.PageMigrationError, {
  3228. paths: nonNormalizablePagePaths,
  3229. });
  3230. logger.debug(
  3231. 'Some pages could not be converted.',
  3232. nonNormalizablePagePaths,
  3233. );
  3234. }
  3235. /*
  3236. * Main Operation (s)
  3237. */
  3238. const errorPagePaths: string[] = [];
  3239. for await (const page of normalizablePages) {
  3240. const canOperate = await this.crowi.pageOperationService.canOperate(
  3241. true,
  3242. page.path,
  3243. page.path,
  3244. );
  3245. if (!canOperate) {
  3246. errorPagePaths.push(page.path);
  3247. throw Error(
  3248. `Cannot operate normalizeParentRecursiively to path "${page.path}" right now.`,
  3249. );
  3250. }
  3251. const Page = mongoose.model('Page') as unknown as PageModel;
  3252. const { PageQueryBuilder } = Page;
  3253. const builder = new PageQueryBuilder(Page.findOne());
  3254. builder.addConditionAsOnTree();
  3255. builder.addConditionToListByPathsArray([page.path]);
  3256. const existingPage = await builder.query.exec();
  3257. if (existingPage?.parent != null) {
  3258. errorPagePaths.push(page.path);
  3259. throw Error('This page has already converted.');
  3260. }
  3261. let pageOp: PageOperationDocument;
  3262. try {
  3263. pageOp = await PageOperation.create({
  3264. actionType: PageActionType.NormalizeParent,
  3265. actionStage: PageActionStage.Main,
  3266. page,
  3267. user,
  3268. fromPath: page.path,
  3269. toPath: page.path,
  3270. });
  3271. } catch (err) {
  3272. errorPagePaths.push(page.path);
  3273. logger.error('Failed to create PageOperation document.', err);
  3274. throw err;
  3275. }
  3276. try {
  3277. await this.normalizeParentRecursivelyMainOperation(
  3278. page,
  3279. user,
  3280. pageOp._id,
  3281. );
  3282. } catch (err) {
  3283. errorPagePaths.push(page.path);
  3284. logger.error(
  3285. 'Failed to run normalizeParentRecursivelyMainOperation.',
  3286. err,
  3287. );
  3288. // cleanup
  3289. await PageOperation.deleteOne({ _id: pageOp._id });
  3290. throw err;
  3291. }
  3292. }
  3293. if (errorPagePaths.length === 0) {
  3294. socket.emit(SocketEventName.PageMigrationSuccess);
  3295. } else {
  3296. socket.emit(SocketEventName.PageMigrationError, {
  3297. paths: errorPagePaths,
  3298. });
  3299. }
  3300. }
  3301. async normalizeParentRecursivelyMainOperation(
  3302. page,
  3303. user,
  3304. pageOpId: ObjectIdLike,
  3305. ): Promise<number> {
  3306. // Save prevDescendantCount for sub-operation
  3307. const Page = mongoose.model('Page') as unknown as PageModel;
  3308. const { PageQueryBuilder } = Page;
  3309. const builder = new PageQueryBuilder(Page.findOne(), true);
  3310. builder.addConditionAsOnTree();
  3311. builder.addConditionToListByPathsArray([page.path]);
  3312. const exPage = await builder.query.exec();
  3313. const options = { prevDescendantCount: exPage?.descendantCount ?? 0 };
  3314. let count: number;
  3315. try {
  3316. count = await this.normalizeParentRecursively([page.path], user);
  3317. } catch (err) {
  3318. logger.error('V5 initial miration failed.', err);
  3319. // socket.emit('normalizeParentRecursivelyByPageIds', { error: err.message }); TODO: use socket to tell user
  3320. throw err;
  3321. }
  3322. // Set to Sub
  3323. const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(
  3324. pageOpId,
  3325. PageActionStage.Sub,
  3326. );
  3327. if (pageOp == null) {
  3328. throw Error('PageOperation document not found');
  3329. }
  3330. await this.normalizeParentRecursivelySubOperation(
  3331. page,
  3332. user,
  3333. pageOp._id,
  3334. options,
  3335. );
  3336. return count;
  3337. }
  3338. async normalizeParentRecursivelySubOperation(
  3339. page,
  3340. user,
  3341. pageOpId: ObjectIdLike,
  3342. options: { prevDescendantCount: number },
  3343. ): Promise<void> {
  3344. const Page = mongoose.model('Page') as unknown as PageModel;
  3345. try {
  3346. // update descendantCount of self and descendant pages first
  3347. await this.updateDescendantCountOfSelfAndDescendants(page.path);
  3348. // find pages again to get updated descendantCount
  3349. // then calculate inc
  3350. const pageAfterUpdatingDescendantCount = await Page.findByIdAndViewer(
  3351. page._id,
  3352. user,
  3353. );
  3354. if (pageAfterUpdatingDescendantCount == null) {
  3355. throw Error('Page not found after updating descendantCount');
  3356. }
  3357. const { prevDescendantCount } = options;
  3358. const newDescendantCount =
  3359. pageAfterUpdatingDescendantCount.descendantCount;
  3360. let inc = newDescendantCount - prevDescendantCount;
  3361. const isAlreadyConverted = page.parent != null;
  3362. if (!isAlreadyConverted) {
  3363. inc += 1;
  3364. }
  3365. await this.updateDescendantCountOfAncestors(page._id, inc, false);
  3366. } catch (err) {
  3367. logger.error(
  3368. 'Failed to update descendantCount after normalizing parent:',
  3369. err,
  3370. );
  3371. throw Error(
  3372. `Failed to update descendantCount after normalizing parent: ${err}`,
  3373. );
  3374. }
  3375. await PageOperation.findByIdAndDelete(pageOpId);
  3376. }
  3377. async _isPagePathIndexUnique() {
  3378. const Page = mongoose.model('Page') as unknown as PageModel;
  3379. const now = new Date().toString();
  3380. const path = `growi_check_is_path_index_unique_${now}`;
  3381. let isUnique = false;
  3382. try {
  3383. await Page.insertMany([{ path }, { path }]);
  3384. } catch (err) {
  3385. if (err?.code === 11000) {
  3386. // Error code 11000 indicates the index is unique
  3387. isUnique = true;
  3388. logger.info('Page path index is unique.');
  3389. } else {
  3390. throw err;
  3391. }
  3392. } finally {
  3393. await Page.deleteMany({
  3394. path: { $regex: /growi_check_is_path_index_unique/g },
  3395. });
  3396. }
  3397. return isUnique;
  3398. }
  3399. async normalizeAllPublicPages(): Promise<void> {
  3400. let isUnique: boolean;
  3401. try {
  3402. isUnique = await this._isPagePathIndexUnique();
  3403. } catch (err) {
  3404. logger.error('Failed to check path index status', err);
  3405. throw err;
  3406. }
  3407. // drop unique index first
  3408. if (isUnique) {
  3409. try {
  3410. await this._v5NormalizeIndex();
  3411. } catch (err) {
  3412. logger.error('V5 index normalization failed.', err);
  3413. throw err;
  3414. }
  3415. }
  3416. // then migrate
  3417. try {
  3418. await this.normalizeParentRecursively(['/'], null, false, true);
  3419. } catch (err) {
  3420. logger.error('V5 initial miration failed.', err);
  3421. throw err;
  3422. }
  3423. // update descendantCount of all public pages
  3424. try {
  3425. await this.updateDescendantCountOfSelfAndDescendants('/');
  3426. logger.info('Successfully updated all descendantCount of public pages.');
  3427. } catch (err) {
  3428. logger.error('Failed updating descendantCount of public pages.', err);
  3429. throw err;
  3430. }
  3431. await this._setIsV5CompatibleTrue();
  3432. }
  3433. private async _setIsV5CompatibleTrue() {
  3434. try {
  3435. await configManager.updateConfig('app:isV5Compatible', true);
  3436. logger.info('Successfully migrated all public pages.');
  3437. } catch (err) {
  3438. logger.warn('Failed to update app:isV5Compatible to true.');
  3439. throw err;
  3440. }
  3441. }
  3442. private async normalizeParentAndDescendantCountOfDescendants(
  3443. path: string,
  3444. user,
  3445. isDuplicateOperation = false,
  3446. ): Promise<void> {
  3447. await this.normalizeParentRecursively([path], user, isDuplicateOperation);
  3448. // update descendantCount of descendant pages
  3449. await this.updateDescendantCountOfSelfAndDescendants(path);
  3450. }
  3451. /**
  3452. * Normalize parent attribute by passing paths and user.
  3453. * @param paths Pages under this paths value will be updated.
  3454. * @param user To be used to filter pages to update. If null, only public pages will be updated.
  3455. * @returns Promise<void>
  3456. */
  3457. async normalizeParentRecursively(
  3458. paths: string[],
  3459. user: any | null,
  3460. isDuplicateOperation = false,
  3461. shouldEmitProgress = false,
  3462. ): Promise<number> {
  3463. const Page = mongoose.model('Page') as unknown as PageModel;
  3464. const ancestorPaths = paths.flatMap((p) => collectAncestorPaths(p, []));
  3465. // targets' descendants
  3466. const pathAndRegExpsToNormalize: (RegExp | string)[] = paths.map(
  3467. (p) => new RegExp(`^${escapeStringRegexp(addTrailingSlash(p))}`, 'i'),
  3468. );
  3469. // include targets' path
  3470. pathAndRegExpsToNormalize.push(...paths);
  3471. // determine UserGroup condition
  3472. const userGroups =
  3473. user != null
  3474. ? [
  3475. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  3476. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
  3477. user,
  3478. )),
  3479. ]
  3480. : null;
  3481. const grantFiltersByUser: { $or: any[] } | null = !isDuplicateOperation
  3482. ? Page.generateGrantCondition(user, userGroups)
  3483. : null;
  3484. return this._normalizeParentRecursively(
  3485. pathAndRegExpsToNormalize,
  3486. ancestorPaths,
  3487. user,
  3488. grantFiltersByUser,
  3489. shouldEmitProgress,
  3490. );
  3491. }
  3492. private buildFilterForNormalizeParentRecursively(
  3493. pathOrRegExps: (RegExp | string)[],
  3494. publicPathsToNormalize: string[],
  3495. grantFiltersByUser?: { $or: any[] } | null,
  3496. ) {
  3497. const Page = mongoose.model('Page') as unknown as PageModel;
  3498. const andFilter: any = {
  3499. $and: [
  3500. {
  3501. parent: null,
  3502. status: Page.STATUS_PUBLISHED,
  3503. path: { $ne: '/' },
  3504. },
  3505. ],
  3506. };
  3507. const orFilter: any = { $or: [] };
  3508. // specified pathOrRegExps
  3509. if (pathOrRegExps.length > 0) {
  3510. orFilter.$or.push({
  3511. path: { $in: pathOrRegExps },
  3512. });
  3513. }
  3514. // not specified but ancestors of specified pathOrRegExps
  3515. if (publicPathsToNormalize.length > 0) {
  3516. orFilter.$or.push({
  3517. path: { $in: publicPathsToNormalize },
  3518. grant: Page.GRANT_PUBLIC, // use only public pages to complete the tree
  3519. });
  3520. }
  3521. // Merge filters
  3522. const mergedFilter = {
  3523. $and: [
  3524. {
  3525. $and:
  3526. grantFiltersByUser != null
  3527. ? [grantFiltersByUser, ...andFilter.$and]
  3528. : [...andFilter.$and],
  3529. },
  3530. { $or: orFilter.$or },
  3531. ],
  3532. };
  3533. return mergedFilter;
  3534. }
  3535. private async _normalizeParentRecursively(
  3536. pathOrRegExps: (RegExp | string)[],
  3537. publicPathsToNormalize: string[],
  3538. user,
  3539. grantFiltersByUser: { $or: any[] } | null,
  3540. shouldEmitProgress = false,
  3541. count = 0,
  3542. skiped = 0,
  3543. isFirst = true,
  3544. ): Promise<number> {
  3545. const BATCH_SIZE = 100;
  3546. const PAGES_LIMIT = 1000;
  3547. const socket = shouldEmitProgress
  3548. ? this.crowi.socketIoService.getAdminSocket()
  3549. : null;
  3550. const Page = mongoose.model('Page') as unknown as PageModel;
  3551. const { PageQueryBuilder } = Page;
  3552. // Build filter
  3553. const matchFilter = this.buildFilterForNormalizeParentRecursively(
  3554. pathOrRegExps,
  3555. publicPathsToNormalize,
  3556. grantFiltersByUser,
  3557. );
  3558. let baseAggregation = Page.aggregate([
  3559. { $match: matchFilter },
  3560. {
  3561. $project: {
  3562. // minimize data to fetch
  3563. _id: 1,
  3564. path: 1,
  3565. },
  3566. },
  3567. ]);
  3568. // Limit pages to get
  3569. const total = await Page.countDocuments(matchFilter);
  3570. if (isFirst) {
  3571. socket?.emit(SocketEventName.PMStarted, { total });
  3572. }
  3573. if (total > PAGES_LIMIT) {
  3574. baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
  3575. }
  3576. const pagesStream = await baseAggregation.cursor({ batchSize: BATCH_SIZE });
  3577. const batchStream = createBatchStream(BATCH_SIZE);
  3578. let shouldContinue = true;
  3579. let nextCount = count;
  3580. let nextSkiped = skiped;
  3581. const buildPipelineToCreateEmptyPagesByUser =
  3582. this.buildPipelineToCreateEmptyPagesByUser.bind(this);
  3583. const migratePagesStream = new Writable({
  3584. objectMode: true,
  3585. async write(pages, encoding, callback) {
  3586. const parentPaths = Array.from(
  3587. new Set<string>(pages.map((p) => pathlib.dirname(p.path))),
  3588. );
  3589. // 1. Remove unnecessary empty pages & reset parent for pages which had had those empty pages
  3590. const pageIdsToNotDelete = pages.map((p) => p._id);
  3591. const emptyPagePathsToDelete = pages.map((p) => p.path);
  3592. const builder1 = new PageQueryBuilder(
  3593. Page.find({ isEmpty: true }, { _id: 1 }),
  3594. true,
  3595. );
  3596. builder1.addConditionToListByPathsArray(emptyPagePathsToDelete);
  3597. builder1.addConditionToExcludeByPageIdsArray(pageIdsToNotDelete);
  3598. const emptyPagesToDelete = await builder1.query.lean().exec();
  3599. const resetParentOperations = emptyPagesToDelete.map((p) => {
  3600. return {
  3601. updateOne: {
  3602. filter: {
  3603. parent: p._id,
  3604. },
  3605. update: {
  3606. parent: null,
  3607. },
  3608. },
  3609. };
  3610. });
  3611. await Page.bulkWrite(resetParentOperations);
  3612. await Page.removeEmptyPages(pageIdsToNotDelete, emptyPagePathsToDelete);
  3613. // 2. Create lacking parents as empty pages
  3614. const orFilters = [
  3615. { path: '/' },
  3616. {
  3617. path: { $in: publicPathsToNormalize },
  3618. grant: Page.GRANT_PUBLIC,
  3619. status: Page.STATUS_PUBLISHED,
  3620. },
  3621. {
  3622. path: { $in: publicPathsToNormalize },
  3623. parent: { $ne: null },
  3624. status: Page.STATUS_PUBLISHED,
  3625. },
  3626. {
  3627. path: { $nin: publicPathsToNormalize },
  3628. status: Page.STATUS_PUBLISHED,
  3629. },
  3630. ];
  3631. const filterForApplicableAncestors = { $or: orFilters };
  3632. const aggregationPipeline = await buildPipelineToCreateEmptyPagesByUser(
  3633. user,
  3634. parentPaths,
  3635. false,
  3636. filterForApplicableAncestors,
  3637. );
  3638. await Page.createEmptyPagesByPaths(parentPaths, aggregationPipeline);
  3639. // 3. Find parents
  3640. const builder2 = new PageQueryBuilder(Page.find(), true);
  3641. if (grantFiltersByUser != null) {
  3642. builder2.query = builder2.query.and(grantFiltersByUser);
  3643. }
  3644. const parents = await builder2
  3645. .addConditionToListByPathsArray(parentPaths)
  3646. .addConditionToFilterByApplicableAncestors(publicPathsToNormalize)
  3647. .query.lean()
  3648. .exec();
  3649. // Normalize all siblings for each page
  3650. const updateManyOperations = parents.map((parent) => {
  3651. const parentId = parent._id;
  3652. // Build filter
  3653. const parentPathEscaped = escapeStringRegexp(
  3654. parent.path === '/' ? '' : parent.path,
  3655. ); // adjust the path for RegExp
  3656. const filter: any = {
  3657. $and: [
  3658. {
  3659. path: {
  3660. $regex: new RegExp(
  3661. `^${parentPathEscaped}(\\/[^/]+)\\/?$`,
  3662. 'i',
  3663. ),
  3664. }, // see: regexr.com/6889f (e.g. /parent/any_child or /any_level1)
  3665. },
  3666. {
  3667. path: { $in: pathOrRegExps.concat(publicPathsToNormalize) },
  3668. },
  3669. filterForApplicableAncestors,
  3670. ],
  3671. };
  3672. if (grantFiltersByUser != null) {
  3673. filter.$and.push(grantFiltersByUser);
  3674. }
  3675. return {
  3676. updateMany: {
  3677. filter,
  3678. update: {
  3679. parent: parentId,
  3680. },
  3681. },
  3682. };
  3683. });
  3684. try {
  3685. const res = await Page.bulkWrite(updateManyOperations);
  3686. nextCount += res.result.nModified;
  3687. nextSkiped += res.result.writeErrors.length;
  3688. logger.info(
  3689. `Page migration processing: (migratedPages=${res.result.nModified})`,
  3690. );
  3691. socket?.emit(SocketEventName.PMMigrating, { count: nextCount });
  3692. socket?.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
  3693. // Throw if any error is found
  3694. if (res.result.writeErrors.length > 0) {
  3695. logger.error(
  3696. 'Failed to migrate some pages',
  3697. res.result.writeErrors,
  3698. );
  3699. socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
  3700. throw Error('Failed to migrate some pages');
  3701. }
  3702. // Finish migration if no modification occurred
  3703. if (res.result.nModified === 0 && res.result.nMatched === 0) {
  3704. shouldContinue = false;
  3705. logger.error(
  3706. 'Migration is unable to continue',
  3707. 'parentPaths:',
  3708. parentPaths,
  3709. 'bulkWriteResult:',
  3710. res,
  3711. );
  3712. socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
  3713. }
  3714. } catch (err) {
  3715. logger.error('Failed to update page.parent.', err);
  3716. throw err;
  3717. }
  3718. callback();
  3719. },
  3720. final(callback) {
  3721. callback();
  3722. },
  3723. });
  3724. await pipeline(pagesStream, batchStream, migratePagesStream);
  3725. if ((await Page.exists(matchFilter)) && shouldContinue) {
  3726. return this._normalizeParentRecursively(
  3727. pathOrRegExps,
  3728. publicPathsToNormalize,
  3729. user,
  3730. grantFiltersByUser,
  3731. shouldEmitProgress,
  3732. nextCount,
  3733. nextSkiped,
  3734. false,
  3735. );
  3736. }
  3737. // End
  3738. socket?.emit(SocketEventName.PMEnded, { isSucceeded: true });
  3739. return nextCount;
  3740. }
  3741. private async _v5NormalizeIndex() {
  3742. const collection = mongoose.connection.collection('pages');
  3743. try {
  3744. // drop pages.path_1 indexes
  3745. await collection.dropIndex('path_1');
  3746. logger.info('Succeeded to drop unique indexes from pages.path.');
  3747. } catch (err) {
  3748. logger.warn('Failed to drop unique indexes from pages.path.', err);
  3749. throw err;
  3750. }
  3751. try {
  3752. // create indexes without
  3753. await collection.createIndex({ path: 1 }, { unique: false });
  3754. logger.info('Succeeded to create non-unique indexes on pages.path.');
  3755. } catch (err) {
  3756. logger.warn('Failed to create non-unique indexes on pages.path.', err);
  3757. throw err;
  3758. }
  3759. }
  3760. async countPagesCanNormalizeParentByUser(user): Promise<number> {
  3761. if (user == null) {
  3762. throw Error('user is required');
  3763. }
  3764. const Page = mongoose.model('Page') as unknown as PageModel;
  3765. const { PageQueryBuilder } = Page;
  3766. const builder = new PageQueryBuilder(Page.count(), false);
  3767. await builder.addConditionAsMigratablePages(user);
  3768. const nMigratablePages = await builder.query.exec();
  3769. return nMigratablePages;
  3770. }
  3771. /**
  3772. * update descendantCount of the following pages
  3773. * - page that has the same path as the provided path
  3774. * - pages that are descendants of the above page
  3775. */
  3776. async updateDescendantCountOfSelfAndDescendants(path: string): Promise<void> {
  3777. const BATCH_SIZE = 200;
  3778. const Page = mongoose.model<IPage, PageModel>('Page');
  3779. const { PageQueryBuilder } = Page;
  3780. const builder = new PageQueryBuilder(Page.find(), true);
  3781. builder.addConditionAsOnTree();
  3782. builder.addConditionToListWithDescendants(path);
  3783. builder.addConditionToSortPagesByDescPath();
  3784. const aggregatedPages = await builder.query
  3785. .lean()
  3786. .cursor({ batchSize: BATCH_SIZE });
  3787. await this.recountAndUpdateDescendantCountOfPages(
  3788. aggregatedPages,
  3789. BATCH_SIZE,
  3790. );
  3791. }
  3792. /**
  3793. * update descendantCount of the pages sequentially from longer path to shorter path
  3794. */
  3795. async updateDescendantCountOfPagesWithPaths(paths: string[]): Promise<void> {
  3796. const BATCH_SIZE = 200;
  3797. const Page = mongoose.model<IPage, PageModel>('Page');
  3798. const { PageQueryBuilder } = Page;
  3799. const builder = new PageQueryBuilder(Page.find(), true);
  3800. builder.addConditionToListByPathsArray(paths); // find by paths
  3801. builder.addConditionToSortPagesByDescPath(); // sort in DESC
  3802. const aggregatedPages = await builder.query
  3803. .lean()
  3804. .cursor({ batchSize: BATCH_SIZE });
  3805. await this.recountAndUpdateDescendantCountOfPages(
  3806. aggregatedPages,
  3807. BATCH_SIZE,
  3808. );
  3809. }
  3810. /**
  3811. * Recount descendantCount of pages one by one
  3812. */
  3813. async recountAndUpdateDescendantCountOfPages(
  3814. pageCursor: Cursor<any>,
  3815. batchSize: number,
  3816. ): Promise<void> {
  3817. const Page = mongoose.model<IPage, PageModel>('Page');
  3818. const batchStream = createBatchStream(batchSize);
  3819. const recountWriteStream = new Writable({
  3820. objectMode: true,
  3821. async write(pageDocuments, encoding, callback) {
  3822. for await (const document of pageDocuments) {
  3823. const descendantCount = await Page.recountDescendantCount(
  3824. document._id,
  3825. );
  3826. await Page.findByIdAndUpdate(document._id, { descendantCount });
  3827. }
  3828. callback();
  3829. },
  3830. final(callback) {
  3831. callback();
  3832. },
  3833. });
  3834. await pipeline(pageCursor, batchStream, recountWriteStream);
  3835. }
  3836. // update descendantCount of all pages that are ancestors of a provided pageId by count
  3837. async updateDescendantCountOfAncestors(
  3838. pageId: ObjectIdLike,
  3839. inc: number,
  3840. shouldIncludeTarget: boolean,
  3841. ): Promise<void> {
  3842. const Page = mongoose.model<IPage, PageModel>('Page');
  3843. const ancestors = await Page.findAncestorsUsingParentRecursively(
  3844. pageId,
  3845. shouldIncludeTarget,
  3846. );
  3847. const ancestorPageIds = ancestors.map((p) => p._id);
  3848. await Page.incrementDescendantCountOfPageIds(ancestorPageIds, inc);
  3849. const updateDescCountData: UpdateDescCountRawData = Object.fromEntries(
  3850. ancestors.map((p) => [p._id.toString(), p.descendantCount + inc]),
  3851. );
  3852. this.emitUpdateDescCount(updateDescCountData);
  3853. }
  3854. private emitUpdateDescCount(data: UpdateDescCountRawData): void {
  3855. const socket = this.crowi.socketIoService.getDefaultSocket();
  3856. socket.emit(SocketEventName.UpdateDescCount, data);
  3857. }
  3858. /**
  3859. * Build the base aggregation pipeline for fillAncestors--- methods
  3860. * @param onlyMigratedAsExistingPages Determine whether to include non-migrated pages as existing pages. If a page exists,
  3861. * an empty page will not be created at that page's path.
  3862. */
  3863. private buildBasePipelineToCreateEmptyPages(
  3864. paths: string[],
  3865. onlyMigratedAsExistingPages = true,
  3866. andFilter?,
  3867. ): any[] {
  3868. const aggregationPipeline: any[] = [];
  3869. const Page = mongoose.model('Page') as unknown as PageModel;
  3870. // -- Filter by paths
  3871. aggregationPipeline.push({ $match: { path: { $in: paths } } });
  3872. // -- Normalized condition
  3873. if (onlyMigratedAsExistingPages) {
  3874. aggregationPipeline.push({
  3875. $match: {
  3876. $or: [
  3877. { grant: Page.GRANT_PUBLIC },
  3878. { parent: { $ne: null } },
  3879. { path: '/' },
  3880. ],
  3881. },
  3882. });
  3883. }
  3884. // -- Add custom pipeline
  3885. if (andFilter != null) {
  3886. aggregationPipeline.push({ $match: andFilter });
  3887. }
  3888. return aggregationPipeline;
  3889. }
  3890. private async buildPipelineToCreateEmptyPagesByUser(
  3891. user,
  3892. paths: string[],
  3893. onlyMigratedAsExistingPages = true,
  3894. andFilter?,
  3895. ): Promise<any[]> {
  3896. const Page = mongoose.model('Page') as unknown as PageModel;
  3897. const pipeline = this.buildBasePipelineToCreateEmptyPages(
  3898. paths,
  3899. onlyMigratedAsExistingPages,
  3900. andFilter,
  3901. );
  3902. const userGroups =
  3903. user != null
  3904. ? [
  3905. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
  3906. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
  3907. user,
  3908. )),
  3909. ]
  3910. : null;
  3911. const grantCondition = Page.generateGrantCondition(user, userGroups);
  3912. pipeline.push({ $match: grantCondition });
  3913. return pipeline;
  3914. }
  3915. private buildPipelineToCreateEmptyPagesBySystem(paths: string[]): any[] {
  3916. return this.buildBasePipelineToCreateEmptyPages(paths);
  3917. }
  3918. private async connectPageTree(path: string): Promise<void> {
  3919. const Page = mongoose.model('Page') as unknown as PageModel;
  3920. const { PageQueryBuilder } = Page;
  3921. const ancestorPaths = collectAncestorPaths(path);
  3922. // Find ancestors
  3923. const builder = new PageQueryBuilder(Page.find(), true);
  3924. builder.addConditionToFilterByApplicableAncestors(ancestorPaths); // avoid including not normalized pages
  3925. const ancestors = await builder
  3926. .addConditionToListByPathsArray(ancestorPaths)
  3927. .addConditionToSortPagesByDescPath()
  3928. .query.exec();
  3929. // Update parent attrs
  3930. const ancestorsMap = new Map(); // Map<path, page>
  3931. for (const ancestor of ancestors) {
  3932. if (!ancestorsMap.has(ancestor.path)) {
  3933. ancestorsMap.set(ancestor.path, ancestor); // the earlier element should be the true ancestor
  3934. }
  3935. }
  3936. const nonRootAncestors = ancestors.filter((page) => !isTopPage(page.path));
  3937. const operations = nonRootAncestors.map((page) => {
  3938. const parentPath = pathlib.dirname(page.path);
  3939. return {
  3940. updateOne: {
  3941. filter: {
  3942. _id: page._id,
  3943. },
  3944. update: {
  3945. parent: ancestorsMap.get(parentPath)._id,
  3946. },
  3947. },
  3948. };
  3949. });
  3950. await Page.bulkWrite(operations);
  3951. }
  3952. /**
  3953. * Find parent or create parent if not exists.
  3954. * It also updates parent of ancestors
  3955. * @param path string
  3956. * @returns Promise<PageDocument>
  3957. */
  3958. async getParentAndFillAncestorsByUser(
  3959. user,
  3960. path: string,
  3961. ): Promise<HydratedDocument<PageDocument>> {
  3962. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  3963. 'Page',
  3964. );
  3965. // Find parent
  3966. const parent = await Page.findParentByPath(path);
  3967. if (parent != null) {
  3968. return parent;
  3969. }
  3970. const ancestorPaths = collectAncestorPaths(path);
  3971. // Fill ancestors
  3972. const aggregationPipeline: any[] =
  3973. await this.buildPipelineToCreateEmptyPagesByUser(user, ancestorPaths);
  3974. await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
  3975. // Connect ancestors
  3976. await this.connectPageTree(path);
  3977. // Return the created parent
  3978. const createdParent = await Page.findParentByPath(path);
  3979. if (createdParent == null) {
  3980. throw Error(
  3981. 'Failed to find the created parent by getParentAndFillAncestorsByUser',
  3982. );
  3983. }
  3984. return createdParent;
  3985. }
  3986. async getParentAndFillAncestorsBySystem(
  3987. path: string,
  3988. ): Promise<HydratedDocument<PageDocument>> {
  3989. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  3990. 'Page',
  3991. );
  3992. // Find parent
  3993. const parent = await Page.findParentByPath(path);
  3994. if (parent != null) {
  3995. return parent;
  3996. }
  3997. // Fill ancestors
  3998. const ancestorPaths = collectAncestorPaths(path);
  3999. const aggregationPipeline: any[] =
  4000. this.buildPipelineToCreateEmptyPagesBySystem(ancestorPaths);
  4001. await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
  4002. // Connect ancestors
  4003. await this.connectPageTree(path);
  4004. // Return the created parent
  4005. const createdParent = await Page.findParentByPath(path);
  4006. if (createdParent == null) {
  4007. throw Error(
  4008. 'Failed to find the created parent by getParentAndFillAncestorsByUser',
  4009. );
  4010. }
  4011. return createdParent;
  4012. }
  4013. // --------- Create ---------
  4014. private async preparePageDocumentToCreate(
  4015. path: string,
  4016. shouldNew: boolean,
  4017. ): Promise<HydratedDocument<PageDocument>> {
  4018. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  4019. 'Page',
  4020. );
  4021. const emptyPage = await Page.findOne({ path, isEmpty: true });
  4022. // Use empty page if exists, if not, create a new page
  4023. let page: HydratedDocument<PageDocument>;
  4024. if (shouldNew) {
  4025. page = new Page();
  4026. } else if (emptyPage != null) {
  4027. page = emptyPage;
  4028. const descendantCount = await Page.recountDescendantCount(page._id);
  4029. page.descendantCount = descendantCount;
  4030. page.isEmpty = false;
  4031. } else {
  4032. page = new Page();
  4033. }
  4034. return page;
  4035. }
  4036. private setFieldExceptForGrantRevisionParent(
  4037. pageDocument: PageDocument,
  4038. path: string,
  4039. user?,
  4040. ): void {
  4041. const Page = mongoose.model('Page') as unknown as PageModel;
  4042. pageDocument.path = path;
  4043. pageDocument.creator = user;
  4044. pageDocument.lastUpdateUser = user;
  4045. pageDocument.status = Page.STATUS_PUBLISHED;
  4046. }
  4047. private async validateAppliedScope(
  4048. user,
  4049. grant,
  4050. grantUserGroupIds: IGrantedGroup[],
  4051. ) {
  4052. if (grant === PageGrant.GRANT_USER_GROUP && grantUserGroupIds == null) {
  4053. throw new Error('grantUserGroupIds is not specified');
  4054. }
  4055. if (grant === PageGrant.GRANT_USER_GROUP) {
  4056. const {
  4057. grantedUserGroups: grantedUserGroupIds,
  4058. grantedExternalUserGroups: grantedExternalUserGroupIds,
  4059. } = divideByType(grantUserGroupIds);
  4060. const count =
  4061. (await UserGroupRelation.countByGroupIdsAndUser(
  4062. grantedUserGroupIds,
  4063. user,
  4064. )) +
  4065. (await ExternalUserGroupRelation.countByGroupIdsAndUser(
  4066. grantedExternalUserGroupIds,
  4067. user,
  4068. ));
  4069. if (count === 0) {
  4070. throw new Error('no relations were exist for group and user.');
  4071. }
  4072. }
  4073. }
  4074. private async canProcessCreate(
  4075. path: string,
  4076. grantData: {
  4077. grant?: PageGrant;
  4078. grantUserIds?: ObjectIdLike[];
  4079. grantUserGroupIds?: IGrantedGroup[];
  4080. },
  4081. shouldValidateGrant: boolean,
  4082. user?,
  4083. options?: IOptionsForCreate,
  4084. ): Promise<boolean> {
  4085. const Page = mongoose.model('Page') as unknown as PageModel;
  4086. // Operatability validation
  4087. const canOperate = await this.crowi.pageOperationService.canOperate(
  4088. false,
  4089. null,
  4090. path,
  4091. );
  4092. if (!canOperate) {
  4093. logger.error(`Cannot operate create to path "${path}" right now.`);
  4094. return false;
  4095. }
  4096. // Existance validation
  4097. const isExist = (await Page.count({ path, isEmpty: false })) > 0; // not validate empty page
  4098. if (isExist) {
  4099. logger.error('Cannot create new page to existed path');
  4100. return false;
  4101. }
  4102. // UserGroup & Owner validation
  4103. const { grant, grantUserIds, grantUserGroupIds } = grantData;
  4104. if (shouldValidateGrant) {
  4105. if (user == null) {
  4106. throw Error('user is required to validate grant');
  4107. }
  4108. let isGrantNormalized = false;
  4109. try {
  4110. // It must check descendants as well if emptyTarget is not null
  4111. const isEmptyPageAlreadyExist =
  4112. (await Page.count({ path, isEmpty: true })) > 0;
  4113. const shouldCheckDescendants =
  4114. isEmptyPageAlreadyExist && !options?.overwriteScopesOfDescendants;
  4115. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  4116. user,
  4117. path,
  4118. grant,
  4119. grantUserIds,
  4120. grantUserGroupIds,
  4121. shouldCheckDescendants,
  4122. );
  4123. } catch (err) {
  4124. logger.error(
  4125. `Failed to validate grant of page at "${path}" of grant ${grant}:`,
  4126. err,
  4127. );
  4128. throw err;
  4129. }
  4130. if (!isGrantNormalized) {
  4131. throw Error(
  4132. 'The selected grant or grantedGroup is not assignable to this page.',
  4133. );
  4134. }
  4135. if (options?.overwriteScopesOfDescendants) {
  4136. const updateGrantInfo =
  4137. await this.pageGrantService.generateUpdateGrantInfoToOverwriteDescendants(
  4138. user,
  4139. grant,
  4140. options.grantUserGroupIds,
  4141. );
  4142. const canOverwriteDescendants =
  4143. await this.pageGrantService.canOverwriteDescendants(
  4144. path,
  4145. user,
  4146. updateGrantInfo,
  4147. );
  4148. if (!canOverwriteDescendants) {
  4149. throw Error('Cannot overwrite scopes of descendants.');
  4150. }
  4151. }
  4152. }
  4153. return true;
  4154. }
  4155. /**
  4156. * Create a page
  4157. * Set options.isSynchronously to true to await all process when you want to run this method multiple times at short intervals.
  4158. */
  4159. async create(
  4160. _path: string,
  4161. body: string,
  4162. user: HasObjectId,
  4163. options: IOptionsForCreate = {},
  4164. ): Promise<HydratedDocument<PageDocument>> {
  4165. // Switch method
  4166. const isV5Compatible = configManager.getConfig('app:isV5Compatible');
  4167. if (!isV5Compatible) {
  4168. return this.createV4(_path, body, user, options);
  4169. }
  4170. // Values
  4171. const path: string = generalXssFilter.process(_path); // sanitize path
  4172. // Retrieve closest ancestor document
  4173. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  4174. 'Page',
  4175. );
  4176. const closestAncestor = await Page.findNonEmptyClosestAncestor(path);
  4177. // Determine grantData
  4178. const grant =
  4179. options.grant ?? closestAncestor?.grant ?? PageGrant.GRANT_PUBLIC;
  4180. const grantUserIds =
  4181. grant === PageGrant.GRANT_OWNER ? [user._id] : undefined;
  4182. const getGrantedGroupsFromClosestAncestor = async () => {
  4183. if (closestAncestor == null) return undefined;
  4184. if (options.onlyInheritUserRelatedGrantedGroups) {
  4185. return this.pageGrantService.getUserRelatedGrantedGroups(
  4186. closestAncestor,
  4187. user,
  4188. );
  4189. }
  4190. return closestAncestor.grantedGroups;
  4191. };
  4192. const grantUserGroupIds =
  4193. options.grantUserGroupIds ??
  4194. (await getGrantedGroupsFromClosestAncestor());
  4195. const grantData = {
  4196. grant,
  4197. grantUserIds,
  4198. grantUserGroupIds,
  4199. };
  4200. const isGrantRestricted = grant === PageGrant.GRANT_RESTRICTED;
  4201. // Validate
  4202. const shouldValidateGrant = !isGrantRestricted;
  4203. const canProcessCreate = await this.canProcessCreate(
  4204. path,
  4205. grantData,
  4206. shouldValidateGrant,
  4207. user,
  4208. options,
  4209. );
  4210. if (!canProcessCreate) {
  4211. throw Error('Cannot process create');
  4212. }
  4213. // Prepare a page document
  4214. const shouldNew = isGrantRestricted;
  4215. const page = await this.preparePageDocumentToCreate(path, shouldNew);
  4216. // Set field
  4217. this.setFieldExceptForGrantRevisionParent(page, path, user);
  4218. // Apply scope
  4219. page.applyScope(user, grant, grantUserGroupIds);
  4220. // Set parent
  4221. if (isTopPage(path) || isGrantRestricted) {
  4222. // set parent to null when GRANT_RESTRICTED
  4223. page.parent = null;
  4224. } else {
  4225. const parent = await this.getParentAndFillAncestorsByUser(user, path);
  4226. page.parent = parent._id;
  4227. }
  4228. // Make WIP
  4229. if (options.wip) {
  4230. const hasChildren = await Page.exists({ parent: page._id });
  4231. page.makeWip(hasChildren != null); // disableTtl = hasChildren != null
  4232. }
  4233. // Save
  4234. let savedPage = await page.save();
  4235. // Create revision
  4236. const newRevision = Revision.prepareRevision(
  4237. savedPage,
  4238. body,
  4239. null,
  4240. user,
  4241. options.origin,
  4242. );
  4243. savedPage = await pushRevision(savedPage, newRevision, user);
  4244. await savedPage.populateDataToShowRevision();
  4245. // Emit create event
  4246. this.pageEvent.emit('create', savedPage, user);
  4247. // Directly run sub operation for now since it might be complex to handle main operation for creating pages -- Taichi Masuyama 2022.11.08
  4248. let pageOp: PageOperationDocument;
  4249. try {
  4250. pageOp = await PageOperation.create({
  4251. actionType: PageActionType.Create,
  4252. actionStage: PageActionStage.Sub,
  4253. page: savedPage,
  4254. user,
  4255. fromPath: path,
  4256. options,
  4257. });
  4258. } catch (err) {
  4259. logger.error('Failed to create PageOperation document.', err);
  4260. throw err;
  4261. }
  4262. this.createSubOperation(savedPage, user, options, pageOp._id);
  4263. return savedPage;
  4264. }
  4265. /**
  4266. * Used to run sub operation in create method
  4267. */
  4268. async createSubOperation(
  4269. page,
  4270. user,
  4271. options: IOptionsForCreate,
  4272. pageOpId: ObjectIdLike,
  4273. ): Promise<void> {
  4274. await this.disableAncestorPagesTtl(page.path);
  4275. // Update descendantCount
  4276. await this.updateDescendantCountOfAncestors(page._id, 1, false);
  4277. // Delete PageRedirect if exists
  4278. try {
  4279. await PageRedirect.deleteOne({ fromPath: page.path });
  4280. logger.warn(
  4281. `Deleted page redirect after creating a new page at path "${page.path}".`,
  4282. );
  4283. } catch (err) {
  4284. // no throw
  4285. logger.error('Failed to delete PageRedirect');
  4286. }
  4287. // update scopes for descendants
  4288. if (options.overwriteScopesOfDescendants) {
  4289. await this.applyScopesToDescendantsWithStream(page, user);
  4290. }
  4291. await PageOperation.findByIdAndDelete(pageOpId);
  4292. }
  4293. /**
  4294. * V4 compatible create method
  4295. */
  4296. private async createV4(path, body, user, options: any = {}) {
  4297. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  4298. 'Page',
  4299. );
  4300. const format = options.format || 'markdown';
  4301. const grantUserGroupIds = options.grantUserGroupIds || null;
  4302. const expandContentWidth = configManager.getConfig(
  4303. 'customize:isContainerFluid',
  4304. );
  4305. // sanitize path
  4306. const pathSanitized = generalXssFilter.process(path);
  4307. let grant = options.grant;
  4308. // force public
  4309. if (isTopPage(pathSanitized)) {
  4310. grant = PageGrant.GRANT_PUBLIC;
  4311. }
  4312. const isExist = await Page.count({ path: pathSanitized });
  4313. if (isExist) {
  4314. throw new Error('Cannot create new page to existed path');
  4315. }
  4316. const page = new Page();
  4317. page.path = pathSanitized;
  4318. page.creator = user;
  4319. page.lastUpdateUser = user;
  4320. page.status = PageStatus.STATUS_PUBLISHED;
  4321. if (expandContentWidth != null) {
  4322. page.expandContentWidth = expandContentWidth;
  4323. }
  4324. await this.validateAppliedScope(user, grant, grantUserGroupIds);
  4325. page.applyScope(user, grant, grantUserGroupIds);
  4326. let savedPage = await page.save();
  4327. const newRevision = Revision.prepareRevision(
  4328. savedPage,
  4329. body,
  4330. null,
  4331. user,
  4332. undefined,
  4333. { format },
  4334. );
  4335. savedPage = await pushRevision(savedPage, newRevision, user);
  4336. await savedPage.populateDataToShowRevision();
  4337. this.pageEvent.emit('create', savedPage, user);
  4338. // update scopes for descendants
  4339. if (options.overwriteScopesOfDescendants) {
  4340. this.applyScopesToDescendantsWithStream(savedPage, user, true);
  4341. }
  4342. return savedPage;
  4343. }
  4344. private async canProcessForceCreateBySystem(
  4345. path: string,
  4346. grantData: {
  4347. grant: PageGrant;
  4348. grantUserIds?: ObjectIdLike[];
  4349. grantUserGroupId?: ObjectIdLike;
  4350. },
  4351. ): Promise<boolean> {
  4352. return this.canProcessCreate(path, grantData, false);
  4353. }
  4354. private async disableAncestorPagesTtl(path: string): Promise<void> {
  4355. const Page = mongoose.model<PageDocument, PageModel>('Page');
  4356. const ancestorPaths = collectAncestorPaths(path);
  4357. const ancestorPageIds = await Page.aggregate([
  4358. { $match: { path: { $in: ancestorPaths, $nin: ['/'] }, isEmpty: false } },
  4359. { $project: { _id: 1 } },
  4360. ]);
  4361. await Page.updateMany(
  4362. { _id: { $in: ancestorPageIds } },
  4363. { $unset: { ttlTimestamp: true } },
  4364. );
  4365. }
  4366. /**
  4367. * @private
  4368. * This method receives the same arguments as the PageService.create method does except for the added type '{ grantUserIds?: ObjectIdLike[] }'.
  4369. * This additional value is used to determine the grantedUser of the page to be created by system.
  4370. * This method must not run isGrantNormalized method to validate grant. **If necessary, run it before use this method.**
  4371. * -- Reason 1: This is because it is not expected to use this method when the grant validation is required.
  4372. * -- Reason 2: This is because it is not expected to use this method when the program cannot determine the operator.
  4373. */
  4374. async forceCreateBySystem(
  4375. path: string,
  4376. body: string,
  4377. options: IOptionsForCreate & { grantUserIds?: ObjectIdLike[] },
  4378. ): Promise<PageDocument> {
  4379. const Page = mongoose.model('Page') as unknown as PageModel;
  4380. const isV5Compatible = configManager.getConfig('app:isV5Compatible');
  4381. if (!isV5Compatible) {
  4382. throw Error('This method is available only when v5 compatible');
  4383. }
  4384. // Values
  4385. const pathSanitized = generalXssFilter.process(path);
  4386. const { grantUserGroupIds, grantUserIds } = options;
  4387. const grant = isTopPage(pathSanitized) ? Page.GRANT_PUBLIC : options.grant;
  4388. const isGrantRestricted = grant === Page.GRANT_RESTRICTED;
  4389. const isGrantOwner = grant === Page.GRANT_OWNER;
  4390. const grantData = {
  4391. grant,
  4392. grantUserIds: isGrantOwner ? grantUserIds : undefined,
  4393. grantUserGroupIds,
  4394. };
  4395. // Validate
  4396. if (isGrantOwner && grantUserIds?.length !== 1) {
  4397. throw Error('grantedUser must exist when grant is GRANT_OWNER');
  4398. }
  4399. const canProcessForceCreateBySystem =
  4400. await this.canProcessForceCreateBySystem(pathSanitized, grantData);
  4401. if (!canProcessForceCreateBySystem) {
  4402. throw Error('Cannot process forceCreateBySystem');
  4403. }
  4404. // Prepare a page document
  4405. const shouldNew = isGrantRestricted;
  4406. const page = await this.preparePageDocumentToCreate(
  4407. pathSanitized,
  4408. shouldNew,
  4409. );
  4410. // Set field
  4411. this.setFieldExceptForGrantRevisionParent(page, pathSanitized);
  4412. // Apply scope
  4413. page.applyScope({ _id: grantUserIds?.[0] }, grant, grantUserGroupIds);
  4414. // Set parent
  4415. if (isTopPage(pathSanitized) || isGrantRestricted) {
  4416. // set parent to null when GRANT_RESTRICTED
  4417. page.parent = null;
  4418. } else {
  4419. const parent =
  4420. await this.getParentAndFillAncestorsBySystem(pathSanitized);
  4421. page.parent = parent._id;
  4422. }
  4423. // Save
  4424. let savedPage = await page.save();
  4425. // Create revision
  4426. const dummyUser: HasObjectId = {
  4427. _id: new mongoose.Types.ObjectId().toString(),
  4428. };
  4429. const newRevision = Revision.prepareRevision(
  4430. savedPage,
  4431. body,
  4432. null,
  4433. dummyUser,
  4434. );
  4435. savedPage = await pushRevision(savedPage, newRevision, dummyUser);
  4436. if (savedPage._id == null) {
  4437. throw new Error('Something went wrong: _id is null');
  4438. }
  4439. // Update descendantCount
  4440. await this.updateDescendantCountOfAncestors(savedPage._id, 1, false);
  4441. // Emit create event
  4442. this.pageEvent.emit('create', savedPage, dummyUser);
  4443. return savedPage;
  4444. }
  4445. private shouldUseUpdatePageV4(
  4446. grant: number,
  4447. isV5Compatible: boolean,
  4448. isOnTree: boolean,
  4449. ): boolean {
  4450. const isRestricted = grant === PageGrant.GRANT_RESTRICTED;
  4451. return !isRestricted && (!isV5Compatible || !isOnTree);
  4452. }
  4453. /**
  4454. * A wrapper method of updatePage for updating grant only.
  4455. */
  4456. async updateGrant(
  4457. page: HydratedDocument<PageDocument>,
  4458. user: IUserHasId,
  4459. grantData: { grant: PageGrant; userRelatedGrantedGroups: IGrantedGroup[] },
  4460. ): Promise<PageDocument> {
  4461. const { grant, userRelatedGrantedGroups } = grantData;
  4462. const options: IOptionsForUpdate = {
  4463. grant,
  4464. userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
  4465. };
  4466. return this.updatePage(page, null, null, user, options);
  4467. }
  4468. async updatePageSubOperation(
  4469. page,
  4470. user,
  4471. exPage,
  4472. options: IOptionsForUpdate,
  4473. pageOpId: ObjectIdLike,
  4474. ): Promise<void> {
  4475. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  4476. 'Page',
  4477. );
  4478. const currentPage = page;
  4479. const exParent = exPage.parent;
  4480. const wasOnTree = exPage.parent != null || isTopPage(exPage.path);
  4481. const shouldBeOnTree = currentPage.grant !== PageGrant.GRANT_RESTRICTED;
  4482. const isChildrenExist = await Page.count({
  4483. path: new RegExp(
  4484. `^${escapeStringRegexp(addTrailingSlash(currentPage.path))}`,
  4485. ),
  4486. parent: { $ne: null },
  4487. });
  4488. // 1. Update descendantCount
  4489. const shouldPlusDescCount = !wasOnTree && shouldBeOnTree;
  4490. const shouldMinusDescCount = wasOnTree && !shouldBeOnTree;
  4491. if (shouldPlusDescCount) {
  4492. await this.updateDescendantCountOfAncestors(currentPage._id, 1, false);
  4493. const newDescendantCount = await Page.recountDescendantCount(
  4494. currentPage._id,
  4495. );
  4496. await Page.updateOne(
  4497. { _id: currentPage._id },
  4498. { descendantCount: newDescendantCount },
  4499. );
  4500. } else if (shouldMinusDescCount) {
  4501. // Update from parent. Parent is null if currentPage.grant is RESTRECTED.
  4502. if (currentPage.grant === PageGrant.GRANT_RESTRICTED) {
  4503. await this.updateDescendantCountOfAncestors(exParent, -1, true);
  4504. }
  4505. }
  4506. // 2. Delete unnecessary empty pages
  4507. const shouldRemoveLeafEmpPages = wasOnTree && !isChildrenExist;
  4508. if (shouldRemoveLeafEmpPages) {
  4509. await Page.removeLeafEmptyPagesRecursively(exParent);
  4510. }
  4511. // 3. Update scopes for descendants
  4512. if (options.overwriteScopesOfDescendants && shouldBeOnTree) {
  4513. await this.applyScopesToDescendantsWithStream(currentPage, user);
  4514. }
  4515. await PageOperation.findByIdAndDelete(pageOpId);
  4516. }
  4517. /**
  4518. * Get the new GrantedGroups for the page going through an update operation.
  4519. * It will include the groups specified by the operator, and groups which the user does not belong to, but was related to the page before the update.
  4520. * @param userRelatedGrantedGroups The groups specified by the operator
  4521. * @param page The page going through an update operation
  4522. * @param user The operator
  4523. * @returns The new GrantedGroups array to be set to the page
  4524. */
  4525. async getNewGrantedGroups(
  4526. userRelatedGrantedGroups: IGrantedGroup[],
  4527. page: PageDocument,
  4528. user,
  4529. ): Promise<IGrantedGroup[]> {
  4530. const userRelatedGroups =
  4531. await this.pageGrantService.getUserRelatedGroups(user);
  4532. return this.getNewGrantedGroupsSyncronously(
  4533. userRelatedGroups,
  4534. userRelatedGrantedGroups,
  4535. page,
  4536. );
  4537. }
  4538. /**
  4539. * Use when you do not want to use getNewGrantedGroups with async/await (e.g inside loops that process a large amount of pages)
  4540. * Specification of userRelatedGroups is necessary to avoid the cost of fetching userRelatedGroups from DB every time.
  4541. */
  4542. getNewGrantedGroupsSyncronously(
  4543. userRelatedGroups: PopulatedGrantedGroup[],
  4544. userRelatedGrantedGroups: IGrantedGroup[],
  4545. page: PageDocument,
  4546. ): IGrantedGroup[] {
  4547. const previousGrantedGroups = page.grantedGroups;
  4548. const userRelatedPreviousGrantedGroups = this.pageGrantService
  4549. .getUserRelatedGrantedGroupsSyncronously(userRelatedGroups, page)
  4550. .map((g) => getIdForRef(g.item));
  4551. const userUnrelatedPreviousGrantedGroups = previousGrantedGroups.filter(
  4552. (g) => !userRelatedPreviousGrantedGroups.includes(getIdForRef(g.item)),
  4553. );
  4554. return [...userUnrelatedPreviousGrantedGroups, ...userRelatedGrantedGroups];
  4555. }
  4556. // There are cases where "revisionId" is not required for revision updates
  4557. // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
  4558. async updatePage(
  4559. pageData: HydratedDocument<PageDocument>,
  4560. body: string | null,
  4561. previousBody: string | null,
  4562. user: IUserHasId,
  4563. options: IOptionsForUpdate = {},
  4564. ): Promise<HydratedDocument<PageDocument>> {
  4565. const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
  4566. 'Page',
  4567. );
  4568. const wasOnTree = pageData.parent != null || isTopPage(pageData.path);
  4569. const isV5Compatible = configManager.getConfig('app:isV5Compatible');
  4570. const shouldUseV4Process = this.shouldUseUpdatePageV4(
  4571. pageData.grant,
  4572. isV5Compatible,
  4573. wasOnTree,
  4574. );
  4575. if (shouldUseV4Process) {
  4576. // v4 compatible process
  4577. return this.updatePageV4(pageData, body, previousBody, user, options);
  4578. }
  4579. // Clone page document
  4580. const clonedPageData = Page.hydrate(pageData.toObject());
  4581. const newPageData = pageData;
  4582. // Once updated it's exempt from automatic deletion
  4583. if (options.wip == null) {
  4584. newPageData.ttlTimestamp = undefined;
  4585. } else if (options.wip) {
  4586. newPageData.unpublish();
  4587. } else {
  4588. newPageData.publish();
  4589. }
  4590. // use the previous data if absent
  4591. const grant = options.grant ?? clonedPageData.grant;
  4592. const grantUserGroupIds =
  4593. options.userRelatedGrantUserGroupIds != null
  4594. ? await this.getNewGrantedGroups(
  4595. options.userRelatedGrantUserGroupIds,
  4596. clonedPageData,
  4597. user,
  4598. )
  4599. : clonedPageData.grantedGroups;
  4600. const grantedUserIds = clonedPageData.grantedUserIds || [user._id];
  4601. const shouldBeOnTree = grant !== PageGrant.GRANT_RESTRICTED;
  4602. const isChildrenExist = await Page.count({
  4603. path: new RegExp(
  4604. `^${escapeStringRegexp(addTrailingSlash(clonedPageData.path))}`,
  4605. ),
  4606. parent: { $ne: null },
  4607. });
  4608. const isGrantChangeable = await this.pageGrantService.validateGrantChange(
  4609. user,
  4610. pageData.grantedGroups,
  4611. grant,
  4612. grantUserGroupIds,
  4613. );
  4614. if (!isGrantChangeable) {
  4615. throw Error(
  4616. 'The selected grant or grantedGroup is not assignable to this page.',
  4617. );
  4618. }
  4619. if (shouldBeOnTree) {
  4620. let isGrantNormalized = false;
  4621. try {
  4622. const shouldCheckDescendants = !options.overwriteScopesOfDescendants;
  4623. isGrantNormalized = await this.pageGrantService.isGrantNormalized(
  4624. user,
  4625. clonedPageData.path,
  4626. grant,
  4627. grantedUserIds,
  4628. grantUserGroupIds,
  4629. shouldCheckDescendants,
  4630. false,
  4631. );
  4632. } catch (err) {
  4633. logger.error(
  4634. `Failed to validate grant of page at "${clonedPageData.path}" of grant ${grant}:`,
  4635. err,
  4636. );
  4637. throw err;
  4638. }
  4639. if (!isGrantNormalized) {
  4640. throw Error(
  4641. 'The selected grant or grantedGroup is not assignable to this page.',
  4642. );
  4643. }
  4644. if (options.overwriteScopesOfDescendants) {
  4645. const updateGrantInfo =
  4646. await this.pageGrantService.generateUpdateGrantInfoToOverwriteDescendants(
  4647. user,
  4648. grant,
  4649. options.userRelatedGrantUserGroupIds,
  4650. );
  4651. const canOverwriteDescendants =
  4652. await this.pageGrantService.canOverwriteDescendants(
  4653. clonedPageData.path,
  4654. user,
  4655. updateGrantInfo,
  4656. );
  4657. if (!canOverwriteDescendants) {
  4658. throw Error('Cannot overwrite scopes of descendants.');
  4659. }
  4660. }
  4661. if (!wasOnTree) {
  4662. const newParent = await this.getParentAndFillAncestorsByUser(
  4663. user,
  4664. newPageData.path,
  4665. );
  4666. newPageData.parent = newParent._id;
  4667. }
  4668. } else {
  4669. if (wasOnTree && isChildrenExist) {
  4670. // Update children's parent with new parent
  4671. const newParentForChildren = await Page.createEmptyPage(
  4672. clonedPageData.path,
  4673. clonedPageData.parent,
  4674. clonedPageData.descendantCount,
  4675. );
  4676. await Page.updateMany(
  4677. { parent: clonedPageData._id },
  4678. { parent: newParentForChildren._id },
  4679. );
  4680. }
  4681. newPageData.parent = null;
  4682. newPageData.descendantCount = 0;
  4683. }
  4684. newPageData.applyScope(user, grant, grantUserGroupIds);
  4685. // update existing page
  4686. let savedPage = await newPageData.save();
  4687. // Update body
  4688. const isBodyPresent = body != null;
  4689. const shouldUpdateBody = isBodyPresent;
  4690. if (shouldUpdateBody) {
  4691. const origin = options.origin;
  4692. const newRevision = await Revision.prepareRevision(
  4693. newPageData,
  4694. body,
  4695. previousBody,
  4696. user,
  4697. origin,
  4698. );
  4699. savedPage = await pushRevision(savedPage, newRevision, user);
  4700. await savedPage.populateDataToShowRevision();
  4701. }
  4702. this.pageEvent.emit('update', savedPage, user);
  4703. // Update ex children's parent
  4704. if (!wasOnTree && shouldBeOnTree) {
  4705. const emptyPageAtSamePath = await Page.findOne({
  4706. path: clonedPageData.path,
  4707. isEmpty: true,
  4708. }); // this page is necessary to find children
  4709. if (isChildrenExist) {
  4710. if (emptyPageAtSamePath != null) {
  4711. // Update children's parent with new parent
  4712. await Page.updateMany(
  4713. { parent: emptyPageAtSamePath._id },
  4714. { parent: savedPage._id },
  4715. );
  4716. }
  4717. }
  4718. await Page.findOneAndDelete({ path: clonedPageData.path, isEmpty: true }); // delete here
  4719. }
  4720. // Directly run sub operation for now since it might be complex to handle main operation for updating pages -- Taichi Masuyama 2022.11.08
  4721. let pageOp: PageOperationDocument;
  4722. try {
  4723. pageOp = await PageOperation.create({
  4724. actionType: PageActionType.Update,
  4725. actionStage: PageActionStage.Sub,
  4726. page: savedPage,
  4727. exPage: clonedPageData,
  4728. user,
  4729. fromPath: clonedPageData.path,
  4730. options,
  4731. });
  4732. } catch (err) {
  4733. logger.error('Failed to create PageOperation document.', err);
  4734. throw err;
  4735. }
  4736. this.updatePageSubOperation(
  4737. savedPage,
  4738. user,
  4739. clonedPageData,
  4740. options,
  4741. pageOp._id,
  4742. );
  4743. return savedPage;
  4744. }
  4745. // There are cases where "revisionId" is not required for revision updates
  4746. // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
  4747. async updatePageV4(
  4748. pageData: HydratedDocument<PageDocument>,
  4749. body,
  4750. previousBody,
  4751. user,
  4752. options: IOptionsForUpdate = {},
  4753. ): Promise<HydratedDocument<PageDocument>> {
  4754. // use the previous data if absent
  4755. const grant = options.grant || pageData.grant;
  4756. const grantUserGroupIds =
  4757. options.userRelatedGrantUserGroupIds != null
  4758. ? await this.getNewGrantedGroups(
  4759. options.userRelatedGrantUserGroupIds,
  4760. pageData,
  4761. user,
  4762. )
  4763. : pageData.grantedGroups;
  4764. // validate multiple group grant before save using pageData and options
  4765. await this.pageGrantService.validateGrantChange(
  4766. user,
  4767. pageData.grantedGroups,
  4768. grant,
  4769. grantUserGroupIds,
  4770. );
  4771. await this.validateAppliedScope(user, grant, grantUserGroupIds);
  4772. pageData.applyScope(user, grant, grantUserGroupIds);
  4773. // update existing page
  4774. let savedPage = await pageData.save();
  4775. // Update revision
  4776. const isBodyPresent = body != null;
  4777. const shouldUpdateBody = isBodyPresent;
  4778. if (shouldUpdateBody) {
  4779. const newRevision = await Revision.prepareRevision(
  4780. pageData,
  4781. body,
  4782. previousBody,
  4783. user,
  4784. options.origin,
  4785. );
  4786. savedPage = await pushRevision(savedPage, newRevision, user);
  4787. await savedPage.populateDataToShowRevision();
  4788. }
  4789. // update scopes for descendants
  4790. if (options.overwriteScopesOfDescendants) {
  4791. this.applyScopesToDescendantsWithStream(savedPage, user, true);
  4792. }
  4793. this.pageEvent.emit('update', savedPage, user);
  4794. return savedPage;
  4795. }
  4796. /**
  4797. * Find all pages in trash page
  4798. */
  4799. async findAllTrashPages(
  4800. user: IUserHasId,
  4801. userGroups = null,
  4802. ): Promise<HydratedDocument<IPage>[]> {
  4803. const Page = mongoose.model('Page') as unknown as PageModel;
  4804. // https://regex101.com/r/KYZWls/1
  4805. // ex. /trash/.*
  4806. const regexp = /^\/trash\/.*$/;
  4807. const queryBuilder = new PageQueryBuilder(
  4808. Page.find({ path: { $regex: regexp } }),
  4809. true,
  4810. );
  4811. await queryBuilder.addViewerCondition(user, userGroups);
  4812. const pages: HydratedDocument<IPage>[] = await queryBuilder
  4813. .addConditionToSortPagesByAscPath()
  4814. .query.lean()
  4815. .exec();
  4816. return pages;
  4817. }
  4818. async getYjsData(pageId: string): Promise<CurrentPageYjsData> {
  4819. const yjsService = getYjsService();
  4820. const currentYdoc = yjsService.getCurrentYdoc(pageId);
  4821. const ydocStatus = await yjsService.getYDocStatus(pageId);
  4822. const hasYdocsNewerThanLatestRevision = ydocStatus === YDocStatus.DRAFT;
  4823. return {
  4824. hasYdocsNewerThanLatestRevision,
  4825. awarenessStateSize: currentYdoc?.awareness.states.size,
  4826. };
  4827. }
  4828. async createTtlIndex(): Promise<void> {
  4829. const wipPageExpirationSeconds =
  4830. configManager.getConfig('app:wipPageExpirationSeconds') ?? 172800;
  4831. const collection = mongoose.connection.collection('pages');
  4832. try {
  4833. const targetField = 'ttlTimestamp_1';
  4834. const indexes = await collection.indexes();
  4835. const foundTargetField = indexes.find((i) => i.name === targetField);
  4836. const isNotSpec =
  4837. foundTargetField?.expireAfterSeconds == null ||
  4838. foundTargetField?.expireAfterSeconds !== wipPageExpirationSeconds;
  4839. const shoudDropIndex = foundTargetField != null && isNotSpec;
  4840. const shoudCreateIndex = foundTargetField == null || shoudDropIndex;
  4841. if (shoudDropIndex) {
  4842. await collection.dropIndex(targetField);
  4843. }
  4844. if (shoudCreateIndex) {
  4845. await collection.createIndex(
  4846. { ttlTimestamp: 1 },
  4847. { expireAfterSeconds: wipPageExpirationSeconds },
  4848. );
  4849. }
  4850. } catch (err) {
  4851. logger.error('Failed to create TTL Index', err);
  4852. throw err;
  4853. }
  4854. }
  4855. }
  4856. export default PageService;