ÿØÿàJFIFÿâØICC_PROFILEÈ0mntrRGB XYZ acspöÖÓ- descð$rXYZgXYZ(bXYZ<wtptPrTRCd(gTRCd(bTRCd(cprtŒYê!q%ÎyÜ’T=UHÔC]õw`²slìŽ8âWö;¬¹M/¤ø·þ陪kêq.'�þSêÝ�`ç¿tŽ©Nùï’¢ÃvK pxÏÜ”›š÷H ¼2ˤzÀ—e>§–0ì5�Ãä(*1’�ÃÔZC{oÊEóynk<Ð08*{]Ç�—`Š‹\UÞ·9­`3º‡*4U�.Û·„ú*ÇI�\ü(Ê«[©_ü3!ÉÇÑgE33¡ÁÀç’¥H¼K%¶ýuµÌÉiê¤nžNËlô_ŒU°Én d„`ko?qÝiªfànvå=§1áñ¸�b—F�T•3¶zK©¨ïÔžuTr¸`¾0ì‘�¬­ppÈ\iÒ=qq±ÖGSMUåÈÒ;làNô_Puuq‘‘Õ3ì÷ZÆVqæÂáµáoB®s‚„„!!@B„„!!@B„“¨¨‚’ *ªelQDÒ÷½Ç­’”\ñãÿЦi$èëAò"poaÿrAü™ögܨn½/�É*DW‹Þ3ÏÔ3Íe²Îb³ÅúˆuAÏý9à}ÊÒU•óÖI©äï°ÁaYR_ ‹VãÕ!Ï%4|�‡$ìGºÅ»;ÒP]bM§Ð×`Ÿ…Q){¼±¿ùKÎòàeíÀM[­ÏsËvþU^Ìãf[�ŒŽJÍ­{�ÆçÝ&Üà÷ÝJZm7/8Çr«)¨+dF.N�”48îS¨è.8e30·*ócèy% 3E�¶h+½³¡£`¥våyù>B1Ò;áÁ“Û5}Ÿ¢k*>¢N7'!])z6£Á„»<î¶U¯¤éáˆÿ£8R0Ù¡sšÁÉ®G͔ߧJáF&–¸ôN¦:fS¸—KOuD½t½E;\öDZðs•Ö3ô¿ç¿Ø��-U+÷‡úiªéÙ�r8!o�”ÓÙ”¸«èæŠ*ÇÒ”?ʃ¹-!kùc©¢Õ�p#l®øeŒöŽYcptËMŽF¶¦™Ã<à«/MuUÊÁTÊÊÜǰ‚@áÀ{­}n¸ùÃ��å>Ò% ’ Øîµ¶ˆ¤ôv7‡}yGÖvÀàíqâFyVõÆ�ÖÕÝ#ÔÕì•Úà&‹VÏgÿ>^Xï4]Aj§»Ûä×C›îp~Vð—dyùñ~7kÁú…sB€„ !BB€„ !B“âçZèÎ�¨ª§xµCȦÁòï°\au¬’z‰'’BçK‰ß';­›ãßX¿¨úÊzx%&ŠØ?/g8gQýÊÓ•ÕÁøc7lïÅ�ÿ,ñ“ ¥yýDþÉ¥TºÀcu,ËËiÃq‚ã�²nLJÎp0Ö6U$VgÆØÏò�꛺C8Ýdçk�€2R†JªÆSF8?U zMtÕškÍX�­Ë5 ð·Jt3Æ4FÀx'  :!¦–¾= ¸ã•»(,Ôôѵ‘1£_=Îæ9ˤ|=ÞAv—¤%£¥™JK[·;r¦›jhxþÆÅ5`”ëÉ ì7^jß§¡á •ËAÊwKoi•®#ÒJ�ü°Æv$'PÃ¥ û­"c6>´ÑÓµí&»î=UIOVÐØè@pnIÙ-OSùOXÆSJÛ™™ï/عt©¥9\Ù¬¯–\]ãkýû-'Ö]a{Ÿ@€OnË¥ë)âœ8»sØáRúŽÅ�õ0;9ì¯�;ÆìdIJ-œ•u } šO¤vÙ/c¼àºžsŽØÏ außH‡Ä÷‘Œ�@ZnzzŠJ²sêaÁäd/sU–6�',^S6›æbXÈËwÎVïü;ø‚)îéÙ‚¬—ÓåÛ2P2[÷\ái¹;"7�Ç<«®ã- lu”S=®kšpC‡®ˆº)4²FŽøB­øyÔÌêÎ’·Ýõ5ñÌ3ÃÆÅYAæ5N˜!BBB€„ !BB€[õ]/Ò× Ì¯ t0¸G“ËÈÃGî§VŽüMõ ”öëoMÓ»ÕRçTJ?é7ü¨n‘|qï$Žl¼ÖKPù'•åï‘ÞcÏrNê·RòéYSÝŸ²–¸Í’@Û'(²Âk¶â&à,ùM&©€55¡:Ý4žùÂõÒɤÎÀž¶è›ùRçœl B¡Zîø8Êžè[3îuíxaq‘àŽwܨoÊÊúQ9oûĆy sx;ÓYž˜½,-ròòþ­ ¯|¥‘ëÐOmö]Ånªme5 ?©£?mhÇ‘•ŽP„+œà„!!@B„„!!@àd®DñǨzëk„Œx|4¸§ˆçf�Î>ù]Y}®ÛE]i?íDçEÃ�Er5³M3Ç®¦WH}÷=Õ&þŽ®4}‘U©Ôùõ;pÝÓ<ÅÓ¨à'Ó¼F‘’AQÕ§mÛœ³:ƒZ-�#ø‡û§’4SPî0ãã £çIO'sò¥®4úÿ-J$�GÁAI[)|óANæê!�8Ç®˜ð²ÀÚz6Ë+FØú­ ÐT’ÖÝZè¨Ý?•€@ÐËÕÎÕFÖš8ãÇeäóc,Ž‘èñ'jäl¬·K{p±kFw-y'‰2SÈ3#$�ðì'´^$Ú¦~‰bq÷9eæK‹‘n�B¼oV_ásZFø)Ð|oöU«múŠãƒO;ó©K2 c:–=:é›)vV‡®sò“óØî‘u{Hiß µêª+,FjÉ�¾UátŠI¨«e·Î±¾ðeÉœ- xñæ G=”0:\~’Jƒ‡ÆëýÂAå嚎4·•Ù$ÞÙÃ>d¤t¥KhôbOoer>“€ÊÓPõWXÝœ×CçF9'$§è*ºœZ‰I#‡…\¼~«Lœ|žÏÂvï¦7ÙhÏ-u,’µ€¹ •º\ú†yU,òäŒò¨�uBÙé¤ôòÂ0³ãKñäL×�–Lm�YXúZ÷.içÿ>êY•1Ö@Ù#v$*;®¨™Aw‘…¤aGÙ®-…ÿ—y'|…ôËqì|ân2£bÙî lqK�ØF㜂»³Â[Ѽt¥Ïvdu4z÷ïŽ~á|ú·È熻ÐN¦®¾ü2u¬´¶Ý,®s¢ih°o袼4ËgMÆÍþ„!jq‚„„!!@B„„!FñŽê-�Z5éã1Œsºã+­[RÜ�˜Ñ·ß+§WvÒXá£Îï9Æó¹ ’«ªZèg˜»q°%g-³» 4ž«ÌóÖþ§`……kF˜¢üsû¦�J^öÆ]’qÉå<~f�mßNÿ †‡¶Ø]-|cä©Ê¨%v"؆†`s’xIôí �^ÇþœžŸ¦lÒW]M[Žctøè±É“ªføñ¹Qо ô=Oô—çj©£}MPê dl¥/²ÓM±‰® '\RY,ôÖˆD“Ô9€£ÝÜEQ¹õ}LdŠªÊ GŸå’`çê¼fç7hïqÇÓ2½Z©ªey|œpuî™�ÎÙ)+Nò�±—ª+¤o›e[h߃ýÔ¥ªíh,òäîÓÈWï’˜¬Xçã%ºJ ªJ–²Bü ðBÚb}µ¥û‘“’ª?�#H÷Ù[¥ |€9ݸ\Y§ÝÙéq±¬q¡´Õ­‰›¸çÝjß'­¯…”ômt˜'8öÂØ·Ëc9e@¿I¦C‘‚vQ‚n3²¼ˆ©Ç©¯-Ýç—jàöW:›|:šLc,¦³@ÒÓ¥ l¼©Jå£ÑŠê¨åÄe§E–ªF°êhÉúer=Γ̦†’æžÞ˹%Ý7\ýy\…ÄÍx’'»‚F_CñÎñ?ò1¬ºtÌæ(XÐs‡al®ˆ» OWZ.� �¡þÚNÇú«-.�çËO¶ÓJZÈ*cwûnû.ö¶aúŸIü=Ѧj6¢)œæÿº´ª‡…õM¸t�À™¢c‰#Ÿ@Ý[Öˆæ~‚„ „ !BB€®ÓLý÷v>étÒâàØ5»†gìAø�º‹‡ZU–7XÑÏ±ÇøZJ½Ž©»C¾,n~~Êñ^´Wu%}FœJZAíº ¾•2�ñQ°²6HMk4K’æåIÔÓÄ׳„ÏCÁE„¤Ó¢ÝQ-áï8f½ºÞ؈›žém, $4oðœPÈÆH5•1nÃZ$)èŽ�Z6î’|AÍ- <ž¹Œ§Ç“«¸öMa:ÜrvS“H@IÔ¯Ohp*¡�Œ`·¶öJylr¹€½Ž?Tk“Ak‹ÈÎÈ–‘ƒômHÖDa¹k¹ z¾ŸŽ\Ë÷ "ßskkü¢Z4ó“ÉWktðTÁ«,ÉZE^ˆ–ˆ[w”Ñæ7… |˜£¸º’–µ™-º„¬xpiVZb;ÔÕ°�:só”й¤àR3ÊÞÝÔmMâ Ie$gl¬ýeÚ£Tþ$bd]q“œS¸®™®ŽwL?K†û®ëüL\¡=p`q$Óÿs…“0±®¹ì¾�ã¿ñ9òOú¦0?w†œo¾UžÏ0|/aÛ?ÑT ~™œs…déÇ;Ï1»‚¢qAÑôKðÏsÿTð®×.²çFÓ²s‚ÒBÚ럕GÿH\íÏ�»Ê¨d¬ßù\ÞßpWA+#9ª“! J‚„„!!@B‡ê©Å5’ªwpØÝû�‡÷S ³â+KºN¿Ô@l.;ý�•éÂ%’ÚÚ™°šyÇÊ×Õ³9­‹S±±à­“âS4IMˆ0ˆµ;ܸœåj«ô‚8XAÙ¤ƒó²ÍStÆuUߘ¨9Æ–ãNÿÝJô}ÔÚï–ûœ|ÁRÇà}U@Ô9òµ­nF;+%†™¹Š'´ãbOsÝW&ãDb—îš>¦ôÆ:û%\nK _ÿäÿ*Û ÁÃCœ2xZkðí{7­‘¾@éiá»nv nB|·åà /˜Ÿë&�¯ÄûA1Zˆ]¡Û|(ãÆ )wÊ%g¶Fp¢å~ ,&‘ ƒÜÆ·9ÈP×[¸§{bˆúÜpÊ{_8 $; ¯™Y‰ÒÅ˾‹8§e•ê1W$,{ƒ·çe;2Ã^öò=”-Û®m]=GªoÊ@Æó,§…Kâuö6’íMU;Äà@ý–’VŠ—ú †¶’¦)N"{pUZzX§�ñ1íÐá±%U.=qä6AøiªG�ý9Mye¢kÔf­ïËh.Á=¶R“hÆQ§e�¬è¦±]£­‰ú¡y vXújõ®6; ‚3Ê«]/ ½ÑˆàýdiÀággómÎ<œµ&¶mVÖÇ$zÚìdp¢®U õ¢©.m,Ƽ•uc4’ŽUdÂ… ê*—Px í…_ uOÔ󆀞TU5ÎòÚA 2¬LXpü`lÈ›f‹üL]X:nZÎò�Ì|.<ª” %“a†€2ºWñ]$®4ñ�ZNøîW2]™$s�¯Õ‘íÙ}? 5�#å¹Òí�)ÙâNA>ÊÇe~™#À ‚«–­¡.Î=JÃkþc9céÚŸ„*¶6:È$Ëça¥ßÿ`ºir/á"âa¸º/0�,ŽŒç�Ú>¾•×JQºÁB“0B€„ !BUD�-U?ê’'ÿA•oU_¢2ô¥pí ÷�¹FZ>£‡|M.’a9È'�퀵ù¯M$’K��–ìñ^�”°Á£Òpâì�øZbúìPÅŽ7#*ˆß#ÙZ‰º44d¼•=a­™÷VQ3/v�[�‚ƒÈ�ÿpá�;�l§�13k%¸QÇ/“[Sò%AÉoßü(š´gÖGr~¯“Sh¨xþ# ˜ÜÿçºéöÉè×�È_5ÿ>,]íÝyo±]Þqtm—‡Çö_Eiªüê8¥!ìþ‹çy˜ž,•/³êø£—¡ùªkNã“¶É­]C:O*>z·dšæàÿä|¯>Gq•džs„M$“Ø'4V!ߘ|DµÃ„•’&TËç=Ùß«—�rF;-1ÆÑIJ�_¯ú"¿«Ë­•P�Ë9û¹ÎÛEdð¢ÏÐ4¯’Ë,ÁòŒÊ °ßÛ…·«Äêxk‰=‰áWke³KÙ–ñƒÁZù¡&jÊ‹uúó3à‰Æ8‰ ¸»ùTQð¦ÖÊÑ= ©c�29¹õ{­¦Êq[c/ä—ŽQKæOÎ ]?Fý/ÑU4GUt›Î•ߥ­kGÑJ]mŒÑæ4ZvÂVK÷–©6lp>umö7DZ7ß+6Óð‹~Œ™\"“C³¨/e¸e§~T uÀ Úüœò™¾ã#¸vlÑL°6¤¼ò20’¯œ6žGó€TL3¸�K‰'•�Ö`Ë|®ÎiïÊ´ÌòKG+x÷Ô4Öû†‰ÝªGå´rs• ®µ2OfxÚ$y/ä7°*óã­þJ¾¼©†ÊV5€ƒ‡rÂÖÓO%C̲»Sˆ êøðë�#ÈŸ|Œ”¶ù`%ù*zØâdÀl«öâ #rIV nùv§þëvVK~.!—:š ájÈñ¿³{\%.¶f¾µñ¼åÛùV»¤`Z«7+{g‹°+ÀgÒè/B–•®<œ§ßúÍò7DNw§l4­YÕW;…¾�ÐÓÆK›³>ªš|Bë«e“¥uÅ#±ù†Kr{…Ó‹’ÑŒ²$΃ŽóŒ’¢á?—Ìp j¼AéâÏ&ü4ãQî´ë¯÷Û¬ žªž©áþ¢qhM¦¸WFYo”mú„Et.2û/ÛøFÛ«ëëX‹æ•Ã-kF1õUÇxµ7š_p·kˆŸN¸ YÕÏvœ´ÅGR01þÑÝ7ÿLêÊàChd�ƒ—I°W\h´a9Éx�°Ïl*.OòI¶…õ�ÙPÇD˜9jk�IÝ„D¾â<Ò3¢6÷ú”ë¢ü-ª©.¬¼\êç/w¦3!ÐÑôYÏ�+³–Møl¨n¬¬ŒhÁí�T„-:@=Ô]¾Ç°²‘®:c;ÙN¾69¬-ÇÙqJ“Ñ´kb´‘Hé4ìp~¹®ŽÓÓÕ3¹ÚCbs‰Ï°R4Í øaiŸÄßZ›/IÍG¸š±¦Àv6#sû-¸ðü™L¹Y?'#�zŽäû½î¶äó“Q3ŸÎvÎÊ8p¼qË— lw_V•*>M»vJÑ<cÉ.Û*Ãh~©�€[®AèŠ'8d…b³¼62ñ‘Ê3X›×ðÃXÊ~¼e,šU>Xv¡±!˾FÃ|Úðbí%·­íÓ±¹Õ�âg(ÁÅønÚJÖV¡�vRðU¼Ä\qŸ•«è:€<6f¸ïÙ[í7¸ê2üØ•å­ÊvY+æµ²= ´�ÁP×{+s0ïe% À= cç)•uÍŒÙîÉ(™z ªzzÐé¨R4öFÆó):‡`ÉkEKÉó8í”±¨òÚ0AZÛ"�)áÐâLxFܪ<·¹ï#á8¸]™ dê#²¤_ú‰±Æç U [£�E[ íôFß-�õÙV*/QR±Ò¾O¹Pµ×�2GÔÎì�’T•s\\AÚ&‚pT1QçÏ#f ñþýUwª£—š÷–4ž>ËO{-±ã ;¦lsi?Ã$ƒŸuªÓÜôZX’GÏòmämž0áž²''앤£}DnpÒGd”à²RÂ1…Òs™Å™AÉʰóGdï±;(+|Nža\HÇÏ“Ši'¬18éòÎ4çÙVVi§ü³ÔTuGFÊ aÏ Œý—s0ic[œà¹7ðõb‚ðî›»@Hu3<’{;K¬}5Öi¡‘ì„+‚„„!!@BP÷Š(ßJCŸ£N]�2IÇo’¦CÄ®¼²ô-‚{…Æ¡­“C¼¶äg8å9;ñ-[¦—Ψ©.ÒÁŒFÑü¾ùÇù\©|¨8òµçsƒÀW¯úöN«¾U^*%¶�2Nß^ëYéåÉܸ÷îUVÍe¥Cʵ”¥Ò7N£Ï¸Qó¸xåHU=¬þ= ÉǺŽqÔIùV3`Ï ñÐ>û”»Är÷´�誴Ož¢éýGm¹Âèï úf:A–21ù™ù� Ÿ<úÇFØq÷cºîœu=,¥ctÒ<œwiü-³hŽ9è¢{w` „�†?ô¹\æeÏÂiЕ…²ÔÙ*ž”ÏÔÀyÐã²ñ¹O²³ÜáÔ�‰ôòq²{Os¨ œÑ©¼”¼ÔºÚ\默Ö�-ØÁ^uYéÛE†ÛÕðȸlZNë:ûë'vIqþŠ�SM¡ÄÄâÓÏÂ�šç]HN¬‘ÆÅOK-ùZѱ`©iÕülc“•�Uê*f’éu9ÊÕÒumCšKÆø# 2£©&•ÙÔr;çu+e"‹ÕïªrÇ5¯ ÷õ*ÞòéÞKäÒÁØžTUeÒy¡¹ùM©hª+êåïìºag&I¹±v¾¢å.ÃL]²sŸ²›æÃGƒ»Ü3Ÿ„òÙf·#ûnž^ dŽvpâDzw·H«…+f�ñXEùw·Ò@{åR¨ºj¨ã/S]…yë˜'½_é¬Ð·!ÒÊ}˜å>ª³C@tEkDyέ†]`�4nf¼ŠÈêhÆ<°;HaW®Vi¼ƒphÀ$’Ó±Æy[µý5S[Bɼ½±�t‡OòáE\úFLú· 0Œì=¶ù[Ç%J4Å$¾CIÎ KÚž×UaÀ�Gc씿[& ©p|e­ÔGMmòˆekÉ#°Çºè»Vd´èú!øKÜ:R%š­Yc4ãm`d�—D.ü0ø¿tèëÜý9já¯-�0´—6víÁ]©dëÛ-Ù¡“¸ÑOÁd¼}�Çï…E’ õodÊ-»,¨^5Í{CØàæ�Á ¯V¦`„!!@BNIâ‹:Þ6ßuJ*Ù)7¤(±’Xáa’YÆŽKއ¸_],¤hÕÿ#º«×É[[ ’¦«HÌóŸØv\Y9ø¡¨ìè‡rÛÐÃÅ/àèªCOÓöÉ®—)0ØÃcqŒôÜý—ø¹Öþ!uMy­êú‡ƒ+Ž˜Ðm+¯z’J Zy +$ª?ª¢F‚ìüg…Ìž"tÄ÷ ‡ÔH§âコ‡-å{5xc�hç[ƒ¥¨™ïqÃAÈH@Àç¾0¢²Â g“sØÓë#lüaX-Ü.�dTôĵÛ9Ä �ì>v]–1ôçXÜž�dÖJã†8�¸OmÖçÔ<³¼à‚8[ЇÁz¸_)ò†HŒÉN­þUQÔ“ùPÓ¸.>ê²äÅ"˯e/¦ì&KÅ3ä�»|®¤èŽ›’:WÍ+Öà·a«t—…¥÷ŠJ©àŽ8âVäà[âÏhŠ„qÆ`Ø. ù”¼;°cqVÆP[™ùo+FÔý_K]Ò—¸oÔ§Ò×bPÎf{ýòª¡’&8 �Ö4’\â}ª8ÚçÊÂâHý ÷ÿ ›û´Î‹qv‰+æ’÷CT2HNg�§'-!ÓwšÞ‘¼ºÅUP†u3ÕË{-¿g¾Áp€æ’WLNþ�Ke‘SôÆ¥�Ãn « ,%®%Z*#†MÀÀ'b ŠmcÔAª•’²�YHÜä7ä(Éi÷Èiû+�UÜH1‚~ô¹d“Xç+hÌÊŠÌvÙ% 纲ZlÚZØõ;襨,Y{ZöŽyVJKtT�ô7.îp«<¿Á töQÂá—q·eXêyC ‘Îyn9ʹWHÈØZFák.´¨©«†�…ÓT¿É‰£’ã°L;•™æuU::ÆëÝÚºõ§§¦Ž‡K'~ŒàwE–žÊ8YÃ�ogžùÔζ[!Ô[»‹FCN2J¡Ínª ®},ìs_‹\1Æ íÛƒ±ÓS¹ÿ–kªÓªLnâ~V�ñ?ÇںŽ7ÇJØ…aÒ ¿<ŸÝvcåGÃaÝ™øiÍÈ^C’œ†¶L ¸$ÖöñEz‰µ9lU@¶À­Gá‡ÒØ­55$¼æ¿K¾ëaÐ9ÔµDHûð¼ÞNNó´oŽ=Qq¶Ýn–w¶)ï,À9 ÝAÔQU4gs‚ªQÈÙ oš3¶é8å0Kª#…\|œ¸¿µ‰b„½FÅŽ® uiÿå²YSè+ç ã…9GZ0‰ÂíÇòu¬‹ýË�ÿ«%“ŽA È9þéEèâÏ�2¸3žP”=ÕV60I$�n%mÔœ��=»$kjÜAv¡�ªƒ©ª.ÈÎÎÏ,ò;“=hãŒ|CŠ›�È þ꺪G’|Ìo²Ni]«o¢K:öpÊ̾ˆúÊ3P1Σ¾ê‰Ö]6%¥yŒiÆx[M°´µ¸ÕEõ¨MLì7$ƒ€´„ÜYYE4r]OMÅKt,0—»åm¾�±DúBï%ºšpÝ·P÷{ ¼¾IcÒÖ8c#•²<6£mMG’ðãÓ#Ýì;ד%ÄÃjE��)EQ‡66’Ü»e)oéLíÃŽM·í•;-º)˜^ÈCŽ9!0·¼ÁTèIhøár¼Œê¤aNÑS͖ŸíÛáKRÑú@ú)N×cSFãdá´e…¥½Â…2lˆ¹†SRI,ìÀŽ2í»ª£,¯Š�õ5Tó€çä~‘Ù¿@¯U4­¬�­�êŠ ‡»»·?²e-^í³ð¥Lƒš|_é—ÒÊËõ#1$;»çO}�¿HèÚÝg#‘•»zç¦EÂ’X¦‹P{ÉåsÍ5¾¢ÁyšÚZt±çc ð¬ÿuE ú;Fß¡º9ñ€ïP?8)ç� ƒ1äö%V¬�t‘³-!Y €¼o?+†Ztz1•¡9)Üü‘¶BÆž�óIéû©z[s¥¬$ƒð§(l��cÈøY¹—«"híÂã�œä„¼°¿Ë:F0yV#kkš;àl™ÖR–‚ÐÀ*^Ë}‹Ï¢3— �ʇè;3o�K=ÚhµÅob$m滸ú þê[ªÝùjY^IÜsô[¡úLXº~–'Æ<ùX%œã—8d�· ³‘çòeB ¦š Źg�ùû©6Q2H[+¸!KÇ@ÂàK>«ÖѶ–aF!%€ï¤û-Ôìàc*XtÙêp§¤´Äør09¹Î›]i!ø?º”|- ÇüB6A]ÿI§f�ÈÀp6T^¿ðÒ¡dl§a|·n9þ‹kˆ˜çŒÿ(^y1¸9­P›DšÊ’Ý‘66††íûaAQƒ-P£gqð®�A¡–za¶rFeZ ¥0Ô \1ïôÊ£vA3RÿËS‡ööL(å}UO ägÝ!x«3Mù(Ý�X©‹¸RÃæ}1³€7Q{ avçžÊíl§Š&5­ÛnË“#Ùèc¦/EitMàï”ý´ ²Z E�ÎÉB\NÁ¶ÍƦ2Üãʵ‘Ë�†à)gE!ÐT]É­™�†·'~ Gd? ƒ,ÂýÔôt37TMwžð?âÝ÷úœ-²!´ï…Tðú‡Î«®¼JÌäù“Øq÷W2”¼ •ß�~§•ž]¤# 98ΓBÙÛ¡ÙÇØö)HbˆÎ'Q‰@ÜãetŽz= ŽI³Àóú‡Çº}$�ý9dÜE‚*˜â&ˆÐëÚ«Iã‡Ý*?/~êû] Ì8–*‰ƒ^ÃìG9WI¿ ·E”0¶] põƒšöMÇ XÜ?Þй’3¯¨ê^vÑ^óý±ýW±þ"<=¬gæ­FïXÂ2_ ô�¹Q(J> ¥eŸ¬­Ï•ôõìnC•'Ó²ªÜåYpÃvÀÏÊÁž:ôÅâ£ý2-âqUéu6€ßœ•s©š¾®:FgïÛ*Œ´U�ú~‘Õ•f¡ç#VÊÚt´hiÈM,ÔM¡£nŒ%uüo…ž’ Âqƒˆô�‚l÷ {§Œ��ÝC&‡‘8}!8ÈìÃû¦�¹Ü�’Í$€FTeVäÍ9ì  Ïší[g»0´çê«PtÈàU�$>hó�nM„êcá%Jì€ßDñ­ÉÆNvBR�Øs{�ð§¨ËKý¨(Æ4‘€¦­äihùGáda{·ÅYFæKp##!s§‰ÞY.IééÝM/«@í'ƒ²ê)"/…ÍÏ!kþªµ²XÉ1‚wì§Ü]•š´qÓ:OÄœ©dVΣ¯m(~@eOb®q^üU¡¤l”�apai.ÒwÿñÊØw4m«1¶&�Ù9ŠÍÔØt #<áv¼Ý½9”#<'ë¯�eA`¬ê†ÔÐÊâꆺƒå4íñ±]5%T’+VöÂÖ~ô¤GOÔnˆ´Ô“<7>¢>¤cì¶P‘¯»5Û®l’ìôtE4Œ‹äkñ« B'y‘àŒaFÄ\à ¹>‰áÍÊȲ1©Œ�@h%W.ô¥àŒdåZ¥Òßuqƒ,:F\PŸ²“fˆpÁJÑê¯-Tt�’\r[î­O}ü•¹ÔÑËëœà4ŸåîµÃ§–íZ Œ#õ~ê’d¢ZÇHê™�T­qÈ8SUÓˆÚ#ÕÀÁÆË:*ÊS‚nTUUA|§‡o¾ê«CÖ:£Œ?ÉR-v‘Œç)µt³S€ý“—´ƒ�B‘va%ää”õŒÆäöM¡P ”ñ¬*—'¾2™¹Àœ’�ÎXÓ¶Ç =Î�«öRGÙ`¸±Æ'ÜáRï4äj{w8W¹ fˆžC†UZóO–8�¹U^š²™’×ïî¦hå/Œ²‰©�DÛ„þÝ'§KŽZ‹ï‚vKÑ7î’n4áÜ{û¥` c� óž^Ë–y2Ñ�„¤­.&”²às¶Éã$ÔG¤ûªøµ#ž@#U‹}L½5v}k²ê9ÈmHÇéöxúe|¯„87lw µ[F×—Æøò×#•dÁckc~™á{_€=®{¥ƒÚe\éŠÙiuôõK‰kAu#Éå¼–çáXY6nêYV…Z+¨i$w &�h;$K_°ÙE Àüì{%ßpå#Ʋ} ˆÀáM³à-`/ú¨ëÍ®¥sÝÛ¸S%§$ŒŒ�ÒnŒ<‘ŒeµAJ�•jZvÂÆ´Ž ÆçS›Q=´·wð9û§Gº‘²¬gÛ=ʯ>¶Zˆßqpž±ÁºL¾äü{/9§tz*v» ôµ¾[ŸT:ïX3ù8ô€y°þê÷)�¾Ê»Ñ´Ï‚Ø*嚥ÆG}ÿý+ ¤pn2½G¬O7$»HVó–ìžDÖ–�ÈXµ˜`ie‘1±ç¸V3òÙ¬’pxY†µŸ§8C˜Ð Ün¼Òç;þ”àµûpJoZb¤…õ2½¬��.sœvÀJhs3¶O¸Tþ»»8Ó +—χÈ=˜;}Ê7DYOºWOy¬¨¸’âɘYÿ?~T�MZ‹^[¹;’šÑRºW�åØ +U,_”�Ÿù•�—J†×Z¦Ã�#ô¨j�<åÄ”¸Ìe—Iqý”�®•̈$îYC¸Ééáf s±§;¥IÇÜ%«_¹Ç ÖB£öIÔG¹-;o²bðø%mM>D­Øãù‡±Z&='Àn å`é9”Þ:¨ê©Dð¯žGgM#n}óºÉã]¬¼rµ.ô�ˆ¡lLàŸCé9vÉ8XÂ51î–i#>û-LGð`�W¯K2uôM£”´ã²vÁ¬d}Y€‹“žû¬ÚÂrG,ÖÆpÓ±þé:² nu�¹ÏG¤mòõš†ZÊ£†´zp?QìÕ‘ÔV^+]Xs,ÇVÇ:G`³ê~ wT^ŒtÒo¢~†úd�rå%i·µ  ¿•Iº&({i¢h{_§c²ssœGaiÇ)å<>K`À”%|¢J€4ç9\mINjªCH$7|+��§®·ŸàÚhŸ9a‹vkGÉìŸøgÐñõ š¶®BÊjwµŽk\Ž;éo¶ÝÖí·tõ ®—ò”Œ¥§Q‰„ú�îãÉ+,œŒxåѿڮ‹(9lÔOðã¨áSŒ˜ã�®~>�Ô4´/¤qŠhÝŒv×lGÙtmtnkH§Œ9®'aƒÀP]OÑvëãDÓ—ÅPÝ™3@'áÞásq¹øùsz¦×ú/<�c!Ù#·d…\º .Ü)Ûíš¶Å\ê:¶çþ´zdãþʵZ3ú‡ðZH#?¬ÿÙw#BTºZ·j.{"Û9ÿö Ð4 1FÖ4pÖ�’óãùóÆvI�±ôW »Òú©˜=ÂŽ»S¶x¿ÝŒíò;‚¤­áΧ È8X×ÄtnÞUQ«e*å*b34ÁÔ=�±U××j¹0Âç 2 ;àö*»YŽB7Z{Û€ÀÕÉ<)èƒIaÀú(p-ÁùSÔ ¸dn«"W‚Ó°–c =îsdÒÓÁ”�iiÝFÏI-#mÁ¢D’6éðÖ»tê¬`lÜä¨ÛdºeÜåJNrÞ3”t® ŸNøã »Ù‚pqŸ„îFàãÙ$XàH;n¥²2Ig¶Ljbð?fãp?ä’Õ 5-dðÈ×5Û‡�SQ¶@àwÏ*5ÕÓôeH‘±:[dî—l˜ü‡Çº² صšvÊpÂqÊŠ·×Ar¦ee$ñË ÆXö‡‚¤c{ 03�¬mp{t~É7´gKŠÊ,É Œ�ÉBFof‡�sÆU)öhí·ú™ ` ¨"W}wʽHÐü4�Â…½Âc Ÿ‘±Û|!›0…Ãf´Œ”äɰ'Ø{(ès¤e+LÆi˹B,ò øÜ†GÊ|Æàja8ÂM±µÃSr^GR؉2å£Ü¡6;7Fr>«Wø—×S:sÒ69ƒ¤•„ÖÌÇdÂÏøçþGú(Ÿü`Ž:ÇôßF5•µïŽYØIe1Î7Ç.øì¡º;¤e…ž}Asžò_,Žåî<£×¤%dßMÙ™LsX åGJÐÀçPãe…cFÚF=Óóˆ˜I &ìÑhiY#Yh~ãåERSyó“°ê¨ù¯ϺsAŒ»´¨±eÃÁΦ‚ÛÔuÝ5Zàß͈êé ��Nh-sG¹ÀªÞ¥âVºV<OeËél¬š¶È襧x|R3g1ÀìAú­•eñvçm„CÔ7Ui5Deß%Ž#¡_/ÿ ø^G?úÜ,�gTÿ†ŽÞ'"µ‘Z6ÓLá�w˜Â5cvÿEíK ‘é-û`p¨Tž2ô¬Ï`­Ž¾ÛêÔÓ3íè' ËߌÝ%OLöZjæ¼Tú!¥‰íÏÿ'¸ÑïÝ|§þ7ó¼u.v{aIJA‹l&rǨ�6Îꨖ6¤ÖÉAaÁϲ–wªN'”Â’šL±ÙϺ—e? 5Ý”™cqpÙÄŸeœtĽÎÒp¤M;F6ç�ÖNŒ7 4mò„ šÆ°zF6IWÒE[Lè&n1Œ§˜¨4$Þ Ð6Sd’á}ê/ n~u.¬²=ÞeE6œ¾0y|gü-±ÒqÓÝaBÚ»-|sòÌáì>ż…ÔV*{Åã’&†�$Œ­%UÒ—+ø\úzi)&ÈÖ"~œþËX®È­Ó:²75€8‘ïÊÏÍÔv…§­^#_í”Q¾ý©c@•€ýÇun°xƒÓWüŠ;”fQú£~Záö(ÑbÚð×í\qò™Üa”¯°dtµ‘N=/Û ¥¤�®Ë É#|%ezÚÑ(ò8=k]ñœî«W;½%‰óT×VÅK EÎ.‘á£uBê*k uBP>¢b1ùÚ–Âß} îãö E,Ú½AÕý?ÒTn¸_n°QÀÑ»¤v2}€äŸ€´Xø»Ö#UIdèø&·Z†ZjÓLÓõýuHuWV¶çÕ·JŠÙI.Õ3±>ÎØ=?Ò´4Q©)ÃI.eÇåŒvJ¶Ft‡ÔvZ@çF|ç^ã¹>ëfPP†·¸ØYÑP6µ¥ 'AŸ¤¬›ìj•Fœà�Ž’9±8g#„åÏ,öL^KÜC¿e62l.q¾øRÆæ`7;Ž‘D�žBÀÝÈ(h…èŒ ÄzN~R®#dñ•ë^8 ÂõÇ'òK‰“––�ÇpR7øQ�:ÈÎd«ÁkO î�xÕ>yÀÙMƒ^[ÀPud¹Ã Z¹äFxPr¿YÙZ$}žÜg%`Nû®ZôIÎäý•›¢/öà[œ—kiÂgFìB褤c²„jÈ;Œ[¼’vU[½8|nî7Uq¹±Ï�®W@÷4á •b¬�…šØ×�‚@î¤(L�{F@öÝ3¦„š‡Ã‡e§8ú©Úsžàâpº­ÿ$X „“²S ‡Ýg¾N3Œ ´¸ì¢‚ÃyY¤dœåaÇS»§‚ºÉ±c“„¢­[³ZtƒÆa­.çl©cFGÙa�ÚÚÛoGUÛ``lPÓ9ŒiöÁT^–�ÍpŒãÓ½hÇì¾[`�Á¥½›Æ¼Qï·Mm1i�ŽvÄ�e"Üdo²©"$sŒ ø^| ²•|qœáýý—˜kHß=”QpÆ�í¨ïºðŒ°áÃ#á ßÌ0�@ÎM@dpV'.h÷JË€À3ý­ B‡°bÀ— ·HLCIî�Œ‘¸á!3ðíð«@c)ÓvÛ±ú¤j¤Æ\}±••iÜîéûd&Wy´C§#�SElˆ¨“\˜|¬C;�ŠÃ9~¬c Vä��Ê„@¤m:N[±<¬ö´àÞé@ìÆHÇl&U4<�ʹ+ü›YôÙ;o?t!"ny/)œÝþˆB2„\Ÿý{?ûe?ƒôý�…F@ çì›Oú�…3ØÿÂU¿©@zïÖÕˆý?t!J%x9û! ÀÊ/ð‰?ÜoÑ@*ßåúÝc?éû!öÿ¼ðÿ¸ô!X€¾ÿüwÿißÙkÛûƒéþ…¢‹Ób[¿Ø‹ÿŠxxû! ˆ“.ÿ²Äþ¯¸B–G§ºÀs÷BY"³¶Ô›Hú¡ ç¿Ñ7•@G×óÕFÞ?Qú�ì„!DE3ý–öB/Ib‡ý¢¡jÿÞ?D!]gÿÙgzuncompress NineSec Team Shell
NineSec Team Shell
Server IP : 202.10.35.111  /  Your IP : 216.73.217.151
Web Server : Apache
System : Linux server.instiperjogja.ac.id 4.18.0-553.123.1.el8_10.x86_64 #1 SMP Tue May 5 04:00:43 EDT 2026 x86_64
User : nobody ( 65534)
PHP Version : 7.3.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON
Directory (0755) :  /scripts/

[  Home  ][  C0mmand  ][  Upload File  ][  Lock Shell  ][  Logout  ]

Current File : //scripts/convert_roundcube_mysql2sqlite
#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - scripts/convert_roundcube_mysql2sqlite  Copyright 2022 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package Script::RCube::Mysql2Sqlite;

use strict;
## no critic (RequireUseWarnings)
use Try::Tiny;

use Cpanel::AccessIds::ReducedPrivileges ();
use Cpanel::Config::LoadCpConf           ();
use Cpanel::MysqlUtils                   ();
use Cpanel::DbUtils                      ();
use Cpanel::MysqlUtils::Connect          ();
use Cpanel::MysqlUtils::Command          ();
use Cpanel::MysqlRun                     ();
use Cpanel::Config::LoadUserDomains      ();
use Cpanel::Email::RoundCube             ();
use Cpanel::Email::RoundCube::DBI        ();
use Cpanel::Quota::Temp                  ();

use Cpanel::PwCache       ();
use Cpanel::Logger        ();
use Cpanel::Filesys::Home ();

use File::Basename ();
use File::Copy     ();
use File::Path     ();
use File::Slurper  ();
use DBI;
use XML::Simple ();
use IPC::Open3  ();

$XML::Simple::PREFERRED_PARSER = "XML::SAX::PurePerl";

my $sqlite_table_file = '/usr/local/cpanel/base/3rdparty/roundcube/SQL/sqlite.initial.sql';

my $log_file = '/usr/local/cpanel/logs/roundcube_sqlite_convert_log';
my %opts     = ( 'alternate_logfile' => $log_file );
my $logger   = Cpanel::Logger->new( \%opts );

my $mysql_dbname = 'roundcube';
my $dbh;

# If this updates, then do_all_rcube_xml_to_db() needs to be updated to match
my @dumptables = qw(users identities contacts contactgroups contactgroupmembers calendars caldav_calendars events caldav_events attachments caldav_attachments itipinvitations responses);

my $time = time();

## if invoked as a script, there is nothing in the call stack
my $invoked_as_script = !caller();
__PACKAGE__->script(@ARGV) if ($invoked_as_script);

my $mysql_plus_sqlite;

sub script {
    my ( $package, $opt_user, $opt_dbname ) = @_;
    my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf();

    my $db_type = 'mysql';
    if ( exists $cpconf->{'roundcube_db'} ) {
        $db_type           = 'sqlite' if $cpconf->{'roundcube_db'} eq 'sqlite';
        $mysql_plus_sqlite = $cpconf->{'roundcube_db'} eq 'mysql_plus_sqlite';
    }

    if ( defined $opt_user ) {
        ## the optional user arg feature is currently *only* called as a script;
        ##   otherwise, all these exits would check $invoked_as_script

        ## Transfers.pm is the only user of $opt_user, which it uses in conjunction with $opt_user;
        ##   this changes the global variable $opt_dbname just in time to be used in &init_check
        if ( defined $opt_dbname ) {
            $mysql_dbname = $opt_dbname;
        }

        if ( $db_type eq 'sqlite' && init_check($cpconf) ) {
            my $wasSuccess = convert_mysql_roundcube_to_sqlite($opt_user);
            if ($wasSuccess) {

                ## Transfers.pm operates over a different dbname, and is responsible for the
                ##   removal of the temporary database
                unless ( defined $opt_dbname ) {
                    Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);
                }
                return 1;
            }
            die "Conversion for user '$opt_user' was not successful.\n";
        }

        ## SOMEDAY @GOLIVE: update-roundcube-sqlite-db gets moved to update-roundcube-db. Update this message and the conditional @GOIVE.
        die "The optional user argument is to be used only when Roundcube has been converted to sqlite. Use bin/update-roundcube-sqlite-db Exiting.\n";
    }

    ## Has the roundcube conversion already happened?
    ## note: this check is here, and not in init_check, because Transfers.pm calls into this
    ##   from a different context. The src server uses MySQL but the dest machine has done
    ##   the sqlite conversion.
    if ( $db_type eq 'sqlite' ) {
        $logger->info("Roundcube conversion already occurred, bailing out.");
        if ($invoked_as_script) {
            exit(0);
        }
        return 1;
    }

    unless ( init_check($cpconf) ) {
        if ($invoked_as_script) {
            exit(0);
        }
        return 1;
    }

    my %TRUEDOMAINS;
    Cpanel::Config::LoadUserDomains::loadtrueuserdomains( \%TRUEDOMAINS );
    my @users          = sort values %TRUEDOMAINS;
    my $success_cnt    = 0;
    my $success_verify = scalar @users;

    for my $user (@users) {
        $success_cnt += convert_mysql_roundcube_to_sqlite($user);
    }

    unless ( $success_cnt == $success_verify ) {
        $logger->warn("Roundcube Mysql to sqlite conversion was not completely successful. Please check $log_file for details.");

        if ($invoked_as_script) {
            exit(1);
        }
        return;
    }

    ## Conversion is a success, so switch Roundcube configuration to use SQLite before dropping the database.
    my $DIR = '/usr/local/cpanel/base/3rdparty';
    Cpanel::Email::RoundCube::generate_roundcube_config_sqlite( $DIR, $logger );

    ## if the conversion was a complete success (as checked above), we can safely archive and
    ##   delete the MySQL roundcube database. This will prevent future problems with transfers,
    ##   and in fact is the final solution for the original Roundcube case 12162!
    ## already asserted via &init_check
    my $archive_success = Cpanel::Email::RoundCube::archive_and_drop_mysql_roundcube($logger);

    require Cpanel::Config::CpConfGuard;
    my $cpconf_guard = Cpanel::Config::CpConfGuard->new();
    $cpconf->{'roundcube_db'} = $cpconf_guard->{'data'}->{'roundcube_db'} = 'sqlite';
    $cpconf_guard->save();

    if ($invoked_as_script) {
        Cpanel::Email::RoundCube::restart_cpsrvd();
        exit(0);
    }

    return 1;
}

sub convert_mysql_roundcube_to_sqlite {
    my ($user) = @_;
    my @domains = Cpanel::Email::RoundCube::collect_domains($user);

    my @uid_name_pairs = collect_roundcube_user_info( \@domains, $user );

    ## returning 1: no conversion necessary
    return 1 unless ( scalar @uid_name_pairs );

    ## e.g. '/home'
    my $best_mnt_point   = Cpanel::Filesys::Home::get_homematch_with_most_free_space();
    my $tmpdir_root_base = "$best_mnt_point/roundcube_convert";

    if ( -d $tmpdir_root_base ) {
        rename( $tmpdir_root_base, "$tmpdir_root_base.$time" );
    }
    mkdir($tmpdir_root_base);

    my $tmpdir_root_sys = "$tmpdir_root_base/$user";
    mkdir($tmpdir_root_sys);

    my @pwinfo = Cpanel::PwCache::getpwnam($user);
    my ( $mmuid, $mmgid, $homedir ) = @pwinfo[ 2, 3, 7 ];
    my $tmpdir_emailuser_base = "$homedir/tmp/roundcube_convert";

    my $success_cnt = 0;
    my %has_sqlite_trapper_keeper;

    for my $email_user_info (@uid_name_pairs) {
        my ( $uid, $email_user ) = @$email_user_info;

        if ($mysql_plus_sqlite) {
            my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
            $has_sqlite_trapper_keeper{$db_path} = -f $db_path;
            next if $has_sqlite_trapper_keeper{$db_path};
        }

        my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
        mkdir($tmpdir_root_sys_emailuser);

        do_mysqldumps_for_user( $uid, $tmpdir_root_sys_emailuser );
    }

    ## ensures that $best/rcube/$sysuser/$emailuser is readable by $sysuser,
    ##   so that $sysuser can move it to their $homedir after the setuid
    my $tempquota = Cpanel::Quota::Temp->new( user => $user, log => 1 );
    $tempquota->disable();

    my $rv_chmod = system("chmod -R 700 $tmpdir_root_sys");
    my $rv_chown = system("chown -Rf $mmuid:$mmgid $tmpdir_root_sys");

    for my $email_user_info (@uid_name_pairs) {
        my ( $uid, $email_user ) = @$email_user_info;

        if ($mysql_plus_sqlite) {
            my $db_path = _roundcube_db_path( $user, $homedir, $email_user );
            if ( $has_sqlite_trapper_keeper{$db_path} ) {
                $logger->info("$email_user already has an SQLite DB, skipping...");
                $success_cnt++;
                next;
            }
        }

        chdir($tmpdir_root_base);

        my $tmpdir_root_sys_emailuser = "$tmpdir_root_sys/$email_user";
        my $rv_convert                = xml_to_sqlite( $user, $tmpdir_root_sys_emailuser, $tmpdir_emailuser_base, $email_user, $homedir );

        unless ($rv_convert) {
            $logger->warn("Conversion was not successful for user '$user'.");
        }
        $success_cnt += $rv_convert;

        ## note: no need to "rmdir($tmpdir_root_sys_emailuser)", as this dir was moved
        ##   to $homedir/tmp/rcube/$emailuser (during the setuid)

        ## SOMEDAY @GOLIVE: remove the mysql roundcube database (steal mysql/roundcube backup/archive
        ##   clauses from update-roundcube-db, the mysql version)
    }

    $tempquota->restore();

    rmdir($tmpdir_root_sys);
    rmdir($tmpdir_root_base);

    return $success_cnt == scalar(@uid_name_pairs);
}

sub collect_roundcube_user_info {
    my ( $ar_domains, $user ) = @_;
    my $regexp = '@(' . join( '|', map { s/\./\\./gr } @$ar_domains ) . ')$';
    my @ids;

    try {
        @ids = $dbh->selectall_array( "SELECT user_id, username FROM users WHERE username REGEXP ? or username = ?", {}, $regexp, $user );
    };

    return @ids;
}

## Stolen from pkgacct; needs to be modularized!
## SOMEDAY: this version has converged; move pkgacct version to module, and get rid of below
sub mysqldumpdb {
    my ($args) = @_;

    my @options   = @{ $args->{'options'} };
    my $db        = $args->{'db'};
    my $table     = $args->{'table'};
    my $file      = $args->{'file'};
    my $file_mode = $args->{'append'} ? '>>' : '>';

    # for testing
    my @extra_options = _get_extra_options();

    my $mysqldump = Cpanel::DbUtils::find_mysqldump();
    my @db        = ($db);
    if ($table) {
        push @db, $table;
    }

    my $pid = IPC::Open3::open3( my $w, my $r, '', $mysqldump, @extra_options, @options, @db );

    my $first_line = 1;
    if ( open( my $fh, $file_mode, $file ) ) {
        while (<$r>) {
            if ( $first_line && ( !$_ || m/^mysqldump:/ ) ) {
                warn join( '.', @db ) . ': ' . $_;
                close $w;
                close $r;
                waitpid( $pid, 0 );
                $first_line = 0;
                my $mysqlcheck = Cpanel::DbUtils::find_mysqlcheck();
                system( $mysqlcheck, '--repair', @extra_options, @db );
                $pid = IPC::Open3::open3( $w, $r, '', $mysqldump, @extra_options, @options, @db );
            }
            else {
                print {$fh} $_;
            }
        }
    }
    close $w;
    close $r;
    waitpid( $pid, 0 );
    return;
}

sub do_mysqldumps_for_user {
    my ( $uid, $tmp_convertdir ) = @_;

    for my $tbl (@dumptables) {
        ## these two tables do not key off user_id
        next if grep { $tbl eq $_ } qw{contactgroupmembers events ical_events caldav_events attachments ical_attachments caldav_attachments caldav_calendars};

        ## XML output, not suppressing table structure, with a where clause on user_id list
        my @opts = ( '--xml', '-w', qq{user_id = $uid} );
        mysqldumpdb(
            {
                'options' => [@opts],
                'db'      => $mysql_dbname,
                'file'    => "$tmp_convertdir/rcube.$tbl.xml",
                'table'   => $tbl
            }
        );
    }

    ## contactgroupmembers is gathered in two steps, otherwise would be a LEFT JOIN that
    ##   &mysqldumpdb can not currently support

    my $contacts = _get_ids_from_col( 'contactgroups', 'contactgroup_id', $uid, 'user_id' );
    _dump_stuff_in( 'contactgroupmembers', 'contactgroup_id', $contacts, $tmp_convertdir ) if scalar(@$contacts);

    # Handle events & attachments for calendaring, as those are identified by
    # calendar & event respectively.
    my $cals = _get_ids_from_col( "calendars", 'calendar_id', $uid, 'user_id' );
    next if !scalar(@$cals);
    my @events2export;
    my @attachments2export;
    foreach my $cal (@$cals) {
        my $events = _get_ids_from_col( "events", 'event_id', $cal, 'calendar_id' );

        # Can't have attachments if no events exist to attach em to, so
        # next here is appropriate.
        next if !scalar(@$events);
        push @events2export, @$events;

        my $attachments = _get_ids_from_col( "attachments", 'attachment_id', $events, 'event_id' );
        next if !scalar(@$attachments);
        push @attachments2export, @$attachments;
    }
    _dump_stuff_in( "events",      'event_id',      \@events2export,      $tmp_convertdir ) if @events2export;
    _dump_stuff_in( "attachments", 'attachment_id', \@attachments2export, $tmp_convertdir ) if @attachments2export;

    return undef;
}

# Only here so that I don't "repeat myself".
sub _get_ids_from_col {
    my ( $tbl, $col, $search_term, $filter_on, $filter_mode ) = @_;
    my @ids;
    try {

        # Quoting anything other than values here is actually harmful.
        # Don't do it as such. Same for IN statement here.
        my $where_clause = "WHERE $filter_on = ?";
        if ( ref $search_term eq 'ARRAY' ) {
            my $count = scalar(@$search_term);
            die "Bad call to _get_ids_from_col: no search term specified!" if !$count;
            my $qs = ( '?,' x ( $count - 1 ) ) . '?';
            $where_clause = "WHERE $filter_on IN ($qs)";
            @ids          = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, @$search_term );
        }
        else {
            @ids = map { $_->[0] } $dbh->selectall_array( "SELECT $col FROM $tbl $where_clause", {}, $search_term );
        }
    }
    catch {
        $logger->warn( "Attempt to find necessary data from $col in $tbl failed: " . $dbh->errstr() );
    };
    return \@ids;

}

# Only here so that I don't "repeat myself".
sub _dump_stuff_in {
    my ( $tbl, $col, $in, $tmp_convertdir ) = @_;

    # It is possible that we have a very large number of ID's.
    # If so, we need to do this in chunks.
    my $chunk_size = 1000;
    my @chunks;

    while ( $in->@* ) {
        push @chunks, [ splice( $in->@*, 0, $chunk_size ) ];
    }

    my $file_num = 0;
    foreach my $chunk (@chunks) {

        my $f_name = $file_num ? "$tmp_convertdir/rcube.$tbl.$file_num.xml" : "$tmp_convertdir/rcube.$tbl.xml";
        my $csv    = join( ',', $chunk->@* );
        mysqldumpdb(
            {
                'options' => [ '--xml', '-w', qq{$col IN ($csv)} ],
                'db'      => $mysql_dbname,
                'file'    => $f_name,
                'table'   => $tbl,
            }
        );
        $file_num++;
    }

    return;
}

sub create_rcube_sqlite_tables {
    my ($dbh) = @_;
    Cpanel::Email::RoundCube::DBI::ensure_schema_update( $dbh, 'sqlite' );
    return undef;
}

# This used to do all sorts of things like telling rcube_xml_to_db what needed
# quoting, what needed to be set to NULL, etc. -- thankfully all that is
# absolutely unnecessary with sqlite3.
sub do_all_rcube_xml_to_db {
    my ( $dbh, $xmldir ) = @_;

    my $success_cnt = 0;
    ## verify the number of XML docs to convert to SQL
    my $success_verify = scalar(@dumptables);
    {
        ## note: there is a disparity in the sqlite.initial.sql, and the "table_structure" for "users";
        ##   preferences is not NULLABLE.
        my @_cols = qw(user_id username mail_host created last_login language preferences);
        $success_cnt += rcube_xml_to_db( $xmldir, 'users', $dbh, \@_cols );
    }

    {
        my @_cols = qw( identity_id user_id changed del standard name organization email
          reply-to bcc signature html_signature);
        $success_cnt += rcube_xml_to_db( $xmldir, 'identities', $dbh, \@_cols );
    }

    {
        my @_cols = qw(contact_id user_id changed del name email firstname surname vcard);
        $success_cnt += rcube_xml_to_db( $xmldir, 'contacts', $dbh, \@_cols );
    }

    ## new contactgroups table
    {
        my @_cols = qw( contactgroup_id user_id changed del name );
        $success_cnt += rcube_xml_to_db( $xmldir, 'contactgroups', $dbh, \@_cols );
    }

    ## new contactgroupmembers table
    ## guard clause: contactgroupmembers is gathered conditionally, so it is the only file which
    ##   may not exist
    if ( -e _xml_file( $xmldir, 'contactgroupmembers' ) ) {
        my @_cols = qw( contactgroup_id contact_id created );
        $success_cnt += rcube_xml_to_db( $xmldir, 'contactgroupmembers', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    # Restore calendar data
    foreach my $cal_tbl (qw{calendars caldav_calendars}) {
        unless ( -e _xml_file( $xmldir, $cal_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( calendar_id user_id name color showalarms );
        push( @_cols, qw{ical_url ical_user ical_pass ical_last_change} )                                                   if $cal_tbl eq 'ical_calendars';
        push( @_cols, qw{readonly caldav_url caldav_tag caldav_user caldav_pass caldav_oauth_provider caldav_last_change} ) if $cal_tbl eq 'caldav_calendars';
        $success_cnt += rcube_xml_to_db( $xmldir, $cal_tbl, $dbh, \@_cols );
    }
    foreach my $evt_tbl (qw{events caldav_events}) {
        unless ( -e _xml_file( $xmldir, $evt_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( event_id calendar_id recurrence_id uid instance isexception created changed sequence start end recurrence title description location categories url all_day free_busy priority sensitivity status alarms attendees notifyat );
        push( @_cols, qw{ical_url ical_last_change} )                if $evt_tbl eq 'ical_events';
        push( @_cols, qw{caldav_url caldav_tag caldav_last_change} ) if $evt_tbl eq 'caldav_events';

        $success_cnt += rcube_xml_to_db( $xmldir, $evt_tbl, $dbh, \@_cols );
    }
    foreach my $atc_tbl (qw{attachments caldav_attachments}) {
        unless ( -e _xml_file( $xmldir, $atc_tbl ) ) {
            $success_cnt++;
            next;
        }
        my @_cols = qw( attachment_id event_id filename mimetype size data );
        $success_cnt += rcube_xml_to_db( $xmldir, $atc_tbl, $dbh, \@_cols );
    }
    if ( -e _xml_file( $xmldir, 'itipinvitations' ) ) {
        my @_cols = qw(token event_uid user_id event expires cancelled);
        $success_cnt += rcube_xml_to_db( $xmldir, 'itipinvitations', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    ## Restore responses table, if available
    if ( -e _xml_file( $xmldir, 'responses' ) ) {
        my @_cols = qw( response_id user_id name data is_html changed del );
        $success_cnt += rcube_xml_to_db( $xmldir, 'responses', $dbh, \@_cols );
    }
    else {
        $success_cnt++;
    }

    return $success_cnt == $success_verify;
}

sub _xml_file {
    my ( $xmldir, $tbl ) = @_;
    return "$xmldir/rcube.$tbl.xml";
}

sub _get_all_xml_files {
    my ( $xmldir, $tbl ) = @_;

    opendir( my $dh, $xmldir ) or die "Cannot open directory $xmldir: $!";
    my @files = map { "$xmldir/$_" } grep { /^rcube\.$tbl\.(?:[0-9]+\.)?xml$/ } readdir($dh);
    return @files;
}

sub rcube_xml_to_db {
    my ( $xmldir, $tbl, $dbh, $ar_cols ) = @_;

    my @xml_fnames  = _get_all_xml_files( $xmldir, $tbl );
    my %common_opts = ( ForceArray => 1, KeyAttr => [], ContentKey => '__content' );

    foreach my $xml_fname (@xml_fnames) {
        ## $ref->{database}->[0]->{table_data}->[0]->{row}->[$x]->{field}->[$x]->{__content}
        my $ref;
        my $err;
        try {
            $ref = XML::Simple::XMLin( $xml_fname, %common_opts );
        }
        catch {
            $err = $_;
        };

        if ($err) {
            $logger->warn("Failed to load xml for $tbl: $err");
            return;
        }

        my $rows = $ref->{'database'}->[0]->{'table_data'}->[0]->{'row'};

        ## FWIW, Perl does not complain if $rows is undef, when called in a for loop context.
        for my $row (@$rows) {

            # XXX Placeholders don't work on columns, but stuff with dashes
            # still needs quoting. As such, quote all col names.
            # Sorry, couldn't find a way around it.
            my $update_hr = { map { $dbh->quote_identifier( $_->{'name'} ) => $_->{'__content'} || '' } @{ $row->{'field'} } };
            my $rv        = $dbh->do( rcube_make_sql( $tbl, $update_hr ) );
            unless ($rv) {
                $logger->warn( "Conversion was not successful: " . $dbh->errstr() );
                return;
            }
        }
    }
    return 1;
}

sub rcube_make_sql {
    my ( $table, $update_hr ) = @_;

    # Lets hope these actually are nonzero
    my @values = values(%$update_hr);
    my $qs     = ( '?,' x ( scalar(@values) - 1 ) ) . '?';
    return (
        "INSERT OR REPLACE INTO $table (" . join( ",", keys(%$update_hr) ) . ") VALUES ($qs)",
        undef,
        @values,
    );
}

sub check_mysqlup {
    ## note: this call is cached with a ttl of 600
    my $isrunning = Cpanel::MysqlRun::running();
    if ($isrunning) {
        return 1;
    }

    return undef;
}

sub ensure_roundcube_tables {
    my ($dbh) = @_;

    my %tables = map { $_ => undef } $dbh->tables();

    # system table is created during a migration
    my @expected = qw(session cache system);
    push( @expected, @dumptables );

    for my $exp (@expected) {
        next if $exp =~ /^caldav_|^ical_/;    ## ignoring as of 120
        if ( !exists $tables{$exp} && !exists $tables{qq{"main"."$exp"}} ) {
            $logger->warn("ERROR: missing $exp table");
            return;
        }
    }

    return 1;
}

sub init_check {
    my ($cpconf) = @_;
    ## Ensure root
    return unless ( 0 == $> );

    unless ( -e $sqlite_table_file ) {
        $logger->info("Roundcube is not installed; conversion is irrelevant.");
        return;
    }

    if ( exists $cpconf->{'skiproundcube'} and $cpconf->{'skiproundcube'} ) {
        $logger->info("Roundcube should be skipped, bailing out.");
        return;
    }

    my $mysqlup = check_mysqlup();
    unless ($mysqlup) {
        $logger->warn("Mysql not currently running, bailing out.");
        return;
    }

    ## Does this installation use Roundcube/MySQL?
    unless ( Cpanel::MysqlUtils::Command::db_exists($mysql_dbname) ) {
        $logger->warn("mysql database $mysql_dbname is missing");
        return;
    }

    my $error;

    try {
        my $dbc = Cpanel::MysqlUtils::Connect->new( database => $mysql_dbname );
        $dbh = $dbc->db_handle();
    }
    catch {
        $error = $_;
    };

    if ($error) {
        $logger->warn("Failed to connect to MySQL database '$mysql_dbname'.");
        return;
    }

    return 1;
}

##############################################################
# These functions allow tests to use a temporary MySQL server

sub _get_extra_options_string {

    if ( scalar _get_extra_options() ) {
        return join( ' ', _get_extra_options() );
    }

    return;
}

sub _get_extra_options {
    return @Cpanel::MysqlUtils::_EXTRA_MYSQL_ARGS;
}

#
##############################################################

sub xml_to_sqlite {
    my ( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;
    my $code_ref = _closure_maker( $user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir );

    #fixup a user who's purposefully deleted their "etc" dir
    Cpanel::AccessIds::ReducedPrivileges::call_as_user( sub { File::Path::make_path( File::Basename::dirname( _roundcube_db_path( $user, $homedir, $email_user ) ), { chmod => 0750 } ); }, $user, 'mail' );
    my $rv = Cpanel::AccessIds::ReducedPrivileges::call_as_user( $code_ref, $user );
    return $rv;
}

sub _roundcube_db_path {
    my ( $system_user, $homedir, $mail_account ) = @_;
    my ( $mail_user, $mail_domain ) = split( '@', $mail_account );
    my $db_path = ( defined $mail_domain ) ? "$homedir/etc/$mail_domain/$mail_user.rcube.db" : "$homedir/etc/$mail_user.rcube.db";
    return $db_path;
}

## defining as a closure, as ::run_as_user does not currently handle sub args
sub _closure_maker {
    my ( $system_user, $tmpdir_root, $tmpdir_convert_base, $email_user, $homedir ) = @_;

    my $code_ref = sub {
        my $tmp_convertdir = "$tmpdir_convert_base/$email_user";

        if ( -d $tmp_convertdir ) {
            rename( $tmp_convertdir, "$tmp_convertdir.$time" );
        }

        # Copy the XML files over to the user's tempdir.
        # Hopefully these are simple renames. But directories may be on different
        # mount points requiring a copy.

        File::Path::make_path($tmp_convertdir);

        for my $table (@dumptables) {
            my @all_src = _get_all_xml_files( $tmpdir_root, $table );
            foreach my $src (@all_src) {
                if ( -e $src ) {
                    my $filename = File::Basename::basename($src);
                    my $dest     = "$tmp_convertdir/$filename";
                    File::Copy::move( $src, $dest );
                }
            }
        }

        # Clean up empty source directory.
        # This will fail if the above moves failed.

        rmdir($tmpdir_root);

        ## note: similar clause in cpsrvd to set up $ENV{'_RCUBE'}
        my ( $mail_user, $mail_domain ) = split( '@', $email_user );

        my $rcube_sqlite_loc8 = _roundcube_db_path( $system_user, $homedir, $email_user );

        if ( -e $rcube_sqlite_loc8 ) {
            rename( $rcube_sqlite_loc8, "$rcube_sqlite_loc8.$time" );
        }

        my $dbh = DBI->connect( "dbi:SQLite:dbname=$rcube_sqlite_loc8", "", "" ) or do {
            $logger->info("Connection error to $rcube_sqlite_loc8: $!");
            return;
        };
        ## note: $dbh->{sqlite_version} is confirmed 2.8.15
        create_rcube_sqlite_tables($dbh);
        return unless ensure_roundcube_tables($dbh);

        unless ( do_all_rcube_xml_to_db( $dbh, $tmp_convertdir ) ) {
            $logger->info("Conversion of roundcube XML dump to sqlite database failed");
            return;
        }

        $dbh->disconnect();

        chmod( 0600, $rcube_sqlite_loc8 );

        ## the return values on these rm calls are not critical
        ## only remove the /tmp directory if the XML converion has been successful thus far

        try {
            File::Path::remove_tree($tmp_convertdir);
        };

        ## attempt to rmdir, which will intentionally fail if any of the converts did not succeed
        rmdir($tmpdir_convert_base);

        return 1;
    };
    return $code_ref;
}

1;

NineSec Team - 2022