JFIFC   %# , #&')*)-0-(0%()(C   (((((((((((((((((((((((((((((((((((((((((((((((((((" ,.Fh Ch@ 10D``DBB h4 @dX bD iD ІI$TBB'$"`I)Eb`(m9@0hb&!1114  b` Dh "lTH)TAiN  A" hf%n£!aY4hcC"5J2#Tզ@ #(a`QI+JHB8h@!!!hSMNhC4$11SB!`&2Dc(p*`"XE b!IJ&0C41 b `hL0JHLi1L -XX`ݚb% )*Cp& ! $40)!b䜢hC@D 6JJቨ4B!`b `0@ b`&ё^IÆ LO7dX h@)A "I`6H !L'@ DQ B!Bj4  L@ @hb&%$ D LQ~7ҜtZ&pӘ b `&)F؆` 7DBB&qI:LVF2B1 5iL4$ mj4 @ @ b`0b iS` 14V1l˦I7 @` L&ȒB[lC!FlIY +@!"!%$  HX J00CبDE18! L r2ϳ>Tس:=8Ӓb  & !`) "0$EMSIAL6D$B`&BBX&1C CT4h! @@4 0Yf |,tCE\T}nn` b$1AN&$ &IS`0118` 4  9_^8B14yꞿ3wlK 7 &@ 0@ @ `Ȓ b( +$2DR:]Z3cqcAȴNb11@#@18b`!upyt|z8lZ+]}3:zKcwA9SUU5AJ   2LUp*HR+EUEvF2qIW8)-JYDUQ  b `16!B& n$I9y~yntpX"QE,m[&C44 b ``@  BQ0&:Qud J7*"S-5(J7U@`  b1n.2/| ZrJY]3~ڕyצ1Ͳʬ3}[9NΨWVun}Tc~g6g=Mq6}GKsx b``L!nu"6ڬQ}_4 4IMtSҫ(610 b`],k4r:\_GOn骻q[,C*ͳԖzhUݐ9w L01 L& hQm(4d]nNiF wfG&ܱx*uθIbBʤSnܢaFj(@`8箄Ꝿ&IltgxgɻM%Mږ{z)]vSqUټ& b`0CT 8&`% '** -L/(4$cךRjp.h @1b!  0n7ʮB Kt}UF˞tr\7Jϖ~%Ҹ[!hUqp!&7Č1] *O4צN.Ǽt0!J%S101CC&1 Lh b bey ?fW7Ƨ,ʒ2t}֚m[PzvvF@ʀ  hb @%(#!!bBâM4BF=x Pցdd'YS̷ͬ 118h`bSv\>}Ux/ޝ7UI5h,pܞ^[U9=&v8@I!(XjaS,S3]av(KWP4j` -#ݒ7Jն&W"1t!^0 ! &X2y=yomNz.zVwfKݚж26ϗMa5L0C]q$8EQTl;yj]\U:znT62U f%uLb!  o7Q/{jyϣCJgS[oޮOO>_W6O~oC,2T`44\3zc(B A\cuݛU4컗AK2B6vǷ\n9WXQ,y:Bz` `4 @C&r_'RdxyNu <SQUM+#S⎬7v㦩K]Jy:KX5b`!!)*d 1RYn+έӚKUJX7U3˟EA}lŪe6@@  b b4x2\>|z^WvB{3^S׺Np^kέ㜅VզhW6rw{xz=)@h+ !daYZC.~mQniڲ7|0Qgj_J}l;8Po)Ά>4 @dtsNqОgͷ>ǻ \T`ыfNf7(pu9|]͙c{#(h1 @ @}6yn;*SHI*Bj"9̻&{y]4գ7>Wf~םZ0niMRsTH/NL` @ `/9ywVY-tkZJ~sGCz|z[cV-KX+csSTWu6kK2"2QiM b6y֝^]k׍ʻK=U**MVK2R.ZE9}v6{i1m]jZҌRUJ)De%dR*K~eS>-у$eͮsuκh%lGNl8#~:n5Yߎqf?L'@ @ojȲ*d.ܴn3q$ngլNKbS%{ߓ\qM(zOk=R͕zX_~=hE'J]\YA&]ƣLk4>5tdUFm8ʋ+7T+K-%3oU]kRKV=cNjkCiGY)s󝧂뫟CX=na\^ RgOA5F|-P_ew9jWM;暜Q}rUh;p_>|+ng<%̙uӧ>phss.SE67FH[W+8sc<=3Z_FJ^Mz('.Rǖ=<}<=hr7Z6v"pV-:jS٩}vf2UeYN\K JN*|y.!~O{ k#;1rt݃:>8sVL]*gs*-dY*Wdnb b&@?=1Ms*|ZW3VY.+ӋcSZg EWfgvZNDeSBWʋ$ӟLu?CԎvܚ/\hُR]zu3&UWZRvj^l[֢3u[ةZ2=Ox]wԥΛbyu͝p뚫3UsaVX;I>7~xgpa;_կM5yĔ1dD׳<K}*D&P&@18{N]n)E=Mg_811YGE) "J cMQ]e3>_Q=:f]IzTQS US-izΛ$Iv3Q]]JM$[VT *N5-eBHJO<侴euRVzseOv--m(JƬi`jKڹW+n}1Z^.sLyq9}4/sw@ZH!]M&y،l-nq沯Ets'mi9E: Q"Z 5ֽC^mkV[ʝ>]3n2,#\B `T(U6-N,gF~&[bB^w*<=UÎ+mBePW:IPڪ7䫲anm J0 Pg=iQpڎz\~-kRqXl9]O.w}Ku&kSuHS $BRee:̢r fnYmSE9Hr3PQuVE 6AM "vty|yU.Y!nm4kqB.N4UdF鶫,qLں[e ⒅kYknpwBϓU>^Ѳ+214E8,:"=YվٛG\N{UǭJ1؆( -Rd [ۏͣ1f^6%fF$sB̠YUӲs]0 &\Z\_dL)f{!f7}6_w5SYŵUUYe]=73uԌybv#3]ё+fXx?ί'jĪZ'KZCOmVg ٚ5![omjbїxue ؒuU̔g5ziW:7':]Uˎ:ur;ês솅Dq#$BGVQ}cWQd.ŋZ5yrhgg^1ʎxGo|u?=%[V63fH41ӿFBwwnlӯǵ*vp$FJdi::qӏ^|{sF5skb+b;+ɳǽy9mIAJ1ɚz9j]<+htU!lNZ`tafcʍ4⁳G/LJ|TZ5%TͲBLSd-.ط%ؓ5ˡæRdĉV bc@$::v֋oV\fwtr~.V:2.8n.YX͎hk1.Jvտ}ڸm볧-%\s^Lݾ}fƥ<;9 o-^,/B9T,ųXҬ o,4 hxiӛfR-zlFfR&oSG/G=fl"#o %$4W٫#1e;Y(62+W4:lt#:;1[G3YfzseN8dًI8Oy@ԉ``&!#8Hs3_OFRثRulvth;Ì:dl @TqVR* ˣnsuX4%y:f2h]KƣVi%:f'w?LkU?,iÑIg]B%6aUiUg&>zuƧM_5^^Z役:stNg\Y+6ٞEֹgZγV5vkD-d=y55(&: F%`Ȏ-@ 9}l|dNPGDWmp%܍=mbZFlӺ23jqъuپY|| FxiP+$'*싶M+oEșPBf x8O;)3:!319t5!K kϥ:o 鞖3;=QY٣ܘ0JCM`I5f|֭sb)[b6xe8Ne!Bq2c8&(Nv񺭁TmdB6AI"^OOA(D#4o,i󶞼 ^ϯɽEz{κչ  J2# J0lewn~̚!)N(џLbU9:x}qҲ6m~/LmҘ>F蛖޿q]V FbRF|qV]ب5ltO՜&e\u5N\&\تP ʕ^dKN}!F'3ԌIT-!Ќ\%||&zcy].:yٿ,n㨍vL1I"5I4ЇJ+y_4t[Aݦ>f:i2\2eP۱kqED1g۟NxǫOMJ4uH\EūB ]I!["IHl>GW t0peEN]2_g:nm#7S{qR7.ŲAVL,qhJ A$n,iօ7>]0g3MiKkK^#PJ8@LjVD,kU yz̪|NKυI@.v}5wy}~cLIWw!o )E(JT1RjґW{!#4}g(CD%bJ+WKO+ &3doFtr걤Zabb!ͫ7%ѯךU-Ăj*ÿУTҷ=|<=X[q6*iC"(d'"$- yyTnh-|z]fSn'dZ1Ky} />u_3\8 Nz8~GLP;iHvL@`SM"1`8x`q/mAI}E9qOןơ^r2U`JP,cBkW!$I)d+bܩir+уXJ-)~tc>&ĂVB-K_?z$. h0R)F@9"ʑe>\z\;5P:M9u9ɮsaOz{qҬsq6ȦN@gm ;\$8' #R#%M_28ІU[j,#"˟P=++| g!4n^䪶 i5P$ϮYCc`Wr^010#Њr3$H ۀ29# ?ӯ ,q=ی;G0O,, 4A@83s3o !<5-׼ 1?430D$a ;8cO4 ̲9G&o4 1ͫ?8<3w>9? 6 8E Ǡ~ߙs,< ,/1\O8<:Հn:,ӽDb.4'8+Jr<<9]+rˑ0 <8"CP/ < s c?2<O;x7}000 Á(N5M0ϯFo<Q!w0 # L4Ҏ +1`=LѨAuM 8 @h  Ϊg0[8d_o|n00 8 whhtS/-ŸsC8 0 07o8$ڍ"ʘq{ T2ѱa0sFsrљu[ ?Nz2"8fɒ{Oc1+3vzM|"D:I}KYaLω` 0 G+(+f?)ŖR+}0q@{1'7#:w4VO0 $βէFS4LBer JeN*/ =A1=$l\Ӯ@j.檄kz%eqe^PU콹4x=3` X?Rʺn.Z׍x)y"ř?21l6oW5O䐘eނ͠@{B2y^%kZ*ogxBVW`h9mh]zXX,нP,ۍ44&}=fJ4E6~JC 06}+n'Ui1᠗$ClLE՝)[T@Ub̶&R3[gXPB =J(B41|xs}Px蒲@[5"J۲syo#$;X#L z\,;tEfwҸ,=ěeӽ'O (7=u~*"x(Q$I0Nm5ͬz hEb0?%0+l2ͻXl RH#rA/TmXb̪?>޻|P:}f}Sb*QnW4{5\@9I{;MWjMxs1;1dY~>r[WRlW2 UսKzrIv6G'1gglOrm"(zLfo`Tx0fbhmNW= [c3 $'4jy32`$^vԩWW|[|{TFg4CPaڝ {X6]0[Ö4W`'LqϊJ.,3U[1[v Q!!FuZe$ eQw?ieg]TL-N @X-nqBٸGV'd H- 47O3y=Q ,swwF%"wXMhO{5! p:;K(o;1O6`.9I~hŶͱ]Yqưpmaƾk^'y; S!",`8t侑5qGZw)Ayw/<^?Oz1tӪ($S]n91#T2yJφ |R|3sJ(]U+G{a&Pd>i6ClR|2Ռ7Cgخurڛgs.3uo=p,!5bh-?KM)UzUk81ְ` ZIy6 qJN-ե5ymχ7cl,iX .CR oz⯫y/R褻kPp20%˄c`6HapC[q7C(Dz0DG ϴu{m[˯Ac" i;?vGms$יg,h?(sc}^?Z׼s8&IGhDm?Kosy[r)| Cq{د}4} /{ePE4]s։* -?ۉ붻]:+m'Wum}njj(l*J }mKVT~ 5o|YYLPK,"jC*6i<}}mg*ޏ<2cs|㐓bI/v}Hvw gM$YQm<}}}}ڡ, 5`na%mv}]}UhQۼu,0<2}5uSU[-3lMUQ }d[a-9qLԻƥŻIu<<}g}mD#͓[}3qԗq[\^|+ (ŵP }qqe5=߷ (Bt597=#a*8^ȁ 2y`Åqi}<887w]Գ=xQD\}Dr)XI 1ϻ( cK<u6YqJ|4tu<o0[$-| 4&'=M}R&ʨLs.Uij M\_҂B!wh,o3g]o4Q7u ?o.\o(iˣ&CpLTz7ʙqoyv1 ԄajAĕ]ȘYB1/&aжiv1$J]I1ڂ#y ہ+-AiVmmRYY̺}S*_އ #׭qrBv YR̿XB=kqYH8Dvq%=j1 PUjץ0,#>!Tף,8Ns|i,:$BWpہ0NNʼTrJkY?4@K_oYa @1沩(SgaA4Q6HwF!J`7pVhָe"֬jO>$,JdLTf9BV;(L\ h7 6: /[)+R1.?`2UM|r*Mѫ/-?H@l!M*"% d͖<HbģHo몯H^nTG[-9#%9I"9MԾtd%yhGN Z`˿LJܘ1 3Zޥ0ږ[z hfɎ$X览N7a./m ՖG]8_:)]`9xw(F.&n$6NJ?[^F GYdn΄΋9>z nf`~@lֽL&".qj'1q8hWݎ's@;B ;fdBy|q=S$`RD>]F ig ^%"MHt4SIK+fe Tnf/޳tdy%[1!Jdx'@^PMoxMS{TPfB|^*}'sUC-JA!AFx(i؟.C` \nx<졆|nxYPd(n`/fL#2t>#DũE^?кq OmNkoȚ6Y?7*&-BA0QRj`鋘LϡL61O{˗&T܊TΛ7 q5tfԯ09mKxt\6j0"4x/\ҙ UL}%jXƄ QYgк87d]G#aPJHrCN\xKg 3]Jy1`\` Ә![MͅX\ΖABn %̃rd@fL*tf]>]x*G~|ˀ`1|>;;_`GEqIؔ嚜 o3TrUsqǣĭ`FC1No}~`?52%f o5P  B楩I<$̙G.4v|ͬa,U+)7v1yP&_6WcBa1g$љWx5G!TxHRbL>}UȈ26KNV_OAk-eT~0 ""3›O1Pg(>L<^F,hF㘘ټËlauVGW5$Švչ6b.3N?/4Ow!~& <~0"1rvb Qf0U5Ax=Fo3S1z9f|1/&E(q3dq1&F #(Ua<DM@Af.gI똏fnD$;2?05(B A) >0Kp|~ED6EFZFv癩>Pڥ0 ӛf. Tk3mGVc0Qf;,ƥT`B"ߖ7*s iN*3("U FbqBbZ - ma_\X5 3.(drb;R`@58q@T&bfj\|{Tӹ$4AهB9itbĪہ̮D{ud\%jc5Ɍw,Le"m̠\xꙮkC ʔLD鷕x*D1(~?P& f&3Y1[c`J LzARspa\|t(TWJlKT"z 3 +ɨ80&]>Mg;0Ll&Ll61Z { MRSn(-=:fP& 8]!ryI'U,ynX\ n?sB9$̈́ u6`6/Y3Sd%1)w< 54QO8nYSMd&@k&<[DkcQ>&\ [%N:VA g08AyG淟R4qډswD:AOyDm<*\Mg3zA0c"țDɄf. ,jH#U6;U}1M 83y|kcBk889?Lچpd4L\W 2g&SLue~2-2!¦"Ӗ]:)&m;4bA?0k8d?)}AkV,&E(hڅ۸2S5Aq0F"UbYө䉗JF:6ry.6CMw> b&'5M{P|]T}8_3P,X Bc{A(7g9кӅr(55HDRiS~I:M_Ǐ  ZÕZ-k54ZV3Mg2/̹ڢiYԊD͐c]#6чu>Lv"pAFaJh]Q,MFfܢ&)>@CG(X%ߒhF(m.U?i.q.مw2c('kXIcbZkXD&#lGu g\)KlP#B`P[y}sR(UpAn%MW>fqԳM/|5FOm?ٷbx*ٚ-X9BP16ճ0ԲPP^cuyLF*e Ù­ -Ar8ybf&l$>!WQHQ.TIL ,?y"zmճ65Vq|L_"Sgϸ 72Ϸ#5Rܾb{["R>#9&e0Nf|6s5 ]?3:`"Γm@'gfLyybXq #=7VsE2%ĠG,y,n~"T~ _eXt^Q70ٝA7%ÑAf%3Vr(ij7@{_a_}od橶bdgGGɈlw˜' u.fYRh96i[9!>2p@*1&ELf`B~TM3F᱂\N`J AAH |e0sPJ|1>'L1L t nLQ cqsLUIQu4^a )u; о`vNbJP!1&Œ,.Rݳe(Gb}ޠ06mŒ`Q5ܙ#[`hP 6 cu 36`־& :3>SmxuxNMFdԐ9$G 4;++!>L(bdGRG pgʟ)3cܻ ~e3Hdiѷ)"T??ŋU"aENbw .SP@@_&l*1<8arcj/POܰ!48v ˊ|w3PrՄmCju594"~Q:LA[&-ϊc:f#YQ)="T{ajW><>D8[ǃz ExF6IOb"/"e֜965 &x&c̍h ~X9|K`01gm; 0md<Ӏ*3 >%]%Ӷ>4\AC85LY|@(vu{]:d5c::VzjzF]g.1dM3]bQn* "'6Vӯ28 e? Lu3IcaZ|T؉fm5:gyd9Rq:nǗ0:6ZvsB`G˘Lh8Oèf^Jk깠kMԸ~"nCop&. mS̹p{3b\ML;1|op܍l~_~&7<+\4g a+2 k_ߙ[ S7"ĚU@ѬGP&=7]=(!>cdːa,:~\r?i>ij28̵=<vm<)pc `1CهX õ{0t91 _`'cQf(\ehP" #YA>Q>yzS7Jy 1X Bp8s,v|G";_1+ks7#LZpڐ ?81|+fB\OB8<aMf*ȃϻo~5yֻ$ïb91LA1{xU5") E`+byg<\_lZ3aE c`;O0f,KNDd0XHN3tW3 OlHCaoUܒ&"Pه[2 FZW0'KWOt/ A δ 㹇E5L,JJ6DmZcdtRf w'1h_bc|ith\h/{XWfg`r#v=s<\ĻlkRkaٚţO:[鿿ogʡ<|h1TqNau3.,Y`c! Di3qۙaVh~ˆE<4бWf A{g2rL>8ljocMCAMO ';Q|Tv8&h5nqW IUdO\9P6y<fG&OT|8А&-22fp\tl~4zllCq] L9wB tiX\Fܠo~h?y/~AP*~ OãR(q` SfKN gfE]4hɈ6c Bkܮ3p=; DBAg0? =ˁS|Kt2ci4F3gJpee˪*~qwՐۏa4b1}S55 Cs EbE˸Q#4yCv{L^%XЈN6 ǭª&H*qsWv+gFuAƢy)MfhB2@PC '%}k"Lϑ9"z]BUΝ5@9&5';\>%H;u tۄ8V`zo3{@>'_l6dG+f:;A]BM;@D87"u@r2}t[ ¥4ll&>r!\O6&}n0!=`8'pftؾ=k1Yf(|uOrZn4(cuQɞ /L - T؊ۅ<ZEb*~&vߐ%+Fa*YSFM7/n&d5&i\>0@c"h#h n` `8 >=Ȍ(u`RǑ46`4{&R(H``7 abo"`ݻ5,('7j =5f\ '``g1|L˜"Vi[3HbT1g>`|č米?5_r~IjY 2p=kX0L4jLP-!Pr/gQm37}魻N}Ri`C4ŋ6,]R=ӷL#)(eF'%i&0L| 2UOULjliIP|b:}Bd_]vjvSCWe5$Q0>6!1A "0Q2@a#PqBR$%3?l/;?(g=T3iMm#D =>J~¿h,%_\rB>Q_qSMi3*:t(h{TR|aYR[oϧESFZ5`ܿ07a_8")&])5cbzԯF7KGz(JHP(F3X>?T6ʄJJޞJ dͰp&a)x]R~7NɘY18hHRĝL|2~#갢Sn<ً1ѓr]ٴq'>[\LoQ`צeBTf[ٌxmcgr`_ؾ!ݐ660-EQ  Ɂg@SC^&\z'Q8B= a?)?P:U?N@*>4}BrgX:;\N7jМ QK&ZNܯT6a6oa㸍練0d8E+`rVuhhhD3q=x멯oݙQfg<x?ӦÉV2?=`͟H$DXt`?TEeS'5g !{Aw~O2k'%8?6 1bmxls48>Hx55T[|G"0~{L`KPT4oU1c6|OF. >"De? FasQ^ʬrMne@3`d4tDDn8?2VC+VxHLeV748M* qU?M7& r g.Ѵ'o&\̀]` MqF*D,hA14l"\"@&T.f<r.2)&}0i#Qdƃ&nD3L@|@r"&#ɍs Eړ:cHfvd"G*fA\YB@S\X Fngt&,Yr*E!CDf mbiwd49Аc2uPr%&PCLlw\EP?1BSO(7#(☎B V0h@0SQrfn!kv?uw5LT!E "+2%}eAv`@Wc͒30+26Tc>fn<RT(9ֱO+n&W˦?UDZJAdQ`ZBAq0e*`"㈊Ld0X36fR@, #aJ?a 00GQ B% jfe WώE7iF ӏ(2}1:&e3A:%E]DPT A(DF=YTUm%d EPy<@ k mF^ft *Gs\DmšmAK列N2?gI8.0#%0 Fb. .f(FqV&P:vhtCwb-& ˇ#Lώ&#E!Rp'Og( xXX%,[V`Y LX!65mG],|Y*> i )4wdDs\f44M5Al|J8 f 3ןfyeSl"3]1X̸O+s"saWqR)yTccLCP, ;qh 4}y!IɴEv{9T2EXٚlG&@&W,j 3ǽ@j&zG&bLTno'cƘ<OLx=?(ؙȵʴRZ/R<֝Mq, nTյ$Yk] e`.u'V-w!h cOc4Y61&Rǁ1)4bG ,Cs 3'B@+1bg[Q4‰'˽&, o'"T5=`UvOj?BrC 8C XP ɤ:kȌMԻ1&ogG[@@aQp34_B QP_ hVbb C:c-h.!A ω 81J[ل'&)(ۣ,'X)\A 8D=Bo]7[{1QCP3&#Ez/gܻc~]q`QRf,eT ͤL=5#MC:.1\PT-8w Gan|c"%Y0LMٛ&L rsSd8u+W/Rè@E\\٦զ}1zQ,b~;"k6)F:YWc2TLnjb6ۓ9.><~1,NEn '74o_(*lD+u wӜٕDƼ@G3(e&lQзd@l. ȪjÓlUٔHn!:l"fL9v5hIu ǍL-o7:7EK.crmը௙ u3c]XGȘS2}#XZ?dO)f!ɐAbfG8T3OfpiNrfX)4cN2"F4!ʕL֠]ZŇLɷ%~e3܄E؈a/D>zzc=5V>L[i'b 'C>R#I( eR@9修euaFt`ŊSN]#bqk 3 j75lU*n}jOMWfUT0nf"!%zːc&gڥJ*i؜BV n|@7:0}bf\7M@9@gPМ`i 4j&p1m5?Qfk!ְcd luR>L'м̄&*}?툊怇J 0MfM) NՎs;)rlT=" (?9ɅZul@;%R&}: ^yVԛ# g-@@PRLg94^C>`&\.G7gйUL1 *: QDsAjqDl2-4u7Eox`܌c˼/*'fd*9㱆 D ;da:K2gbeh4{FҡgL |v3 }P ?tz`Ț&&o"{preLB$5fl L6وAn &3za)ϑŴ(ϩɄMQ\ޣ 3>=v~n ԐVPTWLĹnt̛3W=ØuqyYؿp}:O?g'ŷY(vf ,gQ9AL^?1!+n$֢&*`հ3$Ծ &5)\\@:&0E!iS3y7/URA<~"1e0YFNioIB;?Lpl=1V1w0`Nd`$C#O-ϙp!vkZ˹.fn|NɁOBdԽ(݇_-l3i0FԢ7+fLe9*D h;Ob`F1<"ڑح(E`Owֻc(VUlY{slc5UGESowѹ oy0Q{v剷 lsI6 FƖ8cimJIΓ 7TQsQ9F $h1"U/]Ps2+7s73YO|U|ΠS vcB=Tǘ>aۧjN3(côRƣ){Z;_@\P#؜gK=2͸1-qs:"1+*~`Ρ r"6b&mη$j 6>aTPŚ#1`R FD֔My$fI`ʻC3. >aӱ*1%g'i1lJfxjPðvېnk_%8 Q)RY4SLoɧ>lhđ&*&"ϑWj 8f_Xv3#B:;116&@caS0?`1sR37-b!q7Fa+4cE;S&Z3t;rq34)jQAJ!}c]@>`E3w]ҳ/8pÌe\I›, "1!cjTfb[if_i~ߨ4=ndY(L5cGQ ̣!5l=S2w,π-Fk6&wľ͆,UCJgP9cswbPݍ&ǰq0o*`C ȕ cF+/@B˩5fU #62‰(h۩2b`m4xm1V/lZo}VjiT/n 05w2Ʃ7b`<> 5٠%z"wv.n,LYWjfJ3wBm(w@\D|)d=Yqs?=f} s}&W4&Ĵ Cj}34s\m 0 &, &Lm3 % LTʶ.<x>FTLP"u-8 x (@QJy̨ʂT7?1}?n (6u^#>eh@Nnf*I<)aaԛ{}֊c]f{ UGT;,ͦʠyӌr1#nZE标80a+`DT\˘O(ٔa7`N[#6H#[YIU>'͙(M:.e/ONfRk޿> aٿ`G?~!^a£SP)cPD"l& 6ȇ)1t&zn|0dAc#'Calumb 4 ֩L=fݸ2oy.2I$\@=%?.$γc.U' &6yhc4~DžYz<fc@X~è81]K n DRk Ž &QbƣP pэZ:ljX3ʕ?ݎѲ(G` (z(PԅB~`kS6xM,bmwQ_ 6rMp{CdСG՝K cmؓG'ӲTnTM4k#Q{1O\GeN(QbsDO{h:Kihc'\?3(PBgmA)g 9aTq%#8N0m^T\cXQ%…^(/au9羳:o=3 =n-~Q~މs>0f' dΟ?n:cW=vKCiREd]|E9=(faݹ[9d* 8۳ݣeJ0}BKtLdIPGcGTıa+6M/" e'af[:^ 03~z?7*TqG Px{e`Q+yh'*94omJ(1W/Ɠ^+LJӓNJy-_$^kS h&fn!fusB'j9pV!8 0(蹦*9U4Xgc{ZUVӧw=Wf8TXңe\we7D ;>@SZ;VjgqHjP9 z#"bȯpcv)(et+w4gQ ,{ Ži$5EX:m/\ntXG\Frv~,Ԩp5! cL%O*Lx yŦqdGh 7cnSz L mgZl0G\dSݼ.UiVm FVcNH9dעlUe^,.I&Q&] mѝL/賀A5aS4s|CQqpoIH;|e_ -!Rb}/2tu #r@Uy6Tܻ0Phu]Ļٹ_@lrLдt#T1OS76~uwliΧ[L}LLiM9!7_O? IuZuNv,/!P25䷴CrwhcVI *U5ϢDiYLtaUvp}:'{?]fmst' LzMVrMMʮ0uFS naW7Ԃe Fk;B>Ƕ5D7w2&dK~ُƂڴ,;2Z{CX S5Mh?-$JfID£&x,-q#DQO2tj@GI @!Sv.2JvY*d &V`~#=27Z,TrdBCdFW K Tz;x5qTæʙ`;*kHQ&\JyݳKBt fL :|zt^c;`3 +ɀ-V;t)>au?7Xw`GRwAWHSnNeII*pU)ai0i8i9ͧPѥfcXUV6zy4Hg]NJ<-=֪xbS3fYOC. Py֩+kYZylg 9j:D3d\އoipwa6@nbq#\A}#R5Bl p2U]& %QΓ~3TLG]BbIU MӒcl*T&t9c=eC'~:;6x5@M4 [BnդM0I>򫅨VOt525j=wS1 0b L{mkEUm O *c T̸AiD? N;Ӛ`s^ǀ檓N|sPL:6HI?Eޙ'sf2M}'TjhhwL)? 煨 J Nu'䎉 ԅB4NU|)Tm<8B0k{2`i &KCe~Mޚx!%OA!Wk|!;T$S%L;kiTxG ײ4AL&ТJ.#4hmGhZ$4LtUeˡK*r{:s^D\TwPnQ[t\5ee=UG7˞je0GC,T8|P}(lsjhvwdOWgUtr;\DM)eEOHM >Rxs|BM:M}6aFbD eQ1[߽?P 9ke6N|hyD:9jpj6?ڎ-Kz*sDKay %^цAoyK)4nj]ˮ78uXjz5"汲!QQI=75iͳu}Ձ;V*}!TKC8nys]0etPmaS`CT*Tvb:ױ9*g<L0ʧU|!&eaL'϶DM$g53ZLk)#-S'wDcwE{0G;Ii#6?CrnC +Ϟp$puC=2oY4t 4FC9/Q:~'ET\t?T󯺰u8{C3B+'^XdamZZM<:}imް׎D+rKanUi*e"ςZs2潤CsS]sNRζqFHh4asۍaxJG9 ̕"ks7u)ѽLOE>>.suU?&hTUy}T!@& ?%̣| K]yoU#p#ZUDdNl9!R hNNg0[[o&O@p/ݯC3ȦRf+9إM)džA:J7@ˌqOK y]^3cNDѠqkXX~Z*AW,&קk*qglamFtqJ-'B|m>hwk!6!S)yBcW NmUZUIˈ.Ϫj܌~vFG$Ba;fVm:k x]^GN 1t /Tۓ@h+¼rW2gOݗ.Jxl"y%MzE1[uy,s-p,MTo8xʯ k $1Y:ȉ&̨;ÃVߧi dˠ "9BOȧ訸; i]c-{p ԉZ°1gif2nv2ZCW5[Pup|i ԜRʨX}I4w@Mny'K):\全9O%KxA Fn_٪*&i {%.*È*m &gR$Le" .$8OTT\*H:~F_ 5BF1ͱNrk Und*.|sޓ=Ld*6x2 v@)I/S5=%_ACFTثZ.xÒכxfd']%J܈#Ul4:({.|uꦵ'j{a~0r9#U4!apAd@ȣ190&hM_Ē)UUmsM%d 3o8ɵ=k:T*qLwu]h]chʓK9:_ө41{y.n|-@Wgl(:\D4cm:d Ow<_T8biS{d.ڃ(!UߴUMo7LԨ}mp:O: ^5*|MSK~`zSm\T ]& Mq+HTK YQR`s3΋ "'NߚܷunKv2/o㖍`vl&iA:VmE,9zg`̞>G^IwyTШzOM%4&@*O4; ˾*=}Bk?BaoA6 Csns M`Yy,"=S3{qn*'ڵrrՊ||LUHnJC) ve1/-ѣ@U[Uȵb*omx`n.Ӵ>*SLUZ *Ea6L\UR\w{g6Dhk[g)LdeW02BV5U6U,— cG `LOU8}~ K op|4q9(\~_=m7CdxH9eMl*ᕉ1Թa2D(܏TaBTUՄf EPC ?Obu'5Hk8G$y*cCrՅrM9Nyk>GTXdnoh4Xj;]9#YM­2ڲ3*joEARzeh@rküOWwZoU^69)'Xی;kߒcN ]< s'{lp Vi3R[ T`LE^ GS: P L}} (႘Ӫ}AlwEZzev#޳)Ȯq}C{bSֻ;IT}6s]:y)\ֻ{`L;%{zFKO}Uv-oHhQ2һK Y~LeڻPrDMwbby'rcTL$Z\2ǸT+*8Qt8]R)cAsdxDz6CDagXܩvk̦uQsdPqa|2ۏ`V w u6Fzho]m&wtX|>!<]Q1Z\ӡ݀ aȄ[qasUM>XrN ~LJ 0H}&ɨj=ʿı ^_> us{39+Z~%iv#03uO_תUeq| f}eR:,E ۗ.{`U\@ dgM04c6cj R3Xl@{n5X1-fࠉïNg~~wsDR:(rJ"\yŻ_o=ʼnZ*ѻ.4a*1uaouُ$I,)c[F@^ TGR&Mbnc\wMw*vVS6yL8džjvU ԅ@Mvy{D3=rYSkC@V@3QbP!q|Vڎ#Pf2{F2}!pT{M7T{F\L_0[5ZtTgi]97Y.ըfJf,s'D:6Yt}U4q-4cB>+ cmƲpvmh:ZGf32'k#o<'' _Bcd'CM#fp5k,;OV=G3Muj\eԕA-e1 ʬ4l9l)hÏUITk- y޻PŹ}ڸ50yQ'U2O@!T<SF6/H z7?XN3 BS)0-9s!SiTUw.HҚL~)\eT{Fly*.,Qp:gy. h-O`]}1ZPkP t:rXϧ^⏉8CM2c-&,y9MvXމCp8fmJuY,tHS]k:&x`!ۊ`r)lKS».R~~J9iuuވK{лO]W ?"Nh(uBJm췍܀sToKM.Cyho6{`CCvYpv9be0U??D *X~ӡ_4T'"ԤX㪥JjtđIu)w犏aT @{6X|Ml.rGf!V3+UȔ %Z|Ujze0ֹ^1OT>0 %2HX'vN+Ҝ!7'hp8+v.,R=Qѻjxѧ'dMH?HnX\=3AuĻ>^G_m 8\K@XAAk" +YsUt5vi?cLhI_5 <)JC /Ak\6,0;&)s h&q9SKU@^5_,ͱ$&nȷOݷڧ 7u^G89y m _,Rg/pjǘҫu0\?J]#\w"b=G%γX7l8a-[QU}r=B$h MUJLƦGS3@*\"B4E /VnͩNw,**Z[V.p9gpXgյ̧kc)ou7Bh 5U@FJ&,~t7)%37 S@sns)YMtn w4'xisWXa>o%kV}G'TU0vg J~lp*D$&vgkTwU%Yi^!b\xF+' ! 3\:G5iDWhGO*iЫwMwD|qvc*5C-aDjp^k V5 (7kKi7ywYoTʨ,B9rriL]60QR *`mVaY.e1R뾩Yn5c/k{xuﵤhCFjپ冤Vh zPab4^eRbgB 4]+_񓪜SC9[QG:Q`Y+Qw̬ v@ R,^,. 짖W _yDu๲-b>.$ )'}^'. {jWR9hb(6IVh ӬxI6Z1U Nm.ޣ4E@f>Ues-)*3 u=UJna 8:~K M-ܰm̧wd+I*imw Ri&Uh>VysnԳ>"2yNv%Pb:T.a5T=\S({*G^EhZ>G5هع өrU 4XC_Ul8 o5 Vو|uDasuDO%-0n5XgUpK+#2UiR6N 3G uBײa5u9gIN+*7O$H0Tn{qla_4O@RÅkXItEGqpyjihOͭ'>IUQu6,vg)Qc~ùh=Ή*}Wq ~ձ|UEYTqW.-y&Sxl_%in&v_y{oTG#%xy,E==ځ}C -oRHxuk L%昦A(qf|N7%Li-+ j#柕O4a_n!'(o9wK{ UOV絽:wbsAbUq5˟$r uL '*75n MB- /u:܈Xj&QNMyNH0TOUJ U0u[$o__ 9hqftY'J8; ?A2F2\dYtWh^ke*ƅp^j +|+Z>DmH B]Ty,#<=s ֛MٔZHsPF\8TiK˹NӅ:h(H\Jc&tJ}jnlo}CI!::<5\wBQK"3Qp51:eZ=Ät*# [)ܺ֙Jߴ?6hӒTkZ*zu|8GSktN|62f'3FOڵҝSH0ZKnj=ڰr֕!U|`1è?-ph >Jr: ~M`ۼwC%(Nn{ h ht]\0tL|~Knhø.q7D G  jANt#EO.`89跜1isCUkATiSkq#O^L@y2E x',(3 JpM9,Q蟲aC1L dWg{x|;vbkd杈uVaT׫knK ˭h,q&VEPf3RygbP2rjS]G>I"AG&$.ϧ$Yk/r{s4ւO c&5 h[(u!7x*5&89?>wu$2CR5F_[>u*=QΥiH=f.4l@-}#54@ ΉO0 l2?ҝ#0iO̩>. L ~X[`L_iAAx[XbuY8GЪT&gp9X٦:&yh 5%pRG%8&jS&LTXvjyn`{<5x4Â\9kچ4Laa^֜X.M4íf%;`G2~s ƛ;aRWm7 y"b0,hSmggԬ\>X:~kVGftRGyoᕼc|װezER4GUFPě.WX he< Z>NO85L5Uꖵx9amٻE$9 PH7~JmLtJӣZUS*FEauV.9+hȧze&73#^yg 4ˮh"7k {af ?ȉ[tZ<-XTʹ q,YXurr=L#iӺj~fL--jkRDwGkRѧUFe+w/E+ O_ XXQ|韆Dh Uw S=2 X[IRL1a.V"lxXZFWQZ] Ht9:uGV5U1oJ0Ktb:2~v:ջ6;Իv4O5 pfts\78S)fD+y놚A' (դ$2YC*9Tu*bXG \/<P hWejgiemmf<5@u 2Uud4i# dm;03 y/SA@L1-Uh0sGDse:CYV͵̭p;Rqꁹ->#Ś)wn(X!^UfL.ȧxx*7Cx,%?*|T Ҥ zͻFJM5?򫦣ˀp>{aЅyzMZbmn4 QʂZV; pKNTr@*bJgFDd7Soxuf>eb 9`\ZM;; 1.NCCs>KXiLW=G1g-q Ku[<|a'U9fF){DU*ݝg+HeJdazDiZu1P /GCD=ե{G%"oCJm*!B."J D>J"ᜅO8ےy͟{@~C R\rS=!0ѧK#yT!(tE"hF$y J.*X\1{ySjATZAy*7,I-p/ ȱkH^U 0<5?6g.7,'{]?!°檃!c/uLko#]"3թSi{8F)u6{QRrjh^e`mQ.k nBZ.Сu wcoVm4+ԫj}793 wz|,;3!'a\4+}X OC:V: v&ʐܚj2$ƫ 75Nmi"K}eLC |fD?LZ DHQ-YLA t2tj81br`yp٢d*9pNLɎmAeUUmFSk9D,Vm+vMN +߽hsC~jMu f^7%xSkO|K`&[%G 25 pH*:xOUP 2ElЎרּ`AzAne%By&O2N~JfsЧ*urJk*}"O>ES9,8;zhUs-$Z*\ ᒢw"3Nmi.D[*jUBրT8wM#Q‹]}HzK]rWf^v4:j[}\5ge7M; ʁN\f2|YaZLĪJ5y$E9 a);҅[kKwFs|tXO>"%od.wXi TV+Y= YI瓳w*.اR=5V u DHIMl&G޽s3~S XpZ,N3.+NCϢ6)TkUCK/y[~ɞhb*Y!֓:}\;OTЈUܟe?Hȷ3AS0 OHd8c^e/+zK]N*0}BDDfh ҷ7u0Ч!5i9gT}hUˋTeg)T?#x,h$xD4MHj]hBT M~ z7Jm':[96K ;o-Ӝa-)BfM'5W(;iWR4*Zs\YեUeGR9*g!y5DqNx}{MZS{IM7G$Ԟ8Vlߛ5V#JSPĿLp22X?mLx7fsM&%vJMߝVq3&"u+i<\$'^cg-@緸<x[:5 i>iЧa̔Ǵ) ei.:+U:cȦ֋DѪh>iL}8N}\ BVΑ$!s44^hwW>:xpY[gPp:*ױ||y[we4d4ٜ±sM9@Jz$cPee>nMSԂn\Lڊ/S57ky-s ADn 57}`.7-J^x)eArW,۫mVHUi,EKDTWf;ܘ@b< @ve`m';Xu:02޷{[PTe@$$4[jqGhy'g@QRx,> O&BgdӦ$U,)؆9 f(*f?־\ڔTMʡZ|0V0]:A5 ǻNjnaSΙi'h26Vo5I3dpH' 쎬*K^ZǫM!6e 2FɏU(Xch'-e`0SшżDesFt \52䫋*} >)o\Wm)0ŷyD41akUK'MVّs'!S\tE[F\&b2Lg iJ{p橐Y颖FSFDt 0N!>yPi) j0P (TELW%  OB{u<0oJ״Nf+#%a⒨Pm4¹M0aC۟А_wUUgaw, 4wE q g$BV;eӪҗi晢J*)t'876x`8uL1˄B$nOC^ Ѱ` ^5.)o/erlRۏtn3oBegTc̷{1ZdWg*WԈ FTo?!Ҭ]0px*,j2SCT+E0%9‡bwaSӼCrsm0sG<UҘB9ܴ6ho!&!UC/e[=*UamVEa`p۷zEIM֞+TtpX8yLaJQr]\OT0 peCDw-k{߂Ԇ* fn0حŶePi{h9L&؀ByVSlpJq98[19*!Uwӵf,R mv3 AY -%i)UTCa4dcc{ydZ8\UnU>O9?v7k!kw]!aiT{ƖP[K|]TYXxzsLT2=.e=Z\;v9 Oc]rW^ ΩR;PLIsX^*-k2Ui1%R@˪uWI]PڦRH'1>OӚCȏ /rQq,cSjwIғbpQ$[O9se; ӣ[ao^hP%Fj'?%/|*FnwWt=>LZ(妨TyVLNl>.ᕏ;CMZTAkcf]QQn傘3 ohdKO|rXJ]Iܴ 8Z+\Vl/a, ³bIWɐ\3GhEmrU|DuԢiRBmUtmrjԫW1H](˻q.s_{ɓ0u4Tj?OfA:a&_&\<=Pi0{5*Lx^4oq: xUuwM@I\Fқ1iz%Ps olUsbӏSM9AT/ʂq%RDStt鳆 ]} }ZdIaM"JaW`gtF3$Ð9x+̆Ɠ@ˉJp,6Qu>]Sh#Lp  UTS#7yP&zǗX9.VsG4LTmHnAE0HP̭knp6p*w901qE5Bvlvj3Q2\z8L!'9+F߳ly'CQwݪ'޺X;iOS\l v-Yn!uXhiϚb|d%bu'2x&ccyJ-aOjwU ;)9BJ*fLseP4fn/U-dG ƈlysR_=*¹5o$8jT#^J{x5CڲJ *MDD:SD+Sp W{U3s'䯜<x!59[7ИCEVRᓪ[ӢX3> 9o)/]/" M`qچqopS\hQwo,Ȫnm~JSdnNtHY57_|Sʬ{rΪH1$+ʛYCFaSi/rMw77NeuFF\uR 1QO-9!9ʧLn'5S: uTE&G&T%ZR9yu'HPiקCZC ȂE;6-_s-ks `Q)6!KZ\yM8-b 4fߺw,Zv؍&Gka WFBteb[Ҍ@sXb7`~(" qqOڌ{@sWw=[k XSsia^O-7:}REĽ%֋bMXGT N\wqf_lh ~zdW 35cNH^- m~^J%Li U*=h*99 dNי m'4 =^ . gU,})H-2=\qvQBiܲ-TNSk槌5jA`v] ƔSl\nƗD Dp˞cU:&Gsfc0ċ[2+[ leUstO0+Mw,M9җOY~Ik,|k UAg*ė.W,M  ]jaҜrȭNU= W$2FP8H,8L;J" M~92~RhoO"2P a0Y!b ơӉt*FNe[48Sʁ#00!„uN!B%JjNS` fm<[L X]u6=murq ) HCŒuV?wݴz*2tM*Tis=ѥ1HSܖDo(yOsH*o1O(CLӜ-v{ˍ̢skU:z+`G$O g/?Z5Maf^0o v̑™d1Ls I|,[%ZrWDJ*خ.>ISME6sT%V͍ UIU5c龠]nssa eisne9w'D 璥y!N;NkF\]uE"lY%1!;@Fn赘9jNV$"5:c)d+FY&gT~#%Vg+[ {<ˉ4JB6ۡe0 #67'i"sa$oz-vy8ndYRyT4 ?ֹ/oSV>U8e &2s޷-p,NAnV ՍCy2skþisx ϼSZ2o S\`<t9"I俁Nvd֟MJFن>*R Sj Zŧz@]Y^wPYZO9ALr~MYsT(Ѷe5. X~*8+-CSih]#H,*=֪n-R4PsGBTE=i'(Md2Sv:mҟ{@DCT`ys*d3*38JwB9'l/v* WpƥS~jwCvVzQEPlrofyFI5<&Qu٫S\Ot#j"'gJZfUzuTkK}+_^ jnq7wS:bGIW&O%yuBC( ؎!U(L vRӞQSgRK%We4ܮlQ'V3r` M,SN5`^ MpR'`*TJ\RXzTMVΪ-<{ȈJim뾉棄{f曊#\kǂJ%BoEwR匩;Uk|r@iV ''Z-'#0eIg;U'dNÇ!hPUFj])Nb&TOlL> V]᧽,8mVĹT8+XִHh:'2m uAµRT*WRz&419tKP8dUVXF7yhGvy/]R(aB eU"ntLl:k}a4Ʃ97~ =IWtF\NM+To$é7@T$ocZ eW]Nn5淘iXBo&HE{JJEك\i,>6uH) ND&} r^S_&*Gk訷wE,gz 7xEu "M*Y OIr*`yU5r^TAQP*+Xp 몴oBuѣOMU570莫z,嘒\5 _CA8g$3PcJ!90V"s`}|2M[f p9YUpYF\Z/ԫ{9J;UYCZ9v7gOk9ͤ*oI̪o:j}1u^L]MډT=}ٜh~r͒ʬUJu@2{MIw sn*,IW0vxO+HtUM:cD2s䍆hr4ө5\"~ix鰦SW!9R)ٷg&)T{LL*$#;lz|Ka{KQ{Y=Rxie}Bc n2X иsrVku1~j)\w! 8rꯦ3sHTa>IvF״r5wODi <&4 S[̑MBtԩb+:QȕBduqU ^o//lt+zL;,ް=NY"%E)FHx&rN 2U M-m2fJLBf LtswŎ ijUo;MÒ0zQĨ;;D^rr_(duty-}Ӓjb~ٽZfa uE2=9F>J/en|?p$Z69;ܿ5%O}Jgx9qpRz]a>jCi>ͪKG;+°&ꆫZQ@dUVdi)Akp{ ENо.l`)W) %hMb~ xvU٧~k E$^NlIth1NkɸTs6RՍD4*nžpN&DHPEc*7xE1D(vFS*Ԩ[żAAJO)>2d?OOvz6~jj2Y rw\RG{!ba-=OagQy"XSs#+v?6rN ouT5c4V7*'s)ﵤԞM9s0uIR$~0LFY}U;iGXU:#2:"DTd*dR{8\Ce \Z b~yަ~mGxћ] ufb}nG%@ BNNl#]5S+D禊Ri><>f\^zrMhh:(VR$ǂ'6jaVUDGUwltU;E7x-6E4Vtk#6SHjR.DgR}:&Ϣ;Ng%15n7\(maiញԞM@t7{ \;C^HB2kS97 fBe+5G2WխRi.{Qh=G0 ,‘vAN$:W35W7U#z쪔Lw__:d*] O%~Sp5^[bhlQs2/)z;5iBq.Ъ}Xݐ7؆7VT, Io8}GnTZ> hXpXv统2TF4hiS{-tX|S4ƆC%jЂ湠>J@@X;6 _4>US6YFY咎0Sja$O##$ /4W4\Nj)r@N e8l+Mkipo~d'5{ajw^9m&M♘3bknYN/x,FWv޺WeaSRc0%7uXLs[L%f|4!n)wFeTQֶe'G u(ӧ<ƪu3LjKIbZp 0oi>ebnT(!4 U*m8 K2Dv%:Xz6.rprn'z.h-JɍXی9> LZ%awj# m26l'DFK٢Uz4m> Ana ھ|+轌&g.i'[ ^Is# LjeW3B&Ys'ՍO N i-2GEA tR⃏vSpd+<G=CSi6*r]6V|%Xs)-M7`l] a&e;CkOC#AYiXk 89љ j NB;=.yTR,7{\rv(U#019?-$NS0=ػ{f::AkI 0|D61.:}3rM|D˺ǒrBU|"}DڜUZ+u3+w=C -Dvnlޙ*e$4ݪ4(lg,NY >8y.-mrjpvbvJC$*gCaC(Or2`Ach&.U{xf6X;a,Bu?X7$O8Uq>ʙ u`Ԯs .7. $stnvpgM<􅀾7{+}]֗d2uh_Vv%E&Lu32lXaMsT7o{IwlߖʂXSgx0Ϻ83PܜUkR~G J\iʣ"|s>AT3=>JȊtۉ [FϚ̔GM}(rA \sC]-eaj8Sk^-QB%5 fsu%]"NjRITxl9,}FcWg׃roŰ_.iuNU{&-N?못OF3F&i4UټMdHtU sȬ%}rjmQ%B*(PBjjZVHżOD:W8s{Wd2EUnL)%6Ub,Y==9'ɐtld\!P֖˓qL!hAdN nmV2= ִ 3 Vnג Dq#T{aꂵ BÚ3eSWxM祖תv 3d-B. Xa;CmXSaUa*yM I=Zݮns|FjDfs@?UfYP^mSLU;mpӲ %5?Ui!a9Z׺ R֑~G)X"ThwsR=Ld7K@]a:g%b(U^915 A'xm$|kZ^i } sN{r懚U reZd4 tZl(hOU0O"4Ҫwµ̔@s|Tҏ{: koV!3r&ZnU}Wd}u.qz#Q !4!s@~~UЧv}Ou#;_"֛ޑ桽Uܙ|kN¯sT #Ԉ]J%=vTe S&9f,Pky1Z r6$?6)%Fi=Rw& L1؆C'4.h0>h_dSB&sxjNܼhahC[5T\Cs|M'-|wfL/7UPjI?W4*HwQĵǝ:UZoHt41 } wB%ak4`v T4f9ʕa9}wBZ2V:$5EbL>tO>˧jkʕAQw?%<_3"LB=#Ȧ,{f]$;kxО\:Cs#=`?]NkHeyhi҃2VB r\i<7FIĺteZhRXU.ͱ)aMa=&ӱ^շ=ӏZJqZWx&TU4DTЪjMKWiWsD(ŚIUAȝ=L..cs/Ɨ݀g'e&:>9oX4.n;pcSt-DHF[7 VW\Z4^X& m h@JWS(_4{6%bnCÚzrXVRa{X*"X=1 d^2(ves̯֗&}j=א⪁.>U80IgHT]}6A٠Z e;ӕZE:O8TϮ;FliCϼZgW mrźOS-ہn碝]TyܧLWqƆ3h>j/ 'bg,=[LǠޛBz)D<5G0}j̀B{H>\JjQUhTTi~ .mQ]u<7;Oo3)w\-FJ g`ZoD)x)9#s.i$w^a9X߆oHj6v\:Ueg7LFp/n\6g}jRCi#ԕ x,3s(ViNf*Ҡ*E혒R%FkLֹn nR):]ҪDCQM-yJ!qCp#CITm[!ScZchvjv75O;WeN|әMIn'{jpdHdQ.YAavlz Wn:'8#>ΛUJ֍tySCfnW6T#)we@(-i@V_ ?6P7 | ķyftx {FJv,sbp? #.5 R/5%4Gf8wBKxMzmNצT.oߪkq M8OZ_)~OK?/~[R⭯_U?]OS<U[E7˹t*r }\= xk+Ѷd VNhZNIxfۂk- sc< jՇKG4h:Tzʺ]tD>aO5lMk/!T3ƎMD0uw5ID*uYXCr(3iSwy9pQ_JNFEnb&2u]>jKFlgihEcYלx.h=Pc]*uNWJH+{L9/9*X~j`溵7̥N:vs)4O3_ُ5]ի'+O%rQLsWeL9'"B| 4 jA @6y*[ʏ n+qWe)c\k9gaSAѸj` <̗@3UO<; N qDCG4uz5/ShQPTSOQL`J>HY:\VEd tVΪ DL(CvMkG=Sapzc C mV6&zKsMt8N})i§!P:sDv^V'kwZgMT}'3HJ7QnP" zJTY2O%Ul @ ,c:mZT fB##`ZuPŴQt0BR:Xw @,fQć8>< f"Y49&fgeU~5^]:e`7':u,S\Y[1ⷕ(5j? |G#.|gzefx8ukB?i|X:#A([:FY;1 j-ϒ,{-O'-P3£e'Q06-ZG%IkTꝘ[[S[w3fٵ)B jhh@#cJM*T䝢MJi~i |3& תcZ4().ˬ*iS>P4ʡ2<¤׹+| .m@AM2ܑ;2Pa -Lw.+0!krK[iڴd}L vТgEKMU vasyƂ:ibiu\C1qUjƊW8{IO ݖQµ=:/@:Eu'f#U DʬᄤrzØ_uW[!zUPQ2m%vχ[cZτB2[ݟig4l[MZ [Fgލ ˸Jϓ^#{?U$1RŗU+L E%4{QŞuV=h{E5 EBSD>aǪ:.ĶTve4vB湁j4RUèR:UxWh>^4k @<9JFD#/0 rThv@)Z<iDɘPB2nav=3z_U|0U.l6Mit}5yoT;2?D;63{;ٍX:Peʥ0Z֋O*0>;]Й__CtE>s`\@N*q2F6hJK ^\ײ59(kNcz*"sr~ Ɩ "Tq[>}"|!n? ^Z]2|jW^+FJ(^ժߺj|@~[Y nqG^_ .+?g:Q؃zwRvD3qP~mLj+UDi4ieg:ފ"ȡ =QԔ@lm0S"v#Ѩ]Tc G*g'?S5s,4 C pOuMaΌ;\c]6MNwBJզXNuЅu,{90@>6㩕O Q[aT ->K?(#jM+z}nޡNm-<|dJ)^2\uL'*:Ҏ)ky(wTtxX_ItOJ8*cʂu+N-{rPA6i $CBn`p2U,F\\^2sAŎu>aqշ8wxTj ԘVsYEvM[t(EQ71Qy-E^S02Oe}ۺ5k=l%3ۢk5脑Fa= qtQ&;HC~%wGl>Hf5*3n~'r#[S#p02ۊl{@'(!f:#gʔwBO0SXV[fRZ%> L;NSfZW~ZJq4pqO5Zl+ Zk:m#qʻJ9BsI1FX snXogU!JvA(ӞIB+w6=T:XG=y\; R(s1f<=2@Ts](/P 6l;P5rR,LYV״ N4[4tN:X29UUN.}E,7>V1f%We,óy^:'CD a9>IτriK HFYl-uԴǩ 6J' Mͣc!=[̑Aˎy,vy#vR$Ss^L(ex{>6TSo)nYڭvu <[ .K~uG[y4%Ti܋2VG S堟 MG{sx;Y+kD OԢ2)ۄxڏQ.UZg,1('8eVX`KNzme[gS3MWyPUKtWgϥJ~ 17T^`d 'xRbI(=:nrpmo/Wdjk 7ef՝S]|DmV4R O4V)T/K9B5hѩikgO5%Nxy]Xcc*K4E܁i"-/$7lȢ@Q(#]|zq,daQ]Zg&ʻƏNXmPzT Q\HBm>(*7_, niGTiyx#De@MdO܁sO"@UO n#YAVoS^oǺy=dmi=q;SLWhewO|dn7mi*$ !5vJ^OiVh> lfuLIkD\xZ#6Hٸiin B^:L8OĆtFѭMq23T+==Jcԩ[ﻗU|p^J޿L/ɡ2{xA{gf@EG0\>9a˪K:Y#FӔs+-kE>\â8rz#D̦Sr\6́Xwq H-czϨTi>Jn\*j%sTܱ}E>J!Jķgc4Rö=IOe\JOY *?YXe?Sm ŸQ#O8o uHBwOG;=p SE6ZV'Z7ڿ̫r[*frD5+:ֹ*6l:x'9O;yu ?VUJUjS1nV5۷wg?lj0NDѬ#pU=Gj٨FBZrMAuC4Ji$D\Ni}z-T65ZV2*2+a麩68QiVgQ>"sRk"AL&*Ew֤s@:V xF$3 lA. hԮѪk /SMJG)O3(z)_$S7JJvO%g\@lڻLN}oq޳z}~~TfSNKS8"&ssP`cIB\G23Nhx!E[ta ,~6ia֕ڮ`jğl=iPlt~OɅ)J gfkS䫙x~}'dZ ݵnPbwڬKNGW0Sƒp,5Kvb^@BgogvӓSmw5jZUxty*bicXV_SCZ^~kxOwz%ժa&qmG谴i vchS mJv> -Leτlے_iĠe:簎FY N~W1q2VWa^#I޷?'o=m]vdB#ll$aooX'=<^ݟ߳ǧ/Oُ,jL=[ڟ)TK/\Q=/M]9 'UN&閫g JۿمʋUTuW0拭nj~A>V>ϟT)%4jUnr@,q2LftB 2d45'SkA5:ըiG)P>^)nwU  >4r $ ٔYk {Y8 sd7.J؟5E ]z#P8z!fT _4l)ԣj+jVJNBJrMRRj:tg/#%%wTP*V$P੘jWsF[v*3Uqg5T2ghpp*t5 WFU].m60]9O%ӻ8&sXvW8eĪ6ʎoC4V AD%T' ȢP7.kPw6Wl ܲV({a䩙` gَ~y>K5_F}@'ŧ}Kyo-?]OG'/Ysf>c|eLϑ> !7~  q脲\isSu> 0_B}E.[9gD.)Á̎$KLiR۞qJd^hV'*o$2%`"S Vj:ݍQCdeqdV;5jX[oSs΍`_Gn7hG캎s 7FĈapwL6AiT3!xI˒kČh OǾ|SqfPs DӁb!kD<q4؉k>W"F7_tQ0Q i꣢ᖪG"@o!o|ܚ#5],"Lpea>zɥj|ef4֎gԍ(F']MOoK]d inmȐ &юMyq@Ore_P, wzY&NtOUjS(CC"ouF\ Ag?(>4V z"SG4LS fs觢>/V%ًXnN+ RkT!W͖Q1]>O F(XoKmi~JFl:l"u m-k:h4Ph`6yf 2uD-LDSIaez.ਚtYhAKg%9ׯD5ѦFڬwB pGX_e//o;4^}܂VKEMRM,~v5=JƉ)}V^t2sӮjeTL.`,5Bg4doa*l}Gg2-:Ӓ&T8ŭ*t)Nt\ch戹nNJ '0z-lوL}.tG⩹|&dBu OA [q=%bg5 2!S{s69Ǫi%FQM}` *a c<pz S l:eRC$jUkEk8^܎[FpCVWihϚi FO{5s_ZZr7K\~m01K/`6EW@ EHwq;zpS2PKt(憋aX'~QQ%S9&Fg%sA4ӈUrnPV'V˧E!s]vL6BqP T6YQmkd=3i{X@Я{=eTeBr0+~Ti> d>m9I^!-?4j1úQ"mKX[mvk6 Dw3g }"~p&h6WqfU!i1: ǩnp[t`ٟrhsn-AZ*:´R.k]uIXnZUk3P{k6C:,V-ӺQH:tҦA7waaXvqqT60 Tk~4#":S,jù~F]7KXnqs`&"KXJ)ucg/P(Cdz`z`/͞MT1~s]/@r *-xcF'w% 5kC1;E@_Z@p!YW.yT:>a,U">%aulp8jqyu$Th~9*ȯLwEڎ=#[B~c jsV25C5$I/FK9xXa_ C搏Y2YHk k񚅏yL!QbX ;)TɹZNPĹjZr̦3ؓwąs:N9@Bn9Ŭs.M=o ]e\h4+=U3P(N e"MFl:,ڀO9 =76QPB!Fzt^=G jiw=!s|=NK IvxRcVKꏪ*Ylkn#6Pe&9uVs{܏Dw ̂{.XOrtw%OxSۙ'w^{NcT67˞9,!zӛKb|SiXB32Ng5V/cɸ]5iZvv/l'Nw M㫑mџuW4Ѹ'H@TWkv7R<KLKxHXw5ָ"i .]ƦsO5Ru_5GT0{1憐-vl(*Ttb)0xpiNPwPThiC|s)=f765x'8O-\ A9ce4xf)aȻX*)9SCEsRI$a'`^ݽj*1GcrUmqZrbiT47gjxl#UVT0C8Ӣ5 Ou{9rٮ4r*6I樄Ջz#IYQR ҡz=C9h门=q9 KaVeMt$ꛃ5#!apo5*ln5nchx˸}+K .szeU?86!cs^C K|psnnm*2eȾ26xFkgT hAiU1vIyק*x%# c(SvB1)j+EFWyW{g/dvBW*Y^iվ[)eT*;aoGs.qxqm-nz.4o:?xZsEUs5nOS^hQ tO*i0N(cIy'c@ آS<W#SPMny#5}ymw~Y*Ns.q19Q b|"dJZ=Hl5(}AN=HGc̹(u9M%*LiRDs cJ@{]@Wg*T)CM3G9&ϦMᎰ993:Pv"Bu0Ak9d`Y iR1ڂ}Ȋ̔D;1⭈s/oUw@ڔ\eBF8UB'ZySk|O Kď }IO]cdm:-T"9&^?Tlq0JnAv)g+m3ncU7|>ypmpSS+JohmkG Y5=Z14x("bUJUh+]T6x-dHkd4%Z.r)Xu2S h)7UzSsog7vzmHTm,> `4zXc5; lxNȠ!"%ǒrUS Xr%,%?[ 2~}-Cռ붅Vs`94N~g۱,6Pj4l.)9aRR(Z@Ca|@U<{"v 182*x8;w~yKOXSd\GQ)6VjԼc\s1RF}sAUw2+X37ѫU5hOd| uR`-3/F7 | Zb*ꅃ,iQBpU W yUl ;lzNUoe%¦q0n2h'\A iq^=BaG v=9k@U:' (b\cR'l+VKU!BQɡVw ~+TӤ7sUDt' Qq4.\1cZp194v"'N֟%OsV6:䱔ǫNg0<,}cy^is`8qb0Ŕ!#%P\ ṖRQf[7㘔ݐdl!T 8u`PB}z/7ʄju9l5ͧV p!qƣ" A&faL$&ytB'eM!"}HQjJ%\٪D"!{dj)5ֺ+vPg-[SuA n6g$pޅvk!ȃ$ϬK6{ksY(QȊm^s;ޘXjK\Rܲ@`1M4)]HAI$=*WYRC+c~}Uz>u_lмz4=c'g_~T._ @!ڷa@"U<^z_~~/@*0G~iF\n_?K_Ex$ ZUY>緈w~X\~z+^6#7~z5z]M0dܭhOAՍ_櫽K/1A .JT^z'Hѷ~VЊGp\Ki>?Uz\"EJIRz1as~'Mz>3юo ryBw6?1 >"[NE_/KYCRJ~?/1;r߸&ehk1;r)*ܗа oWԌ}.\r=JE~RT}ULFW[lsbp&low._EJr+֥~ z?c0{.RV&*[^(Korn>w kftbʑ??g2J5^*TQ%z+ֽ.z GzxQzo_ 4a*fg8éf2#wT|Ns>m!GЊٷϪ*GV R>Wj쎫Uo7/>WtQ]4??rEܹr2C>RpޜD:Bݦfrs2J5ϭJ^%7j.nja$8QF; +fh #>cLEޅ6鸍a ?螧JeIHM+ٔj`b*tB`gHpZ <²MvE87ԇii7gңY8cb2d+Xß iGv} 53m@Cu~/YhXq;???bjU?NvVyG3N& mVsTJ2u>gѝNNs؈<,U1(*yNDU8NI\.j3 OF&my+oqڑݛ@|5;F6mmχգf=JYcxiP.+,E+vlWE:qU3<>*e.Gac/߻ .^ڥC͌?'藔T"yϜN2m֝Oah6W^"9!Ҫ{2y%L"c/@ s JqnqҪ8.Pi)J.eB%qiҖ2=lXoy!Jvmy4\?O?Lyf_&+i9gn!T ,]$ZXtw 4vN_RU<`q63TT*@ͭ2>=?賘YrKhGC yC;D"ݮ3\4Yt!kEt4D*Ǣmlf!H2ҾOmW3Lh7+;z=L:̱حFf}fplhHW(Rp=L`Oa2vوe(  >|eN9)%Snoq}״"=&]0*ovK;Lg̳rwmԽ[]]"hT[Sa|%{bDlıF!)vy/>6t32j#+LmZ=c&F.[9vR&p`U [x8579;s1Q)q803cb.7n|SfmyBHIv?0PusNjq8bQC/2Q<\7=sn[+5ۙrQ:tvE2}c=೩Z_/IkYQ0ޥ⧺tY,c5<J4jzjp0į< 6oLx,E\;\y0#U3Xf2:/?ybs"@W5 -BPȘ$;P)pep+-@E ۱h ]616_,*s0}MojDbKTP/RlN!8rNEXS }}u,u7^w/>#a-x0z DG&_hX`AaP+؋|G!gh;AWAn&sKPҍq*%q:ypzvs@Ydv b\S?8Gt,ra g#TBcؼkOc˯HrIGf#LLJd,N<gTBsWCA0ےqX_?y1|\3PZg?f9\yGtCL6q:',& -ܼ{pmy{\ARk<^Sľ(HK-qRg107R> IЋ^[+\שjHkIot@-⧴7V,*9 R\UyTVuaBbƿ1 dQ`v= @WX3RSٙAkXZ~IZ*4a.iC6T,wlJnGDKu.j+FRlg3r;.mSyx{WgQ*Yt{1op0/b3M>YMj:-ffA3 p#_qa!-kCSLxyByn#Ek~. ⏴ ֈ RnOyxM w͌0ΰ[ܭB%ׂ__B&x..lDJDZh[l +5F:x;K+ʲ\yW_@?ISo[i<ՀܤWvFe? جck-وj([ݿf06]I/]dUT&8/4/_3+2GRj*AM O9bur*),% o}L20~5L}(~xِ!8l܋ßxfʍbt5ůKK=ԼjnmdDϢT3ݧDmLΑlMu2W*0Dqc%a" iEi¸Ut0A/vcEyx@} *ΪQ^ﳬWsbdNC=EFf5xmoȔCWib(4ЧZ[փ0)FA+t>ʽyN1lƯ>0x8gAl+lA2FI=C(3G1S48?쳵 1!g-j7wnT,rlN u㙀#_ beĪ`e-?t;xoq^[7y*srnR13kOeG ەKsDG0TgIm&"*.|x]y{҅즾"&X :F Z;/ZVrusx/%zwV[{5^E0PTo]{]|LLM  חS  6(~g1*ҹpmE _SpZ{և-5 ¿ܯ; d9Έk$i TR&~ȣCL;1u ݹv`! \c~%{u}1^2͐5VƢKuMJ)PցcE bX_%3^<ŠeQniGyJW_ΊbcgCg'Ɇ8\UJpz,C/ ;ڊe*p}eԷ s̭l0*Rw (J2V7dSY,\^D,YcS^^cP@n@ l?(ljivþ[RTcm,x C qz^ h5)okѴL@lk^pi03sfR!W{ruE0޽P7WF*pFS' qa#kɰ }u'T 2rxѾV /5i7Ra"ԦkSn DS @ߺjNf >=̅vfeԍƱ(]g/i\Ӌz@}ӦXiP&N 3Gr7v-h;AA08Het;@TldM7Ѯ8Vhׇ"t-_.#e PglqAvR~IV|_JU)wmF"rU*B(ڷ v `j)?Kf:+R˗P*>ڞX & iӨyzAn?L*vʺq:g!ZbF+Jx9eLьF``R\',xҜ% k^ 4,j8L3r8(b(d̥]UgT/E.AX7X< L¹8z] >&Өyc/u?h5SD#x.+y`tvB4 %{,”fǥC} ܣG ԏo X$wgYeCPa)D!pP=H !P_(w:x״Vf%F_/(J> 0 X0Aw {17drn򍣱8N58or=C\eTs\F*.iZ/""NCƠ6b=Y fFp'_1Fa,~^ѹvL%NZ;Ҹj6^Y]NKQ9jZy .[;i euʸf0S߿IN{KCg\(-~ fA`9ka.`:]bRl~nMw^ n)*,/@rA^rGJ"SwXT&Qi?X9d\zeDzE@濾 ™)uɴ=_i\;]>k#~7,b75tStpyMn%OG3⢪y~hLcah3veÈnV꾦0/y}fwf h{`r{ΜRq&HMɸ?|i4(, saky4 =LX`10̂^|GBM;(3O,Mnd,C|)4VD3l@v!`(')il"++(cXcJۓ,z@ Mw!RَCh9J- ϓlP|[.lxD(XcP!Z A* -;G1*̳Es+̶yS=:"ʻLjoe(#b[ ϼn:Nx(cWeK^)=9v 阱YU{͙;R=it%İw1&˃L+e nkcɺ5W_{]X73E)cNO[?׬~y3]pP9v')@P܍+y:L}v_ R9iCغ1l&MP9 3.ؖO0ipo;u12ͣ-l4W4Dte_aX#>ޱQ!YW{JþiN7eȩ2 \P]XI\ y[Z#s{KmM)%<ܬumf:Fߙ7uh&(RPlr8͜)rۆe&<`Ru6cY@]piX`!RH֎ =P;n=\3" AH\5'M GP*pF"_Dv8ʑ 59w6Cv uc`N,eBݢlS>`-zSG0+US_`INk(O#[X5n58|T[2蘜)> Rl*FI>:]zinwOMu_-R՘8 LZ4S(̞|J^eC8ԶMj80 kb:#䍥t?x Mijz"y4)RKHŦ\T2X4#ڱ݄۳n,'vY"mx5 ;+(}k ˿lJpo#/(lUQp8S5NC/-76j%+Lps0iS i:<@h}UӞbĠ~߃aQ7#*v}}7GGGQ$X֥reۥuqC1ʧk/X7pJuVo3Ji%FפZ:VR 8Vdeu-o=k@-.E~D~9j 2ccT2+fR-*myqnLeTEn9˔,5b] N(Q 2ݞXmxNqf/-WT3,LQa|.:-Ws&1BJKUUQu/ Xqc̰ K(uf UkHL~ "YYƷaSa3jÒn3908#_X4@5α}pt; q *m\AZ8r2ƫEهR> pza=75{Ģ2W*:@Z KoX%k~􉉧P@5RI 6UET_vN{7dfk_!N9%PY7ۤu!PSTBUroA%-H"2XVGIsIQP|п rB5.p}`!riC&n tKzL˘DF55+;O러DYElA -grl= 91Q+.%JSS2ӯՌ/^1!􂥀ٞ+hc^Ng>pdu9:M37+5g,EEkAy*lFQnZ~C}!WpwU7*k f6fQQjȝ扺:ڟȧ l kmAp@fC#ˡLqs/$;uu; ^S7+M:Cдۊ ;&ic.CgMU}'\!{%z5X:55\CQTW8fk>0  w,yB9<v1{ܪb6ǴW\thP c|z |6|qqgM9V:B ?FT9ה(7G N8 iw rx! jϡ(ݙxX2ʶcK5}i=u5Cs+Eqhj׈EU_)!_mK xb珙@r̍l2{9C,T(r\rCb\S'J[';K#؊!*UŠ jcuD5)ia*c̷muoAd`@k Kb1קoN`DR¸ , zb%骻AϤtλ4Q,|acEyuYֹ-|A9/iV^jTF/LOvpOc9C=?빉Pfrzw Ɋ<Z.˗>ފfM;Fh3V% P',-+a] KgDZZVq9D+f=f RK|7 1eQGwt9T'i˓ԏ ^O9Qt0 Jĕkkc:>[~q Dh<=&`{dJΠ BxWPt%T~{i(vnPF 5KC"H2\Wi'ٹ|8P 3 ĸ1$t} P3g.%{n{ -*k$OA<@k^{VF rŴo=wίhT 2v)itsrK)0RQ&̩[tg\k YmZ "gdIPљiӣ=}fgl8l}c.ǘ4*`fj\/ъAg̤pO35B_2G7QsʨhRၥI{푱at\{h![LJhcu>D׵L@3([|Fq<ĺq4#L^jA8}9#ѷA% 1K_V`菊j`JsZ, n5|DX t.}%ٜK^ p7+2TPjffn&b..`HԏJ6kI*hVSe+j>ePɖW (prCo8W"8"eXΐYMaˮE5\0QX#q1A/!~ϛ"BfAyc]!N ^ٍve(YA3CvьMҔ\4 i8HNPeLk^1,a. =!'V2r^XK֫`!*AǼ!u# -:@׹ Z_׏JE%1S-naNefdK;ezd 4')6;Wr˅)Sw٨ w {JuxB/fL=Jd2ڰဿq*1ײj0lX:jMޥ59=a_~%x/`=vf]VWY`VHhkTy?ݽo +,D6Kޘ:Bڕ ‚S$93k7WU|fiD5|2ߔGRӻvw !2'<WEo{lK!hvz[e1}fo~¥3 _1S_蓏*mR:Vp8qvo@ 8CVK@;$ eBs̛\AC #K䎉fU>&p*X/tpD\"X Jd,2e h-W0ֹW0-!;%.D!uԱo Geѭӓ MRPe/*%̴u89Umt'U*avC]ʼn6}JB GKU+xtu_tE,Wԕ_N1Tq1T}?NOw0  WlPKtK!*]He|ͽGl)cMÓ")KŐzEas>*O:h4Bg+/ DhlٰykY̺f.HJAH9rDWq2tb6Se۪K*,gC@%U0tWȻAD|Gg˒0qbY VZ0:V]Z")}Y4ltNyqQM13D 9ZΦbG&nҠÉg\$p3&%UI}J8ޓ( N}ҙfIc26w@z+S2sc,?Xg^c$FIqUl {_&Tͷ̺z۪_އ+D1b^gCpA\m>]+1`R_T ;sa IZ|[9s1"n 5CJ(auqݭP:O4.s+=rgЎ2v< HPn/O8sbA|0<%8^FtA9԰Zw4*s,r8@ hpgBU+o7NEq @w ѾoNd\W2oa0sI"'Ml)S30f5o,nQLBk84:i)LxodPkg\ L/wK./t k}BV%7kC7eÇ$h! xGZ6 &z?+LcgrԩUӴLgC1sӦmdh:N'W~-LLsT|z%-X9h[|%%Ur)Q:l+͗|CQC qRx; tJ 4_Y͘|fep;@ KcL '|z852w& 0&QڽY(EI#7ˈ[K Կ|c?R4ωe3)G Y2q) ?XVx|L`QMavMҀ YQX%mpg3M`x J+oˊ-0_k?_n:1-# n*vjs/jxO[U :_R{C|S5IpV E[ tDxALZ22FAܹ§KPv,B FNVp?E}f$;L; K_\w۲sS>n:^W3DsMʲ/OB$Zt=|L"~Yrkl@BO p1R¾H0M ڨA+mħ荆qJPz8PJ@59" C QIT6yn.v#\φmmQj ^s/Ky#*"!6C |J&֘2%ӈeK%0y[x5ᙅK"!^b2Z2i˙˟ e)Eݯeqȥ5j,3]lVN2ƫ5bo11u)~gz~"r_I{( =sɃT{˼u~)9g%.XE)Lf |XI~R3/WI!=#)):J^.Zqܑ[@aGfiޏUYvNndE䧳LTTJ^qPY%'A8b㼩WM?1%V^.Q%{mpSC;#0$ƈWmTˑ5.ԡ7QU~R,\6R!:]A V @x=}fơLy#=pSW A&h>LQ,VY"u9[v)̣j2xf'a'Zy23#v} ݡs"l=m9(&%q*VT N%x[~&%~Sx%x X[^#ӹz̫S4dv:CGPy마NYНۀ.VVD`r }%z~ ahr1b ~GRSvJݵs2>t2q՗z9ˎ𣈬ll_(JzrӨ>!R2z03gY>Ъ]61.5Xeݩ(/ӈZΦK7q,Ctu3 hQn}2)eJTfp)Й+x#3_fMƷeU{%T;·B!$3bkhr{w+'94d]NAx=j S+* m9-`u8Ҿ%Er|D5 O̓){]0T8@<[F?5@r⯷]"-X!O \`b5Y6lZc`&rbanK?@FrE a[D6_M4 JLq3+yQy=">龡+'%G70j*eHs=X2Y]~ muYH}‹28^Sb8QT՟hL#x9Zxk~!¨|\6$p }%7tK' ̎YU3rzO6Jۙ3P ү@f YY޸] a2uC,hs ;,wcmcP}cNC A0[qK?(bYiy 8G8{xѴ+ߙ*@k6^N\cqEB0 ![4švKWvPR\b-WB}Eܶu<ǰO*O7ַ+fvؖ0 FkSb^ }n͋ףFN5dPNY(/-VT#oըfImn6Ʀd@B_ISў+:Ni]Z2^.`Kex?)J2uM%b yFXf%D g)k1!jwGO*usn7S2wI@ʢ1fs 2RJ6R 7)?Mbfzn'L"&E 1'z!R^FK!}Iӊ ]bZ@Jt$6^.v3+ PIJ)2FsYi+OUVMyDK_dǰˡq9Кa va-de`ģ|Z:=yo`Xw1緘δJqYr^4|t乱G,X)X,S \PGb_I`cv&H9%V `&6ʼ so꒩ 1CAo,))>,Ez-OA g0] aK֞GШ1yd[T6&؏q ,t`G_&s19Pg!'7 9 fwg3I.eDc>F)ΉLWY Y4xL &,m2weN!ĽgTh仪LqZMd&Xad\1243+q 0H)}QJE* Fk"G>m>ӓa!"{ѝ}i  pLXQ\< /o߿݈L 1 !y 6`dӻ2nX)^,ZU2F'2q}rTAaVUNжYje\=]fFYږ\/3$}.Yr.kCNh ;Z~k%-.;!Ny"}W&#y.s(=/wؘ>VO#jiЇ 6kTE ETu4,}AGXo/uܕ9+r8+ a!eM1$p0>"Pe F\g_5QQN;C [m3 ee_pg‡SZ|[?dfVl8-mJ{2G\־g'mSM;1M%ҹz' Omkn`/ӆ?D:ԡCyͺd -f/Qx_aZuJi.WlDR=/*EcnTL}!=WXoC|-v3u9k5"y&|X,\;8`<,GCkDܨ)v@XfEL-"\*s([߫}gC2E%:*3iPX9ݶf27ԗ6@4*c!ŘOɣa _s+8秆i<6|\(y_)a{,'78;[ O{\4.(BQ<!/oio.ml)2b!q d˿MbfT0j[he]hyD`/3w~I\^(&,ےÇ'C QLЩӿĻe<& `1/ PwQp=YY|ŝa1nZa~sh혠IkԻW YYu\J.uqa).A+sS)Z"7u.!ʤ y{Lyt˭}Z>k)2:21yu7K;83Ծrena4zz1s/G$̻Q#AX%te}"Ef51s3[T/j n6n*g5*+iNP.Q" ʳGf;Fgf6Z7*ӯM!y6d37K0JƠVvgkk kgK5\R]w2_s,}Ha5V(ԀYyS%_ MIB=XP,p'#l%QuSN}FTZ`A\p}s*TɆdٿYf<ޮekΆ$AϼQa)_R`~ܰ5gl0 0=_V(^@|Po3,s}HfWf}|Yf!~ l#"eHg蜲AkѦ98B2D+R et3p2IbSal˼04#7qljۼ@}%,m^rÔ5 ^&pkњFJ0@ -_/_#Gt=ߥ&*z&~Wa\תvj[3|g'̹,a73ƜC:u3b[q&P;3n٤~f4l#h0;eN$.\׭Ds_nI[L($S#jG1:e[[a :Mߍ9sKG1fJ"T/H?TJeB:lqIbgpWl>pd9VѿD 0FZ\8L_iOyuTՇٍ̽`c6#ӬxhT~s-Yؕq3n2J`M3*?b }#GeVyFof<.0˩YcÈ0[N"Id9뼭urK.˲[)A=Rs9Mq2#|ͿO7~ʂh1_mk#~o05 D6Q+_8h0TA8~!In߹+sWJt:mg}љ=xVfe{9N_Z0a!b2uO/CzMl+c*Xbά>@ĥ ÝJ+ҏ~p[s 5pY͉OɹQƊ 1fj|Ҍ bz<.;:Cp8<ǡQCmjqL ܱKG J/B+W2x3e uw5'ZY@;ObGAylz˸oG,_+MarV|E f\}e}B,tg>'򏥱{YC@;OCs&q(Yٹlʬ@]IONv2^j , ^tF%/JO-6F]JD!n5۔5̱ooS,_ghe;ןJN zX%t/֏T$+g/zCP5V xP/5wpVj㯩o512]-#\6u0BqWXegEom|EuS"J-Z&k/gLp2ZMYNs,F: r/w}6l[xunU6D62#]R-n+"bS۷SϠg៊4CJl_ Ŷ*=SV#|33"9pva7U&,-JjncG\5GflXruE 0[ \<^~YSe-qybX5/\?tI*o\X*efؿB[]#dOywNgR#Srňs%0?(, M3W_i|c]H;} PsSj$|ɣm+ y>ψSs(:z;j=G5yFTZ^ 0WxYL@PTIR3#oWaGy'b.\\cޅMS\0S1pŸ~?lLfX/?eMw ߂ozhu#g`z.,0ј NB 0f<0S~+@nj癖J'i+9en1:fjcё0Sƥ-L{Dys. vzJW̪Z2M2hG&68F,hseCGej]89V]X[2E~_> 2؜dr]B ۼx)܃Ŷ:ܲ ecĢi{\G<aIȕ/+q.q*N!O"V$1}Y7>VܚeGcuocBV0jwcmh,By16Uvv< 5BXLy)ĽVx' Fr0XwaN.20vgcQc4H>&ǙytKH/_2,]#-ݼ̉g)Ei71w4Gn)_ $J޻MGNf-SamڋE3NqU>鈀PƥF-i=EvVF9-U<7*~Ia-ku21O ڐs#Z5qc1B+9n1@=tӆqʩAyw,ZÍ`+os)"/%@ Jή%^L6/yA,iT&rSrwܥGgICבLOgfg&0E d4;Sg2@5fSAD:Wyn zX#y~U7f̸X1G\U{k7=$vCATDw7q1ü;1 T'RX0;S>^.' %pZ>aH:i*q?}=Q J.:J(OCܔf#I 0TKHY۩-e; ިL՗A7.We20tv:ٝ~g0*ߖZ1!LqiC.gb+70p`UjGh9G[ q:yr,d|Զ 㙞 ?=sq&Y=>O-n0JaADj~uMIk.vDsS&Еq閇0Ic^f)q^f9B\fu q 1+܉TvJ{&r`擴^U~s0Bf nTEEwԵ2;/~p:]~Rƻ"#:OKq^%s Tc{ʰ~띟SЇiv G9obvche\3;@-n=#_g̹Ri)}=ƍP_!uNn{[KW|JHXv1)fC]MGw ޠ]ǫ|1С\,xX]cu /9Q; v2qb^%Sat);3Me|Cr9*a`3^nwz.g̥qԮӡLG? LRcge8άܢVxR/y%<}Q==Z6c楛8L<@ k=c9 %)%Ǧ'3~ g>˧4coO9"s7ـvXwgt#ɡFJҳF0І2C9d¬wjAϏI2B.(+)Ybbnc+3G0X;ͣN"`]7ܞݝLЅ(>DžʶQ{NjS]S]B:4x%,U0:Ϣ>=KVF}陔X+ai3a7yvZ4 )ڃ[NDwO^~#ѣP2:Kc-k.Ȋ^k%2\˄szjLL1&qqʻ3dt;MADP+c{jh ^&Z^ +%{âh-bծe~#5[PFcB2-0%n2{LѼb,~aSqȫU6ī6ʋQϻO>'N% '1y}<¼L!bopsb\?hz4WzQb{ b'<]#kUQz9+-+Q! :1MOE\el_&wYMGImOV?812zƹ'ȷgg3LՓ` 5R"7틤.`NS%ʆ=9 wrzns9ۜ_[/iVcF-oXm0MR <>Dqǎ=Ftxϙ]5s^~gs4+7RmPV~jq (P+<1#0Q vD܎J`ZV{5I5h/9$sQ6(;kSdeivJQbssmi~c8Ks~==" L32cxr-LLgN`2Ѭ0)JDt}*d.Rs0q]%T-hJ tjLtOi nvo:6A/Z8Snڥ=xiޱ7,F 8gZICلmKY܍PܳWȈM!ꌚ/1q^;9h= SQ̒R,NW~T6޾ed1~]x: +]438=Ǡw cQTucqb2[zIk˰B.D37(wvy*3g/K9xeJ; zY{J[*.)^%~Ck8ZW%刱m3!0CX/LOĻ,>J}(J%Td~H}ѱCf/a'S vm GInw bs-ǟS!d۝RG>5Īnc!vaX xYUJu\o-;17K{x/ea}NJLf lN*4#| ]f_uwswĿ 3Vlpg΃!B4%,Of}7 Ny%ܓwR/ѯ^/5 jsң屪˞ۨ$+2f]RN/9~+PN*@sLĠ~Bʀʥ ݾƥYUoZ5,6TgUBW jU[۟H*Ú0 &.1p#ڠWAiB]~0}+db ]vEgh):͈I(`|?tcQXDLwgNɨzae>S718w h}Y3T W:?Ƭ{況}0lpao$t6C6}fW_9DIg^LgP S!h?skZ `{|5P{ m!g:"MLC= Y}q+SF22R(Ļōyo"&Ne|+-7a5 48ӿ_ގe GH˸+q`'Bgz6{~=XY 'tD=1OdN7AeTR'3^YdNDy&} z-S Д3{eG {"[.o;QxkT#fMf+g0|*KXās^Cܖ3~bm { ]=u8z)ܠh+1]]ݏGc(ؚ51@iLx0f\RseHɁ͞bX㈻"Z;$X(:89O;b/>}'髠> $-%TuY#MXbep pFpƝb")<^.eq ON\)}!3p {:VX Por=4Q؎=As X(^3<̪L,}ĵ% `勸<@z^2Ma(-lG׫X'BVu/XqT[IQ0&UNV#/f$kxzzkIe724yzo2Wa4@ vt c$hY9ε7%w% Aٞ`H8'()%u3.~4py;.Sxk՗fSobѤ̡ 萯 (gs˙YzC lp@daLYnk{f1vnK\YL:K) *K&,ρ~gmR/KdQ=WчCT\Ks238vp#Ib-c>P@a&h%Fos~"s"Am1L#,D𘾒`do&fqQA4I{ē$*@Uڍ8qK[-j z mtq+0flHg9pL )݄v`c>#<х}@p%ncџi"T1+u.+d^*P1n}y#SL9G_eu=݋1}aD>X6:[;YzOОIwl;AHxرQ-r ӇYi3Lt^20E3znsL0i[rvwNc}7\Tj|ٽ ǥ?2S!2ƙ]#fK^BO1,KJ}-W傰xJ1 秴/i 0Y-}3-(h-N_.W!ZU0UҎ(s,50u3(ryl k? ; {,AؤyW_>wij .k hNҜe8/ܞDvҷ/ C6 e1h/T2n!0_ޑj"*ܗ*-Tfx+`-7D.w*_Ϫߤ12ޥ9ͶOy 89yv &&q4b"wR%Lm_7U(2  2F̺AvT8ZT`)92ֶ̠w 7r].XPm79A3"~浘 }(<#['M X-̙U3Q^дP Sh9>m|])5JhTuIPY-& s49ΦLŲ yi/c%C}fZzz2_K=cu7Eyֽ1Ms;&,[UQl-o%T؆ ;J`'0pԾ:Jke{TJ;s`5(<] J ߈^wV%<{\ʧG \2|Jo%f(c~&r,W5Oiǘ`M Ûo%h !pA(`1 (CEch,_i5Lc8O*Q`M B 2BW(Uz$0&w˳` 8 k3EH\u1̫W FڎT蟑ƫ76Bf~4kq5ƿmX4jR`)rU&Kd5_hMlalLD&~pr$zj8g%z Me\eFFiӏFopz03]3ǥF %PJi/^s|"4Q.bb.1V1j1rCzЍ5g)}a'I1ݧ[L0 `T,};jf#; *uC*5`ORܑuҥ, 5X(s(1}Kg@QPSL/\Muxj%fbtnKჸktXQaW `)!_ 'e@%#6pfd,'w-"* uIpaMv*rf.9"eB5ǰu! _J+9^&FS̽4>/?J>Y|jjcY(pMosuj˴"<: }BzMO1ǯ2#؏} <z&Sy(VJ踄N%WWĪg!JMs9y#pplЕ¿4dǙm۴\Kc)xU@(F>C\xZjc2AcW-t3aќG.9EW 5 3bi2rܻTeeЗJXBU60"Kݱ`?/7>҈"1sZu{nb wipt{ʑ1Nc*Ѡ/rfE=b^950 ]Y* pq@}e${"QI4cw#~ص~7rfQeoDNefjԫ~!cN.fp_F%W9fs*gʢ@:Mn Ö9>!Zi_AwsQJ3̊Cz0Lquwc$ aԮ '%̹ ֮Q` XLjJ?;1)rzX_2^&&aRu#hwacΌD,%Ĭ-aB;-L Ś#E"d^gmeu"Ըe[M'0uVBb& (Oe|&ԮRW-~7%M,;8`:pJgH:8\ L5u Hu5ƒGJ+vk{RbQK{eCw@^(Ꙧ9D}e&ۇg9ɂ:!G0)f}.{C tq.4@0g zF0f;.C`Qn7:FRʲl ` ΄]Mb--2Hx"NyaUѬq3pwCi~!fsG63E{ť="[~AzNO5D 8*ݟ cKs=<Hi L_ڗ >`Z\ 90Q.C<AN8 4@rcw_lu,>{?M"򹒍x ͬJ2a3̵,Û,S"YmJQ+3SҽN`9ԋ1hz*i2fxf(&ebpT?V X>f/<]j1^#JFGBmn1`.U #k *?Pmx<gH7C ʷj26_#3D_KF8f#cҨ'hdI^TJb1 v&&Z͕ ip5;tPTXqslFi#9jg̨w&& d{N!sewk~_$ Ji#YrPUb;f"/Kc9i z5Z=<55ʢl @|,3*>铄,9L$! ϡkѿ]>5˛Hϣ~YjQ1T?uۊdڹ2^͗bc"S''dNMxA͹ _hP|qEcPlT jϰe!.-;yҭi ]r\Acۜ 5g&Suh>uc3}jvBb`.zi G702gP̪bY/=QLh0 d:KkY|3cI!\j+2,GXvc$!8^J:A^3:B5DrK<2=BPXJC8RрIoG'P eep]y _#9=C߬2F 6i J.g/Emr>:=(ff,tZ,C3ZoE)I@~4@fHĢOX8|DBɾrDe_X9 7! CC}>"TR@ Yߢ\KĪ7d73}bw>YFRS*(=sa}a8lje}XR`7M9/Ҩ_yd..k d֘ W^0,%J :_XY`C/A+Sp34#Bimeqś*醥0yk:53b* ǘ=Jv2GieR$\QS}c(ێ,`)ŸH<@nJV(.sA4u7٣B\oLs*THzWTR\3} Lښh|]hk,4*k>hNg EKzi~Ie@W@/Ó@`gcѴJL ~\lZmm[򩢧91;s;u(-re^]18"WA,oi%·lF+zv\T,J0I>JNJc9%WZ=22*ehZE=AUxQɿi}Y\E!$վT'DycmpVly3( FKt40bS<:* 535 ]~[:8=>2Lec^ԩY|76^)1u/ "WT*.UI% o3 rɛ9f~Rnl6̪-;N{6&-Q>%t59 =SD} =35aAy5*Bh+mX7q/.:S8 n[9oPw+טp)`]_3c7)'9әUܰWŌh!f&G"^L2ZhДMZD:y!:?4Z[{gK~9 v>O%7|K_XGYYLCO}X=e*!1AQaq 0@P?/TR ҿ}B.\ ./B.\r.\HAGqcYKH:8Z- ^ r˗/+.\peƢtr/~\aU@˗t(0Eȸ.C \(HL :?rѹr˗`˗\ yʉ 8?ĹqK?}<=1[3=P@:J333a!/_\r_K.__a6 uG=02/aHtFTRt*S:$r˗*ErTJ+ tW򨒥tN+RJRQb躃^eAX._*T}oYR W&[`UiQCfU|dNL#Ѓܹ}.\}_}/*J\qbqܸ *U[]ZS]@5T*TAper˃.?Q%tu ~@6&B1s_n\Yrˋ/(zBTI_en-q!c ҄_*T\/*TR}n\}n\Yrآ+miHrM%JTQ%u*.qt#lhtCۡab=0ƒK#$u\2չrοJ+/qa O)D8f՟~~%uRu.\} _CsGAp /u]rUꌾ K.\Zҥtj$a0^J`Dfaf#% ;ch[$]#|oM*!P%J/lJJ+e1_ٔʘrԯ}.\r\}*T+ + l|B涞 2#ԓ.5io_edͶYr˗JRq ˗._\QǢ e"G>BQ(*$p`˗.}C*;.\r\}jWa:nΠbi{x* +u/K -(,wF* ވLڮMsX[n\r.\rR'f/6XvkLJ< n㺪eOF A* M\a2X\uV+d4U!"`w2yFXZLPr .CGt%n;]*B `[ekIܴ(䟿撿rӹ0z;T0Q8HTAi,splXUo+L`Bj|ےpӎ^vAUTUr@ ݩxF- ҉6X`JVt*422tr˗_r J*_NB|gh6 fc ANs%$5& 2]FE\`awt$K.`=//]W{rXIPHͰ]/l h4גBlHƱ#߿ig~0)#Q^B}~/1~isX(ϘK? O)׼ǩœ| F":_*W0e9꧜ف!NsSUKB:0 !JoRv00B#< dSxKY/d`y Ц^‹c^/ e88)Ef!PӃ{0I9!npHW .,y贝0rǟ5r` P{ioPw8B4)ōj*k+LvS2riʆT =o3#Vf ;+ODA, B!n=Ņxq9MBϓ=cf%n7-o%[*ϙV Ru}@+E?\rѹUnTJDK1V9F$yQ! ՕbdaR8+V-)9KHHW>yuhw$%5˗._r.\W*\+}vEzd(l0(LZ>_T0A r|m,%Zd:Jk#s{|22%baizIf; R[*r9>0m5V򚔆C~f YuӮ 7 BhrԿ.\rɨof);`S# " X' 1.Xq ]S!qr#?SdHrX^ ̏:5QtuS&Gi/v/99IsXr?/B082Y_Әjg?Ws[O*j]460T/:=e4XX-yJIv쵨<̮r I<1K˿}wLx)P!osAոm;:QPJ)^׵6OD*& $NJ_E4^ ljRҹr$RPG"DzcUn W]JRU[J*7 q!k~JU7.ԫFv)ح@%괡_.;3yY7CՓ.J pS~KCβJݝ[ Bo_*TIRWST\z,<Ϳ;#i}=o+o"L*Y\ޞC-eeuIyW*j& cvsA-koG ZP&>.(14ODVwwwzV>+ @ ү?#P ZDHqV2ŔN!S-_C8xn$_w.\_wy`堨5SP=[]Uhߨ+%Iʺ!^d"-r׹R=Ve}t#TWM} Ǚj'kLPY1P1 #c{j0{B|l2s0e?Tf4ypwFoʵ 65D"_ '%. C@mcǍy~&mR{ 7J/H N_rr%J#/^zmm^4Lny%Q3''=8MAoklJFNפx-e˭+R[Ÿ.Uq 7.\"UmIP`̺֋UAZ&K8! |sr'ĻN**WG~0UxCoAMrR)Q 1ejleϭTZ\ L RԡV( %r7(򐥗FJk&ْ+l;nT "^6Al? QEB7i3NU z Jkk0ч㔙f}X=+E| =Cf⮾kC?5JӰnJM$q 77)RV9Db;g{3jø ^ M!{J&ST8b0|}n\KrkM ᙪg`N%fi)nX\Bh*~An?79^,雔? 2˵`f+ 5Pmu;[26aB[*z2,!MI2p9WBmc,tzWr˗/L[)+ZvsCڊٓ:EXJcjnݔCrw _L܌ؽH)ݧ=@;pU/%QvS98㜭q?wrSO8L]aY#;wѥKiK)c7)c@L"/[1JXP`h[y!EboeZqO{C#\_._/(4rRJ3ʸ"9b5De]l"I))?v2ª#bu+'E`⇵k{ݒ)ǿ`BW9\%V, ݕajk*O*WQ̹D׶:G{D|Lcx| |Z u;yϠQ)2❘V0Z%>' ¥={9 J^rl!,!߸2, JN-`!b`D[QYvcf`2`)q77gSjB6>!ݼk˗/r%]*k#rf7ӈ4$57,`B4#*e)G?,1̺"ިʇSk-tTݨX hEs%%==u<8c;y$Iڢ_O37`|& *u}HTj;?bielU<dHcC,sD$&651ah-`ERJjXh` 2_D}Zu> Ha# ʸƔ6?}ʘG$KKK< r,1 r^2 1â!:^qm̗zi~!ME{=OVHv2Y._}oQ2T66R^}H?~lEk9S }eƕ=O If¢0bXb7sv<.\zy"C5olLD\!6 Kr}o\r+øj&ʣbxZr66.0h#M`킣UNQ,9 7ZEZhri-,QU-_!b_r~LEN% Y8Vˢ\˗/w-eD dv>'iዀ6"C=/SP3Co?ք׾n6'{A*ntf.ܼfg[h4ݧ?n豎!}X4Z!)J7fdg3pc+}ށTN8M{d0n䲂csΧm>"[Sߊ.ϧw r˗._J7/tPiI_ ɦxc d5c0O5Jk$BVe!M[FwׄX1 bCzeӫ@څa pd瑗y.pT>Q9H˓u}ru/jW}.\˗/_[k r ÿc3d0EoUKh0֯f*|b:neоY ʚQMN#ZbJ||b#1!b#IYb8a&£0Uo!J^7zG+"+B1vP/Sʌ_JVRY~S+vy!TiBɸ|[2%Ɏ^ kJ;[C;pTX:ZWbam4ps}53c2g[xh0(bP+g>f~#j3m Y2BT1Wous<L4~Xu *~be˗/\"i&)㝻9_k'e3l-a8/ A._^,&wRHϘ+ z3b@LTz&^G{jig鸎ځM/o+3 ԳRrd4BM :nI++Њ+~(D,)\Gn<:e1w8~f7\2}La_bm5 U8#22OUM=хJ*A.U{\+-ێ5L?.L9mJ0TJw?WՍ]^UM^Xy{6(w," Ev ;QB|`X7 ֘|{JzW|2W.\r ev~ }?|:Vb<8}~*)q Rӽivj $2pTh @}e0w~=Ȥso!SyvCq~? ," v{':#&jr Q}3c6 6'j-MرSJR&tt<^RT>gץ -GDqKƭЪqUqy !pʏ"ݱk{Q5,?f~茗U]߃xbݯ!T U?uL7LI!n$ϯw/u@sɳFpb:(ٜ&v<-!Ji5]ahj|NpqG7īf-gzWXp.FX}TD嵍/\< lwA-R]ц7NX- i>%tB!rls%;.rRJ޵*_|U$WoJ])yM3cOi8BG &)t"J6K* ^pk$%+Px}Pp+ T&IQpς ئ$c+i v0Ơ.܌bW!Zq4y!YrC"b6VHJsv\?BG3v<6b ؀g!|0GXɹ&\  C?8drSl͓fGo$e)cZ5'ҩO M[RRw3ȳ+x7e8EsaqUeFEApJE$Ko }o$z*hsk|gs(J.f!E ὏ `,yC}J"ӤKxNA.=]мɦTӲv34=(+ T1k)L`XR* (+[_c3%UJ][%s+?ځ t(4MɬnMe=̻@a@Ƽt T's>F½ɞ?L.U}FE[%y8T NrDB QH @m$ +xȕZ3@f"3 &11T31&tX#ζ4d 5/C"i UKɖ/0P_{k _o,8y^1nx˲1f,u+VDt5߀)C@<:g`pBVicАѷ]ku._lwe[""mƛȝcpg[}(S`ԯn&N3ɃFgusf#0eqY 0 NnuƬ^Y9 P( lL@N Өy8M<8pJMn 036S4\9u-g CWkU˗._KЮ͙0mCE %ak+>M]1~bs,vCΎ dx=8LZ00ො^o |=تUDϴ^)7V\^Sұm (> bjŽ3 1k'#Y2'FNQP9P'?&zԩ_.=YkіU#s_H&L(szf3ty5`'Qs4$4ld|cU+w F1Qq3ev0KȌn y ZjF>X{ X5EuƠ/wHnͷK7%$U33U|f6cPaM{ƹn)7Vþ+/m`3N tݚs~ N`?*`rU'(Y.Ԥ1#،aYGm) X%,j*v~ێNQ{[W(!| Ǭ Co5W1Q3HsX<]AgT6?K*OOWm!23ow=NB/CR[%L_ aB˖K",Dl4oX}Ec{+NC_ <}#цH22ҊPB EW )X 1b X- XD p3v"adv%j ¹ca-Kޣl;Wg/ F/Y&Wj+8lßlq2N*YN0_7‚lՙ> ͋!&צ,[L R>Pq%I/P'ǤXc0I?셞*/aXA PŶ im>J4PdZŔpOP[,߄Y@+UeeIYbSukQ3`e#Cp4ªddcMU2VKa.Wc Ԧg]7Byxo>=h~6~|j#W%.15Uo_㾗/꿃SWt=_/10)tx ˥e?T ^~FE b ž'.lT<69հVDhoL4nk_-Id`66s^t+ )Z4i 5`[dޠ~Ț[jA1uNTSBhƫs1/&cSMiLpKxqV>+aRp!/?bA6ʬh [1gNDZg?_V5N4jpq;|E=WR F_'/g_1 Vo\Ꮏ&W7J '-UhTAC/"FemyBdMe®3p/Zb?Qn6a@$2Nl8#@9le#.!9pzPXIS +q*z4kjs?8.Rv0.Pa]438gI@8 mfh<4T$݈^/A(/mmyTXmeI{KfnԗUf%aܐ[-N*YK c'x녮gljbqw`wr *C[cpɗ7lӫy X0̉oR^U]2 k8[aOTk./ByBllNDǢ zr힇_ORkǘY,J;MQ*˗6S 4nɥbR0K];*y<b,WQ36 ]=t|v `ÃMxC Ac@J3{UԢStcU 9=gT9S 2I?C2]ciiBhFfnb[bml65rexLULY>8(j.l\1UZDsue ,MkEBƞU_%kbvqQ&j^,y82ZFh"-d N`0;lmr wREE0k`, b8:Z%ĻPklN;c+8e_[wc8ߗe;AsGvW඲_dG¿~12[ǨZudҥ}]+p؏@ 0j_cVgٙc:W}XF6W JSpߎ*[Tp*RKr܊%xnʐ,N3_b| D4 Xp C" \iż FypMd&Yj` q4t%*ʇp_`j9Xv-FQlee~ tlbCXZL)DҳErj-<8\N&9cx`51F\YcWGtn_5ÕL+JB;aB hdI|vJHgjbQDG9ya`pfzFԿt 5Zu,4Ŭ5z׹Ah!Bj)V4h$vn+%L2grI(Ҷcɗ& 9sPagI{@T)(xuZj'8 uݩ{@XBȮh9HX ^E k0,- `J?-R#1FU<{U'h4@ x@̢ͫ>(s {Q= Ѻ qIW`RgRPR эoe4GMQ«k:iݟ0A@!\mB/ٍEaEFy&]}7 fMxaBw/9_-ƗOy mf*gB'Nj#/rղ&e0`*#bz^K Ib}(ow rH y\RІfҲ0m KTFRx‡vfPw}60he Wn IJaE1Klp*6?K@ݷ4'Bt )DNOnvBrPxizI WEoq3_;edx旒3Yj^NVvޛ:Ra34鴅Ijͮ$M[WX{"7堂W;Ɓ`8X@+eBPbOk֪hKuhYWʦX[0X+u)\ؼ*˭AbRႋ78#~ H19r\jk_eheB[4LD9F(KYnuĽC燹Eu`m?ilpt_vT`L_ ewXAXq$ե>X*v=H ,HU5pXRW\]2SD#BXt)صr*3Gyc_/e]z^a_)A>)1MATi&*-ʋdٞ[< ׅѦ0 3^EIP21|\ Mבw2xܸm j~EcwKu8D \&<%.-2z4(F^ ^"ʽT@Rv!!m9~g;n-YQq+ U:ז&ZY#T;1KE3ZOoGیO,C~~;_^o4D'LJ%ƈ@C1EH_^C߃hN܁i}.b_P5DEKϙA8DYMiZ̪ruX@CIZzKds X.Ȳ1峼m1L-n YE(k+P\Ns` g5* whYfa}Rռ9,  o^m|,CTc4B*۫?@ qC^~+pbA1X[ "98z6w u ט[39:{wB<,_>ŦNx8}3F{oL}:φʳ)| .HA/*%)[Q^gWjZ(Ⳙd6^9dEٶx ?8Pq~nYH54n!7Z/,%O J:vZkl]=einRVJ%^V5I, f)I! [  Y.aC"cuF%+ڥUi4NDcsPG{9*YAv9p"` ~3)$L~tB֒kno0缩-Ear =t Ҹgh n g 2:*Urb#IG'r ,b_;JSP"#PET`5ⸯ)2,/cYc/L3h@P _r3z>5b]XXW֢ڣ`g#RF@Xp ԡanԘ!U@b--Ӧ̀A򈣹c0ѷ@p9WBAF!k)"Få۟rK.K$jŀvVJ#] n"3sD*̠74Ns) 蔭SWd$o <Կ vJn{1Er_Ob 2vCcp{V".yb_[`j0ૂ9fOӦ7h+ǔ{Lr dlu˵>l|&<1;Sa2`+54qBn|XmVë;QjsX @LSVDolJ5}a\=x͐x?.ގa.ʍ^fIR-E* gf`uD0B` ."Oz(K֭P#$雄R.+W>e7]-O-*PY)36\(\J:=(sr*"3p."rTw)6`F1r /Oirso=ATډ)wOhP_ aevPN`՛ٍXچÑ S^, i@j ;ucgDV ,3 Qp=Owq`,s$.42? G"vƍRryʖS]$DWQ0ȞA`Bn{z$Xza[Ь), L{q XE/3F2c ^'g_$շL(fjw nf)lD b#~e}U lgdm2W Eh\ybB[VD.KeoK(X ̝)/hb6Xskx_1Uu&4TbXUwtx&\Lr o9 jɄ"zk j2 4AAZr鵹!#'S08mM׉)ŵUǡ!)nO1-n\5(ܟTG>TZ\czTv 0@W1 @0mRvRtdJܼR@F2"54BA6(gLkBiNfRQݼ*0<,805 g>=2R/m/5Ev7;"79n 32 %5Щ3Qƻ ȿ "WյGU QSN-·.{ʞm.WjN'Ԍ,(ڰ`Q%,.@|B O[igOx,(cQs*8!A@!Z\"ɲd vvM(hM#$=Stϑ׷8cZvF#SOLِb!: U|P*VZb֡O+SK/cPx r {/"ٽc30a,KTBf{|Ӵ3Cee\[a0ivQ0PZl3\ҳFc Pg VJ.`Uyq=xu+46\h+ m{]xϰf@>#v㰖)^V.1I퀘 [XٖuZ)`&l{٪9kGA(Uut@]T+y_*\p=fZS*X~p܅m9m* u3oԷ+m!Ig7L_buRXj}Pϸ's7 b ObK#~b-(b\sLD, dҏtfR/;ui%^qb to!PggkXAScNC}ojHb1:fKF qN?.e? qPi|v" &bKq tf"0b]>#V.QpswMcz,@[~b~Q骸U&6 [!+/}azHe2f-K8q1N֪UNSe.,7pDh>z%w497a*Ulmh"sb k75{bP]Q'Ee5wO+uUDwfljE0[w!,чɘY+[؊A}/~]hj?eAgQ<35V' ;o/,lEe. Q6pY2 X 7*S;.+־ɚAՌO.YV\3^<#;(F奱O~Q򧐝vX׳*1v0# M)FhFfDfY~߷NGi'AKq 8Ɛl}V;B&񕯍"D(Ͳ|BEN އ/t$e[CvdCL MYmƎ.TWp =xүew@c%h̨UJ5@Ҧ)pneYJ0P-"aq`7oi,!bZ+q( y9Y̻q8R e8I46eY"[RsHhز4A…o^p^7ܢrg2ݟ\;.ị% l%B`QyG\F[[\% Ơ ^8DUzUl*D5'1EW y;p56b( :7v~(MmSVn`:VB+l"o-;̸ՋL:D, fw~rF#: ߴbΘ%&ILdJq \4m.qv*ٵcDW,3,ᭅFBh@f y7Vr1-qU@!yWn-ET%hY6S2ĭR(Kl.8n4 'l#ԸF.c>cVhj#( 9v>nS(ޮ+ L>B,6:Uj,LS^@fP֕ y,he*5dȇvQbr#SAu0Z{̿ĭLV(6/zeо%r8r4#-%Rsi̳<_/CY2 >ks28{K"\1l^Ɲ&b|i!X\H`j+,B >a* l׻.pJr@-¥Uo-SXR7[d3b5CXams2}43اDF2𜟨KwEƍ/Z1Nd2n'޻NTd|Mx)?fm][~ɗv% Hߒ Ar‡1iмX4X&ʓA~.Z.GI^v|BUɘ_4Kڲ,!A;( (`Sd+GuWtq rZSW5UCMRPXL1m8|@g8̡tlm,]ʪe.4M1}v2rITĭ9~PH߆qdhFܙ7M@yUanEE1lZZ0Pq@uK^W gWA1gx-S%n}Y" "3;'0h[Co-("@}b_mANvNo࿹_wCINvGL슕h?/v|~ S~?a-XҲ+?0TZFfN/E?r1Jⱏ*{ؽ1.U.0s*xw|ᘤ$~I&]C=K0]'ܴ 9}x2i4Xd<&Ƞ6)|d3c .&N{\UԶ6]V aшPA]&ͭZsSg򷃛2*M%^M&u 7bV@/rQMfDQÇV1q8"Emw+_{&$J+;@4gO̲ ˳߁>D/ &Z;GVV^m)|R5H&6=`~ѨnYdzR`M".Eb]R}EBoo`Z&Jx ߙfj5=E" | !Y3&p ι"$g;˪$D4,5M]HJ&X`6,FTE ]+CYV]7Y]a .lͱE 4#1b3 h],tby bpX%%su[f Qnh&Q|D \HoA;`4؇!7AnKa3guBpL(o}K'Wq F \PK$̬qIN},?07)-iܳq5 m2Y (#k|gXR'?#QBo Հ!m}@.˚Arr%-]Y<ȋz&L&{[ÿ2zLc : 3-:4/}iwPch#|Vh|ƹ5rGwmf:w2RL+QqnY4شuD Vv Wj1򺶧kAR+egb*ཏK÷7@4%1M]%T%VX@$l{fvsօ ^W9~V{%S$<"; #6_=΄w}RAbV*bGSR\c4;Lf%DqKswᘀKsMF;BGt뗣 2*qVfh6_lr+$GDwv_'<{)cy9qpV\Uj=ʙlRZA"Φs_;#Hbe&!%> Pԡ!4xҔ”_DTÚnT E]7W%"휆Tv.ͿE€8)e*]{FexĶedTƝFH[Lbv @tTC|Z\pJaAVjZ;cT*)n_!%ў26Θow&f,o `,Z8+ g S78n+$.L/"k}{ Jah*Q~q7|ef+ł1Ʉ2[$c,eAiZhU}$s,G/ TYj"AbweCB̓=ux]Cl&l|4ZrPX45ah  y5`=;9e 4A ئ`yk&XQc C5M0ԳA!o*!0e68GLb` –/(\U˥%b'f bVDI>*)|2C5q!/4=[LH6%i!abEVjU2D~X/UQz>Pܾs6*bes XF ኹJ>lb8鋯A:'"xS!MT%(WB {DAn7oy\GGZ!d}Bh %A*(QG{j:d n -,$f [Bqm)"Lh UP5X[. ~+ 3׮ oaߕhTL,E"c@U]h,נcKpro2t[&y3gzK}F^$mUj1}#wcU1˿1Ífo'VdzIH[tL)ipN9Z*dآiZeQxã'fgyO] jRq To(Xٖ`2)⩑1cx< ٘z0A H򙋖g*C_A,(7!@0@[0I&уTVtNnWN ٘,.'m_*un֠q-%j6f)R ^{K+UF6T/q>6QVeUq3-D%VW[weoUM7ž#f@UZ 5(/$ei+loqi Ւ֮i5J;a3VŸW*[ZUPH8&;$4%a|lq@&oD8@oo/.R.^,?WIBgeЗ< 򦒺NT=2c!ڲ@?XcK xaV]> Lb}SjI4q9LRan}(:Ȍdy1Q0)뙈c7yn*FQ[``mf5{ &hes3=j]RYwjR)4QT*WiVhSl9bʁXj! fkLJ0;wKnT,hyBk>heWK)7=<Wjp*9 eXj7r|aPzv5vq{tS<@*!E2P ^pA6QIGgYvHKxgf!nw+LDVdWzdw ֗=HK0W^ yYww P1߷UKGmoiYNe-=n9sEJxǖ`97'NG EU8ݙƕe C 1J-pDIh%Z x?'a97}O64@tLR|G&theԔ B("d&a.Hee.q~h [0>FV/x -mt@4Z)dEY[fc?2LSASM )6ALU#$sNPZx9qnŸ4#c 4@wt0_JؓU;18#[ SFVXcyNM;*)*̷KGvA "0Kҭ,J"xӏ(jaYŘ7+\ ɔLQa2ih ;TstTD丘 Gf@{Љ?r zVفQ9n_׆v)wڋXaS|۽],q|E0!:RS= Zd/89^+kb uDWl)v8 Af{4(Yyn̰䯍{ {Z6DkRQ@  C^&jD!6q~ _@zH|FZ ՗0Qq{I|"Pz-AC6L-D)4spq`b0^c Kv,(U|EAnp6xށ\.c ^#(1TeLO%bR >JS 0eAH6ʄiWQ4;eTw}xD7 3;'%s~Ҟhܫx};&2}יnnљP+, qe)O0C men6Zl çDRT2|nʎg~ s~`/QUa_{Nߘ3 ADD] 5w `N AV(Ķi5NۻBO'~mt1(\BPM7uF!md ,rlآ^WF'v52PZ܈~'0ZV6P3,K">6[?b/s;vr^QK ZU;xT^LKg,n4/Ҋ(I9 V wC֠*Zp #O5zn@[`)w*K5͙nBK0k9̍5ey.~>b zqZC|Oٖg[3h |3n|Jpnr>ʝ?+*7VPLMb[Qݍ2,e ^ 7:$Di-I:{ڢRS`ʭݞd?]U/ Y8G+}٪Ke ɮPGI"ɭ `4Ɇn8`wlH3% l=V]$0B#N_c/aDĠS4J3z`WeCEwTb9|o1E/bw"v=q7:i.CCheEŰ('sMr֮4"`d{ogԣj7b kR@b/u0(f&ں7ĺ3wQ4y%,4_I[MKS#' K;#;a(:ebJs~WCIxVa]O7o8 Id +/۰'`nMO1I\> 79{Nu7@IPttn,4qW@UZ" v@{>c)u|MȽWI [N"#V>"AKS!vfzK8Cؗɦ[xBRaa5ŹLs`˖!PÝQK'AwCݧs&j)[Y}ٖ2O1jA%:*eKP^}sƎ* + =Rh(}"!>l Zw>e#Er+,Os|^RF A/-=$g,ݡ~1l40Bd)m'Aߚ1J,.kL>E' c~o o0\d{_$טd'\N f@U@{$X 8{'5?hcn5чJ>e/D{6U;fW9XG}[!q13a@,6W3whdy!fQiw*XEx_XfM A`̢\Y,qzc,}{&vndv˫ۂTļbŃ._E˗J2~^b8e203E@EA@.Y'&_]i< qy|a8z#6HRŜc#4/xPJyy7`1t=UCkaϸZpfDpG?A2$h(A5v67 NUocEUy{m%Mȗg0^<%"Ҹķ0UhUe;ZqqF$n}s9J(tV(a9\;I 7p:1vkYFmf:+w)N٥v/V+Lʿ3ATo&>IH&f誶*\sY;k[Yг1"& `Y{pK|EWe;%V4Z*Tۇ1b/"pA AKja q͌.j9U1[BG8#,d- sZLpt`oiU> )&Y9!.,RwRYPXC"FXlxln9۩3Y< je,e+6"v]۸6V>p#q7M|Z& M'i4xѭExq1PѵʉQ6hN&OeӦA"hd0ea1 mҎZk %j=Wt RR? G"b*Km~hW1/e9|sNnOZy UQV8_ʃQ['y$1,A98`rUX~܍@A,AAsYVjߘ7.i\L˩+e"HBhQh*{֫{/Yj*SM!dQzc:0ˌ9`~^ 56e&HVaX*uor;M*, hYi̻*0^a#ܫDފN: 8!x+k Xc*8>S0{\ Zô6.XP. B`sCZ?(ca K0K 1:ZE[.1,^;&:x*2{%; robTohV+g>zCNbmeawU+K0b8|=g*d4=ĨTRG~bj*) oAi0\EK 6"Tþ"l 4- vN>LA*{%ADXPDwao0Y}r0GȏZfw?Mܰ.iw.( S& 9UPo.sٜKo/><$UFx%071>F6KHq Dz\pC厭w% .*%`x8nұ,݌tTBext.rE.1vS UdA z4\k*LA.h{?IGDt)4XnBlĺvg},UܼKn̽^tgLnxJ9^3l"$aȁ LM/h;ʦz+%l5 ۫( ӵ8 z[.1uW8^ZoKswXKLb/ q"D= xD\b^}VWYL+dV̦h /p/,BDwTzo{!՝o苓f#kf࠱>ABl](ɨ' CfjhYi5aB A/1q )E(o2?Ɗ`{>6"Vdlt֮*x:Q`6dіZĿDW3z]#`4q*O䕝:i@Pr4xw-^+\9yA̴Y9&܉t +U_XPhq J dœ[_:Lo|!գ)kO4/2z0?!wO8o [>(jbT7/GcdGEc Md75!v BF<1,!*"3LtJK%NzBu OYG1bH{Kpeo%o a ʼney06&Lp5 hP^cXU{?1qLeÊ#}0[͑XWT<#+%K\SH0_I$UMҷAt#5<߆t >J=%SG#A>Jt*#m/D0O ހ~п ,W_P#H`.%-ۿL _+%7/*dZgYA~N|,H+3ƩO7~na0aWZZӱ>.6w@ TVm }BGDx%)dV4&0 f,4Khulpb\ӱ 3*Q0iUO([FO;NIED BII0_r1%l%o1v"09.!pkj4XRXpѝ|+6prŘn] n^R# U<!#(8Hiem - [N`J.T҉S" ROhk %8 u >)geQx`.q,qZ"i/e0>UqNJ\x @+rհnahG7yB!(D(̳-X'4f9[6;YP̦t35<]*%[ɇ]@nOD6#Nҽ^0yE^oh0`e;&-@yd(tx)1p'`R^E-5{Ga[_-9TեxL+m8ipV\rXqxE.ձwNupܶ9ycu)فR9:ҨMވj1Yj*!u]$\-l /£y\1i撬=Ls>Qiw`fT0dШimiX&e8TUv"( Eȯ#cpN,z0n2ZwYZP*A oga`@ Q/i -@蝪FZDM©_0 :\ΦuM\ E6V8ք GB@l|ah+Ȱ*Q'lE^fop] sYsnarX" CR%8h 䔥neIp3F0?0 hyv!^fji4eNK[x8Gj f%af/?c/ZK mW?K9A]!fvq-{h4(Q @+ZG嗟v\*,fEl^&8q[LP"rlj4 XZ;B"US l>).&. \P{)pfx2>/¬r/xx/F%Q_L|?!Fc|U_ n](N|B#P86Y蛁#agAEA18A)QxJGr.0T3W*̣]땊)*W IV]w"]Y .VS.9yT(հe3[ķc wTQ!|Nڌ\ZKxE :'v"ۊkUE3F\ g'uk,Q{45JᎾp*4J7rCe./5.a–-OlNF#+Ǹ*Ih+4{ 1%zhS<˛--,[9=S1q12˷`Hsc0S\ U}Y>X.Q)(7+5)Jw3eRkAv`s[@8weg\@5 -L@ o /1pV{Vrhde &Z b{.uu(Ec 72ؖdHg&4S-Ūyfd܊hhrܸw !ouZюoU2;`ٕX+rJRZ=Մُh6\@vąMψ:C0qIP"X9 5~.YT#DS_4\ :|8"OEs; Z*ٰ(G6gBPKߐ|{FdP3JI^ OxfeMܮG@oQ?vnR (cH ؐXm@~@Ǡ¶!EL7=VPUwx `/~1ZsL+F^* (t$SDKXwQ!aPM zŗvpj%=!h8%2FfzEiYBzO &bt hUñ#\+*d/+?nPap!N@F.ɖ{ a(%Ǚ B\b&t}gii[lG&6h<D;~.LӔ|dQ,;X4^kQl̉@5sXv0\#c!(mjYJB<76ŵ1U|. ZKffx3HD9) Icc.#H1e) ֡ܬ$]|O!x3r6c.m&88Hܗ+Dj@о&{0EQJiT8E QM/&g[oFO.[(jX$w$ݰ9:XMҸ!n%U-A@ ƒ6@~ |1H06T^ҎKGh ʦx~ݡ(]Z-Vbfod0'ZWlsC*4)8ahP#I%D\m=4 +>VM'ZHUFKwy](BQk - .2ϔT axUvz.YWX9ʨsacnC@385)_#ꇡNB0]Aġ,=&40 `'qolø ~+ R-]Q*OG4)!⢓B`= JwSU}پ,Xbcn\Me7'VL)bōm!>,ߊ_TA99P)Uce\STF=KpA FB,Xk[.qfgҟqݸjj: >b2m& [;z,0a;b D?3xhX7bʗ%p4{;#._15߷.T;oQq"'kaP^HװE#]F]V=t[)l=%e 3qnz/4; Uh JtFĺS[fV*,XF3Z@Rk4!ayr?˨9.ha a Jހb7<_اH ,uQ2y ;֭C\wz!106,F{y!ώP7D*w+bzIrOܵqK}6dDLO %,Sz')a SK2+%Ĥ7 *%5AЇ k EP8hdR[8-5Ƃ#d:dإJ卛{:[s{O":(d+ R]d] %Up7(EGs]#dǁ%7B›Hdp/w_ kjnY N1򂶷xH׶0 ɿy.W^y px{>!֤mmzT.wuM©!r xw^ݒKUT.5V0Ʌ aq w%Fyfj 7mK Kt'3UL.5,ɿ~mbĜMͳNp=1+f5Chbw\LȆ%$RRM#..9R炇?E!Db(;ɛٹ+x~ѱ&f#׉IND40JD[|/ /N V5T>%4k&]YcnR\,2p+QcK2<nW,̡RȬI6ylu$ |DmfK)-МSeA_æi;?iq&Vjterxc]q,YP\K`'Ld"&l~N +9uCaRmލR&oFpTFW١Z`]۟$uen"e~MYu}@{PmB\:B5( SDgR..)soc`"fy)^P!P:f3%`^J1jwHQ@*a _snpl>eCT S+'{.V U(1PXl@2AZ-|9yiW #O %w3k6afjJ;cM0yJ>FlQAPVS-賠86ja@$Z7eKf:?!k * xB cs̽UJ%+d|M,@ 8{scmÈW-65 KufrԸ K"(!^y?FeW@`lf~zYSg[T3f7@nez]C.0@]r,vX*w2 fj+tUC1^x{Ǹb;OB qG,ޣDEzj-`&lwءy ?gAĴ Cp゠#l 7g1{XU0.m7;0U\.RβUU0 [[Fp\"9@QqAlLveL兘-a+X ~O$Kdǖ:n!WhL؈-֏ܻ/\qR͌ÌZ8}I-jƘ`eK07!% /UɚnT",o(p~P-г&.~cJ#"@&_, ̂°ަe FT 9L o2p 2P+d`hg{vo_1-~cO1TOw K,L栂ޕ&D@u.٬ƊU.}ȚV4dU[pJ|ҖU dX¨̧Yȷ-UhDvѳaR0Y%obQT^pGȈ-K(Jdv -` ûw+D^ NϕR!M_IWGRЂ{ M%jNkFR[;G3 PwZ +/aEu>E#dyVf xXov7$%)ڹG)0zj#AjY mxFū D.2#"vp/p6=s(ܮQ悆X`m7XJȇgq`Ӄmka ̶M)RB`[ &-%kTӪ10q6 0z+#+f#A|$o%C?؏TWLXx؎TY6|Ԫa0@HH0Td ZVeArQ|m"4 >me,5; +DB(lR[̯$ P )BCD\`Mwa97b(&3h[tԁeQx(\$b#Bs 095BEуnWS8)|D=tЕPKh]-MXP2f#Rp:V's0vx<&=uiو# mm  s7M16` KdV(`ef<^ҸL[\"]1ޠ|0<^UycG$9?bˈXrbjhx @RsWKޭ6ЃW,pWf|j KU;\Y^a! M(.Xc*86 er+U'F_<]=q\30C Ҍ9.,Bs 2R͌J4 ǵHÃFYPGÌ3^̗`z3n l I4m̃v7Ohdr5* E& 5`_Mie3%hɉp1Z(k@*E'^<+uɉ]pCL[Y%TlDUt ;V']kp0]& \D(:&iu瘿Hmy/%^& ei-V,))> Z?e[4!ݓb)" W{+eӕ5y2,w,W9zK/B{f­|6 F&p+z{P鵗Rl<3{z*Ux+qa|"ysBu@zYD,9^K5K}P-艔K>f躋y/#2Gdqn` ^ )Kaǔ) \WB-90 ys2[Lfc|H*̭@IK95!t'4L2EM!21 kj92 @L̕ͅv0ӄ11|UpQ79En`1,g'yx (K k!0ґ:ROҞ{Ѐ0md=jEdۮ1 B%j Utn]^cmԤL5,‹cl5Z muzZ wK7uX 1mBO+k0q6L7V` sNa=HC:a4BÁCݿeͿ)y-4F@ H9Y,ړ!|  ,ض\j`(Ja}NNQ t,@q]EBPF.ES-ҡFF4{1-GSIjfuQz|3#3j.Fc8z1hGst옩(.UU 3xc v%,&UxC +B6=ܾOd802›KnnAo}XgF͢cϽ/N?1R>-+u^ ȍxUnC`UQvCw;_pT-0|~ 85[r>V7Jm{>(Ÿ%DSfŨ~ Dq$@ ˂pG8eU&6 xcbC6Y%%%0p!mh+G@a-P2-=ڵ+Mvek,H!*gy3ʀ2r% 3^B6(wP= 4)G4TZEH3V<[A@#!@>)vxa'/cmJ(Pa8^c7}" ʯ+9[/P\@@#PV]QmObC)`ܡ'}PŏiQ'6;h4kY%̳1"![18eq`áYi.D>yFg.Z?aIu`}B_yeVA 7UKA9ؠΎuN.a! *iAM-*F LiĬvܵ~vm>NH`KeSFY3B*cN2F#sw˖䬹AeYZ R\KX{׬v$.k ʿ3Ef֦'W=0T_Kck^n-ZB0\RJ ReA%u5P ɫ ߃ kɹ_l m p,edgS{m{s ,hzki͡y-7͑ m+=)xSp{gG`XJcU9fhbL@4. ng\fgţl~&,&6E-n,&B LejQ Fy4TZ6_RR`t^5 4i棣}9鰘"O,f?LYA˜y]) ]+0gQBI FqU/]ыx0zL)%P3Ig̼.) 12W%\j~JٚK mZal٧a @jWC/71 L+eU,<L5dR,69ơ+Zn? rȌ.+)2CAR#;25< @;=1O67:Y"~5I%]8UċBk0ǥv&Wj8PXJ,68HY;U~SGJ~dꩤyEL֒T/yd ŰǴh18\Rf$Ӝ~V 4l֌*6é<}"u(c xavf ۹:%% %؏sf1#1!oZ_8\`T֮Xg&lC`ULP9MLFt}lk\@+Mʈ1x(K3b-&Ҕj6@`!QV.Xl4>Ax啇6URLD6r7KkԲM/u TDA/k6@Q5QY a*6Xg +nԠ">pK3Q.8qMl.*/G0O`jyߋF[W.<\Tqh8MHY#7]ŀi4"a;qG Nw;%F6FXSWuf1_ht+X9IzJNjS`H+meyb7 1^I_ Zns/ ;-2 Jlo.30.w=_5R(Wph袹L:/1_k6h-# &4@s?PF0R(s'"8NLq5TSHMʱV ᴄ Zv%8jxQ*E,(9EjhةH m7x'.L@lpZ`\Qa,Y,65k6g^ˤe`K>'.Vrfq LJ М6isV*}0Q;Eo)W̒jзm wJ-0%w?A tj7S$q^if.LF  Ma!0ۺOИtèXZ$)g v}m? *>e\Ee>3V>PU߂)v0]ַ0#L9WٌTBq[m_|{㢚Jȵ T[e 8VܬjR^ TF4 hdlcdp b[j^^%L7pzfQ>udAc~tPc3TA¯n{Q2q~ 9VK:T@aL/pL0,L:eo!* Jh.jR󽌤n@>%%%TA.23<Ŏ Vn?fHm1dSLüe^Ⱥ+p2(cظ4myaaZh^:CI䀛unr4$6֌;FXuBzc?3FǵyHr=6?*=Vd'## =1 JeB[oJ(Pji&ܥ[dFTyDP!:S:[Hp@t!?drߕJnʌÍ1=Qh'Q>VXj":ˇTXd@HfX Q7+!~:'J1Fd̞` )hӥ%* )fO  ,%j\-~Fzad3/KYEky'.˦4%\/.<U*4)enlX..>hip&Y@ɔ+TUƌqTńܭm xauyn8ͽ՚fƽRiO7cN <N\O*R\W剪6S^;p+G33FEb{YXU|ehG<" v(T¥1J]!w"81FjUN#-;́?U 2Ytw&#)` $I.@4\JhʔpVXN&Ռ!~ C MC}+
Linux 4gvps.4gvps.com 3.10.0-1127.18.2.vz7.163.46 #1 SMP Fri Nov 20 21:47:55 MSK 2020 x86_64
  SOFT : Apache PHP : 7.4.33
/scripts/
38.135.39.45

 
[ NAME ] [ SIZE ] [ PERM ] [ DATE ] [ ACT ]
+FILE +DIR
cpan_sandbox dir drwxr-xr-x 2016-07-29 13:50 R D
php_sandbox dir drwxr-xr-x 2019-11-27 01:18 R D
MirrorSearch_pingtest 2.38 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
activesync-invite-reply 1.693 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
add_dns 2.361 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
adddns 2.361 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
addpop 6.082 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
addsystemuser 3.267 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
adduser 0.09 KB -rwxr-xr-x 2016-07-29 13:50 R E G D
apachelimits 4.307 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
archive_sync_zones 3.023 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
auto-adjust-mysql-limits 1.811 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
autorepair 1.244 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
backups_clean_metadata_for_missing_backups 1.574 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
backups_create_metadata 15.748 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
backups_list_user_files 4.562 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
balance_linked_node_quotas 2.581 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
biglogcheck 1.688 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
build_bandwidthdb_root_cache_in_background 1.524 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
build_cpnat 3.412 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
build_mail_sni 3.873 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
build_maxemails_config 1.142 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
builddovecotconf 6.76 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
buildeximconf 6.999 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
buildhttpdconf 2.602 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
buildnsdconf 1.007 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
buildpureftproot 0.526 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ccs-check 4.913 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_cpanel_pkgs 10.749 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_cpanel_rpms 0.213 KB -rwxr-xr-x 2021-11-09 01:17 R E G D
check_domain_tls_service_domains.pl 6.681 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_immutable_files 5.489 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_mail_spamassassin_compiledregexps_body_0 0.183 KB -rwxr-xr-x 2016-07-29 13:50 R E G D
check_maxmem_against_domains_count 3.566 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_mount_procfs 2.023 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_mysql 5.551 KB -rwxr-xr-x 2022-09-20 01:15 R E G D
check_security_advice_changes 8.278 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
check_unmonitored_enabled_services 4.557 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
check_unreliable_resolvers 3.586 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_users_my_cnf 6.046 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
check_valid_server_hostname 7.656 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
checkalldomainsmxs 2.404 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checkbashshell 1.177 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checkccompiler 1.224 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checkexim.pl 3.098 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checklink 1.292 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checknsddirs 0.99 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
checkusers 0.836 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
chkmydns 0.548 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
chkpaths 0.138 KB -rwxr-xr-x 2016-07-29 13:50 R E G D
chpass 0.406 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ckillall 1.112 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
clean_dead_mailman_locks 2.091 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
clean_up_temp_wheel_users 2.439 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
clean_user_php_sessions 4.761 KB -rwxr-xr-x 2022-08-25 01:16 R E G D
cleandns 13.094 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
cleandns8 0.407 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cleanmsglog 0.718 KB -rwxr-xr-x 2016-07-29 13:50 R E G D
cleanphpsessions 0.91 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cleanphpsessions.php 0.643 KB -rw-r--r-- 2022-08-16 01:16 R E G D
cleanquotas 1.612 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cleansessions 5.908 KB -rwxr-xr-x 2023-04-13 01:15 R E G D
cleanupinterchange 2.643 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cleanupmysqlprivs 0.521 KB -rwxr-xr-x 2021-11-09 01:17 R E G D
clear_cpaddon_ui_caches 1.271 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
clear_orphaned_virtfs_mounts 3.56 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
comparecdb 1.524 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
compilers 2.863 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
compilerscheck 0.976 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
configure_firewall_for_cpanel 0.508 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
configure_rh_firewall_for_cpanel 0.508 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
configure_rh_ipv6_firewall_for_cpanel 0.508 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert2dovecot 0.666 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_accesshash_to_token 4.073 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_and_migrate_from_legacy_backup 1.97 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_maildir_to_mdbox 1.663 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_mdbox_to_maildir 1.658 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_roundcube_mysql2sqlite 25.282 KB -rwxr-xr-x 2024-11-05 01:15 R E G D
convert_to_dovecot_delivery 4.334 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
convert_whmxfer_to_sqlite 1.464 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
copy_user_mail_as_root 1.251 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
copy_user_mail_as_user 1.343 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpaddonsup 3.246 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpan_config 2.803 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpanel_initial_install 67.363 KB -rwxr-xr-x 2024-04-16 01:15 R E G D
cpanelsync 28.312 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpanelsync_postprocessor 1.618 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpanpingtest 0.942 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpbackup 44.767 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
cpbackup_transport_file 5.646 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
cpdig 1.81 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpfetch 1.229 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cphulkdblacklist 0.423 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cphulkdwhitelist 1.305 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpservice 2.865 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpuser_port_authority 19.292 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
cpuser_service_manager 10.853 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
createacct 24.57 MB -rwx------ 2024-08-07 01:15 R E G D
custom_backup_destination.pl.sample 5.061 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
custom_backup_destination.pl.skeleton 2.838 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dav_change_hostname 3.566 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dcpumon-wrapper 0.83 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
delpop 6.201 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
detect_env_capabilities 0.496 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
disable_prelink 2.774 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
disable_sqloptimizer 1.488 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
disablefileprotect 2.087 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
distro_changed_hook 1.157 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dnscluster 4.439 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dnsqueuecron 1.285 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dnssec-cluster-keys 3.75 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dovecot_maintenance 7.933 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dovecot_set_defaults.pl 0.961 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dumpcdb 0.846 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dumpinodes 0.671 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dumpquotas 0.602 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
dumpstor 0.892 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ea4_fresh_install 2.636 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
edit_cpanelsync_exclude_list 2.579 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
editquota 3.436 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
elevate-cpanel 377.866 KB -rwx------ 2025-06-12 01:25 R E G D
email_archive_maintenance 6.152 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
email_hold_maintenance 1.46 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
enable_spf_dkim_globally 8.827 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
enable_sqloptimizer 1.571 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
enablefileprotect 2.099 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_autoenabled_features 2.5 MB -rwx------ 2024-06-12 01:15 R E G D
ensure_conf_dir_crt_key 4.824 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_cpuser_file_ip 2.549 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_crontab_permissions 1.075 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_dovecot_memory_limits_meet_minimum 3.133 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_hostname_resolves 2.566 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
ensure_includes 0.587 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ensure_vhost_includes 13.526 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
exim_tidydb 2.965 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
eximconfgen 1.318 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
eximstats_spam_check 0.847 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
export_horde_calendars_to_ics 15.069 KB -rwxr-xr-x 2023-08-01 01:15 R E G D
export_horde_contacts_to_vcf 13.944 KB -rwxr-xr-x 2023-05-02 01:15 R E G D
exportmydnsdb 3.469 KB -rwxr-xr-x 2022-04-12 01:15 R E G D
expunge_expired_certificates_from_sslstorage 3.563 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
expunge_expired_pkgacct_sessions 0.832 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
expunge_expired_transfer_sessions 1.063 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fastmail 5.157 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
featuremod 1.924 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fetchfile 0.412 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
find_and_fix_rpm_issues 6.988 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
find_outdated_services 5.408 KB -rwxr-xr-x 2023-05-31 01:15 R E G D
find_pids_with_inotify_watch_on_path 3.657 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix-cpanel-perl 28.824 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
fix-listen-on-localhost 3.52 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix-web-vhost-configuration 6.148 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix_addon_permissions 7.68 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix_dns_zone_ttls 1.337 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix_innodb_tables 4.052 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix_pear_registry 4.073 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fix_reseller_acls 10.883 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixetchosts 4.32 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixheaders 0.559 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixmailinglistperms 0.984 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixmailman 2.094 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixnamedviews 1.218 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixndc 0.403 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixquotas 17.454 KB -rwxr-xr-x 2023-04-13 01:15 R E G D
fixrelayd 1.742 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixrndc 16.481 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixtar 0.491 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixtlsversions 4.703 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixvaliases 1.999 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
fixwebalizer 0.943 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
forcelocaldomain 0.874 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ftpfetch 2.198 KB -rwxr-xr-x 2019-11-27 01:18 R E G D
ftpquotacheck 8.312 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ftpsfetch 2.359 KB -rwxr-xr-x 2019-11-27 01:18 R E G D
ftpupdate 0.255 KB -rwxr-xr-x 2020-06-15 01:16 R E G D
gather_update_log_stats 4.252 KB -rwx------ 2025-06-10 14:31 R E G D
gather_update_logs_setupcrontab 5.451 KB -rwx------ 2025-06-10 14:31 R E G D
gemwrapper 1.741 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
gencrt 6.26 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
generate_account_suspension_include 5.703 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
generate_google_drive_credentials 1.108 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
generate_google_drive_oauth_uri 0.961 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
generate_maildirsize 13.938 KB -rwxr-xr-x 2023-02-20 01:15 R E G D
gensysinfo 1.157 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
get_locale_from_legacy_name_info 1.993 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
getremotecpmove 12.674 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
grpck 1.189 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
hackcheck 3.02 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
hook 1.452 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
httpspamdetect 2.66 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
hulk-unban-ip 3.93 MB -rwx------ 2024-07-02 01:15 R E G D
import_exim_data 8.392 KB -rwxr-xr-x 2022-09-20 01:15 R E G D
importmydnsdb 11.338 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
increase_filesystem_limits 0.87 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
initacls 4.987 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
initfpsuexec 0.434 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
initquotas 19.475 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
initsuexec 4.026 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
install_cpanel_analytics 1.927 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
install_dovecot_fts 1.567 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
install_plugin 2.802 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
installpkg 0.562 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
installpostgres 6.557 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
installsqlite3 1.822 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ipcheck 3.926 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ipusage 7.445 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
isdedicatedip 0.588 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
jetbackup-check 3.688 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killdns 0.412 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killdns-dnsadmin 1.152 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killmysqluserprivs 0.423 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killmysqlwildcard 1.152 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killpvhost 0.833 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
killspamkeys 0.915 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
link_3rdparty_binaries 1.241 KB -rwxr-xr-x 2022-06-28 01:17 R E G D
linksubemailtomainacct 3.172 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
listcheck 0.525 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
listsubdomains 1.049 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
litespeed-check 3.859 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
locale_export 4.819 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
locale_import 4.349 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
locale_info 3.99 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
logo.dat 0.2 KB -rw-r--r-- 2016-07-29 13:50 R E G D
magicloader 1.938 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
maildir_converter 6.076 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mailperm 16.575 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mailscannerupdate 2.42 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mainipcheck 9.996 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
maintenance 46.618 KB -rwxr-xr-x 2024-07-02 01:15 R E G D
make_config 0.397 KB -rw-r--r-- 2016-07-29 13:50 R E G D
make_hostname_unowned 1.161 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
manage_extra_marketing 12.414 KB -rwx------ 2025-05-28 19:42 R E G D
manage_greylisting 16.188 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
manage_mysql_profiles 20.018 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
migrate-pdns-conf 9.826 KB -rwxr-xr-x 2024-09-19 01:15 R E G D
migrate_legacy_wordpress_to_modern_wordpress 12.985 KB -rwx------ 2021-02-08 01:09 R E G D
migrate_local_ini_to_php_ini 7.409 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
migrate_whmtheme_file_to_userdata 2.954 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mkwwwacctconf 2.329 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
modify_accounts 4.086 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
modify_featurelist 9.199 KB -rwx------ 2025-05-28 19:42 R E G D
modify_packages 3.647 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
modsec_vendor 15.633 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mysqlconnectioncheck 6.547 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
mysqlpasswd 4.092 KB -rwxr-xr-x 2022-04-12 01:15 R E G D
named.ca 1.565 KB -rw-r--r-- 2016-07-29 13:50 R E G D
named.rfc1912.zones 0.756 KB -rw-r--r-- 2017-11-07 01:18 R E G D
nixstatsagent.sh 58.675 KB -rwx------ 2022-07-06 14:06 R E G D
notify_expiring_certificates 9.367 KB -rwxr-xr-x 2022-06-28 01:17 R E G D
notify_expiring_certificates_on_linked_nodes 1.329 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
oopscheck 1.115 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
optimize_eximstats 3.882 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
patch_mail_spamassassin_compiledregexps_body_0 2.395 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
patchfdsetsize 2.719 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
pedquota 2.256 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
perform_sqlite_auto_rebuild_db_maintenance 2.199 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
perlinstaller 0.516 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
perlmods 1.176 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
php_fpm_config 9.734 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
phpini_tidy 0.671 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
pkgacct 87.819 KB -rwxr-xr-x 2023-04-13 01:15 R E G D
post_snapshot 3.156 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
post_sync_cleanup 6.091 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
primary_virtual_host_migration 2.443 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
process_pending_cpanel_php_pear_registration 3.491 KB -rwxr-xr-x 2022-04-12 01:15 R E G D
process_site_templates 7.271 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
proxydomains 9.344 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ptycheck 0.707 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
purge_modsec_log 1.526 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
purge_old_config_caches 2.075 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
pwck 0.691 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
quickdnslookup 1.132 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
quickwhoisips 2.293 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
quota_auto_fix 1.406 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
quotacheck 22.363 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
rawchpass 0.449 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rdate 4.798 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
realadduser 5.608 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
realchpass 3.258 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
realperlinstaller 5.669 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
realrawchpass 0.415 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_available_addons_packages_cache 1.271 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_available_rpm_addons_cache 1.271 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_bandwidthdb_root_cache 1.452 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_dbmap 5.798 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_provider_openid_connect_links_db 1.015 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuild_whm_chrome 2.224 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuilddnsconfig 26.213 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuildhttpdconf 2.602 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuildinstalledssldb 2.849 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuildippool 0.497 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuildnsdzones 1.137 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rebuilduserssldb 0.926 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
refresh-dkim-validity-cache 5.967 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
regenerate_tokens 2.176 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
reloadnsd 0.802 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
remote_log_transfer 11.597 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
remove_dovecot_index_files 5.887 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
removeacct 20.91 MB -rwx------ 2024-10-25 01:15 R E G D
rescan_user_dovecot_fts 2.977 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
reset_mail_quotas_to_sane_values 6.818 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
resetmailmanurls 2.028 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
resetquotas 4.677 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv 3.232 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_apache 0.412 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_apache_php_fpm 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_base 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_bind 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_chkservd 0.417 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_clamd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpanalyticsd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpanel_dovecot_solr 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpanel_php_fpm 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpanellogd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpdavd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpgreylistd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cphulkd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpipv6 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_cpsrvd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_crond 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_dnsadmin 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_dovecot 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_exim 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_eximstats 0.492 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_ftpd 0.416 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_ftpserver 0.89 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_httpd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_imap 0.427 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_inetd 2.466 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
restartsrv_ipaliases 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_lmtp 0.427 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_mailman 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_mydns 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_mysql 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_named 0.759 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_nscd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_nsd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_p0f 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_pdns 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_pop3 0.427 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_postgres 0.417 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_postgresql 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_powerdns 0.432 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_proftpd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_pureftpd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_queueprocd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_rsyslog 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_rsyslogd 0.427 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restartsrv_spamd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_sshd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_syslogd 2.4 KB -rwxr-xr-x 2022-06-28 01:17 R E G D
restartsrv_tailwatchd 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_unknown 9.9 MB -rwxr-xr-x 2024-08-07 01:15 R E G D
restartsrv_xinetd 0.412 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restorecpuserfromcache 1.961 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
restorepkg 36.49 MB -rwx------ 2024-10-03 01:15 R E G D
rfc1912_zones.tar 10 KB -rw-r--r-- 2016-07-29 13:50 R E G D
rpmup 4.773 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
rsync-user-homedir.pl 5.765 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
run_if_exists 0.5 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
run_plugin_lifecycle 3.531 KB -rwx------ 2025-05-28 19:42 R E G D
runstatsonce 0.43 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
runweblogs 1.021 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
sa-update_wrapper 3.338 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
safetybits.pl 0.824 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
secureit 4.721 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
securemysql 4.542 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
securerailsapps 3.575 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
securetmp 15.987 KB -rwxr-xr-x 2023-08-01 01:15 R E G D
sendicq 0.463 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
servicedomains 9.344 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
set_mailman_archive_perms 1.754 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
set_php_memory_limits 3.67 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
setpostgresconfig 6.036 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
setup_greylist_db 16.188 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
setup_modsec_db 1.304 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
setup_systemd_timer_for_plugins 3.921 KB -rwx------ 2025-05-28 19:42 R E G D
setupftpserver 10.475 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
setupmailserver 9.547 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
setupnameserver 13.748 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
shrink_modsec_ip_database 12.974 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
simpleps 3.051 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
slurp_exim_mainlog 5.775 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
smartcheck 15.129 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
smtpmailgidonly 8.15 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
snapshot_prep 5.876 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
spamassassin_dbm_cleaner 5.853 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
spamassassindisable 3.74 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
spamboxdisable 2.27 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
sshcontrol 14.377 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
ssl_crt_status 3.836 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
suspendacct 18.006 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
suspendmysqlusers 4.422 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
swapip 3.822 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
sync-mysql-users-from-grants 1.196 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
sync_child_accounts 1.771 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
sync_contact_emails_to_cpanel_users_files 1.136 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
synccpaddonswithsqlhost 6.595 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
synctransfers 1.925 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
syslog_check 1.358 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
sysup 0.63 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
test_sa_compiled 1.067 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
transfer_account_as_user 2.342 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
transfer_accounts_as_root 4.756 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
transfer_in_progress 3.082 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
transfer_in_progress.pod 0.305 KB -rw-r--r-- 2017-04-01 05:43 R E G D
transfermysqlusers 9.53 MB -rwx------ 2024-07-02 01:15 R E G D
try-later 7.949 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
unblockip 0.651 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
uninstall_cpanel_analytics 1.201 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
uninstall_dovecot_fts 0.549 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
uninstall_plugin 2.839 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
unlink_service_account 2.619 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
unpkgacct 4.603 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
unslavenamedconf 0.843 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
unsuspendacct 17.803 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
unsuspendmysqlusers 6.712 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
upcp 31.558 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
upcp-running 2.703 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
upcp.static 708.597 KB -rwxr-xr-x 2024-12-11 01:15 R E G D
update-packages 4.773 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_apachectl 0.469 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_db_cache 0.42 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_dkim_keys 1.45 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_exim_rejects 1.213 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_existing_mail_quotas_for_account 4.776 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_known_proxy_ips 0.979 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_local_rpm_versions 4.56 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
update_mailman_cache 8.345 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_mysql_systemd_config 1.25 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_neighbor_netblocks 0.476 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_sa_config 2.145 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_spamassassin_config 10.73 KB -rwxr-xr-x 2023-02-20 01:16 R E G D
update_users_jail 0.675 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
update_users_vhosts 0.782 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updatedomainips 0.591 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updatenameserverips 1.656 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updatenow 5.178 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
updatenow.static 1.91 MB -rwxr-xr-x 2024-12-11 01:15 R E G D
updatesigningkey 1.949 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updatessldomains 1.813 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updatesupportauthorizations 2.492 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updateuserdatacache 2.47 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
updateuserdomains 0.756 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
upgrade_bandwidth_dbs 2.219 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
upgrade_subaccount_databases 2.731 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
userdata_wildcard_cleanup 5.739 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
userdirctl 5.014 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
validate_sshkey_passphrase 1.215 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
verify_api_spec_files 0.739 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
verify_pidfile 1.961 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
verify_vhost_includes 7.341 KB -rwxr-xr-x 2022-09-20 01:16 R E G D
vps_optimizer 7.819 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
vzzo-fixer 0.708 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
whmlogin 2.334 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
whoowns 1.128 KB -rwxr-xr-x 2017-04-01 05:30 R E G D
wpt_license 6.27 MB -rwx------ 2024-08-07 01:15 R E G D
wwwacct 24.57 MB -rwx------ 2024-08-07 01:15 R E G D
wwwacct2 0.086 KB -rwxr-xr-x 2016-07-29 13:50 R E G D
xfer_rcube_schema_migrate.pl 2.402 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
xfer_rcube_uid_resolver.pl 1.803 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
xferpoint 3.126 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
xfertool 16.141 KB -rwxr-xr-x 2023-04-13 01:16 R E G D
zoneexists 0.781 KB -rwxr-xr-x 2022-04-12 01:16 R E G D
REQUEST EXIT
#!/usr/local/cpanel/3rdparty/bin/perl BEGIN { # Suppress load of all of these at earliest point. $INC{'HTTP/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Try/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'File/Path/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'cPstrict.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/SysPerlBootstrap.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/ExceptionMessage.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Fallback.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/ExceptionMessage/Raw.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/LoadModule/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/ScalarUtil.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Exception/CORE.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/LoadModule.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Linux.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rhel.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Almalinux.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rhel6.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rhel7.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rhel8.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Almalinux8.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Centos.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Centos7.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Cloudlinux.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Cloudlinux6.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Cloudlinux7.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Cloudlinux8.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Ubuntu.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Ubuntu20.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rocky.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/Rocky8.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OS/All.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/TimeHiRes.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Struct/Common/Time.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Struct/timespec.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/NanoStat.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/NanoUtime.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/HiRes.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Env.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Autodie.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Fcntl/Constants.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Fcntl.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/Touch.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/TouchFileBase.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Update/IsCron.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Time/Local.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/Open.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Parser/Vars.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Encoder/Tiny/Rare.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Encoder/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Regex.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Carp.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Set.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeFileLock.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FHUtils/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Hash.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeFile/LockInfoCache.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeFile/LockWatcher.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Context.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Pack.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Syscall.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Inotify.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeFile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Linux/Constants.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Validate/FilesystemNodeName.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Notify.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Server/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Logger.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Debug.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeDir/MK.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FHUtils/Autoflush.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Update/Logger.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/TouchFile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/LoadFile/ReadFast.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/LoadFile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Usage.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/UPID.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Unix/PID/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/JSON/Unicode.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Encoder/ASCII.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/UTF8/Strict.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/JSON.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/JSON/FailOK.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/ConfigFiles.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Destruct.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Finally.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Readlink.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/Write.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/Write/JSON/Lazy.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/I18N/LangTags.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/I18N/LangTags/Detect.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locale/Maketext.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Normalize.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locales/Legacy.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locales/DB/LocaleDisplayPattern/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locales/DB/CharacterOrientation/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locales/Compile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locales.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/CPAN/Locale/Maketext/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Paths.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/DB/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/AdminBin/Serializer.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/AdminBin/Serializer/FailOK.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/Constants.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Imports.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SSL/KeyTypeLabel.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SSL/DefaultKey/Constants.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpUser/Defaults.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Hash/JSONable.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpUser/Object.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SV.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Path/Normalize.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Hash/Stringify.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Umask.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadConfig.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadWwwAcctConf.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Conf.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadCpUserFile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/HasCpUserFile.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/NSCD/Constants.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Socket/UNIX/Micro.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/NSCD/Check.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwCache/Helpers.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwCache/Cache.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwCache/Find.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwCache/Build.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwCache.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/User.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Cookies.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeDir/Read.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/ArrayFunc/Uniq.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Charmap.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/StringFunc/Case.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Legacy.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadCpUserFile/CurrentUser.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/YAML/Syck.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/PwUtils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/AccessIds/Normalize.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/AccessIds/Utils.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/AccessIds/ReducedPrivileges.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/DataStore.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/StringFunc/Trim.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/3rdparty.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/JS/Variations.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Display.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/Api1.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/FileUtils/Lines.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeRun/Simple.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeRun/Errors.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Timezones.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/DateTime.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Time/ISO.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadUserDomains/Count.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Server/Type.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadUserDomains.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpUser.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/FlushConfig.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpUser/Write.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/LinkedNode/Worker/Storage.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/SafeFile/Replace.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpUserGuard.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale/Utils/User/Modify.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Version/Tiny.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Version/Full.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Version/Compare.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Version.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Locale.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Sys/Uname.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Sys/Hostname/Fallback.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Sys/Hostname.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Hostname.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpConfGuard/CORE.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/CpConfGuard.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Config/LoadCpConf.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Maxmem.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/OSSys/Bits.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Sys/Rlimit.pm'} = '/usr/local/cpanel/scripts/upcp.static'; $INC{'Cpanel/Rlimit.pm'} = '/usr/local/cpanel/scripts/upcp.static'; } { # --- BEGIN File::Path::Tiny package File::Path::Tiny; use strict; use warnings; use Cwd qw(cwd chdir); use Carp (); $File::Path::Tiny::VERSION = "1.0"; sub mk { my ( $path, $mask ) = @_; return 2 if -d $path; if ( -e $path ) { $! = 20; return; } $mask ||= '0777'; # Perl::Critic == Integer with leading zeros at ... $mask = oct($mask) if substr( $mask, 0, 1 ) eq '0'; require File::Spec; my ( $vol, $directories ) = File::Spec->splitpath( $path, 1 ); my @dirs = File::Spec->splitdir($directories); my @list; while ( my ($_dir) = shift @dirs ) { last if not defined $_dir; push @list, $_dir; next if ( $_dir eq '' ); my $progressive = File::Spec->catpath( $vol, File::Spec->catdir(@list), '' ); if ( !-d $progressive ) { mkdir( $progressive, $mask ) or -d $progressive or return; } } return 1 if -d $path; return; } sub rm { my ( $path, $fast ) = @_; my ( $orig_dev, $orig_ino ) = ( lstat $path )[ 0, 1 ]; if ( -e _ && !-d _ ) { $! = 20; return; } return 2 if !-d _; empty_dir( $path, $fast ) or return; _bail_if_changed( $path, $orig_dev, $orig_ino ); rmdir($path) or !-e $path or return; return 1; } sub empty_dir { my ( $path, $fast ) = @_; my ( $orig_dev, $orig_ino ) = ( lstat $path )[ 0, 1 ]; if ( -e _ && !-d _ ) { $! = 20; return; } my ( $starting_point, $starting_dev, $starting_ino ); if ( !$fast ) { $starting_point = cwd(); ( $starting_dev, $starting_ino ) = ( lstat $starting_point )[ 0, 1 ]; chdir($path) or Carp::croak("Failed to change directory to “$path”: $!"); $path = '.'; _bail_if_changed( $path, $orig_dev, $orig_ino ); } opendir( my $dh, $path ) or return; my @contents = grep { $_ ne '.' && $_ ne '..' } readdir($dh); closedir $dh; _bail_if_changed( $path, $orig_dev, $orig_ino ); require File::Spec if @contents; for my $thing (@contents) { my $long = File::Spec->catdir( $path, $thing ); if ( !-l $long && -d _ ) { _bail_if_changed( $path, $orig_dev, $orig_ino ); rm( $long, $fast ) or !-e $long or return; } else { _bail_if_changed( $path, $orig_dev, $orig_ino ); unlink $long or !-e $long or return; } } _bail_if_changed( $path, $orig_dev, $orig_ino ); if ( !$fast ) { chdir($starting_point) or Carp::croak("Failed to change directory to “$starting_point”: $!"); _bail_if_changed( ".", $starting_dev, $starting_ino ); } return 1; } sub mk_parent { my ( $path, $mode ) = @_; $path =~ s{/+$}{}; require File::Spec; my ( $v, $d, $f ) = File::Spec->splitpath( $path, 1 ); my @p = File::Spec->splitdir($d); # pop() is probably cheaper here, benchmark? $d = File::Spec->catdir(@p[0--$#p-1]); pop @p; $d = File::Spec->catdir(@p); my $parent = File::Spec->catpath( $v, $d, $f ); return mk( $parent, $mode ); } sub _bail_if_changed { my ( $path, $orig_dev, $orig_ino ) = @_; my ( $cur_dev, $cur_ino ) = ( lstat $path )[ 0, 1 ]; if ( !defined $cur_dev || !defined $cur_ino ) { $cur_dev ||= "undef(path went away?)"; $cur_ino ||= "undef(path went away?)"; } else { $path = Cwd::abs_path($path); } if ( $orig_dev ne $cur_dev || $orig_ino ne $cur_ino ) { local $Carp::CarpLevel += 1; Carp::croak("directory $path changed: expected dev=$orig_dev ino=$orig_ino, actual dev=$cur_dev ino=$cur_ino, aborting"); } } 1; } # --- END File::Path::Tiny { # --- BEGIN HTTP::Tiny # vim: ts=4 sts=4 sw=4 et: package HTTP::Tiny; use strict; use warnings; # ABSTRACT: A small, simple, correct HTTP/1.1 client our $VERSION = '0.080'; sub _croak { require Carp; Carp::croak(@_) } #pod =method new #pod #pod $http = HTTP::Tiny->new( %attributes ); #pod #pod This constructor returns a new HTTP::Tiny object. Valid attributes include: #pod #pod =for :list #pod * C — A user-agent string (defaults to 'HTTP-Tiny/$VERSION'). If #pod C — ends in a space character, the default user-agent string is #pod appended. #pod * C — An instance of L — or equivalent class #pod that supports the C and C methods #pod * C — A hashref of default headers to apply to requests #pod * C — The local IP address to bind to #pod * C — Whether to reuse the last connection (if for the same #pod scheme, host and port) (defaults to 1) #pod * C — Maximum number of redirects allowed (defaults to 5) #pod * C — Maximum response size in bytes (only when not using a data #pod callback). If defined, requests with responses larger than this will return #pod a 599 status code. #pod * C — URL of a proxy server to use for HTTP connections #pod (default is C<$ENV{http_proxy}> — if set) #pod * C — URL of a proxy server to use for HTTPS connections #pod (default is C<$ENV{https_proxy}> — if set) #pod * C — URL of a generic proxy server for both HTTP and HTTPS #pod connections (default is C<$ENV{all_proxy}> — if set) #pod * C — List of domain suffixes that should not be proxied. Must #pod be a comma-separated string or an array reference. (default is #pod C<$ENV{no_proxy}> —) #pod * C — Request timeout in seconds (default is 60) If a socket open, #pod read or write takes longer than the timeout, the request response status code #pod will be 599. #pod * C — A boolean that indicates whether to validate the SSL #pod certificate of an C — connection (default is false) #pod * C — A hashref of C — options to pass through to #pod L #pod #pod An accessor/mutator method exists for each attribute. #pod #pod Passing an explicit C for C, C or C will #pod prevent getting the corresponding proxies from the environment. #pod #pod Errors during request execution will result in a pseudo-HTTP status code of 599 #pod and a reason of "Internal Exception". The content field in the response will #pod contain the text of the error. #pod #pod The C parameter enables a persistent connection, but only to a #pod single destination scheme, host and port. If any connection-relevant #pod attributes are modified via accessor, or if the process ID or thread ID change, #pod the persistent connection will be dropped. If you want persistent connections #pod across multiple destinations, use multiple HTTP::Tiny objects. #pod #pod See L for more on the C and C attributes. #pod #pod =cut my @attributes; BEGIN { @attributes = qw( cookie_jar default_headers http_proxy https_proxy keep_alive local_address max_redirect max_size proxy no_proxy SSL_options verify_SSL ); my %persist_ok = map {; $_ => 1 } qw( cookie_jar default_headers max_redirect max_size ); no strict 'refs'; no warnings 'uninitialized'; for my $accessor ( @attributes ) { *{$accessor} = sub { @_ > 1 ? do { delete $_[0]->{handle} if !$persist_ok{$accessor} && $_[1] ne $_[0]->{$accessor}; $_[0]->{$accessor} = $_[1] } : $_[0]->{$accessor}; }; } } sub agent { my($self, $agent) = @_; if( @_ > 1 ){ $self->{agent} = (defined $agent && $agent =~ / $/) ? $agent . $self->_agent : $agent; } return $self->{agent}; } sub timeout { my ($self, $timeout) = @_; if ( @_ > 1 ) { $self->{timeout} = $timeout; if ($self->{handle}) { $self->{handle}->timeout($timeout); } } return $self->{timeout}; } sub new { my($class, %args) = @_; my $self = { max_redirect => 5, timeout => defined $args{timeout} ? $args{timeout} : 60, keep_alive => 1, verify_SSL => $args{verify_SSL} || $args{verify_ssl} || 0, # no verification by default no_proxy => $ENV{no_proxy}, }; bless $self, $class; $class->_validate_cookie_jar( $args{cookie_jar} ) if $args{cookie_jar}; for my $key ( @attributes ) { $self->{$key} = $args{$key} if exists $args{$key} } $self->agent( exists $args{agent} ? $args{agent} : $class->_agent ); $self->_set_proxies; return $self; } sub _set_proxies { my ($self) = @_; # get proxies from %ENV only if not provided; explicit undef will disable # getting proxies from the environment # generic proxy if (! exists $self->{proxy} ) { $self->{proxy} = $ENV{all_proxy} || $ENV{ALL_PROXY}; } if ( defined $self->{proxy} ) { $self->_split_proxy( 'generic proxy' => $self->{proxy} ); # validate } else { delete $self->{proxy}; } # http proxy if (! exists $self->{http_proxy} ) { # under CGI, bypass HTTP_PROXY as request sets it from Proxy header local $ENV{HTTP_PROXY} = ($ENV{CGI_HTTP_PROXY} || "") if $ENV{REQUEST_METHOD}; $self->{http_proxy} = $ENV{http_proxy} || $ENV{HTTP_PROXY} || $self->{proxy}; } if ( defined $self->{http_proxy} ) { $self->_split_proxy( http_proxy => $self->{http_proxy} ); # validate $self->{_has_proxy}{http} = 1; } else { delete $self->{http_proxy}; } # https proxy if (! exists $self->{https_proxy} ) { $self->{https_proxy} = $ENV{https_proxy} || $ENV{HTTPS_PROXY} || $self->{proxy}; } if ( $self->{https_proxy} ) { $self->_split_proxy( https_proxy => $self->{https_proxy} ); # validate $self->{_has_proxy}{https} = 1; } else { delete $self->{https_proxy}; } # Split no_proxy to array reference if not provided as such unless ( ref $self->{no_proxy} eq 'ARRAY' ) { $self->{no_proxy} = (defined $self->{no_proxy}) ? [ split /\s*,\s*/, $self->{no_proxy} ] : []; } return; } #pod =method get|head|put|post|patch|delete #pod #pod $response = $http->get($url); #pod $response = $http->get($url, \%options); #pod $response = $http->head($url); #pod #pod These methods are shorthand for calling C for the given method. The #pod URL must have unsafe characters escaped and international domain names encoded. #pod See C for valid options and a description of the response. #pod #pod The C field of the response will be true if the status code is 2XX. #pod #pod =cut for my $sub_name ( qw/get head put post patch delete/ ) { my $req_method = uc $sub_name; no strict 'refs'; eval <<"HERE"; ## no critic sub $sub_name { my (\$self, \$url, \$args) = \@_; \@_ == 2 || (\@_ == 3 && ref \$args eq 'HASH') or _croak(q/Usage: \$http->$sub_name(URL, [HASHREF])/ . "\n"); return \$self->request('$req_method', \$url, \$args || {}); } HERE } #pod =method post_form #pod #pod $response = $http->post_form($url, $form_data); #pod $response = $http->post_form($url, $form_data, \%options); #pod #pod This method executes a C request and sends the key/value pairs from a #pod form data hash or array reference to the given URL with a C of #pod C. If data is provided as an array #pod reference, the order is preserved; if provided as a hash reference, the terms #pod are sorted on key and value for consistency. See documentation for the #pod C method for details on the encoding. #pod #pod The URL must have unsafe characters escaped and international domain names #pod encoded. See C for valid options and a description of the response. #pod Any C header or content in the options hashref will be ignored. #pod #pod The C field of the response will be true if the status code is 2XX. #pod #pod =cut sub post_form { my ($self, $url, $data, $args) = @_; (@_ == 3 || @_ == 4 && ref $args eq 'HASH') or _croak(q/Usage: $http->post_form(URL, DATAREF, [HASHREF])/ . "\n"); my $headers = {}; while ( my ($key, $value) = each %{$args->{headers} || {}} ) { $headers->{lc $key} = $value; } delete $args->{headers}; return $self->request('POST', $url, { %$args, content => $self->www_form_urlencode($data), headers => { %$headers, 'content-type' => 'application/x-www-form-urlencoded' }, } ); } #pod =method mirror #pod #pod $response = $http->mirror($url, $file, \%options) #pod if ( $response->{success} ) { #pod print "$file is up to date\n"; #pod } #pod #pod Executes a C request for the URL and saves the response body to the file #pod name provided. The URL must have unsafe characters escaped and international #pod domain names encoded. If the file already exists, the request will include an #pod C header with the modification timestamp of the file. You #pod may specify a different C header yourself in the C<< #pod $options->{headers} >> hash. #pod #pod The C field of the response will be true if the status code is 2XX #pod or if the status code is 304 (unmodified). #pod #pod If the file was modified and the server response includes a properly #pod formatted C header, the file modification time will #pod be updated accordingly. #pod #pod =cut sub mirror { my ($self, $url, $file, $args) = @_; @_ == 3 || (@_ == 4 && ref $args eq 'HASH') or _croak(q/Usage: $http->mirror(URL, FILE, [HASHREF])/ . "\n"); if ( exists $args->{headers} ) { my $headers = {}; while ( my ($key, $value) = each %{$args->{headers} || {}} ) { $headers->{lc $key} = $value; } $args->{headers} = $headers; } if ( -e $file and my $mtime = (stat($file))[9] ) { $args->{headers}{'if-modified-since'} ||= $self->_http_date($mtime); } my $tempfile = $file . int(rand(2**31)); require Fcntl; sysopen my $fh, $tempfile, Fcntl::O_CREAT()|Fcntl::O_EXCL()|Fcntl::O_WRONLY() or _croak(qq/Error: Could not create temporary file $tempfile for downloading: $!\n/); binmode $fh; $args->{data_callback} = sub { print {$fh} $_[0] }; my $response = $self->request('GET', $url, $args); close $fh or _croak(qq/Error: Caught error closing temporary file $tempfile: $!\n/); if ( $response->{success} ) { rename $tempfile, $file or _croak(qq/Error replacing $file with $tempfile: $!\n/); my $lm = $response->{headers}{'last-modified'}; if ( $lm and my $mtime = $self->_parse_http_date($lm) ) { utime $mtime, $mtime, $file; } } $response->{success} ||= $response->{status} eq '304'; unlink $tempfile; return $response; } #pod =method request #pod #pod $response = $http->request($method, $url); #pod $response = $http->request($method, $url, \%options); #pod #pod Executes an HTTP request of the given method type ('GET', 'HEAD', 'POST', #pod 'PUT', etc.) on the given URL. The URL must have unsafe characters escaped and #pod international domain names encoded. #pod #pod B: Method names are B per the HTTP/1.1 specification. #pod Don't use C when you really want C. See L for #pod how this applies to redirection. #pod #pod If the URL includes a "user:password" stanza, they will be used for Basic-style #pod authorization headers. (Authorization headers will not be included in a #pod redirected request.) For example: #pod #pod $http->request('GET', 'http://Aladdin:open sesame@example.com/'); #pod #pod If the "user:password" stanza contains reserved characters, they must #pod be percent-escaped: #pod #pod $http->request('GET', 'http://john%40example.com:password@example.com/'); #pod #pod A hashref of options may be appended to modify the request. #pod #pod Valid options are: #pod #pod =for :list #pod * C — #pod A hashref containing headers to include with the request. If the value for #pod a header is an array reference, the header will be output multiple times with #pod each value in the array. These headers over-write any default headers. #pod * C — #pod A scalar to include as the body of the request OR a code reference #pod that will be called iteratively to produce the body of the request #pod * C — #pod A code reference that will be called if it exists to provide a hashref #pod of trailing headers (only used with chunked transfer-encoding) #pod * C — #pod A code reference that will be called for each chunks of the response #pod body received. #pod * C — #pod Override host resolution and force all connections to go only to a #pod specific peer address, regardless of the URL of the request. This will #pod include any redirections! This options should be used with extreme #pod caution (e.g. debugging or very special circumstances). It can be given as #pod either a scalar or a code reference that will receive the hostname and #pod whose response will be taken as the address. #pod #pod The C header is generated from the URL in accordance with RFC 2616. It #pod is a fatal error to specify C in the C option. Other headers #pod may be ignored or overwritten if necessary for transport compliance. #pod #pod If the C option is a code reference, it will be called iteratively #pod to provide the content body of the request. It should return the empty #pod string or undef when the iterator is exhausted. #pod #pod If the C option is the empty string, no C or #pod C headers will be generated. #pod #pod If the C option is provided, it will be called iteratively until #pod the entire response body is received. The first argument will be a string #pod containing a chunk of the response body, the second argument will be the #pod in-progress response hash reference, as described below. (This allows #pod customizing the action of the callback based on the C or C #pod received prior to the content body.) #pod #pod The C method returns a hashref containing the response. The hashref #pod will have the following keys: #pod #pod =for :list #pod * C — #pod Boolean indicating whether the operation returned a 2XX status code #pod * C — #pod URL that provided the response. This is the URL of the request unless #pod there were redirections, in which case it is the last URL queried #pod in a redirection chain #pod * C — #pod The HTTP status code of the response #pod * C — #pod The response phrase returned by the server #pod * C — #pod The body of the response. If the response does not have any content #pod or if a data callback is provided to consume the response body, #pod this will be the empty string #pod * C — #pod A hashref of header fields. All header field names will be normalized #pod to be lower case. If a header is repeated, the value will be an arrayref; #pod it will otherwise be a scalar string containing the value #pod * C - #pod If this field exists, it is the protocol of the response #pod such as HTTP/1.0 or HTTP/1.1 #pod * C #pod If this field exists, it is an arrayref of response hash references from #pod redirects in the same order that redirections occurred. If it does #pod not exist, then no redirections occurred. #pod #pod On an error during the execution of the request, the C field will #pod contain 599, and the C field will contain the text of the error. #pod #pod =cut my %idempotent = map { $_ => 1 } qw/GET HEAD PUT DELETE OPTIONS TRACE/; sub request { my ($self, $method, $url, $args) = @_; @_ == 3 || (@_ == 4 && ref $args eq 'HASH') or _croak(q/Usage: $http->request(METHOD, URL, [HASHREF])/ . "\n"); $args ||= {}; # we keep some state in this during _request # RFC 2616 Section 8.1.4 mandates a single retry on broken socket my $response; for ( 0 .. 1 ) { $response = eval { $self->_request($method, $url, $args) }; last unless $@ && $idempotent{$method} && $@ =~ m{^(?:Socket closed|Unexpected end|SSL read error)}; } if (my $e = $@) { # maybe we got a response hash thrown from somewhere deep if ( ref $e eq 'HASH' && exists $e->{status} ) { $e->{redirects} = delete $args->{_redirects} if @{ $args->{_redirects} || []}; return $e; } # otherwise, stringify it $e = "$e"; $response = { url => $url, success => q{}, status => 599, reason => 'Internal Exception', content => $e, headers => { 'content-type' => 'text/plain', 'content-length' => length $e, }, ( @{$args->{_redirects} || []} ? (redirects => delete $args->{_redirects}) : () ), }; } return $response; } #pod =method www_form_urlencode #pod #pod $params = $http->www_form_urlencode( $data ); #pod $response = $http->get("http://example.com/query?$params"); #pod #pod This method converts the key/value pairs from a data hash or array reference #pod into a C string. The keys and values from the data #pod reference will be UTF-8 encoded and escaped per RFC 3986. If a value is an #pod array reference, the key will be repeated with each of the values of the array #pod reference. If data is provided as a hash reference, the key/value pairs in the #pod resulting string will be sorted by key and value for consistent ordering. #pod #pod =cut sub www_form_urlencode { my ($self, $data) = @_; (@_ == 2 && ref $data) or _croak(q/Usage: $http->www_form_urlencode(DATAREF)/ . "\n"); (ref $data eq 'HASH' || ref $data eq 'ARRAY') or _croak("form data must be a hash or array reference\n"); my @params = ref $data eq 'HASH' ? %$data : @$data; @params % 2 == 0 or _croak("form data reference must have an even number of terms\n"); my @terms; while( @params ) { my ($key, $value) = splice(@params, 0, 2); _croak("form data keys must not be undef") if !defined($key); if ( ref $value eq 'ARRAY' ) { unshift @params, map { $key => $_ } @$value; } else { push @terms, join("=", map { $self->_uri_escape($_) } $key, $value); } } return join("&", (ref $data eq 'ARRAY') ? (@terms) : (sort @terms) ); } #pod =method can_ssl #pod #pod $ok = HTTP::Tiny->can_ssl; #pod ($ok, $why) = HTTP::Tiny->can_ssl; #pod ($ok, $why) = $http->can_ssl; #pod #pod Indicates if SSL support is available. When called as a class object, it #pod checks for the correct version of L and L. #pod When called as an object methods, if C is true or if C #pod is set in C, it checks that a CA file is available. #pod #pod In scalar context, returns a boolean indicating if SSL is available. #pod In list context, returns the boolean and a (possibly multi-line) string of #pod errors indicating why SSL isn't available. #pod #pod =cut sub can_ssl { my ($self) = @_; my($ok, $reason) = (1, ''); # Need IO::Socket::SSL 1.42 for SSL_create_ctx_callback local @INC = @INC; pop @INC if $INC[-1] eq '.'; unless (eval {require IO::Socket::SSL; IO::Socket::SSL->VERSION(1.42)}) { $ok = 0; $reason .= qq/IO::Socket::SSL 1.42 must be installed for https support\n/; } # Need Net::SSLeay 1.49 for MODE_AUTO_RETRY unless (eval {require Net::SSLeay; Net::SSLeay->VERSION(1.49)}) { $ok = 0; $reason .= qq/Net::SSLeay 1.49 must be installed for https support\n/; } # If an object, check that SSL config lets us get a CA if necessary if ( ref($self) && ( $self->{verify_SSL} || $self->{SSL_options}{SSL_verify_mode} ) ) { my $handle = HTTP::Tiny::Handle->new( SSL_options => $self->{SSL_options}, verify_SSL => $self->{verify_SSL}, ); unless ( eval { $handle->_find_CA_file; 1 } ) { $ok = 0; $reason .= "$@"; } } wantarray ? ($ok, $reason) : $ok; } #pod =method connected #pod #pod $host = $http->connected; #pod ($host, $port) = $http->connected; #pod #pod Indicates if a connection to a peer is being kept alive, per the C #pod option. #pod #pod In scalar context, returns the peer host and port, joined with a colon, or #pod C (if no peer is connected). #pod In list context, returns the peer host and port or an empty list (if no peer #pod is connected). #pod #pod B: This method cannot reliably be used to discover whether the remote #pod host has closed its end of the socket. #pod #pod =cut sub connected { my ($self) = @_; if ( $self->{handle} ) { return $self->{handle}->connected; } return; } #--------------------------------------------------------------------------# # private methods #--------------------------------------------------------------------------# my %DefaultPort = ( http => 80, https => 443, ); sub _agent { my $class = ref($_[0]) || $_[0]; (my $default_agent = $class) =~ s{::}{-}g; my $version = $class->VERSION; $default_agent .= "/$version" if defined $version; return $default_agent; } sub _request { my ($self, $method, $url, $args) = @_; my ($scheme, $host, $port, $path_query, $auth) = $self->_split_url($url); if ($scheme ne 'http' && $scheme ne 'https') { die(qq/Unsupported URL scheme '$scheme'\n/); } my $request = { method => $method, scheme => $scheme, host => $host, port => $port, host_port => ($port == $DefaultPort{$scheme} ? $host : "$host:$port"), uri => $path_query, headers => {}, }; my $peer = $args->{peer} || $host; # Allow 'peer' to be a coderef. if ('CODE' eq ref $peer) { $peer = $peer->($host); } # We remove the cached handle so it is not reused in the case of redirect. # If all is well, it will be recached at the end of _request. We only # reuse for the same scheme, host and port my $handle = delete $self->{handle}; if ( $handle ) { unless ( $handle->can_reuse( $scheme, $host, $port, $peer ) ) { $handle->close; undef $handle; } } $handle ||= $self->_open_handle( $request, $scheme, $host, $port, $peer ); $self->_prepare_headers_and_cb($request, $args, $url, $auth); $handle->write_request($request); my $response; do { $response = $handle->read_response_header } until (substr($response->{status},0,1) ne '1'); $self->_update_cookie_jar( $url, $response ) if $self->{cookie_jar}; my @redir_args = $self->_maybe_redirect($request, $response, $args); my $known_message_length; if ($method eq 'HEAD' || $response->{status} =~ /^[23]04/) { # response has no message body $known_message_length = 1; } else { # Ignore any data callbacks during redirection. my $cb_args = @redir_args ? +{} : $args; my $data_cb = $self->_prepare_data_cb($response, $cb_args); $known_message_length = $handle->read_body($data_cb, $response); } if ( $self->{keep_alive} && $handle->connected && $known_message_length && $response->{protocol} eq 'HTTP/1.1' && ($response->{headers}{connection} || '') ne 'close' ) { $self->{handle} = $handle; } else { $handle->close; } $response->{success} = substr( $response->{status}, 0, 1 ) eq '2'; $response->{url} = $url; # Push the current response onto the stack of redirects if redirecting. if (@redir_args) { push @{$args->{_redirects}}, $response; return $self->_request(@redir_args, $args); } # Copy the stack of redirects into the response before returning. $response->{redirects} = delete $args->{_redirects} if @{$args->{_redirects}}; return $response; } sub _open_handle { my ($self, $request, $scheme, $host, $port, $peer) = @_; my $handle = HTTP::Tiny::Handle->new( timeout => $self->{timeout}, SSL_options => $self->{SSL_options}, verify_SSL => $self->{verify_SSL}, local_address => $self->{local_address}, keep_alive => $self->{keep_alive} ); if ($self->{_has_proxy}{$scheme} && ! grep { $host =~ /\Q$_\E$/ } @{$self->{no_proxy}}) { return $self->_proxy_connect( $request, $handle ); } else { return $handle->connect($scheme, $host, $port, $peer); } } sub _proxy_connect { my ($self, $request, $handle) = @_; my @proxy_vars; if ( $request->{scheme} eq 'https' ) { _croak(qq{No https_proxy defined}) unless $self->{https_proxy}; @proxy_vars = $self->_split_proxy( https_proxy => $self->{https_proxy} ); if ( $proxy_vars[0] eq 'https' ) { _croak(qq{Can't proxy https over https: $request->{uri} via $self->{https_proxy}}); } } else { _croak(qq{No http_proxy defined}) unless $self->{http_proxy}; @proxy_vars = $self->_split_proxy( http_proxy => $self->{http_proxy} ); } my ($p_scheme, $p_host, $p_port, $p_auth) = @proxy_vars; if ( length $p_auth && ! defined $request->{headers}{'proxy-authorization'} ) { $self->_add_basic_auth_header( $request, 'proxy-authorization' => $p_auth ); } $handle->connect($p_scheme, $p_host, $p_port, $p_host); if ($request->{scheme} eq 'https') { $self->_create_proxy_tunnel( $request, $handle ); } else { # non-tunneled proxy requires absolute URI $request->{uri} = "$request->{scheme}://$request->{host_port}$request->{uri}"; } return $handle; } sub _split_proxy { my ($self, $type, $proxy) = @_; my ($scheme, $host, $port, $path_query, $auth) = eval { $self->_split_url($proxy) }; unless( defined($scheme) && length($scheme) && length($host) && length($port) && $path_query eq '/' ) { _croak(qq{$type URL must be in format http[s]://[auth@]:/\n}); } return ($scheme, $host, $port, $auth); } sub _create_proxy_tunnel { my ($self, $request, $handle) = @_; $handle->_assert_ssl; my $agent = exists($request->{headers}{'user-agent'}) ? $request->{headers}{'user-agent'} : $self->{agent}; my $connect_request = { method => 'CONNECT', uri => "$request->{host}:$request->{port}", headers => { host => "$request->{host}:$request->{port}", 'user-agent' => $agent, } }; if ( $request->{headers}{'proxy-authorization'} ) { $connect_request->{headers}{'proxy-authorization'} = delete $request->{headers}{'proxy-authorization'}; } $handle->write_request($connect_request); my $response; do { $response = $handle->read_response_header } until (substr($response->{status},0,1) ne '1'); # if CONNECT failed, throw the response so it will be # returned from the original request() method; unless (substr($response->{status},0,1) eq '2') { die $response; } # tunnel established, so start SSL handshake $handle->start_ssl( $request->{host} ); return; } sub _prepare_headers_and_cb { my ($self, $request, $args, $url, $auth) = @_; for ($self->{default_headers}, $args->{headers}) { next unless defined; while (my ($k, $v) = each %$_) { $request->{headers}{lc $k} = $v; $request->{header_case}{lc $k} = $k; } } if (exists $request->{headers}{'host'}) { die(qq/The 'Host' header must not be provided as header option\n/); } $request->{headers}{'host'} = $request->{host_port}; $request->{headers}{'user-agent'} ||= $self->{agent}; $request->{headers}{'connection'} = "close" unless $self->{keep_alive}; # Some servers error on an empty-body PUT/POST without a content-length if ( $request->{method} eq 'PUT' || $request->{method} eq 'POST' ) { if (!defined($args->{content}) || !length($args->{content}) ) { $request->{headers}{'content-length'} = 0; } } if ( defined $args->{content} ) { if ( ref $args->{content} eq 'CODE' ) { if ( exists $request->{'content-length'} && $request->{'content-length'} == 0 ) { $request->{cb} = sub { "" }; } else { $request->{headers}{'content-type'} ||= "application/octet-stream"; $request->{headers}{'transfer-encoding'} = 'chunked' unless exists $request->{headers}{'content-length'} || $request->{headers}{'transfer-encoding'}; $request->{cb} = $args->{content}; } } elsif ( length $args->{content} ) { my $content = $args->{content}; if ( $] ge '5.008' ) { utf8::downgrade($content, 1) or die(qq/Wide character in request message body\n/); } $request->{headers}{'content-type'} ||= "application/octet-stream"; $request->{headers}{'content-length'} = length $content unless $request->{headers}{'content-length'} || $request->{headers}{'transfer-encoding'}; $request->{cb} = sub { substr $content, 0, length $content, '' }; } $request->{trailer_cb} = $args->{trailer_callback} if ref $args->{trailer_callback} eq 'CODE'; } ### If we have a cookie jar, then maybe add relevant cookies if ( $self->{cookie_jar} ) { my $cookies = $self->cookie_jar->cookie_header( $url ); $request->{headers}{cookie} = $cookies if length $cookies; } # if we have Basic auth parameters, add them if ( length $auth && ! defined $request->{headers}{authorization} ) { $self->_add_basic_auth_header( $request, 'authorization' => $auth ); } return; } sub _add_basic_auth_header { my ($self, $request, $header, $auth) = @_; require MIME::Base64; $request->{headers}{$header} = "Basic " . MIME::Base64::encode_base64($auth, ""); return; } sub _prepare_data_cb { my ($self, $response, $args) = @_; my $data_cb = $args->{data_callback}; $response->{content} = ''; if (!$data_cb || $response->{status} !~ /^2/) { if (defined $self->{max_size}) { $data_cb = sub { $_[1]->{content} .= $_[0]; die(qq/Size of response body exceeds the maximum allowed of $self->{max_size}\n/) if length $_[1]->{content} > $self->{max_size}; }; } else { $data_cb = sub { $_[1]->{content} .= $_[0] }; } } return $data_cb; } sub _update_cookie_jar { my ($self, $url, $response) = @_; my $cookies = $response->{headers}->{'set-cookie'}; return unless defined $cookies; my @cookies = ref $cookies ? @$cookies : $cookies; $self->cookie_jar->add( $url, $_ ) for @cookies; return; } sub _validate_cookie_jar { my ($class, $jar) = @_; # duck typing for my $method ( qw/add cookie_header/ ) { _croak(qq/Cookie jar must provide the '$method' method\n/) unless ref($jar) && ref($jar)->can($method); } return; } sub _maybe_redirect { my ($self, $request, $response, $args) = @_; my $headers = $response->{headers}; my ($status, $method) = ($response->{status}, $request->{method}); $args->{_redirects} ||= []; if (($status eq '303' or ($status =~ /^30[1278]/ && $method =~ /^GET|HEAD$/)) and $headers->{location} and @{$args->{_redirects}} < $self->{max_redirect} ) { my $location = ($headers->{location} =~ /^\//) ? "$request->{scheme}://$request->{host_port}$headers->{location}" : $headers->{location} ; return (($status eq '303' ? 'GET' : $method), $location); } return; } sub _split_url { my $url = pop; # URI regex adapted from the URI module my ($scheme, $host, $path_query) = $url =~ m<\A([^:/?#]+)://([^/?#]*)([^#]*)> or die(qq/Cannot parse URL: '$url'\n/); $scheme = lc $scheme; $path_query = "/$path_query" unless $path_query =~ m<\A/>; my $auth = ''; if ( (my $i = index $host, '@') != -1 ) { # user:pass@host $auth = substr $host, 0, $i, ''; # take up to the @ for auth substr $host, 0, 1, ''; # knock the @ off the host # userinfo might be percent escaped, so recover real auth info $auth =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; } my $port = $host =~ s/:(\d*)\z// && length $1 ? $1 : $scheme eq 'http' ? 80 : $scheme eq 'https' ? 443 : undef; return ($scheme, (length $host ? lc $host : "localhost") , $port, $path_query, $auth); } # Date conversions adapted from HTTP::Date my $DoW = "Sun|Mon|Tue|Wed|Thu|Fri|Sat"; my $MoY = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec"; sub _http_date { my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($_[1]); return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", substr($DoW,$wday*4,3), $mday, substr($MoY,$mon*4,3), $year+1900, $hour, $min, $sec ); } sub _parse_http_date { my ($self, $str) = @_; require Time::Local; my @tl_parts; if ($str =~ /^[SMTWF][a-z]+, +(\d{1,2}) ($MoY) +(\d\d\d\d) +(\d\d):(\d\d):(\d\d) +GMT$/) { @tl_parts = ($6, $5, $4, $1, (index($MoY,$2)/4), $3); } elsif ($str =~ /^[SMTWF][a-z]+, +(\d\d)-($MoY)-(\d{2,4}) +(\d\d):(\d\d):(\d\d) +GMT$/ ) { @tl_parts = ($6, $5, $4, $1, (index($MoY,$2)/4), $3); } elsif ($str =~ /^[SMTWF][a-z]+ +($MoY) +(\d{1,2}) +(\d\d):(\d\d):(\d\d) +(?:[^0-9]+ +)?(\d\d\d\d)$/ ) { @tl_parts = ($5, $4, $3, $2, (index($MoY,$1)/4), $6); } return eval { my $t = @tl_parts ? Time::Local::timegm(@tl_parts) : -1; $t < 0 ? undef : $t; }; } # URI escaping adapted from URI::Escape # c.f. http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 # perl 5.6 ready UTF-8 encoding adapted from JSON::PP my %escapes = map { chr($_) => sprintf("%%%02X", $_) } 0..255; $escapes{' '}="+"; my $unsafe_char = qr/[^A-Za-z0-9\-\._~]/; sub _uri_escape { my ($self, $str) = @_; return "" if !defined $str; if ( $] ge '5.008' ) { utf8::encode($str); } else { $str = pack("U*", unpack("C*", $str)) # UTF-8 encode a byte string if ( length $str == do { use bytes; length $str } ); $str = pack("C*", unpack("C*", $str)); # clear UTF-8 flag } $str =~ s/($unsafe_char)/$escapes{$1}/g; return $str; } package HTTP::Tiny::Handle; # hide from PAUSE/indexers use strict; use warnings; use Errno qw[EINTR EPIPE]; use IO::Socket qw[SOCK_STREAM]; use Socket qw[SOL_SOCKET SO_KEEPALIVE]; # PERL_HTTP_TINY_IPV4_ONLY is a private environment variable to force old # behavior if someone is unable to boostrap CPAN from a new perl install; it is # not intended for general, per-client use and may be removed in the future my $SOCKET_CLASS = $ENV{PERL_HTTP_TINY_IPV4_ONLY} ? 'IO::Socket::INET' : eval { require IO::Socket::IP; IO::Socket::IP->VERSION(0.32) } ? 'IO::Socket::IP' : 'IO::Socket::INET'; sub BUFSIZE () { 32768 } ## no critic my $Printable = sub { local $_ = shift; s/\r/\\r/g; s/\n/\\n/g; s/\t/\\t/g; s/([^\x20-\x7E])/sprintf('\\x%.2X', ord($1))/ge; $_; }; my $Token = qr/[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/; my $Field_Content = qr/[[:print:]]+ (?: [\x20\x09]+ [[:print:]]+ )*/x; sub new { my ($class, %args) = @_; return bless { rbuf => '', timeout => 60, max_line_size => 16384, max_header_lines => 64, verify_SSL => 0, SSL_options => {}, %args }, $class; } sub timeout { my ($self, $timeout) = @_; if ( @_ > 1 ) { $self->{timeout} = $timeout; if ( $self->{fh} && $self->{fh}->can('timeout') ) { $self->{fh}->timeout($timeout); } } return $self->{timeout}; } sub connect { @_ == 5 || die(q/Usage: $handle->connect(scheme, host, port, peer)/ . "\n"); my ($self, $scheme, $host, $port, $peer) = @_; if ( $scheme eq 'https' ) { $self->_assert_ssl; } $self->{fh} = $SOCKET_CLASS->new( PeerHost => $peer, PeerPort => $port, $self->{local_address} ? ( LocalAddr => $self->{local_address} ) : (), Proto => 'tcp', Type => SOCK_STREAM, Timeout => $self->{timeout}, ) or die(qq/Could not connect to '$host:$port': $@\n/); binmode($self->{fh}) or die(qq/Could not binmode() socket: '$!'\n/); if ( $self->{keep_alive} ) { unless ( defined( $self->{fh}->setsockopt( SOL_SOCKET, SO_KEEPALIVE, 1 ) ) ) { CORE::close($self->{fh}); die(qq/Could not set SO_KEEPALIVE on socket: '$!'\n/); } } $self->start_ssl($host) if $scheme eq 'https'; $self->{scheme} = $scheme; $self->{host} = $host; $self->{peer} = $peer; $self->{port} = $port; $self->{pid} = $$; $self->{tid} = _get_tid(); return $self; } sub connected { my ($self) = @_; if ( $self->{fh} && $self->{fh}->connected ) { return wantarray ? ( $self->{fh}->peerhost, $self->{fh}->peerport ) : join( ':', $self->{fh}->peerhost, $self->{fh}->peerport ); } return; } sub start_ssl { my ($self, $host) = @_; # As this might be used via CONNECT after an SSL session # to a proxy, we shut down any existing SSL before attempting # the handshake if ( ref($self->{fh}) eq 'IO::Socket::SSL' ) { unless ( $self->{fh}->stop_SSL ) { my $ssl_err = IO::Socket::SSL->errstr; die(qq/Error halting prior SSL connection: $ssl_err/); } } my $ssl_args = $self->_ssl_args($host); IO::Socket::SSL->start_SSL( $self->{fh}, %$ssl_args, SSL_create_ctx_callback => sub { my $ctx = shift; Net::SSLeay::CTX_set_mode($ctx, Net::SSLeay::MODE_AUTO_RETRY()); }, ); unless ( ref($self->{fh}) eq 'IO::Socket::SSL' ) { my $ssl_err = IO::Socket::SSL->errstr; die(qq/SSL connection failed for $host: $ssl_err\n/); } } sub close { @_ == 1 || die(q/Usage: $handle->close()/ . "\n"); my ($self) = @_; CORE::close($self->{fh}) or die(qq/Could not close socket: '$!'\n/); } sub write { @_ == 2 || die(q/Usage: $handle->write(buf)/ . "\n"); my ($self, $buf) = @_; if ( $] ge '5.008' ) { utf8::downgrade($buf, 1) or die(qq/Wide character in write()\n/); } my $len = length $buf; my $off = 0; local $SIG{PIPE} = 'IGNORE'; while () { $self->can_write or die(qq/Timed out while waiting for socket to become ready for writing\n/); my $r = syswrite($self->{fh}, $buf, $len, $off); if (defined $r) { $len -= $r; $off += $r; last unless $len > 0; } elsif ($! == EPIPE) { die(qq/Socket closed by remote server: $!\n/); } elsif ($! != EINTR) { if ($self->{fh}->can('errstr')){ my $err = $self->{fh}->errstr(); die (qq/Could not write to SSL socket: '$err'\n /); } else { die(qq/Could not write to socket: '$!'\n/); } } } return $off; } sub read { @_ == 2 || @_ == 3 || die(q/Usage: $handle->read(len [, allow_partial])/ . "\n"); my ($self, $len, $allow_partial) = @_; my $buf = ''; my $got = length $self->{rbuf}; if ($got) { my $take = ($got < $len) ? $got : $len; $buf = substr($self->{rbuf}, 0, $take, ''); $len -= $take; } # Ignore SIGPIPE because SSL reads can result in writes that might error. # See "Expecting exactly the same behavior as plain sockets" in # https://metacpan.org/dist/IO-Socket-SSL/view/lib/IO/Socket/SSL.pod#Common-Usage-Errors local $SIG{PIPE} = 'IGNORE'; while ($len > 0) { $self->can_read or die(q/Timed out while waiting for socket to become ready for reading/ . "\n"); my $r = sysread($self->{fh}, $buf, $len, length $buf); if (defined $r) { last unless $r; $len -= $r; } elsif ($! != EINTR) { if ($self->{fh}->can('errstr')){ my $err = $self->{fh}->errstr(); die (qq/Could not read from SSL socket: '$err'\n /); } else { die(qq/Could not read from socket: '$!'\n/); } } } if ($len && !$allow_partial) { die(qq/Unexpected end of stream\n/); } return $buf; } sub readline { @_ == 1 || die(q/Usage: $handle->readline()/ . "\n"); my ($self) = @_; while () { if ($self->{rbuf} =~ s/\A ([^\x0D\x0A]* \x0D?\x0A)//x) { return $1; } if (length $self->{rbuf} >= $self->{max_line_size}) { die(qq/Line size exceeds the maximum allowed size of $self->{max_line_size}\n/); } $self->can_read or die(qq/Timed out while waiting for socket to become ready for reading\n/); my $r = sysread($self->{fh}, $self->{rbuf}, BUFSIZE, length $self->{rbuf}); if (defined $r) { last unless $r; } elsif ($! != EINTR) { if ($self->{fh}->can('errstr')){ my $err = $self->{fh}->errstr(); die (qq/Could not read from SSL socket: '$err'\n /); } else { die(qq/Could not read from socket: '$!'\n/); } } } die(qq/Unexpected end of stream while looking for line\n/); } sub read_header_lines { @_ == 1 || @_ == 2 || die(q/Usage: $handle->read_header_lines([headers])/ . "\n"); my ($self, $headers) = @_; $headers ||= {}; my $lines = 0; my $val; while () { my $line = $self->readline; if (++$lines >= $self->{max_header_lines}) { die(qq/Header lines exceeds maximum number allowed of $self->{max_header_lines}\n/); } elsif ($line =~ /\A ([^\x00-\x1F\x7F:]+) : [\x09\x20]* ([^\x0D\x0A]*)/x) { my ($field_name) = lc $1; if (exists $headers->{$field_name}) { for ($headers->{$field_name}) { $_ = [$_] unless ref $_ eq "ARRAY"; push @$_, $2; $val = \$_->[-1]; } } else { $val = \($headers->{$field_name} = $2); } } elsif ($line =~ /\A [\x09\x20]+ ([^\x0D\x0A]*)/x) { $val or die(qq/Unexpected header continuation line\n/); next unless length $1; $$val .= ' ' if length $$val; $$val .= $1; } elsif ($line =~ /\A \x0D?\x0A \z/x) { last; } else { die(q/Malformed header line: / . $Printable->($line) . "\n"); } } return $headers; } sub write_request { @_ == 2 || die(q/Usage: $handle->write_request(request)/ . "\n"); my($self, $request) = @_; $self->write_request_header(@{$request}{qw/method uri headers header_case/}); $self->write_body($request) if $request->{cb}; return; } # Standard request header names/case from HTTP/1.1 RFCs my @rfc_request_headers = qw( Accept Accept-Charset Accept-Encoding Accept-Language Authorization Cache-Control Connection Content-Length Expect From Host If-Match If-Modified-Since If-None-Match If-Range If-Unmodified-Since Max-Forwards Pragma Proxy-Authorization Range Referer TE Trailer Transfer-Encoding Upgrade User-Agent Via ); my @other_request_headers = qw( Content-Encoding Content-MD5 Content-Type Cookie DNT Date Origin X-XSS-Protection ); my %HeaderCase = map { lc($_) => $_ } @rfc_request_headers, @other_request_headers; # to avoid multiple small writes and hence nagle, you can pass the method line or anything else to # combine writes. sub write_header_lines { (@_ >= 2 && @_ <= 4 && ref $_[1] eq 'HASH') || die(q/Usage: $handle->write_header_lines(headers, [header_case, prefix])/ . "\n"); my($self, $headers, $header_case, $prefix_data) = @_; $header_case ||= {}; my $buf = (defined $prefix_data ? $prefix_data : ''); # Per RFC, control fields should be listed first my %seen; for my $k ( qw/host cache-control expect max-forwards pragma range te/ ) { next unless exists $headers->{$k}; $seen{$k}++; my $field_name = $HeaderCase{$k}; my $v = $headers->{$k}; for (ref $v eq 'ARRAY' ? @$v : $v) { $_ = '' unless defined $_; $buf .= "$field_name: $_\x0D\x0A"; } } # Other headers sent in arbitrary order while (my ($k, $v) = each %$headers) { my $field_name = lc $k; next if $seen{$field_name}; if (exists $HeaderCase{$field_name}) { $field_name = $HeaderCase{$field_name}; } else { if (exists $header_case->{$field_name}) { $field_name = $header_case->{$field_name}; } else { $field_name =~ s/\b(\w)/\u$1/g; } $field_name =~ /\A $Token+ \z/xo or die(q/Invalid HTTP header field name: / . $Printable->($field_name) . "\n"); $HeaderCase{lc $field_name} = $field_name; } for (ref $v eq 'ARRAY' ? @$v : $v) { # unwrap a field value if pre-wrapped by user s/\x0D?\x0A\s+/ /g; die(qq/Invalid HTTP header field value ($field_name): / . $Printable->($_). "\n") unless $_ eq '' || /\A $Field_Content \z/xo; $_ = '' unless defined $_; $buf .= "$field_name: $_\x0D\x0A"; } } $buf .= "\x0D\x0A"; return $self->write($buf); } # return value indicates whether message length was defined; this is generally # true unless there was no content-length header and we just read until EOF. # Other message length errors are thrown as exceptions sub read_body { @_ == 3 || die(q/Usage: $handle->read_body(callback, response)/ . "\n"); my ($self, $cb, $response) = @_; my $te = $response->{headers}{'transfer-encoding'} || ''; my $chunked = grep { /chunked/i } ( ref $te eq 'ARRAY' ? @$te : $te ) ; return $chunked ? $self->read_chunked_body($cb, $response) : $self->read_content_body($cb, $response); } sub write_body { @_ == 2 || die(q/Usage: $handle->write_body(request)/ . "\n"); my ($self, $request) = @_; if (exists $request->{headers}{'content-length'}) { return unless $request->{headers}{'content-length'}; return $self->write_content_body($request); } else { return $self->write_chunked_body($request); } } sub read_content_body { @_ == 3 || @_ == 4 || die(q/Usage: $handle->read_content_body(callback, response, [read_length])/ . "\n"); my ($self, $cb, $response, $content_length) = @_; $content_length ||= $response->{headers}{'content-length'}; if ( defined $content_length ) { my $len = $content_length; while ($len > 0) { my $read = ($len > BUFSIZE) ? BUFSIZE : $len; $cb->($self->read($read, 0), $response); $len -= $read; } return length($self->{rbuf}) == 0; } my $chunk; $cb->($chunk, $response) while length( $chunk = $self->read(BUFSIZE, 1) ); return; } sub write_content_body { @_ == 2 || die(q/Usage: $handle->write_content_body(request)/ . "\n"); my ($self, $request) = @_; my ($len, $content_length) = (0, $request->{headers}{'content-length'}); while () { my $data = $request->{cb}->(); defined $data && length $data or last; if ( $] ge '5.008' ) { utf8::downgrade($data, 1) or die(qq/Wide character in write_content()\n/); } $len += $self->write($data); } $len == $content_length or die(qq/Content-Length mismatch (got: $len expected: $content_length)\n/); return $len; } sub read_chunked_body { @_ == 3 || die(q/Usage: $handle->read_chunked_body(callback, $response)/ . "\n"); my ($self, $cb, $response) = @_; while () { my $head = $self->readline; $head =~ /\A ([A-Fa-f0-9]+)/x or die(q/Malformed chunk head: / . $Printable->($head) . "\n"); my $len = hex($1) or last; $self->read_content_body($cb, $response, $len); $self->read(2) eq "\x0D\x0A" or die(qq/Malformed chunk: missing CRLF after chunk data\n/); } $self->read_header_lines($response->{headers}); return 1; } sub write_chunked_body { @_ == 2 || die(q/Usage: $handle->write_chunked_body(request)/ . "\n"); my ($self, $request) = @_; my $len = 0; while () { my $data = $request->{cb}->(); defined $data && length $data or last; if ( $] ge '5.008' ) { utf8::downgrade($data, 1) or die(qq/Wide character in write_chunked_body()\n/); } $len += length $data; my $chunk = sprintf '%X', length $data; $chunk .= "\x0D\x0A"; $chunk .= $data; $chunk .= "\x0D\x0A"; $self->write($chunk); } $self->write("0\x0D\x0A"); if ( ref $request->{trailer_cb} eq 'CODE' ) { $self->write_header_lines($request->{trailer_cb}->()) } else { $self->write("\x0D\x0A"); } return $len; } sub read_response_header { @_ == 1 || die(q/Usage: $handle->read_response_header()/ . "\n"); my ($self) = @_; my $line = $self->readline; $line =~ /\A (HTTP\/(0*\d+\.0*\d+)) [\x09\x20]+ ([0-9]{3}) (?: [\x09\x20]+ ([^\x0D\x0A]*) )? \x0D?\x0A/x or die(q/Malformed Status-Line: / . $Printable->($line). "\n"); my ($protocol, $version, $status, $reason) = ($1, $2, $3, $4); $reason = "" unless defined $reason; die (qq/Unsupported HTTP protocol: $protocol\n/) unless $version =~ /0*1\.0*[01]/; return { status => $status, reason => $reason, headers => $self->read_header_lines, protocol => $protocol, }; } sub write_request_header { @_ == 5 || die(q/Usage: $handle->write_request_header(method, request_uri, headers, header_case)/ . "\n"); my ($self, $method, $request_uri, $headers, $header_case) = @_; return $self->write_header_lines($headers, $header_case, "$method $request_uri HTTP/1.1\x0D\x0A"); } sub _do_timeout { my ($self, $type, $timeout) = @_; $timeout = $self->{timeout} unless defined $timeout && $timeout >= 0; my $fd = fileno $self->{fh}; defined $fd && $fd >= 0 or die(qq/select(2): 'Bad file descriptor'\n/); my $initial = time; my $pending = $timeout; my $nfound; vec(my $fdset = '', $fd, 1) = 1; while () { $nfound = ($type eq 'read') ? select($fdset, undef, undef, $pending) : select(undef, $fdset, undef, $pending) ; if ($nfound == -1) { $! == EINTR or die(qq/select(2): '$!'\n/); redo if !$timeout || ($pending = $timeout - (time - $initial)) > 0; $nfound = 0; } last; } $! = 0; return $nfound; } sub can_read { @_ == 1 || @_ == 2 || die(q/Usage: $handle->can_read([timeout])/ . "\n"); my $self = shift; if ( ref($self->{fh}) eq 'IO::Socket::SSL' ) { return 1 if $self->{fh}->pending; } return $self->_do_timeout('read', @_) } sub can_write { @_ == 1 || @_ == 2 || die(q/Usage: $handle->can_write([timeout])/ . "\n"); my $self = shift; return $self->_do_timeout('write', @_) } sub _assert_ssl { my($ok, $reason) = HTTP::Tiny->can_ssl(); die $reason unless $ok; } sub can_reuse { my ($self,$scheme,$host,$port,$peer) = @_; return 0 if $self->{pid} != $$ || $self->{tid} != _get_tid() || length($self->{rbuf}) || $scheme ne $self->{scheme} || $host ne $self->{host} || $port ne $self->{port} || $peer ne $self->{peer} || eval { $self->can_read(0) } || $@ ; return 1; } # Try to find a CA bundle to validate the SSL cert, # prefer Mozilla::CA or fallback to a system file sub _find_CA_file { my $self = shift(); my $ca_file = defined( $self->{SSL_options}->{SSL_ca_file} ) ? $self->{SSL_options}->{SSL_ca_file} : $ENV{SSL_CERT_FILE}; if ( defined $ca_file ) { unless ( -r $ca_file ) { die qq/SSL_ca_file '$ca_file' not found or not readable\n/; } return $ca_file; } local @INC = @INC; pop @INC if $INC[-1] eq '.'; return Mozilla::CA::SSL_ca_file() if eval { require Mozilla::CA; 1 }; # cert list copied from golang src/crypto/x509/root_unix.go foreach my $ca_bundle ( "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/Gentoo etc. "/etc/pki/tls/certs/ca-bundle.crt", # Fedora/RHEL "/etc/ssl/ca-bundle.pem", # OpenSUSE "/etc/openssl/certs/ca-certificates.crt", # NetBSD "/etc/ssl/cert.pem", # OpenBSD "/usr/local/share/certs/ca-root-nss.crt", # FreeBSD/DragonFly "/etc/pki/tls/cacert.pem", # OpenELEC "/etc/certs/ca-certificates.crt", # Solaris 11.2+ ) { return $ca_bundle if -e $ca_bundle; } die qq/Couldn't find a CA bundle with which to verify the SSL certificate.\n/ . qq/Try installing Mozilla::CA from CPAN\n/; } # for thread safety, we need to know thread id if threads are loaded sub _get_tid { no warnings 'reserved'; # for 'threads' return threads->can("tid") ? threads->tid : 0; } sub _ssl_args { my ($self, $host) = @_; my %ssl_args; # This test reimplements IO::Socket::SSL::can_client_sni(), which wasn't # added until IO::Socket::SSL 1.84 if ( Net::SSLeay::OPENSSL_VERSION_NUMBER() >= 0x01000000 ) { $ssl_args{SSL_hostname} = $host, # Sane SNI support } if ($self->{verify_SSL}) { $ssl_args{SSL_verifycn_scheme} = 'http'; # enable CN validation $ssl_args{SSL_verifycn_name} = $host; # set validation hostname $ssl_args{SSL_verify_mode} = 0x01; # enable cert validation $ssl_args{SSL_ca_file} = $self->_find_CA_file; } else { $ssl_args{SSL_verifycn_scheme} = 'none'; # disable CN validation $ssl_args{SSL_verify_mode} = 0x00; # disable cert validation } # user options override settings from verify_SSL for my $k ( keys %{$self->{SSL_options}} ) { $ssl_args{$k} = $self->{SSL_options}{$k} if $k =~ m/^SSL_/; } return \%ssl_args; } 1; } # --- END HTTP::Tiny { # --- BEGIN Try::Tiny package Try::Tiny; # git description: v0.30-11-g1b81d0a use 5.006; # ABSTRACT: Minimal try/catch with proper preservation of $@ our $VERSION = '0.31'; use strict; use warnings; BEGIN { use Exporter 5.57 'import'; our @EXPORT = our @EXPORT_OK = qw(try catch finally); use Carp; $Carp::Internal{+__PACKAGE__}++; if ($INC{'Sub/Util.pm'} && defined &Sub::Util::set_subname ) { *_subname = \&Sub::Util::set_subname; *_HAS_SUBNAME = sub {1}; } elsif( $INC{'Sub/Name.pm'} && eval { Sub::Name->VERSION(0.08) } ){ *_subname = \&Sub::Name::subname; *_HAS_SUBNAME = sub {1}; } else { *_HAS_SUBNAME = sub {0}; } } my %_finally_guards; # Need to prototype as @ not $$ because of the way Perl evaluates the prototype. # Keeping it at $$ means you only ever get 1 sub because we need to eval in a list # context & not a scalar one sub try (&;@) { my ( $try, @code_refs ) = @_; # we need to save this here, the eval block will be in scalar context due # to $failed my $wantarray = wantarray; # work around perl bug by explicitly initializing these, due to the likelyhood # this will be used in global destruction (perl rt#119311) my ( $catch, @finally ) = (); # find labeled blocks in the argument list. # catch and finally tag the blocks by blessing a scalar reference to them. foreach my $code_ref (@code_refs) { if ( ref($code_ref) eq 'Try::Tiny::Catch' ) { croak 'A try() may not be followed by multiple catch() blocks' if $catch; $catch = ${$code_ref}; } elsif ( ref($code_ref) eq 'Try::Tiny::Finally' ) { push @finally, ${$code_ref}; } else { croak( 'try() encountered an unexpected argument (' . ( defined $code_ref ? $code_ref : 'undef' ) . ') - perhaps a missing semi-colon before or' ); } } # FIXME consider using local $SIG{__DIE__} to accumulate all errors. It's # not perfect, but we could provide a list of additional errors for # $catch->(); # name the blocks if we have Sub::Name installed _subname(caller().'::try {...} ' => $try) if _HAS_SUBNAME; # set up scope guards to invoke the finally blocks at the end. # this should really be a function scope lexical variable instead of # file scope + local but that causes issues with perls < 5.20 due to # perl rt#119311 local $_finally_guards{guards} = [ map Try::Tiny::ScopeGuard->_new($_), @finally ]; # save the value of $@ so we can set $@ back to it in the beginning of the eval # and restore $@ after the eval finishes my $prev_error = $@; my ( @ret, $error ); # failed will be true if the eval dies, because 1 will not be returned # from the eval body my $failed = not eval { $@ = $prev_error; # evaluate the try block in the correct context if ( $wantarray ) { @ret = $try->(); } elsif ( defined $wantarray ) { $ret[0] = $try->(); } else { $try->(); }; return 1; # properly set $failed to false }; # preserve the current error and reset the original value of $@ $error = $@; $@ = $prev_error; # at this point $failed contains a true value if the eval died, even if some # destructor overwrote $@ as the eval was unwinding. if ( $failed ) { # pass $error to the finally blocks push @$_, $error for @{$_finally_guards{guards}}; # if we got an error, invoke the catch block. if ( $catch ) { # This works like given($error), but is backwards compatible and # sets $_ in the dynamic scope for the body of C<$catch> for ($error) { return $catch->($error); } # in case when() was used without an explicit return, the C # loop will be aborted and there's no useful return value } return; } else { # no failure, $@ is back to what it was, everything is fine return $wantarray ? @ret : $ret[0]; } } sub catch (&;@) { my ( $block, @rest ) = @_; croak 'Useless bare catch()' unless wantarray; _subname(caller().'::catch {...} ' => $block) if _HAS_SUBNAME; return ( bless(\$block, 'Try::Tiny::Catch'), @rest, ); } sub finally (&;@) { my ( $block, @rest ) = @_; croak 'Useless bare finally()' unless wantarray; _subname(caller().'::finally {...} ' => $block) if _HAS_SUBNAME; return ( bless(\$block, 'Try::Tiny::Finally'), @rest, ); } { package # hide from PAUSE Try::Tiny::ScopeGuard; use constant UNSTABLE_DOLLARAT => ("$]" < '5.013002') ? 1 : 0; sub _new { shift; bless [ @_ ]; } sub DESTROY { my ($code, @args) = @{ $_[0] }; local $@ if UNSTABLE_DOLLARAT; eval { $code->(@args); 1; } or do { warn "Execution of finally() block $code resulted in an exception, which " . '*CAN NOT BE PROPAGATED* due to fundamental limitations of Perl. ' . 'Your program will continue as if this event never took place. ' . "Original exception text follows:\n\n" . (defined $@ ? $@ : '$@ left undefined...') . "\n" ; } } } 1; } # --- END Try::Tiny { # --- BEGIN cPstrict package cPstrict; # cpanel - cPstrict.pm 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 use strict; use warnings; =pod This is importing the following to your namespace use strict; use warnings; use v5.30; use feature 'signatures'; no warnings 'experimental::signatures'; =cut sub import { # auto import strict and warnings to our caller warnings->import(); strict->import(); require feature; feature->import( ':5.30', 'signatures' ); warnings->unimport('experimental::signatures'); return; } 1; } # --- END cPstrict { # --- BEGIN Cpanel/OS/SysPerlBootstrap.pm package Cpanel::OS::SysPerlBootstrap; use strict; use warnings; use constant CACHE_FILE => '/var/cpanel/caches/Cpanel-OS'; use constant CACHE_FILE_CUSTOM => CACHE_FILE . '.custom'; sub get_os_info { my ($iknowwhatimdoing) = @_; die("Please use Cpanel::OS for your OS info needs") unless ( $iknowwhatimdoing && $iknowwhatimdoing eq 'DO NOT USE THIS CALL' ); my @os_info = _read_os_info_cache(); return @os_info if @os_info; my ( $distro, $major, $minor, $build ) = _get_os_without_cache('redhat_first'); if ( !defined $distro || !length $distro || !defined $major || !length $major || !defined $minor || !length $minor || !defined $build || !length $build ) { die sprintf( "Could not determine OS info (distro: %s, major: %s, minor: %s, build: %s)\n", $distro // '', $major // '', $minor // '', $build // '' ); } _cache_os_info( $^O, $distro, $major, $minor, $build ); return ( $^O, $distro, $major, $minor, $build ); } sub _get_os_without_cache { my ($redhat_first) = @_; my @os; if ($redhat_first) { # preserve existing behavior for Cpanel::OS @os = _read_redhat_release(); @os = _read_os_release() unless scalar @os; } else { @os = _read_os_release(); @os = _read_redhat_release() unless scalar @os; } return @os; } sub _read_os_info_cache { my $cache_mtime = ( lstat CACHE_FILE )[9] or return; my $custom_os = readlink CACHE_FILE_CUSTOM; if ( !$custom_os ) { my $os_rel_mtime = ( stat("/etc/os-release") )[9]; $os_rel_mtime //= ( stat("/etc/redhat-release") )[9]; # in the case of cloudlinux 6, we check against this instead return if ( defined($os_rel_mtime) && $cache_mtime <= $os_rel_mtime ); } return split /\|/, readlink(CACHE_FILE); } sub _read_os_release { return unless -e '/etc/os-release'; open( my $os_fh, "<", "/etc/os-release" ) or die "Could not open /etc/os-release for reading: $!\n"; my ( $distro, $ver, $ver_id ); while ( my $line = <$os_fh> ) { my ( $key, $value ) = split( qr/\s*=\s*/, $line, 2 ); chomp $value; $value =~ s/\s.+//; $value =~ s/"\z//; $value =~ s/^"//; if ( !$distro && $key eq "ID" ) { $distro = $value; } elsif ( !$ver_id && $key eq "VERSION_ID" ) { $ver_id = $value; } elsif ( !$ver && $key eq "VERSION" ) { $ver = $value; } last if defined $distro && length $distro && defined $ver && length $ver && defined $ver_id && length $ver_id; } close $os_fh; my ( $major, $minor, $build ) = split( qr/\./, $ver_id ); return unless $distro; # We have to at a minimum have a distro name. All hope is lost otherwise. unless ( defined $major && length $major && defined $minor && length $minor && defined $build && length $build ) { my ( $ver_major, $ver_minor, $ver_build ) = split( qr/\./, $ver ); $major //= $ver_major; $minor //= ( $ver_minor // 0 ); $build //= ( $ver_build // 0 ); } return ( $distro, $major, $minor, $build ); } sub _read_redhat_release { return unless -e '/etc/redhat-release'; open( my $cr_fh, "<", "/etc/redhat-release" ) or die "Could not open /etc/redhat-release for reading: $!\n"; my $line = <$cr_fh>; chomp $line; my ($distro) = $line =~ m/^(\w+)/i; $distro = lc($distro); $distro = 'rhel' if $distro eq 'red'; my ( $major, $minor, $build ) = $line =~ m{\b([0-9]+)\.([0-9]+)\.([0-9]+)}; if ( !defined $major || !length $major ) { ( $major, $minor ) = $line =~ m{\b([0-9]+)\.([0-9]+)}; } if ( !defined $major || !length $major ) { ($major) = $line =~ m{\b([0-9]+)}; } $minor //= 0; $build //= 0; return ( $distro, $major, $minor, $build ); } sub _cache_os_info { my ( $os, $distro, $major, $minor, $build ) = @_; $> == 0 or return; mkdir '/var/cpanel', 0711; mkdir '/var/cpanel/caches', 0711; local $!; unlink CACHE_FILE; symlink "$os|$distro|$major|$minor|$build", CACHE_FILE; return 1; } 1; } # --- END Cpanel/OS/SysPerlBootstrap.pm { # --- BEGIN Cpanel/ExceptionMessage.pm package Cpanel::ExceptionMessage; use strict; # use Cpanel::Exception (); *load_perl_module = \&Cpanel::Exception::load_perl_module; 1; } # --- END Cpanel/ExceptionMessage.pm { # --- BEGIN Cpanel/Locale/Utils/Fallback.pm package Cpanel::Locale::Utils::Fallback; use strict; use warnings; sub interpolate_variables { my ( $str, @maketext_opts ) = @_; my $c = 1; my %h = map { $c++, $_ } @maketext_opts; $str =~ s{(\[(?:[^_]+,)?_([0-9])+\])}{$h{$2}}g; return $str; } 1; } # --- END Cpanel/Locale/Utils/Fallback.pm { # --- BEGIN Cpanel/ExceptionMessage/Raw.pm package Cpanel::ExceptionMessage::Raw; use strict; use warnings; # use Cpanel::ExceptionMessage(); our @ISA; BEGIN { push @ISA, qw(Cpanel::ExceptionMessage); } # use Cpanel::Locale::Utils::Fallback (); sub new { my ( $class, $str ) = @_; my $str_copy = $str; return bless( \$str_copy, $class ); } sub to_string { my ($self) = @_; return $$self; } sub get_language_tag { return 'en'; } BEGIN { *Cpanel::ExceptionMessage::Raw::convert_localized_to_raw = *Cpanel::Locale::Utils::Fallback::interpolate_variables; *Cpanel::ExceptionMessage::Raw::to_locale_string = *Cpanel::ExceptionMessage::Raw::to_string; *Cpanel::ExceptionMessage::Raw::to_en_string = *Cpanel::ExceptionMessage::Raw::to_string; } 1; } # --- END Cpanel/ExceptionMessage/Raw.pm { # --- BEGIN Cpanel/LoadModule/Utils.pm package Cpanel::LoadModule::Utils; use strict; use warnings; sub module_is_loaded { my $p = module_path( $_[0] ); return 0 unless defined $p; return defined $INC{$p} ? 1 : 0; } sub module_path { my ($module_name) = @_; if ( defined $module_name && length($module_name) ) { substr( $module_name, index( $module_name, '::' ), 2, '/' ) while index( $module_name, '::' ) > -1; $module_name .= '.pm' unless substr( $module_name, -3 ) eq '.pm'; } return $module_name; } sub is_valid_module_name { return $_[0] =~ m/\A[A-Za-z_]\w*(?:(?:'|::)\w+)*\z/ ? 1 : 0; } 1; } # --- END Cpanel/LoadModule/Utils.pm { # --- BEGIN Cpanel/ScalarUtil.pm package Cpanel::ScalarUtil; use strict; use warnings; sub blessed { return ref( $_[0] ) && UNIVERSAL::isa( $_[0], 'UNIVERSAL' ) || undef; } 1; } # --- END Cpanel/ScalarUtil.pm { # --- BEGIN Cpanel/Exception/CORE.pm package Cpanel::Exception::CORE; 1; package Cpanel::Exception; use strict; BEGIN { $INC{'Cpanel/Exception.pm'} = '__BYPASSED__'; } our $_SUPPRESS_STACK_TRACES = 0; our $_EXCEPTION_MODULE_PREFIX = 'Cpanel::Exception'; our $IN_EXCEPTION_CREATION = 0; our $_suppressed_msg = '__STACK_TRACE_SUPPRESSED__YOU_SHOULD_NEVER_SEE_THIS_MESSAGE__'; my $PACKAGE = 'Cpanel::Exception'; my $locale; my @ID_CHARS = qw( a b c d e f g h j k m n p q r s t u v w x y z 2 3 4 5 6 7 8 9 ); my $ID_LENGTH = 6; # use Cpanel::ExceptionMessage::Raw (); # use Cpanel::LoadModule::Utils (); use constant _TRUE => 1; use overload ( '""' => \&__spew, bool => \&_TRUE, fallback => 1, ); BEGIN { die "Cannot compile Cpanel::Exception::CORE" if $INC{'B/C.pm'} && $0 !~ m{cpkeyclt|cpsrvd\.so|t/large}; } sub _init { return 1 } # legacy sub create { my ( $exception_type, @args ) = @_; _init(); if ($IN_EXCEPTION_CREATION) { _load_cpanel_carp(); die 'Cpanel::Carp'->can('safe_longmess')->("Attempted to create a “$exception_type” exception with arguments “@args” while creating exception “$IN_EXCEPTION_CREATION->[0]” with arguments “@{$IN_EXCEPTION_CREATION->[1]}”."); } local $IN_EXCEPTION_CREATION = [ $exception_type, \@args ]; if ( $exception_type !~ m/\A[A-Za-z0-9_]+(?:\:\:[A-Za-z0-9_]+)*\z/ ) { die "Invalid exception type: $exception_type"; } my $perl_class; if ( $exception_type eq __PACKAGE__ ) { $perl_class = $exception_type; } else { $perl_class = "${_EXCEPTION_MODULE_PREFIX}::$exception_type"; } _load_perl_module($perl_class) unless $perl_class->can('new'); if ( $args[0] && ref $args[0] eq 'ARRAY' && scalar @{ $args[0] } > 1 ) { $args[0] = { @{ $args[0] } }; } return $perl_class->new(@args); } sub create_raw { my ( $class, $msg, @extra_args ) = @_; _init(); my $msg_obj = 'Cpanel::ExceptionMessage::Raw'->new($msg); if ( $class =~ m<\A(?:\Q${_EXCEPTION_MODULE_PREFIX}::\E)?Collection\z> ) { die "Use create('Collection', ..) to create a Cpanel::Exception::Collection object."; } return create( $class, $msg_obj, @extra_args ); } sub _load_perl_module { my ($module) = @_; local ( $!, $@ ); if ( !defined $module ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module requires a module name.") ); } return 1 if Cpanel::LoadModule::Utils::module_is_loaded($module); my $module_name = $module; $module_name =~ s{\.pm$}{}; if ( !Cpanel::LoadModule::Utils::is_valid_module_name($module_name) ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module requires a valid module name: '$module_name'.") ); } { eval qq{use $module (); 1 } or die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module cannot load '$module_name': $@") ) } return 1; } sub new { my ( $class, @args ) = @_; @args = grep { defined } @args; my $self = {}; bless $self, $class; if ( ref $args[-1] eq 'HASH' ) { $self->{'_metadata'} = pop @args; } if ( defined $self->{'_metadata'}->{'longmess'} ) { $self->{'_longmess'} = &{ $self->{'_metadata'}->{'longmess'} }($self) if $self->{'_metadata'}->{'longmess'}; } elsif ($_SUPPRESS_STACK_TRACES) { $self->{'_longmess'} = $_suppressed_msg; } else { if ( !$INC{'Carp.pm'} ) { _load_carp(); } $self->{'_longmess'} = scalar do { local $Carp::CarpInternal{'Cpanel::Exception'} = 1; local $Carp::CarpInternal{$class} = 1; 'Carp'->can('longmess')->(); }; } _init(); $self->{'_auxiliaries'} = []; if ( UNIVERSAL::isa( $args[0], 'Cpanel::ExceptionMessage' ) ) { $self->{'_message'} = shift @args; } else { my @mt_args; if ( @args && !ref $args[0] ) { @mt_args = ( shift @args ); if ( ref $args[0] eq 'ARRAY' ) { push @mt_args, @{ $args[0] }; } } else { $self->{'_orig_mt_args'} = $args[0]; my $phrase = $self->_default_phrase( $args[0] ); if ($phrase) { if ( ref $phrase ) { @mt_args = $phrase->to_list(); } else { $self->{'_message'} = Cpanel::ExceptionMessage::Raw->new($phrase); return $self; } } } if ( my @extras = grep { !ref } @args ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("Extra scalar(s) passed to $PACKAGE! (@extras)") ); } if ( !length $mt_args[0] ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("No args passed to $PACKAGE constructor!") ); } $self->{'_mt_args'} = \@mt_args; } return $self; } sub get_string { my ( $exc, $no_id_yn ) = @_; return get_string_no_id($exc) if $no_id_yn; return _get_string( $exc, 'to_string' ); } sub get_string_no_id { my ($exc) = @_; return _get_string( $exc, 'to_string_no_id' ); } sub _get_string { my ( $exc, $cp_exc_stringifier_name ) = @_; return $exc if !ref $exc; { local $@; my $ret = eval { $exc->$cp_exc_stringifier_name() }; return $ret if defined $ret && !$@ && !ref $ret; } if ( ref $exc eq 'HASH' && $exc->{'message'} ) { return $exc->{'message'}; } if ( $INC{'Cpanel/YAML.pm'} ) { local $@; my $ret = eval { 'Cpanel::YAML'->can('Dump')->($exc); }; return $ret if defined $ret && !$@; } if ( $INC{'Cpanel/JSON.pm'} ) { local $@; my $ret = eval { 'Cpanel::JSON'->can('Dump')->($exc); }; return $ret if defined $ret && !$@; } return $exc; } sub _create_id { srand(); return join( q<>, map { $ID_CHARS[ int rand( 0 + @ID_CHARS ) ]; } ( 1 .. $ID_LENGTH ), ); } sub get_stack_trace_suppressor { return Cpanel::Exception::_StackTraceSuppression->new(); } sub set_id { my ( $self, $new_id ) = @_; $self->{'_id'} = $new_id; return $self; } sub id { my ($self) = @_; return $self->{'_id'} ||= _create_id(); } sub set { my ( $self, $key ) = @_; $self->{'_metadata'}{$key} = $_[2]; if ( exists $self->{'_orig_mt_args'} ) { my $phrase = $self->_default_phrase( $self->{'_orig_mt_args'} ); if ($phrase) { if ( ref $phrase ) { $self->{'_mt_args'} = [ $phrase->to_list() ]; undef $self->{'_message'}; } else { $self->{'_message'} = Cpanel::ExceptionMessage::Raw->new($phrase); } } } return $self; } sub get { my ( $self, $key ) = @_; my $v = $self->{'_metadata'}{$key}; if ( my $reftype = ref $v ) { local $@; if ( $reftype eq 'HASH' ) { $v = { %{$v} }; # shallow copy } elsif ( $reftype eq 'ARRAY' ) { $v = [ @{$v} ]; # shallow copy } elsif ( $reftype eq 'SCALAR' ) { $v = \${$v}; # shallow copy } else { local ( $@, $! ); require Cpanel::ScalarUtil; if ( $reftype ne 'GLOB' && !Cpanel::ScalarUtil::blessed($v) ) { warn if !eval { _load_perl_module('Clone') if !$INC{'Clone.pm'}; $v = 'Clone'->can('clone')->($v); }; } } } return $v; } sub get_all_metadata { my $self = shift; my %metadata_copy; for my $key ( keys %{ $self->{'_metadata'} } ) { $metadata_copy{$key} = $self->get($key); } return \%metadata_copy; } my $loaded_LocaleString; sub _require_LocaleString { return $loaded_LocaleString ||= do { local $@; eval 'require Cpanel::LocaleString; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) - # PPI NO PARSE - load on demand 1; }; } my $loaded_ExceptionMessage_Locale; sub _require_ExceptionMessage_Locale { return $loaded_ExceptionMessage_Locale ||= do { local $@; eval 'require Cpanel::ExceptionMessage::Locale; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) - # PPI NO PARSE - load on demand 1; }; } sub _default_phrase { _require_LocaleString(); return 'Cpanel::LocaleString'->new( 'An unknown error in the “[_1]” package has occurred.', scalar ref $_[0] ); # PPI NO PARSE - loaded above } sub longmess { my ($self) = @_; return '' if $self->{'_longmess'} eq $_suppressed_msg; _load_cpanel_carp() if !$INC{'Cpanel/Carp.pm'}; return Cpanel::Carp::sanitize_longmess( $self->{'_longmess'} ); } sub to_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_string_no_id() ); } sub to_string_no_id { my ($self) = @_; my $string = $self->to_locale_string_no_id(); if ( $self->_message()->get_language_tag() ne 'en' ) { my $en_string = $self->to_en_string_no_id(); $string .= "\n$en_string" if ( $en_string ne $string ); } return $string; } sub _apply_id_prefix { my ( $id, $msg ) = @_; return sprintf "(XID %s) %s", $id, $msg; } sub to_en_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_en_string_no_id() ); } sub to_en_string_no_id { my ($self) = @_; return $self->_message()->to_en_string() . $self->_stringify_auxiliaries('to_en_string'); } sub to_locale_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_locale_string_no_id() ); } sub to_locale_string_no_id { my ($self) = @_; return $self->_message()->to_locale_string() . $self->_stringify_auxiliaries('to_locale_string'); } sub add_auxiliary_exception { my ( $self, $aux ) = @_; return push @{ $self->{'_auxiliaries'} }, $aux; } sub get_auxiliary_exceptions { my ($self) = @_; die 'List context only!' if !wantarray; #Can’t use Cpanel::Context return @{ $self->{'_auxiliaries'} }; } sub __spew { my ($self) = @_; return $self->_spew(); } sub _spew { my ($self) = @_; return ref($self) . '/' . join "\n", $self->to_string() || '', $self->longmess() || (); } sub _stringify_auxiliaries { my ( $self, $method ) = @_; my @lines; if ( @{ $self->{'_auxiliaries'} } ) { local $@; _require_LocaleString(); my $intro = 'Cpanel::LocaleString'->new( 'The following additional [numerate,_1,error,errors] occurred:', 0 + @{ $self->{'_auxiliaries'} } ); # PPI NO PARSE - required above if ( $method eq 'to_locale_string' ) { push @lines, _locale()->makevar( $intro->to_list() ); } elsif ( $method eq 'to_en_string' ) { push @lines, _locale()->makethis_base( $intro->to_list() ); } else { die "Invalid method: $method"; } push @lines, map { UNIVERSAL::isa( $_, __PACKAGE__ ) ? $_->$method() : $_ } @{ $self->{'_auxiliaries'} }; } return join q<>, map { "\n$_" } @lines; } *TO_JSON = \&to_string; sub _locale { return $locale ||= do { local $@; eval 'require Cpanel::Locale; 1;' or die $@; 'Cpanel::Locale'->get_handle(); # hide from perlcc }; } sub _reset_locale { return undef $locale; } sub _load_carp { if ( !$INC{'Carp.pm'} ) { local $@; eval 'require Carp; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) -- hide from perlcc } return; } sub _load_cpanel_carp { if ( !$INC{'Cpanel/Carp.pm'} ) { local $@; eval 'require Cpanel::Carp; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) -- hide from perlcc } return; } sub _message { my ($self) = @_; return $self->{'_message'} if $self->{'_message'}; local $!; if ($Cpanel::Exception::LOCALIZE_STRINGS) { # the default _require_ExceptionMessage_Locale(); return ( $self->{'_message'} ||= 'Cpanel::ExceptionMessage::Locale'->new( @{ $self->{'_mt_args'} } ) ); # PPI NO PARSE - required above } return ( $self->{'_message'} ||= Cpanel::ExceptionMessage::Raw->new( Cpanel::ExceptionMessage::Raw::convert_localized_to_raw( @{ $self->{'_mt_args'} } ) ) ); } package Cpanel::Exception::_StackTraceSuppression; sub new { my ($class) = @_; $Cpanel::Exception::_SUPPRESS_STACK_TRACES++; return bless [], $class; } sub DESTROY { $Cpanel::Exception::_SUPPRESS_STACK_TRACES--; return; } 1; } # --- END Cpanel/Exception/CORE.pm { # --- BEGIN Cpanel/LoadModule.pm package Cpanel::LoadModule; use strict; # use Cpanel::Exception (); # use Cpanel::LoadModule::Utils (); my $logger; my $has_perl_dir = 0; sub _logger_warn { my ( $msg, $fail_ok ) = @_; return if $fail_ok && $ENV{'CPANEL_BASE_INSTALL'} && index( $^X, '/usr/local/cpanel' ) == -1; if ( $INC{'Cpanel/Logger.pm'} ) { $logger ||= 'Cpanel::Logger'->new(); $logger->warn($msg); } return warn $msg; } sub _reset_has_perl_dir { $has_perl_dir = 0; return; } sub load_perl_module { ## no critic qw(Subroutines::RequireArgUnpacking) if ( -1 != index( $_[0], q<'> ) ) { die Cpanel::Exception::create_raw( 'InvalidParameter', "Module names with single-quotes are prohibited. ($_[0])" ); } return $_[0] if Cpanel::LoadModule::Utils::module_is_loaded( $_[0] ); my ( $mod, @LIST ) = @_; local ( $!, $@ ); if ( !is_valid_module_name($mod) ) { die Cpanel::Exception::create( 'InvalidParameter', '“[_1]” is not a valid name for a Perl module.', [$mod] ); } my $args_str; if (@LIST) { $args_str = join ',', map { die "Only scalar arguments allowed in LIST! (@LIST)" if ref; _single_quote($_); } @LIST; } else { $args_str = q<>; } eval "use $mod ($args_str);"; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) if ($@) { die Cpanel::Exception::create( 'ModuleLoadError', [ module => $mod, error => $@ ] ); } return $mod; } *module_is_loaded = *Cpanel::LoadModule::Utils::module_is_loaded; *is_valid_module_name = *Cpanel::LoadModule::Utils::is_valid_module_name; sub loadmodule { return 1 if cpanel_namespace_module_is_loaded( $_[0] ); return _modloader( $_[0] ); } sub lazy_load_module { my $mod = shift; my $mod_path = $mod; $mod_path =~ s{::}{/}g; if ( exists $INC{ $mod_path . '.pm' } ) { return; } if ( !is_valid_module_name($mod) ) { _logger_warn("Cpanel::LoadModule: Invalid module name ($mod)"); return; } eval "use $mod ();"; if ($@) { delete $INC{ $mod_path . '.pm' }; _logger_warn( "Cpanel::LoadModule:: Failed to load module $mod - $@", 1 ); return; } return 1; } sub cpanel_namespace_module_is_loaded { my ($modpart) = @_; $modpart =~ s{::}{/}g; return exists $INC{"Cpanel/$modpart.pm"} ? 1 : 0; } sub _modloader { my $module = shift; if ( !$module ) { _logger_warn("Empty module name passed to modloader"); return; } if ( !is_valid_module_name($module) ) { _logger_warn("Invalid module name ($module) passed to modloader"); return; } eval qq[ use Cpanel::${module}; Cpanel::${module}::${module}_init() if "Cpanel::${module}"->can("${module}_init"); ]; # PPI USE OK - This looks like usage of the Cpanel module and it's not. if ($@) { _logger_warn("Error loading module $module - $@"); return; } return 1; } sub _single_quote { local ($_) = $_[0]; s/([\\'])/\\$1/g; return qq('$_'); } 1; } # --- END Cpanel/LoadModule.pm { # --- BEGIN Cpanel/OS.pm package Cpanel::OS; use cPstrict; use Carp (); # use Cpanel::OS::SysPerlBootstrap (); # use Cpanel::LoadModule (); our $VERSION = '2.0'; sub _new_instance ( $os, $distro, $major, $minor, $build ) { my $distro_class = 'Cpanel::OS::' . ucfirst($distro) . $major; if ( !eval "require $distro_class; 1" ) { ## no critic qw(ProhibitStringyEval) -- This is how we do a runtime load here. require Cpanel::OS::Linux; # PPI USE OK -- used just after $distro_class = q[Cpanel::OS::Linux]; # unsupported distro $os //= q[Unknown]; } my $self = bless { os => $os, distro => $distro, major => $major, minor => $minor, build => $build, }, $distro_class; return $self; } my $instance; sub clear_cache { $INC{'Test/Cpanel/Policy.pm'} or Carp::croak("This interface is only for unit testing"); undef $instance; return; } sub clear_cache_after_cloudlinux_update { undef $instance; return; } sub _instance { ## no critic(RequireArgUnpacking) - Most of the time we do not need to process args. return $instance if $instance; Carp::croak("Cpanel::OS may not be called during cPanel binary compilation") if $INC{'B/C.pm'}; my ( $os, $distro, $major, $minor, $build ) = @_; if ( !length $build ) { ( $os, $distro, $major, $minor, $build ) = Cpanel::OS::SysPerlBootstrap::get_os_info('DO NOT USE THIS CALL'); } return $instance = _new_instance( $os, $distro, $major, $minor, $build ); } sub flush_disk_caches { local $!; return 0 if readlink Cpanel::OS::SysPerlBootstrap::CACHE_FILE_CUSTOM; unlink Cpanel::OS::SysPerlBootstrap::CACHE_FILE; return 1; } sub distro { return _instance()->{'distro'} } sub major { return _instance()->{'major'} } sub minor { return _instance()->{'minor'} } sub build { return _instance()->{'build'} } my %methods; BEGIN { %methods = map { $_ => 0 } ( 'is_supported', # This OS is supported by cPanel and not a virtual class (these exist!). 'eol_advice', # Additional information provided to updatenow blockers when the customer tries to upgrade on a removed distro. 'support_needs_minor_at_least_at', # miniimum minor version we support for this distro (optional 'is_experimental', # Defines if this distro is in experimental state or not. 'experimental_url', # Provides a link to information about the experimental state if it's currently that way. 'arch', # Somewhat of an unnecessary variable as all of our distros are set to x86_64. 'service_manager', # Does this distro use systemd or initd to manage services? 'is_systemd', # Easy boolean helper we use in most places to determine if the local system uses systemd. 'base_distro', # What is the root distro this distro is derived from? rhel/debian 'pretty_distro', # What is the preferred stylization of the distro name when being displayed for a user? 'display_name', # Example: Centos v7.9.2009 'display_name_lite', # Example: centos 7 'cpanalytics_cpos', # How should we present data regarding the OS to Google Analytics? 'binary_sync_source', # Provides the string corresponding to the binary sync source directory. 'nobody', # name of the user used for nobody 'nogroup', # name of the group used for nobody / nogroup 'sudoers', # name of the group used for sudo (by sudoers) 'has_wheel_group', # flag for whether wheel group is needed by sudo 'default_uid_min', # default value from /etc/login.defs 'default_gid_min', # default value from /etc/login.defs 'default_sys_uid_min', # default value from /etc/login.defs 'default_sys_gid_min', # default value from /etc/login.defs 'has_tcp_wrappers', # The distro supports TPC wrappers. 'setup_tz_method', # what method to use to setup a timezone 'is_cloudlinux', # is it a CloudLinux based distro? boolean 'can_be_elevated', # ELevate supports current OS as a source 'can_elevate_to', # ELevate can directly convert the current OS to these other OSes listed in the arrayref 'rsyslog_triggered_by_socket', # Is rsyslog triggered by syslog.socket 'has_quota_support_for_xfs', # Does this distro support xfs quota? 'program_to_apply_kernel_args', # What program do we need to run to ensure that kernels are booted with updated args? 'has_cloudlinux_enhanced_quotas', # Cloud linux does fancy things with quota we need to know about. 'who_wins_if_soft_gt_hard', # If we try to set a soft quota higher than a hard quota, which value wins? 'quota_packages_conditional', # Hashref of needed kernel package dependencies not encoded in the upstream distro in order for quotas to work 'bin_grub_mkconfig', # path to sbin/grub2-mkconfig 'bin_needs_restarting', # path to needs-restarting binary 'outdated_services_check', # which method to use to check outdated services? 'outdated_processes_check', # which method to use to check outdated processes? 'check_reboot_method', # which method to use to check if we need to reboot 'dns_supported', # Provides a list of dns servers supported on this platform. 'dns_named_basedir', # The path to the bind nameserver files. 'dns_named_conf', # /etc/named.conf 'dns_named_log', # What dir named logs are stored (/var/log/named) 'ssh_supported_algorithms', # list of supported ssh algo [ordered by preference] 'openssl_minimum_supported_version', # minimum openssl version to run 'openssl_escapes_subjects', # On generated certs, openssl started escaping subject lines at some point... 'unsupported_db_versions', # What DB versions does this distro NOT support. 'db_package_manager_key_params', # Hashref describing what to do to ensure keys are in place for DBMSes installed from a 3rdparty repo 'mysql_versions_use_repo_template', # Which MySQL versions use mysql_repo_template. 'mariadb_versions_use_repo_template', # Which MariaDB versions use mariadb_repo_template. 'mysql_repo_template', # What goes in the repo file to download mysql packages. 'mariadb_repo_template', # What goes in the repo file to download mariadb packages. 'mariadb_minimum_supported_version', # Minimum version of MariaDB supported 'mysql_community_packages', # Which MySQL packages need to be installed on this distro? 'mysql_dependencies', # Which distro packages need to be installed for MySQL to be happy? 'mysql_incompatible', # Which mysql packages need to be blocked as incompatible with cPanel packages on this distro? 'mysql_default_version', # Default MySQL version to use 'supports_postgresql', # Do we support PostgreSQL on this distro? 'postgresql_minimum_supported_version', # What is the minimum versions of PostgreSQL supported on this distro? 'postgresql_packages', # What packages do we need to install? 'postgresql_service_aliases', # What aliases, if any, of the service name might we expect to find PostgreSQL using? 'postgresql_initdb_commands', # Which commands do we run to make PostgreSQL initialize its DB storage area? 'ea4_install_repo_from_package', # Does a RPM provide the EA4 repo or do we download the repo file? 'ea4_from_pkg_url', # If we're installing the repo from RPM, where do we get it from? 'ea4_from_pkg_reponame', # ... And what will the repo be called when we install it? 'ea4_from_custom_repo_url', # Where can we download the repo file from? (needs to be over https) 'ea4_from_custom_repo_path', # ... And where should we put it when we download it? 'ea4tooling_all', # LIST - What are the packages to install which provide ea4 tooling on all server types? 'ea4tooling', # LIST - What ea4 tooling packages do we install on full cpanel servers? 'ea4tooling_dnsonly', # LIST - What additional packages do we need for dnsonly? << FACT CHECK 'ea4_modern_openssl', # Which openssl should be used on this platform to get the L&G Stuff? EA4 provides one in the event the distro's version is insufficient. 'ea4_testing_yum_repo', # undef or string with the name of the yum repo to enable 'ea4_conflicting_apache_distro_packages', # Conflicting packages that Cpanel::EA4::MigrateBlocker checks for 'ea4_install_from_profile_enforce_packages', 'package_manager', # which package manager does the distro use? ( yum/dnf/apt) 'package_manager_module', # Cpanel::whatever::$package_manager_module::... ( Yum or Apt ) 'package_repositories', # what additional repos need to be installed and enabled to install needed software? 'package_release_distro_tag', # the postfix extension used for the packages: ~el6, ~el7, ~el8, ~el9, ~u20, ~u22 'system_exclude_rules', # On yum based systems, how what will we block the main distro from installing 'kernel_package_pattern', # What are the kernal packages named so we can sometimes block them when updating. 'check_kernel_version_method', # What method to use to check the kernel version 'stock_kernel_version_regex', # Regular expression used to determine whether the version string returned for the kernel matches what the distro would return with a stock kernel. 'kernel_supports_fs_protected_regular', # Does fs.protected_regular a valid settings 'packages_required', # Which packages should /scripts/sysup assure are present on this system? ( provided during fresh install ) 'packages_supplemental', # Which packages should /scripts/sysup assure are present on this system? ( provided AFTER fresh install ) 'packages_supplemental_epel', # Packages we want to install from epel if it is available to us. 'is_apt_based', # Does this system use apt (and therefore deb packages) for package management? 'is_yum_based', # Does this system use a yum or a yum derivative (dnf) 'is_rpm_based', # Does this system do its package management with rpms? 'system_package_providing_perl', # Name of the package providing system Perl 'retry_rpm_cmd_no_tty_hack', # Hack: retry RPM comand when no TTY 'can_clean_plugins_repo', # Can we clean the 'plugins' repo 'repos_requires_dump_flag', # shiykd we use '--dump' when enabling a repo 'rpm_versions_system', # Which rpm_versions_system is currently used 'packages_arch', # Default architecture used by the rpm.versions system 'package_ImageMagick_Devel', # Name of the imagemagick devel package 'package_MySQL_Shell', # Name of the mysql-shell package (installed on demand) 'package_crond', # Name of the package providing the cron daemon 'plugins_repo_url', # URL to .repo / .list for cpanel-plugins 'repo_suffix', # Suffix for repo files, such as .repo or .list 'repo_dir', # Local directory path where system repo config files are stored for the package manager 'package_descriptions', # Description fields used in manage plugins 'supports_cpanel_cloud_edition', # Is cPCloud supported by this distro? 'supports_cpaddons', # Are cpaddons supported by this distro? 'supports_kernelcare', # Is Kernel Care available for this distro? 'supports_kernelcare_free', # Is Kernel Care Free available for this distro? << FACT CHECK (Note: This check implicitly ensures the system is not running CloudLinux) 'supports_3rdparty_wpt', # Is Wordpress Toolkit supported on this platform? 'supports_plugins_repo', # Is Cpanel::Plugins::Repo supported on this platform? 'supports_or_can_become_cloudlinux', # Does the system can become/or is CloudLinux? 'can_become_cloudlinux', # Can the system become CloudLinux? 'supports_imunify_av', # Can install Imunify AV 'supports_imunify_av_plus', # Can install Imunify AV Plus 'supports_imunify_360', # Can install Imunify AV 360 'jetbackup_repo_pkg', # URL to the package we install to set up the JetBackup repo ( somewhere on http://repo.jetlicense.com/ ) 'supports_letsencrypt_v2', 'supports_cpanel_analytics', 'security_service', # What security service the distro is using? apparmor or selinux 'firewall', # Which firewall is this distro using? (iptables / firewalld_nftables / ufw_iptables) 'firewall_module', # Which firewall module is used to manage it? (IpTables / NFTables) 'networking', # Not sure what this is for. Nothing uses it. ( networkscripts / netplan ) << FACT CHECK 'iptables_ipv4_savefile', # Where to store iptables rules for IPv4 'iptables_ipv6_savefile', # Where to store iptables rules for IPv6 'sysconfig_network', # sysconfig networ file to use, undef when unused. 'supports_hostaccess', # Does the system provide support for /etc/hosts.allow, etc.? 'supports_inetd', # Does the system provide support for inetd? 'supports_syslogd', # Does the system provide support for syslogd? 'install_gcc_from_slc6_devtoolset', # Do we need to install a custom gcc for building? 'check_ntpd_pid_method', # Method used to check the ntp daemon pid 'syslog_service_name', # Name of the service that handles syslog data, such as syslog, rsyslog, rsyslogd, etc, eg: `/usr/bin/systemctl show -p MainPID rsyslog.service` 'cron_bin_path', # Path to the cron daemon 'systemd_service_name_map', # Map of service names to possible counterparts, such as crond -> cron 'prelink_config_path', # Where do the control knobs for prelinking live? 'pam_file_controlling_crypt_algo', # Which file in /etc/pam.d manages the algorithm used to generate the passwd hash written to /etc/shadow for a user? 'user_crontab_dir', # Path to directory where user crontabs are stored by the crontab binary 'needs_els', # Does this server require the ELS product 'maillog_path', # Path to the mail.* syslog output as defined by the distro 'nat_server_buffer_connections', # Number of connections_required to trigger a test failure in simultaneous connections for NAT detection. ); } sub supported_methods { return sort keys %methods; ##no critic qw( ProhibitReturnSort ) - this will always be a list. } our $AUTOLOAD; # keep 'use strict' happy sub AUTOLOAD { ## no critic(RequireArgUnpacking) - Most of the time we do not need to process args. my $sub = $AUTOLOAD; $sub =~ s/.*:://; exists $methods{$sub} or Carp::croak("$sub is not a supported data variable for Cpanel::OS"); my $i = _instance(); my $can = $i->can($sub) or Carp::croak( ref($i) . " does not implement $sub" ); return $can->( $i, @_ ); } sub list_contains_value ( $key, $value ) { my $array_ref = _instance()->$key; ref $array_ref eq 'ARRAY' or Carp::croak("$key is not a list!"); if ( !defined $value ) { return ( grep { !defined $_ } @$array_ref ) ? 1 : 0; } return ( grep { $value eq $_ } @$array_ref ) ? 1 : 0; } sub DESTROY { } # This is a must for autoload modules. sub assert_unreachable_on_ubuntu ( $msg = "Ubuntu reached somewhere it shouldn't!" ) { Carp::croak($msg) if Cpanel::OS::base_distro() eq "debian"; return; } sub lookup_pretty_distro ($target) { require Cpanel::OS::All; my ( $name, $major ) = ( $target =~ m/^([A-Za-z]+)([0-9]+)$/ ); return if !$name || !$major; return unless grep { $_->[0] eq $name && $_->[1] == $major } Cpanel::OS::All::supported_distros(); my $module = "Cpanel::OS::$target"; Cpanel::LoadModule::load_perl_module($module); my $pretty_name = $module->pretty_distro; return "$pretty_name $major"; } 1; } # --- END Cpanel/OS.pm { # --- BEGIN Cpanel/OS/Linux.pm package Cpanel::OS::Linux; use cPstrict; use Carp (); # use Cpanel::OS (); # use Cpanel::OS(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS); } use constant eol_advice => ''; use constant is_supported => 0; # Base OS class for all platforms we currently support. use constant support_needs_minor_at_least_at => undef; # by default no restriction on minor version use constant is_experimental => 0; # By default, no distros are experimental. use constant experimental_url => ''; # ... and so no URL to document it. use constant dns_supported => [qw{ powerdns }]; # All platforms support powerdns as it is our default. use constant supports_3rdparty_wpt => 1; # We support WPT on all platforms ATM. use constant supports_plugins_repo => 1; use constant supports_imunify_av => 0; use constant supports_imunify_av_plus => 0; use constant supports_imunify_360 => 0; use constant supports_letsencrypt_v2 => 1; use constant supports_cpanel_analytics => 1; use constant supports_cpanel_cloud_edition => 0; use constant can_elevate_to => []; use constant setup_tz_method => q[timedatectl]; use constant nobody => q[nobody]; use constant nogroup => q[nobody]; use constant default_uid_min => 1_000; use constant default_gid_min => 1_000; use constant default_sys_uid_min => 201; use constant default_sys_gid_min => 201; use constant dns_named_basedir => '/var/named'; use constant dns_named_conf => '/etc/named.conf'; use constant dns_named_log => '/var/log/named'; use constant service_manager => 'systemd'; use constant arch => 'x86_64'; use constant maillog_path => '/var/log/maillog'; use constant supports_hostaccess => 1; use constant rsyslog_triggered_by_socket => 0; use constant packages_supplemental_epel => []; use constant quota_packages_conditional => {}; use constant unsupported_db_versions => []; use constant mariadb_repo_template => ''; # Not provided on ubuntu for instance. use constant mariadb_minimum_supported_version => '10.0'; use constant mysql_default_version => '5.7'; use constant postgresql_minimum_supported_version => undef; use constant postgresql_packages => []; use constant postgresql_service_aliases => []; use constant postgresql_initdb_commands => []; use constant openssl_minimum_supported_version => '1.0.2e'; use constant ea4_install_repo_from_package => 0; use constant ea4_from_pkg_url => undef; use constant ea4_from_pkg_reponame => undef; use constant ea4_from_custom_repo_url => undef; use constant ea4_from_custom_repo_path => undef; use constant ea4_testing_yum_repo => undef; use constant ea4tooling_all => [qw{ ea-cpanel-tools ea-profiles-cpanel }]; sub ea4tooling_dnsonly { return Carp::confess('unimplemented for this distro') } sub ea4tooling { return Carp::confess('unimplemented for this distro') } sub system_exclude_rules { return Carp::confess('unimplemented for this distro') } sub base_distro { return Carp::confess('unimplemented for this distro') } sub kernel_package_pattern { return Carp::confess('unimplemented for this distro') } use constant check_kernel_version_method => q[grubby]; use constant stock_kernel_version_regex => undef; sub mysql_versions_use_repo_template { return Carp::confess('unimplemented for this distro') } sub mariadb_versions_use_repo_template { return Carp::confess('unimplemented for this distro') } sub binary_sync_source { return Carp::confess('unimplemented for this distro') } use constant package_repositories => []; use constant system_package_providing_perl => 'perl'; use constant rpm_versions_system => 'centos'; use constant packages_arch => 'x86_64'; use constant package_MySQL_Shell => q[mysql-shell]; use constant package_crond => undef; use constant retry_rpm_cmd_no_tty_hack => 0; use constant check_ntpd_pid_method => 'pid_check_var_run_ntpd'; # how to check ntp daemon pid use constant prelink_config_path => undef; use constant pam_file_controlling_crypt_algo => undef; use constant iptables_ipv4_savefile => '/etc/sysconfig/iptables'; use constant iptables_ipv6_savefile => '/etc/sysconfig/ip6tables'; use constant sysconfig_network => q[/etc/sysconfig/network]; use constant bin_grub_mkconfig => q[/usr/sbin/grub2-mkconfig]; use constant ssh_supported_algorithms => [qw{ ed25519 ecdsa rsa }]; use constant bin_needs_restarting => q[/usr/local/cpanel/bin/needs-restarting-cpanel]; use constant outdated_services_check => q[default]; use constant outdated_processes_check => q[default]; use constant check_reboot_method => q[default]; use constant program_to_apply_kernel_args => undef; use constant security_service => 'selinux'; use constant supports_kernelcare => 0; use constant supports_kernelcare_free => 0; use constant supports_or_can_become_cloudlinux => 0; use constant can_become_cloudlinux => 0; use constant supports_inetd => 0; use constant supports_syslogd => 0; use constant install_gcc_from_slc6_devtoolset => 0; use constant supports_postgresql => 0; use constant openssl_escapes_subjects => 0; use constant has_cloudlinux_enhanced_quotas => 0; use constant ea4_install_from_profile_enforce_packages => 0; use constant repos_requires_dump_flag => 0; use constant is_cloudlinux => 0; use constant can_be_elevated => 0; use constant kernel_supports_fs_protected_regular => 0; use constant needs_els => 0; use constant has_quota_support_for_xfs => 1; use constant is_systemd => 1; use constant has_tcp_wrappers => 1; use constant can_clean_plugins_repo => 1; use constant supports_cpaddons => 1; use constant pretty_distro => undef; sub display_name { return sprintf( "%s v%s.%s.%s", Cpanel::OS::pretty_distro() // '', Cpanel::OS::major() // '', Cpanel::OS::minor() // '', Cpanel::OS::build() // '' ); ## no critic(Cpanel::CpanelOS) internal usage } sub display_name_lite { return sprintf( "%s %s", lc( Cpanel::OS::distro() // '' ), Cpanel::OS::major() // '' ); ## no critic(Cpanel::CpanelOS) internal usage } sub cpanalytics_cpos { return sprintf( "%s %s.%s", uc( Cpanel::OS::distro() // '' ), Cpanel::OS::major() // '', Cpanel::OS::minor() // '' ); ## no critic(Cpanel::CpanelOS) internal usage } use constant nat_server_buffer_connections => 4; 1; } # --- END Cpanel/OS/Linux.pm { # --- BEGIN Cpanel/OS/Rhel.pm package Cpanel::OS::Rhel; use cPstrict; # use Cpanel::OS::Linux (); # ea4tooling_all # use Cpanel::OS::Linux(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Linux); } use constant is_supported => 0; # Base OS class for all Rhel derivatives. use constant pretty_distro => 'Red Hat Enterprise Linux'; use constant sudoers => 'wheel'; use constant has_wheel_group => 1; use constant dns_supported => [qw{ bind powerdns }]; use constant supports_kernelcare => 1; use constant supports_kernelcare_free => 1; use constant supports_or_can_become_cloudlinux => 1; use constant can_become_cloudlinux => 1; use constant supports_imunify_av => 1; use constant supports_imunify_av_plus => 1; use constant cron_bin_path => '/usr/sbin/crond'; use constant systemd_service_name_map => {}; use constant ea4_modern_openssl => '/opt/cpanel/ea-openssl11/bin/openssl'; use constant firewall => 'iptables'; use constant firewall_module => 'IpTables'; use constant networking => 'networkscripts'; use constant package_manager => 'yum'; use constant package_manager_module => 'Yum'; use constant base_distro => 'rhel'; use constant is_apt_based => 0; use constant is_yum_based => 1; use constant is_rpm_based => 1; use constant kernel_package_pattern => 'kernel'; use constant stock_kernel_version_regex => qr/\.(?:noarch|x86_64|i[3-6]86)$/; use constant program_to_apply_kernel_args => 'grub2-mkconfig'; use constant prelink_config_path => '/etc/sysconfig/prelink'; use constant pam_file_controlling_crypt_algo => 'system-auth'; use constant user_crontab_dir => '/var/spool/cron'; use constant ea4_from_custom_repo_url => 'https://securedownloads.cpanel.net/EA4/EA4.repo'; use constant ea4_from_custom_repo_path => '/etc/yum.repos.d/EA4.repo'; use constant ea4_conflicting_apache_distro_packages => [qw( httpd httpd-tools php-cli )]; use constant ea4tooling => [ 'yum-plugin-universal-hooks', Cpanel::OS::Linux->ea4tooling_all->@* ]; use constant ea4tooling_dnsonly => ['yum-plugin-universal-hooks']; use constant syslog_service_name => 'rsyslogd'; use constant jetbackup_repo_pkg => 'https://repo.jetlicense.com/centOS/jetapps-repo-latest.rpm'; use constant plugins_repo_url => 'https://securedownloads.cpanel.net/cpanel-plugins/0/cpanel-plugins.repo'; use constant repo_suffix => 'repo'; use constant repo_dir => '/etc/yum.repos.d'; use constant system_exclude_rules => { 'dovecot' => 'dovecot*', 'nsd' => 'nsd*', 'php' => 'php*', 'exim' => 'exim*', 'pure-ftpd' => 'pure-ftpd*', 'proftpd' => 'proftpd*', 'p0f' => 'p0f', 'filesystem' => 'filesystem', 'kernel' => 'kernel kernel-xen kernel-smp kernel-pae kernel-PAE kernel-SMP kernel-hugemem kernel-debug* kernel-core kernel-modules', 'kmod-' => 'kmod-[a-z]*', 'bind-chroot' => 'bind-chroot', }; use constant packages_supplemental => [ 'nfs-utils', qw{ ImageMagick autoconf automake bind-devel bison boost-serialization cairo e2fsprogs-devel expat-devel flex fontconfig freetype ftp gcc-c++ gd-devel gdbm-devel gettext-devel ghostscript giflib glib2 hunspell hunspell-en krb5-devel libX11-devel libXpm libXpm-devel libaio-devel libidn-devel libjpeg-turbo-devel libpng-devel libstdc++-devel libtiff-devel libtool libtool-ltdl libtool-ltdl-devel libwmf libxml2-devel libxslt-devel ncurses ncurses-devel nscd openssl-devel pango perl-CPAN perl-ExtUtils-MakeMaker perl-IO-Tty perl-Module-Build perl-YAML-Syck perl-core perl-devel perl-libwww-perl pixman python-devel python-tools quota-devel strace sysstat tcp_wrappers-devel traceroute urw-fonts zlib-devel } ]; use constant packages_supplemental_epel => [ qw{ perl-Expect perl-JSON-XS perl-Try-Tiny perl-local-lib } ]; use constant packages_required => [ qw{ aspell at bind bind-libs bind-utils binutils boost-program-options bzip2 compat-db coreutils cpio cpp crontabs curl db4 db4-devel e2fsprogs expat file gawk gcc gd gd-progs gdbm gettext glibc-devel gmp gnupg2 grubby gzip initscripts iptables iptables-ipv6 json-c kernel-headers lcms less libaio libevent libgcc libgomp libicu libidn libjpeg-turbo libpcap libpng libstdc++ libtiff libxml2 libxslt libzip lsof make nano openssh openssh-clients openssh-server openssl pam pam-devel passwd patch pcre pcre2 popt procps python python-docs python-setuptools quota rdate rsync sed shadow-utils smartmontools tar tmpwatch unzip util-linux-ng wget which xz yum-utils zip zlib } ]; use constant package_ImageMagick_Devel => 'ImageMagick-devel'; use constant package_crond => 'cronie'; use constant mysql_incompatible => [ qw{ mariadb-client mariadb-devel mariadb-embedded mariadb-embedded-devel mariadb-libs mariadb-libs-compat mariadb-release mariadb-server mariadb-test mysql-client mysql-devel mysql-embedded mysql-embedded-devel mysql-libs mysql-libs-compat mysql-release mysql-server mysql-test mysql55-mysql-bench mysql55-mysql-devel mysql55-mysql-libs mysql55-mysql-server mysql55-mysql-test mysqlclient16 rh-mysql56-mysql-bench rh-mysql56-mysql-common rh-mysql56-mysql-config rh-mysql56-mysql-devel rh-mysql56-mysql-errmsg rh-mysql56-mysql-server rh-mysql56-mysql-test rh-mysql57-mysql-common rh-mysql57-mysql-config rh-mysql57-mysql-devel rh-mysql57-mysql-errmsg rh-mysql57-mysql-server rh-mysql57-mysql-test } ]; use constant mysql_community_packages => [ qw/ mysql-community-devel mysql-community-libs-compat mysql-community-server / ]; use constant mysql_dependencies => [ qw/ coreutils grep perl-DBI shadow-utils / ]; use constant db_package_manager_key_params => { method => 'add_repo_key', keys => [qw{https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 https://repo.mysql.com/RPM-GPG-KEY-MySQL-2022 https://repo.mysql.com/RPM-GPG-KEY-mysql https://archive.mariadb.org/PublicKey https://supplychain.mariadb.com/MariaDB-Server-GPG-KEY}], }; use constant mariadb_repo_template => <<'___END_REPO_TEMPLATE___'; [MariaDB###MARIADB_FLAT_VERSION_SHORT###] name = MariaDB###MARIADB_FLAT_VERSION_SHORT### baseurl = https://archive.mariadb.org/mariadb-###MARIADB_VERSION_SHORT###/yum/centos/###DISTRO_MAJOR###/x86_64 gpgkey=https://archive.mariadb.org/PublicKey https://supplychain.mariadb.com/MariaDB-Server-GPG-KEY gpgcheck=1 ___END_REPO_TEMPLATE___ use constant mysql_repo_template => <<'___END_REPO_TEMPLATE___'; [Mysql-connectors-community] name=MySQL Connectors Community baseurl=https://repo.mysql.com/yum/mysql-connectors-community/el/###DISTRO_MAJOR###/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 https://repo.mysql.com/RPM-GPG-KEY-MySQL-2022 https://repo.mysql.com/RPM-GPG-KEY-mysql [Mysql-tools-community] name=MySQL Tools Community baseurl=https://repo.mysql.com/yum/mysql-tools-community/el/###DISTRO_MAJOR###/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 https://repo.mysql.com/RPM-GPG-KEY-MySQL-2022 https://repo.mysql.com/RPM-GPG-KEY-mysql [Mysql###MYSQL_FLAT_VERSION_SHORT###-community] name=MySQL ###MYSQL_VERSION_SHORT### Community Server baseurl=https://repo.mysql.com/yum/mysql-###MYSQL_VERSION_SHORT###-community/el/###DISTRO_MAJOR###/$basearch/ enabled=1 gpgcheck=1 gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 https://repo.mysql.com/RPM-GPG-KEY-MySQL-2022 https://repo.mysql.com/RPM-GPG-KEY-mysql [Mysql-tools-preview] name=MySQL Tools Preview baseurl=https://repo.mysql.com/yum/mysql-tools-preview/el/###DISTRO_MAJOR###/$basearch/ enabled=0 gpgcheck=1 gpgkey=https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 https://repo.mysql.com/RPM-GPG-KEY-MySQL-2022 https://repo.mysql.com/RPM-GPG-KEY-mysql ___END_REPO_TEMPLATE___ use constant supports_postgresql => 1; use constant package_descriptions => { 'short' => 'summary', 'long' => 'description', }; 1; } # --- END Cpanel/OS/Rhel.pm { # --- BEGIN Cpanel/OS/Almalinux.pm package Cpanel::OS::Almalinux; use cPstrict; # use Cpanel::OS::Rhel(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel); } use constant is_supported => 0; use constant pretty_distro => 'AlmaLinux'; 1; } # --- END Cpanel/OS/Almalinux.pm { # --- BEGIN Cpanel/OS/Rhel6.pm package Cpanel::OS::Rhel6; use cPstrict; # use Cpanel::OS::Rhel(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel); } use constant is_supported => 0; use constant binary_sync_source => 'linux-c6-x86_64'; use constant package_release_distro_tag => '~el6'; use constant service_manager => 'initd'; use constant unsupported_db_versions => [qw/10.5 10.6/]; use constant mariadb_versions_use_repo_template => [qw/10.0 10.1 10.2 10.3/]; use constant mysql_versions_use_repo_template => [qw/8.0/]; use constant dns_supported => [qw{ bind }]; use constant who_wins_if_soft_gt_hard => 'hard'; use constant postgresql_minimum_supported_version => '7.4'; use constant postgresql_packages => [ qw{ postgresql postgresql-devel postgresql-libs postgresql-server rh-postgresql rh-postgresql-devel rh-postgresql-libs rh-postgresql-server } ]; use constant postgresql_service_aliases => [qw/rhdb/]; use constant postgresql_initdb_commands => ['/sbin/service postgresql initdb']; 1; } # --- END Cpanel/OS/Rhel6.pm { # --- BEGIN Cpanel/OS/Rhel7.pm package Cpanel::OS::Rhel7; use cPstrict; # use Cpanel::OS::Rhel6(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel6); } use constant is_supported => 1; # Rhel 7 support use constant binary_sync_source => 'linux-c7-x86_64'; use constant package_release_distro_tag => '~el7'; use constant service_manager => 'systemd'; use constant unsupported_db_versions => []; use constant mariadb_versions_use_repo_template => [qw/10.0 10.1 10.2 10.3 10.5 10.6/]; use constant mysql_versions_use_repo_template => [qw/5.7 8.0/]; use constant dns_supported => [qw{ bind powerdns }]; use constant who_wins_if_soft_gt_hard => 'soft'; use constant postgresql_minimum_supported_version => '9.2'; use constant postgresql_service_aliases => []; use constant postgresql_initdb_commands => ['/usr/bin/postgresql-setup initdb']; use constant check_ntpd_pid_method => 'systemd_ntpd'; sub packages_supplemental ($self) { my @packages = $self->SUPER::packages_supplemental()->@*; push @packages, qw/ncurses-term perl-Try-Tiny perl-local-lib/; return [ sort @packages ]; } sub packages_supplemental_epel ($self) { my @packages = $self->SUPER::packages_supplemental_epel()->@*; push @packages, qw/dpkg/; @packages = grep { my $p = $_; !grep { $p eq $_ } qw/perl-Try-Tiny perl-local-lib/; } @packages; return [ sort @packages ]; } sub packages_required ($self) { my @packages = $self->SUPER::packages_required()->@*; @packages = grep { my $p = $_; !grep { $p eq $_ } qw/db4 db4-devel iptables-ipv6 lcms procps tmpwatch util-linux-ng/ } @packages; push @packages, qw/libdb libmount net-tools procps-ng psmisc python-tools/; return [ sort @packages ]; } 1; } # --- END Cpanel/OS/Rhel7.pm { # --- BEGIN Cpanel/OS/Rhel8.pm package Cpanel::OS::Rhel8; use cPstrict; # use Cpanel::OS::Linux (); # ea4tooling_all # use Cpanel::OS::Rhel7(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel7); } use constant is_supported => 0; # Rhel 8 is NOT supported but we use it as a base class for all Rhel derivatives. use constant binary_sync_source => 'linux-c8-x86_64'; use constant ea4_modern_openssl => '/usr/bin/openssl'; use constant firewall => 'firewalld_nftables'; use constant firewall_module => 'NFTables'; use constant supports_hostaccess => 0; use constant package_manager => 'dnf'; use constant package_release_distro_tag => '~el8'; use constant kernel_supports_fs_protected_regular => 1; use constant can_clean_plugins_repo => 0; use constant mysql_community_packages => [qw/mysql-community-server mysql-community-devel/]; # Removed mysql-community-libs-compat in RHEL 8 use constant mysql_default_version => '8.0'; use constant mariadb_minimum_supported_version => '10.3'; use constant postgresql_packages => [ qw{ postgresql postgresql-devel postgresql-libs postgresql-server } ]; use constant openssl_escapes_subjects => 1; use constant ea4tooling_dnsonly => ['dnf-plugin-universal-hooks']; use constant ea4tooling => [ 'dnf-plugin-universal-hooks', Cpanel::OS::Linux->ea4tooling_all->@* ]; use constant package_repositories => [qw/epel powertools/]; use constant system_package_providing_perl => 'perl-interpreter'; use constant retry_rpm_cmd_no_tty_hack => 1; use constant check_ntpd_pid_method => 'pid_check_var_run_ntpd'; use constant has_tcp_wrappers => 0; use constant ea4_install_from_profile_enforce_packages => 1; use constant repos_requires_dump_flag => 1; use constant system_exclude_rules => { 'dovecot' => 'dovecot*', 'php' => 'php*', 'exim' => 'exim*', 'pure-ftpd' => 'pure-ftpd*', 'proftpd' => 'proftpd*', 'p0f' => 'p0f', 'filesystem' => 'filesystem', 'kernel' => 'kernel kernel-xen kernel-smp kernel-pae kernel-PAE kernel-SMP kernel-hugemem kernel-debug* kernel-core kernel-modules*', 'kmod-' => 'kmod-[a-z]*', 'bind-chroot' => 'bind-chroot', }; use constant bin_needs_restarting => q[/usr/bin/needs-restarting]; sub packages_supplemental ($self) { my @packages = $self->SUPER::packages_supplemental()->@*; @packages = grep { my $p = $_; !grep { $p eq $_ } qw/ncurses-term python-devel python-tools quota-devel tcp_wrappers-devel/ } @packages; push @packages, qw{ python2-devel }; return [ sort @packages ]; } sub packages_required ($self) { my @packages = $self->SUPER::packages_required()->@*; @packages = grep { my $p = $_; !grep { $p eq $_ } qw/compat-db gd-progs libdb libmount procps-ng psmisc python python-docs python-setuptools python-tools rdate / } @packages; push @packages, qw/dnf glibc-locale-source mailx nftables python2 python2-docs python2-setuptools python2-tools python3-dnf python3-docs python3-libdnf python3-setuptools python36 util-linux-user sqlite cmake-filesystem/; return [ sort @packages ]; } use constant nat_server_buffer_connections => 2; 1; } # --- END Cpanel/OS/Rhel8.pm { # --- BEGIN Cpanel/OS/Almalinux8.pm package Cpanel::OS::Almalinux8; use cPstrict; use Cpanel::OS::Almalinux; # use Cpanel::OS::Rhel8(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel8); } use constant is_supported => 1; # Almalinux 8 use constant pretty_distro => Cpanel::OS::Almalinux->pretty_distro; use constant supports_imunify_360 => 1; use constant supports_cpanel_cloud_edition => 1; 1; } # --- END Cpanel/OS/Almalinux8.pm { # --- BEGIN Cpanel/OS/Centos.pm package Cpanel::OS::Centos; use cPstrict; # use Cpanel::OS::Rhel(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel); } use constant is_supported => 0; # Base OS class for all Centos derivatives. use constant pretty_distro => 'CentOS'; 1; } # --- END Cpanel/OS/Centos.pm { # --- BEGIN Cpanel/OS/Centos7.pm package Cpanel::OS::Centos7; use cPstrict; use Cpanel::OS::Centos; # use Cpanel::OS::Rhel7(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel7); } use constant is_supported => 1; # Centos 7 use constant pretty_distro => Cpanel::OS::Centos->pretty_distro; use constant supports_imunify_360 => 1; use constant can_be_elevated => 1; use constant can_elevate_to => [qw(Almalinux8)]; use constant needs_els => 1; 1; } # --- END Cpanel/OS/Centos7.pm { # --- BEGIN Cpanel/OS/Cloudlinux.pm package Cpanel::OS::Cloudlinux; use cPstrict; # use Cpanel::OS::Rhel(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel); } use constant is_supported => 0; # Base class for CL. use constant is_cloudlinux => 1; use constant pretty_distro => 'CloudLinux'; use constant ea4_yum_tooling => [qw{ yum-plugin-universal-hooks ea-cpanel-tools ea-profiles-cloudlinux }]; use constant ea4_dnf_tooling => [qw{ dnf-plugin-universal-hooks ea-cpanel-tools ea-profiles-cloudlinux }]; use constant supports_kernelcare_free => 0; 1; } # --- END Cpanel/OS/Cloudlinux.pm { # --- BEGIN Cpanel/OS/Cloudlinux6.pm package Cpanel::OS::Cloudlinux6; use cPstrict; # use Cpanel::OS (); use Cpanel::OS::Cloudlinux; # use Cpanel::OS::Rhel6(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel6); } use constant is_cloudlinux => 1; use constant default_uid_min => 500; use constant default_gid_min => 500; use constant setup_tz_method => q[sysconfig]; use constant pretty_distro => Cpanel::OS::Cloudlinux->pretty_distro; use constant ea4_install_repo_from_package => 1; use constant ea4_from_pkg_url => 'https://repo.cloudlinux.com/cloudlinux/EA4/cloudlinux-ea4-release-latest-6.noarch.rpm'; use constant ea4_from_pkg_reponame => 'cloudlinux-ea4-release'; use constant ea4_from_custom_repo_url => undef; use constant ea4_from_custom_repo_path => undef; use constant ea4tooling => Cpanel::OS::Cloudlinux->ea4_yum_tooling; use constant supports_kernelcare_free => Cpanel::OS::Cloudlinux->supports_kernelcare_free; use constant has_quota_support_for_xfs => 0; use constant program_to_apply_kernel_args => 'none'; use constant ssh_supported_algorithms => [qw{ ecdsa rsa }]; use constant supports_inetd => 1; use constant supports_syslogd => 1; use constant install_gcc_from_slc6_devtoolset => 1; use constant is_systemd => 0; use constant openssl_minimum_supported_version => 0; # check skipped on CL6 use constant has_cloudlinux_enhanced_quotas => 1; use constant can_become_cloudlinux => 0; use constant supports_imunify_360 => 1; use constant outdated_services_check => q[centos6]; use constant support_needs_minor_at_least_at => 6; sub packages_required ($self) { my @packages = $self->SUPER::packages_required()->@*; push @packages, qw/alt-libcurlssl alt-libxml2/; return [ sort @packages ]; } sub is_supported { my $needs_minor = Cpanel::OS::support_needs_minor_at_least_at(); return 0 if defined $needs_minor && Cpanel::OS::minor() < $needs_minor; return 1; } 1; } # --- END Cpanel/OS/Cloudlinux6.pm { # --- BEGIN Cpanel/OS/Cloudlinux7.pm package Cpanel::OS::Cloudlinux7; use cPstrict; use Cpanel::OS::Cloudlinux; # use Cpanel::OS::Rhel7(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel7); } use constant is_supported => 1; # Cloudlinux 7 use constant is_cloudlinux => 1; use constant pretty_distro => Cpanel::OS::Cloudlinux->pretty_distro; use constant can_be_elevated => 1; use constant can_elevate_to => [qw(Cloudlinux8)]; use constant ea4_install_repo_from_package => 1; use constant ea4_from_pkg_url => 'https://repo.cloudlinux.com/cloudlinux/EA4/cloudlinux-ea4-release-latest-7.noarch.rpm'; use constant ea4_from_pkg_reponame => 'cloudlinux-ea4-release'; use constant ea4_from_custom_repo_url => undef; use constant ea4_from_custom_repo_path => undef; use constant ea4tooling => Cpanel::OS::Cloudlinux->ea4_yum_tooling; use constant supports_kernelcare_free => Cpanel::OS::Cloudlinux->supports_kernelcare_free; use constant has_cloudlinux_enhanced_quotas => 1; use constant program_to_apply_kernel_args => 'none'; use constant can_become_cloudlinux => 0; use constant supports_imunify_360 => 1; 1; } # --- END Cpanel/OS/Cloudlinux7.pm { # --- BEGIN Cpanel/OS/Cloudlinux8.pm package Cpanel::OS::Cloudlinux8; use cPstrict; use Cpanel::OS::Cloudlinux; # use Cpanel::OS::Rhel8(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel8); } use constant is_supported => 1; # Cloudlinux 8 use constant is_cloudlinux => 1; use constant pretty_distro => Cpanel::OS::Cloudlinux->pretty_distro; use constant ea4_install_from_profile_enforce_packages => 0; use constant ea4_install_repo_from_package => 1; use constant ea4_from_pkg_url => 'https://repo.cloudlinux.com/cloudlinux/EA4/cloudlinux-ea4-release-latest-8.noarch.rpm'; use constant ea4_from_pkg_reponame => 'cloudlinux-ea4-release'; use constant ea4_from_custom_repo_url => undef; use constant ea4_from_custom_repo_path => undef; use constant ea4_testing_yum_repo => 'cl-ea4-testing'; use constant ea4tooling => Cpanel::OS::Cloudlinux->ea4_dnf_tooling; use constant package_repositories => [qw/cloudlinux-PowerTools epel/]; use constant supports_kernelcare_free => Cpanel::OS::Cloudlinux->supports_kernelcare_free; use constant has_cloudlinux_enhanced_quotas => 1; use constant can_become_cloudlinux => 0; use constant supports_imunify_360 => 1; 1; } # --- END Cpanel/OS/Cloudlinux8.pm { # --- BEGIN Cpanel/OS/Ubuntu.pm package Cpanel::OS::Ubuntu; use cPstrict; # use Cpanel::OS::Linux (); # ea4tooling_all # use Cpanel::OS::Linux(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Linux); } use constant is_supported => 0; use constant nogroup => q[nogroup]; use constant sudoers => 'sudo'; # Warning: need to change dynamicui.conf when updating this value use constant has_wheel_group => 0; use constant default_sys_uid_min => 100; use constant default_sys_gid_min => 100; use constant pretty_distro => 'Ubuntu'; use constant firewall => 'ufw_iptables'; use constant firewall_module => 'IpTables'; use constant networking => 'netplan'; use constant sysconfig_network => undef; use constant package_manager => 'apt'; use constant package_manager_module => 'Apt'; use constant is_apt_based => 1; use constant is_yum_based => 0; use constant is_rpm_based => 0; use constant base_distro => 'debian'; use constant supports_cpaddons => 0; use constant kernel_supports_fs_protected_regular => 1; use constant rpm_versions_system => 'ubuntu'; use constant packages_arch => 'amd64'; use constant mariadb_versions_use_repo_template => []; use constant mysql_versions_use_repo_template => ['8.0']; use constant db_package_manager_key_params => { method => 'add_repo_key_by_id', keys => [ 'A8D3785C', # For MySQL 8.0.36 and up '3A79BD29', # For MySQL 8.0.28 and up '5072E1F5', # For MySQL 8.0.27 and below 'C74CD1D8', # For all MariaDB versions ], }; use constant who_wins_if_soft_gt_hard => 'hard'; use constant security_service => 'apparmor'; use constant cron_bin_path => '/usr/sbin/cron'; use constant systemd_service_name_map => { 'crond' => 'cron' }; use constant ea4_modern_openssl => '/usr/bin/openssl'; use constant kernel_package_pattern => '^linux-image-[0-9]'; use constant check_kernel_version_method => q[boot-vmlinuz-file]; use constant stock_kernel_version_regex => qr/-(?:(?:(?:generic|lowlatency)(?:-hwe)?)|kvm|aws|azure|gcp|oracle)$/; use constant openssl_escapes_subjects => 1; use constant prelink_config_path => '/etc/default/prelink'; use constant pam_file_controlling_crypt_algo => 'common-password'; use constant user_crontab_dir => '/var/spool/cron/crontabs'; use constant supports_kernelcare => 1; use constant supports_kernelcare_free => 1; use constant iptables_ipv4_savefile => '/etc/iptables/rules.v4'; use constant iptables_ipv6_savefile => '/etc/iptables/rules.v6'; use constant mysql_incompatible => [ qw{ default-mysql-server default-mysql-server-core mariadb-client mariadb-client-10.3 mariadb-server mariadb-server-10.3 mariadb-test mysql-client-8.0 mysql-server-8.0 mysql-testsuite mysql-testsuite-8.0 } ]; use constant mysql_community_packages => [qw/mysql-community-server mysql-shell libmysqlclient-dev/]; use constant mysql_dependencies => [qw/libdbi-perl passwd adduser login coreutils/]; use constant mysql_default_version => '8.0'; use constant mariadb_minimum_supported_version => '10.3'; use constant outdated_services_check => q[needrestart_b]; use constant outdated_processes_check => q[checkrestart]; use constant check_reboot_method => q[check-reboot-required]; use constant bin_needs_restarting => undef; # The needs-restarting program is not supported on Ubuntu, so blank it out to reduce confusion. use constant syslog_service_name => 'rsyslog'; use constant rsyslog_triggered_by_socket => 1; use constant ea4_from_custom_repo_url => 'https://securedownloads.cpanel.net/EA4/EA4.list'; use constant ea4_from_custom_repo_path => '/etc/apt/sources.list.d/EA4.list'; use constant ea4_conflicting_apache_distro_packages => [qw( apache2 apache2-utils php-cli )]; use constant ea4tooling_dnsonly => ['apt-plugin-universal-hooks']; use constant ea4tooling => [ 'apt-plugin-universal-hooks', Cpanel::OS::Linux->ea4tooling_all->@* ]; use constant system_exclude_rules => { 'dovecot' => 'dovecot*', 'exim' => 'exim*', 'filesystem' => 'base-files', # block Ubuntu updates to current installed version (20.04) 'kernel' => 'linux-headers* linux-image* linux-modules*', 'nsd' => 'nsd', 'p0f' => 'p0f', 'php' => 'php*', 'proftpd' => 'proftpd*', 'pure-ftpd' => 'pure-ftpd*', }; use constant packages_supplemental => [ 'nfs-common', qw{ libcpan-perl-releases-perl libexpect-perl libio-pty-perl libjson-xs-perl liblocal-lib-perl libmodule-build-perl libtry-tiny-perl libwww-perl libyaml-syck-perl lsof nscd rpm strace sysstat tcpd util-linux } ]; use constant can_clean_plugins_repo => 0; use constant jetbackup_repo_pkg => 'https://repo.jetlicense.com/ubuntu/jetapps-repo-latest_amd64.deb'; use constant repo_suffix => 'list'; use constant repo_dir => '/etc/apt/sources.list.d'; use constant packages_required => [ qw{ acl apt-file apt-transport-https aspell at bind9 bind9-libs bind9-utils binutils bzip2 coreutils cpio cpp cracklib-runtime cron curl debian-goodies debianutils e2fsprogs expat file g++ g++-9 gawk gcc gdbmtool gettext glibc-source gnupg2 graphicsmagick-imagemagick-compat gzip icu-devtools iptables iptables-persistent language-pack-en-base less libaio1 libapt-pkg-perl libboost-program-options1.71.0 libcairo2-dev libcrack2 libdb5.3 libevent-2.1-7 libfile-fcntllock-perl libfontconfig1-dev libgcc1 libgd-tools libgd3 libgmp10 libgomp1 libicu-dev libicu66 libidn11 libjpeg-turbo8 liblua5.3-dev libmount1 libmysqlclient21 libncurses5 libpam0g libpam0g-dev libpango-1.0-0 libpangocairo-1.0-0 libpcap0.8 libpcre2-8-0 libpcre2-posix2 libpcre3 libpixman-1-0 libpng16-16 libpopt0 libreadline-dev libssl-dev libstdc++-9-dev libstdc++6 libtiff5 libuser libxml2 libxml2-dev libxslt1.1 libzip5 linux-libc-dev lsof make nano needrestart net-tools openssh-client openssh-server openssl passwd patch pcre2-utils procps python-setuptools python2 python2-doc python2.7-dev quota rdate rsync sed smartmontools ssl-cert sysstat tar unzip usrmerge wget xz-utils zip zlib1g } ]; use constant package_ImageMagick_Devel => 'libmagick++-6.q16-dev'; use constant package_crond => 'cron'; use constant system_package_providing_perl => 'perl-base'; use constant bin_grub_mkconfig => q[/usr/sbin/grub-mkconfig]; use constant program_to_apply_kernel_args => 'grub-mkconfig'; use constant package_descriptions => { 'short' => 'description', 'long' => 'longdesc', }; use constant nat_server_buffer_connections => 2; 1; } # --- END Cpanel/OS/Ubuntu.pm { # --- BEGIN Cpanel/OS/Ubuntu20.pm package Cpanel::OS::Ubuntu20; use cPstrict; # use Cpanel::OS::Ubuntu(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Ubuntu); } use constant is_supported => 1; use constant binary_sync_source => 'linux-u20-x86_64'; use constant package_release_distro_tag => '~u20'; use constant maillog_path => '/var/log/mail.log'; use constant plugins_repo_url => 'https://securedownloads.cpanel.net/cpanel-plugins/cpanel-plugins.list'; use constant supports_imunify_360 => 1; use constant supports_cpanel_cloud_edition => 1; use constant mysql_repo_template => <<'___END_REPO_TEMPLATE___'; deb https://repo.mysql.com/apt/ubuntu/ focal mysql-apt-config deb https://repo.mysql.com/apt/ubuntu/ focal mysql-###MYSQL_VERSION_SHORT### deb https://repo.mysql.com/apt/ubuntu/ focal mysql-tools deb-src https://repo.mysql.com/apt/ubuntu/ focal mysql-###MYSQL_VERSION_SHORT### ___END_REPO_TEMPLATE___ use constant quota_packages_conditional => { 'linux-aws' => ['linux-modules-extra-aws'], 'linux-aws-edge' => ['linux-modules-extra-aws-edge'], 'linux-aws-lts-20.04' => ['linux-modules-extra-aws-lts-20.04'], 'linux-azure' => ['linux-modules-extra-azure'], 'linux-azure-cvm' => ['linux-modules-extra-azure-cvm'], 'linux-azure-edge' => ['linux-modules-extra-azure-edge'], 'linux-azure-fde' => ['linux-modules-extra-azure-fde'], 'linux-azure-lts-20.04' => ['linux-modules-extra-azure-lts-20.04'], 'linux-gcp' => ['linux-modules-extra-gcp'], 'linux-gcp-edge' => ['linux-modules-extra-gcp-edge'], 'linux-gcp-lts-20.04' => ['linux-modules-extra-gcp-lts-20.04'], 'linux-gke' => ['linux-modules-extra-gke'], 'linux-gke-5.4' => ['linux-modules-extra-gke-5.4'], 'linux-gkeop' => ['linux-modules-extra-gkeop'], 'linux-gkeop-5.4' => ['linux-modules-extra-gkeop-5.4'], 'linux-ibm' => ['linux-modules-extra-ibm'], 'linux-ibm-lts-20.04' => ['linux-modules-extra-ibm-lts-20.04'], 'linux-virtual' => ['linux-image-extra-virtual'], 'linux-virtual-hwe-20.04' => ['linux-image-extra-virtual-hwe-20.04'], 'linux-virtual-hwe-20.04-edge' => ['linux-image-extra-virtual-hwe-20.04-edge'], }; 1; } # --- END Cpanel/OS/Ubuntu20.pm { # --- BEGIN Cpanel/OS/Rocky.pm package Cpanel::OS::Rocky; use cPstrict; # use Cpanel::OS::Rhel(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel); } use constant is_supported => 0; use constant pretty_distro => 'Rocky Linux'; 1; } # --- END Cpanel/OS/Rocky.pm { # --- BEGIN Cpanel/OS/Rocky8.pm package Cpanel::OS::Rocky8; use cPstrict; use Cpanel::OS::Rocky; # use Cpanel::OS::Rhel8(); our @ISA; BEGIN { push @ISA, qw(Cpanel::OS::Rhel8); } use constant is_supported => 1; # Rockylinux 8 use constant pretty_distro => Cpanel::OS::Rocky->pretty_distro; use constant supports_imunify_360 => 1; 1; } # --- END Cpanel/OS/Rocky8.pm { # --- BEGIN Cpanel/OS/All.pm package Cpanel::OS::All; use cPstrict; use utf8; # use Cpanel::OS::Almalinux8 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Centos7 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Cloudlinux6 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Cloudlinux7 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Cloudlinux8 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Rhel7 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Ubuntu20 (); # PPI USE OK - fatpack usage # use Cpanel::OS::Rocky8 (); # PPI USE OK - fatpack usage sub supported_distros() { return ( [ Almalinux => 8 ], [ Centos => 7 ], [ Cloudlinux => 6 ], [ Cloudlinux => 7 ], [ Cloudlinux => 8 ], [ Rhel => 7 ], [ Rocky => 8 ], [ Ubuntu => 20 ], ); } sub advertise_supported_distros() { my $current_name; my $current_versions = []; my $advertise = ''; my $map_os_display_name = { 'Almalinux' => 'AlmaLinux', 'Centos' => 'CentOS', 'Rhel' => 'Red Hat Enterprise Linux', 'Cloudlinux' => 'CloudLinux®', 'Rocky' => 'Rocky Linux™', }; foreach my $d ( supported_distros() ) { my ( $name, $version ) = $d->@*; if ( !defined $current_name ) { $current_name = $name; push $current_versions->@*, $version; next; } if ( $current_name eq $name ) { push $current_versions->@*, $version; } else { my $display_name = $map_os_display_name->{$current_name} // $current_name; $advertise .= $display_name . ' ' . join( '/', $current_versions->@* ) . ', '; $current_name = $name; $current_versions = [$version]; } } if ( defined $current_name ) { my $display_name = $map_os_display_name->{$current_name} // $current_name; $advertise .= $display_name . ' ' . join( '/', $current_versions->@* ); } return $advertise; } 1; } # --- END Cpanel/OS/All.pm { # --- BEGIN Cpanel/TimeHiRes.pm package Cpanel::TimeHiRes; use strict; use warnings; use constant { _gettimeofday => 96, _clock_gettime => 228, _CLOCK_REALTIME => 0, _EINTR => 4, _PACK_TEMPLATE => 'L!L!', }; sub clock_gettime { my $timeval = pack( _PACK_TEMPLATE, () ); _get_time_from_syscall( _clock_gettime, _CLOCK_REALTIME, $timeval, ); return unpack( _PACK_TEMPLATE, $timeval ); } sub time { my ( $secs, $nsecs ) = clock_gettime(); return $secs + ( $nsecs / 1_000_000_000 ); } sub sleep { my ($secs) = @_; local $!; my $retval = select( undef, undef, undef, $secs ); if ( $retval == -1 && $! != _EINTR ) { require Cpanel::Exception; die 'Cpanel::Exception'->can('create')->( 'SystemCall', 'The system failed to suspend command execution for [quant,_1,second,seconds] because of an error: [_2]', [ $secs, $! ] ); } return $secs; } sub gettimeofday { my $timeval = pack( _PACK_TEMPLATE, () ); _get_time_from_syscall( _gettimeofday, $timeval, undef, ); return unpack( _PACK_TEMPLATE, $timeval ); } sub _get_time_from_syscall { ##no critic qw(RequireArgUnpacking) my $syscall_num = shift; local $!; my $retval = syscall( $syscall_num, @_ ); if ( $retval == -1 ) { require Cpanel::Exception; die 'Cpanel::Exception'->can('create')->( 'SystemCall', 'The system failed to retrieve the time because of an error: [_1]', [$!] ); } return; } 1; } # --- END Cpanel/TimeHiRes.pm { # --- BEGIN Cpanel/Struct/Common/Time.pm package Cpanel::Struct::Common::Time; use strict; use warnings; use constant PACK_TEMPLATE => 'L!L!'; my %CLASS_PRECISION; sub float_to_binary { return pack( PACK_TEMPLATE(), int( $_[1] ), int( 0.5 + ( $_[0]->_PRECISION() * $_[1] ) - ( $_[0]->_PRECISION() * int( $_[1] ) ) ), ); } sub binary_to_float { return $_[0]->_binary_to_float( PACK_TEMPLATE(), $_[1] )->[0]; } sub binaries_to_floats_at { return $_[0]->_binary_to_float( "\@$_[3] " . ( PACK_TEMPLATE() x $_[2] ), $_[1], ); } my ( $i, $precision, @sec_psec_pairs ); sub _binary_to_float { ## no critic qw(RequireArgUnpacking) @sec_psec_pairs = unpack( $_[1], $_[2] ); $i = 0; my @floats; $precision = $CLASS_PRECISION{ $_[0] } ||= $_[0]->_PRECISION(); while ( $i < @sec_psec_pairs ) { push @floats, 0 + ( q<> . ( $sec_psec_pairs[$i] + ( $sec_psec_pairs[ $i + 1 ] / $precision ) ) ); $i += 2; } return \@floats; } 1; } # --- END Cpanel/Struct/Common/Time.pm { # --- BEGIN Cpanel/Struct/timespec.pm package Cpanel::Struct::timespec; use strict; use warnings; # use Cpanel::Struct::Common::Time(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Struct::Common::Time); } use constant { _PRECISION => 1_000_000_000, # nanoseconds }; 1; } # --- END Cpanel/Struct/timespec.pm { # --- BEGIN Cpanel/NanoStat.pm package Cpanel::NanoStat; use strict; use warnings; # use Cpanel::Struct::timespec (); use constant { _NR_stat => 4, _NR_fstat => 5, _NR_lstat => 6, }; use constant _PACK_TEMPLATE => q< Q # st_dev Q # st_ino @24 L # st_mode @16 Q # st_nlink @28 L # st_uid L # st_gid x![Q] Q # st_rdev Q # st_size Q # st_blksize Q # st_blocks >; my $pre_times_pack_len = length pack _PACK_TEMPLATE(); my $buf = ( "\0" x 144 ); sub stat { return _syscall( _NR_stat(), $_[0] ); } sub lstat { return _syscall( _NR_lstat(), $_[0] ); } sub fstat { return _syscall( _NR_fstat(), 0 + ( ref( $_[0] ) ? fileno( $_[0] ) : $_[0] ) ); } sub _syscall { ## no critic qw(RequireArgUnpacking) my $arg_dupe = $_[1]; return undef if -1 == syscall( $_[0], $arg_dupe, $buf ); my @vals = unpack _PACK_TEMPLATE(), $buf; splice( @vals, 8, 0, @{ Cpanel::Struct::timespec->binaries_to_floats_at( $buf, 3, $pre_times_pack_len ) }, ); return @vals; } 1; } # --- END Cpanel/NanoStat.pm { # --- BEGIN Cpanel/NanoUtime.pm package Cpanel::NanoUtime; use strict; use warnings; # use Cpanel::Struct::timespec (); use constant { _NR_utimensat => 280, _AT_FDCWD => -100, _AT_SYMLINK_NOFOLLOW => 0x100, }; sub utime { return _syscall( 0 + _AT_FDCWD(), $_[2], @_[ 0, 1 ], 0 ); } sub futime { return _syscall( 0 + ( ref( $_[2] ) ? fileno( $_[2] ) : $_[2] ), undef, @_[ 0, 1 ], 0, ); } sub lutime { return _syscall( 0 + _AT_FDCWD(), $_[2], @_[ 0, 1 ], 0 + _AT_SYMLINK_NOFOLLOW() ); } my ( $path, $buf ) = @_; sub _syscall { if ( defined $_[-3] ) { if ( defined $_[-2] ) { $buf = Cpanel::Struct::timespec->float_to_binary( $_[-3] ) . Cpanel::Struct::timespec->float_to_binary( $_[-2] ); } else { die "atime is “$_[-3]”, but mtime is undef!"; } } elsif ( defined $_[-2] ) { die "atime is undef, but mtime is “$_[-2]”!"; } else { $buf = undef; } $path = $_[1]; return undef if -1 == syscall( 0 + _NR_utimensat(), $_[0], $path // undef, $buf // undef, $_[-1] ); return 1; } 1; } # --- END Cpanel/NanoUtime.pm { # --- BEGIN Cpanel/HiRes.pm package Cpanel::HiRes; use strict; use warnings; my %_routes = ( 'fstat' => [ 'NanoStat', 'fstat', 'stat', 1 ], 'lstat' => [ 'NanoStat', 'lstat', 'lstat', 1 ], 'stat' => [ 'NanoStat', 'stat', 'stat', 1 ], 'time' => [ 'TimeHiRes', 'time', 'time' ], 'utime' => [ 'NanoUtime', 'utime', 'utime' ], 'futime' => [ 'NanoUtime', 'futime', 'utime' ], 'lutime' => [ 'NanoUtime', 'lutime', undef ], ); my $preloaded; sub import { my ( $class, %opts ) = @_; if ( my $preload = $opts{'preload'} ) { if ( $preload eq 'xs' ) { require Time::HiRes; } elsif ( $preload eq 'perl' ) { if ( !$preloaded ) { require Cpanel::TimeHiRes; # PPI USE OK - preload require Cpanel::NanoStat; # PPI USE OK - preload require Cpanel::NanoUtime; # PPI USE OK - preload } } else { die "Unknown “preload”: “$preload”"; } $preloaded = $preload; } return; } our $AUTOLOAD; sub AUTOLOAD { ## no critic qw(Subroutines::RequireArgUnpacking) substr( $AUTOLOAD, 0, 1 + rindex( $AUTOLOAD, ':' ) ) = q<>; if ( !$AUTOLOAD || !$_routes{$AUTOLOAD} ) { die "Unknown function in Cpanel::HiRes::$_[0]"; } my $function = $AUTOLOAD; undef $AUTOLOAD; my ( $pp_module, $pp_function, $xs_function, $xs_needs_closure ) = @{ $_routes{$function} }; no strict 'refs'; if ( $INC{'Time/HiRes.pm'} && $xs_function ) { *$function = *{"Time::HiRes::$xs_function"}; return Time::HiRes->can($xs_function)->(@_); } else { _require("Cpanel/${pp_module}.pm") if !$INC{"Cpanel/${pp_module}.pm"}; my $pp_cr = "Cpanel::${pp_module}"->can($pp_function); if ($xs_function) { *$function = sub { if ( $INC{'Time/HiRes.pm'} ) { *$function = *{"Time::HiRes::$xs_function"}; return Time::HiRes->can($xs_function)->(@_); } goto &$pp_cr; }; } else { *$function = $pp_cr; } } goto &$function; } sub _require { local ( $!, $^E, $@ ); require $_[0]; return; } 1; } # --- END Cpanel/HiRes.pm { # --- BEGIN Cpanel/Env.pm package Cpanel::Env; use strict; use warnings; our $VERSION = '1.7'; my $SAFE_ENV_VARS; BEGIN { $SAFE_ENV_VARS = q< ALLUSERSPROFILE APPDATA BUNDLE_PATH CLIENTNAME COMMONPROGRAMFILES COMPUTERNAME COMSPEC CPANEL_BASE_INSTALL CPANEL_IS_CRON CPBACKUP DEBIAN_FRONTEND DEBIAN_PRIORITY DOCUMENT_ROOT FORCEDCPUPDATE FP_NO_HOST_CHECK HOMEDRIVE HOMEPATH LANG LANGUAGE LC_ALL LC_MESSAGES LC_CTYPE LOGONSERVER NEWWHMUPDATE NOTIFY_SOCKET NUMBER_OF_PROCESSORS OPENSSL_NO_DEFAULT_ZLIB OS PATH PATHEXT PROCESSOR_ARCHITECTURE PROCESSOR_IDENTIFIER PROCESSOR_LEVEL PROCESSOR_REVISION PROGRAMFILES PROMPT PYTHONIOENCODING SERVER_SOFTWARE SESSIONNAME SKIP_DEFERRAL_CHECK SSH_CLIENT SYSTEMDRIVE SYSTEMROOT TEMP TERM TMP UPDATENOW_NO_RETRY UPDATENOW_PRESERVE_FAILED_FILES USERDOMAIN USERNAME USERPROFILE WINDIR >; $SAFE_ENV_VARS =~ tr<\n >< >s; $SAFE_ENV_VARS =~ s<\A\s+><>; } { no warnings 'once'; *cleanenv = *clean_env; } sub clean_env { my %OPTS = @_; my %SAFE_ENV_VARS = map { $_ => undef } split( m{ }, $SAFE_ENV_VARS ); if ( defined $OPTS{'keep'} && ref $OPTS{'keep'} eq 'ARRAY' ) { @SAFE_ENV_VARS{ @{ $OPTS{'keep'} } } = undef; } if ( defined $OPTS{'delete'} && ref $OPTS{'delete'} eq 'ARRAY' ) { delete @SAFE_ENV_VARS{ @{ $OPTS{'delete'} } }; } delete @ENV{ grep { !exists $SAFE_ENV_VARS{$_} } keys %ENV }; if ( $OPTS{'http_purge'} ) { delete @ENV{ 'SERVER_SOFTWARE', 'DOCUMENT_ROOT' }; } return; } sub get_safe_env_vars { return $SAFE_ENV_VARS; } sub get_safe_path { return '/usr/local/jdk/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:/usr/X11R6/bin:/root/bin:/opt/bin'; } sub set_safe_path { return ( $ENV{'PATH'} = get_safe_path() ); } 1; } # --- END Cpanel/Env.pm { # --- BEGIN Cpanel/Autodie.pm package Cpanel::Autodie; use strict; use warnings; sub _ENOENT { return 2; } sub _EEXIST { return 17; } sub _EINTR { return 4; } sub import { shift; _load_function($_) for @_; return; } our $AUTOLOAD; sub AUTOLOAD { substr( $AUTOLOAD, 0, 1 + rindex( $AUTOLOAD, ':' ) ) = q<>; _load_function($AUTOLOAD); goto &{ Cpanel::Autodie->can($AUTOLOAD) }; } sub _load_function { _require("Cpanel/Autodie/CORE/$_[0].pm"); return; } sub _require { local ( $!, $^E, $@ ); require $_[0]; return; } 1; } # --- END Cpanel/Autodie.pm { # --- BEGIN Cpanel/Fcntl/Constants.pm package Cpanel::Fcntl::Constants; use strict; use warnings; BEGIN { our $O_RDONLY = 0; our $O_WRONLY = 1; our $O_RDWR = 2; our $O_ACCMODE = 3; our $F_GETFD = 1; our $F_SETFD = 2; our $F_GETFL = 3; our $F_SETFL = 4; our $SEEK_SET = 0; our $SEEK_CUR = 1; our $SEEK_END = 2; our $S_IWOTH = 2; our $S_ISUID = 2048; our $S_ISGID = 1024; our $O_CREAT = 64; our $O_EXCL = 128; our $O_TRUNC = 512; our $O_APPEND = 1024; our $O_NONBLOCK = 2048; our $O_DIRECTORY = 65536; our $O_NOFOLLOW = 131072; our $O_CLOEXEC = 524288; our $S_IFREG = 32768; our $S_IFDIR = 16384; our $S_IFCHR = 8192; our $S_IFBLK = 24576; our $S_IFIFO = 4096; our $S_IFLNK = 40960; our $S_IFSOCK = 49152; our $S_IFMT = 61440; our $LOCK_SH = 1; our $LOCK_EX = 2; our $LOCK_NB = 4; our $LOCK_UN = 8; our $FD_CLOEXEC = 1; } 1; } # --- END Cpanel/Fcntl/Constants.pm { # --- BEGIN Cpanel/Fcntl.pm package Cpanel::Fcntl; use strict; use warnings; # use Cpanel::Fcntl::Constants (); my %CONSTANTS; my %CACHE; sub or_flags { my (@flags) = @_; my $flag_cache_key = join( '|', @flags ); return $CACHE{$flag_cache_key} if defined $CACHE{$flag_cache_key}; my $numeric = 0; foreach my $o_const (@flags) { $numeric |= ( $CONSTANTS{$o_const} ||= do { my $glob = $Cpanel::Fcntl::Constants::{$o_const}; my $number_r = $glob && *{$glob}{'SCALAR'}; die "Missing \$Cpanel::Fcntl::Constants::$o_const! (does it need to be added?)" if !$number_r; $$number_r; } ); } return ( $CACHE{$flag_cache_key} = $numeric ); } 1; } # --- END Cpanel/Fcntl.pm { # --- BEGIN Cpanel/FileUtils/Touch.pm package Cpanel::FileUtils::Touch; use strict; use warnings; use Try::Tiny; use Cpanel::Autodie; use Cpanel::Fcntl; sub touch_if_not_exists { my ($path) = @_; my $fh; try { Cpanel::Autodie::sysopen( $fh, $path, Cpanel::Fcntl::or_flags(qw( O_WRONLY O_CREAT O_EXCL )), ); } catch { undef $fh; if ( !try { $_->error_name() eq 'EEXIST' } ) { local $@ = $_; die; } }; return $fh ? 1 : 0; } 1; } # --- END Cpanel/FileUtils/Touch.pm { # --- BEGIN Cpanel/Config/TouchFileBase.pm package Cpanel::Config::TouchFileBase; use strict; use warnings; # use Cpanel::Autodie (); # use Cpanel::Exception (); sub _TOUCH_FILE { die Cpanel::Exception::create('AbstractClass') } sub is_on { my ( $self, @args ) = @_; my $exists = Cpanel::Autodie::exists( $self->_TOUCH_FILE(@args) ); if ( $exists && !-f _ ) { die Cpanel::Exception->create( '“[_1]” exists but is not a file!', [ $self->_TOUCH_FILE(@args) ] ); } return $exists; } sub set_on { my ( $self, @args ) = @_; my $path = $self->_TOUCH_FILE(@args); require Cpanel::FileUtils::Touch; return Cpanel::FileUtils::Touch::touch_if_not_exists($path); } sub set_off { my ( $self, @args ) = @_; return Cpanel::Autodie::unlink_if_exists( $self->_TOUCH_FILE(@args) ); } 1; } # --- END Cpanel/Config/TouchFileBase.pm { # --- BEGIN Cpanel/Update/IsCron.pm package Cpanel::Update::IsCron; use strict; use warnings; # use Cpanel::Config::TouchFileBase(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Config::TouchFileBase); } our $_PATH = '/var/cpanel/upgrade_is_from_cron'; sub _TOUCH_FILE { return $_PATH } 1; } # --- END Cpanel/Update/IsCron.pm { # --- BEGIN Cpanel/Time/Local.pm package Cpanel::Time::Local; use strict; our $server_offset_string; our ( $timecacheref, $localtimecacheref ) = ( [ -1, '', -1 ], [ -1, '', -1 ] ); my $server_offset; my $localtime_link_or_mtime; our $ETC_LOCALTIME = q{/etc/localtime}; sub _clear_caches { undef $_ for ( $server_offset, $server_offset_string, $timecacheref, $localtimecacheref, $localtime_link_or_mtime, ); return; } sub localtime2timestamp { my ( $time, $delimiter ) = @_; $delimiter ||= ' '; $time ||= time(); return $localtimecacheref->[2] if $localtimecacheref->[0] == $time && $localtimecacheref->[1] eq $delimiter; my $tz_offset = get_server_offset_as_offset_string($time); my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime $time; @{$localtimecacheref}[ 0, 1 ] = ( $time, $delimiter ); return ( $localtimecacheref->[2] = sprintf( '%04d-%02d-%02d' . $delimiter . '%02d:%02d:%02d %s', $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $tz_offset ) ); } sub get_server_offset_as_offset_string { my ($time_supplied) = @_; if ( !$time_supplied ) { my $link_or_mtime; if ( -l $ETC_LOCALTIME ) { $link_or_mtime = readlink($ETC_LOCALTIME); } else { $link_or_mtime = ( stat($ETC_LOCALTIME) )[9]; } if ( defined $link_or_mtime ) { $localtime_link_or_mtime ||= $link_or_mtime; if ( $localtime_link_or_mtime ne $link_or_mtime ) { _clear_caches(); $localtime_link_or_mtime = $link_or_mtime; } } } if ( $time_supplied || !defined $server_offset_string ) { UNTIL_SAME_SECOND: { my $starttime = time(); my $time = $time_supplied || $starttime; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = localtime $time; my ( $gmmin, $gmhour, $gmyear, $gmyday ) = ( gmtime($time) )[ 1, 2, 5, 7 ]; redo UNTIL_SAME_SECOND if time != $starttime; my $yday_offset; if ( $year == $gmyear ) { $yday_offset = ( $yday <=> $gmyday ); } elsif ( $year < $gmyear ) { $yday_offset = -1; } elsif ( $year > $gmyear ) { $yday_offset = 1; } my $gmoffset = ( $hour * 60 + $min ) - ( $gmhour * 60 + $gmmin ) + 1440 * $yday_offset; my $offset_string = sprintf( '%+03d%02d', int( $gmoffset / 60 ), $gmoffset % 60 ); if ($time_supplied) { return $offset_string; } else { $server_offset_string = $offset_string; } } } return $server_offset_string; } sub get_server_offset_in_seconds { if ( !defined $server_offset ) { if ( get_server_offset_as_offset_string() =~ m/([-+]?[0-9]{2})([0-9]{2})/ ) { my ( $hours, $minutes ) = ( $1, $2 ); my $seconds = ( ( abs($hours) * 60 * 60 ) + ( $minutes * 60 ) ); $server_offset = $hours < 0 ? "-$seconds" : $seconds; } else { $server_offset = 0; } } return $server_offset; } 1; } # --- END Cpanel/Time/Local.pm { # --- BEGIN Cpanel/FileUtils/Open.pm package Cpanel::FileUtils::Open; use strict; # use Cpanel::Fcntl (); sub sysopen_with_real_perms { ##no critic qw(RequireArgUnpacking) my ( $file, $mode, $custom_perms ) = ( @_[ 1 .. 3 ] ); if ( $mode && substr( $mode, 0, 1 ) eq 'O' ) { $mode = Cpanel::Fcntl::or_flags( split m<\|>, $mode ); } my ( $sysopen_perms, $original_umask ); if ( defined $custom_perms ) { $custom_perms &= 0777; $original_umask = umask( $custom_perms ^ 07777 ); $sysopen_perms = $custom_perms; } else { $sysopen_perms = 0666; } my $ret = sysopen( $_[0], $file, $mode, $sysopen_perms ); if ( defined $custom_perms ) { () = umask($original_umask); } return $ret; } 1; } # --- END Cpanel/FileUtils/Open.pm { # --- BEGIN Cpanel/Parser/Vars.pm package Cpanel::Parser::Vars; use strict; our $current_tag = ''; our $can_leave_cpanelaction = 1; our $buffer = ''; our $loaded_api = 0; our $trial_mode = 0; our $sent_headers = 0; our $live_socket_file; our $incpanelaction = 0; our $altmode = 0; our $jsonmode = 0; our $javascript = 0; our $title = 0; our $input = 0; our $style = 0; our $embtag = 0; our $textarea = 0; our $file = '[stdin]'; our $firstfile = '[stdin]'; our $trap_defaultfh = undef; # Known to be boolean. our %BACKCOMPAT; our $cptag; our $sent_content_type; 1; } # --- END Cpanel/Parser/Vars.pm { # --- BEGIN Cpanel/Encoder/Tiny/Rare.pm package Cpanel::Encoder::Tiny::Rare; use strict; use warnings; sub angle_bracket_decode { my ($string) = @_; $string =~ s{ < }{<}xmsg; $string =~ s{ > }{>}xmsg; return $string; } sub decode_utf8_html_entities { my $str = shift; $str =~ s/&\#(\d{4})\;/chr($1);/eg; return $str; } my %uri_encoding_cache = ( '"' => '%22', q{'} => '%27', '(' => '%28', ')' => '%29', q{ } => '%20', "\t" => '%09', ); sub css_encode_str { my $str = shift; $str =~ s{([\(\)\s"'])}{ $uri_encoding_cache{$1} || require Cpanel::Encoder::URI && Cpanel::Encoder::URI::uri_encode_str($1) }ge; return $str; } 1; } # --- END Cpanel/Encoder/Tiny/Rare.pm { # --- BEGIN Cpanel/Encoder/Tiny.pm package Cpanel::Encoder::Tiny; use strict; my %XML_ENCODE_MAP = ( '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' ); my %HTML_ENCODE_MAP = ( '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' ); my %HTML_DECODE_MAP = ( 'amp' => '&', 'lt' => '<', 'gt' => '>', 'quot' => '"', 'apos' => q{'}, '#39' => q{'} ); my $decode_regex = do { my $tmp = join( '|', keys %HTML_DECODE_MAP ); "&($tmp);"; }; sub angle_bracket_encode { my ($string) = @_; $string =~ s{<}{<}xmsg; $string =~ s{>}{>}xmsg; return $string; } sub safe_xml_encode_str { my $data = join( '', @_ ); return $data if $data !~ tr/&<>"'//; $data =~ s/([&<>"'])/$XML_ENCODE_MAP{$1}/sg; return $data; } sub safe_html_encode_str { return $_[0] if !defined $_[0] || ( !defined $_[1] && $_[0] !~ tr/&<>"'// ); my $data = defined $_[1] ? join( '', @_ ) : $_[0]; return $data if $data !~ tr/&<>"'//; $data =~ s/([&<>"'])/$HTML_ENCODE_MAP{$1}/sg; return $data; } sub safe_html_decode_str { return undef if !defined $_[0]; my $data = join( '', @_ ); $data =~ s/$decode_regex/$HTML_DECODE_MAP{$1}/g; return $data; } sub css_encode_str { require Cpanel::Encoder::Tiny::Rare; *css_encode_str = *Cpanel::Encoder::Tiny::Rare::css_encode_str; goto \&Cpanel::Encoder::Tiny::Rare::css_encode_str; } 1; } # --- END Cpanel/Encoder/Tiny.pm { # --- BEGIN Cpanel/Regex.pm package Cpanel::Regex; use strict; our $VERSION = '0.2.5'; my $dblquotedstr = q{"([^\\\\"]*(?:\\\\.[^\\\\"]*)*)"}; my $sglquotedstr = $dblquotedstr; $sglquotedstr =~ tr{"}{'}; my $zero_through_255 = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)'; our %regex = ( 'emailaddr' => '[a-zA-Z0-9!#\$\-=?^_{}~]+(?:\.[a-zA-Z0-9!#\$\-=?^_{}~]+)*(?:\+[a-zA-Z0-9 \.=\-\_]+)*\@[\da-zA-Z](?:[-\da-zA-Z]*[\da-zA-Z])?(?:\.[\da-zA-Z](?:[-\da-zA-Z]*[\da-zA-Z])?)*', 'oneplusdot' => '\.+', 'oneplusspacetab' => '[\s\t]+', 'multipledot' => '\.{2,}', 'commercialat' => '\@', 'plussign' => '\+', 'singledot' => '\.', 'newline' => '\n', 'doubledot' => '\.\.', 'lineofdigits' => '^\d+$', 'lineofnonprintingchars' => '^[\s\t]*$', 'getemailtransport' => '^from\s+.*\s+by\s+\S+\s+with\s+(\S+)', 'getreceivedfrom' => '^from\s+(.*)\s+by\s+', 'emailheaderterminator' => '^[\r\n]*$', 'forwardslash' => '\/', 'backslash' => chr(92) x 4, 'singlequote' => q('), 'doublequote' => '"', 'allspacetabchars' => '[\s\t]*', 'beginswithspaceortabs' => '^[\s\t]', doublequotedstring => $dblquotedstr, singlequotedstring => $sglquotedstr, DUNS => '[0-9]{2}(?:-[0-9]{3}-[0-9]{4}|[0-9]{7})', YYYY_MM_DD => '[0-9]{4}-(?:1[012]|0[1-9])-(?:3[01]|[12][0-9]|0[1-9])', ipv4 => "(?:$zero_through_255\.){3}$zero_through_255", iso_z_time => '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', ); 1; } # --- END Cpanel/Regex.pm { # --- BEGIN Cpanel/Carp.pm package Cpanel::Carp; use strict; # use Cpanel::Parser::Vars (); our ( $SHOW_TRACE, $OUTPUT_FORMAT, $VERBOSE ) = ( 1, 'text', 0 ); my $__CALLBACK_AFTER_DIE_SPEW; # Set when we need to run a code ref after spewing on die my $error_count = 0; sub import { return enable(); } sub enable { my ( $callback_before_warn_or_die_spew, # Runs before the spew on warn or die, currently used in cpanel to ensure we emit headers before body in the event of a warn or die spew $callback_before_die_spew, # Runs before the spew on die, not currently used $callback_after_die_spew, # Runs after the spew on die, currently used in whostmgr to ensure we emit the javascript footer when we die to avoid the UI breaking ) = @_; $SIG{'__WARN__'} = sub { ## no critic qw(Variables::RequireLocalizedPunctuationVars) my @caller = caller(1); return if defined $caller[3] && index( $caller[3], 'eval' ) > -1; # Case 35335: Quiet spurious warn errors from evals ++$error_count; my $time = time(); my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($time); my ( $gmmin, $gmhour, $gmday ) = ( gmtime($time) )[ 1, 2, 3 ]; my $gmoffset = ( $hour * 60 + $min ) - ( $gmhour * 60 + $gmmin ) + 1440 * ( $mday <=> $gmday ); my $tz = sprintf( '%+03d%02d', int( $gmoffset / 60 ), $gmoffset % 60 ); my $error_timestamp = sprintf( '%04d-%02d-%02d %02d:%02d:%02d %s', $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $tz ); my $longmess; my $ignorable; if ( UNIVERSAL::isa( $_[0], 'Cpanel::Exception' ) ) { $longmess = Cpanel::Carp::safe_longmess( $_[0]->to_locale_string() ); } elsif ( ref $_[0] eq 'Template::Exception' ) { $longmess = Cpanel::Carp::safe_longmess( "Template::Exception:\n\t[TYPE]=[" . $_[0]->[0] . "]\n\t[INFO]=[" . $_[0]->[1] . "]\n\t[TEXT]=[" . ( ref $_[0]->[2] eq 'SCALAR' ? ${ $_[0]->[2] } : $_[0]->[2] ) . "]\n" ); } else { $longmess = Cpanel::Carp::safe_longmess(@_); $ignorable = 1 if index( $_[0], 'Use of uninitialized value' ) == 0; } my $error_container_text = 'A warning occurred while processing this directive.'; my $current_file = $Cpanel::Parser::Vars::file || 'unknown'; print STDERR "[$error_timestamp] warn [Internal Warning while parsing $current_file $$] $longmess\n\n"; return if ( $OUTPUT_FORMAT eq 'suppress' || $OUTPUT_FORMAT eq 'supress' || $ENV{'CPANEL_PHPENGINE'} ); return if $ignorable && !$VERBOSE; _run_callback_without_die_handler($callback_before_warn_or_die_spew) if $callback_before_warn_or_die_spew; if ( $OUTPUT_FORMAT eq 'html' ) { if ($SHOW_TRACE) { _print_without_die_handler( _generate_html_error_message( 'warn', $error_container_text, $longmess ) ); } else { _print_without_die_handler(qq{[$error_container_text]}); } } elsif ( $OUTPUT_FORMAT eq 'xml' ) { _print_without_die_handler("$error_container_text"); } else { _print_without_die_handler("[$error_container_text]\n"); } }; $SIG{'__DIE__'} = sub { ## no critic qw(Variables::RequireLocalizedPunctuationVars) return if $^S; die $_[0] unless defined $^S; delete $SIG{'__DIE__'}; _run_callback_without_die_handler($callback_before_warn_or_die_spew) if $callback_before_warn_or_die_spew; _run_callback_without_die_handler($callback_before_die_spew) if $callback_before_die_spew; $__CALLBACK_AFTER_DIE_SPEW = $callback_after_die_spew; goto \&spew_on_die; }; return 1; } sub spew_on_die { ## no critic qw(Subroutines::RequireArgUnpacking) my ($err) = @_; ++$error_count; my $time = time(); my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($time); my ( $gmmin, $gmhour, $gmday ) = ( gmtime($time) )[ 1, 2, 3 ]; my $gmoffset = ( $hour * 60 + $min ) - ( $gmhour * 60 + $gmmin ) + 1440 * ( $mday <=> $gmday ); my $tz = sprintf( '%+03d%02d', int( $gmoffset / 60 ), $gmoffset % 60 ); my $error_timestamp = sprintf( '%04d-%02d-%02d %02d:%02d:%02d %s', $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $tz ); my $error_text; if ( UNIVERSAL::isa( $err, 'Cpanel::Exception' ) ) { $error_text = Cpanel::Carp::safe_longmess( $err->to_locale_string() ); } elsif ( UNIVERSAL::isa( $err, 'Template::Exception' ) ) { $error_text = Cpanel::Carp::safe_longmess( "Template::Exception:\n\t[TYPE]=[" . $err->type() . "]\n\t[INFO]=[" . $err->info() . "]\n\t[TEXT]=[" . $err->text() . "]\n" ); } else { $error_text = Cpanel::Carp::safe_longmess(@_); } my $current_file = $Cpanel::Parser::Vars::file || 'unknown'; print STDERR "[$error_timestamp] die [Internal Death while parsing $current_file $$] $error_text\n\n"; return if ( $OUTPUT_FORMAT eq 'suppress' || $OUTPUT_FORMAT eq 'supress' || $ENV{'CPANEL_PHPENGINE'} ); my $error_container_text = 'A fatal error or timeout occurred while processing this directive.'; if ( $OUTPUT_FORMAT eq 'html' ) { if ($SHOW_TRACE) { _print_without_die_handler( _generate_html_error_message( 'error', $error_container_text, $error_text ) ); } else { _print_without_die_handler(qq{[$error_container_text]}); } } elsif ( $OUTPUT_FORMAT eq 'xml' ) { _print_without_die_handler("[$error_container_text]"); } else { _print_without_die_handler("[$error_container_text]\n"); } _run_callback_without_die_handler($__CALLBACK_AFTER_DIE_SPEW) if $__CALLBACK_AFTER_DIE_SPEW; return; } my @SAFE_LONGMESS_KEY_REGEXP_ITEMS = ( '(?(); } sub _generate_html_error_message { my ( $type, $error_container_message, $error_message ) = @_; require Cpanel::Encoder::Tiny; my $safe_error_message = Cpanel::Encoder::Tiny::safe_html_encode_str($error_message); return qq[
$error_container_message [show] [close]
]; } sub safe_longmess { require Carp; $Carp::Internal{'Cpanel::Carp'} = 1; return sanitize_longmess( scalar Carp::longmess(@_) ); } my ( $key_regexp, $key_regexp_double, $function_regexp ); sub sanitize_longmess { _build_regexes() if !$key_regexp; return join( "\n", map { ( tr{'"}{} && ( m{$key_regexp}o || m{$key_regexp_double}o || ( ( $_ =~ m{^[ \t]*([^\(]+)\(} )[0] || '' ) =~ m{$function_regexp}o ) ) # matches a line that needs to be sanitized && _sanitize_line($_); # sanitize $_ } split( m{\n}, $_[0] ) ) . "\n"; } sub _sanitize_line { # Operates directly on $_[0] for speed if ( !$INC{'Cpanel/Regex.pm'} ) { # PPI NO PARSE - inc check local $@; eval { local $SIG{__DIE__}; local $SIG{__WARN__}; require Cpanel::Regex; # PPI NO PARSE - inc check }; } $_[0] =~ s/$Cpanel::Regex::regex{'singlequotedstring'}/__CPANEL_HIDDEN__/go if index( $_[0], q{'} ) != -1; $_[0] =~ s/$Cpanel::Regex::regex{'doublequotedstring'}/__CPANEL_HIDDEN__/go if index( $_[0], q{"} ) != -1; return 1; } sub _build_regexes { my $key_regexp_items = join '|', @SAFE_LONGMESS_KEY_REGEXP_ITEMS; $key_regexp = qr< ' .*? (?: $key_regexp_items ) .*? ' \s* , >x; $key_regexp_double = $key_regexp; $key_regexp_double =~ tr{'}{"}; # "' fix for poor editors my $function_regexp_items = join '|', @SAFE_LONGMESS_FUNCTION_REGEXP_ITEMS; $function_regexp = qr< :: .*? (?: $function_regexp_items ) .*? $ >x; return 1; } 1; } # --- END Cpanel/Carp.pm { # --- BEGIN Cpanel/Set.pm package Cpanel::Set; use strict; use warnings; sub difference { my ($super_ar) = @_; my %lookup; @lookup{ map { @$_ } @_[ 1 .. $#_ ] } = (); return grep { !exists $lookup{$_} } @$super_ar; } sub intersection { my ( $super_ar, $sub_ar ) = @_; my %lookup; @lookup{@$sub_ar} = (); return grep { exists $lookup{$_} } @$super_ar; } 1; } # --- END Cpanel/Set.pm { # --- BEGIN Cpanel/SafeFileLock.pm package Cpanel::SafeFileLock; use strict; use warnings; use constant { _ENOENT => 2, _EDQUOT => 122, DEBUG => 0, MAX_LOCKFILE_SIZE => 8192, }; sub new { my ( $class, $path_to_lockfile, $fh, $path_to_file_being_locked ) = @_; if ( scalar @_ != 4 ) { die 'Usage: Cpanel::SafeFileLock->new($path_to_lockfile, $fh, $path_to_file_being_locked)'; } if ($fh) { write_lock_contents( $fh, $path_to_lockfile ) or return; } my $self = bless [ $path_to_lockfile, $fh, $path_to_file_being_locked, ], $class; push @$self, @{ $self->stat_ar() }[ 1, 9 ]; return $self; } sub new_before_lock { my ( $class, $path_to_lockfile, $path_to_file_being_locked ) = @_; if ( scalar @_ != 3 ) { die 'Usage: Cpanel::SafeFileLock->new_before_lock($path_to_lockfile, $path_to_file_being_locked)'; } return bless [ $path_to_lockfile, undef, $path_to_file_being_locked, ], $class; } sub set_filehandle_and_unlinker_after_lock { $_[0][1] = $_[1]; push @{ $_[0] }, @{ $_[0]->stat_ar() }[ 1, 9 ]; $_[0][5] = $_[2]; return $_[0]; } sub get_path { return $_[0]->[0]; } sub get_path_to_file_being_locked { return $_[0]->[2] // die "get_path_to_file_being_locked requires the object to be instantiated with the path_to_file_being_locked"; } sub set_filehandle { $_[0][1] = $_[1]; return $_[0]; } sub get_filehandle { return $_[0]->[1]; } sub get_inode { return $_[0]->[3]; } sub get_mtime { return $_[0]->[4]; } sub get_path_fh_inode_mtime { return @{ $_[0] }[ 0, 1, 3, 4 ]; } sub stat_ar { return [ stat( ( $_[0]->[1] && fileno( $_[0]->[1] ) ) ? $_[0]->[1] : $_[0]->[0] ) ]; } sub lstat_ar { return [ $_[0]->[1] && fileno( $_[0]->[1] ) ? stat( $_[0]->[1] ) : lstat( $_[0]->[0] ) ]; } sub close { return close $_[0]->[1] if ref $_[0]->[1]; $_[0]->[5] = undef; return; } sub write_lock_contents { ## no critic qw(Subroutines::RequireArgUnpacking) -- only unpack on the failure case local $!; if (DEBUG) { require Cpanel::Carp; return 1 if syswrite( $_[0], "$$\n$0\n" . Cpanel::Carp::safe_longmess() . "\n" ); } return 1 if syswrite( $_[0], "$$\n$0\n" ); my ( $fh, $path_to_lockfile ) = @_; my $write_error = $!; CORE::close($fh); unlink $path_to_lockfile; require Cpanel::Exception; die Cpanel::Exception::create( 'IO::FileWriteError', [ 'path' => $path_to_lockfile, 'error' => $write_error ] ); } sub fetch_lock_contents_if_exists { my ($lockfile) = @_; die 'Need lock file!' if !$lockfile; open my $lockfile_fh, '<:stdio', $lockfile or do { return if $! == _ENOENT(); die "open($lockfile): $!"; }; my $buffer; my $read_result = read( $lockfile_fh, $buffer, MAX_LOCKFILE_SIZE ); if ( !defined $read_result ) { die "read($lockfile): $!"; } my ( $pid_line, $lock_name, $lock_obj ) = split( /\n/, $buffer, 3 ); chomp($lock_name) if length $lock_name; my ($lock_pid) = $pid_line && ( $pid_line =~ m/(\d+)/ ); return ( $lock_pid, $lock_name || 'unknown', $lock_obj || 'unknown', $lockfile_fh ); } 1; } # --- END Cpanel/SafeFileLock.pm { # --- BEGIN Cpanel/FHUtils/Tiny.pm package Cpanel::FHUtils::Tiny; use strict; use warnings; sub is_a { return !ref $_[0] ? 0 : ( ref $_[0] eq 'IO::Handle' || ref $_[0] eq 'GLOB' || UNIVERSAL::isa( $_[0], 'GLOB' ) ) ? 1 : 0; } sub are_same { my ( $fh1, $fh2 ) = @_; return 1 if $fh1 eq $fh2; if ( fileno($fh1) && ( fileno($fh1) != -1 ) && fileno($fh2) && ( fileno($fh2) != -1 ) ) { return 1 if fileno($fh1) == fileno($fh2); } return 0; } sub to_bitmask { my @fhs = @_; my $mask = q<>; for my $fh (@fhs) { vec( $mask, ref($fh) ? fileno($fh) : $fh, 1 ) = 1; } return $mask; } 1; } # --- END Cpanel/FHUtils/Tiny.pm { # --- BEGIN Cpanel/Hash.pm package Cpanel::Hash; use strict; *get_fastest_hash = \&fnv1a_32; use constant FNV1_32A_INIT => 0x811c9dc5; use constant FNV_32_PRIME => 0x01000193; use constant FNV_32_MOD => 2**32; # AKA 0x100000000 but that it non-portable; sub fnv1a_32 { my $fnv32 = FNV1_32A_INIT(); ( $fnv32 = ( ( $fnv32 ^ $_ ) * FNV_32_PRIME() ) % FNV_32_MOD ) for unpack( 'C*', $_[0] ); return $fnv32; } 1; } # --- END Cpanel/Hash.pm { # --- BEGIN Cpanel/SafeFile/LockInfoCache.pm package Cpanel::SafeFile::LockInfoCache; use strict; use warnings; # use Cpanel::SafeFileLock (); sub new { my ( $class, $pathname ) = @_; die 'need path!' if !$pathname; return bless { _path => $pathname }, $class; } sub get { my ( $self, $inode, $mtime ) = @_; die 'Need an inode & an mtime!' if !defined $inode || !defined $mtime; if ( !exists $self->{"_inode_${inode}_$mtime"} ) { my ( $pid, $name, $obj, $fh ) = Cpanel::SafeFileLock::fetch_lock_contents_if_exists( $self->{'_path'} ); if ($pid) { my ( $real_inode, $real_mtime ) = ( stat $fh )[ 1, 9 ]; $self->{"_inode_${real_inode}_$real_mtime"} = [ $pid, $name, $obj ]; } } return $self->{"_inode_${inode}_$mtime"} ||= undef; } 1; } # --- END Cpanel/SafeFile/LockInfoCache.pm { # --- BEGIN Cpanel/SafeFile/LockWatcher.pm package Cpanel::SafeFile::LockWatcher; use strict; use warnings; use constant _ENOENT => 2; use constant _FILEHANDLE_TTL => 2; sub new { my ( $class, $lockfile ) = @_; my $self = bless { _path => $lockfile, _new => 1 }, $class; return $self->reload_from_disk(); } sub reload_from_disk { my ($self) = @_; my $old_inode = $self->{'inode'}; @{$self}{qw( inode uid size mtime)} = $self->_get_inode_uid_size_mtime(); if ( delete $self->{'_new'} ) { $self->{'changed'} = 0; } else { $self->{'changed'} = ( $self->{'inode'} || 0 ) != ( $old_inode || 0 ) ? 1 : 0; } return $self; } sub _get_inode_uid_size_mtime { my ($self) = @_; my ( $inode, $uid, $size, $mtime ); local $!; if ( open my $fh, '<', $self->{'_path'} ) { ( $inode, $uid, $size, $mtime ) = ( stat $fh )[ 1, 4, 7, 9 ]; $self->_add_fh_if_needed( $fh, $inode ); } elsif ( $! != _ENOENT ) { die "open(<, $self->{'_path'}): $!"; } return ( $inode, $uid, $size, $mtime ); } sub _add_fh_if_needed { my ( $self, $fh, $inode ) = @_; my $now = time; my $fhs_hr = $self->{'_time_fhs'} //= {}; my $seen_inode = 0; for my $time ( keys %$fhs_hr ) { if ( ( $now - $time ) > _FILEHANDLE_TTL() ) { delete $fhs_hr->{$time}; next; } if ( !$seen_inode ) { foreach my $entry ( @{ $fhs_hr->{$time} } ) { if ( $entry->[1] == $inode ) { $seen_inode = 1; last; } } } } return if $seen_inode; push @{ $fhs_hr->{ time() } }, [ $fh, $inode ]; return; } 1; } # --- END Cpanel/SafeFile/LockWatcher.pm { # --- BEGIN Cpanel/Context.pm package Cpanel::Context; use strict; use warnings; # use Cpanel::Exception (); sub must_be_list { return 1 if ( caller(1) )[5]; # 5 = wantarray my $msg = ( caller(1) )[3]; # 3 = subroutine $msg .= $_[0] if defined $_[0]; return _die_context( 'list', $msg ); } sub must_not_be_scalar { my ($message) = @_; my $wa = ( caller(1) )[5]; # 5 = wantarray if ( !$wa && defined $wa ) { _die_context( 'list or void', $message ); } return 1; } sub must_not_be_void { return if defined( ( caller 1 )[5] ); return _die_context('scalar or list'); } sub _die_context { my ( $context, $message ) = @_; local $Carp::CarpInternal{__PACKAGE__} if $INC{'Carp.pm'}; my $to_throw = length $message ? "Must be $context context ($message)!" : "Must be $context context!"; die Cpanel::Exception::create_raw( 'ContextError', $to_throw ); } 1; } # --- END Cpanel/Context.pm { # --- BEGIN Cpanel/Pack.pm package Cpanel::Pack; use strict; sub new { my ( $class, $template_ar ) = @_; if ( @$template_ar % 2 ) { die "Cpanel::Pack::new detected an odd number of elements in hash assignment!"; } my $self = bless { 'template_str' => '', 'keys' => [], }, $class; my $ti = 0; while ( $ti < $#$template_ar ) { push @{ $self->{'keys'} }, $template_ar->[$ti]; $self->{'template_str'} .= $template_ar->[ 1 + $ti ]; $ti += 2; } return $self; } sub unpack_to_hashref { ## no critic (RequireArgUnpacking) my %result; @result{ @{ $_[0]->{'keys'} } } = unpack( $_[0]->{'template_str'}, $_[1] ); return \%result; } sub pack_from_hashref { my ( $self, $opts_ref ) = @_; no warnings 'uninitialized'; return pack( $self->{'template_str'}, @{$opts_ref}{ @{ $self->{'keys'} } } ); } sub sizeof { my ($self) = @_; return ( $self->{'sizeof'} ||= length pack( $self->{'template_str'}, () ) ); } sub malloc { my ($self) = @_; return pack( $self->{'template_str'} ); } 1; } # --- END Cpanel/Pack.pm { # --- BEGIN Cpanel/Syscall.pm package Cpanel::Syscall; use strict; my %NAME_TO_NUMBER = qw( close 3 fcntl 72 lchown 94 getrlimit 97 getsid 124 gettimeofday 96 sendfile 40 setrlimit 160 splice 275 write 1 setsid 112 getsid 124 inotify_init1 294 inotify_add_watch 254 inotify_rm_watch 255 setresuid 117 setresgid 119 setgroups 116 umount2 166 ); sub name_to_number { my ($name) = @_; return $NAME_TO_NUMBER{$name} || _die_unknown_syscall($name); } sub _die_unknown_syscall { my ($name) = @_; die "Unknown system call: “$name”"; } sub syscall { ##no critic qw(RequireArgUnpacking) local $!; _die_unknown_syscall( $_[0] ) unless defined $_[0] && $NAME_TO_NUMBER{ $_[0] }; my $ret = CORE::syscall( $NAME_TO_NUMBER{ $_[0] }, scalar @_ > 1 ? @_[ 1 .. $#_ ] : () ); if ( ( $ret == -1 ) && $! ) { if ( $INC{'Cpanel/Exception.pm'} ) { die Cpanel::Exception::create( 'SystemCall', [ name => $_[0], error => $!, arguments => [ @_[ 1 .. $#_ ] ] ] ); } else { die "Failed system call “$_[0]”: $!"; } } return $ret; } 1; } # --- END Cpanel/Syscall.pm { # --- BEGIN Cpanel/Inotify.pm package Cpanel::Inotify; use strict; use warnings; # use Cpanel::Autodie (); # use Cpanel::Context (); # use Cpanel::Exception (); # use Cpanel::Fcntl::Constants (); # use Cpanel::Pack (); # use Cpanel::Syscall (); use constant POLL_SIZE => 65536; use constant READ_TEMPLATE => ( wd => 'i', #int Watch descriptor mask => 'I', #uint32_t Mask of events cookie => 'I', #uint32_t Unique cookie associating related events len => 'I', #uint32_t Size of “name” field ); my %add_flags; my %read_flags; my %init1_flag; my $UNPACK_OBJ; my $UNPACK_SIZE; sub new { my ( $class, %opts ) = @_; if ( !$UNPACK_OBJ ) { $UNPACK_OBJ = Cpanel::Pack->new( [ READ_TEMPLATE() ] ); $UNPACK_SIZE = $UNPACK_OBJ->sizeof(); _setup_flags(); } my @given_flags = $opts{'flags'} ? @{ $opts{'flags'} } : (); my $mask = 0; for my $f (@given_flags) { $mask |= $init1_flag{$f} || do { die Cpanel::Exception->create_raw("Invalid inotify_init1 flag: “$f”"); }; } my $fd = Cpanel::Syscall::syscall( 'inotify_init1', $mask ); my %self = ( _fd => $fd, ); Cpanel::Autodie::open( $self{'_fh'}, '<&=', $fd ); return bless \%self, $class; } sub add { my ( $self, $path, %opts ) = @_; my @flags = @{ $opts{'flags'} }; my $mask = 0; for my $f (@flags) { $mask |= $add_flags{$f} || do { die Cpanel::Exception->create_raw("Invalid inotify_add_watch flag: “$f”"); }; } my $wd = Cpanel::Syscall::syscall( 'inotify_add_watch', $self->{'_fd'}, $path, $mask, ); if ( $wd < 1 ) { die Cpanel::Exception->create_raw("inotify watch descriptor “$wd” means something is wrong?"); } $self->{'_watches'}{$wd} = $path; return $wd; } sub remove { my ( $self, $wd ) = @_; Cpanel::Syscall::syscall( 'inotify_rm_watch', $self->{'_fd'}, $wd ); return; } sub poll { my ($self) = @_; Cpanel::Context::must_be_list(); my $buf = q<>; Cpanel::Autodie::sysread_sigguard( $self->{'_fh'}, $buf, POLL_SIZE() ); my @events; while ( length $buf ) { my $evt = $UNPACK_OBJ->unpack_to_hashref( substr( $buf, 0, $UNPACK_SIZE, q<> ) ); $evt->{'name'} = substr( $buf, 0, delete( $evt->{'len'} ), q<> ); $evt->{'name'} =~ s<\0+\z><>; #trailing NULs $evt->{'flags'} = _mask_to_flags_ar( delete $evt->{'mask'} ); push @events, $evt; } return @events; } sub fileno { my ($self) = @_; return fileno( $self->{'_fh'} ); } sub _mask_to_flags_ar { my ($mask) = @_; my @flags; for my $k ( keys %read_flags ) { push @flags, $k if $mask & $read_flags{$k}; } @flags = sort @flags; return \@flags; } sub _setup_flags { my %flag_num = ( ACCESS => 0x1, # File was accessed MODIFY => 0x2, # File was modified ATTRIB => 0x4, # Metadata changed CLOSE_WRITE => 0x8, # File opened for writing was closed CLOSE_NOWRITE => 0x10, # File not opened for writing was closed OPEN => 0x20, # File was opened MOVED_FROM => 0x40, # File was moved from X MOVED_TO => 0x80, # File was moved to Y CREATE => 0x100, # Subfile was created DELETE => 0x200, # Subfile was deleted DELETE_SELF => 0x400, # Self was deleted MOVE_SELF => 0x800, # Self was moved ); %read_flags = ( %flag_num, UNMOUNT => 0x00002000, # Backing fs was unmounted Q_OVERFLOW => 0x00004000, # Event queued overflowed ('wd' is -1) IGNORED => 0x00008000, # Watch was removed ISDIR => 0x40000000, # event occurred against dir ); %add_flags = ( %flag_num, ONLYDIR => 0x01000000, # only watch the path if it is a directory DONT_FOLLOW => 0x02000000, # don't follow a sym link EXCL_UNLINK => 0x04000000, # exclude events on unlinked objects MASK_ADD => 0x20000000, # add to the mask of an already existing watch ONESHOT => 0x80000000, # only send event once CLOSE => $read_flags{'CLOSE_WRITE'} | $read_flags{'CLOSE_NOWRITE'}, MOVE => $read_flags{'MOVED_FROM'} | $read_flags{'MOVED_TO'}, ); my $mask = 0; $mask |= $_ for values %flag_num; $add_flags{'ALL_EVENTS'} = $mask; %init1_flag = ( CLOEXEC => $Cpanel::Fcntl::Constants::O_CLOEXEC, NONBLOCK => $Cpanel::Fcntl::Constants::O_NONBLOCK, ); return; } 1; } # --- END Cpanel/Inotify.pm { # --- BEGIN Cpanel/SafeFile.pm package Cpanel::SafeFile; use strict; use warnings; # use Cpanel::TimeHiRes (); # use Cpanel::Fcntl::Constants (); # use Cpanel::SafeFileLock (); # use Cpanel::FHUtils::Tiny (); use constant { _EWOULDBLOCK => 11, _EACCES => 13, _EDQUOT => 122, _ENOENT => 2, _EINTR => 4, _EEXIST => 17, _ENOSPC => 28, _EPERM => 1, MAX_LOCK_CREATE_ATTEMPTS => 90, NO_PERM_TO_WRITE_TO_DOTLOCK_DIR => -1, INOTIFY_FILE_DISAPPEARED => 2, CREATE_FCNTL_VALUE => ( $Cpanel::Fcntl::Constants::O_WRONLY | $Cpanel::Fcntl::Constants::O_EXCL | $Cpanel::Fcntl::Constants::O_CREAT | $Cpanel::Fcntl::Constants::O_NONBLOCK ), UNLOCK_FCNTL_VALUE => $Cpanel::Fcntl::Constants::LOCK_UN, LOCK_FILE_PERMS => 0644, DEFAULT_LOCK_WAIT_TIME => 196, MAX_LOCK_WAIT_TIME => 400, MAX_LOCK_FILE_LENGTH => 225, }; $Cpanel::SafeFile::VERSION = '5.0'; my $OVERWRITE_FCNTL_VALUE; my $verbose = 0; # initialized in safelock our $LOCK_WAIT_TIME; #allow lock wait time to be overwritten my $OPEN_LOCKS = 0; our $TIME_BETWEEN_DOTLOCK_CHECKS = 0.3; our $TIME_BETWEEN_FLOCK_CHECKS = 0.05; our $MAX_FLOCK_WAIT = 60; # allowed to be overwritten in tests our $_SKIP_DOTLOCK_WHEN_NO_PERMS = 0; our $_SKIP_WARN_ON_OPEN_FAIL = 0; my $DOUBLE_LOCK_DETECTED = 4096; sub safeopen { #fh, open()-style mode, path my ( $mode, $file ) = _get_open_args( @_[ 1 .. $#_ ] ); my $open_method_coderef = sub { my $ret = open( $_[0], $_[1], $_[2] ) || do { _log_warn("open($_[1], $_[2]): $!"); return undef; }; return $ret; }; return _safe_open( $_[0], $mode, $file, $open_method_coderef, 'safeopen' ); } sub safesysopen_no_warn_on_fail { local $_SKIP_WARN_ON_OPEN_FAIL = 1; return safesysopen(@_); } sub safesysopen_skip_dotlock_if_not_root { local $_SKIP_DOTLOCK_WHEN_NO_PERMS = $> == 0 ? 0 : 1; return safesysopen(@_); } sub safeopen_skip_dotlock_if_not_root { local $_SKIP_DOTLOCK_WHEN_NO_PERMS = $> == 0 ? 0 : 1; return safeopen(@_); } sub safelock_skip_dotlock_if_not_root { local $_SKIP_DOTLOCK_WHEN_NO_PERMS = $> == 0 ? 0 : 1; return safelock(@_); } sub safereopen { ##no critic qw(RequireArgUnpacking) my $fh = shift; if ( !$fh ) { require Cpanel::Carp; die Cpanel::Carp::safe_longmess("Undefined filehandle not allowed!"); } elsif ( !fileno $fh ) { require Cpanel::Carp; die Cpanel::Carp::safe_longmess("Closed filehandle ($fh) not allowed!"); } my ( $mode, $file ) = _get_open_args(@_); my $open_method_coderef = sub { return open( $_[0], $_[1], $_[2] ) || do { _log_warn("open($_[1], $_[2]): $!"); return undef; }; }; return _safe_re_open( $fh, $mode, $file, $open_method_coderef, 'safereopen' ); } sub safesysopen { ##no critic qw(RequireArgUnpacking) my ( $file, $open_mode, $custom_perms ) = ( @_[ 1 .. 3 ] ); my ( $sysopen_perms, $original_umask ); $open_mode = _sanitize_open_mode($open_mode); my $open_method_coderef = sub { return sysopen( $_[0], $_[2], $_[1], $sysopen_perms ) || do { _log_warn("open($_[2], $_[1], $sysopen_perms): $!") unless $_SKIP_WARN_ON_OPEN_FAIL; return undef; }; }; if ( defined $custom_perms ) { $custom_perms &= 0777; $original_umask = umask( $custom_perms ^ 07777 ); $sysopen_perms = $custom_perms; } else { $sysopen_perms = 0666; } my $lock_ref; local $@; my $ok = eval { $lock_ref = _safe_open( $_[0], $open_mode, $file, $open_method_coderef, 'safesysopen' ); 1; }; if ( defined $custom_perms ) { umask($original_umask); } die if !$ok; return $lock_ref; } sub safeclose { my ( $fh, $lockref, $do_something_before_releasing_lock ) = @_; if ( $do_something_before_releasing_lock && ref $do_something_before_releasing_lock eq 'CODE' ) { $do_something_before_releasing_lock->(); } my $success = 1; if ( $fh && defined fileno $fh ) { flock( $fh, UNLOCK_FCNTL_VALUE ) or _log_warn( "flock(LOCK_UN) on “" . $lockref->get_path() . "” failed with error: $!" ); # LOCK_UN $success = close $fh; } my $safe_unlock = safeunlock($lockref); $OPEN_LOCKS-- if ( $safe_unlock && $success ); return ( $safe_unlock && $success ); } sub safelock { my ($file) = @_; my $lock_obj = _safelock($file); return if !ref $lock_obj; return $lock_obj; } sub _safelock { my ($file) = @_; if ( !$file || $file =~ tr/\0// ) { _log_warn('safelock: Invalid arguments'); return; } $verbose ||= ( _verbose_flag_file_exists() ? 1 : -1 ); my $lockfile = _calculate_lockfile($file); my $safefile_lock = Cpanel::SafeFileLock->new_before_lock( $lockfile, $file ); my ( $lock_status, $lock_fh, $attempts, $last_err ); { local $@; while ( ++$attempts < MAX_LOCK_CREATE_ATTEMPTS ) { ( $lock_status, $lock_fh ) = _lock_wait( $file, $safefile_lock, $lockfile ); last if $lock_status; $last_err = $!; if ( $lock_fh && $lock_fh == $DOUBLE_LOCK_DETECTED ) { return 0; } } } if ( $lock_fh == 1 ) { return 1; } elsif ( $lock_status && $lock_fh ) { return $safefile_lock; } _log_warn( 'safelock: waited for lock (' . $lockfile . ') ' . $attempts . ' times' ); require Cpanel::Exception; die Cpanel::Exception::create( 'IO::FileCreateError', [ 'path' => $lockfile, 'error' => $last_err ] ); } sub _write_temp_lock_file { my ($lockfile) = @_; my $temp_file = sprintf( '%s-%x-%x-%x', $lockfile, substr( rand, 2 ), scalar( reverse time ), scalar( reverse $$ ), ); my ( $ok, $fh_or_err ) = _create_lockfile($temp_file); if ( !$ok ) { if ( $fh_or_err == _EPERM() || $fh_or_err == _EACCES() ) { local $!; my $lock_dir = _getdir($lockfile); if ( !-w $lock_dir ) { if ($_SKIP_DOTLOCK_WHEN_NO_PERMS) { # A hack to allow /etc/valiases to still be flock()ed until we can refactor return ( NO_PERM_TO_WRITE_TO_DOTLOCK_DIR, $fh_or_err ); } else { _log_warn("safelock: Failed to create a lockfile '$temp_file' in the directory '$lock_dir' that isn't writable: $fh_or_err"); } } } return ( 0, $fh_or_err ); } Cpanel::SafeFileLock::write_lock_contents( $fh_or_err, $temp_file ); return ( $temp_file, $fh_or_err ); } sub _try_to_install_lockfile { my ( $temp_file, $lockfile ) = @_; link( $temp_file => $lockfile ) or do { return 0 if $! == _EEXIST; require Cpanel::Exception; die Cpanel::Exception::create( 'IO::LinkError', [ oldpath => $temp_file, newpath => $lockfile, error => $! ] ); }; return 1; } sub safeunlock { my $lockref = shift; if ( !$lockref ) { _log_warn('safeunlock: Invalid arguments'); return; } elsif ( !ref $lockref ) { return 1 if $lockref eq '1'; # No lock file created so just succeed $lockref = Cpanel::SafeFileLock->new( $lockref, undef, undef ); if ( !$lockref ) { _log_warn("safeunlock: failed to generate a Cpanel::SafeFileLock object from a path"); return; } } my ( $lock_path, $fh, $lock_inode, $lock_mtime ) = $lockref->get_path_fh_inode_mtime(); my ( $filesys_lock_ino, $filesys_lock_mtime ) = ( lstat $lock_path )[ 1, 9 ]; if ( $fh && !defined fileno($fh) ) { return 1; } elsif ( !$filesys_lock_mtime ) { _log_warn( 'Lock on ' . $lockref->get_path_to_file_being_locked() . ' lost!' ); $lockref->close(); return; # return false on false } elsif ( $lock_inode && ( $lock_inode == $filesys_lock_ino ) && $lock_path && ( $lock_mtime == $filesys_lock_mtime ) ) { unlink $lock_path or do { _log_warn("Could not unlink lock file “$lock_path” as ($>/$)): $!\n"); $lockref->close(); return; # return false on false }; return $lockref->close(); } $lockref->close(); my ( $lock_pid, $lock_name, $lock_obj ) = Cpanel::SafeFileLock::fetch_lock_contents_if_exists($lock_path); if ($lock_pid) { $lock_inode ||= 0; $lock_mtime ||= 0; _log_warn("[$$] Attempt to unlock file that was locked by another process [LOCK_PATH]=[$lock_path] [LOCK_PID]=[$lock_pid] [LOCK_PROCESS]=[$lock_name] [LOCK_INODE]=[$filesys_lock_ino] [LOCK_MTIME]=[$filesys_lock_mtime] -- [NON_LOCK_PID]=[$$] [NON_LOCK_PROCESS]=[$0] [NON_LOCK_INODE]=[$lock_inode] [NON_LOCK_MTIME]=[$lock_mtime]"); } return; } sub _safe_open { my ( undef, $open_mode, $file, $open_method_coderef, $open_method ) = @_; if ( !defined $open_mode || !$open_method_coderef || !$file || $file =~ tr/\0// ) { _log_warn('_safe_open: Invalid arguments'); return; } elsif ( defined $_[0] ) { my $fh_type = ref $_[0]; if ( !Cpanel::FHUtils::Tiny::is_a( $_[0] ) ) { _log_warn("Invalid file handle type '$fh_type' provided for $open_method of '$file'"); return; } } if ( my $lockref = _safelock($file) ) { if ( $open_method_coderef->( $_[0], $open_mode, $file ) ) { if ( my $err = _do_flock_or_return_exception( $_[0], $open_mode, $file ) ) { safeunlock($lockref); local $@ = $err; die; } $OPEN_LOCKS++; return $lockref; } else { local $!; safeunlock($lockref); return; } } else { _log_warn("safeopen: could not acquire a lock for '$file': $!"); return; } } my $_lock_ex_nb; my $_lock_sh_nb; sub _do_flock_or_return_exception { my ( $fh, $open_mode, $path ) = @_; my $flock_start_time; my $lock_op = _is_write_open_mode($open_mode) ? ( $_lock_ex_nb //= $Cpanel::Fcntl::Constants::LOCK_EX | $Cpanel::Fcntl::Constants::LOCK_NB ) : ( $_lock_sh_nb //= $Cpanel::Fcntl::Constants::LOCK_SH | $Cpanel::Fcntl::Constants::LOCK_NB ); local $!; my $flock_err; my $flock_max_wait_time_is_whole_number = int($MAX_FLOCK_WAIT) == $MAX_FLOCK_WAIT; while ( !flock $fh, $lock_op ) { $flock_err = $!; if ( $flock_err == _EINTR || $flock_err == _EWOULDBLOCK ) { if ( !$flock_start_time ) { $flock_start_time = $flock_max_wait_time_is_whole_number ? time() : Cpanel::TimeHiRes::time(); next; } if ( ( ( $flock_max_wait_time_is_whole_number ? time() : Cpanel::TimeHiRes::time() ) - $flock_start_time ) > $MAX_FLOCK_WAIT ) { require Cpanel::Exception; return _timeout_exception( $path, $MAX_FLOCK_WAIT ); } else { Cpanel::TimeHiRes::sleep($TIME_BETWEEN_FLOCK_CHECKS); } next; } require Cpanel::Exception; return Cpanel::Exception::create( 'IO::FlockError', [ path => $path, error => $flock_err, operation => $lock_op ] ); } return undef; } sub _safe_re_open { my ( $fh, $open_mode, $file, $open_method_coderef, $open_method ) = @_; if ( !defined $open_mode || !$open_method_coderef || !$file || $file =~ tr/\0// ) { _log_warn('_safe_re_open: Invalid arguments'); return; } else { my $fh_type = ref $fh; if ( !Cpanel::FHUtils::Tiny::is_a($fh) ) { _log_warn("Invalid file handle type '$fh_type' provided for $open_method of '$file'"); return; } } close $fh; if ( $open_method_coderef->( $fh, $open_mode, $file ) ) { if ( my $err = _do_flock_or_return_exception( $fh, $open_mode, $file ) ) { die $err; } return $fh; } return; } sub _log_warn { require Cpanel::Debug; goto &Cpanel::Debug::log_warn; } sub _get_open_args { my ( $mode, $file ) = @_; if ( !$file ) { ( $mode, $file ) = $mode =~ m/^([<>+|]+|)(.*)/; if ( $file && !$mode ) { $mode = '<'; } elsif ( !$file ) { return; } } $mode = $mode eq '<' ? '<' : $mode eq '>' ? '>' : $mode eq '>>' ? '>>' : $mode eq '+<' ? '+<' : $mode eq '+>' ? '+>' : $mode eq '+>>' ? '+>>' : return; return ( $mode, $file ); } sub _sanitize_open_mode { my ($mode) = @_; return if $mode =~ m/[^0-9]/; my $safe_mode = ( $mode & $Cpanel::Fcntl::Constants::O_RDONLY ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_WRONLY ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_RDWR ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_CREAT ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_EXCL ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_APPEND ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_TRUNC ); $safe_mode |= ( $mode & $Cpanel::Fcntl::Constants::O_NONBLOCK ); return $safe_mode; } sub _calculate_lockfile { ## no critic qw(Subroutines::RequireArgUnpacking) my $lockfile = $_[0] =~ tr{<>}{} ? ( ( $_[0] =~ /^[><]*(.*)/ )[0] . '.lock' ) : $_[0] . '.lock'; return $lockfile if ( length $lockfile <= MAX_LOCK_FILE_LENGTH ); require File::Basename; my $lock_basename = File::Basename::basename($lockfile); return $lockfile if ( length $lock_basename <= MAX_LOCK_FILE_LENGTH ); require Cpanel::Hash; my $hashed_lock_basename = Cpanel::Hash::get_fastest_hash($lock_basename) . ".lock"; if ( $lockfile eq $lock_basename ) { return $hashed_lock_basename; } else { return File::Basename::dirname($lockfile) . '/' . $hashed_lock_basename; } } sub is_locked { my ($file) = @_; my $lockfile = _calculate_lockfile($file); my ( $lock_pid, $lock_name, $lock_obj ) = Cpanel::SafeFileLock::fetch_lock_contents_if_exists($lockfile); if ( _is_valid_pid($lock_pid) && _pid_is_alive($lock_pid) ) { return 1; } return 0; } sub _timeout_exception { my ( $path, $waited ) = @_; require Cpanel::Exception; return Cpanel::Exception::create( 'Timeout', 'The system failed to lock the file “[_1]” after [quant,_2,second,seconds].', [ $path, $waited ] ); } sub _die_if_file_is_flocked_cuz_already_waited_a_while { my ( $file, $waited ) = @_; if ( _open_to_write( my $fh, $file ) ) { $_lock_ex_nb //= $Cpanel::Fcntl::Constants::LOCK_EX | $Cpanel::Fcntl::Constants::LOCK_NB; if ( flock( $fh, $_lock_ex_nb ) == 1 ) { flock $fh, UNLOCK_FCNTL_VALUE or die "Failed to unlock “$file” after having just locked it: $!"; } else { require Cpanel::Exception; if ( $! == _EWOULDBLOCK ) { die _timeout_exception( $file, $waited ); } else { die Cpanel::Exception::create( 'IO::FlockError', [ path => $file, error => $!, operation => $_lock_ex_nb ] ); } } } return; } sub _lock_wait { ## no critic qw(Subroutines::ProhibitExcessComplexity) my ( $file, $safefile_lock, $lockfile ) = @_; my ( $temp_file, $fh ) = _write_temp_lock_file( $lockfile, $file ); if ( $temp_file eq NO_PERM_TO_WRITE_TO_DOTLOCK_DIR ) { return ( 1, 1 ); } if ( !$temp_file ) { return ( 0, $fh ); } $safefile_lock->set_filehandle_and_unlinker_after_lock( $fh, Cpanel::SafeFile::_temp->new($temp_file) ); return ( 1, $fh ) if _try_to_install_lockfile( $temp_file, $lockfile ); local $0 = ( $verbose == 1 ) ? "$0 - waiting for lock on $file" : "$0 - waiting for lock"; require Cpanel::SafeFile::LockInfoCache; require Cpanel::SafeFile::LockWatcher; my $watcher = Cpanel::SafeFile::LockWatcher->new($lockfile); my $waittime = _calculate_waittime_for_file($file); my ( $inotify_obj, $inotify_mask, $inotify_file_disappeared ); my $start_time = time; my $waited = 0; my $lockfile_cache = Cpanel::SafeFile::LockInfoCache->new($lockfile); my ( $inotify_inode, $inotify_mtime ); LOCK_WAIT: while (1) { $waited = ( time() - $start_time ); if ( $waited > $waittime ) { _die_if_file_is_flocked_cuz_already_waited_a_while( $file, $waited ); if ( defined $watcher->{'inode'} ) { require Cpanel::Debug; Cpanel::Debug::log_warn( sprintf "Replacing stale lock file: $lockfile. The kernel’s lock is gone, last modified %s seconds ago (mtime=$watcher->{'mtime'}), and waited over $waittime seconds.", time - $watcher->{'mtime'} ); } return ( 1, $fh ) if _overwrite_lockfile_if_inode_mtime_matches( $temp_file, $lockfile, $watcher->{'inode'}, $watcher->{'mtime'} ); die _timeout_exception( $file, $waittime ); } if ( $watcher->{'inode'} ) { my $lock_get = $lockfile_cache->get( @{$watcher}{ 'inode', 'mtime' } ); if ( !$lock_get ) { my $size_before_reload = $watcher->{'size'}; $watcher->reload_from_disk(); if ( $size_before_reload == 0 && $watcher->{'size'} == 0 ) { _log_warn("[$$] UID $> clobbering empty lock file “$lockfile” (UID $watcher->{'uid'}) written by “unknown” at $watcher->{'mtime'}"); return ( 1, $fh ) if _overwrite_lockfile_if_inode_mtime_matches( $temp_file, $lockfile, $watcher->{'inode'}, $watcher->{'mtime'} ); } next LOCK_WAIT; } my ( $lock_pid, $lock_name, $lock_obj ) = @$lock_get; if ( $lock_pid == $$ ) { $watcher->reload_from_disk(); _log_warn("[$$] Double locking detected by self [LOCK_PATH]=[$lockfile] [LOCK_PID]=[$lock_pid] [LOCK_OBJ]=[$lock_obj] [LOCK_PROCESS]=[$lock_name] [ACTUAL_INODE]=[$watcher->{'inode'}] [ACTUAL_MTIME]=[$watcher->{'mtime'}]"); return ( 0, $DOUBLE_LOCK_DETECTED ); } elsif ( !_pid_is_alive($lock_pid) ) { my $time = time(); if ( _overwrite_lockfile_if_inode_mtime_matches( $temp_file, $lockfile, $watcher->{'inode'}, $watcher->{'mtime'} ) ) { _log_warn("[$$] TIME $time UID $> clobbered stale lock file “$lockfile” (NAME “$lock_name”, UID $watcher->{'uid'}) written by PID $lock_pid at $watcher->{'mtime'}"); return ( 1, $fh ); } $watcher->reload_from_disk(); next LOCK_WAIT; } else { require Cpanel::Debug; Cpanel::Debug::log_info("[$$] Waiting for lock on $file held by $lock_name with pid $lock_pid") if $verbose == 1; } } return ( 1, $fh ) if _try_to_install_lockfile( $temp_file, $lockfile ); $watcher->reload_from_disk(); if ( !$inotify_obj || !$inotify_inode || !$watcher->{'inode'} || $inotify_inode != $watcher->{'inode'} || $inotify_mtime != $watcher->{'mtime'} ) { INOTIFY: { ( $inotify_obj, $inotify_mask, $inotify_file_disappeared ) = _generate_inotify_for_lock_file($lockfile); $watcher->reload_from_disk(); if ( $inotify_file_disappeared || !$watcher->{'inode'} ) { undef $inotify_obj; next LOCK_WAIT; } redo INOTIFY if $watcher->{'changed'}; ( $inotify_inode, $inotify_mtime ) = @{$watcher}{ 'inode', 'mtime' }; } } my $selected = _select( my $m = $inotify_mask, undef, undef, $TIME_BETWEEN_DOTLOCK_CHECKS ); if ( $selected == -1 ) { die "select() error: $!" if $! != _EINTR(); } elsif ($selected) { return ( 1, $fh ) if _try_to_install_lockfile( $temp_file, $lockfile ); $watcher->reload_from_disk(); () = $inotify_obj->poll(); } } return; } sub _select { return select( $_[0], $_[1], $_[2], $_[3] ); } sub _generate_inotify_for_lock_file { my ($file) = @_; require Cpanel::Inotify; my $inotify_obj; my $rin = ''; local $@; eval { $inotify_obj = Cpanel::Inotify->new( flags => ['NONBLOCK'] ); $inotify_obj->add( $file, flags => [ 'ATTRIB', 'DELETE_SELF' ] ); vec( $rin, $inotify_obj->fileno(), 1 ) = 1; }; if ($@) { my $err = $@; if ( eval { $err->isa('Cpanel::Exception::SystemCall') } ) { my $err = $err->get('error'); if ( $err == _ENOENT ) { return ( undef, undef, INOTIFY_FILE_DISAPPEARED ); } elsif ( $err != _EACCES ) { # Don’t warn if EACCES local $@ = $err; warn; } } else { local $@ = $err; warn; } return; } return ( $inotify_obj, $rin, 0 ); } sub _pid_is_alive { my ($pid) = @_; local $!; if ( kill( 0, $pid ) ) { return 1; } elsif ( $! == _EPERM ) { return !!( stat "/proc/$pid" )[0]; } return 0; } sub _calculate_waittime_for_file { my ($file) = @_; return $LOCK_WAIT_TIME if $LOCK_WAIT_TIME; my $waittime = DEFAULT_LOCK_WAIT_TIME; if ( -e $file ) { $waittime = int( ( stat _ )[7] / 10000 ); $waittime = $waittime > MAX_LOCK_WAIT_TIME ? MAX_LOCK_WAIT_TIME : $waittime < DEFAULT_LOCK_WAIT_TIME ? DEFAULT_LOCK_WAIT_TIME : $waittime; } return $waittime; } sub _is_valid_pid { my $pid = shift; return 0 unless defined $pid; return $pid =~ tr{0-9}{}c ? 0 : 1; } sub _getdir { my @path = split( /\/+/, $_[0] ); return join( '/', (@path)[ 0 .. ( $#path - 1 ) ] ) || '.'; } sub _create_lockfile { my $lock_fh; return sysopen( $lock_fh, $_[0], CREATE_FCNTL_VALUE, LOCK_FILE_PERMS ) ? ( 1, $lock_fh ) : ( 0, $! ); } sub _open_to_write { my $path = $_[1]; $OVERWRITE_FCNTL_VALUE ||= ( $Cpanel::Fcntl::Constants::O_WRONLY | $Cpanel::Fcntl::Constants::O_NONBLOCK | $Cpanel::Fcntl::Constants::O_APPEND | $Cpanel::Fcntl::Constants::O_NOFOLLOW ); return sysopen( $_[0], $path, $OVERWRITE_FCNTL_VALUE, LOCK_FILE_PERMS ); } sub _overwrite_lockfile_if_inode_mtime_matches { my ( $temp_file, $lockfile, $lockfile_inode, $lockfile_mtime ) = @_; my ( $inode, $mtime ) = ( stat $lockfile )[ 1, 9 ]; if ( !$inode ) { die "stat($lockfile): $!" if $! != _ENOENT(); } if ( !$inode || ( $inode == $lockfile_inode && $mtime == $lockfile_mtime ) ) { rename( $temp_file, $lockfile ) or do { require Cpanel::Exception; die Cpanel::Exception::create( 'IO::RenameError', [ oldpath => $temp_file, newpath => $lockfile, error => $! ] ); }; return 1; } return 0; } sub _is_write_open_mode { my ($mode) = @_; if ( $mode =~ tr{0-9}{}c ) { if ( $mode && ( -1 != index( $mode, '>' ) || -1 != index( $mode, '+' ) ) ) { return 1; } } else { if ( $mode && ( ( $mode & $Cpanel::Fcntl::Constants::O_WRONLY ) || ( $mode & $Cpanel::Fcntl::Constants::O_RDWR ) ) ) { return 1; } } return 0; } sub _verbose_flag_file_exists { return -e '/var/cpanel/safefile_verbose'; } package Cpanel::SafeFile::_temp; use constant _ENOENT => 2; sub new { return bless [ $_[1], $_SKIP_DOTLOCK_WHEN_NO_PERMS, $$ ], $_[0]; } sub DESTROY { local $!; unlink $_[0]->[0] or do { if ( !$_[0]->[1] && $! != _ENOENT && $_[0]->[2] == $$ ) { warn "unlink($_[0]->[0]): $!"; } }; return; } 1; } # --- END Cpanel/SafeFile.pm { # --- BEGIN Cpanel/Linux/Constants.pm package Cpanel::Linux::Constants; use strict; use warnings; use constant { NAME_MAX => 255, PATH_MAX => 4096, }; 1; } # --- END Cpanel/Linux/Constants.pm { # --- BEGIN Cpanel/Validate/FilesystemNodeName.pm package Cpanel::Validate::FilesystemNodeName; use strict; use warnings; # use Cpanel::Exception (); # use Cpanel::Linux::Constants (); sub is_valid { my ($node) = @_; local $@; eval { validate_or_die($node); }; return $@ ? 0 : 1; } sub validate_or_die { my ($name) = @_; if ( !length $name ) { die Cpanel::Exception::create('Empty'); } elsif ( $name eq '.' || $name eq '..' ) { die Cpanel::Exception::create( 'Reserved', [ value => $name ] ); } elsif ( length $name > Cpanel::Linux::Constants::NAME_MAX() ) { die Cpanel::Exception::create( 'TooManyBytes', [ value => $name, maxlength => Cpanel::Linux::Constants::NAME_MAX() ] ); } elsif ( index( $name, '/' ) != -1 ) { die Cpanel::Exception::create( 'InvalidCharacters', [ value => $name, invalid_characters => ['/'] ] ); } elsif ( index( $name, "\0" ) != -1 ) { die Cpanel::Exception::create( 'InvalidCharacters', 'This value may not contain a [asis,NUL] byte.', [ value => $name, invalid_characters => ["\0"] ] ); } return 1; } 1; } # --- END Cpanel/Validate/FilesystemNodeName.pm { # --- BEGIN Cpanel/Notify.pm package Cpanel::Notify; use strict; use warnings; # use Cpanel::Set (); # use Cpanel::Fcntl (); # use Cpanel::SafeFile (); # use Cpanel::LoadModule (); # use Cpanel::Validate::FilesystemNodeName (); # use Cpanel::Exception (); # use Cpanel::Debug (); our $VERSION = '1.8'; my $DEFAULT_CONTENT_TYPE = 'text/plain; charset=utf-8'; our $NOTIFY_INTERVAL_STORAGE_DIR = '/var/cpanel/notifications'; sub notification_class { my (%args) = @_; if ( !defined $args{'interval'} ) { $args{'interval'} = 1; } if ( !defined $args{'status'} ) { $args{'status'} = 'No status set'; } foreach my $param (qw(application status class constructor_args)) { die Cpanel::Exception::create( 'MissingParameter', [ 'name' => $param ] ) if !defined $args{$param}; } if ( my @unwelcome_params = Cpanel::Set::difference( [ keys %args ], [qw(application status class constructor_args interval)] ) ) { die Cpanel::Exception::create_raw( 'InvalidParameters', "The following parameters don't belong as an argument to notification_class(); you may have meant to pass these in constructor_args instead: " . join( ' ', @unwelcome_params ) ); } my $constructor_args = { @{ $args{'constructor_args'} } }; if ( $constructor_args->{'skip_send'} ) { my $class = "Cpanel::iContact::Class::$args{'class'}"; Cpanel::LoadModule::load_perl_module($class); return $class->new(%$constructor_args); } return _notification_backend( $args{'application'}, $args{'status'}, $args{'interval'}, sub { my $class = "Cpanel::iContact::Class::$args{'class'}"; Cpanel::LoadModule::load_perl_module($class); return $class->new(%$constructor_args); }, ); } sub notification { my %AGS = @_; my $app = $AGS{'app'} || $AGS{'application'} || 'Notice'; return _notification_backend( $app, $AGS{'status'}, $AGS{'interval'} || 0, sub { my $module = "Cpanel::iContact"; Cpanel::LoadModule::load_perl_module($module); my $from = $AGS{'from'}; my $to = $AGS{'to'}; my $msgheader = $AGS{'msgheader'} || $AGS{'subject'}; my $message = $AGS{'message'}; my $plaintext_message = $AGS{'plaintext_message'}; my $priority = $AGS{'priority'} || 3; my $attach_files = $AGS{'attach_files'} || []; my $content_type = $AGS{'content-type'} || $DEFAULT_CONTENT_TYPE; "$module"->can('icontact')->( 'attach_files' => $attach_files, 'application' => $app, 'level' => $priority, 'from' => $from, 'to' => $to, 'subject' => $msgheader, 'message' => $message, 'plaintext_message' => $plaintext_message, 'content-type' => $content_type, ); } ); } sub _notification_backend { my ( $app, $status, $interval, $todo_cr ) = @_; my $is_ready = _checkstatusinterval( 'app' => $app, 'status' => $status, 'interval' => $interval, ); if ($is_ready) { return $todo_cr->(); } elsif ( $Cpanel::Debug::level > 3 ) { Cpanel::Debug::log_warn("not sending notify app=[$app] status=[$status] interval=[$interval]"); } return $is_ready ? 1 : 0; } sub notify_blocked { my %AGS = @_; my $app = $AGS{'app'}; my $status = $AGS{'status'}; my $interval = $AGS{'interval'}; return 0 if $interval <= 1; # Special Case (ignore interval check); $app =~ s{/}{_}g; # Its possible to have slashes in the app name $status =~ s{:}{_}g; # Its possible to have colons in the status my $db_file = "$NOTIFY_INTERVAL_STORAGE_DIR/$app"; return 0 if !-e $db_file; my %notifications; my $notify_db_fh; if ( my $nlock = Cpanel::SafeFile::safesysopen( $notify_db_fh, $db_file, Cpanel::Fcntl::or_flags('O_RDONLY'), 0600 ) ) { local $/; %notifications = map { ( split( /:/, $_, 2 ) )[ 0, 1 ] } split( m{\n}, readline($notify_db_fh) ); Cpanel::SafeFile::safeclose( $notify_db_fh, $nlock ); } else { Cpanel::Debug::log_warn("Could not open $db_file: $!"); return; } if ( $notifications{$status} && ( ( $notifications{$status} + $interval ) > time() ) ) { return 1; } return 0; } { no warnings 'once'; *update_notification_time_if_interval_reached = \&_checkstatusinterval; } sub _checkstatusinterval { my %AGS = @_; my $app = $AGS{'app'}; my $status = $AGS{'status'}; my $interval = $AGS{'interval'}; return 1 if $interval <= 1; # Special Case (ignore interval check); $app =~ s{/}{_}g; # Its possible to have slashes in the app name $status =~ s{:}{_}g; # Its possible to have colons in the status Cpanel::Validate::FilesystemNodeName::validate_or_die($app); my $notify = 0; if ( !-e $NOTIFY_INTERVAL_STORAGE_DIR ) { Cpanel::LoadModule::load_perl_module('Cpanel::SafeDir::MK'); Cpanel::SafeDir::MK::safemkdir( $NOTIFY_INTERVAL_STORAGE_DIR, '0700' ); if ( !-d $NOTIFY_INTERVAL_STORAGE_DIR ) { Cpanel::Debug::log_warn("Failed to setup notifications directory: $NOTIFY_INTERVAL_STORAGE_DIR: $!"); return; } } my %notifications; my $notify_db_fh; my $db_file = "$NOTIFY_INTERVAL_STORAGE_DIR/$app"; if ( my $nlock = Cpanel::SafeFile::safesysopen( $notify_db_fh, $db_file, Cpanel::Fcntl::or_flags(qw( O_RDWR O_CREAT )), 0600 ) ) { local $/; %notifications = map { ( split( /:/, $_, 2 ) )[ 0, 1 ] } split( m{\n}, readline($notify_db_fh) ); if ( !exists $notifications{$status} || ( int( $notifications{$status} ) + int($interval) ) < time() ) { $notifications{$status} = time; $notify = 1; } seek( $notify_db_fh, 0, 0 ); print {$notify_db_fh} join( "\n", map { $_ . ':' . $notifications{$_} } sort keys %notifications ); truncate( $notify_db_fh, tell($notify_db_fh) ); Cpanel::SafeFile::safeclose( $notify_db_fh, $nlock ); } else { Cpanel::Debug::log_warn("Could not open $db_file: $!"); return; } return $notify; } 1; } # --- END Cpanel/Notify.pm { # --- BEGIN Cpanel/Server/Utils.pm package Cpanel::Server::Utils; use strict; sub is_subprocess_of_cpsrvd { return 0 if $INC{'cpanel/cpsrvd.pm'}; # If we ARE cpsrvd we do not want this behavior return $ENV{'CPANEL'} ? 1 : 0; } 1; } # --- END Cpanel/Server/Utils.pm { # --- BEGIN Cpanel/Logger.pm package Cpanel::Logger; use strict; # use Cpanel::Time::Local (); my $is_sandbox; my $is_smoker; our $VERSION = 1.3; use constant TRACE_TOUCH_FILE => '/var/cpanel/log_stack_traces'; our $ENABLE_BACKTRACE; our $DISABLE_OUTPUT; # used by cpanminus our $ALWAYS_OUTPUT_TO_STDERR; our $STD_LOG_FILE = '/usr/local/cpanel/logs/error_log'; our $PANIC_LOG_FILE = '/usr/local/cpanel/logs/panic_log'; my ( $cached_progname, $cached_prog_pid, %singleton_stash ); sub new { my ( $class, $hr_args ) = @_; if ( $hr_args->{'open_now'} && $hr_args->{'use_no_files'} ) { die "“open_now” and “use_no_files” mutually exclude!"; } my $args_sig = 'no_args'; if ( $hr_args && ref($hr_args) eq 'HASH' ) { $args_sig = join( ',', map { $_ . '=>' . $hr_args->{$_} } sort keys %{$hr_args} ); # Storable::freeze($hr_args); } my $no_load_from_cache = $hr_args->{'no_load_from_cache'} ? 1 : 0; if ( exists $singleton_stash{$class}{$args_sig} and !$no_load_from_cache ) { $singleton_stash{$class}{$args_sig}->{'cloned'}++; } else { $singleton_stash{$class}{$args_sig} = bless( {}, $class ); if ( $hr_args && ref($hr_args) eq 'HASH' ) { foreach my $k ( keys %$hr_args ) { $singleton_stash{$class}{$args_sig}->{$k} = $hr_args->{$k}; } } } my $self = $singleton_stash{$class}{$args_sig}; if ( !$self->{'cloned'} ) { if ( $self->{'open_now'} && !$self->{'use_no_files'} ) { $self->_open_logfile(); } } $self->_set_backtrace( $ENABLE_BACKTRACE // $self->{'backtrace'} // _get_backtrace_touchfile() ); return $self; } sub __Logger_pushback { if ( @_ && index( ref( $_[0] ), __PACKAGE__ ) == 0 ) { return @_; } return ( __PACKAGE__->new(), @_ ); } sub invalid { my ( $self, @list ) = __Logger_pushback(@_); my %log = ( 'message' => $list[0], 'level' => 'invalid', 'output' => 0, 'service' => $self->find_progname(), 'backtrace' => $self->get_backtrace(), 'die' => 0, ); if ( is_sandbox() ) { if ( !-e '/var/cpanel/DEBUG' ) { $self->notify( 'invalid', \%log ); } $log{'output'} = _stdin_is_tty() ? 2 : 1; } return $self->logger( \%log ); } # end of invalid sub is_sandbox { return 0 if $INC{'B/C.pm'}; # avoid cache during compile return $is_sandbox if defined $is_sandbox; return ( $is_sandbox = -e '/var/cpanel/dev_sandbox' ? 1 : 0 ); } sub is_smoker { return 0 if $INC{'B/C.pm'}; # avoid cache during compile return $is_smoker if defined $is_smoker; return ( $is_smoker = -e '/var/cpanel/smoker' ? 1 : 0 ); } sub deprecated { ## no critic qw(Subroutines::RequireArgUnpacking) my ( $self, @list ) = __Logger_pushback(@_); my %log = ( 'message' => $list[0], 'level' => 'deprecated', 'output' => 0, 'service' => $self->find_progname(), 'backtrace' => $self->get_backtrace(), 'die' => 0, ); unless ( is_sandbox() ) { $self->logger( \%log ); return; } $self->notify( 'deprecated', \%log ); $log{'output'} = _stdin_is_tty() ? 2 : 1; $log{'die'} = 1; return $self->logger( \%log ); } sub debug { my ( $self, $message, $conf_hr ) = @_; # not appropriate for debug() : __Logger_pushback(@_); $self = $self->new() if !ref $self; $conf_hr ||= { 'force' => 0, 'backtrace' => 0, 'output' => 1, # Logger's debug level should output to STDOUT }; return unless $conf_hr->{'force'} || ( defined $Cpanel::Debug::level && $Cpanel::Debug::level ); ## PPI NO PARSE - avoid recursive use statements if ( !defined $message ) { my @caller = caller(); $message = "debug() at $caller[1] line $caller[2]."; } my %log = ( 'message' => $message, 'level' => 'debug', 'output' => $conf_hr->{'output'}, 'backtrace' => $conf_hr->{'backtrace'}, ); if ( ref $log{'message'} ) { my $outmsg = $log{'message'}; eval 'local $SIG{__DIE__}; local $SIG{__WARN__}; require Cpanel::YAML::Syck; $outmsg = YAML::Syck::Dump($outmsg);'; my @caller = caller(); $log{'message'} = "$log{'message'} at $caller[1] line $caller[2]:\n" . $outmsg; } elsif ( $log{'message'} =~ m/\A\d+(?:\.\d+)?\z/ ) { $log{'message'} = "debug() number $log{'message'}"; } $self->logger( \%log ); return \%log; } sub info { my ( $self, @list ) = __Logger_pushback(@_); return $self->logger( { 'message' => $list[0], 'level' => 'info', 'output' => $self->{'open_now'} ? 0 : 1, # FB#59177: info level should output to STDOUT 'backtrace' => 0 } ); } # end of info sub warn { my ( $self, @list ) = __Logger_pushback(@_); return $self->logger( { 'message' => $list[0], 'level' => 'warn', 'output' => _stdin_is_tty() ? 2 : 0, 'backtrace' => $self->get_backtrace() } ); } # end of warn sub error { my ( $self, @list ) = __Logger_pushback(@_); return $self->logger( { 'message' => $list[0], 'level' => 'error', 'output' => -t STDIN ? 2 : 0, 'backtrace' => $self->get_backtrace() } ); } # end of error sub die { my ( $self, @list ) = __Logger_pushback(@_); my %log = ( 'message' => $list[0], 'level' => 'die', 'output' => _stdin_is_tty() ? 2 : 0, 'backtrace' => $self->get_backtrace() ); return $self->logger( \%log ); } # end of die sub panic { my ( $self, @list ) = __Logger_pushback(@_); my %log = ( 'message' => $list[0], 'level' => 'panic', 'output' => 2, 'backtrace' => $self->get_backtrace() ); return $self->logger( \%log ); } # end of panic sub raw { return $_[0]->logger( { 'message' => $_[1], 'level' => 'raw', 'output' => 0, 'backtrace' => 0 } ); } sub cplog { my $msg = shift; my $loglevel = shift; my $service = shift; my $nostdout = shift; if ( !$nostdout ) { $nostdout = 1; } else { $nostdout = 0; } logger( { 'message' => $msg, 'level' => $loglevel, 'service' => $service, 'output' => $nostdout, 'backtrace' => $ENABLE_BACKTRACE // _get_backtrace_touchfile() } ); } # end of cplog (deprecated) sub _get_configuration_for_logger { my ( $self, $cfg_or_msg ) = @_; my $hr = ref($cfg_or_msg) eq 'HASH' ? $cfg_or_msg : { 'message' => $cfg_or_msg }; $hr->{'message'} ||= 'Something is wrong'; $hr->{'level'} ||= ''; $hr->{'output'} ||= 0; $hr->{'output'} = 0 if $DISABLE_OUTPUT; if ( !exists $hr->{'backtrace'} ) { $hr->{'backtrace'} = $self->get_backtrace(); } $hr->{'use_no_files'} ||= 0; $hr->{'use_fullmsg'} ||= 0; return $hr; } sub _write { return print { $_[0] } $_[1]; } sub get_backtrace { my ($self) = __Logger_pushback(@_); return $ENABLE_BACKTRACE // $self->{'backtrace'}; } sub _set_backtrace { my ( $self, @args ) = __Logger_pushback(@_); $self->{'backtrace'} = $args[0] ? 1 : 0; return; } sub _get_backtrace_touchfile { return -e TRACE_TOUCH_FILE ? 1 : 0; } sub get_fh { my ($self) = @_; return $self->{'log_fh'}; } sub set_fh { my ( $self, $fh ) = @_; $self->{'log_fh'} = $fh; return 1; } sub logger { ## no critic(RequireArgUnpacking) my ( $self, @list ) = __Logger_pushback(@_); my $hr = $self->_get_configuration_for_logger( $list[0] ); my ( $msg, $time, $status ); $status = 1; my ($msg_maybe_bt) = $hr->{'backtrace'} ? $self->backtrace( $hr->{'message'} ) : $hr->{'message'} . "\n"; if ( $hr->{'level'} eq 'raw' ) { $msg = $hr->{'message'}; } else { $time ||= Cpanel::Time::Local::localtime2timestamp(); $hr->{'service'} ||= $self->find_progname(); # only compute the service name if we HAVE to do so as it can be expensive if ( $self->{'log_pid'} ) { $msg = "[$time] $hr->{'level'} [$hr->{'service'}] [$$] $msg_maybe_bt"; } else { $msg = "[$time] $hr->{'level'} [$hr->{'service'}] $msg_maybe_bt"; } } unless ( $hr->{'use_no_files'} ) { local $self->{'log_fh'} = \*STDERR if $ALWAYS_OUTPUT_TO_STDERR; $self->_open_logfile() if !$self->{'log_fh'} || ( !eval { fileno( $self->{'log_fh'} ) } && !UNIVERSAL::isa( $self->{'log_fh'}, 'IO::Scalar' ) ); _write( $self->{'log_fh'}, $msg ) or $status = 0; if ( $hr->{'level'} eq 'panic' || $hr->{'level'} eq 'invalid' || $hr->{'level'} eq 'deprecated' ) { my $panic_fh; require Cpanel::FileUtils::Open; if ( Cpanel::FileUtils::Open::sysopen_with_real_perms( $panic_fh, $PANIC_LOG_FILE, 'O_WRONLY|O_APPEND|O_CREAT', 0600 ) ) { $time ||= Cpanel::Time::Local::localtime2timestamp(); $hr->{'service'} ||= $self->find_progname(); # only compute the service name if we HAVE to do so as it can be expensive _write( $panic_fh, "$time $hr->{level} [$hr->{'service'}] $msg_maybe_bt" ); close $panic_fh; } } } if ( $hr->{'output'} ) { $hr->{'service'} ||= $self->find_progname(); # only compute the service name if we HAVE to do so as it can be expensive my $out = "$hr->{level} [$hr->{'service'}] $hr->{'message'}\n"; if ( $self->{'timestamp_prefix'} ) { $out = "[$time] $out"; } $out = $msg if $hr->{'use_fullmsg'}; $status &&= $self->_write_message( $hr, $out ); } if ( ( $hr->{'level'} eq 'die' || $hr->{'level'} eq 'panic' || $hr->{'die'} ) ) { CORE::die "exit level [$hr->{'level'}] [pid=$$] ($hr->{'message'})\n"; # make sure we die if die is overwritten } return $status; } # end of logger sub _write_message { my ( $self, $hr, $out ) = @_; my $status = 1; if ( $hr->{'output'} == 3 ) { _write( \*STDOUT, $out ) or $status = 0; _write( \*STDERR, $out ) or $status = 0; } elsif ( $hr->{'output'} == 1 && ( $self->{'use_stdout'} || _stdout_is_tty() ) ) { _write( \*STDOUT, $out ) or $status = 0; } elsif ( $hr->{'output'} == 2 ) { _write( \*STDERR, $out ) or $status = 0; } return $status; } sub find_progname { if ( $cached_progname && $cached_prog_pid == $$ ) { return $cached_progname; } my $s = $0; if ( !length $s ) { # Someone _could_ set $0 = ''; my $i = 1; # 0 is always find_progname while ( my @service = caller( $i++ ) ) { last if ( $service[3] =~ /::BEGIN$/ ); $s = $service[1] if ( $service[1] ne '' ); } } $s =~ s@.+/(.+)$@$1@ if $s =~ tr{/}{}; $s =~ s@\..+$@@ if $s =~ tr{\.}{}; $s =~ s@ .*$@@ if $s =~ tr{ }{}; $cached_progname = $s; $cached_prog_pid = $$; return $s; } sub backtrace { ## no critic qw(Subroutines::RequireArgUnpacking) my ( $self, @list ) = __Logger_pushback(@_); if ( ref $list[0] ) { return $list[0] if scalar @list == 1; return (@list); } require Cpanel::Carp; local $_; # Protect surrounding program - just in case... local $Carp::Internal{ (__PACKAGE__) } = 1; local $Carp::Internal{'Cpanel::Debug'} = 1; return Cpanel::Carp::safe_longmess(@list); } sub redirect_stderr_to_error_log { return open( STDERR, '>>', $STD_LOG_FILE ); } sub notify { my ( $self, $call, $log_ref ) = @_; my $time = Cpanel::Time::Local::localtime2timestamp(); my ($bt) = $self->backtrace( $log_ref->{'message'} ); $log_ref->{'service'} //= ''; my $logfile = qq{$time [$log_ref->{'service'}] } . ( $bt // '' ); if ( eval { require Cpanel::LoadModule; Cpanel::LoadModule::load_perl_module('Cpanel::iContact::Class::Logger::Notify'); 1; } ) { eval { require Cpanel::Notify; Cpanel::Notify::notification_class( 'class' => 'Logger::Notify', 'application' => 'Logger::Notify', 'constructor_args' => [ 'origin' => $log_ref->{'service'}, 'logger_call' => $call, 'attach_files' => [ { name => 'cpanel-logger-log.txt', content => \$logfile } ], 'subject' => $log_ref->{'subject'}, ] ); }; } elsif ( eval { require Cpanel::LoadModule; Cpanel::LoadModule::load_perl_module('Cpanel::iContact'); 1; } ) { Cpanel::iContact::icontact( 'application' => $log_ref->{'service'}, 'subject' => $log_ref->{'subject'} ? $log_ref->{'subject'} : qq{Cpanel::Logger::$call called in $log_ref->{'service'}}, 'message' => $logfile, ); } else { CORE::warn( $log_ref->{'subject'} ? $log_ref->{'subject'} : qq{Cpanel::Logger::$call called in $log_ref->{'service'}} . ": $logfile" ); } return; } *fatal = *die; *out = *info; *success = *info; *throw = *die; *warning = *warn; sub _is_subprocess_of_cpsrvd { require Cpanel::Server::Utils; goto \&Cpanel::Server::Utils::is_subprocess_of_cpsrvd; } sub _open_logfile { my ($self) = @_; my $usingstderr = 0; my $log_fh; $self->{'alternate_logfile'} ||= $STD_LOG_FILE; if ( $STD_LOG_FILE eq $self->{'alternate_logfile'} && _is_subprocess_of_cpsrvd() ) { $log_fh = \*STDERR; $usingstderr = 1; } else { require Cpanel::FileUtils::Open; if ( !Cpanel::FileUtils::Open::sysopen_with_real_perms( $log_fh, $self->{'alternate_logfile'}, 'O_WRONLY|O_APPEND|O_CREAT', 0600 ) ) { ( $usingstderr, $log_fh ) = ( 1, \*STDERR ); } select( ( select($log_fh), $| = 1 )[0] ); ## no critic qw(Variables::RequireLocalizedPunctuationVars InputOutput::ProhibitOneArgSelect) -- Cpanel::FHUtils::Autoflush would be expensive to load every time } $self->{'log_fh'} = $log_fh; $self->{'usingstderr'} = $usingstderr; return 1; } sub _stdin_is_tty { local $@; return eval { -t STDIN }; } sub _stdout_is_tty { local $@; return eval { -t STDOUT }; } sub clear_singleton_stash { %singleton_stash = (); return; } 1; } # --- END Cpanel/Logger.pm { # --- BEGIN Cpanel/Debug.pm package Cpanel::Debug; use strict; use warnings; our $HOOKS_DEBUG_FILE = '/var/cpanel/debughooks'; our $level = ( exists $ENV{'CPANEL_DEBUG_LEVEL'} && $ENV{'CPANEL_DEBUG_LEVEL'} ? int $ENV{'CPANEL_DEBUG_LEVEL'} : 0 ); my $debug_hooks_value; my $logger; sub debug_level { my ($level) = @_; $Cpanel::Debug::level = $level if defined $level; return $Cpanel::Debug::level; } sub logger { $logger = shift if (@_); # Set method for $logger if something is passed in. return $logger ||= do { local ( $@, $! ); require Cpanel::Logger; Cpanel::Logger->new(); }; } sub log_error { local $!; #prevent logger from overwriting $! return logger()->error( $_[0] ); } sub log_warn { local $!; #prevent logger from overwriting $! return logger()->warn( $_[0] ); } sub log_warn_no_backtrace { local $!; #prevent logger from overwriting $! my $logger = logger(); local $Cpanel::Logger::ENABLE_BACKTRACE = 0; return $logger->warn( $_[0] ); } sub log_invalid { local $!; #prevent logger from overwriting $! return logger()->invalid( $_[0] ); } sub log_deprecated { local $!; #prevent logger from overwriting $! return logger()->deprecated( $_[0] ); } sub log_panic { local $!; #prevent logger from overwriting $! return logger()->panic( $_[0] ); } sub log_die { local $!; #prevent logger from overwriting $! return logger()->die( $_[0] ); } sub log_info { local $!; #prevent logger from overwriting $! return logger()->info( $_[0] ); } sub log_debug { local $!; #prevent logger from overwriting $! return logger()->debug( $_[0] ); } sub debug_hooks_value { return $debug_hooks_value if defined $debug_hooks_value; return ( $debug_hooks_value = ( stat($HOOKS_DEBUG_FILE) )[7] || 0 ); } 1; } # --- END Cpanel/Debug.pm { # --- BEGIN Cpanel/SafeDir/MK.pm package Cpanel::SafeDir::MK; use strict; use warnings; # use Cpanel::Debug (); my $DEFAULT_PERMISSIONS = 0755; sub safemkdir_or_die { my ( $dir, $mode, $created ) = @_; my $ok = safemkdir( $dir, $mode, $created ); if ( !$ok ) { my $error = $!; require Cpanel::Exception; die Cpanel::Exception::create( 'IO::DirectoryCreateError', [ path => $dir, error => $error, ] ); } return $ok; } sub safemkdir { ## no critic(Subroutines::ProhibitExcessComplexity) -- Refactoring this function is a project, not a bug fix my ( $dir, $mode, $errors, $created ) = @_; if ( defined $mode ) { if ( $mode eq '' ) { $mode = undef; } elsif ( index( $mode, '0' ) == 0 ) { if ( length $mode < 3 || $mode =~ tr{0-7}{}c || !defined oct $mode ) { $mode = $DEFAULT_PERMISSIONS; } else { $mode = oct($mode); } } elsif ( $mode =~ tr{0-9}{}c ) { $mode = $DEFAULT_PERMISSIONS; } } $dir =~ tr{/}{}s; my $default = ''; if ( index( $dir, '/' ) == 0 ) { $default = '/'; } elsif ( $dir eq '.' || $dir eq './' ) { if ( !-l $dir && defined $mode ) { return chmod $mode, $dir; } return 1; } else { substr( $dir, 0, 2, '' ) if index( $dir, './' ) == 0; } if ( _has_dot_dot($dir) ) { Cpanel::Debug::log_warn("Possible improper directory $dir specified"); my @dir_parts = split m{/}, $dir; my @good_parts; my $first; foreach my $part (@dir_parts) { next if ( !defined $part || $part eq '' ); next if $part eq '.'; if ( $part eq '..' ) { if ( !$first || !@good_parts ) { Cpanel::Debug::log_warn("Will not proceed above first directory part $first"); return 0; } if ( $first eq $good_parts[$#good_parts] ) { undef $first; } pop @good_parts; next; } elsif ( $part !~ tr{.}{}c ) { Cpanel::Debug::log_warn("Total stupidity found in directory $dir"); return 0; } push @good_parts, $part; if ( !$first ) { $first = $part } } $dir = $default . join '/', @good_parts; if ( !$dir ) { Cpanel::Debug::log_warn("Could not validate given directory"); return; } Cpanel::Debug::log_warn("Improper directory updated to $dir"); } if ( -d $dir ) { if ( !-l $dir && defined $mode ) { return chmod $mode, $dir; } return 1; } elsif ( -e _ ) { Cpanel::Debug::log_warn("$dir was expected to be a directory!"); require Errno; $! = Errno::ENOTDIR(); ## no critic qw(Variables::RequireLocalizedPunctuationVars) -- for legacy reasons return 0; } my @dir_parts = split m{/}, $dir; if ( scalar @dir_parts > 100 ) { Cpanel::Debug::log_warn("Encountered excessive directory length. This should never happen."); return 0; } my $returnvalue; foreach my $i ( 0 .. $#dir_parts ) { my $newdir = join( '/', @dir_parts[ 0 .. $i ] ); next if $newdir eq ''; my $is_dir = -d $newdir; my $exists = -e _; if ( !$exists ) { my $local_mode = defined $mode ? $mode : $DEFAULT_PERMISSIONS; if ( mkdir( $newdir, $local_mode ) ) { push @{$created}, $newdir if $created; $returnvalue++; } else { Cpanel::Debug::log_warn("mkdir $newdir failed: $!"); return; } } elsif ( !$is_dir ) { Cpanel::Debug::log_warn("Encountered non-directory $newdir in path of $dir: $!"); require Errno; $! = Errno::ENOTDIR(); ## no critic qw(Variables::RequireLocalizedPunctuationVars) -- for legacy reasons last; } } return $returnvalue; } sub _has_dot_dot { ## no critic qw(RequireArgUnpacking) return 1 if $_[0] eq '..'; return 1 if -1 != index( $_[0], '/../' ); return 1 if 0 == index( $_[0], '../' ); return 1 if ( length( $_[0] ) - 3 ) == rindex( $_[0], '/..' ); return 0; } 1; } # --- END Cpanel/SafeDir/MK.pm { # --- BEGIN Cpanel/FHUtils/Autoflush.pm package Cpanel::FHUtils::Autoflush; use strict; use warnings; sub enable { select( ( select( $_[0] ), $| = 1 )[0] ); ## no critic qw(InputOutput::ProhibitOneArgSelect Variables::RequireLocalizedPunctuationVars) - aka $socket->autoflush(1) without importing IO::Socket return; } 1; } # --- END Cpanel/FHUtils/Autoflush.pm { # --- BEGIN Cpanel/Update/Logger.pm package Cpanel::Update::Logger; use strict; use warnings; # use Cpanel::SafeDir::MK (); # use Cpanel::Time::Local (); # use Cpanel::FHUtils::Autoflush (); use File::Basename (); use constant { DEBUG => 0, INFO => 25, WARN => 50, ERROR => 75, FATAL => 100, }; our $VERSION = '1.2'; our $_BACKLOG_TIE_CLASS; sub new { my $class = shift; my $self = shift || {}; ref($self) eq 'HASH' or CORE::die("hashref not passed to new"); bless( $self, $class ); $self->{'stdout'} = 1 if ( !defined $self->{'stdout'} ); $self->{'timestamp'} = 1 if ( !defined $self->{'timestamp'} ); if ( $self->{'to_memory'} ) { $self->{'backlog'} = []; tie @{ $self->{'backlog'} }, $_BACKLOG_TIE_CLASS if $_BACKLOG_TIE_CLASS; } eval { $self->set_logging_level( $self->{'log_level'} ); 1 } or CORE::die("An invalid logging level was passed to new: $self->{'log_level'}"); $self->open_log() if $self->{'logfile'}; if ( exists $self->{'pbar'} and defined $self->{'pbar'} ) { $self->{'pbar'} += 0; $self->update_pbar( $self->{'pbar'} ); } return $self; } sub open_log { my $self = shift or CORE::die(); my $log_file = $self->{'logfile'}; my $logfile_dir = File::Basename::dirname($log_file); my $created_dir = 0; if ( !-d $logfile_dir ) { Cpanel::SafeDir::MK::safemkdir( $logfile_dir, '0700', 2 ); $created_dir = 1; } my $old_umask = umask(0077); # Case 92381: Logs should not be world-readable open( my $fh, '>>', $log_file ) or do { CORE::die("Failed to open '$log_file' for append: $!"); }; umask($old_umask); Cpanel::FHUtils::Autoflush::enable($fh); Cpanel::FHUtils::Autoflush::enable( \*STDOUT ) if $self->{'stdout'}; $self->{'fh'} = $fh; unless ( $self->{brief} ) { print {$fh} '-' x 100 . "\n"; print {$fh} "=> Log opened from $0 ($$) at " . localtime(time) . "\n"; } $self->warning("Had to create directory $logfile_dir before opening log") if ($created_dir); return; } sub close_log { my $self = shift or CORE::die(); return if ( !$self->{'fh'} ); my $fh = $self->{'fh'}; unless ( $self->{brief} ) { print {$fh} "=> Log closed " . localtime(time) . "\n"; } warn("Failed to close file handle for $self->{'logfile'}") if ( !close $fh ); delete $self->{'fh'}; return; } sub DESTROY { my $self = shift or CORE::die("DESTROY called without an object"); $self->close_log if ( $self->{'fh'} ); return; } sub log { my $self = shift or CORE::die("log called as a class"); ref $self eq __PACKAGE__ or CORE::die("log called as a class"); my $msg = shift or return; my $stdout = shift; $stdout = $self->{'stdout'} if ( !defined $stdout ); my $to_memory = $self->{'to_memory'}; my $fh = $self->{'fh'}; foreach my $line ( split( /[\r\n]+/, $msg ) ) { if ( $self->{'timestamp'} ) { substr( $line, 0, 0, '[' . Cpanel::Time::Local::localtime2timestamp() . '] ' ); } chomp $line; print STDOUT "$line\n" if $stdout; print {$fh} "$line\n" if $fh; push @{ $self->{'backlog'} }, "$line" if ($to_memory); } return; } sub _die { my $self = shift or CORE::die(); my $message = shift || ''; $self->log("***** DIE: $message"); return CORE::die( "exit level [die] [pid=$$] ($message) " . join ' ', caller() ); } sub fatal { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > FATAL ); my $message = shift || ''; $self->log("***** FATAL: $message"); $self->set_need_notify(); return; } sub error { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > ERROR ); my $message = shift || ''; $self->log("E $message"); return; } sub warning { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > WARN ); my $message = shift || ''; $self->log("W $message"); return; } sub panic { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > ERROR ); my $message = shift || ''; $self->log("***** PANIC!"); $self->log("E $message"); $self->log("***** PANIC!"); $self->set_need_notify(); return; } sub info { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > INFO ); my $message = shift || ''; $self->log(" $message"); return; } sub debug { my $self = shift or CORE::die(); return if ( $self->{'log_level_numeric'} > DEBUG ); my $message = shift || ''; $self->log("D $message"); return; } sub get_logging_level { return shift->{'log_level'} } sub set_logging_level { my $self = shift or CORE::die(); my $log_level = shift; $log_level = 'info' if ( !defined $log_level ); my $old_log_level = $self->get_logging_level(); if ( $log_level =~ m/^fatal/i ) { $self->{'log_level'} = 'fatal'; $self->{'log_level_numeric'} = FATAL; } elsif ( $log_level =~ m/^error/i ) { $self->{'log_level'} = 'error'; $self->{'log_level_numeric'} = ERROR; } elsif ( $log_level =~ m/^warn/i ) { $self->{'log_level'} = 'warning'; $self->{'log_level_numeric'} = WARN; } elsif ( $log_level =~ m/^info/i ) { $self->{'log_level'} = 'info'; $self->{'log_level_numeric'} = INFO; } elsif ( $log_level =~ m/^debug/i ) { $self->{'log_level'} = 'debug'; $self->{'log_level_numeric'} = DEBUG; } else { CORE::die("Unknown logging level '$log_level' passed to set_logging_level"); } return $old_log_level; } sub get_pbar { return shift->{'pbar'} } sub increment_pbar { my $self = shift or CORE::die(); return if ( !exists $self->{'pbar'} ); my $amount = shift || 1; my $new_value = $self->{'pbar'} + $amount; return $self->update_pbar($new_value); } sub update_pbar { my $self = shift or CORE::die(); return if ( !exists $self->{'pbar'} ); my $new_value = shift || 0; if ( $new_value > 100 ) { $self->debug("Pbar set to > 100 ($new_value)"); $new_value = 100; } return if $new_value == $self->{'pbar'}; $self->{'pbar'} = $new_value; $self->info( $new_value . '% complete' ); return; } sub set_need_notify { my $self = shift; ref $self eq __PACKAGE__ or CORE::die("log called as a class"); $self->info("The Administrator will be notified to review this output when this script completes"); return $self->{'need_notify'} = 1; } sub get_need_notify { my $self = shift; ref $self eq __PACKAGE__ or CORE::die("log called as a class"); return $self->{'need_notify'}; } sub get_stored_log { my $self = shift; ref $self eq __PACKAGE__ or CORE::die("log called as a class"); return if ( !$self->{'to_memory'} ); return $self->{'backlog'}; } sub get_next_log_message { my $self = shift; ref $self eq __PACKAGE__ or CORE::die("log called as a class"); return if ( !$self->{'to_memory'} ); return shift @{ $self->{'backlog'} }; } sub success { goto \&info; } sub out { goto \&info; } sub warn { goto \&warning; } sub die { goto \&_die; } 1; } # --- END Cpanel/Update/Logger.pm { # --- BEGIN Cpanel/FileUtils/TouchFile.pm package Cpanel::FileUtils::TouchFile; use strict; use warnings; use constant { _ENOENT => 2, }; my $logger; our $VERSION = '1.3'; sub _log { my ( $level, $msg ) = @_; require Cpanel::Logger; $logger ||= Cpanel::Logger->new(); $logger->$level($msg); return; } my $mtime; sub touchfile { my ( $file, $verbose, $fail_ok ) = @_; if ( !defined $file ) { _log( 'warn', "touchfile called with undefined file" ); return; } my $mtime; if ( utime undef, undef, $file ) { return 1; } elsif ( $! != _ENOENT() ) { _log( 'warn', "utime($file) as $>: $!" ); $mtime = -e $file ? ( stat _ )[9] : 0; # for warnings-safe numeric comparison if ( !$mtime && $! != _ENOENT ) { _log( 'warn', "Failed to stat($file) as $>: $!" ); return; } } $mtime = ( stat $file )[9] // 0; if ( open my $fh, '>>', $file ) { # append so we don't wipe out contents my $mtime_after_open = ( stat $fh )[9] || 0; # for warnings safe numeric comparison return 1 if $mtime != $mtime_after_open; # in case open does not change it, see comment below } else { _log( 'warn', "Failed to open(>> $file) as $>: $!" ) unless $fail_ok; } if ($fail_ok) { return; } my $at_this_point = ( stat $file )[9] || 0; # for warnings safe numeric comparison if ( $mtime == $at_this_point ) { my $new_at_this_point = ( stat $file )[9] || 0; # for warnings safe numeric comparison if ( $mtime == $new_at_this_point ) { if ($verbose) { _log( 'info', 'Trying to do system “touch” command!' ); } if ( system( 'touch', $file ) != 0 ) { if ($verbose) { _log( 'info', 'system method 1 failed.' ); } } } } if ( !-e $file ) { # obvisouly it didn't touch it if it doesn't exist... _log( 'warn', "Failed to create $file: $!" ); return; } else { my $after_all_that = ( stat $file )[9] || 0; # for warnings safe numeric comparison if ( $mtime && $mtime == $after_all_that ) { _log( 'warn', "mtime of “$file” not changed!" ); return; } return 1; } } 1; } # --- END Cpanel/FileUtils/TouchFile.pm { # --- BEGIN Cpanel/LoadFile/ReadFast.pm package Cpanel::LoadFile::ReadFast; use strict; use warnings; use constant READ_CHUNK => 1 << 18; # 262144 use constant _EINTR => 4; sub read_fast { $_[1] //= q<>; return ( @_ > 3 ? sysread( $_[0], $_[1], $_[2], $_[3] ) : sysread( $_[0], $_[1], $_[2] ) ) // do { goto \&read_fast if $! == _EINTR; die "Failed to read data: $!"; }; } my $_ret; sub read_all_fast { $_[1] //= q<>; $_ret = 1; while ($_ret) { $_ret = sysread( $_[0], $_[1], READ_CHUNK, length $_[1] ) // do { redo if $! == _EINTR; die "Failed to read data: $!"; } } return; } 1; } # --- END Cpanel/LoadFile/ReadFast.pm { # --- BEGIN Cpanel/LoadFile.pm package Cpanel::LoadFile; use strict; use warnings; # use Cpanel::Exception (); # use Cpanel::Fcntl::Constants (); # use Cpanel::LoadFile::ReadFast (); sub loadfileasarrayref { my $fileref = _load_file( shift, { 'array_ref' => 1 } ); return ref $fileref eq 'ARRAY' ? $fileref : undef; } sub loadbinfile { my $fileref = _load_file( shift, { 'binmode' => 1 } ); return ref $fileref eq 'SCALAR' ? $$fileref : undef; } sub slurpfile { my $fh = shift; my $fileref = _load_file(shift); if ( ref $fileref eq 'SCALAR' ) { print {$fh} $$fileref; } return; } sub loadfile { my $fileref = _load_file(@_); return ref $fileref eq 'SCALAR' ? $$fileref : undef; } sub loadfile_r { my ( $file, $arg_ref ) = @_; if ( open my $lf_fh, '<:stdio', $file ) { if ( $arg_ref->{'binmode'} ) { binmode $lf_fh; } my $data; if ( $arg_ref->{'array_ref'} ) { @{$data} = readline $lf_fh; close $lf_fh; return $data; } else { $data = ''; local $@; eval { Cpanel::LoadFile::ReadFast::read_all_fast( $lf_fh, $data ); }; return $@ ? undef : \$data; } } return; } *_load_file = *loadfile_r; sub _open { return _open_if_exists( $_[0] ) || die Cpanel::Exception::create( 'IO::FileNotFound', [ path => $_[0], error => _ENOENT() ] ); } sub _open_if_exists { local $!; open my $fh, '<:stdio', $_[0] or do { if ( $! == _ENOENT() ) { return undef; } die Cpanel::Exception::create( 'IO::FileOpenError', [ path => $_[0], error => $!, mode => '<' ] ); }; return $fh; } sub load_if_exists { my $ref = _load_r( \&_open_if_exists, @_ ); return $ref ? $$ref : undef; } sub load_r_if_exists { return _load_r( \&_open_if_exists, @_ ); } sub load { return ${ _load_r( \&_open, @_ ) }; } sub load_r { return _load_r( \&_open,, @_ ); } sub _load_r { my ( $open_coderef, $path, $offset, $length ) = @_; my $fh = $open_coderef->($path) or return undef; local $!; if ($offset) { sysseek( $fh, $offset, $Cpanel::Fcntl::Constants::SEEK_SET ); if ($!) { die Cpanel::Exception::create( 'IO::FileSeekError', [ path => $path, position => $offset, whence => $Cpanel::Fcntl::Constants::SEEK_SET, error => $!, ] ); } } my $data = q<>; if ( !defined $length ) { my $bytes_read = Cpanel::LoadFile::ReadFast::read_fast( $fh, $data, Cpanel::LoadFile::ReadFast::READ_CHUNK ); if ( $bytes_read == Cpanel::LoadFile::ReadFast::READ_CHUNK ) { my $file_size = -f $fh && -s _; if ($file_size) { Cpanel::LoadFile::ReadFast::read_fast( $fh, $data, $file_size, length $data ) // die _read_err($path); } } Cpanel::LoadFile::ReadFast::read_all_fast( $fh, $data ); } else { my $togo = $length; my $bytes_read; while ( $bytes_read = Cpanel::LoadFile::ReadFast::read_fast( $fh, $data, $togo, length $data ) && length $data < $length ) { $togo -= $bytes_read; } } if ($!) { die Cpanel::Exception::create( 'IO::FileReadError', [ path => $path, error => $! ] ); } close $fh or warn "The system failed to close the file “$path” because of an error: $!"; return \$data; } sub _ENOENT { return 2; } 1; } # --- END Cpanel/LoadFile.pm { # --- BEGIN Cpanel/Usage.pm package Cpanel::Usage; my $g_prefs; # Ref to hash containing up to three boolean preferences, as follows: $Cpanel::Usage::VERSION = '1.08'; sub version { # Reports our current revision number. $Cpanel::Usage::VERSION; } sub wrap_options { my $arg1 = $_[0]; $g_prefs = {}; if ( defined $arg1 && ( ref $arg1 ) =~ /\bHASH\b/ ) { # hash of preferences $g_prefs = $arg1; shift; } my ( $ar_argv, $cr_usage, $hr_opts ) = @_; getoptions( usage( $ar_argv, $cr_usage ), $hr_opts ); } sub usage { my ( $ar_argv, $cr_usage ) = @_; foreach my $arg (@$ar_argv) { if ( $arg =~ /^-+(h|help|usage)$/ ) { if ( defined($cr_usage) ) { &$cr_usage(); } return 1; } } $ar_argv; } sub getoptions { my ( $ar_cmdline, $hr ) = @_; my $non_opt_arg_seen = ""; return $ar_cmdline if ( ref $ar_cmdline || "" ) !~ /\bARRAY\b/; if ( !$#$ar_cmdline && $ar_cmdline->[0] eq "1" ) { return 1; } unless ( defined $hr && ( ref $hr ) =~ /\bHASH\b/ ) { print "Error: opts must be a hash reference\n"; return 2; } my $predefined = keys %{$hr}; my @cmdline_out = @$ar_cmdline; # save a copy of the arg array if ( !$predefined ) { if ( no_switches($ar_cmdline) ) { my $i = 0; foreach my $arg (@$ar_cmdline) { $hr->{ $i++ } = $arg; } return ""; } } if ($predefined) { my $default_value = exists $g_prefs->{'default_value'} ? $g_prefs->{'default_value'} : 0; foreach my $k ( keys %$hr ) { if ( ref( $hr->{$k} ) =~ /^HASH/ ) { foreach my $kk ( keys %{ $hr->{$k} } ) { ${ $hr->{$k}->{$kk} } = $default_value unless ( defined ${ $hr->{$k}->{$kk} } ); } } else { ${ $hr->{$k} } = $default_value unless ( defined ${ $hr->{$k} } ); } } } my $seen_dash_dash = 0; for ( my $i = 0; $i <= $#$ar_cmdline; $i++ ) { if ( $ar_cmdline->[$i] eq '--' ) { $seen_dash_dash = 1; } elsif ( !$seen_dash_dash && $ar_cmdline->[$i] =~ /^-+(.+)$/ ) { my $o = $1; if ( "" ne $non_opt_arg_seen and $g_prefs->{'require_left'} ) { print qq{Error: Preference require_left was specified, all opt args must therefore appear first on the command line; option "-$o" found after "$non_opt_arg_seen" violates this rule\n}; return 3; } my $eq_value = ''; if ( $o =~ /(.+?)=(.+)/ ) { $o = $1; $eq_value = $2; $eq_value =~ s@^\s+@@; $eq_value =~ s@\s+$@@; } if ( $g_prefs->{'strict'} && $predefined && !exists $hr->{$o} ) { print qq{Error: While "strict" is in effect, we have encountered option --$o on the command line, an option that was not specified in the opts hash.\n}; return 4; } if ( # It is a "lone switch", that is, an $eq_value eq '' && ( $i == $#$ar_cmdline || $ar_cmdline->[ $i + 1 ] =~ /^-+.+$/ ) ) { if ( ref( $hr->{$o} ) =~ /^HASH/ ) { foreach my $kk ( keys %{ $hr->{$o} } ) { if ($predefined) { ${ $hr->{$o}->{$kk} }++ if ( exists( $hr->{$o} ) ); } } } else { if ($predefined) { ${ $hr->{$o} }++ if ( exists( $hr->{$o} ) ); } else { $hr->{ _multihelp($o) }++; } } } else { # not a "lone switch"; the next arg might be the value if ( ref( $hr->{$o} ) =~ /^HASH/ ) { print "Error: A multi-level option can only be used when implicitly boolean (true), but you have attempted to use a multi-level option with an explicitly specified option argument.\n"; return 5; } if ( $eq_value ne '' ) { # Sorry, we already have a value for the switch if ($predefined) { ${ $hr->{$o} } = $eq_value if ( exists( $hr->{$o} ) ); } else { $hr->{$o} = $eq_value; } } else { # We have no value yet for the switch, so use next arg as the value $cmdline_out[$i] = undef if $g_prefs->{'remove'}; ++$i; if ($predefined) { ${ $hr->{$o} } = $ar_cmdline->[$i] if ( exists( $hr->{$o} ) ); } else { $hr->{$o} = $ar_cmdline->[$i]; } } } $cmdline_out[$i] = undef if $g_prefs->{'remove'}; } else { # It's a regular (non-hyphen-prefixed) arg, not an option arg if ( "" eq $non_opt_arg_seen ) { $non_opt_arg_seen = $ar_cmdline->[$i]; } } } if ( $g_prefs->{'remove'} ) { @cmdline_out = grep { defined } @cmdline_out; @{$ar_cmdline} = @cmdline_out; } return ""; # aka 0, successful completion } sub _multihelp { # For internal use only my $name = shift; return $name =~ /^(h|help|usage)$/ ? 'help' : $name; } sub no_switches { my $ar = shift; return !grep { /^-+.+/ } @{$ar}; } sub dump_args { my $hr_opts = shift; require Data::Dumper; print Data::Dumper::Dumper($hr_opts); } 1; } # --- END Cpanel/Usage.pm { # --- BEGIN Cpanel/UPID.pm package Cpanel::UPID; use strict; use warnings; # use Cpanel::LoadFile (); my $_boot_time; sub get { my ($pid) = @_; die "Need PID, not “$pid”!" if !$pid || $pid =~ tr<0-9><>c; my $start_delta = _get_pid_start_delta($pid); return undef if !$start_delta; $_boot_time ||= _get_boot_time(); return join( '.', $pid, $start_delta, $_boot_time ); } sub extract_pid { my ($upid) = @_; return substr( $upid, 0, index( $upid, '.' ) ); } sub is_alive { my ($upid) = @_; my $pid = extract_pid($upid); return ( $upid eq ( Cpanel::UPID::get($pid) // q<> ) ); } sub _get_boot_time { my $proc_stat = Cpanel::LoadFile::load('/proc/stat'); my $where_btime = index( $proc_stat, 'btime ' ); die '/proc/stat format changed???' if $where_btime == -1; my $where_lf = index( $proc_stat, "\n", $where_btime ); my $number_start = 6 + $where_btime; return substr( $proc_stat, $number_start, $where_lf - $number_start ); } sub _get_pid_start_delta { my ($pid) = @_; my $proc_pid_stat = Cpanel::LoadFile::load_if_exists("/proc/$pid/stat"); return undef if !$proc_pid_stat; my $right_paren_at = rindex( $proc_pid_stat, ')' ); substr( $proc_pid_stat, 0, 2 + $right_paren_at, q<> ); return ( split m<\s+>, $proc_pid_stat )[19]; } 1; } # --- END Cpanel/UPID.pm { # --- BEGIN Cpanel/Unix/PID/Tiny.pm package Cpanel::Unix::PID::Tiny; use strict; $Cpanel::Unix::PID::Tiny::VERSION = 0.9_2; sub new { my ( $self, $args_hr ) = @_; $args_hr->{'minimum_pid'} = 11 if !exists $args_hr->{'minimum_pid'} || $args_hr->{'minimum_pid'} !~ m{\A\d+\z}ms; # this does what one assumes m{^\d+$} would do if ( defined $args_hr->{'ps_path'} ) { $args_hr->{'ps_path'} .= '/' if $args_hr->{'ps_path'} !~ m{/$}; if ( !-d $args_hr->{'ps_path'} || !-x "$args_hr->{'ps_path'}ps" ) { $args_hr->{'ps_path'} = ''; } } else { $args_hr->{'ps_path'} = ''; } return bless { 'ps_path' => $args_hr->{'ps_path'}, 'minimum_pid' => $args_hr->{'minimum_pid'} }, $self; } sub kill { my ( $self, $pid, $give_kill_a_chance ) = @_; $give_kill_a_chance = int $give_kill_a_chance if defined $give_kill_a_chance; $pid = int $pid; my $min = int $self->{'minimum_pid'}; if ( $pid < $min ) { warn "kill() called with integer value less than $min"; return; } return 1 unless $self->is_pid_running($pid); my @signals = ( 15, 2, 1, 9 ); # TERM, INT, HUP, KILL foreach my $signal ( 15, 2, 1, 9 ) { # TERM, INT, HUP, KILL _kill( $signal, $pid ); if ($give_kill_a_chance) { my $start_time = time(); while ( time() < $start_time + $give_kill_a_chance ) { if ( $self->is_pid_running($pid) ) { select( undef, undef, undef, 0.25 ); } else { return 1; } } } return 1 unless $self->is_pid_running($pid); } return; } sub is_pid_running { my ( $self, $check_pid ) = @_; $check_pid = int $check_pid; return if !$check_pid || $check_pid < 0; return 1 if $> == 0 && _kill( 0, $check_pid ); # if we are superuser we can avoid the the system call. For details see `perldoc -f kill` return 1 if -e "/proc/$$" && -r "/proc/$$" && -r "/proc/$check_pid"; return; } sub pid_info_hash { my ( $self, $pid ) = @_; $pid = int $pid; return if !$pid || $pid < 0; my @outp = $self->_raw_ps( 'u', '-p', $pid ); chomp @outp; my %info; @info{ split( /\s+/, $outp[0], 11 ) } = split( /\s+/, $outp[1], 11 ); return wantarray ? %info : \%info; } sub _raw_ps { my ( $self, @ps_args ) = @_; my $psargs = join( ' ', @ps_args ); my @res = `$self->{'ps_path'}ps $psargs`; return wantarray ? @res : join '', @res; } sub get_pid_from_pidfile { my ( $self, $pid_file ) = @_; return 0 if !-e $pid_file or -z _; open my $pid_fh, '<', $pid_file or return; my $pid = <$pid_fh>; close $pid_fh; return 0 if !$pid; chomp $pid; return int( abs($pid) ); } sub is_pidfile_running { my ( $self, $pid_file ) = @_; my $pid = $self->get_pid_from_pidfile($pid_file) || return; return $pid if $self->is_pid_running($pid); return; } sub pid_file { my ( $self, $pid_file, $newpid, $retry_conf ) = @_; $newpid = $$ if !$newpid; my $rc = $self->pid_file_no_unlink( $pid_file, $newpid, $retry_conf ); if ( $rc && $newpid == $$ ) { $self->create_end_blocks($pid_file); } return 1 if defined $rc && $rc == 1; return 0 if defined $rc && $rc == 0; return; } sub create_end_blocks { my ( $self, $pid_file ) = @_; ## no critic qw(Variables::ProhibitUnusedVariables); if ( $self->{'unlink_end_use_current_pid_only'} ) { eval 'END { unlink $pid_file if $$ eq ' . $$ . '}'; ## no critic qw(ProhibitStringyEval) if ( $self->{'carp_unlink_end'} ) { eval 'END { require Carp;Carp::carp("[info] $$ unlink $pid_file (current pid check)") if $$ eq ' . $$ . '}'; ## no critic qw(ProhibitStringyEval) } } else { eval 'END { unlink $pid_file if Cpanel::Unix::PID::Tiny->get_pid_from_pidfile($pid_file) eq $$ }'; ## no critic qw(ProhibitStringyEval) if ( $self->{'carp_unlink_end'} ) { eval 'END { require Carp;Carp::carp("[info] $$ unlink $pid_file (pid file check)") if Cpanel::Unix::PID::Tiny->get_pid_from_pidfile($pid_file) eq $$ }'; ## no critic qw(ProhibitStringyEval) } } return; } *pid_file_no_cleanup = \&pid_file_no_unlink; # more intuitively named alias sub pid_file_no_unlink { my ( $self, $pid_file, $newpid, $retry_conf ) = @_; $newpid = $$ if !$newpid; if ( ref($retry_conf) eq 'ARRAY' ) { $retry_conf->[0] = int( abs( $retry_conf->[0] ) ); for my $idx ( 1 .. scalar( @{$retry_conf} ) - 1 ) { next if ref $retry_conf->[$idx] eq 'CODE'; $retry_conf->[$idx] = int( abs( $retry_conf->[$idx] ) ); } } else { $retry_conf = [ 3, 1, 2 ]; } my $passes = 0; require Fcntl; EXISTS: $passes++; if ( -e $pid_file ) { my $curpid = $self->get_pid_from_pidfile($pid_file); return 1 if int $curpid == $$ && $newpid == $$; # already setup return if int $curpid == $$; # can't change it while $$ is alive return if $self->is_pid_running( int $curpid ); unlink $pid_file; # must be a stale PID file, so try to remove it for sysopen() } my $pid_fh = _sysopen($pid_file); if ( !$pid_fh ) { return 0 if $passes >= $retry_conf->[0]; if ( ref( $retry_conf->[$passes] ) eq 'CODE' ) { $retry_conf->[$passes]->( $self, $pid_file, $passes ); } else { sleep( $retry_conf->[$passes] ) if $retry_conf->[$passes]; } goto EXISTS; } print {$pid_fh} int( abs($newpid) ); close $pid_fh; return 1; } sub _sysopen { my ($pid_file) = @_; sysopen( my $pid_fh, $pid_file, Fcntl::O_WRONLY() | Fcntl::O_EXCL() | Fcntl::O_CREAT() ) || return; return $pid_fh; } sub _kill { ## no critic(RequireArgUnpacking return CORE::kill(@_); # goto &CORE::kill; is problematic } sub get_run_lock { my ( $pid_file, $min_age_seconds, $max_age_seconds, $cmdline_regex ) = @_; $pid_file or die("Need a pid file to get a run lock."); defined $min_age_seconds or $min_age_seconds = 15 * 60; defined $max_age_seconds or $max_age_seconds = 20 * 60 * 60; foreach ( 1 .. 2 ) { my $upid = Cpanel::Unix::PID::Tiny->new(); my $got_pid = $upid->pid_file($pid_file); return 1 if ($got_pid); my @pid_stat = stat($pid_file); next if ( !@pid_stat ); my $pid_age = time() - $pid_stat[9]; return 0 if ( $min_age_seconds && $pid_age < $min_age_seconds ); my $active_pid = $upid->get_pid_from_pidfile($pid_file); if ( !-e "/proc/$active_pid" ) { unlink $pid_file; next; } open( my $fh, '<', "/proc/$active_pid/cmdline" ) or next; my $cmdline = <$fh>; if ( $max_age_seconds && $pid_age > $max_age_seconds ) { _kill( 'TERM', $active_pid ); unlink $pid_file; } if ( !$cmdline or ( $cmdline_regex && $cmdline !~ $cmdline_regex ) ) { unlink $pid_file; } } return undef; # I give up! } 1; } # --- END Cpanel/Unix/PID/Tiny.pm { # --- BEGIN Cpanel/JSON/Unicode.pm package Cpanel::JSON::Unicode; use strict; use warnings; use constant { _LEAD_SURROGATE_MIN => 0xd800, _TAIL_SURROGATE_MIN => 0xdc00, _SURROGATE_MASK => 0xfc00, _BACKSLASH_ORD => 0x5c, _DOUBLE_QUOTE_ORD => 0x22, }; my $UNICODE_ESCAPE_REGEXP = qr/ (?< _replacement(\$lead_surrogate, $json_sr, $+[0], @{^CAPTURE}) >ge; if ($lead_surrogate) { die sprintf "Incomplete surrogate pair (0x%04x)", $lead_surrogate; } return $ret; } sub _replacement { my ( $lead_surrogate_sr, $json_sr, $match_end, @captures ) = @_; my $num = hex $captures[1]; if ( ( $num & _SURROGATE_MASK ) == _TAIL_SURROGATE_MIN ) { if ($$lead_surrogate_sr) { my $utf8 = _decode_surrogates( $$lead_surrogate_sr, $num ); $$lead_surrogate_sr = undef; return $utf8; } die sprintf "Unpaired trailing surrogate (0x%04x)", $num; } elsif ( ( $num & _SURROGATE_MASK ) == _LEAD_SURROGATE_MIN ) { my $next2 = substr( $$json_sr, $match_end, 2 ); if ( !$next2 || $next2 ne '\\u' ) { die sprintf "Unpaired leading surrogate (0x%04x)", $num; } $$lead_surrogate_sr = $num; return q<>; } elsif ( $num < 0x20 || $num == _BACKSLASH_ORD || $num == _DOUBLE_QUOTE_ORD ) { return $captures[0]; } my $utf8 = chr $num; utf8::encode($utf8); return $utf8; } sub _decode_surrogates { my ( $lead, $tail ) = @_; my $uni = 0x10000 + ( ( $lead - 0xd800 ) << 10 ) + ( $tail - 0xdc00 ); my $un = chr $uni; utf8::encode($un); return $un; } 1; } # --- END Cpanel/JSON/Unicode.pm { # --- BEGIN Cpanel/Encoder/ASCII.pm package Cpanel::Encoder::ASCII; use strict; use warnings; sub to_hex { my ($readable) = @_; $readable =~ s<\\><\\\\>g; $readable =~ s<([\0-\x{1f}\x{7f}-\x{ff}])>eg; return $readable; } 1; } # --- END Cpanel/Encoder/ASCII.pm { # --- BEGIN Cpanel/UTF8/Strict.pm package Cpanel::UTF8::Strict; use strict; use warnings; sub decode { utf8::decode( $_[0] ) or do { local ( $@, $! ); require Cpanel::Encoder::ASCII; die sprintf "Invalid UTF-8 in string: “%s”", Cpanel::Encoder::ASCII::to_hex( $_[0] ); }; return $_[0]; } 1; } # --- END Cpanel/UTF8/Strict.pm { # --- BEGIN Cpanel/JSON.pm package Cpanel::JSON; use strict; # use Cpanel::Fcntl::Constants (); # use Cpanel::FHUtils::Tiny (); # use Cpanel::JSON::Unicode (); # use Cpanel::LoadFile::ReadFast (); use JSON::XS (); # use Cpanel::UTF8::Strict (); our $NO_DECODE_UTF8 = 0; our $DECODE_UTF8 = 1; our $LOAD_STRICT = 0; our $LOAD_RELAXED = 1; our $MAX_LOAD_LENGTH_UNLIMITED = 0; our $MAX_LOAD_LENGTH = 65535; our $MAX_PRIV_LOAD_LENGTH = 4194304; # four megs our $XS_ConvertBlessed_obj; our $XS_RelaxedConvertBlessed_obj; our $XS_NoSetUTF8RelaxedConvertBlessed_obj; our $XS_NoSetUTF8ConvertBlessed_obj; our $VERSION = '2.5'; my $copied_boolean = 0; sub DumpFile { my ( $file, $data ) = @_; if ( Cpanel::FHUtils::Tiny::is_a($file) ) { print {$file} Dump($data) || return 0; } else { if ( open( my $fh, '>', $file ) ) { print {$fh} Dump($data); close($fh); } else { return 0; } } return 1; } sub copy_boolean { if ( !$copied_boolean ) { *Types::Serialiser::Boolean:: = *JSON::PP::Boolean::; $copied_boolean = 1; } return; } sub _create_new_json_object { copy_boolean() if !$copied_boolean; return JSON::XS->new()->shrink(1)->allow_nonref(1)->convert_blessed(1); } sub true { copy_boolean() if !$copied_boolean; my $x = 1; return bless \$x, 'Types::Serialiser::Boolean'; } sub false { copy_boolean() if !$copied_boolean; my $x = 0; return bless \$x, 'Types::Serialiser::Boolean'; } sub pretty_dump { return _create_new_json_object()->pretty(1)->encode( $_[0] ); } my $XS_Canonical_obj; sub canonical_dump { return ( $XS_Canonical_obj ||= _create_new_json_object()->canonical(1) )->encode( $_[0] ); } sub pretty_canonical_dump { return _create_new_json_object()->canonical(1)->indent->space_before->space_after->encode( $_[0] ); } sub Dump { return ( $XS_ConvertBlessed_obj ||= _create_new_json_object() )->encode( $_[0] ); } sub Load { local $@; _replace_unicode_escapes_if_needed( \$_[0] ); return eval { ( $XS_ConvertBlessed_obj ||= _create_new_json_object() )->decode( $_[0] ); } // ( ( $@ && _throw_json_error( $@, $_[1], \$_[0] ) ) || undef ); } sub LoadRelaxed { local $@; _replace_unicode_escapes_if_needed( \$_[0] ); return eval { ( $XS_RelaxedConvertBlessed_obj ||= _create_new_json_object()->relaxed(1) )->decode( $_[0] ); } // ( ( $@ && _throw_json_error( $@, $_[1], \$_[0] ) ) || undef ); } sub _throw_json_error { my ( $exception, $path, $dataref ) = @_; local $@; require Cpanel::Exception; die $exception if $@; die 'Cpanel::Exception'->can('create')->( 'JSONParseError', { 'error' => $exception, 'path' => $path, 'dataref' => $dataref } ); } sub LoadNoSetUTF8 { local $@; _replace_unicode_escapes_if_needed( \$_[0] ); return eval { ( $XS_NoSetUTF8ConvertBlessed_obj ||= _create_new_no_set_utf8_json_object() )->decode( $_[0] ); } // ( ( $@ && _throw_json_error( $@, $_[1], \$_[0] ) ) || undef ); } sub LoadNoSetUTF8Relaxed { local $@; _replace_unicode_escapes_if_needed( \$_[0] ); return eval { ( $XS_NoSetUTF8RelaxedConvertBlessed_obj ||= _create_new_no_set_utf8_json_object()->relaxed(1) )->decode( $_[0] ); } // ( ( $@ && _throw_json_error( $@, $_[1], \$_[0] ) ) || undef ); } sub _create_new_no_set_utf8_json_object { my $obj = _create_new_json_object(); if ( $obj->can('no_set_utf8') ) { $obj->no_set_utf8(1); } else { warn "JSON::XS is missing the no_set_utf8 flag"; } return $obj; } sub _replace_unicode_escapes_if_needed { my $json_r = shift; if ( -1 != index( $$json_r, '\\u' ) ) { Cpanel::JSON::Unicode::replace_unicode_escapes_with_utf8($json_r); } return; } sub SafeLoadFile { # only allow a small bit of data to be loaded return _LoadFile( $_[0], $MAX_LOAD_LENGTH, $_[2] || $NO_DECODE_UTF8, $_[1], $LOAD_STRICT ); } sub LoadFile { return _LoadFile( $_[0], $MAX_LOAD_LENGTH_UNLIMITED, $_[2] || $NO_DECODE_UTF8, $_[1], $LOAD_STRICT ); } sub LoadFileRelaxed { return _LoadFile( $_[0], $MAX_LOAD_LENGTH_UNLIMITED, $_[2] || $NO_DECODE_UTF8, $_[1], $LOAD_RELAXED ); } sub LoadFileNoSetUTF8 { return _LoadFile( $_[0], $_[1] || $MAX_LOAD_LENGTH_UNLIMITED, $DECODE_UTF8, $_[2], $LOAD_STRICT ); } sub _LoadFile { my ( $file, $max, $decode_utf8, $path, $relaxed ) = @_; my $data; if ( Cpanel::FHUtils::Tiny::is_a($file) ) { if ($max) { my $togo = $max; $data = ''; my $bytes_read; while ( $bytes_read = read( $file, $data, $togo, length $data ) && length $data < $max ) { $togo -= $bytes_read; } } else { Cpanel::LoadFile::ReadFast::read_all_fast( $file, $data ); } } else { local $!; open( my $fh, '<:stdio', $file ) or do { my $err = $!; require Cpanel::Carp; die Cpanel::Carp::safe_longmess("Cannot open “$file”: $err"); }; Cpanel::LoadFile::ReadFast::read_all_fast( $fh, $data ); if ( !length $data ) { require Cpanel::Carp; die Cpanel::Carp::safe_longmess("“$file” is empty."); } close $fh or warn "close($file) failed: $!"; } if ( $decode_utf8 && $decode_utf8 == $DECODE_UTF8 ) { Cpanel::UTF8::Strict::decode($data); return $relaxed ? LoadNoSetUTF8Relaxed( $data, $path || $file ) : LoadNoSetUTF8( $data, $path || $file ); } return $relaxed ? LoadRelaxed( $data, $path || $file ) : Load( $data, $path || $file ); } sub SafeDump { my $raw_json = ( $XS_ConvertBlessed_obj ||= _create_new_json_object() )->encode( $_[0] ); $raw_json =~ s{\/}{\\/}g if $raw_json =~ tr{/}{}; return $raw_json; } sub _fh_looks_like_json { my ($fh) = @_; my $bytes_read = 0; my $buffer = q{}; local $!; while ( $buffer !~ tr{ \t\r\n\f}{}c && !eof $fh ) { $bytes_read += ( read( $fh, $buffer, 1, length $buffer ) // die "read() failed: $!" ); } return ( _string_looks_like_json($buffer), \$buffer, ); } sub _string_looks_like_json { ##no critic qw(RequireArgUnpacking) return $_[0] =~ m/\A\s*[\[\{"0-9]/ ? 1 : 0; } sub looks_like_json { ##no critic qw(RequireArgUnpacking) if ( Cpanel::FHUtils::Tiny::is_a( $_[0] ) ) { my $fh = $_[0]; my ( $looks_like_json, $fragment_ref ) = _fh_looks_like_json($fh); my $bytes_read = length $$fragment_ref; if ($bytes_read) { seek( $fh, -$bytes_read, $Cpanel::Fcntl::Constants::SEEK_CUR ) or die "seek() failed: $!"; } return $looks_like_json; } return _string_looks_like_json( $_[0] ); } 1; } # --- END Cpanel/JSON.pm { # --- BEGIN Cpanel/JSON/FailOK.pm package Cpanel::JSON::FailOK; use strict; use warnings; sub LoadJSONModule { local $@; my $load_ok = eval { local $SIG{'__DIE__'}; # Suppress spewage as we may be reading an invalid cache local $SIG{'__WARN__'}; # and since failure is ok to throw it away require Cpanel::JSON; # PPI NO PARSE - FailOK 1; }; if ( !$load_ok && !$ENV{'CPANEL_BASE_INSTALL'} && index( $^X, '/usr/local/cpanel' ) == 0 ) { warn $@; } return $load_ok ? 1 : 0; } sub LoadFile { return undef if !$INC{'Cpanel/JSON.pm'}; return eval { local $SIG{'__DIE__'}; # Suppress spewage as we may be reading an invalid cache local $SIG{'__WARN__'}; # and since failure is ok to throw it away Cpanel::JSON::LoadFile(@_); # PPI NO PARSE - inc check above }; } 1; } # --- END Cpanel/JSON/FailOK.pm { # --- BEGIN Cpanel/ConfigFiles.pm package Cpanel::ConfigFiles; use strict; our $VERSION = '1.4'; our $cpanel_users = '/var/cpanel/users'; our $cpanel_users_cache = '/var/cpanel/users.cache'; our $backup_config_touchfile = '/var/cpanel/config/backups/metadata_disabled'; our $backup_config_touchfile_dir = '/var/cpanel/config/backups/'; our $backup_config = '/var/cpanel/backups/config'; our $cpanel_config_file = '/var/cpanel/cpanel.config'; our $cpanel_config_cache_file = '/var/cpanel/cpanel.config.cache'; our $cpanel_config_defaults_file = '/usr/local/cpanel/etc/cpanel.config'; our $features_cache_dir = "/var/cpanel/features.cache"; our $BASE_INSTALL_IN_PROGRESS_FILE = '/root/installer.lock'; our $CPSRVD_CHECK_CPLISC_FILE = q{/var/cpanel/cpsrvd_check_license}; our $ROOT_CPANEL_HOMEDIR = '/var/cpanel/userhomes/cpanel'; our $RESELLERS_FILE = '/var/cpanel/resellers'; our $RESELLERS_NAMESERVERS_FILE = '/var/cpanel/resellers-nameservers'; our $ACCOUNTING_LOG_FILE = '/var/cpanel/accounting.log'; our $FEATURES_DIR = '/var/cpanel/features'; our $BANDWIDTH_LIMIT_DIR = '/var/cpanel/bwlimited'; our $CUSTOM_PERL_MODULES_DIR = '/var/cpanel/perl'; our $PACKAGES_DIR; #defined below our $DEDICATED_IPS_FILE = '/etc/domainips'; our $DELEGATED_IPS_DIR = '/var/cpanel/dips'; our $MAIN_IPS_DIR = '/var/cpanel/mainips'; our $RESERVED_IPS_FILE = '/etc/reservedips'; our $RESERVED_IP_REASONS_FILE = '/etc/reservedipreasons'; our $IP_ADDRESS_POOL_FILE = '/etc/ipaddrpool'; our $ACL_LISTS_DIR = '/var/cpanel/acllists'; our $OUTGOING_MAIL_SUSPENDED_USERS_FILE = '/etc/outgoing_mail_suspended_users'; our $OUTGOING_MAIL_HOLD_USERS_FILE = '/etc/outgoing_mail_hold_users'; our $TRUEUSEROWNERS_FILE = '/etc/trueuserowners'; our $TRUEUSERDOMAINS_FILE = '/etc/trueuserdomains'; our $USERDOMAINS_FILE = '/etc/userdomains'; our $DBOWNERS_FILE = '/etc/dbowners'; our $DOMAINUSERS_FILE = '/etc/domainusers'; our $LOCALDOMAINS_FILE = '/etc/localdomains'; our $REMOTEDOMAINS_FILE = '/etc/remotedomains'; our $SECONDARYMX_FILE = '/etc/secondarymx'; our $MANUALMX_FILE = '/etc/manualmx'; our $USERBWLIMITS_FILE = '/etc/userbwlimits'; our $MAILIPS_FILE = '/etc/mailips'; our $MAILHELO_FILE = '/etc/mailhelo'; our $NEIGHBOR_NETBLOCKS_FILE = '/etc/neighbor_netblocks'; our $CPANEL_MAIL_NETBLOCKS_FILE = '/etc/cpanel_mail_netblocks'; our $GREYLIST_TRUSTED_NETBLOCKS_FILE = '/etc/greylist_trusted_netblocks'; our $GREYLIST_COMMON_MAIL_PROVIDERS_FILE = '/etc/greylist_common_mail_providers'; our $RECENT_RECIPIENT_MAIL_SERVER_IPS_FILE = '/etc/recent_recipient_mail_server_ips'; our $DEMOUSERS_FILE = '/etc/demousers'; our $APACHE_CONFIG_DIR = '/var/cpanel/conf/apache'; our $APACHE_PRIMARY_VHOSTS_FILE = '/var/cpanel/conf/apache/primary_virtual_hosts.conf'; our $MYSQL_CNF = '/etc/my.cnf'; our $SERVICEAUTH_DIR = '/var/cpanel/serviceauth'; our $DORMANT_SERVICES_DIR = '/var/cpanel/dormant_services'; our $DOMAIN_KEYS_ROOT = '/var/cpanel/domain_keys'; our $USER_NOTIFICATIONS_DIR = '/var/cpanel/user_notifications'; our $DATABASES_INFO_DIR = '/var/cpanel/databases'; our $CPANEL_ROOT = '/usr/local/cpanel'; our $MAILMAN_ROOT = "$CPANEL_ROOT/3rdparty/mailman"; our $FPM_CONFIG_ROOT = "/var/cpanel/php-fpm.d"; our $FPM_ROOT = "/var/cpanel/php-fpm"; our $MAILMAN_LISTS_DIR = "$MAILMAN_ROOT/lists"; our $MAILMAN_USER = 'mailman'; our $FTP_PASSWD_DIR = '/etc/proftpd'; our $FTP_SYMLINKS_DIR = '/etc/pure-ftpd'; our $VALIASES_DIR = '/etc/valiases'; our $VDOMAINALIASES_DIR = '/etc/vdomainaliases'; our $VFILTERS_DIR = '/etc/vfilters'; our $JAILSHELL_PATH = '/usr/local/cpanel/bin/jailshell'; our @COMMONDOMAINS_FILES = qw{/usr/local/cpanel/etc/commondomains /var/cpanel/commondomains}; our $BANDWIDTH_DIRECTORY = '/var/cpanel/bandwidth'; our $BANDWIDTH_CACHE_DIRECTORY = '/var/cpanel/bandwidth.cache'; our $BANDWIDTH_USAGE_CACHE_DIRECTORY = '/var/cpanel/bwusagecache'; our $TEMPLATE_COMPILE_DIR = '/var/cpanel/template_compiles'; our $DOVECOT_SNI_CONF = '/etc/dovecot/sni.conf'; our $DOVECOT_SSL_CONF = '/etc/dovecot/ssl.conf'; our $DOVECOT_SSL_KEY = '/etc/dovecot/ssl/dovecot.key'; our $DOVECOT_SSL_CRT = '/etc/dovecot/ssl/dovecot.crt'; our $GOOGLE_AUTH_TEMPFILE_PREFIX = '/var/cpanel/backups/google_oauth_tempfile_'; our $APACHE_LOGFILE_CLEANUP_QUEUE = '/var/cpanel/apache_logfile_cleanup.json'; our $SKIP_REPO_SETUP_FLAG = '/var/cpanel/skip-repo-setup'; our $ACCOUNT_ENHANCEMENTS_DIR = '/var/cpanel/account_enhancements'; our $ACCOUNT_ENHANCEMENTS_CONFIG_DIR = $Cpanel::ConfigFiles::ACCOUNT_ENHANCEMENTS_DIR . '/config'; our $ACCOUNT_ENHANCEMENTS_INSTALL_FILE = $Cpanel::ConfigFiles::ACCOUNT_ENHANCEMENTS_CONFIG_DIR . '/installed.json'; BEGIN { $PACKAGES_DIR = '/var/cpanel/packages'; } 1; } # --- END Cpanel/ConfigFiles.pm { # --- BEGIN Cpanel/Destruct.pm package Cpanel::Destruct; use strict; my $in_global_destruction = 0; my ( $package, $filename, $line, $subroutine ); # preallocate sub in_dangerous_global_destruction { if ( !$INC{'Test2/API.pm'} ) { return 1 if in_global_destruction() && $INC{'Cpanel/BinCheck.pm'}; } return 0; } sub in_global_destruction { return $in_global_destruction if $in_global_destruction; if ( defined( ${^GLOBAL_PHASE} ) ) { if ( ${^GLOBAL_PHASE} eq 'DESTRUCT' ) { $in_global_destruction = 1; } } else { local $SIG{'__WARN__'} = \&_detect_global_destruction_pre_514_WARN_handler; warn; } return $in_global_destruction; } sub _detect_global_destruction_pre_514_WARN_handler { if ( length $_[0] > 26 && rindex( $_[0], 'during global destruction.' ) == ( length( $_[0] ) - 26 ) ) { $in_global_destruction = 1; } return; } 1; } # --- END Cpanel/Destruct.pm { # --- BEGIN Cpanel/Finally.pm package Cpanel::Finally; use strict; sub new { my ( $class, @todo_crs ) = @_; return bless { 'pid' => $$, 'todo' => \@todo_crs }, $class; } sub add { my ( $self, @new_crs ) = @_; push @{ $self->{'todo'} }, @new_crs; return; } sub skip { my ($self) = @_; return delete $self->{'todo'}; } sub DESTROY { my ($self) = @_; return if $$ != $self->{'pid'} || !$self->{'todo'}; local $@; #prevent insidious clobber of error messages while ( @{ $self->{'todo'} } ) { my $ok = eval { while ( my $item = shift @{ $self->{'todo'} } ) { $item->(); } 1; }; warn $@ if !$ok; } return; } 1; } # --- END Cpanel/Finally.pm { # --- BEGIN Cpanel/Readlink.pm package Cpanel::Readlink; use strict; use warnings; # use Cpanel::Autodie (); # use Cpanel::Exception (); use Cwd (); our $MAX_SYMLINK_DEPTH = 1024; sub deep { my ( $link, $provide_trailing_slash ) = @_; die Cpanel::Exception::create( 'MissingParameter', 'Provide a link path.' ) if !length $link; if ( length($link) > 1 && substr( $link, -1, 1 ) eq '/' ) { $link = substr( $link, 0, length($link) - 1 ); return deep( $link, 1 ); } if ( !-l $link ) { return $provide_trailing_slash ? qq{$link/} : $link; } my %is_link; $is_link{$link} = 1; my $depth = 0; my $base = _get_base_for($link); if ( substr( $link, 0, 1 ) ne '/' ) { $base = Cwd::abs_path() . '/' . $base; } while ( ( $is_link{$link} ||= -l $link ) && ++$depth <= $MAX_SYMLINK_DEPTH ) { $link = Cpanel::Autodie::readlink($link); if ( substr( $link, 0, 1 ) ne '/' ) { $link = $base . '/' . $link; } $base = _get_base_for($link); } return $provide_trailing_slash ? qq{$link/} : $link; } sub _get_base_for { my $basename = shift; my @path = split( '/', $basename ); pop(@path); return join( '/', @path ); } 1; } # --- END Cpanel/Readlink.pm { # --- BEGIN Cpanel/FileUtils/Write.pm package Cpanel::FileUtils::Write; use strict; use warnings; # use Cpanel::Fcntl::Constants (); use Cpanel::Autodie ( 'rename', 'syswrite_sigguard', 'seek', 'print', 'truncate' ); # use Cpanel::Exception (); # use Cpanel::FileUtils::Open (); # use Cpanel::Finally (); # use Cpanel::Debug (); our $Errno_EEXIST = 17; our $MAX_TMPFILE_CREATE_ATTEMPTS = 1024; my $DEFAULT_PERMS = 0600; my $_WRONLY_CREAT_EXCL; sub write_fh { ##no critic qw(RequireArgUnpacking) my $fh = $_[0]; Cpanel::Autodie::seek( $fh, 0, 0 ); Cpanel::Autodie::print( $fh, $_[1] ); Cpanel::Autodie::truncate( $fh, tell($fh) ); return 1; } sub write { return _write_to_tmpfile( @_[ 0 .. 2 ], \&_write_finish ); } sub overwrite { return _write_to_tmpfile( @_[ 0 .. 2 ], \&_overwrite_finish ); } sub overwrite_no_exceptions { my $fh; local $@; eval { $fh = overwrite(@_); 1; } or Cpanel::Debug::log_warn("overwrite exception: $@"); return !!$fh; } sub _write_to_tmpfile { ##no critic qw(RequireArgUnpacking) my ( $filename, $perms_or_hr, $finish_cr ) = ( $_[0], $_[2], $_[3] ); if ( !defined $filename ) { exists $INC{'Carp.pm'} ? Carp::confess("write() called with undefined filename") : die("write() called with undefined filename"); } if ( ref $filename ) { die "Use write_fh to write to a file handle. ($filename is a filehandle, right?)"; } my ( $fh, $tmpfile_is_renamed ); if ( -l $filename ) { require Cpanel::Readlink; $filename = Cpanel::Readlink::deep($filename); } my ( $callback_cr, $tmp_perms ); if ( 'HASH' eq ref $perms_or_hr ) { $callback_cr = $perms_or_hr->{'before_installation'}; } else { $tmp_perms = $perms_or_hr; } $tmp_perms //= $DEFAULT_PERMS; my ( $tmpfile, $attempts ) = ( '', 0 ); while (1) { local $!; my $rand = rand(99999999); $rand = sprintf( '%x', substr( $rand, 2 ) ); my $last_slash_idx = rindex( $filename, '/' ); $tmpfile = $filename; substr( $tmpfile, 1 + $last_slash_idx, 0 ) = ".tmp.$rand."; last if Cpanel::FileUtils::Open::sysopen_with_real_perms( $fh, $tmpfile, ( $_WRONLY_CREAT_EXCL ||= ( $Cpanel::Fcntl::Constants::O_CREAT | $Cpanel::Fcntl::Constants::O_EXCL | $Cpanel::Fcntl::Constants::O_WRONLY ) ), $tmp_perms, ); if ( $! != $Errno_EEXIST ) { die Cpanel::Exception::create( 'IO::FileCreateError', [ error => $!, path => $tmpfile, permissions => $tmp_perms ] ); } ++$attempts; if ( $attempts >= $MAX_TMPFILE_CREATE_ATTEMPTS ) { die Cpanel::Exception::create_raw( 'IO::FileCreateError', "Too many ($MAX_TMPFILE_CREATE_ATTEMPTS) failed attempts to create a temp file as EUID $> and GID $) based on “$filename”! The last tried file was “$tmpfile”, and the last error was: $!" ); } } my $finally = Cpanel::Finally->new( sub { if ( !$tmpfile_is_renamed ) { Cpanel::Autodie::unlink_if_exists($tmpfile); } return; } ); if ( my $ref = ref $_[1] ) { if ( $ref eq 'SCALAR' ) { _write_fh( $fh, ${ $_[1] } ); } else { die Cpanel::Exception::create( 'InvalidParameter', 'Invalid content type “[_1]”, expect a scalar.', [$ref] ); } } else { _write_fh( $fh, $_[1] ); } $callback_cr->($fh) if $callback_cr; $tmpfile_is_renamed = $finish_cr->( $tmpfile, $filename ); if ( !$tmpfile_is_renamed ) { Cpanel::Autodie::unlink_if_exists($tmpfile); } $finally->skip(); return $fh; } *_syswrite = *Cpanel::Autodie::syswrite_sigguard; our $DEBUG_WRITE; sub _write_fh { if ( length $_[1] ) { my $pos = 0; do { local $SIG{'XFSZ'} = 'IGNORE' if $pos; $pos += _syswrite( $_[0], $_[1], length( $_[1] ), $pos ) || do { die "Zero bytes written, non-error!"; }; } while $pos < length( $_[1] ); } return; } sub _write_finish { Cpanel::Autodie::link(@_); return 0; } *_overwrite_finish = *Cpanel::Autodie::rename; 1; } # --- END Cpanel/FileUtils/Write.pm { # --- BEGIN Cpanel/FileUtils/Write/JSON/Lazy.pm package Cpanel::FileUtils::Write::JSON::Lazy; use strict; use warnings; sub write_file { my ( $file_or_fh, $data, $perms ) = @_; if ( exists $INC{'Cpanel/JSON.pm'} && exists $INC{'JSON/XS.pm'} && ( my $Dump = 'Cpanel::JSON'->can('Dump') ) ) { # PPI NO PARSE -- check earlier - must be quoted or it ends up in the stash require Cpanel::FileUtils::Write if !$INC{'Cpanel/FileUtils/Write.pm'}; require Cpanel::FHUtils::Tiny if !$INC{'Cpanel/FHUtils/Tiny.pm'}; my $func = Cpanel::FHUtils::Tiny::is_a($file_or_fh) ? 'write_fh' : 'overwrite'; if ( $func eq 'write_fh' ) { if ( !defined $perms ) { $perms = 0600; } chmod( $perms, $file_or_fh ) or die "Failed to set permissions on the file handle passed to Cpanel::FileUtils::Write::JSON::Lazy::write_file because of an error: $!"; } return Cpanel::FileUtils::Write->can($func)->( $file_or_fh, $Dump->($data), $perms ); } return 0; } sub write_file_pretty { my ( $file_or_fh, $data, $perms ) = @_; if ( exists $INC{'Cpanel/JSON.pm'} && exists $INC{'JSON/XS.pm'} && ( my $Dump = 'Cpanel::JSON'->can('pretty_dump') ) ) { # PPI NO PARSE -- check earlier - must be quoted or it ends up in the stash require Cpanel::FileUtils::Write if !$INC{'Cpanel/FileUtils/Write.pm'}; require Cpanel::FHUtils::Tiny if !$INC{'Cpanel/FHUtils/Tiny.pm'}; my $func = Cpanel::FHUtils::Tiny::is_a($file_or_fh) ? 'write_fh' : 'overwrite'; if ( $func eq 'write_fh' ) { if ( !defined $perms ) { $perms = 0600; } chmod( $perms, $file_or_fh ) or die "Failed to set permissions on the file handle passed to Cpanel::FileUtils::Write::JSON::Lazy::write_file because of an error: $!"; } return Cpanel::FileUtils::Write->can($func)->( $file_or_fh, $Dump->($data), $perms ); } return 0; } 1; } # --- END Cpanel/FileUtils/Write/JSON/Lazy.pm { # --- BEGIN Cpanel/CPAN/I18N/LangTags.pm package Cpanel::CPAN::I18N::LangTags; use strict; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(); our @EXPORT_OK = qw(is_language_tag same_language_tag extract_language_tags super_languages similarity_language_tag is_dialect_of locale2language_tag alternate_language_tags encode_language_tag panic_languages implicate_supers implicate_supers_strictly ); our %EXPORT_TAGS = ( 'ALL' => \@EXPORT_OK ); our %Panic; our $VERSION = "0.35"; sub uniq { my %seen; return grep( !( $seen{$_}++ ), @_ ); } # a util function sub is_language_tag { my ($tag) = lc( $_[0] ); return 0 if $tag eq "i" or $tag eq "x"; return $tag =~ /^(?: # First subtag [xi] | [a-z]{2,3} ) (?: # Subtags thereafter - # separator [a-z0-9]{1,8} # subtag )* $/xs ? 1 : 0; } sub extract_language_tags { my ($text) = $_[0] =~ m/(.+)/ # to make for an untainted result ? $1 : ''; return grep( !m/^[ixIX]$/s, # 'i' and 'x' aren't good tags $text =~ m/ \b (?: # First subtag [iIxX] | [a-zA-Z]{2,3} ) (?: # Subtags thereafter - # separator [a-zA-Z0-9]{1,8} # subtag )* \b /xsg ); } sub same_language_tag { my $el1 = &encode_language_tag( $_[0] ); return 0 unless defined $el1; return $el1 eq &encode_language_tag( $_[1] ) ? 1 : 0; } sub similarity_language_tag { my $lang1 = &encode_language_tag( $_[0] ); my $lang2 = &encode_language_tag( $_[1] ); return undef if !defined($lang1) and !defined($lang2); return 0 if !defined($lang1) or !defined($lang2); my @l1_subtags = split( '-', $lang1 ); my @l2_subtags = split( '-', $lang2 ); my $similarity = 0; while ( @l1_subtags and @l2_subtags ) { if ( shift(@l1_subtags) eq shift(@l2_subtags) ) { ++$similarity; } else { last; } } return $similarity; } sub is_dialect_of { my $lang1 = &encode_language_tag( $_[0] ); my $lang2 = &encode_language_tag( $_[1] ); return undef if !defined($lang1) and !defined($lang2); return 0 if !defined($lang1) or !defined($lang2); return 1 if $lang1 eq $lang2; return 0 if length($lang1) < length($lang2); $lang1 .= '-'; $lang2 .= '-'; return ( substr( $lang1, 0, length($lang2) ) eq $lang2 ) ? 1 : 0; } sub super_languages { my $lang1 = $_[0]; return () unless defined($lang1) && &is_language_tag($lang1); $lang1 =~ s/^nb\b/no-bok/i; # yes, backwards $lang1 =~ s/^nn\b/no-nyn/i; # yes, backwards $lang1 =~ s/^[ix](-hakka\b)/zh$1/i; # goes the right way my @l1_subtags = split( '-', $lang1 ); my @supers = (); foreach my $bit (@l1_subtags) { push @supers, scalar(@supers) ? ( $supers[-1] . '-' . $bit ) : $bit; } pop @supers if @supers; shift @supers if @supers && $supers[0] =~ m<^[iIxX]$>s; return reverse @supers; } sub locale2language_tag { my $lang = $_[0] =~ m/(.+)/ # to make for an untainted result ? $1 : ''; return $lang if &is_language_tag($lang); # like "en" $lang =~ tr<_><->; # "en_US" -> en-US $lang =~ s<(?:[\.\@][-_a-zA-Z0-9]+)+$><>s; # "en_US.ISO8859-1" -> en-US return $lang if &is_language_tag($lang); return; } sub encode_language_tag { my ($tag) = $_[0] || return undef; return undef unless &is_language_tag($tag); $tag =~ s/^iw\b/he/i; # Hebrew $tag =~ s/^in\b/id/i; # Indonesian $tag =~ s/^cre\b/cr/i; # Cree $tag =~ s/^jw\b/jv/i; # Javanese $tag =~ s/^[ix]-lux\b/lb/i; # Luxemburger $tag =~ s/^[ix]-navajo\b/nv/i; # Navajo $tag =~ s/^ji\b/yi/i; # Yiddish $tag =~ s/^[ix]-hakka\b/zh-hakka/i; # Hakka $tag =~ s/^nb\b/no-bok/i; # BACKWARDS for Bokmal $tag =~ s/^nn\b/no-nyn/i; # BACKWARDS for Nynorsk $tag =~ s/^[xiXI]-//s; return "~" . uc($tag); } my %alt = qw( i x x i I X X I ); sub alternate_language_tags { my $tag = $_[0]; return () unless &is_language_tag($tag); my @em; # push 'em real goood! if ( $tag =~ m/^[ix]-hakka\b(.*)/i ) { push @em, "zh-hakka$1"; } elsif ( $tag =~ m/^zh-hakka\b(.*)/i ) { push @em, "x-hakka$1", "i-hakka$1"; } elsif ( $tag =~ m/^he\b(.*)/i ) { push @em, "iw$1"; } elsif ( $tag =~ m/^iw\b(.*)/i ) { push @em, "he$1"; } elsif ( $tag =~ m/^in\b(.*)/i ) { push @em, "id$1"; } elsif ( $tag =~ m/^id\b(.*)/i ) { push @em, "in$1"; } elsif ( $tag =~ m/^[ix]-lux\b(.*)/i ) { push @em, "lb$1"; } elsif ( $tag =~ m/^lb\b(.*)/i ) { push @em, "i-lux$1", "x-lux$1"; } elsif ( $tag =~ m/^[ix]-navajo\b(.*)/i ) { push @em, "nv$1"; } elsif ( $tag =~ m/^nv\b(.*)/i ) { push @em, "i-navajo$1", "x-navajo$1"; } elsif ( $tag =~ m/^yi\b(.*)/i ) { push @em, "ji$1"; } elsif ( $tag =~ m/^ji\b(.*)/i ) { push @em, "yi$1"; } elsif ( $tag =~ m/^nb\b(.*)/i ) { push @em, "no-bok$1"; } elsif ( $tag =~ m/^no-bok\b(.*)/i ) { push @em, "nb$1"; } elsif ( $tag =~ m/^nn\b(.*)/i ) { push @em, "no-nyn$1"; } elsif ( $tag =~ m/^no-nyn\b(.*)/i ) { push @em, "nn$1"; } push @em, $alt{$1} . $2 if $tag =~ /^([XIxi])(-.+)/; return @em; } { my @panic = ( # MUST all be lowercase! 'sv' => [qw(nb no da nn)], 'da' => [qw(nb no sv nn)], # I guess [qw(no nn nb)], [qw(no nn nb sv da)], 'is' => [qw(da sv no nb nn)], 'fo' => [qw(da is no nb nn sv)], # I guess 'pt' => [qw(es ca it fr)], # Portuguese, Spanish, Catalan, Italian, French 'ca' => [qw(es pt it fr)], 'es' => [qw(ca it fr pt)], 'it' => [qw(es fr ca pt)], 'fr' => [qw(es it ca pt)], [ qw( as bn gu kn ks kok ml mni mr ne or pa sa sd te ta ur ) ] => 'hi', 'hi' => [qw(bn pa as or)], ( [qw(ru be uk)] ) x 2, # Russian, Belarusian, Ukranian 'sr' => 'hr', 'hr' => 'sr', # Serb + Croat 'cs' => 'sk', 'sk' => 'cs', # Czech + Slovak 'ms' => 'id', 'id' => 'ms', # Malay + Indonesian 'et' => 'fi', 'fi' => 'et', # Estonian + Finnish ); my ( $k, $v ); while (@panic) { ( $k, $v ) = splice( @panic, 0, 2 ); foreach my $k ( ref($k) ? @$k : $k ) { foreach my $v ( ref($v) ? @$v : $v ) { push @{ $Panic{$k} ||= [] }, $v unless $k eq $v; } } } } sub panic_languages { my ( @out, %seen ); foreach my $t (@_) { next unless $t; next if $seen{$t}++; # so we don't return it or hit it again push @out, @{ $Panic{ lc $t } || next }; } return grep !$seen{$_}++, @out, 'en'; } sub implicate_supers { my @languages = grep is_language_tag($_), @_; my %seen_encoded; foreach my $lang (@languages) { $seen_encoded{ Cpanel::CPAN::I18N::LangTags::encode_language_tag($lang) } = 1; } my (@output_languages); foreach my $lang (@languages) { push @output_languages, $lang; foreach my $s ( Cpanel::CPAN::I18N::LangTags::super_languages($lang) ) { last if $seen_encoded{ Cpanel::CPAN::I18N::LangTags::encode_language_tag($s) }; push @output_languages, $s; } } return uniq(@output_languages); } sub implicate_supers_strictly { my @tags = grep is_language_tag($_), @_; return uniq( @_, map super_languages($_), @_ ); } 1; } # --- END Cpanel/CPAN/I18N/LangTags.pm { # --- BEGIN Cpanel/CPAN/I18N/LangTags/Detect.pm package Cpanel::CPAN::I18N::LangTags::Detect; use strict; use vars qw( @ISA $VERSION $MATCH_SUPERS $USING_LANGUAGE_TAGS $USE_LITERALS $MATCH_SUPERS_TIGHTLY); BEGIN { unless ( defined &DEBUG ) { *DEBUG = sub () { 0 } } } $VERSION = "1.04"; @ISA = (); # use Cpanel::CPAN::I18N::LangTags (); sub _uniq { my %seen; return grep( !( $seen{$_}++ ), @_ ); } sub _normalize { my (@languages) = map lc($_), grep $_, map { ; $_, Cpanel::CPAN::I18N::LangTags::alternate_language_tags($_) } @_; return _uniq(@languages) if wantarray; return $languages[0]; } sub detect () { return __PACKAGE__->ambient_langprefs; } sub ambient_langprefs { # always returns things untainted my $base_class = $_[0]; return $base_class->http_accept_langs if length( $ENV{'REQUEST_METHOD'} || '' ); # I'm a CGI my @languages; foreach my $envname (qw( LANGUAGE LC_ALL LC_MESSAGES LANG )) { next unless $ENV{$envname}; DEBUG and print "Noting \$$envname: $ENV{$envname}\n"; push @languages, map Cpanel::CPAN::I18N::LangTags::locale2language_tag($_), split m/[,:]/, $ENV{$envname}; last; # first one wins } if ( $ENV{'IGNORE_WIN32_LOCALE'} ) { } elsif ( &_try_use('Win32::Locale') ) { push @languages, Win32::Locale::get_language() || '' if defined &Win32::Locale::get_language; } return _normalize @languages; } sub http_accept_langs { no integer; my $in = ( @_ > 1 ) ? $_[1] : $ENV{'HTTP_ACCEPT_LANGUAGE'}; return () unless defined $in and length $in; $in =~ s/\([^\)]*\)//g; # nix just about any comment if ( $in =~ m/^\s*([a-zA-Z][-a-zA-Z]+)\s*$/s ) { return _normalize $1; } elsif ( $in =~ m/^\s*[a-zA-Z][-a-zA-Z]+(?:\s*,\s*[a-zA-Z][-a-zA-Z]+)*\s*$/s ) { return _normalize( $in =~ m/([a-zA-Z][-a-zA-Z]+)/g ); } $in =~ s/\s+//g; # Yes, we can just do without the WS! my @in = $in =~ m/([^,]+)/g; my %pref; my $q; foreach my $tag (@in) { next unless $tag =~ m/^([a-zA-Z][-a-zA-Z]+) (?: ;q= ( \d* # a bit too broad of a RE, but so what. (?: \.\d+ )? ) )? $ /sx ; $q = ( defined $2 and length $2 ) ? $2 : 1; push @{ $pref{$q} }, lc $1; } return _normalize( map @{ $pref{$_} }, sort { $b <=> $a } keys %pref ); } my %tried = (); sub _try_use { # Basically a wrapper around "require Modulename" return $tried{ $_[0] } if exists $tried{ $_[0] }; # memoization my $module = $_[0]; # ASSUME sane module name! { no strict 'refs'; return ( $tried{$module} = 1 ) if %{ $module . "::Lexicon" } or @{ $module . "::ISA" }; } print " About to use $module ...\n" if DEBUG; { local $SIG{'__DIE__'}; eval "require $module"; # used to be "use $module", but no point in that. } if ($@) { print "Error using $module \: $@\n" if DEBUG > 1; return $tried{$module} = 0; } else { print " OK, $module is used\n" if DEBUG; return $tried{$module} = 1; } } 1; } # --- END Cpanel/CPAN/I18N/LangTags/Detect.pm { # --- BEGIN Cpanel/CPAN/Locale/Maketext.pm package Cpanel::CPAN::Locale::Maketext; use strict; our @ISA; our $VERSION; our $MATCH_SUPERS; our $USING_LANGUAGE_TAGS; our $USE_LITERALS; our $MATCH_SUPERS_TIGHTLY; use constant IS_ASCII => ord('A') == 65; BEGIN { unless ( defined &DEBUG ) { *DEBUG = sub () { 0 } } } $VERSION = '1.13_89'; $VERSION = eval $VERSION; @ISA = (); $MATCH_SUPERS = 1; $MATCH_SUPERS_TIGHTLY = 1; $USING_LANGUAGE_TAGS = 1; my $FORCE_REGEX_LAZY = ''; $USE_LITERALS = 1 unless defined $USE_LITERALS; my %isa_scan = (); my %isa_ones = (); sub quant { my ( $handle, $num, @forms ) = @_; return $num if @forms == 0; # what should this mean? return $forms[2] if @forms > 2 and $num == 0; # special zeroth case return ( $handle->numf($num) . ' ' . $handle->numerate( $num, @forms ) ); } sub numerate { my ( $handle, $num, @forms ) = @_; my $s = ( $num == 1 ); return '' unless @forms; if ( @forms == 1 ) { # only the headword form specified return $s ? $forms[0] : ( $forms[0] . 's' ); # very cheap hack. } else { # sing and plural were specified return $s ? $forms[0] : $forms[1]; } } sub numf { my ( $handle, $num ) = @_[ 0, 1 ]; if ( $num < 10_000_000_000 and $num > -10_000_000_000 and $num == int($num) ) { $num += 0; # Just use normal integer stringification. } else { $num = CORE::sprintf( '%G', $num ); } while ( $num =~ s/$FORCE_REGEX_LAZY^([-+]?\d+)(\d{3})/$1,$2/os ) { 1 } # right from perlfaq5 $num =~ tr<.,><,.> if ref($handle) and $handle->{'numf_comma'}; return $num; } sub sprintf { no integer; my ( $handle, $format, @params ) = @_; return CORE::sprintf( $format, @params ); } use integer; # vroom vroom... applies to the whole rest of the module sub language_tag { my $it = ref( $_[0] ) || $_[0]; return undef unless $it =~ m/$FORCE_REGEX_LAZY([^':]+)(?:::)?$/os; $it = lc($1); $it =~ tr<_><->; return $it; } sub encoding { my $it = $_[0]; return ( ( ref($it) && $it->{'encoding'} ) || 'iso-8859-1' # Latin-1 ); } sub fallback_languages { return ( 'i-default', 'en', 'en-US' ) } sub fallback_language_classes { return () } sub fail_with { # an actual attribute method! my ( $handle, @params ) = @_; return unless ref($handle); $handle->{'fail'} = $params[0] if @params; return $handle->{'fail'}; } sub blacklist { my ( $handle, @methods ) = @_; unless ( defined $handle->{'blacklist'} ) { no strict 'refs'; $handle->{'blacklist'} = { map { $_ => 1 } ( qw/ blacklist encoding fail_with failure_handler_auto fallback_language_classes fallback_languages get_handle init language_tag maketext new whitelist /, grep { substr( $_, 0, 1 ) eq '_' } keys %{ __PACKAGE__ . "::" } ), }; } if ( scalar @methods ) { $handle->{'blacklist'} = { %{ $handle->{'blacklist'} }, map { $_ => 1 } @methods }; } delete $handle->{'_external_lex_cache'}; return; } sub whitelist { my ( $handle, @methods ) = @_; if ( scalar @methods ) { if ( defined $handle->{'whitelist'} ) { $handle->{'whitelist'} = { %{ $handle->{'whitelist'} }, map { $_ => 1 } @methods }; } else { $handle->{'whitelist'} = { map { $_ => 1 } @methods }; } } delete $handle->{'_external_lex_cache'}; return; } sub failure_handler_auto { my $handle = shift; my $phrase = shift; $handle->{'failure_lex'} ||= {}; my $lex = $handle->{'failure_lex'}; my $value = $lex->{$phrase} ||= ( $phrase !~ tr/[// ? \"$phrase" : $handle->_compile($phrase) ); return ${$value} if ref($value) eq 'SCALAR'; return $value if ref($value) ne 'CODE'; { local $SIG{'__DIE__'}; eval { $value = &$value( $handle, @_ ) }; } if ($@) { my $err = $@; $err =~ s{\s+at\s+\(eval\s+\d+\)\s+line\s+(\d+)\.?\n?} {\n in bracket code [compiled line $1],}s; require Carp; Carp::croak("Error in maketexting \"$phrase\":\n$err as used"); } else { return $value; } } sub new { my $class = ref( $_[0] ) || $_[0]; my $handle = bless {}, $class; $handle->blacklist; $handle->init; return $handle; } sub init { return } # no-op sub maketext { unless ( @_ > 1 ) { require Carp; Carp::croak('maketext requires at least one parameter'); } my ( $handle, $phrase ) = splice( @_, 0, 2 ); unless ( defined($handle) && defined($phrase) ) { require Carp; Carp::confess('No handle/phrase'); } my $value; if ( exists $handle->{'_external_lex_cache'}{$phrase} ) { DEBUG and warn "* Using external lex cache version of \"$phrase\"\n"; $value = $handle->{'_external_lex_cache'}{$phrase}; } else { my $ns = ref($handle) || $handle; foreach my $h_r ( @{ $isa_scan{$ns} || $handle->_lex_refs } ) { DEBUG and warn "* Looking up \"$phrase\" in $h_r\n"; if ( defined( $value = $h_r->{$phrase} ) ) { # Minimize looking at $h_r as much as possible as an expensive tied hash to CDB_File DEBUG and warn " Found \"$phrase\" in $h_r\n"; unless ( ref $value ) { if ( !length $value ) { DEBUG and warn " value is undef or ''"; if ( $isa_ones{"$h_r"} ) { DEBUG and warn " $ns ($h_r) is Onesided and \"$phrase\" entry is undef or ''\n"; $value = $phrase; } } if ( $handle->{'use_external_lex_cache'} ) { $handle->{'_external_lex_cache'}{$phrase} = $value = ( $value !~ tr/[// ? \"$value" : $handle->_compile($value) ); } else { $h_r->{$phrase} = $value = ( $value !~ tr/[// ? \"$value" : $handle->_compile($value) ); } } last; } elsif ( substr( $phrase, 0, 1 ) ne '_' and ( $handle->{'use_external_lex_cache'} ? ( exists $handle->{'_external_lex_cache'}{'_AUTO'} ? $handle->{'_external_lex_cache'}{'_AUTO'} : $h_r->{'_AUTO'} ) : $h_r->{'_AUTO'} ) ) { DEBUG and warn " Automaking \"$phrase\" into $h_r\n"; if ( $handle->{'use_external_lex_cache'} ) { $handle->{'_external_lex_cache'}{$phrase} = $value = ( $phrase !~ tr/[// ? \"$phrase" : $handle->_compile($phrase) ); } else { $h_r->{$phrase} = $value = ( $phrase !~ tr/[// ? \"$phrase" : $handle->_compile($phrase) ); } last; } DEBUG > 1 and print " Not found in $h_r, nor automakable\n"; } if ( !defined($value) ) { delete $handle->{'_external_lex_cache'}{$phrase}; DEBUG and warn "! Lookup of \"$phrase\" in/under ", ref($handle) || $handle, " fails.\n"; if ( ref($handle) and $handle->{'fail'} ) { DEBUG and warn "WARNING0: maketext fails looking for <$phrase>\n"; my $fail; if ( ref( $fail = $handle->{'fail'} ) eq 'CODE' ) { # it's a sub reference return &{$fail}( $handle, $phrase, @_ ); } else { # It's a method name return $handle->$fail( $phrase, @_ ); } } else { require Carp; Carp::croak("maketext doesn't know how to say:\n$phrase\nas needed"); } } } if ( ref($value) eq 'SCALAR' ) { return $$value; } elsif ( ref($value) ne 'CODE' ) { return $value; } local $@; { local $SIG{'__DIE__'}; return eval { &$value( $handle, @_ ) } unless $@; } my $err = $@; $err =~ s{\s+at\s+\(eval\s+\d+\)\s+line\s+(\d+)\.?\n?} {\n in bracket code [compiled line $1],}s; require Carp; Carp::croak("Error in maketexting \"$phrase\":\n$err as used"); } sub get_handle { # This is a constructor and, yes, it CAN FAIL. my ( $base_class, @languages ) = @_; $base_class = ref($base_class) || $base_class; my $load_alternate_language_tags = 0; if (@languages) { DEBUG and warn 'Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; $load_alternate_language_tags = 1 if $USING_LANGUAGE_TAGS; # An explicit language-list was given! } else { @languages = $base_class->_ambient_langprefs; } my %seen; foreach my $module_name ( map { $base_class . '::' . $_ } @languages ) { next if !length $module_name # sanity || $seen{$module_name}++ # Already been here, and it was no-go || $module_name =~ tr{/-}{} || !&_try_use($module_name); # Try to use() it, but can't it. return ( $module_name->new ); # Make it! } if ($load_alternate_language_tags) { require Cpanel::CPAN::I18N::LangTags; @languages = map { ; $_, Cpanel::CPAN::I18N::LangTags::alternate_language_tags($_) } map Cpanel::CPAN::I18N::LangTags::locale2language_tag($_), @languages; DEBUG and warn 'Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } @languages = $base_class->_langtag_munging(@languages); foreach my $module_name ( map { $base_class . '::' . $_ } @languages ) { next if !length $module_name # sanity || $seen{$module_name}++ # Already been here, and it was no-go || $module_name =~ tr{/-}{} || !&_try_use($module_name); # Try to use() it, but can't it. return ( $module_name->new ); # Make it! } return undef; # Fail! } sub _langtag_munging { my ( $base_class, @languages ) = @_; DEBUG and warn 'Lgs1: ', map( "<$_>", @languages ), "\n"; if ($USING_LANGUAGE_TAGS) { require Cpanel::CPAN::I18N::LangTags; DEBUG and warn 'Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; @languages = $base_class->_add_supers(@languages); push @languages, Cpanel::CPAN::I18N::LangTags::panic_languages(@languages); DEBUG and warn "After adding panic languages:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; push @languages, $base_class->fallback_languages; DEBUG and warn 'Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; @languages = # final bit of processing to turn them into classname things map { my $it = $_; # copy $it =~ tr<-A-Z><_a-z>; # lc, and turn - to _ $it =~ tr<_a-z0-9><>cd; # remove all but a-z0-9_ $it; } @languages; DEBUG and warn "Nearing end of munging:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } else { DEBUG and warn "Bypassing language-tags.\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } DEBUG and warn "Before adding fallback classes:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; push @languages, $base_class->fallback_language_classes; DEBUG and warn "Finally:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; return @languages; } sub _ambient_langprefs { require Cpanel::CPAN::I18N::LangTags::Detect; return Cpanel::CPAN::I18N::LangTags::Detect::detect(); } sub _add_supers { my ( $base_class, @languages ) = @_; if ( !$MATCH_SUPERS ) { DEBUG and warn "Bypassing any super-matching.\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } elsif ($MATCH_SUPERS_TIGHTLY) { require Cpanel::CPAN::I18N::LangTags; DEBUG and warn "Before adding new supers tightly:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; @languages = Cpanel::CPAN::I18N::LangTags::implicate_supers(@languages); DEBUG and warn "After adding new supers tightly:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } else { require Cpanel::CPAN::I18N::LangTags; DEBUG and warn "Before adding supers to end:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; @languages = Cpanel::CPAN::I18N::LangTags::implicate_supers_strictly(@languages); DEBUG and warn "After adding supers to end:\n", ' Lgs@', __LINE__, ': ', map( "<$_>", @languages ), "\n"; } return @languages; } my %tried = (); sub _try_use { # Basically a wrapper around "require Modulename" return $tried{ $_[0] } if exists $tried{ $_[0] }; # memoization my $module = $_[0]; # ASSUME sane module name! { no strict 'refs'; return ( $tried{$module} = 1 ) if ( %{ $module . '::Lexicon' } or @{ $module . '::ISA' } ); } DEBUG and warn " About to use $module ...\n"; { local $SIG{'__DIE__'}; eval "require $module"; # used to be "use $module", but no point in that. } if ($@) { DEBUG and warn "Error using $module \: $@\n"; return $tried{$module} = 0; } else { DEBUG and warn " OK, $module is used\n"; return $tried{$module} = 1; } } sub _lex_refs { # report the lexicon references for this handle's class no strict 'refs'; no warnings 'once'; my $class = ref( $_[0] ) || $_[0]; DEBUG and warn "Lex refs lookup on $class\n"; return $isa_scan{$class} if exists $isa_scan{$class}; # memoization! my @lex_refs; my $seen_r = ref( $_[1] ) ? $_[1] : {}; if ( defined( *{ $class . '::Lexicon' }{'HASH'} ) ) { push @lex_refs, *{ $class . '::Lexicon' }{'HASH'}; $isa_ones{"$lex_refs[-1]"} = defined ${ $class . '::Onesided' } && ${ $class . '::Onesided' } ? 1 : 0; DEBUG and warn '%' . $class . '::Lexicon contains ', scalar( keys %{ $class . '::Lexicon' } ), " entries\n"; } foreach my $superclass ( @{ $class . '::ISA' } ) { DEBUG and warn " Super-class search into $superclass\n"; next if $seen_r->{$superclass}++; push @lex_refs, @{ &_lex_refs( $superclass, $seen_r ) }; # call myself } $isa_scan{$class} = \@lex_refs; # save for next time return \@lex_refs; } sub clear_isa_scan { %isa_scan = (); return; } # end on a note of simplicity! BEGIN { } sub _compile { return \"$_[1]" if $_[1] !~ tr/[//; my ( $handle, $call_count, $big_pile, @c, @code ) = ( $_[0], 0, '', '' ); { my ( $in_group, $m, @params ) = (0); # scratch my $under_one = $_[1]; # There are taint issues using regex on $_ - perlbug 60378,27344 while ( $under_one =~ # Iterate over chunks. m/\G( [^\~\[\]]+ # non-~[] stuff | ~. # ~[, ~], ~~, ~other | \[ # [ presumably opening a group | \] # ] presumably closing a group | ~ # terminal ~ ? | $ )/xgs ) { DEBUG > 2 and warn qq{ "$1"\n}; if ( $1 eq '[' or $1 eq '' ) { # "[" or end if ($in_group) { if ( $1 eq '' ) { $handle->_die_pointing( $under_one, 'Unterminated bracket group' ); } else { $handle->_die_pointing( $under_one, 'You can\'t nest bracket groups' ); } } else { if ( $1 eq '' ) { DEBUG > 2 and warn " [end-string]\n"; } else { $in_group = 1; } die "How come \@c is empty?? in <$under_one>" unless @c; # sanity if ( length $c[-1] ) { $big_pile .= $c[-1]; if ( $USE_LITERALS and ( IS_ASCII ? $c[-1] !~ tr/\x20-\x7E//c : $c[-1] !~ m/$FORCE_REGEX_LAZY[^ !"\#\$%&'()*+,\-.\/0-9:;<=>?\@A-Z[\\\]^_`a-z{|}~\x07]/os ) ) { $c[-1] =~ s/'/\\'/g if $c[-1] =~ tr{'}{}; push @code, q{ '} . $c[-1] . "',\n"; $c[-1] = ''; # reuse this slot } else { $c[-1] =~ s/\\\\/\\/g if $c[-1] =~ tr{\\}{}; push @code, ' $c[' . $#c . "],\n"; push @c, ''; # new chunk } } } } elsif ( $1 eq ']' ) { # "]" if ($in_group) { $in_group = 0; DEBUG > 2 and warn " --Closing group [$c[-1]]\n"; if ( !length( $c[-1] ) or $c[-1] !~ tr/ \t\r\n\f//c ) { DEBUG > 2 and warn " -- (Ignoring)\n"; $c[-1] = ''; # reset out chink next; } ( $m, @params ) = split( /,/, $c[-1], -1 ); # was /\s*,\s*/ if (IS_ASCII) { # ASCII, etc foreach ( $m, @params ) { tr/\x7F/,/ } } else { # EBCDIC (1047, 0037, POSIX-BC) foreach ( $m, @params ) { tr/\x07/,/ } } if ( $m eq '_1' or $m eq '_2' or $m eq '_3' or $m eq '_*' or ( substr( $m, 0, 1 ) eq '_' && $m =~ m/^_(-?\d+)$/s ) ) { unshift @params, $m; $m = ''; } elsif ( $m eq '*' ) { $m = 'quant'; # "*" for "times": "4 cars" is 4 times "cars" } elsif ( $m eq '#' ) { $m = 'numf'; # "#" for "number": [#,_1] for "the number _1" } if ( $m eq '' ) { push @code, ' ('; } elsif ( $m !~ tr{a-zA-Z0-9_}{}c # does not contain non-word characters && !$handle->{'blacklist'}{$m} && ( !defined $handle->{'whitelist'} || $handle->{'whitelist'}{$m} ) ) { push @code, ' $_[0]->' . $m . '('; } else { $handle->_die_pointing( $under_one, "Can't use \"$m\" as a method name in bracket group", 2 + length( $c[-1] ) ); } pop @c; # we don't need that chunk anymore ++$call_count; foreach my $p (@params) { if ( $p eq '_*' ) { $code[-1] .= ' @_[1 .. $#_], '; } elsif ( substr( $p, 0, 1 ) eq '_' && ( $p eq '_1' || $p eq '_2' || $p eq '_3' || $p =~ m/^_-?\d+$/s ) ) { $code[-1] .= '$_[' . ( 0 + substr( $p, 1 ) ) . '], '; } elsif ( $USE_LITERALS and ( IS_ASCII ? $p !~ tr/\x20-\x7E//c : $p !~ m/$FORCE_REGEX_LAZY[^ !"\#\$%&'()*+,\-.\/0-9:;<=>?\@A-Z[\\\]^_`a-z{|}~\x07]/os ) ) { $p =~ s/'/\\'/g if $p =~ tr{'}{}; $code[-1] .= q{'} . $p . q{', }; } else { push @c, $p; push @code, ' $c[' . $#c . '], '; } } $code[-1] .= "),\n"; push @c, ''; } else { $handle->_die_pointing( $under_one, q{Unbalanced ']'} ); } } elsif ( substr( $1, 0, 1 ) ne '~' ) { if ( $1 =~ tr{\\}{} ) { my $text = $1; $text =~ s/\\/\\\\/g; $c[-1] .= $text; } else { $c[-1] .= $1; } } elsif ( $1 eq '~~' ) { # "~~" $c[-1] .= '~'; } elsif ( $1 eq '~[' ) { # "~[" $c[-1] .= '['; } elsif ( $1 eq '~]' ) { # "~]" $c[-1] .= ']'; } elsif ( $1 eq '~,' ) { # "~," if ($in_group) { if (IS_ASCII) { # ASCII etc $c[-1] .= "\x7F"; } else { # EBCDIC (cp 1047, 0037, POSIX-BC) $c[-1] .= "\x07"; } } else { $c[-1] .= '~,'; } } elsif ( $1 eq '~' ) { # possible only at string-end, it seems. $c[-1] .= '~'; } else { my $text = $1; $text =~ s/\\/\\\\/g if $text =~ tr{\\}{}; $c[-1] .= $text; } } } if ($call_count) { undef $big_pile; # Well, nevermind that. } else { return \$big_pile; } die q{Last chunk isn't null??} if @c and length $c[-1]; # sanity DEBUG and warn scalar(@c), " chunks under closure\n"; my $sub; if ( @code == 0 ) { # not possible? DEBUG and warn "Empty code\n"; return \''; } elsif ( scalar @code > 1 ) { # most cases, presumably! $sub = "sub { join '', map { defined \$_ ? \$_ : '' } @code }"; } else { $sub = "sub { $code[0] }"; } DEBUG and warn $sub; my $code; { use strict; $code = eval $sub; die "$@ while evalling" . $sub if $@; # Should be impossible. } return $code; } sub _die_pointing { my $target = shift; $target = ref($target) || $target; # class name my $i = index( $_[0], "\n" ); my $pointy; my $pos = pos( $_[0] ) - ( defined( $_[2] ) ? $_[2] : 0 ) - 1; if ( $pos < 1 ) { $pointy = "^=== near there\n"; } else { # we need to space over my $first_tab = index( $_[0], "\t" ); if ( $pos > 2 and ( -1 == $first_tab or $first_tab > pos( $_[0] ) ) ) { $pointy = ( '=' x $pos ) . "^ near there\n"; } else { $pointy = substr( $_[0], 0, $pos ); $pointy =~ tr/\t //cd; $pointy .= "^=== near there\n"; } } my $errmsg = "$_[1], in\:\n$_[0]"; if ( $i == -1 ) { $errmsg .= "\n" . $pointy; } elsif ( $i == ( length( $_[0] ) - 1 ) ) { $errmsg .= $pointy; } else { } require Carp; Carp::croak("$errmsg via $target, as used"); } 1; } # --- END Cpanel/CPAN/Locale/Maketext.pm { # --- BEGIN Cpanel/Locale/Utils/Normalize.pm package Cpanel::Locale::Utils::Normalize; use strict; use warnings; sub normalize_tag { my ($tag) = @_; return if !defined $tag; $tag =~ tr/A-Z/a-z/; $tag =~ tr{\r\n \t\f}{}d; if ( $tag =~ tr{a-z0-9}{}c ) { $tag =~ s{[^a-z0-9]+$}{}; # I18N::LangTags::locale2language_tag() does not allow trailing '_' $tag =~ tr{a-z0-9}{_}c; } if ( length $tag > 8 ) { while ( $tag =~ s/([^_]{8})([^_])/$1\_$2/ ) { } # I18N::LangTags::locale2language_tag() only allows parts between 1 and 8 character } return $tag; } 1; } # --- END Cpanel/Locale/Utils/Normalize.pm { # --- BEGIN Cpanel/CPAN/Locales/Legacy.pm package Cpanel::CPAN::Locales::Legacy; use strict; sub numf { my ( $self, $always_return ) = @_; my $class = ref($self) ? ref($self) : $self; $always_return ||= 0; $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} = '' if !defined $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'}; $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} = '' if !defined $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'}; if ( !$self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} || !$self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} ) { if ($always_return) { if ( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} || !$self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} ) { return 2 if $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} eq '.'; return 1; } elsif ( !$self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} || $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} ) { return 2 if $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} eq ','; return 1; } else { return 1; } } } if ( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'decimal'} eq "\#\,\#\#0\.\#\#\#" ) { if ( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} eq ',' && $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} eq '.' ) { return 1; } elsif ( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} eq '.' && $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} eq ',' ) { return 2; } } elsif ( $always_return && $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} && $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} ) { return 2 if $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} eq ','; return 2 if $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} eq '.'; return 1; } return [ $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'decimal'}, $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'}, $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'}, ]; } 1; } # --- END Cpanel/CPAN/Locales/Legacy.pm { # --- BEGIN Cpanel/CPAN/Locales/DB/LocaleDisplayPattern/Tiny.pm package Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny; use strict; $Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny::VERSION = '0.09'; $Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny::cldr_version = '2.0'; my %locale_display_lookup = ( 'ksh' => '{0} en {1}', 'ja' => '{0}({1})', 'zh' => '{0}({1})', 'ko' => '{0}({1})', ); sub get_locale_display_pattern { if ( exists $locale_display_lookup{ $_[0] } ) { return $locale_display_lookup{ $_[0] }; } else { require Cpanel::CPAN::Locales; my ($l) = Cpanel::CPAN::Locales::split_tag( $_[0] ); if ( $l ne $_[0] ) { return $locale_display_lookup{$l} if exists $locale_display_lookup{$l}; } return "\{0\}\ \(\{1\}\)"; } } 1; } # --- END Cpanel/CPAN/Locales/DB/LocaleDisplayPattern/Tiny.pm { # --- BEGIN Cpanel/CPAN/Locales/DB/CharacterOrientation/Tiny.pm package Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny; use strict; $Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny::VERSION = '0.09'; $Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny::cldr_version = '2.0'; my %rtl = ( 'ur' => '', 'ku' => '', 'he' => '', 'fa' => '', 'ps' => '', 'ar' => '', ); sub get_orientation { if ( exists $rtl{ $_[0] } ) { return 'right-to-left'; } else { require Cpanel::CPAN::Locales; my ($l) = Cpanel::CPAN::Locales::split_tag( $_[0] ); if ( $l ne $_[0] ) { return 'right-to-left' if exists $rtl{$l}; } return 'left-to-right'; } } 1; } # --- END Cpanel/CPAN/Locales/DB/CharacterOrientation/Tiny.pm { # --- BEGIN Cpanel/CPAN/Locales/Compile.pm package Cpanel::CPAN::Locales::Compile; use strict; use warnings; sub plural_rule_string_to_code { my ( $plural_rule_string, $return ) = @_; if ( !defined $return ) { $return = 1; } my %m; while ( $plural_rule_string =~ m/mod ([0-9]+)/g ) { $m{$1} = "( (\$_[0] % $1) + (\$_[0]-int(\$_[0])) )"; } my $perl_code = "sub { if ("; for my $or ( split /\s+or\s+/i, $plural_rule_string ) { my $and_exp; for my $and ( split /\s+and\s+/i, $or ) { my $copy = $and; my $n = '$_[0]'; $copy =~ s/ ?n is not / $n \!\= /g; $copy =~ s/ ?n is / $n \=\= /g; $copy =~ s/ ?n mod ([0-9]+) is not / $m{$1} \!\= /g; $copy =~ s/ ?n mod ([0-9]+) is / $m{$1} \=\= /g; $copy =~ s/ ?n not in ([0-9]+)\s*\.\.\s*([0-9]+) ?/ int\($n\) \!\= $n \|\| $n < $1 \|\| $n \> $2 /g; $copy =~ s/ ?n mod ([0-9]+) not in ([0-9]+)\s*\.\.\s*([0-9]+) ?/ int\($n\) \!\= $n \|\| $m{$1} < $2 \|\| $m{$1} \> $3 /g; $copy =~ s/ ?n not within ([0-9]+)\s*\.\.\s*([0-9]+) ?/ \($n < $1 \|\| $n > $2\) /g; $copy =~ s/ ?n mod ([0-9]+) not within ([0-9]+)\s*\.\.\s*([0-9]+) ?/ \($m{$1} < $2 \|\| $m{$1} > $3\) /g; $copy =~ s/ ?n in ([0-9]+)\s*\.\.\s*([0-9]+) ?/ int\($n\) \=\= $n \&\& $n \>\= $1 \&\& $n \<\= $2 /g; $copy =~ s/ ?n mod ([0-9]+) in ([0-9]+)\s*\.\.\s*([0-9]+) ?/ int\($n\) \=\= $n \&\& $m{$1} \>\= $2 \&\& $m{$1} \<\= $3 /g; $copy =~ s/ ?n within ([0-9]+)\s*\.\.\s*([0-9]+) ?/ $n \>\= $1 \&\& $n \<\= $2 /g; $copy =~ s/ ?n mod ([0-9]+) within ([0-9]+)\s*\.\.\s*([0-9]+) ?/ $m{$1} \>\= $2 \&\& $m{$1} \<\= $3 /g; if ( $copy eq $and ) { require Carp; Carp::carp("Unknown plural rule syntax"); return; } else { $and_exp .= "($copy) && "; } } $and_exp =~ s/\s+\&\&\s*$//; if ($and_exp) { $perl_code .= " ($and_exp) || "; } } $perl_code =~ s/\s+\|\|\s*$//; $perl_code .= ") { return '$return'; } return;}"; return $perl_code; } sub plural_rule_string_to_javascript_code { my ( $plural_rule_string, $return ) = @_; my $perl = plural_rule_string_to_code( $plural_rule_string, $return ); $perl =~ s/sub \{ /function (n) \{/; $perl =~ s/\$_\[0\]/n/g; $perl =~ s/ \(n \% ([0-9]+)\) \+ \(n-int\(n\)\) /n % $1/g; $perl =~ s/int\(/parseInt\(/g; return $perl; } 1; } # --- END Cpanel/CPAN/Locales/Compile.pm { # --- BEGIN Cpanel/CPAN/Locales.pm package Cpanel::CPAN::Locales; use strict; # use Cpanel::Locale::Utils::Normalize (); $Cpanel::CPAN::Locales::VERSION = 0.30_1; # change in POD $Cpanel::CPAN::Locales::cldr_version = '2.0'; # change in POD my $FORCE_REGEX_LAZY = ''; *normalize_tag = *Cpanel::Locale::Utils::Normalize::normalize_tag; my %singleton_stash; sub get_cldr_version { return $Cpanel::CPAN::Locales::cldr_version; } sub new { my ( $class, $tag ) = @_; $tag = normalize_tag($tag) || 'en'; if ( !exists $singleton_stash{$tag} ) { my $locale = { 'locale' => $tag, }; if ( my $soft = tag_is_soft_locale($tag) ) { $locale->{'soft_locale_fallback'} = $soft; $tag = $soft; } my $inc_class = ref($class) ? ref($class) : $class; $inc_class =~ s{\:\:|\'}{/}g; # per Module::Want::get_inc_key() if ( !exists $INC{"$inc_class/DB/Language/$tag.pm"} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::Language::$tag" || return; # Module::Want::have_mod("$class\::DB::Language::$tag"); } my ( $language, $territory ) = split_tag( $locale->{'locale'} ); $locale->{'language'} = $language; { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $locale->{'language_data'} = { 'VERSION' => \${"$class\::DB::Language::$tag\::VERSION"}, 'cldr_version' => \${"$class\::DB::Language::$tag\::cldr_version"}, 'misc_info' => \%{"$class\::DB::Language::$tag\::misc_info"}, }; } $locale->{'territory'} = $territory; $locale->{'misc'}{'list_quote_mode'} = 'none'; $singleton_stash{$tag} = bless $locale, $class; } return $singleton_stash{$tag}; } sub _load_territory_data { my ($self) = @_; my $tag = $self->{'locale'}; my $class = scalar ref $self; my $inc_class = $class; $inc_class =~ s{\:\:|\'}{/}g; # per Module::Want::get_inc_key() if ( !exists $INC{"$inc_class/DB/Territory/$tag.pm"} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::Territory::$tag" || return; # Module::Want::have_mod("$class\::DB::Language::$tag"); } { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $self->{'territory_data'} = { 'VERSION' => \${"$class\::DB::Territory::$tag\::VERSION"}, 'cldr_version' => \${"$class\::DB::Territory::$tag\::cldr_version"}, 'code_to_name' => \%{"$class\::DB::Territory::$tag\::code_to_name"}, }; } return 1; } sub _load_language_data_code_to_name { my ($self) = @_; my $tag = $self->{'locale'}; my $class = scalar ref $self; my $inc_class = $class; $inc_class =~ s{\:\:|\'}{/}g; # per Module::Want::get_inc_key() if ( !exists $INC{"$inc_class/DB/Language/code_to_name/$tag.pm"} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::Language::code_to_name::$tag" || return; # Module::Want::have_mod("$class\::DB::Language::$tag"); } { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $self->{'language_data'}{'code_to_name'} = \%{"$class\::DB::Language::$tag\::code_to_name"}; } return 1; } sub get_soft_locale_fallback { return $_[0]->{'soft_locale_fallback'} if $_[0]->{'soft_locale_fallback'}; return; } sub get_locale { shift->{'locale'} } sub get_territory { shift->{'territory'} } sub get_language { shift->{'language'} } sub get_native_language_from_code { my ( $self, $code, $always_return ) = @_; my $class = ref($self) ? ref($self) : $self; if ( !exists $self->{'native_data'} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::Native;" || return; # Module::Want::have_mod("$class\::DB::Native"); { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $self->{'native_data'} = { 'VERSION' => \${"$class\::DB::Native::VERSION"}, 'cldr_version' => \${"$class\::DB::Native::cldr_version"}, 'code_to_name' => \%{"$class\::DB::Native::code_to_name"}, }; } } $code ||= $self->{'locale'}; $code = normalize_tag($code); return if !defined $code; $always_return ||= 1 if $code eq $self->get_locale() && $self->get_soft_locale_fallback(); # force $always_return under soft locale objects $always_return ||= 0; if ( exists $self->{'native_data'}{'code_to_name'}{$code} ) { return $self->{'native_data'}{'code_to_name'}{$code}; } elsif ($always_return) { my ( $l, $t ) = split_tag($code); my $ln = $self->{'native_data'}{'code_to_name'}{$l}; $self->_load_territory_data() if !$self->{'territory_data'}; my $tn = defined $t ? $self->{'territory_data'}{'code_to_name'}{$t} : ''; return $code if !$ln && !$tn; if ( defined $t ) { my $tmp = Cpanel::CPAN::Locales->new($l); # if we even get to this point: this is a singleton so it is cheap if ($tmp) { if ( $tmp->get_territory_from_code($t) ) { $tn = $tmp->get_territory_from_code($t); } } } $ln ||= $l; $tn ||= $t; my $string = get_locale_display_pattern_from_code_fast($code) || $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'locale'} || '{0} ({1})'; substr( $string, index( $string, '{0}' ), 3, $ln ) while index( $string, '{0}' ) > -1; substr( $string, index( $string, '{1}' ), 3, $tn ) while index( $string, '{1}' ) > -1; return $string; } return; } sub numf { require Cpanel::CPAN::Locales::Legacy if !$INC{'Cpanel/CPAN/Locales/Legacy.pm'}; *numf = *Cpanel::CPAN::Locales::Legacy::numf; goto \&Cpanel::CPAN::Locales::Legacy::numf; } my $get_locale_display_pattern_from_code_fast = 0; sub get_locale_display_pattern_from_code_fast { if ( !$get_locale_display_pattern_from_code_fast ) { $get_locale_display_pattern_from_code_fast++; require Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny; } if ( @_ == 1 && ref( $_[0] ) ) { return Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny::get_locale_display_pattern( $_[0]->get_locale() ); } return Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny::get_locale_display_pattern( $_[-1] ); # last arg so it works as function or class method or object method } sub get_locale_display_pattern_from_code { my ( $self, $code, $always_return ) = @_; my $class = ref($self) ? ref($self) : $self; if ( !exists $self->{'locale_display_pattern_data'} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::LocaleDisplayPattern;" || return; # Module::Want::have_mod("$class\::DB::LocaleDisplayPattern"); { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $self->{'locale_display_pattern_data'} = { 'VERSION' => \${"$class\::DB::LocaleDisplayPattern::VERSION"}, 'cldr_version' => \${"$class\::DB::LocaleDisplayPattern::cldr_version"}, 'code_to_pattern' => \%{"$class\::DB::LocaleDisplayPattern::code_to_pattern"}, }; } } $code ||= $self->{'locale'}; $code = normalize_tag($code); return if !defined $code; $always_return ||= 1 if $code eq $self->get_locale() && $self->get_soft_locale_fallback(); # force $always_return under soft locale objects $always_return ||= 0; if ( exists $self->{'locale_display_pattern_data'}{'code_to_pattern'}{$code} ) { return $self->{'locale_display_pattern_data'}{'code_to_pattern'}{$code}; } elsif ($always_return) { my ( $l, $t ) = split_tag($code); if ( exists $self->{'locale_display_pattern_data'}{'code_to_pattern'}{$l} ) { return $self->{'locale_display_pattern_data'}{'code_to_pattern'}{$l}; } return '{0} ({1})'; } return; } my $get_character_orientation_from_code_fast = 0; sub get_character_orientation_from_code_fast { if ( !$get_character_orientation_from_code_fast ) { $get_character_orientation_from_code_fast++; require Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny; } if ( @_ == 1 && ref( $_[0] ) ) { return Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny::get_orientation( $_[0]->get_locale() ); } return Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny::get_orientation( $_[-1] ); # last arg so it works as function or class method or object method } sub get_character_orientation_from_code { my ( $self, $code, $always_return ) = @_; my $class = ref($self) ? ref($self) : $self; if ( !exists $self->{'character_orientation_data'} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require $class\::DB::CharacterOrientation;" || return; # Module::Want::have_mod("$class\::DB::CharacterOrientation"); { BEGIN { $^H = 0; }; # cheap no strict to allow for ref copy $self->{'character_orientation_data'} = { 'VERSION' => \${"$class\::DB::CharacterOrientation::VERSION"}, 'cldr_version' => \${"$class\::DB::CharacterOrientation::cldr_version"}, 'code_to_name' => \%{"$class\::DB::CharacterOrientation::code_to_name"}, }; } } $code ||= $self->{'locale'}; $code = normalize_tag($code); return if !defined $code; $always_return ||= 1 if $code eq $self->get_locale() && $self->get_soft_locale_fallback(); # force $always_return under soft locale objects $always_return ||= 0; if ( exists $self->{'character_orientation_data'}{'code_to_name'}{$code} ) { return $self->{'character_orientation_data'}{'code_to_name'}{$code}; } elsif ($always_return) { my ( $l, $t ) = split_tag($code); if ( exists $self->{'character_orientation_data'}{'code_to_name'}{$l} ) { return $self->{'character_orientation_data'}{'code_to_name'}{$l}; } return 'left-to-right'; } return; } sub get_plural_form_categories { return @{ $_[0]->{'language_data'}{'misc_info'}{'plural_forms'}{'category_list'} }; } sub supports_special_zeroth { return 1 if $_[0]->get_plural_form(0) eq 'other'; return; } sub plural_category_count { return scalar( $_[0]->get_plural_form_categories() ); } sub get_plural_form { my ( $self, $n, @category_values ) = @_; my $category; my $has_extra_for_zero = 0; my $abs_n = abs($n); # negatives keep same category as positive if ( !$self->{'language_data'}{'misc_info'}{'plural_forms'}{'category_rules_function'} ) { $self->{'language_data'}{'misc_info'}{'plural_forms'}{'category_rules_function'} = Cpanel::CPAN::Locales::plural_rule_hashref_to_code( $self->{'language_data'}{'misc_info'}{'plural_forms'} ); if ( !defined $self->{'language_data'}{'misc_info'}{'plural_forms'}{'category_rules_function'} ) { require Carp; Carp::carp("Could not determine plural logic."); } } $category = $self->{'language_data'}{'misc_info'}{'plural_forms'}{'category_rules_function'}->($abs_n); my @categories = $self->get_plural_form_categories(); if ( !@category_values ) { @category_values = @categories; } else { my $cat_len = @categories; my $val_len = @category_values; if ( $val_len == ( $cat_len + 1 ) ) { $has_extra_for_zero++; } elsif ( $cat_len != $val_len && $self->{'verbose'} ) { require Carp; Carp::carp("The number of given values ($val_len) does not match the number of categories ($cat_len)."); } } if ( !defined $category ) { my $cat_idx = $has_extra_for_zero && $abs_n != 0 ? -2 : -1; return wantarray ? ( $category_values[$cat_idx], $has_extra_for_zero && $abs_n == 0 ? 1 : 0 ) : $category_values[$cat_idx]; } else { GET_POSITION: my $cat_pos_in_list; my $index = -1; CATEGORY: for my $cat (@categories) { $index++; if ( $cat eq $category ) { $cat_pos_in_list = $index; last CATEGORY; } } if ( !defined $cat_pos_in_list && $category ne 'other' ) { require Carp; Carp::carp("The category ($category) is not used by this locale."); $category = 'other'; goto GET_POSITION; } elsif ( !defined $cat_pos_in_list ) { my $cat_idx = $has_extra_for_zero && $abs_n != 0 ? -2 : -1; return wantarray ? ( $category_values[$cat_idx], $has_extra_for_zero && $abs_n == 0 ? 1 : 0 ) : $category_values[$cat_idx]; } else { if ( $has_extra_for_zero && $category eq 'other' ) { # and 'other' is at the end of the list? nah... && $cat_pos_in_list + 1 == $#category_values my $cat_idx = $has_extra_for_zero && $abs_n == 0 ? -1 : $cat_pos_in_list; return wantarray ? ( $category_values[$cat_idx], $has_extra_for_zero && $abs_n == 0 ? 1 : 0 ) : $category_values[$cat_idx]; } else { return wantarray ? ( $category_values[$cat_pos_in_list], 0 ) : $category_values[$cat_pos_in_list]; } } } } sub _quote_get_list_items { my ( $self, $items_ar ) = @_; my $cnt = 0; if ( exists $self->{'misc'}{'list_quote_mode'} && $self->{'misc'}{'list_quote_mode'} ne 'none' ) { if ( $self->{'misc'}{'list_quote_mode'} eq 'all' ) { @{$items_ar} = ('') if @{$items_ar} == 0; for my $i ( 0 .. scalar( @{$items_ar} ) - 1 ) { $items_ar->[$i] = '' if !defined $items_ar->[$i]; $items_ar->[$i] = $self->quote( $items_ar->[$i] ); $cnt++; } } elsif ( $self->{'misc'}{'list_quote_mode'} eq 'some' ) { @{$items_ar} = ('') if @{$items_ar} == 0; for my $i ( 0 .. scalar( @{$items_ar} ) - 1 ) { $items_ar->[$i] = '' if !defined $items_ar->[$i]; if ( $items_ar->[$i] eq '' || $items_ar->[$i] eq ' ' || $items_ar->[$i] eq "\xc2\xa0" ) { $items_ar->[$i] = $self->quote( $items_ar->[$i] ); $cnt++; } } } else { require Carp; Carp::carp('$self->{misc}{list_quote_mode} is set to an unknown value'); } } return $cnt; } sub get_list_and { my $self = shift; return $self->_get_list_joined( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'list'}, @_, ); } sub get_list_or { my $self = shift; return $self->_get_list_joined( $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'list_or'}, @_, ); } sub _get_list_joined { my ( $self, $templates_hr, @items ) = @_; $self->_quote_get_list_items( \@items ); return if !@items; return $items[0] if @items == 1; my $ix; # used to cache index results in the following oneliner if ( @items == 2 ) { my $two = $templates_hr->{'2'}; substr( $two, $ix, 3, $items[0] ) while ( $ix = index( $two, '{0}' ) ) > -1; substr( $two, $ix, 3, $items[1] ) while ( $ix = index( $two, '{1}' ) ) > -1; return $two; } else { for (@items) { next if !defined $_; substr( $_, $ix, 3, '__{__0__}__' ) while ( $ix = index( $_, '{0}' ) ) > -1; substr( $_, $ix, 3, '__{__1__}__' ) while ( $ix = index( $_, '{1}' ) ) > -1; } my $aggregate = $templates_hr->{'start'}; substr( $aggregate, $ix, 3, $items[0] ) while ( $ix = index( $aggregate, '{0}' ) ) > -1; substr( $aggregate, $ix, 3, $items[1] ) while ( $ix = index( $aggregate, '{1}' ) ) > -1; for my $i ( 2 .. $#items ) { next if $i == $#items; my $middle = $templates_hr->{'middle'}; substr( $middle, $ix, 3, $aggregate ) while ( $ix = index( $middle, '{0}' ) ) > -1; my $item = defined $items[$i] ? $items[$i] : ''; substr( $middle, $ix, 3, $item ) while ( $ix = index( $middle, '{1}' ) ) > -1; $aggregate = $middle; } my $end = $templates_hr->{'end'}; substr( $end, $ix, 3, $aggregate ) while ( $ix = index( $end, '{0}' ) ) > -1; substr( $end, $ix, 3, $items[-1] ) while ( $ix = index( $end, '{1}' ) ) > -1; substr( $end, $ix, 11, '{0}' ) while ( $ix = index( $end, '__{__0__}__' ) ) > -1; substr( $end, $ix, 11, '{1}' ) while ( $ix = index( $end, '__{__1__}__' ) ) > -1; return $end; } } sub quote { my ( $self, $value ) = @_; $value = '' if !defined $value; return $self->{'language_data'}{'misc_info'}{'delimiters'}{'quotation_start'} . $value . $self->{'language_data'}{'misc_info'}{'delimiters'}{'quotation_end'}; } sub quote_alt { my ( $self, $value ) = @_; $value = '' if !defined $value; return $self->{'language_data'}{'misc_info'}{'delimiters'}{'alternate_quotation_start'} . $value . $self->{'language_data'}{'misc_info'}{'delimiters'}{'alternate_quotation_end'}; } sub get_formatted_ellipsis_initial { my ( $self, $str ) = @_; my $pattern = $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'ellipsis'}{'initial'} || '…{0}'; substr( $pattern, index( $pattern, '{0}' ), 3, $str ) while index( $pattern, '{0}' ) > -1; return $pattern; } sub get_formatted_ellipsis_medial { my ($self) = @_; # my ($self, $first, $second) = @_; my $pattern = $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'ellipsis'}{'medial'} || '{0}…{1}'; substr( $pattern, index( $pattern, '{0}' ), 3, $_[1] ) while index( $pattern, '{0}' ) > -1; substr( $pattern, index( $pattern, '{1}' ), 3, $_[2] ) while index( $pattern, '{1}' ) > -1; return $pattern; } sub get_formatted_ellipsis_final { my ( $self, $str ) = @_; my $pattern = $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'ellipsis'}{'final'} || '{0}…'; substr( $pattern, index( $pattern, '{0}' ), 3, $str ) while index( $pattern, '{0}' ) > -1; return $pattern; } sub get_formatted_decimal { my ( $self, $n, $max_decimal_places, $_my_pattern ) = @_; # $_my_pattern not documented on purpose, it is only intended for internal use, and may dropepd/changed at any time return if !defined $n; my $is_negative = $n < 0 ? 1 : 0; my $max_len = defined $max_decimal_places ? abs( int($max_decimal_places) ) : 6; # %f default is 6 $max_len = 14 if $max_len > 14; if ( $n > 10_000_000_000 || $n < -10_000_000_000 ) { return $n if $n =~ tr/Ee//; # poor man's is exponential check. if ( $n =~ m/\.([0-9]{$max_len})([0-9])?/ ) { my $trim = $1; # (defined $2 && $2 > 4) ? $1 + 1 : $1; if ( defined $2 && $2 > 4 ) { if ( ( $trim + 1 ) !~ tr/Ee// ) { # poor man's is exponential check. $trim++; } } $n =~ s/$FORCE_REGEX_LAZY\.[0-9]+/\.$trim/o; } } else { return $n if length $n < 3 && $n !~ tr{0-9}{}c; $n = sprintf( '%.' . $max_len . 'f', $n ); return $n if $n =~ tr/Ee//; # poor man's is exponential check. } $n =~ s{$FORCE_REGEX_LAZY([^0-9]+[0-9]*?[1-9])0+$}{$1}o; $n =~ s{$FORCE_REGEX_LAZY[^0-9]+0+$}{}o; if ( $n =~ tr{.0-9}{}c ) { # Only strip signs if the string has non-numeric and '.' characters such as '+' or '-' substr( $n, 0, 1, '' ) while substr( $n, 1 ) =~ tr{0-9}{}c; } my $cldr_formats = $self->{'language_data'}{'misc_info'}{'cldr_formats'}; my $format = $_my_pattern || $cldr_formats->{'decimal'}; # from http://unicode.org/repos/cldr-tmp/trunk/diff/by_type/number.pattern.html my ( $zero_positive_pat, $negative_pat, $err ) = split( /$FORCE_REGEX_LAZY(? 3 ) { while ( $result =~ s/$FORCE_REGEX_LAZY^([-+]?\d+)(\d{3})/$1,$2/os ) { 1 } # right from perlfaq5 } } else { my ( $integer, $decimals ) = split( /\./, $n, 2 ); my ( $i_pat, $d_pat ) = split( /$FORCE_REGEX_LAZY(?{_decimal_format_decimal} ne '.' && index( $result, '.' ) > -1 && $result =~ s/$FORCE_REGEX_LAZY(?{_decimal_format_group} ne ',' && index( $result, ',' ) > -1 ) { $result =~ s/$FORCE_REGEX_LAZY(?{_decimal_format_group}/og; } if ($used_place_holder) { my $ix; substr( $result, $ix, 29, $cldr_formats->{_decimal_format_decimal} ) while ( $ix = index( $result, '_LOCALES-DECIMAL-PLACEHOLDER_' ) ) > -1; } if ( $is_negative && !$negative_pat ) { $result = "-$result"; } return $result; } sub get_territory_codes { $_[0]->_load_territory_data() if !$_[0]->{'territory_data'}; return keys %{ shift->{'territory_data'}{'code_to_name'} }; } sub get_territory_names { $_[0]->_load_territory_data() if !$_[0]->{'territory_data'}; return values %{ shift->{'territory_data'}{'code_to_name'} }; } sub get_territory_lookup { $_[0]->_load_territory_data() if !$_[0]->{'territory_data'}; return %{ shift->{'territory_data'}{'code_to_name'} }; } sub get_territory_from_code { my ( $self, $code, $always_return ) = @_; $code ||= $self->{'territory'}; $code = normalize_tag($code); return if !defined $code; $self->_load_territory_data() if !$self->{'territory_data'}; if ( exists $self->{'territory_data'}{'code_to_name'}{$code} ) { return $self->{'territory_data'}{'code_to_name'}{$code}; } elsif ( !defined $self->{'territory'} || $code ne $self->{'territory'} ) { my ( $l, $t ) = split_tag($code); if ( $t && exists $self->{'territory_data'}{'code_to_name'}{$t} ) { return $self->{'territory_data'}{'code_to_name'}{$t}; } } return $code if $always_return; return; } sub get_code_from_territory { my ( $self, $name ) = @_; return if !$name; my $key = normalize_for_key_lookup($name); $self->_load_territory_data() if !$self->{'territory_data'}; if ( !$self->{'territory_data'}{'nam'} ) { $self->{'territory_data'}{'name_to_code'} = { map { normalize_for_key_lookup( $self->{'territory_data'}{'code_to_name'}->{$_} ) => $_ } keys %{ $self->{'territory_data'}{'code_to_name'} } }; } if ( exists $self->{'territory_data'}{'name_to_code'}{$key} ) { return $self->{'territory_data'}{'name_to_code'}{$key}; } return; } { no warnings 'once'; *code2territory = *get_territory_from_code; *territory2code = *get_code_from_territory; } sub get_language_codes { $_[0]->_load_language_data_code_to_name() if !$_[0]->{'language_data'}{'code_to_name'}; return keys %{ $_[0]->{'language_data'}{'code_to_name'} }; } sub get_language_names { $_[0]->_load_language_data_code_to_name() if !$_[0]->{'language_data'}{'code_to_name'}; return values %{ $_[0]->{'language_data'}{'code_to_name'} }; } sub get_language_lookup { $_[0]->_load_language_data_code_to_name() if !$_[0]->{'language_data'}{'code_to_name'}; return %{ $_[0]->{'language_data'}{'code_to_name'} }; } sub get_language_from_code { my ( $self, $code, $always_return ) = @_; $code ||= $self->{'locale'}; $code = normalize_tag($code); return if !defined $code; $always_return ||= 1 if $code eq $self->get_locale() && $self->get_soft_locale_fallback(); # force $always_return under soft locale objects $always_return ||= 0; $self->_load_language_data_code_to_name() if !$_[0]->{'language_data'}{'code_to_name'}; if ( exists $self->{'language_data'}{'code_to_name'}{$code} ) { return $self->{'language_data'}{'code_to_name'}{$code}; } elsif ($always_return) { $self->_load_territory_data() if !$self->{'territory_data'}; my ( $l, $t ) = split_tag($code); my $ln = $self->{'language_data'}{'code_to_name'}{$l}; my $tn = defined $t ? $self->{'territory_data'}{'code_to_name'}{$t} : ''; return $code if !$ln && !$tn; $ln ||= $l; $tn ||= $t; my $string = $self->{'language_data'}{'misc_info'}{'cldr_formats'}{'locale'} || '{0} ({1})'; substr( $string, index( $string, '{0}' ), 3, $ln ) while index( $string, '{0}' ) > -1; substr( $string, index( $string, '{1}' ), 3, $tn ) while index( $string, '{1}' ) > -1; return $string; } return; } sub get_code_from_language { my ( $self, $name ) = @_; return if !$name; my $key = normalize_for_key_lookup($name); $self->_load_language_data_code_to_name() if !$_[0]->{'language_data'}{'code_to_name'}; if ( !$self->{'language_data'}{'name_to_code'} ) { $self->{'language_data'}{'name_to_code'} = { map { normalize_for_key_lookup( $self->{'language_data'}{'code_to_name'}->{$_} ) => $_ } keys %{ $self->{'language_data'}{'code_to_name'} } }; } if ( exists $self->{'language_data'}{'name_to_code'}{$key} ) { return $self->{'language_data'}{'name_to_code'}{$key}; } return; } { no warnings 'once'; *code2language = *get_language_from_code; *language2code = *get_code_from_language; } sub tag_is_soft_locale { my ($tag) = @_; my ( $l, $t ) = split_tag($tag); return if !defined $l; # invalid tag is not soft return if !$t; # no territory part means it is not soft return if tag_is_loadable($tag); # if it can be loaded directly then it is not soft return if !territory_code_is_known($t); # if the territory part is not known then it is not soft return if !tag_is_loadable($l); # if the language part is not known then it is not soft return $l; # it is soft, so return the value suitable for 'soft_locale_fallback' } sub tag_is_loadable { my ( $tag, $as_territory ) = @_; # not documenting internal $as_territory, just use territory_code_is_known() directly if ( !exists $INC{"Cpanel/CPAN/Locales/DB/Loadable.pm"} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require Cpanel::CPAN::Locales::DB::Loadable" || return; # Module::Want::have_mod("Cpanel::CPAN::Locales::DB::Loadable") || return; } if ($as_territory) { no warnings 'once'; return 1 if exists $Cpanel::CPAN::Locales::DB::Loadable::territory{$tag}; } else { return 1 if exists $Cpanel::CPAN::Locales::DB::Loadable::code{$tag}; } return; } sub get_loadable_language_codes { if ( !exists $INC{"Cpanel/CPAN/Locales/DB/Loadable.pm"} ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) eval "require Cpanel::CPAN::Locales::DB::Loadable" || return; # Module::Want::have_mod("Cpanel::CPAN::Locales::DB::Loadable") || return; } return keys %Cpanel::CPAN::Locales::DB::Loadable::code; } sub territory_code_is_known { return tag_is_loadable( $_[0], 1 ); } sub split_tag { return split( /_/, normalize_tag( $_[0] ), 2 ); # we only do language[_territory] } sub get_i_tag_for_string { my $norm = normalize_tag( $_[0] ); if ( substr( $norm, 0, 2 ) eq 'i_' ) { return $norm; } else { return 'i_' . $norm; } } my %non_locales = ( 'und' => 1, 'zxx' => 1, 'mul' => 1, 'mis' => 1, 'art' => 1, ); sub non_locale_list { return ( sort keys %non_locales ); } sub is_non_locale { my $tag = normalize_tag( $_[0] ) || return; return 1 if exists $non_locales{$tag}; return; } sub typical_en_alias_list { return ( 'en_us', 'i_default' ); } sub is_typical_en_alias { my $tag = normalize_tag( $_[0] ) || return; return 1 if $tag eq 'en_us' || $tag eq 'i_default'; return; } sub normalize_tag_for_datetime_locale { my ( $pre, $pst ) = split_tag( $_[0] ); # we only do language[_territory] return if !defined $pre; if ($pst) { return $pre . '_' . uc($pst); } else { return $pre; } } sub normalize_tag_for_ietf { my ( $pre, $pst ) = split_tag( $_[0] ); # we only do language[_territory] return if !defined $pre; if ($pst) { return $pre . '-' . uc($pst); } else { return $pre; } } sub normalize_for_key_lookup { my $key = $_[0]; return '' if !defined $key; $key =~ tr/A-Z/a-z/; # lowercase $key =~ s{\s+}{}g if $key =~ tr{ \t\r\n\f}{}; $key =~ tr{\'\"\-\(\)\[\]\_}{}d; return $key; } sub plural_rule_string_to_javascript_code { require Cpanel::CPAN::Locales::Compile; *plural_rule_string_to_javascript_code = \&Cpanel::CPAN::Locales::Compile::plural_rule_string_to_javascript_code; goto \&Cpanel::CPAN::Locales::Compile::plural_rule_string_to_javascript_code; } sub plural_rule_string_to_code { require Cpanel::CPAN::Locales::Compile; *plural_rule_string_to_code = \&Cpanel::CPAN::Locales::Compile::plural_rule_string_to_code; goto \&Cpanel::CPAN::Locales::Compile::plural_rule_string_to_code; } sub plural_rule_hashref_to_code { my ($hr) = @_; if ( ref( $hr->{'category_rules'} ) ne 'HASH' ) { $hr->{'category_rules_compiled'} = { 'one' => q{sub { return 'one' if ( ( $n == 1 ) ); return;};}, }; return sub { my ($n) = @_; return 'one' if $n == 1; return; }; } else { for my $cat ( get_cldr_plural_category_list(1) ) { next if !exists $hr->{'category_rules'}{$cat}; next if exists $hr->{'category_rules_compiled'}{$cat}; $hr->{'category_rules_compiled'}{$cat} = plural_rule_string_to_code( $hr->{'category_rules'}{$cat}, $cat ); } return sub { my ($n) = @_; my $match; PCAT: for my $cat ( get_cldr_plural_category_list(1) ) { # use function instead of keys to preserve processing order next if !exists $hr->{'category_rules_compiled'}{$cat}; if ( ref( $hr->{'category_rules_compiled'}{$cat} ) ne 'CODE' ) { local $SIG{'__DIE__'}; # cpanel specific: ensure a benign eval does not trigger cpsrvd's DIE handler (may be made moot by internal case 50857) $hr->{'category_rules_compiled'}{$cat} = eval "$hr->{'category_rules_compiled'}{$cat}"; ## no critic (ProhibitStringyEval) # As of 0.22 this will be skipped for modules included w/ the main dist } if ( $hr->{'category_rules_compiled'}{$cat}->($n) ) { $match = $cat; last PCAT; } } return $match if $match; return; }; } } sub get_cldr_plural_category_list { return qw(zero one two few many other) if $_[0]; # check order return qw(one two few many other zero); # quant() arg order } sub get_fallback_list { my ( $self, $special_lookup ) = @_; my ( $super, $ter ) = split_tag( $self->{'locale'} ); return ( $self->{'locale'}, ( $super ne $self->{'locale'} && $super ne 'i' ? $super : () ), ( @{ $self->{'language_data'}{'misc_info'}{'fallback'} } ), ( defined $special_lookup && ref($special_lookup) eq 'CODE' ? ( map { my $n = Cpanel::Locale::Utils::Normalize::normalize_tag($_); $n ? ($n) : () } $special_lookup->( $self->{'locale'} ) ) : () ), 'en' ); } sub get_cldr_number_symbol_decimal { return $_[0]->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_decimal'} || '.'; } sub get_cldr_number_symbol_group { return $_[0]->{'language_data'}{'misc_info'}{'cldr_formats'}{'_decimal_format_group'} || ','; } 1; } # --- END Cpanel/CPAN/Locales.pm { # --- BEGIN Cpanel/CPAN/Locale/Maketext/Utils.pm package Cpanel::CPAN::Locale::Maketext::Utils; $Cpanel::CPAN::Locale::Maketext::Utils::VERSION = 0.33_95; # use Cpanel::CPAN::Locale::Maketext 1.13_89 (); # our 1.13_89 contains some optimizations and support for external_lex_cache that made its way to CPAN by v1.22 @Cpanel::CPAN::Locale::Maketext::Utils::ISA = qw(Cpanel::CPAN::Locale::Maketext); use constant LOCALE_FALLBACK_CACHE_DIR => '/usr/local/cpanel/etc/locale/fallback'; my $FORCE_REGEX_LAZY = ''; my %singleton_stash = (); sub _compile { my ( $lh, $string ) = @_; substr( $string, index( $string, '_TILDE_' ), 7, '~~' ) while index( $string, '_TILDE_' ) > -1; # this helps make parsing easier (via code or visually) my $compiled = $lh->SUPER::_compile($string); return $compiled if ref($compiled) ne 'CODE'; return sub { return $compiled->( $_[0], @_[ 1 .. $#_ ] ) if !grep { defined && index( $_, '_' ) > -1 } @_[ 1 .. $#_ ]; my ( $lh, @ref_args ) = @_; my $built = $compiled->( $lh, map { if ( defined && index( $_, '_' ) > -1 ) { s/$FORCE_REGEX_LAZY\_(\-?[0-9]+|\*)/-!-$1-!-/og; } $_ # Change embedded-arg-looking-string to a } @ref_args ); $built =~ s/$FORCE_REGEX_LAZY-!-(\-?[0-9]+|\*)-!-/_$1/og; # Change placeholders back to their original return $built; }; } sub get_handle { my ( $class, @langtags ) = @_; my $args_sig = join( ',', @langtags ) || 'no_args'; if ( exists $singleton_stash{$class}{$args_sig} ) { $singleton_stash{$class}{$args_sig}->{'_singleton_reused'}++; } else { $singleton_stash{$class}{$args_sig} = $class->SUPER::get_handle(@langtags); } return $singleton_stash{$class}{$args_sig}; } sub get_locales_obj { my ( $lh, $tag ) = @_; $tag ||= $lh->get_language_tag(); if ( !exists $lh->{'Locales.pm'}{$tag} ) { require Cpanel::CPAN::Locales; $lh->{'Locales.pm'}{$tag} = Cpanel::CPAN::Locales->new($tag) || ( $tag ne substr( $tag, 0, 2 ) ? Cpanel::CPAN::Locales->new( substr( $tag, 0, 2 ) ) : '' ) || ( $lh->{'fallback_locale'} ? ( Cpanel::CPAN::Locales->new( $lh->{'fallback_locale'} ) || ( $lh->{'fallback_locale'} ne substr( $lh->{'fallback_locale'}, 0, 2 ) ? Cpanel::CPAN::Locales->new( substr( $lh->{'fallback_locale'}, 0, 2 ) ) : '' ) ) : '' ) || Cpanel::CPAN::Locales->new('en'); } return $lh->{'Locales.pm'}{$tag}; } sub init { my ($lh) = @_; $lh->SUPER::init(); $lh->remove_key_from_lexicons('_AUTO'); no strict 'refs'; for my $ns ( $lh->get_base_class(), $lh->get_language_class() ) { if ( defined ${ $ns . '::Encoding' } ) { $lh->{'encoding'} = ${ $ns . '::Encoding' } if ${ $ns . '::Encoding' }; } } $lh->fail_with( sub { my ( $lh, $key, @args ) = @_; my $lookup; if ( exists $lh->{'_get_key_from_lookup'} ) { if ( ref $lh->{'_get_key_from_lookup'} eq 'CODE' ) { $lookup = $lh->{'_get_key_from_lookup'}->( $lh, $key, @args ); } } return $lookup if defined $lookup; if ( exists $lh->{'_log_phantom_key'} ) { if ( ref $lh->{'_log_phantom_key'} eq 'CODE' ) { $lh->{'_log_phantom_key'}->( $lh, $key, @args ); } } if ( $lh->{'use_external_lex_cache'} ) { local $lh->{'_external_lex_cache'}{'_AUTO'} = 1; if ( index( $key, '_' ) == 0 ) { return $lh->{'_external_lex_cache'}{$key} = $key; } return $lh->maketext( $key, @args ); } else { no strict 'refs'; local ${ $lh->get_base_class() . '::Lexicon' }{'_AUTO'} = 1; if ( index( $key, '_' ) == 0 ) { return ${ $lh->get_base_class() . '::Lexicon' }{$key} = $key; } return $lh->maketext( $key, @args ); } } ); } *makevar = \&Cpanel::CPAN::Locale::Maketext::maketext; sub makethis { my ( $lh, $phrase, @phrase_args ) = @_; $lh->{'cache'}{'makethis'}{$phrase} ||= $lh->_compile($phrase); my $type = ref( $lh->{'cache'}{'makethis'}{$phrase} ); if ( $type eq 'SCALAR' ) { return ${ $lh->{'cache'}{'makethis'}{$phrase} }; } elsif ( $type eq 'CODE' ) { return $lh->{'cache'}{'makethis'}{$phrase}->( $lh, @phrase_args ); } else { return $lh->{'cache'}{'makethis'}{$phrase}; } } sub makethis_base { my ($lh) = @_; $lh->{'cache'}{'makethis_base'} ||= $lh->get_base_class()->get_handle( $lh->{'fallback_locale'} || 'en' ); # this allows to have a separate cache of compiled phrases (? get_handle() explicit or base_locales() (i.e. en en_us i_default || L::M->fallback_languages) ?) return $lh->{'cache'}{'makethis_base'}->makethis( @_[ 1 .. $#_ ] ); } sub make_alias { my ( $lh, $pkgs, $is_base_class ) = @_; my $ns = $lh->get_language_class(); return if $ns =~ tr{:0-9A-Za-z_-}{}c; my $base = $is_base_class ? $ns : $lh->get_base_class(); no strict 'refs'; for my $pkg ( ref $pkgs ? @{$pkgs} : $pkgs ) { next if $pkg =~ tr{:0-9A-Za-z_-}{}c; *{ $base . '::' . $pkg . '::Encoding' } = *{ $ns . '::Encoding' }; *{ $base . '::' . $pkg . '::Lexicon' } = *{ $ns . '::Lexicon' }; @{ $base . '::' . $pkg . '::ISA' } = ($ns); } } sub remove_key_from_lexicons { my ( $lh, $key ) = @_; my $idx = 0; for my $lex_hr ( @{ $lh->_lex_refs() } ) { $lh->{'_removed_from_lexicons'}{$idx}{$key} = delete $lex_hr->{$key} if exists $lex_hr->{$key}; $idx++; } } my %grapheme_lookup = ( 'trademark' => "\xE2\x84\xA2", # 'TRADE MARK SIGN' (U+2122) 'registered' => "\xC2\xAE", # 'REGISTERED SIGN' (U+00AE) 'copyright' => "\xC2\xA9", # 'COPYRIGHT SIGN' (U+00A9) 'left_double_quote' => "\xE2\x80\x9C", # 'LEFT DOUBLE QUOTATION MARK' (U+201C) 'right_double_quote' => "\xE2\x80\x9D", # 'RIGHT DOUBLE QUOTATION MARK' (U+201D) 'ellipsis' => "\xE2\x80\xA6", # 'HORIZONTAL ELLIPSIS' (U+2026) 'left_single_quote' => "\xE2\x80\x98", # 'LEFT SINGLE QUOTATION MARK' (U+2018) 'right_single_quote' => "\xE2\x80\x99", # 'RIGHT SINGLE QUOTATION MARK' 'infinity' => "\xE2\x88\x9E", # 'INFINITY' (U+221E) ); sub get_grapheme_helper_hashref { return {%grapheme_lookup}; # copy } sub get_base_class { my $ns = $_[0]->get_language_class(); return $ns if $ns eq 'Cpanel::Locale'; return substr( $ns, 0, rindex( $ns, '::' ) ); } sub append_to_lexicons { my ( $lh, $appendage ) = @_; return if ref $appendage ne 'HASH'; no strict 'refs'; for my $lang ( keys %{$appendage} ) { my $ns = $lh->get_base_class() . ( $lang eq '_' ? '' : "::$lang" ) . '::Lexicon'; %{$ns} = ( %{$ns}, %{ $appendage->{$lang} } ); } } sub langtag_is_loadable { my ( $lh, $wants_tag ) = @_; $wants_tag = Cpanel::CPAN::Locale::Maketext::language_tag($wants_tag); my $tag_obj = eval $lh->get_base_class() . q{->get_handle( $wants_tag );}; my $has_tag = $tag_obj->language_tag(); return $wants_tag eq $has_tag ? $tag_obj : 0; } sub get_language_tag { return ( split '::', $_[0]->get_language_class() )[-1]; } sub print { local $Carp::CarpLevel = 1; print $_[0]->maketext( @_[ 1 .. $#_ ] ); } sub fetch { local $Carp::CarpLevel = 1; return $_[0]->maketext( @_[ 1 .. $#_ ] ); } sub say { local $Carp::CarpLevel = 1; my $text = $_[0]->maketext( @_[ 1 .. $#_ ] ); local $/ = !defined $/ || !$/ ? "\n" : $/; # otherwise assume they are not stupid print $text . $/ if $text; } sub get { local $Carp::CarpLevel = 1; my $text = $_[0]->maketext( @_[ 1 .. $#_ ] ); local $/ = !defined $/ || !$/ ? "\n" : $/; # otherwise assume they are not stupid return $text . $/ if $text; return; } sub get_language_tag_name { my ( $lh, $tag, $in_locale_tongue ) = @_; $tag ||= $lh->get_language_tag(); my $loc_obj = $lh->get_locales_obj( $in_locale_tongue ? () : ($tag) ); if ( $loc_obj->{'native_data'} && $tag eq $lh->get_language_tag() ) { return $loc_obj->get_native_language_from_code($tag); } return $loc_obj->get_language_from_code($tag); } sub get_html_dir_attr { my ( $lh, $raw_cldr, $is_tag ) = @_; if ($is_tag) { $raw_cldr = $lh->get_language_tag_character_orientation($raw_cldr); } else { $raw_cldr ||= $lh->get_language_tag_character_orientation(); } if ( $raw_cldr eq 'left-to-right' ) { return 'ltr'; } elsif ( $raw_cldr eq 'right-to-left' ) { return 'rtl'; } return; } sub get_locale_display_pattern { require Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny; return Cpanel::CPAN::Locales::DB::LocaleDisplayPattern::Tiny::get_locale_display_pattern( $_[1] || $_[0]->{'fallback_locale'} || $_[0]->get_language_tag() ); } sub get_language_tag_character_orientation { require Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny; return Cpanel::CPAN::Locales::DB::CharacterOrientation::Tiny::get_orientation( $_[1] || $_[0]->{'fallback_locale'} || $_[0]->get_language_tag() ); } *lextext = *text; sub text { if ( @_ != 2 ) { require Carp; Carp::croak('text() requires a singlef parameter'); } my ( $handle, $phrase ) = splice( @_, 0, 2 ); unless ( defined($handle) && defined($phrase) ) { require Carp; Carp::confess('No handle/phrase'); } if ( !$handle->{'use_external_lex_cache'} ) { require Carp; Carp::carp("text() requires you to have 'use_external_lex_cache' enabled."); return; } local $@; my $value; foreach my $h_r ( @{ $handle->_lex_refs } ) { # _lex_refs() caches itself if ( defined( $value = $h_r->{$phrase} ) ) { if ( ref $value ) { require Carp; Carp::carp("Previously compiled phrase ('use_external_lex_cache' enabled after phrase was compiled?)"); } return $value eq '' ? $phrase : $value; } elsif ( index( $phrase, '_' ) != 0 and $h_r->{'_AUTO'} ) { return $phrase; } } return ( !defined $value || $value eq '' ) ? $phrase : $value; } our $_NATIVE_ONLY = 0; sub lang_names_hashref_native_only { local $_NATIVE_ONLY = 1; return lang_names_hashref(@_); } sub lang_names_hashref { my ( $lh, @langcodes ) = @_; if ( !@langcodes ) { # they havn't specified any langcodes... require File::Spec; # only needed here, so we don't use() it my @search; my $path = $lh->get_base_class(); substr( $path, index( $path, '::' ), 2, '/' ) while index( $path, '::' ) > -1; if ( ref $lh->{'_lang_pm_search_paths'} eq 'ARRAY' ) { @search = @{ $lh->{'_lang_pm_search_paths'} }; } @search = @INC if !@search; # they havn't told us where they are specifically DIR: for my $dir (@search) { my $lookin = File::Spec->catdir( $dir, $path ); next DIR if !-d $lookin; if ( opendir my $dh, $lookin ) { PM: for my $pm ( grep { /^\w+\.pm$/ } grep !/^\.+$/, readdir($dh) ) { substr( $pm, -3, 3, '' ); # checked above - if substr( $pm, -3 ) eq '.pm'; next PM if !$pm; next PM if $pm eq 'Utils'; next PM if $pm eq 'Context'; next PM if $pm eq 'Lazy'; push @langcodes, $pm; } closedir $dh; } } } require Cpanel::CPAN::Locales; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); my $langname = {}; my $native = wantarray && $Cpanel::CPAN::Locales::VERSION > 0.06 ? {} : undef; my $direction = wantarray && $Cpanel::CPAN::Locales::VERSION > 0.09 ? {} : undef; for my $code ( 'en', @langcodes ) { # en since it is "built in" if ( defined $native ) { $native->{$code} = $lh->{'Locales.pm'}{'_main_'}->get_native_language_from_code( $code, 1 ); } $langname->{$code} = $_NATIVE_ONLY ? $native->{$code} : $lh->{'Locales.pm'}{'_main_'}->get_language_from_code( $code, 1 ); if ( defined $direction ) { $direction->{$code} = $lh->{'Locales.pm'}{'_main_'}->get_character_orientation_from_code_fast($code); } } return wantarray ? ( $langname, $native, $direction ) : $langname; } sub loadable_lang_names_hashref { my ( $lh, @langcodes ) = @_; my $langname = $lh->lang_names_hashref(@langcodes); for my $tag ( keys %{$langname} ) { delete $langname->{$tag} if !$lh->langtag_is_loadable($tag); } return $langname; } sub add_lexicon_override_hash { my ( $lh, $langtag, $name, $hr ) = @_; if ( @_ == 3 ) { $hr = $name; $name = $langtag; $langtag = $lh->get_language_tag(); } my $ns = $lh->get_language_tag() eq $langtag ? $lh->get_language_class() : $lh->get_base_class(); no strict 'refs'; if ( my $ref = tied( %{ $ns . '::Lexicon' } ) ) { return 1 if $lh->{'add_lex_hash_silent_if_already_added'} && exists $ref->{'hashes'} && exists $ref->{'hashes'}{$name}; if ( $ref->can('add_lookup_override_hash') ) { return $ref->add_lookup_override_hash( $name, $hr ); } } my $cur_errno = $!; if ( eval { require Sub::Todo } ) { goto &Sub::Todo::todo; } else { $! = $cur_errno; return; } } sub add_lexicon_fallback_hash { my ( $lh, $langtag, $name, $hr ) = @_; if ( @_ == 3 ) { $hr = $name; $name = $langtag; $langtag = $lh->get_language_tag(); } my $ns = $lh->get_language_tag() eq $langtag ? $lh->get_language_class() : $lh->get_base_class(); no strict 'refs'; if ( my $ref = tied( %{ $ns . '::Lexicon' } ) ) { return 1 if $lh->{'add_lex_hash_silent_if_already_added'} && exists $ref->{'hashes'} && exists $ref->{'hashes'}{$name}; if ( $ref->can('add_lookup_fallback_hash') ) { return $ref->add_lookup_fallback_hash( $name, $hr ); } } my $cur_errno = $!; if ( eval { require Sub::Todo } ) { goto &Sub::Todo::todo; } else { $! = $cur_errno; return; } } sub del_lexicon_hash { my ( $lh, $langtag, $name ) = @_; if ( @_ == 2 ) { return if $langtag eq '*'; $name = $langtag; $langtag = '*'; } return if !$langtag; my $count = 0; if ( $langtag eq '*' ) { no strict 'refs'; for my $ns ( $lh->get_base_class(), $lh->get_language_class() ) { if ( my $ref = tied( %{ $ns . '::Lexicon' } ) ) { if ( $ref->can('del_lookup_hash') ) { $ref->del_lookup_hash($name); $count++; } } } return 1 if $count; my $cur_errno = $!; if ( eval { require Sub::Todo } ) { goto &Sub::Todo::todo; } else { $! = $cur_errno; return; } } else { my $ns = $lh->get_language_tag() eq $langtag ? $lh->get_language_class() : $lh->get_base_class(); no strict 'refs'; if ( my $ref = tied( %{ $ns . '::Lexicon' } ) ) { if ( $ref->can('del_lookup_hash') ) { return $ref->del_lookup_hash($name); } } my $cur_errno = $!; if ( eval { require Sub::Todo } ) { goto &Sub::Todo::todo; } else { $! = $cur_errno; return; } } } sub get_language_class { return ref( $_[0] ) || $_[0]; } sub get_base_class_dir { my ($lh) = @_; if ( !exists $lh->{'Cpanel::CPAN::Locale::Maketext::Utils'}{'_base_clase_dir'} ) { $lh->{'Cpanel::CPAN::Locale::Maketext::Utils'}{'_base_clase_dir'} = undef; my $inc_key = $lh->get_base_class(); substr( $inc_key, index( $inc_key, '::' ), 2, '/' ) while index( $inc_key, '::' ) > -1; $inc_key .= '.pm'; if ( exists $INC{$inc_key} ) { if ( -e $INC{$inc_key} ) { my $hr = $lh->{'Cpanel::CPAN::Locale::Maketext::Utils'}; $hr->{'_base_clase_dir'} = $INC{$inc_key}; substr( $hr->{'_base_clase_dir'}, -3, 3, '' ) if substr( $hr->{'_base_clase_dir'}, -3 ) eq '.pm'; } } } return $lh->{'Cpanel::CPAN::Locale::Maketext::Utils'}{'_base_clase_dir'}; } sub list_available_locales { my ($lh) = @_; die "List context only!" if !wantarray; my $main_ns_dir = $lh->get_base_class_dir() || return; local $!; opendir my $dh, $main_ns_dir or die "Failed to open: $main_ns_dir: $!"; return map { ( substr( $_, -3 ) eq '.pm' && $_ ne 'Utils.pm' && $_ ne 'Lazy.pm' && $_ ne 'Context.pm' && $_ ne 'Fallback.pm' ) ? substr( $_, 0, -3 ) : () } readdir($dh); #de-taint } sub get_asset { my ( $lh, $code, $tag ) = @_; # No caching since $code can do anything. my $root = $tag || $lh->get_language_tag; my $ret; die "Invalid locale: $root" if index( $root, '/' ) > -1; $ret = $code->($root); return $ret if defined $ret; my $loc; # buffer my %seen = ( $root => 1 ); my @fallback_locales; if ( $lh->_has_fallback_list($root) ) { my $loc_obj = $lh->get_locales_obj($tag); @fallback_locales = $loc_obj->get_fallback_list( $lh->{'Locales.pm'}{'get_fallback_list_special_lookup_coderef'} ); } elsif ( $root ne 'en' ) { my $super = ( split( m{_}, $root ) )[0]; @fallback_locales = ( ( $super ne $root && $super ne 'i' ? $super : () ), 'en' ); } for $loc (@fallback_locales) { next if $seen{$loc}; # get_fallback_list can provide back dupes and its expensive to enumerate each one $ret = $code->($loc); $seen{$loc}++; last if defined $ret; } return $ret if defined $ret; return; } sub _has_fallback_list { return $_[0]->{'_has_fallback_list'}{ $_[1] } if defined $_[0]->{'_has_fallback_list'}{ $_[1] }; my $size = -s LOCALE_FALLBACK_CACHE_DIR . '/' . $_[1]; return ( $_[0]->{'_has_fallback_list'}{ $_[1] } = ( !defined $size || $size ) ? 1 : 0 ); } sub get_asset_file { my ( $lh, $find, $return ) = @_; $return = $find if !defined $return; return $lh->{'cache'}{'get_asset_file'}{$find}{$return} if exists $lh->{'cache'}{'get_asset_file'}{$find}{$return}; $lh->{'cache'}{'get_asset_file'}{$find}{$return} = $lh->get_asset( sub { return sprintf( $return, $_[0] ) if -f sprintf( $find, $_[0] ); return; } ); return $lh->{'cache'}{'get_asset_file'}{$find}{$return} if defined $lh->{'cache'}{'get_asset_file'}{$find}{$return}; return; } sub get_asset_dir { my ( $lh, $find, $return ) = @_; $return = $find if !defined $return; return $lh->{'cache'}{'get_asset_dir'}{$find}{$return} if exists $lh->{'cache'}{'get_asset_dir'}{$find}{$return}; $lh->{'cache'}{'get_asset_dir'}{$find}{$return} = $lh->get_asset( sub { return sprintf( $return, $_[0] ) if -d sprintf( $find, $_[0] ); return; } ); return $lh->{'cache'}{'get_asset_dir'}{$find}{$return} if defined $lh->{'cache'}{'get_asset_dir'}{$find}{$return}; return; } sub delete_cache { my ( $lh, $which ) = @_; if ( defined $which ) { return delete $lh->{'cache'}{$which}; } else { return delete $lh->{'cache'}; } } sub quant { my ( $handle, $num, @forms ) = @_; my $max_decimal_places = 3; if ( ref($num) eq 'ARRAY' ) { $max_decimal_places = $num->[1]; $num = $num->[0]; } $handle->{'Locales.pm'}{'_main_'} ||= $handle->get_locales_obj(); my ( $string, $spec_zero ) = $handle->{'Locales.pm'}{'_main_'}->get_plural_form( $num, @forms ); if ( index( $string, '%s' ) > -1 ) { return sprintf( $string, $handle->numf( $num, $max_decimal_places ) ); } elsif ( $num == 0 && $spec_zero ) { return $string; } else { $handle->numf( $num, $max_decimal_places ) . " $string"; } } sub numerate { my ( $handle, $num, @forms ) = @_; $handle->{'Locales.pm'}{'_main_'} ||= $handle->get_locales_obj(); return scalar( $handle->{'Locales.pm'}{'_main_'}->get_plural_form( $num, @forms ) ); } sub numf { my ( $handle, $num, $max_decimal_places ) = @_; $handle->{'Locales.pm'}{'_main_'} ||= $handle->get_locales_obj(); return $handle->{'Locales.pm'}{'_main_'}->get_formatted_decimal( $num, $max_decimal_places ); } sub join { shift; return CORE::join( shift, map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @_ ); } sub list_and { my $lh = shift; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); return $lh->{'Locales.pm'}{'_main_'}->get_list_and( map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @_ ); } sub list_or { my $lh = shift; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); return $lh->{'Locales.pm'}{'_main_'}->get_list_or( map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @_ ); } sub list_and_quoted { my ( $lh, @args ) = @_; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); local $lh->{'Locales.pm'}{'_main_'}{'misc'}{'list_quote_mode'} = 'all'; return $lh->list_and(@args); } sub list_or_quoted { my ( $lh, @args ) = @_; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); local $lh->{'Locales.pm'}{'_main_'}{'misc'}{'list_quote_mode'} = 'all'; return $lh->list_or(@args); } sub output_asis { return $_[1]; } sub asis { return $_[0]->output( 'asis', $_[1] ); # this allows for embedded methods but still called via [asis,...] instead of [output,asis,...] } sub comment { return ''; } sub is_future { my ( $lh, $dt, $future, $past, $current, $current_type ) = @_; if ( $dt =~ tr{0-9}{}c ) { $dt = __get_dt_obj_from_arg( $dt, 0 ); $dt = $dt->epoch(); } if ($current) { if ( !ref $dt ) { $dt = __get_dt_obj_from_arg( $dt, 0 ); } $current_type ||= 'hour'; if ( $current_type eq 'day' ) { } elsif ( $current_type eq 'minute' ) { } else { } } return ref $dt ? $dt->epoch() : $dt > time() ? $future : $past; } sub __get_dt_obj_from_arg { require # hide from Cpanel::Static DateTime; return !defined $_[0] || $_[0] eq '' ? DateTime->now() : ref $_[0] eq 'HASH' ? DateTime->new( %{ $_[0] } ) : $_[0] =~ m{ \A (\d+ (?: [.] \d+ )? ) (?: [:] (.*) )? \z }xms ? DateTime->from_epoch( 'epoch' => $1, 'time_zone' => ( $2 || 'UTC' ) ) : !ref $_[0] ? DateTime->now( 'time_zone' => ( $_[0] || 'UTC' ) ) : $_[1] ? $_[0]->clone() : $_[0]; } sub current_year { $_[0]->datetime( '', 'YYYY' ); } sub datetime { my ( $lh, $dta, $str ) = @_; my $dt = __get_dt_obj_from_arg( $dta, 1 ); if ( !$INC{'DateTime/Locale.pm'} ) { # __get_dt_obj_from_arg is loading DateTime eval q{ require DateTime::Locale; 1 } or die "Cannot load DateTime::Locale: $!"; } $dt->{'locale'} = DateTime::Locale->load( $lh->language_tag() ); my $format = ref $str eq 'CODE' ? $str->($dt) : $str; if ( defined $format ) { if ( $dt->{'locale'}->can($format) ) { $format = $dt->{'locale'}->$format(); } } $format = '' if !defined $format; return $dt->format_cldr( $dt->{'locale'}->format_for($format) || $format || $dt->{'locale'}->date_format_long() ); } sub output_amp { return $_[0]->output_chr(38) } sub output_lt { return $_[0]->output_chr(60) } # TODO: ? make the rest of these embeddable like amp() ? sub output_gt { return $_[0]->output_chr(62) } sub output_apos { return $_[0]->output_chr(39) } sub output_quot { return $_[0]->output_chr(34) } sub output_shy { return $_[0]->output_chr(173) } use constant output_nbsp => "\xC2\xA0"; my $space; sub format_bytes { my ( $lh, $bytes, $max_decimal_place ) = @_; $bytes ||= 0; if ( !defined $max_decimal_place ) { $max_decimal_place = 2; } else { $max_decimal_place = int( abs($max_decimal_place) ); } my $absnum = abs($bytes); $space ||= $lh->output_nbsp(); # avoid method call if we already have it if ( $absnum < 1024 ) { return ( $lh->{'_format_bytes_cache'}{ $bytes . '_' . $max_decimal_place } ||= $lh->maketext( '[quant,_1,%s byte,%s bytes]', [ $bytes, $max_decimal_place ] ) ); # the space between the '%s' and the 'b' is a non-break space (e.g. option-spacebar, not spacebar) } elsif ( $absnum < 1048576 ) { return $lh->numf( ( $bytes / 1024 ), $max_decimal_place ) . $space . 'KB'; } elsif ( $absnum < 1073741824 ) { return $lh->numf( ( $bytes / 1048576 ), $max_decimal_place ) . $space . 'MB'; } elsif ( $absnum < 1099511627776 ) { return $lh->numf( ( $bytes / 1073741824 ), $max_decimal_place ) . $space . 'GB'; } elsif ( $absnum < 1125899906842624 ) { return $lh->numf( ( $bytes / 1099511627776 ), $max_decimal_place ) . $space . 'TB'; } elsif ( $absnum < ( 1125899906842624 * 1024 ) ) { return $lh->numf( ( $bytes / 1125899906842624 ), $max_decimal_place ) . $space . 'PB'; } elsif ( $absnum < ( 1125899906842624 * 1024 * 1024 ) ) { return $lh->numf( ( $bytes / ( 1125899906842624 * 1024 ) ), $max_decimal_place ) . $space . 'EB'; } elsif ( $absnum < ( 1125899906842624 * 1024 * 1024 * 1024 ) ) { return $lh->numf( ( $bytes / ( 1125899906842624 * 1024 * 1024 ) ), $max_decimal_place ) . $space . 'ZB'; } else { return $lh->numf( ( $bytes / ( 1125899906842624 * 1024 * 1024 * 1024 ) ), $max_decimal_place ) . $space . 'YB'; } } sub convert { die __PACKAGE__ . "::convert is not supported (missing Math::Units)"; } sub is_defined { my ( $lh, $value, $is_defined, $not_defined, $is_defined_but_false ) = @_; return __proc_string_with_embedded_under_vars($not_defined) if !defined $value; if ( defined $is_defined_but_false && !$value ) { return __proc_string_with_embedded_under_vars($is_defined_but_false); } else { return __proc_string_with_embedded_under_vars($is_defined); } } sub boolean { my ( $lh, $boolean, $true, $false, $null ) = @_; if ($boolean) { return __proc_string_with_embedded_under_vars($true); } else { if ( !defined $boolean && defined $null ) { return __proc_string_with_embedded_under_vars($null); } return __proc_string_with_embedded_under_vars($false); } } sub __proc_string_with_embedded_under_vars { my $str = $_[0]; return $str if index( $str, '_' ) == -1 || $str !~ m/$FORCE_REGEX_LAZY\_(?:\-?[0-9]+)/o; my @args = __caller_args( $_[1] ); # this way be dragons $str =~ s/$FORCE_REGEX_LAZY\_(\-?[0-9]+)/$args[$1]/og; return $str; } sub __caller_args { package DB; () = caller( $_[0] + 3 ); return @DB::args; } sub __proc_emb_meth { my ( $lh, $str ) = @_; $str =~ s/$FORCE_REGEX_LAZY(su[bp])\(((?:\\\)|[^\)])+?)\)/my $s=$2;my $m="output_$1";$s=~s{\\\)}{\)}g;$lh->$m($s)/oeg if index( $str, 'su' ) > -1; $str =~ s/${FORCE_REGEX_LAZY}chr\(((?:\d+|[\S]))\)/$lh->output_chr($1)/oeg if index( $str, 'chr(' ) > -1; $str =~ s/${FORCE_REGEX_LAZY}numf\((\d+(?:\.\d+)?)\)/$lh->numf($1)/oeg if index( $str, 'numf(' ) > -1; substr( $str, index( $str, 'amp()' ), 5, $lh->output_amp() ) while index( $str, 'amp()' ) > -1; return $str; } sub output { my ( $lh, $output_function, $string, @output_function_args ) = @_; if ( defined $string && $string ne '' && index( $string, '(' ) > -1 ) { $string = __proc_emb_meth( $lh, $string ); } if ( $output_function eq 'url' && defined $output_function_args[0] && $output_function_args[0] ne '' && index( $output_function_args[0], '(' ) > -1 ) { $output_function_args[0] = __proc_emb_meth( $lh, $output_function_args[0] ); } if ( my $cr = ( $lh->{'_output_function_cache'}{$output_function} ||= $lh->can( 'output_' . $output_function ) ) ) { return $cr->( $lh, $string, @output_function_args ); } else { my $cur_errno = $!; if ( eval { require Sub::Todo } ) { $! = Sub::Todo::get_errno_func_not_impl(); } else { $! = $cur_errno; } return $string; } } sub output_encode_puny { my ( $self, $s ) = @_; require # do not include it in updatenow.static Cpanel::Encoder::Punycode; return Cpanel::Encoder::Punycode::punycode_encode_str($s); } sub output_decode_puny { my ( $self, $s ) = @_; require # do not include it in updatenow.static Cpanel::Encoder::Punycode; return Cpanel::Encoder::Punycode::punycode_decode_str($s); } my $has_encode; # checking for Encode this way facilitates only checking @INC once for the module on systems that do not have Encode sub output_chr { my ( $lh, $chr_num ) = @_; if ( $chr_num !~ m/$FORCE_REGEX_LAZY\A\d+\z/o ) { return if length($chr_num) != 1; return $chr_num if !$lh->context_is_html(); return $chr_num eq '"' ? '"' : $chr_num eq '&' ? '&' : $chr_num eq "'" ? ''' : $chr_num eq '<' ? '<' : $chr_num eq '>' ? '>' : $chr_num; } return if $chr_num !~ m/$FORCE_REGEX_LAZY\A\d+\z/o; my $chr = chr($chr_num); if ( $chr_num > 127 ) { if ( !defined $has_encode ) { $has_encode = 0; eval { require Encode; $has_encode = 1; }; } if ($has_encode) { $chr = Encode::encode( $lh->encoding(), $chr ); } else { $chr = eval '"\x{' . sprintf( '%04X', $chr_num ) . '}"'; } } if ( !$lh->context_is_html() ) { return $chr; } else { return $chr_num == 34 || $chr_num == 147 || $chr_num == 148 ? '"' : $chr_num == 38 ? '&' : $chr_num == 39 || $chr_num == 145 || $chr_num == 146 ? ''' : $chr_num == 60 ? '<' : $chr_num == 62 ? '>' : $chr_num == 173 ? '­' : $chr; } } sub output_class { my ( $lh, $string, @classes ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if $lh->context_is_plain(); return $lh->context_is_ansi() ? "\e[1m$string\e[0m" : qq{$string}; } sub output_asis_for_tests { my ( $lh, $string ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string; } sub __make_attr_str_from_ar { my ( $attr_ar, $strip_hr, $addin ) = @_; if ( ref($attr_ar) eq 'HASH' ) { $strip_hr = $attr_ar; $attr_ar = []; } my $attr = ''; my $general_hr = ref( $attr_ar->[-1] ) eq 'HASH' ? pop( @{$attr_ar} ) : undef; my $idx = 0; my $ar_len = @{$attr_ar}; $idx = 1 if $ar_len % 2; # handle “Odd number of elements” … my $did_addin; while ( $idx < $ar_len ) { if ( exists $strip_hr->{ $attr_ar->[$idx] } ) { $idx += 2; next; } my $atr = $attr_ar->[$idx]; my $val = $attr_ar->[ ++$idx ]; if ( exists $addin->{$atr} ) { $val = "$addin->{$atr} $val"; $did_addin->{$atr}++; } $attr .= qq{ $atr="$val"}; $idx++; } if ($general_hr) { for my $k ( keys %{$general_hr} ) { next if exists $strip_hr->{$k}; if ( exists $addin->{$k} ) { $general_hr->{$k} = "$addin->{$k} $general_hr->{$k}"; $did_addin->{$k}++; } $attr .= qq{ $k="$general_hr->{$k}"}; } } for my $r ( keys %{$addin} ) { if ( !exists $did_addin->{$r} ) { $attr .= qq{ $r="$addin->{$r}"}; } } return $attr; } sub output_inline { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if !$lh->context_is_html(); my $attr = __make_attr_str_from_ar( \@attrs ); return qq{$string}; } *output_attr = \&output_inline; sub output_block { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if !$lh->context_is_html(); my $attr = __make_attr_str_from_ar( \@attrs ); return qq{$string}; } sub output_img { my ( $lh, $src, $alt, @attrs ) = @_; if ( !defined $alt || $alt eq '' ) { $alt = $src; } else { $alt = __proc_string_with_embedded_under_vars( $alt, 1 ); } return $alt if !$lh->context_is_html(); my $attr = __make_attr_str_from_ar( \@attrs, { 'alt' => 1, 'src' => 1 } ); return qq{$alt}; } sub output_abbr { my ( $lh, $abbr, $full, @attrs ) = @_; return !$lh->context_is_html() ? "$abbr ($full)" : qq{ 1 } ) . qq{>$abbr}; } sub output_acronym { my ( $lh, $acronym, $full, @attrs ) = @_; return !$lh->context_is_html() ? "$acronym ($full)" : qq{ 1 }, { 'class' => 'initialism' } ) . qq{>$acronym}; } sub output_sup { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return !$lh->context_is_html() ? $string : qq{$string}; } sub output_sub { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return !$lh->context_is_html() ? $string : qq{$string}; } sub output_underline { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if $lh->context_is_plain(); return $lh->context_is_ansi() ? "\e[4m$string\e[0m" : qq{$string}; } sub output_strong { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if $lh->context_is_plain(); return $lh->context_is_ansi() ? "\e[1m$string\e[0m" : '$string"; } sub output_em { my ( $lh, $string, @attrs ) = @_; $string = __proc_string_with_embedded_under_vars( $string, 1 ); return $string if $lh->context_is_plain(); return $lh->context_is_ansi() ? "\e[3m$string\e[0m" : '$string"; } sub output_url { my ( $lh, $url, @args ) = @_; $url ||= ''; # carp() ? my $arb_args_hr = ref $args[-1] eq 'HASH' ? pop(@args) : {}; my ( $url_text, %output_config ) = @args % 2 ? @args : ( undef, @args ); my $return = $url; if ( !$lh->context_is_html() ) { if ($url_text) { return "$url_text ($url)"; } if ( exists $output_config{'plain'} ) { $output_config{'plain'} ||= $url; my $orig = $output_config{'plain'}; $output_config{'plain'} = __proc_string_with_embedded_under_vars( $output_config{'plain'}, 1 ); $return = $orig ne $output_config{'plain'} && $output_config{'plain'} =~ m/\Q$url\E/ ? $output_config{'plain'} : "$output_config{'plain'} $url"; } } else { if ( exists $output_config{'html'} ) { $output_config{'html'} = __proc_string_with_embedded_under_vars( $output_config{'html'}, 1 ); } $output_config{'html'} ||= $url_text || $url; my $attr = __make_attr_str_from_ar( [ @args, $arb_args_hr ], { 'html' => 1, 'plain' => 1, '_type' => 1, } ); $return = exists $output_config{'_type'} && $output_config{'_type'} eq 'offsite' ? qq{$output_config{'html'}} : qq{$output_config{'html'}}; } return $return; } sub set_context_html { my ($lh) = @_; my $cur = $lh->get_context(); $lh->set_context('html'); return if !$lh->context_is_html(); return $cur; } sub set_context_ansi { my ($lh) = @_; my $cur = $lh->get_context(); $lh->set_context('ansi'); return if !$lh->context_is_ansi(); return $cur; } sub set_context_plain { my ($lh) = @_; my $cur = $lh->get_context(); $lh->set_context('plain'); return if !$lh->context_is_plain(); return $cur; } my %contexts = ( 'plain' => undef(), 'ansi' => 1, 'html' => 0, ); sub set_context { my ( $lh, $context ) = @_; if ( !$context ) { $lh->{'-t-STDIN'} = -t *STDIN ? 1 : 0; } elsif ( exists $contexts{$context} ) { $lh->{'-t-STDIN'} = $contexts{$context}; } else { require Carp; local $Carp::CarpLevel = 1; Carp::carp("Given context '$context' is unknown."); $lh->{'-t-STDIN'} = $context; } } sub context_is_html { return $_[0]->get_context() eq 'html'; } sub context_is_ansi { return $_[0]->get_context() eq 'ansi'; } sub context_is_plain { return $_[0]->get_context() eq 'plain'; } sub context_is { return $_[0]->get_context() eq $_[1]; } sub get_context { $_[0]->set_context() if !exists $_[0]->{'-t-STDIN'}; return !defined $_[0]->{'-t-STDIN'} ? 'plain' : $_[0]->{'-t-STDIN'} ? 'ansi' : 'html'; } sub maketext_html_context { my ( $lh, @mt_args ) = @_; my $cur = $lh->set_context_html(); my $res = $lh->maketext(@mt_args); $lh->set_context($cur); return $res; } sub maketext_ansi_context { my ( $lh, @mt_args ) = @_; my $cur = $lh->set_context_ansi(); my $res = $lh->maketext(@mt_args); $lh->set_context($cur); return $res; } sub maketext_plain_context { my ( $lh, @mt_args ) = @_; my $cur = $lh->set_context_plain(); my $res = $lh->maketext(@mt_args); $lh->set_context($cur); return $res; } 1; } # --- END Cpanel/CPAN/Locale/Maketext/Utils.pm { # --- BEGIN Cpanel/Locale/Utils/Paths.pm package Cpanel::Locale::Utils::Paths; use strict; use warnings; use constant { get_legacy_lang_cache_root => '/var/cpanel/lang.cache', get_i_locales_config_path => '/var/cpanel/i_locales', get_custom_whitelist_path => '/var/cpanel/maketext_whitelist' }; sub get_locale_database_root { return '/var/cpanel/locale' } sub get_locale_yaml_root { return '/usr/local/cpanel/locale' } sub get_legacy_lang_root { return '/usr/local/cpanel/lang' } sub get_locale_yaml_local_root { return '/var/cpanel/locale.local' } 1; } # --- END Cpanel/Locale/Utils/Paths.pm { # --- BEGIN Cpanel/Locale/Utils.pm package Cpanel::Locale::Utils; use strict; use warnings; BEGIN { eval { require CDB_File; }; } # use Cpanel::Locale::Utils::Paths (); $Cpanel::Locale::Utils::i_am_the_compiler = 0; my $logger; sub _logger { require Cpanel::Logger; $logger ||= Cpanel::Logger->new(); } sub get_readonly_tie { my ( $cdb_file, $cdb_hr ) = @_; if ( !$cdb_file ) { _logger()->warn('Undefined CDB file specified for readonly operation'); return; } elsif ( !$INC{'CDB_File.pm'} || !exists $CDB_File::{'TIEHASH'} ) { _logger()->warn("Failed to load CDB_File.pm") if $^X ne '/usr/bin/perl'; return; } my $tie_obj = tie %{$cdb_hr}, 'CDB_File', $cdb_file; if ( !$tie_obj && !-e $cdb_file ) { _logger()->warn("Missing CDB file $cdb_file specified for readonly operation"); return; } eval { exists $cdb_hr->{'__VERSION'} }; if ($@) { $tie_obj = undef; untie %$cdb_hr; } if ( !$tie_obj ) { _logger()->warn("CDB_File could not get read-only association to '$cdb_file': $!"); } return $tie_obj; } sub create_cdb { my ( $cdb_file, $cdb_hr ) = @_; if ( !$cdb_file ) { _logger()->warn('Undefined CDB file specified for writable operation'); return; } return CDB_File::create( %{$cdb_hr}, $cdb_file, "$cdb_file.$$" ); } sub get_writable_tie { require Carp; Carp::confess("cdb files are not writable"); } sub init_lexicon { my ( $langtag, $hr, $version_sr, $encoding_sr ) = @_; my $cdb_file; my $db_root = Cpanel::Locale::Utils::Paths::get_locale_database_root(); for my $file ( $Cpanel::CPDATA{'RS'} ? ("themes/$Cpanel::CPDATA{RS}/$langtag.cdb") : (), "$langtag.cdb" ) { # PPI NO PARSE - Only include Cpanel() when some other module uses it if ( -e "$db_root/$file" ) { $cdb_file = "$db_root/$file"; last; } } if ( !$cdb_file ) { if ( -e Cpanel::Locale::Utils::Paths::get_locale_yaml_root() . "/$langtag.yaml" && !$Cpanel::Locale::Utils::i_am_the_compiler ) { _logger()->info(qq{Locale needs to be compiled by root (/usr/local/cpanel/bin/build_locale_databases --locale=$langtag)}); } return; } my $cdb_tie = get_readonly_tie( $cdb_file, $hr ); if ( exists $hr->{'__VERSION'} && ref $version_sr ) { ${$version_sr} = $hr->{'__VERSION'}; } if ( ref $encoding_sr ) { ${$encoding_sr} ||= 'utf-8'; } return $cdb_file; } sub init_package { my ($caller) = caller(); my ($langtag) = reverse( split( /::/, $caller ) ); no strict 'refs'; no warnings 'once'; ${ $caller . '::CDB_File_Path' } ||= init_lexicon( "$langtag", \%{ $caller . '::Lexicon' }, \${ $caller . '::VERSION' }, \${ $caller . '::Encoding' }, ); return; } 1; } # --- END Cpanel/Locale/Utils.pm { # --- BEGIN Cpanel/DB/Utils.pm package Cpanel::DB::Utils; use strict; sub username_to_dbowner { my ($username) = @_; $username =~ tr<_.><>d if defined $username; return $username; } 1; } # --- END Cpanel/DB/Utils.pm { # --- BEGIN Cpanel/AdminBin/Serializer.pm package Cpanel::AdminBin::Serializer; use strict; use warnings; # use Cpanel::JSON (); our $VERSION = '2.4'; our $MAX_LOAD_LENGTH; our $MAX_PRIV_LOAD_LENGTH; BEGIN { *MAX_LOAD_LENGTH = \$Cpanel::JSON::MAX_LOAD_LENGTH; *MAX_PRIV_LOAD_LENGTH = \$Cpanel::JSON::MAX_PRIV_LOAD_LENGTH; *DumpFile = *Cpanel::JSON::DumpFile; } BEGIN { *Dump = *Cpanel::JSON::Dump; *SafeDump = *Cpanel::JSON::SafeDump; *LoadFile = *Cpanel::JSON::LoadFileNoSetUTF8; *Load = *Cpanel::JSON::Load; *looks_like_serialized_data = *Cpanel::JSON::looks_like_json; } sub SafeLoadFile { return Cpanel::JSON::_LoadFile( $_[0], $Cpanel::JSON::MAX_LOAD_LENGTH, $Cpanel::JSON::DECODE_UTF8, $_[1], $Cpanel::JSON::LOAD_STRICT ); } sub SafeLoad { utf8::decode( $_[0] ); return Cpanel::JSON::LoadNoSetUTF8(@_); } sub clone { return Cpanel::JSON::LoadNoSetUTF8( Cpanel::JSON::Dump( $_[0] ) ); } 1; } # --- END Cpanel/AdminBin/Serializer.pm { # --- BEGIN Cpanel/AdminBin/Serializer/FailOK.pm package Cpanel::AdminBin::Serializer::FailOK; use strict; use warnings; sub LoadModule { local $@; return 1 if $INC{'Cpanel/AdminBin/Serializer.pm'}; my $load_ok = eval { local $SIG{'__DIE__'}; # Suppress spewage as we may be reading an invalid cache local $SIG{'__WARN__'}; # and since failure is ok to throw it away require Cpanel::AdminBin::Serializer; 1; }; if ( !$load_ok && !$ENV{'CPANEL_BASE_INSTALL'} && index( $^X, '/usr/local/cpanel' ) == 0 ) { warn $@; } return $load_ok ? 1 : 0; } sub LoadFile { my ( $file_or_fh, $path ) = @_; return undef if !$INC{'Cpanel/AdminBin/Serializer.pm'}; return eval { local $SIG{'__DIE__'}; # Suppress spewage as we may be reading an invalid cache local $SIG{'__WARN__'}; # and since failure is ok to throw it away Cpanel::AdminBin::Serializer::LoadFile( $file_or_fh, undef, $path ); }; } 1; } # --- END Cpanel/AdminBin/Serializer/FailOK.pm { # --- BEGIN Cpanel/Config/Constants.pm package Cpanel::Config::Constants; use strict; use warnings; our $DEFAULT_CPANEL_THEME = 'jupiter'; our $DEFAULT_CPANEL_MAILONLY_THEME = 'jupiter'; our $DEFAULT_WEBMAIL_THEME = 'jupiter'; our $DEFAULT_WEBMAIL_MAILONLY_THEME = 'jupiter'; our @DORMANT_SERVICES_LIST = qw(cpanalyticsd cpdavd cphulkd cpsrvd dnsadmin spamd); our $MAX_HOMEDIR_STREAM_TIME = ( 86400 * 2 ); 1; } # --- END Cpanel/Config/Constants.pm { # --- BEGIN Cpanel/Imports.pm package Cpanel::Imports; use strict; $Cpanel::Imports::VERSION = '0.02'; sub import { my $caller = caller; no strict 'refs'; ## no critic(ProhibitNoStrict) *{ $caller . '::logger' } = \&__logger; *{ $caller . '::locale' } = \&__locale; return; } my ( $logger, $locale ); sub _reset_lazy_facade { # usually for testing $logger = undef; $locale = undef; return; } sub __logger { require Cpanel::Logger if !$INC{'Cpanel/Logger.pm'}; if ( !$logger ) { # return $var ||= XYZ->new; works but, we keep it super vanilla to make it more likley to perlcc OK $logger = Cpanel::Logger->new; } return $logger; } sub __locale { require Cpanel::Locale if !$INC{'Cpanel/Locale.pm'}; if ( !$locale ) { # return $var ||= XYZ->new; works but, we keep it super vanilla to make it more likley to perlcc OK $locale = Cpanel::Locale->get_handle; } return $locale; } 1; } # --- END Cpanel/Imports.pm { # --- BEGIN Cpanel/SSL/KeyTypeLabel.pm package Cpanel::SSL::KeyTypeLabel; use cPstrict; use Cpanel::Imports; my %_ECDSA_DETAIL = ( prime256v1 => 'P-256 (prime256v1)', secp384r1 => 'P-384 (secp384r1)', ); sub to_label ($the_type) { my ( $type, $detail ) = split m<->, $the_type; die _invalid_type_msg($the_type) if !defined $detail; $type =~ tr; if ( $type eq 'RSA' ) { $detail = locale()->maketext( '[numf,_1]-bit', $detail ); } elsif ( $type eq 'ECDSA' ) { $detail = $_ECDSA_DETAIL{$detail} or die _invalid_type_msg($the_type); } else { die "need update? ($the_type)"; } return "$type, $detail"; } sub _invalid_type_msg ($the_type) { return "Invalid key type: “$the_type”"; } 1; } # --- END Cpanel/SSL/KeyTypeLabel.pm { # --- BEGIN Cpanel/SSL/DefaultKey/Constants.pm package Cpanel::SSL::DefaultKey::Constants; use cPstrict; # use Cpanel::SSL::KeyTypeLabel (); use constant OPTIONS => ( 'rsa-2048', 'ecdsa-secp384r1', 'ecdsa-prime256v1', 'rsa-4096', ); sub OPTIONS_AND_LABELS() { local ( $@, $! ); require Cpanel::Locale; my $lh = Cpanel::Locale->get_handle(); return map { ( $_ => Cpanel::SSL::KeyTypeLabel::to_label($_) ) } OPTIONS; } sub KEY_DESCRIPTIONS() { require Cpanel::Locale; my $lh = Cpanel::Locale->get_handle(); return { "rsa-2048" => $lh->maketext("[asis,RSA] is more compatible with older clients (for example, browsers older than [asis,Internet Explorer] 11) than [asis,ECDSA]. New installations of [asis,cPanel amp() WHM] ship with this setting."), "rsa-4096" => $lh->maketext( "[asis,RSA] is more compatible with older clients (for example, browsers older than [asis,Internet Explorer] 11) than [asis,ECDSA]. This is more secure than [_1]-bit, but will perform slower than [_1]-bit keys.", 'RSA, 2,048' ), "ecdsa-prime256v1" => $lh->maketext("[asis,ECDSA] allows websites to support [asis,Internet Explorer] 11 and retain compliance with [output,acronym,PCI,Payment Card Industry] standards."), "ecdsa-secp384r1" => $lh->maketext("[asis,ECDSA] allows websites to support [asis,Internet Explorer] 11 and retain compliance with [output,acronym,PCI,Payment Card Industry] standards. [asis,secp384r1] is more secure than [asis,prime256v1], but may perform slower."), }; } use constant USER_SYSTEM => 'system'; 1; } # --- END Cpanel/SSL/DefaultKey/Constants.pm { # --- BEGIN Cpanel/Config/CpUser/Defaults.pm package Cpanel::Config::CpUser::Defaults; use strict; use warnings; # use Cpanel::SSL::DefaultKey::Constants (); our @DEFAULTS_KV = ( 'BWLIMIT' => 'unlimited', 'CHILD_WORKLOADS' => q<>, 'DEADDOMAINS' => undef, 'DEMO' => 0, 'DOMAIN' => '', 'DOMAINS' => undef, 'FEATURELIST' => 'default', 'HASCGI' => 0, 'HASDKIM' => 0, 'HASSPF' => 0, 'IP' => '127.0.0.1', 'MAILBOX_FORMAT' => 'maildir', #keep in sync with cpconf 'MAX_EMAILACCT_QUOTA' => 'unlimited', 'MAXADDON' => 0, 'MAXFTP' => 'unlimited', 'MAXLST' => 'unlimited', 'MAXPARK' => 0, 'MAXPOP' => 'unlimited', 'MAXSQL' => 'unlimited', 'MAXSUB' => 'unlimited', 'OWNER' => 'root', 'PLAN' => 'undefined', 'RS' => '', 'STARTDATE' => '0000000000', 'MAXPASSENGERAPPS' => 4, 'SSL_DEFAULT_KEY_TYPE' => Cpanel::SSL::DefaultKey::Constants::USER_SYSTEM, ); 1; } # --- END Cpanel/Config/CpUser/Defaults.pm { # --- BEGIN Cpanel/Hash/JSONable.pm package Cpanel::Hash::JSONable; use cPstrict; sub TO_JSON ($self) { return {%$self}; } 1; } # --- END Cpanel/Hash/JSONable.pm { # --- BEGIN Cpanel/Config/CpUser/Object.pm package Cpanel::Config::CpUser::Object; use cPstrict; # use Cpanel::Hash::JSONable(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Hash::JSONable); } use Class::XSAccessor ( getters => { username => 'USER', }, ); sub adopt ( $class, $ref ) { return bless $ref, $class; } sub domains_ar ($self) { return [ $self->{'DOMAIN'}, @{ $self->{'DOMAINS'} } ]; } sub contact_emails_ar ($self) { return [ grep { length } @{$self}{ 'CONTACTEMAIL', 'CONTACTEMAIL2' } ]; } sub child_workloads ($self) { if (wantarray) { return if !$self->{'CHILD_WORKLOADS'}; return split( m<,>, $self->{'CHILD_WORKLOADS'}, -1 ); } return 0 if !$self->{'CHILD_WORKLOADS'}; return 1 + ( $self->{'CHILD_WORKLOADS'} =~ tr<,><> ); } 1; } # --- END Cpanel/Config/CpUser/Object.pm { # --- BEGIN Cpanel/SV.pm package Cpanel::SV; use strict; use warnings; sub untaint { return $_[0] unless ${^TAINT}; require # Cpanel::Static OK - we should not untaint variables as part of updatenow.static Taint::Util; Taint::Util::untaint( $_[0] ); return $_[0]; } 1; } # --- END Cpanel/SV.pm { # --- BEGIN Cpanel/Path/Normalize.pm package Cpanel::Path::Normalize; use strict; use warnings; sub normalize { my $uncleanpath = shift || return; my $is_abspath = ( 0 == index( $uncleanpath, '/' ) ); my @pathdirs = split( m[/], $uncleanpath ); my @cleanpathdirs; my $leading_dot_dots = 0; foreach my $dir (@pathdirs) { next if !length $dir; #Remove extraneous "//" and leading "/" next if $dir eq '.'; if ( $dir eq '..' ) { if (@cleanpathdirs) { pop(@cleanpathdirs); } else { $leading_dot_dots++; } } else { push( @cleanpathdirs, $dir ); } } if ($is_abspath) { return ( '/' . join( '/', @cleanpathdirs ) ); } unshift @cleanpathdirs, ('..') x $leading_dot_dots; return join( '/', @cleanpathdirs ); } 1; } # --- END Cpanel/Path/Normalize.pm { # --- BEGIN Cpanel/Hash/Stringify.pm package Cpanel::Hash::Stringify; use strict; use warnings; sub sorted_hashref_string { my ($hashref) = @_; return ( ( scalar keys %$hashref ) ? join( '_____', map { $_, ( ref $hashref->{$_} eq 'HASH' ? sorted_hashref_string( $hashref->{$_} ) : ref $hashref->{$_} eq 'ARRAY' ? join( '_____', @{ $hashref->{$_} } ) : defined $hashref->{$_} ? $hashref->{$_} : '' ) } sort keys %$hashref ) : '' ); #sort is important for order; } 1; } # --- END Cpanel/Hash/Stringify.pm { # --- BEGIN Cpanel/Umask.pm package Cpanel::Umask; use strict; # use Cpanel::Finally(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Finally); } sub new { my ( $class, $new ) = @_; my $old = umask(); umask($new); return $class->SUPER::new( sub { my $cur = umask(); if ( $cur != $new ) { my ( $cur_o, $old_o, $new_o ) = map { '0' . sprintf( '%o', $_ ) } ( $cur, $old, $new ); warn "I want to umask($old_o). I expected the current umask to be $new_o, but it’s actually $cur_o."; } umask($old); } ); } 1; } # --- END Cpanel/Umask.pm { # --- BEGIN Cpanel/Config/LoadConfig.pm package Cpanel::Config::LoadConfig; use strict; use warnings; # use Cpanel::Hash::Stringify (); # use Cpanel::Debug (); # use Cpanel::FileUtils::Write::JSON::Lazy (); # use Cpanel::AdminBin::Serializer::FailOK (); # use Cpanel::LoadFile::ReadFast (); # use Cpanel::HiRes (); # use Cpanel::SV (); use constant _ENOENT => 2; my $logger; our $PRODUCT_CONF_DIR = '/var/cpanel'; our $_DEBUG_SAFEFILE = 0; my %COMMON_CACHE_NAMES = ( ':__^\s*[#;]____0__' => 'default_colon', ':\s+__^\s*[#;]____0__' => 'default_colon_any_space', ': __^\s*[#;]____0__' => 'default_colon_with_one_space', '=__^\s*[#;]____0__skip_readable_check_____1' => 'default_skip_readable', '=__^\s*[#;]____0__' => 'default', '=__^\s*[#;]__(?^:\s+)__0__' => 'default_with_preproc_newline', '=__^\s*[#;]____1__' => 'default_allow_undef', '\s*[:]\s*__^\s*[#;]____0__' => 'default_colon_before_after_space', '\s*=\s*__^\s*[#;]____1__' => 'default_equal_before_after_space_allow_undef', '\s*[\=]\s*__^\s*[#]____0__use_reverse_____0' => 'default_equal_before_after_space', ': __^\s*[#;]____0__limit_____10000000000_____use_reverse_____0' => 'default_with_10000000000_limit', '\s*[:]\s*__^\s*[#;]____0__use_hash_of_arr_refs_____0_____use_reverse_____0' => 'default_use_hash_of_arr_refs', ': __^\s*[#;]____0__limit__________use_reverse_____0' => 'default_colon_single_space_no_limit', ': __^\s*[#;]____1__skip_keys_____nobody_____use_hash_of_arr_refs_____0_____use_reverse_____0' => 'default_colon_skip_nobody_no_limit', ': __^\s*[#;]____1__use_reverse_____1' => 'default_reverse_allow_undef', '\s+__^\s*[#;]____0__' => 'default_space_seperated_config', '\s*=\s*__^\s*[#;]__^\s*__0__' => 'default_equal_space_seperated_config', #ea4.conf ); my $DEFAULT_DELIMITER = '='; my $DEFAULT_COMMENT_REGEXP = '^\s*[#;]'; #Keep in sync with tr{} below!! my @BOOLEAN_OPTIONS = qw( allow_undef_values use_hash_of_arr_refs use_reverse ); my $CACHE_DIR_PERMS = 0700; sub _process_parse_args { my (%opts) = @_; if ( !defined $opts{'delimiter'} ) { $opts{'delimiter'} = $DEFAULT_DELIMITER; } $opts{'regexp_to_preprune'} ||= q{}; $opts{'comment'} ||= $DEFAULT_COMMENT_REGEXP; $opts{'comment'} = '' if $opts{'comment'} eq '0E0'; $opts{$_} ||= 0 for @BOOLEAN_OPTIONS; return %opts; } { no warnings 'once'; *get_homedir_and_cache_dir = *_get_homedir_and_cache_dir; } sub _get_homedir_and_cache_dir { my ( $homedir, $cache_dir ); if ( $> == 0 ) { $cache_dir = "$PRODUCT_CONF_DIR/configs.cache"; } else { { no warnings 'once'; $homedir = $Cpanel::homedir; } if ( !$homedir ) { eval 'local $SIG{__DIE__}; local $SIG{__WARN__}; require Cpanel::PwCache'; ## no critic qw(ProhibitStringyEval) # PPI USE OK - just after $homedir = Cpanel::PwCache::gethomedir() if $INC{'Cpanel/PwCache.pm'}; return unless $homedir; # undef for homedir and cache_dir avoid issues later when using undef as hash key } Cpanel::SV::untaint($homedir); $homedir =~ tr{/}{}s; return ( $homedir, undef ) if $homedir eq '/'; if ( $ENV{'TEAM_USER'} ) { $cache_dir = "$homedir/$ENV{'TEAM_USER'}/.cpanel/caches/config"; } else { $cache_dir = "$homedir/.cpanel/caches/config"; } } return ( $homedir, $cache_dir ); } sub loadConfig { ## no critic qw(Subroutines::ProhibitExcessComplexity Subroutines::ProhibitManyArgs) my ( $file, $conf_ref, $delimiter, $comment, $regexp_to_preprune, $allow_undef_values, $arg_ref ) = @_; $conf_ref ||= -1; my %processed_positional_args = _process_parse_args( delimiter => $delimiter, comment => $comment, regexp_to_preprune => $regexp_to_preprune, allow_undef_values => $allow_undef_values, $arg_ref ? %$arg_ref : (), ); my $empty_is_invalid = ( defined $arg_ref ) ? delete $arg_ref->{'empty_is_invalid'} : undef; my ( $use_reverse, $use_hash_of_arr_refs ); ( $delimiter, $comment, $regexp_to_preprune, $allow_undef_values, $use_reverse, $use_hash_of_arr_refs ) = @processed_positional_args{ qw( delimiter comment regexp_to_preprune allow_undef_values use_reverse use_hash_of_arr_refs ) }; if ( !$file || $file =~ tr/\0// ) { _do_logger( 'warn', 'loadConfig requires valid filename' ); if ( $arg_ref->{'keep_locked_open'} ) { return ( undef, undef, undef, "loadConfig requires valid filename" ); } return; } my $filesys_mtime = ( Cpanel::HiRes::stat($file) )[9] or do { if ( $arg_ref->{'keep_locked_open'} ) { return ( undef, undef, undef, "Unable to stat $file: $!" ); } return; }; my $load_into_conf_ref = ( !ref $conf_ref && $conf_ref == -1 ) ? 0 : 1; if ($load_into_conf_ref) { $conf_ref = _hashify_ref($conf_ref); } my ( $homedir, $cache_dir ) = _get_homedir_and_cache_dir(); my $cache_file; Cpanel::AdminBin::Serializer::FailOK::LoadModule() if !$INC{'Cpanel/AdminBin/Serializer.pm'}; if ( $cache_dir && $INC{'Cpanel/JSON.pm'} && ( !defined $arg_ref || !ref $arg_ref || !exists $arg_ref->{'nocache'} && !$arg_ref->{'keep_locked_open'} ) ) { $cache_file = get_cache_file( 'file' => $file, 'cache_dir' => $cache_dir, 'delimiter' => $delimiter, 'comment' => $comment, 'regexp_to_preprune' => $regexp_to_preprune, 'allow_undef_values' => $allow_undef_values, 'arg_ref' => $arg_ref, ); my ( $cache_valid, $ref ) = load_from_cache_if_valid( 'file' => $file, 'cache_file' => $cache_file, 'filesys_mtime' => $filesys_mtime, 'conf_ref' => $conf_ref, 'load_into_conf_ref' => $load_into_conf_ref, 'empty_is_invalid' => $empty_is_invalid, ); if ($cache_valid) { return $ref; } } $conf_ref = {} if !$load_into_conf_ref; my $conf_fh; my $conflock; my $locked; if ( $arg_ref->{'keep_locked_open'} || $arg_ref->{'rw'} ) { require Cpanel::SafeFile; $locked = 1; $conflock = Cpanel::SafeFile::safeopen( $conf_fh, '+<', $file ); } else { $conflock = open( $conf_fh, '<', $file ); } if ( !$conflock ) { my $open_err = $! || '(unspecified error)'; local $_DEBUG_SAFEFILE = 1; require Cpanel::Logger; my $is_root = ( $> == 0 ? 1 : 0 ); if ( !$is_root && !$arg_ref->{'skip_readable_check'} ) { if ( !-r $file ) { my $msg; if ( my $err = $! ) { $msg = "$file’s readability check failed: $err"; } else { my $euser = getpwuid $>; $msg = "$file is not readable as $euser."; } _do_logger( 'warn', $msg ); if ( $arg_ref->{'keep_locked_open'} ) { return ( undef, undef, undef, $msg ); } return; } } my $verb = ( $locked ? 'lock/' : q<> ) . 'open'; my $msg = "Unable to $verb $file as UIDs $: $open_err"; Cpanel::Logger::cplog( $msg, 'warn', __PACKAGE__ ); if ( $arg_ref->{'keep_locked_open'} ) { return ( undef, undef, undef, $msg ); } return; } my ( $parse_ok, $parsed ) = _parse_from_filehandle( $conf_fh, comment => $comment, delimiter => $delimiter, regexp_to_preprune => $regexp_to_preprune, allow_undef_values => $allow_undef_values, use_reverse => $use_reverse, use_hash_of_arr_refs => $use_hash_of_arr_refs, $arg_ref ? %$arg_ref : (), ); if ( $locked && !$arg_ref->{'keep_locked_open'} ) { require Cpanel::SafeFile; Cpanel::SafeFile::safeclose( $conf_fh, $conflock ); } if ( !$parse_ok ) { require Cpanel::Logger; Cpanel::Logger::cplog( "Unable to parse $file: $parsed", 'warn', __PACKAGE__ ); if ( $arg_ref->{'keep_locked_open'} ) { return ( undef, undef, undef, "Unable to parse $file: $parsed" ); } return; } @{$conf_ref}{ keys %$parsed } = values %$parsed; if ($cache_file) { write_cache( 'cache_dir' => $cache_dir, 'cache_file' => $cache_file, 'homedir' => $homedir, 'is_root' => ( $> == 0 ? 1 : 0 ), 'data' => $parsed, ); } if ( $arg_ref->{'keep_locked_open'} ) { return $conf_ref, $conf_fh, $conflock, "open success"; } return $conf_ref; } sub load_from_cache_if_valid { my (%opts) = @_; my $cache_file = $opts{'cache_file'} or die "need cache_file!"; my $file = $opts{'file'}; my $conf_ref = $opts{'conf_ref'}; my $load_into_conf_ref = $opts{'load_into_conf_ref'}; my $filesys_mtime = $opts{'filesys_mtime'} || ( Cpanel::HiRes::stat($file) )[9]; open( my $cache_fh, '<:stdio', $cache_file ) or do { my $err = $!; my $msg = "non-fatal error: open($cache_file): $err"; warn $msg if $! != _ENOENT(); return ( 0, $msg ); }; my ( $cache_filesys_mtime, $now, $cache_conf_ref ) = ( ( Cpanel::HiRes::fstat($cache_fh) )[9], Cpanel::HiRes::time() ); # stat the file after we have it open to avoid a race condition if ( ( $Cpanel::Debug::level || 0 ) >= 5 ) { print STDERR __PACKAGE__ . "::loadConfig file:$file, cache_file:$cache_file, cache_filesys_mtime:$cache_filesys_mtime, filesys_mtime:$filesys_mtime, now:$now\n"; } if ( $filesys_mtime && _greater_with_same_precision( $cache_filesys_mtime, $filesys_mtime ) && _greater_with_same_precision( $now, $cache_filesys_mtime ) ) { if ( ( $Cpanel::Debug::level || 0 ) >= 5 ) { print STDERR __PACKAGE__ . "::loadConfig using cache_file:$cache_file\n"; } Cpanel::AdminBin::Serializer::FailOK::LoadModule() if !$INC{'Cpanel/AdminBin/Serializer.pm'}; if ( $cache_conf_ref = Cpanel::AdminBin::Serializer::FailOK::LoadFile($cache_fh) ) { #zero keys is a valid file still it may just be all comments or empty close($cache_fh); if ( $opts{'empty_is_invalid'} && scalar keys %$cache_conf_ref == 0 ) { return ( 0, 'Cache is empty' ); } my $ref_to_return; if ($load_into_conf_ref) { @{$conf_ref}{ keys %$cache_conf_ref } = values %$cache_conf_ref; $ref_to_return = $conf_ref; } else { $ref_to_return = $cache_conf_ref; } return ( 1, $ref_to_return ); } elsif ( ( $Cpanel::Debug::level || 0 ) >= 5 ) { print STDERR __PACKAGE__ . "::loadConfig failed to load cache_file:$cache_file\n"; } } else { if ( ( $Cpanel::Debug::level || 0 ) >= 5 ) { print STDERR __PACKAGE__ . "::loadConfig NOT using cache_file:$cache_file\n"; } } return ( 0, 'Cache not valid' ); } sub _greater_with_same_precision { my ( $float1, $float2 ) = @_; my ( $int1, $int2 ) = ( int($float1), int($float2) ); if ( $float1 == $int1 or $float2 == $int2 ) { return $int1 > $int2; } return $float1 > $float2; } sub get_cache_file { ## no critic qw(Subroutines::RequireArgUnpacking) - Args unpacked by _process_parse_args my %opts = _process_parse_args(@_); die 'need cache_dir!' if !$opts{'cache_dir'}; my $stringified_args = join( '__', @opts{qw(delimiter comment regexp_to_preprune allow_undef_values)}, ( scalar keys %{ $opts{'arg_ref'} } ? Cpanel::Hash::Stringify::sorted_hashref_string( $opts{'arg_ref'} ) : '' ) ); if ( ( $Cpanel::Debug::level || 0 ) >= 5 ) { # PPI NO PARSE - ok missing print STDERR __PACKAGE__ . "::loadConfig stringified_args[$stringified_args]\n"; } my $safe_filename = $opts{'file'}; $safe_filename =~ tr{/}{_}; return $opts{'cache_dir'} . '/' . $safe_filename . '___' . ( $COMMON_CACHE_NAMES{$stringified_args} || _get_fastest_hash($stringified_args) ); } sub _get_fastest_hash { require Cpanel::Hash; goto \&Cpanel::Hash::get_fastest_hash; } sub write_cache { my (%opts) = @_; my $cache_file = $opts{'cache_file'}; my $cache_dir = $opts{'cache_dir'}; my $homedir = $opts{'homedir'}; my $is_root = $opts{'is_root'}; my $parsed = $opts{'data'}; my @dirs = ($cache_dir); if ( !$is_root ) { if ( $ENV{'TEAM_USER'} ) { unshift @dirs, "$homedir/$ENV{'TEAM_USER'}", "$homedir/$ENV{'TEAM_USER'}/.cpanel", "$homedir/$ENV{'TEAM_USER'}/.cpanel/caches"; } else { unshift @dirs, "$homedir/.cpanel", "$homedir/.cpanel/caches"; } } foreach my $dir (@dirs) { Cpanel::SV::untaint($dir); chmod( $CACHE_DIR_PERMS, $dir ) or do { if ( $! == _ENOENT() ) { require Cpanel::Umask; my $umask = Cpanel::Umask->new(0); mkdir( $dir, $CACHE_DIR_PERMS ) or do { _do_logger( 'warn', "Failed to create dir “$dir”: $!" ); }; } else { _do_logger( 'warn', "chmod($dir): $!" ); } }; } my $wrote_ok = eval { Cpanel::FileUtils::Write::JSON::Lazy::write_file( $cache_file, $parsed, 0600 ) }; my $error = $@; $error ||= "Unknown error" if !defined $wrote_ok; if ($error) { _do_logger( 'warn', "Could not create cache file “$cache_file”: $error" ); unlink $cache_file; #outdated } if ( ( $Cpanel::Debug::level || 0 ) > 4 ) { # PPI NO PARSE - ok missing print STDERR __PACKAGE__ . "::loadConfig [lazy write cache file] [$cache_file] wrote_ok:[$wrote_ok]\n"; } return 1; } sub _do_logger { my ( $action, $msg ) = @_; require Cpanel::Logger; $logger ||= Cpanel::Logger->new(); return $logger->$action($msg); } sub parse_from_filehandle { my ( $conf_fh, %opts ) = @_; return _parse_from_filehandle( $conf_fh, _process_parse_args(%opts) ); } sub _parse_from_filehandle { my ( $conf_fh, %opts ) = @_; my ( $comment, $limit, $regexp_to_preprune, $delimiter, $allow_undef_values, $use_hash_of_arr_refs, $skip_keys, $use_reverse ) = @opts{ qw( comment limit regexp_to_preprune delimiter allow_undef_values use_hash_of_arr_refs skip_keys use_reverse ) }; my $conf_ref = {}; my $parser_code; my ( $k, $v ); ## no critic qw(Variables::ProhibitUnusedVariables) my $keys = 0; my $key_value_text = $use_reverse ? '1,0' : '0,1'; my $cfg_txt = ''; Cpanel::LoadFile::ReadFast::read_all_fast( $conf_fh, $cfg_txt ); my $has_cr = index( $cfg_txt, "\r" ) > -1 ? 1 : 0; _remove_comments_from_text( \$cfg_txt, $comment, \$has_cr ) if $cfg_txt && $comment; my $split_on = $has_cr ? '\r?\n' : '\n'; if ( !$limit && !$regexp_to_preprune && !$use_hash_of_arr_refs && length $delimiter ) { if ($allow_undef_values) { $parser_code = qq< \$conf_ref = { map { (split(m/> . $delimiter . qq . $split_on . qq; } else { $parser_code = ' $conf_ref = { map { ' . '($k,$v) = (split(m/' . $delimiter . '/, $_, 2))[' . $key_value_text . ']; ' . 'defined($v) ? ($k,$v) : () ' . '} split(/' . $split_on . '/, $cfg_txt ) }'; } } else { if ( ( $Cpanel::Debug::level || 0 ) > 4 ) { # PPI NO PARSE - ok if not there $limit ||= 0; print STDERR __PACKAGE__ . "::parse_from_filehandle [slow LoadConfig parser used] LIMIT:[!$limit] REGEXP_TO_DELETE[!$regexp_to_preprune] USE_HASH_OF_ARR_REFS[$use_hash_of_arr_refs)]\n"; } $parser_code = 'foreach (split(m/' . $split_on . '/, $cfg_txt)) {' . "\n" # . q{next if !length;} . "\n" # . ( $limit ? q{last if $keys++ == } . $limit . ';' : '' ) . "\n" . ( $regexp_to_preprune ? q{ s/} . $regexp_to_preprune . q{//g;} : '' ) . "\n" # . ( length $delimiter ? # ( q{( $k, $v ) = (split( /} . $delimiter . q{/, $_, 2 ))[} . $key_value_text . q{];} . "\n" . # ( !$allow_undef_values ? q{ next if !defined($v); } : '' ) . "\n" . # ( $use_hash_of_arr_refs ? q{ push @{ $conf_ref->{$k} }, $v; } : q{ $conf_ref->{$k} = $v; } ) . "\n" # ) : q{$conf_ref->{$_} = 1; } . "\n" ) . '};'; } $parser_code .= "; 1"; $parser_code =~ tr{\n}{\r}; ## no critic qw(Cpanel::TransliterationUsage) eval($parser_code) or do { ## no critic qw(BuiltinFunctions::ProhibitStringyEval) $parser_code =~ tr{\r}{\n}; ## no critic qw(Cpanel::TransliterationUsage) _do_logger( 'panic', "Failed to parse :: $parser_code: $@" ); return ( 0, "$@\n$parser_code" ); }; delete $conf_ref->{''} if !defined( $conf_ref->{''} ); if ($skip_keys) { my $skip_keys_ar; if ( ref $skip_keys eq 'ARRAY' ) { $skip_keys_ar = $skip_keys; } elsif ( ref $skip_keys eq 'HASH' ) { $skip_keys_ar = [ keys %$skip_keys ]; } else { return ( 0, 'skip_keys must be an ARRAY or HASH reference' ); } delete @{$conf_ref}{@$skip_keys_ar}; } return ( 1, $conf_ref ); } sub _hashify_ref { my $conf_ref = shift; if ( !defined($conf_ref) ) { $conf_ref = {}; return $conf_ref; } unless ( ref $conf_ref eq 'HASH' ) { if ( ref $conf_ref ) { require Cpanel::Logger; Cpanel::Logger::cplog( 'hashifying non-HASH reference', 'warn', __PACKAGE__ ); ${$conf_ref} = {}; $conf_ref = ${$conf_ref}; } else { require Cpanel::Logger; Cpanel::Logger::cplog( 'defined value encountered where reference expected', 'die', __PACKAGE__ ); } } return $conf_ref; } sub default_product_dir { $PRODUCT_CONF_DIR = shift if @_; return $PRODUCT_CONF_DIR; } sub _remove_comments_from_text { my ( $cfg_txt_sr, $comment, $has_cr_sr ) = @_; if ($$has_cr_sr) { $$cfg_txt_sr = join( "\n", grep ( !m/$comment/, split( m{\r?\n}, $$cfg_txt_sr ) ) ); $$has_cr_sr = 0; } elsif ( $comment eq $DEFAULT_COMMENT_REGEXP ) { if ( rindex( $$cfg_txt_sr, '#', 0 ) == 0 && index( $$cfg_txt_sr, "\n" ) > -1 ) { substr( $$cfg_txt_sr, 0, index( $$cfg_txt_sr, "\n" ) + 1, '' ); } $$cfg_txt_sr =~ s{$DEFAULT_COMMENT_REGEXP.*}{}omg if $$cfg_txt_sr =~ tr{#;}{}; } else { $$cfg_txt_sr =~ s{$comment.*}{}mg; } return 1; } 1; } # --- END Cpanel/Config/LoadConfig.pm { # --- BEGIN Cpanel/Config/LoadWwwAcctConf.pm package Cpanel::Config::LoadWwwAcctConf; use strict; use warnings; # use Cpanel::HiRes (); # use Cpanel::Path::Normalize (); # use Cpanel::Debug (); # use Cpanel::JSON::FailOK (); my $SYSTEM_CONF_DIR = '/etc'; my $wwwconf_cache; my $wwwconf_mtime = 0; my $has_serializer; our $wwwacctconf = "$SYSTEM_CONF_DIR/wwwacct.conf"; our $wwwacctconfshadow = "$SYSTEM_CONF_DIR/wwwacct.conf.shadow"; sub import { my $this = shift; if ( !exists $INC{'Cpanel/JSON.pm'} ) { Cpanel::JSON::FailOK::LoadJSONModule(); } if ( $INC{'Cpanel/JSON.pm'} ) { $has_serializer = 1; } return Exporter::import( $this, @_ ); } sub loadwwwacctconf { ## no critic qw(Subroutines::ProhibitExcessComplexity) if ( $INC{'Cpanel/JSON.pm'} ) { $has_serializer = 1; } #something else loaded it my $filesys_mtime = ( Cpanel::HiRes::stat($wwwacctconf) )[9]; return if !$filesys_mtime; if ( $filesys_mtime == $wwwconf_mtime && $wwwconf_cache ) { return wantarray ? %{$wwwconf_cache} : $wwwconf_cache; } my $wwwacctconf_cache = "$wwwacctconf.cache"; my $wwwacctconfshadow_cache = "$wwwacctconfshadow.cache"; my $is_root = $> ? 0 : 1; if ($has_serializer) { my $cache_file; my $cache_filesys_mtime; my $have_valid_cache = 1; if ( $is_root && -e $wwwacctconfshadow_cache ) { $cache_filesys_mtime = ( Cpanel::HiRes::stat($wwwacctconfshadow_cache) )[9]; #shadow cache's mtime my $shadow_file_mtime = ( Cpanel::HiRes::stat $wwwacctconfshadow )[9] || 0; if ( $shadow_file_mtime < $cache_filesys_mtime ) { $cache_file = $wwwacctconfshadow_cache; } else { #don't use shadow cache if shadow file is newer $have_valid_cache = undef; } } elsif ( -e $wwwacctconf_cache && !( $is_root && -r $wwwacctconfshadow ) ) { $cache_filesys_mtime = ( Cpanel::HiRes::stat $wwwacctconf_cache )[9]; #regular cache's mtime $cache_file = $wwwacctconf_cache; } else { $have_valid_cache = undef; } my $now = Cpanel::HiRes::time(); if ( $Cpanel::Debug::level >= 5 ) { print STDERR __PACKAGE__ . "::loadwwwacctconf cache_filesys_mtime = $cache_filesys_mtime , filesys_mtime: $filesys_mtime , now : $now\n"; } if ( $have_valid_cache && $cache_filesys_mtime > $filesys_mtime && $cache_filesys_mtime < $now ) { my $wwwconf_ref; if ( open( my $conf_fh, '<', $cache_file ) ) { $wwwconf_ref = Cpanel::JSON::FailOK::LoadFile($conf_fh); close($conf_fh); } if ( $wwwconf_ref && ( scalar keys %{$wwwconf_ref} ) > 0 ) { if ( $Cpanel::Debug::level >= 5 ) { print STDERR __PACKAGE__ . "::loadwwwconf file system cache hit\n"; } $wwwconf_cache = $wwwconf_ref; $wwwconf_mtime = $filesys_mtime; return wantarray ? %{$wwwconf_ref} : $wwwconf_ref; } } } my @configfiles; push @configfiles, $wwwacctconf; if ($is_root) { push @configfiles, $wwwacctconfshadow; } #shadow file must be last as the cache gets written for each file with all the files before it in it my $can_write_cache; if ( $is_root && $has_serializer ) { $can_write_cache = 1; } my %CONF = ( 'ADDR' => undef, 'CONTACTEMAIL' => undef, 'DEFMOD' => undef, 'ETHDEV' => undef, 'HOST' => undef, 'NS' => undef, 'NS2' => undef, ); require Cpanel::Config::LoadConfig; foreach my $configfile (@configfiles) { Cpanel::Config::LoadConfig::loadConfig( $configfile, \%CONF, '\s+', undef, undef, undef, { 'nocache' => 1 } ); foreach ( keys %CONF ) { $CONF{$_} =~ s{\s+$}{} if defined $CONF{$_}; } $CONF{'HOMEMATCH'} =~ s{/+$}{} if defined $CONF{'HOMEMATCH'}; # Remove trailing slashes $CONF{'HOMEDIR'} = Cpanel::Path::Normalize::normalize( $CONF{'HOMEDIR'} ) if defined $CONF{'HOMEDIR'}; if ($can_write_cache) { my $cache_file = $configfile . '.cache'; require Cpanel::FileUtils::Write::JSON::Lazy; Cpanel::FileUtils::Write::JSON::Lazy::write_file( $cache_file, \%CONF, ( $configfile eq $wwwacctconfshadow ) ? 0600 : 0644 ); } } $wwwconf_mtime = $filesys_mtime; $wwwconf_cache = \%CONF; return wantarray ? %CONF : \%CONF; } sub reset_mem_cache { ( $wwwconf_mtime, $wwwconf_cache ) = ( 0, undef ); } sub reset_has_serializer { $has_serializer = 0; } sub default_conf_dir { $SYSTEM_CONF_DIR = shift if @_; $wwwacctconf = "$SYSTEM_CONF_DIR/wwwacct.conf"; $wwwacctconfshadow = "$SYSTEM_CONF_DIR/wwwacct.conf.shadow"; return $SYSTEM_CONF_DIR; } sub reset_caches { my @cache_files = map { "$_.cache" } ( $wwwacctconf, $wwwacctconfshadow ); for my $cache_file (@cache_files) { unlink $cache_file if -e $cache_file; } reset_mem_cache(); return; } 1; } # --- END Cpanel/Config/LoadWwwAcctConf.pm { # --- BEGIN Cpanel/Conf.pm package Cpanel::Conf; # use Cpanel::Config::Constants (); my $cpanel_theme; my $webmail_theme; sub new { my ( $class, %opts ) = @_; my $self = {}; bless $self, $class; if ( exists $opts{'wwwacct'} && ref $opts{'wwwacct'} eq 'HASH' ) { $self->{'wwwacct'} = $opts{'wwwacct'}; } undef $cpanel_theme; undef $webmail_theme; return $self; } sub system_config_dir { my ($self) = @_; return '/etc'; } sub product_config_dir { my ($self) = @_; return '/var/cpanel'; } sub product_base_dir { my ($self) = @_; return '/usr/local/cpanel'; } sub whm_base_dir { my ($self) = @_; return $self->product_base_dir . '/whostmgr'; } sub cpanel_theme_dir { my ($self) = @_; return $self->product_base_dir . '/base/frontend'; } sub whm_theme_dir { my ($self) = @_; return $self->whm_base_dir . '/docroot/themes'; } sub whm_theme { my ($self) = @_; return 'x'; } sub account_creation_defaults { my ($self) = @_; if ( exists $self->{'wwwacct'} ) { my %wwwacct = %{ $self->{'wwwacct'} }; return \%wwwacct; } require Cpanel::Config::LoadWwwAcctConf; return Cpanel::Config::LoadWwwAcctConf::loadwwwacctconf(); } sub cpanel_theme { my ($self) = @_; return $cpanel_theme if defined $cpanel_theme; $cpanel_theme = $Cpanel::Config::Constants::DEFAULT_CPANEL_THEME; my $defaults = {}; $defaults = $self->account_creation_defaults(); if ( ref $defaults eq 'HASH' && $defaults->{'DEFMOD'} ) { $cpanel_theme = $defaults->{'DEFMOD'}; } return $cpanel_theme; } sub default_webmail_theme { my ($self) = @_; return $webmail_theme if defined $webmail_theme; $webmail_theme = $Cpanel::Config::Constants::DEFAULT_WEBMAIL_THEME; my $defaults = {}; $defaults = $self->account_creation_defaults(); if ( ref $defaults eq 'HASH' && $defaults->{'DEFMOD'} ) { $webmail_theme = $defaults->{'DEFMOD'}; } return $webmail_theme; } 1; } # --- END Cpanel/Conf.pm { # --- BEGIN Cpanel/Config/LoadCpUserFile.pm package Cpanel::Config::LoadCpUserFile; use strict; use warnings; use Try::Tiny; # use Cpanel::DB::Utils (); # use Cpanel::Exception (); # use Cpanel::FileUtils::Write::JSON::Lazy (); # use Cpanel::AdminBin::Serializer::FailOK (); # use Cpanel::Config::Constants (); # use Cpanel::Config::CpUser::Defaults (); # use Cpanel::Config::CpUser::Object (); # use Cpanel::ConfigFiles (); # use Cpanel::LoadFile::ReadFast (); # use Cpanel::SV (); our $VERSION = '0.82'; # DO NOT CHANGE THIS FROM A DECIMAL sub _cpuser_defaults { return @Cpanel::Config::CpUser::Defaults::DEFAULTS_KV; } my %should_never_be_on_disk = map { $_ => undef } qw( DBOWNER DOMAIN DOMAINS DEADDOMAINS HOMEDIRLINKS ); my $logger; sub load_or_die { return ( _load( $_[0], undef, if_missing => 'die' ) )[2]; } sub load_if_exists { return ( _load( $_[0], undef, if_missing => 'return' ) )[2] // undef; } sub load_file { my ($file) = @_; return parse_cpuser_file( _open_cpuser_file( '<', $file ) ); } sub _open_cpuser_file_locked { my ( $mode, $file ) = @_; local $!; my $cpuser_fh; require Cpanel::SafeFile; my $lock_obj = Cpanel::SafeFile::safeopen( $cpuser_fh, $mode, $file ) or do { die Cpanel::Exception::create( 'IO::FileOpenError', [ path => $file, error => $!, mode => $mode ] ); }; return ( $lock_obj, $cpuser_fh ); } sub _open_cpuser_file { my ( $mode, $file ) = @_; local $!; my $cpuser_fh; open( $cpuser_fh, $mode, $file ) or do { die Cpanel::Exception::create( 'IO::FileOpenError', [ path => $file, error => $!, mode => $mode ] ); }; return $cpuser_fh; } sub parse_cpuser_file { my ($cpuser_fh) = @_; my $buffer = ''; Cpanel::LoadFile::ReadFast::read_all_fast( $cpuser_fh, $buffer ); return parse_cpuser_file_buffer($buffer); } sub parse_cpuser_file_buffer { my ($buffer) = @_; my %cpuser = _cpuser_defaults(); my %DOMAIN_MAP; my %DEAD_DOMAIN_MAP; my %HOMEDIRLINKS_MAP; local ( $!, $_ ); foreach ( split( m{\n}, $buffer ) ) { next if index( $_, '#' ) > -1 && m/^\s*#/; my ( $key, $value ) = split( /\s*=/, $_, 2 ); if ( !defined $value || exists $should_never_be_on_disk{$key} ) { next; } elsif ( $key eq 'DNS' ) { $cpuser{'DOMAIN'} = lc $value; } elsif ( index( $key, 'DNS' ) == 0 && substr( $key, 3, 1 ) =~ tr{0-9}{} ) { $DOMAIN_MAP{ lc $value } = undef; } elsif ( index( $key, 'XDNS' ) == 0 && substr( $key, 4, 1 ) =~ tr{0-9}{} ) { $DEAD_DOMAIN_MAP{ lc $value } = undef; } elsif ( index( $key, 'HOMEDIRPATHS' ) == 0 && $key =~ m{ \A HOMEDIRPATHS \d* \z }xms ) { $HOMEDIRLINKS_MAP{$value} = undef; } else { $cpuser{$key} = $value; } } delete @DEAD_DOMAIN_MAP{ keys %DOMAIN_MAP }; delete $DOMAIN_MAP{ $cpuser{'DOMAIN'} }; if ($!) { die Cpanel::Exception::create( 'IO::FileReadError', [ error => $! ] ); } if ( exists $cpuser{'USER'} ) { $cpuser{'DBOWNER'} = Cpanel::DB::Utils::username_to_dbowner( $cpuser{'USER'} ); } if ( !length $cpuser{'RS'} ) { require Cpanel::Conf; my $cp_defaults = Cpanel::Conf->new(); $cpuser{'RS'} = $cp_defaults->cpanel_theme; } if ( !$cpuser{'LOCALE'} ) { $cpuser{'LOCALE'} = 'en'; $cpuser{'__LOCALE_MISSING'} = 1; } $cpuser{'DOMAINS'} = [ sort keys %DOMAIN_MAP ]; # Sorted here so they can be tested with TM::is_deeply $cpuser{'DEADDOMAINS'} = [ sort keys %DEAD_DOMAIN_MAP ]; # Sorted here so they can be tested with TM::is_deeply $cpuser{'HOMEDIRLINKS'} = [ sort keys %HOMEDIRLINKS_MAP ]; return _wrap_cpuser( \%cpuser ); } sub _wrap_cpuser { return Cpanel::Config::CpUser::Object->adopt(shift); } sub _logger { return $logger ||= do { require Cpanel::Logger; Cpanel::Logger->new(); }; } sub load { my ( $user, $opts ) = @_; my $cpuser = ( _load( $user, $opts ) )[2]; if ( !ref $cpuser ) { _logger()->warn( "Failed to load cPanel user file for '" . ( $user || '' ) . "'" ) unless $opts->{'quiet'}; return wantarray ? () : bless( {}, 'Cpanel::Config::CpUser::Object' ); } return wantarray ? %$cpuser : $cpuser; } sub _load_locked { my ($user) = @_; my ( $fh, $lock_fh, $cpuser ) = _load( $user, { lock => 1 } ); return unless $fh && $lock_fh && $cpuser; return { 'file' => $fh, 'lock' => $lock_fh, 'data' => $cpuser, }; } sub clear_cache { my ($user) = @_; return unlink "$Cpanel::ConfigFiles::cpanel_users.cache/$user"; } sub _load { ## no critic(Subroutines::ProhibitExcessComplexity) -- Refactoring this function is a project, not a bug fix my ( $user, $load_opts_ref, %internal_opts ) = @_; if ( !$user || $user =~ tr<> ) { #no eq '' needed as !$user covers this _logger()->warn("Invalid username (falsy or forbidden character) given to loadcpuserfile."); if ( $internal_opts{'if_missing'} ) { die Cpanel::Exception::create( 'UserNotFound', [ name => '' ] ); } return; } my ( $now, $has_serializer, $user_file, $user_cache_file ) = ( time(), #now ( exists $INC{'Cpanel/JSON.pm'} ? 1 : 0 ), #has_serializer $load_opts_ref->{'file'} || "$Cpanel::ConfigFiles::cpanel_users/$user", # user_file "$Cpanel::ConfigFiles::cpanel_users.cache/$user", # user_cache_file ); my ( $cpuid, $cpgid, $size, $mtime ) = ( stat($user_file) )[ 4, 5, 7, 9 ]; if ( not defined($size) and my $if_missing = $internal_opts{'if_missing'} ) { if ( $! == _ENOENT() ) { if ( $if_missing eq 'return' ) { return; } die Cpanel::Exception::create( 'UserNotFound', [ name => $user ] ); } die Cpanel::Exception->create( 'The system failed to find the file “[_1]” because of an error: [_2]', [ $user_file, $! ] ); } $mtime ||= 0; my $lock_fh; my $cpuser_fh; if ( $load_opts_ref->{'lock'} ) { my $mode = $mtime ? '+<' : '+>'; try { ( $lock_fh, $cpuser_fh ) = _open_cpuser_file_locked( $mode, $user_file ); } catch { if ( my $if_missing = $internal_opts{'if_missing'} ) { die $_ if $if_missing ne 'return'; } else { _logger()->warn($_); } }; return if !$lock_fh; } elsif ( !$size ) { if ( $user eq 'cpanel' ) { my $result = _load_cpanel_user(); _wrap_cpuser($result); return ( $cpuser_fh, $lock_fh, $result ); } else { _logger()->warn("User file '$user_file' is empty or non-existent.") unless $load_opts_ref->{'quiet'}; return; } } if ( $Cpanel::Debug::level && $Cpanel::Debug::level > 3 ) { # PPI NO PARSE - This doesn't need to be loaded _logger()->debug("load cPanel user file [$user]"); } if ($has_serializer) { Cpanel::SV::untaint($user_cache_file); # case CPANEL-11199 if ( open( my $cache_fh, '<:stdio', $user_cache_file ) ) { #ok if the file is not there my $cache_mtime = ( stat($cache_fh) )[9]; # Check the mtime after we have opened the file to prevent a race condition if ( $cache_mtime >= $mtime && $cache_mtime <= $now ) { my $cpuser_ref = Cpanel::AdminBin::Serializer::FailOK::LoadFile($cache_fh); if ( $cpuser_ref && ref $cpuser_ref eq 'HASH' ) { if ( $Cpanel::Debug::level && $Cpanel::Debug::level > 3 ) { # PPI NO PARSE - This doesn't need to be loaded _logger()->debug("load cache hit user[$user] now[$now] mtime[$mtime] cache_mtime[$cache_mtime]"); } $cpuser_ref->{'MTIME'} = $mtime; if ( ( $cpuser_ref->{'__CACHE_DATA_VERSION'} // 0 ) == $VERSION ) { _wrap_cpuser($cpuser_ref); return ( $cpuser_fh, $lock_fh, $cpuser_ref ); } else { unlink $user_cache_file; # force a re-cache of the latest data set } } } else { if ( $Cpanel::Debug::level && $Cpanel::Debug::level > 3 ) { # PPI NO PARSE - This doesn't need to be loaded _logger()->debug("load cache miss user[$user] now[$now] mtime[$mtime] cache_mtime[$cache_mtime]"); } } close($cache_fh); } else { if ( $Cpanel::Debug::level && $Cpanel::Debug::level > 3 ) { # PPI NO PARSE - This doesn't need to be loaded _logger()->debug("load cache miss user[$user] now[$now] mtime[$mtime] cache_mtime[0]"); } } } if ( !$lock_fh ) { try { $cpuser_fh = _open_cpuser_file( '<', $user_file ); } catch { die $_ if $internal_opts{'if_missing'}; _logger()->warn($_); }; return if !$cpuser_fh; } my $cpuser_hr; try { $cpuser_hr = parse_cpuser_file($cpuser_fh); } catch { _logger()->warn("Failed to read “$user_file”: $_"); }; return if !$cpuser_hr; $cpuser_hr->{'USER'} = $user; $cpuser_hr->{'DBOWNER'} = Cpanel::DB::Utils::username_to_dbowner($user); $cpuser_hr->{'__CACHE_DATA_VERSION'} = $VERSION; # set this before the cache is written so that it will be included in the cache if ( $> == 0 ) { create_users_cache_dir(); if ( $has_serializer && Cpanel::FileUtils::Write::JSON::Lazy::write_file( $user_cache_file, $cpuser_hr, 0640 ) ) { chown 0, $cpgid, $user_cache_file if $cpgid; # this is ok if the chown happens after as we fall though to reading the non-cache on a failed open } else { unlink $user_cache_file; #outdated } } $cpuser_hr->{'MTIME'} = ( stat($cpuser_fh) )[9]; if ( $load_opts_ref->{'lock'} ) { seek( $cpuser_fh, 0, 0 ); } else { if ($lock_fh) { require Cpanel::SafeFile; Cpanel::SafeFile::safeclose( $cpuser_fh, $lock_fh ); } $cpuser_fh = $lock_fh = undef; } return ( $cpuser_fh, $lock_fh, $cpuser_hr ); } sub loadcpuserfile { return load( $_[0] ); } sub _load_cpanel_user { my %cpuser = ( _cpuser_defaults(), 'DEADDOMAINS' => [], 'DOMAIN' => 'domain.tld', 'DOMAINS' => [], 'HASCGI' => 1, 'HOMEDIRLINKS' => [], 'LOCALE' => 'en', 'MAXADDON' => 'unlimited', 'MAXPARK' => 'unlimited', 'RS' => $Cpanel::Config::Constants::DEFAULT_CPANEL_THEME, 'USER' => 'cpanel', ); return wantarray ? %cpuser : \%cpuser; } sub create_users_cache_dir { my $uc = "$Cpanel::ConfigFiles::cpanel_users.cache"; if ( -f $uc || -l $uc ) { my $bad = "$uc.bad"; unlink $bad if -e $bad; rename $uc, $bad; } if ( !-e $uc ) { mkdir $uc; } return; } sub _ENOENT { return 2; } 1; } # --- END Cpanel/Config/LoadCpUserFile.pm { # --- BEGIN Cpanel/Config/HasCpUserFile.pm package Cpanel::Config::HasCpUserFile; use strict; use warnings; # use Cpanel::ConfigFiles (); sub has_cpuser_file { return 0 if !length $_[0] || $_[0] =~ tr{/\0}{}; return -e "$Cpanel::ConfigFiles::cpanel_users/$_[0]" && -s _; } sub has_readable_cpuser_file { my ($user) = @_; return unless defined $user and $user ne '' and $user !~ tr/\/\0//; return -e "$Cpanel::ConfigFiles::cpanel_users/$user" && -s _ && -r _; } 1; } # --- END Cpanel/Config/HasCpUserFile.pm { # --- BEGIN Cpanel/NSCD/Constants.pm package Cpanel::NSCD::Constants; use strict; our $NSCD_CONFIG_FILE = '/etc/nscd.conf'; our $NSCD_SOCKET = '/var/run/nscd/socket'; 1; } # --- END Cpanel/NSCD/Constants.pm { # --- BEGIN Cpanel/Socket/UNIX/Micro.pm package Cpanel::Socket::UNIX::Micro; use strict; my $MAX_PATH_LENGTH = 107; my $LITTLE_ENDIAN_TEMPLATE = 'vZ' . ( 1 + $MAX_PATH_LENGTH ); # x86_64 is always little endian my $AF_UNIX = 1; my $SOCK_STREAM = 1; sub connect { socket( $_[0], $AF_UNIX, $SOCK_STREAM, 0 ) or warn "socket(AF_UNIX, SOCK_STREAM): $!"; return connect( $_[0], micro_sockaddr_un( $_[1] ) ); } sub micro_sockaddr_un { if ( length( $_[0] ) > $MAX_PATH_LENGTH ) { my $excess = length( $_[0] ) - $MAX_PATH_LENGTH; die "“$_[0]” is $excess character(s) too long to be a path to a local socket ($MAX_PATH_LENGTH bytes maximum)!"; } return pack( 'va*', $AF_UNIX, $_[0] ) if 0 == rindex( $_[0], "\0", 0 ); return pack( $LITTLE_ENDIAN_TEMPLATE, # x86_64 is always little endian $AF_UNIX, $_[0], ); } sub unpack_sockaddr_un { return substr( $_[0], 2 ) if 2 == rindex( $_[0], "\0", 2 ); return ( unpack $LITTLE_ENDIAN_TEMPLATE, $_[0] )[1]; } 1; } # --- END Cpanel/Socket/UNIX/Micro.pm { # --- BEGIN Cpanel/NSCD/Check.pm package Cpanel::NSCD::Check; use strict; # use Cpanel::NSCD::Constants (); # use Cpanel::Socket::UNIX::Micro (); our $CACHE_TTL = 600; my $last_check_time = 0; my $nscd_is_running_cache; sub nscd_is_running { my $now = time(); if ( $last_check_time && $last_check_time + $CACHE_TTL > $now ) { return $nscd_is_running_cache; } $last_check_time = $now; my $socket; if ( Cpanel::Socket::UNIX::Micro::connect( $socket, $Cpanel::NSCD::Constants::NSCD_SOCKET ) ) { return ( $nscd_is_running_cache = 1 ); } return ( $nscd_is_running_cache = 0 ); } 1; } # --- END Cpanel/NSCD/Check.pm { # --- BEGIN Cpanel/PwCache/Helpers.pm package Cpanel::PwCache::Helpers; use strict; use warnings; my $skip_uid_cache = 0; sub no_uid_cache { $skip_uid_cache = 1; return } sub uid_cache { $skip_uid_cache = 0; return } sub skip_uid_cache { return $skip_uid_cache; } sub init { my ( $totie, $skip_uid_cache_value ) = @_; tiedto($totie); $skip_uid_cache = $skip_uid_cache_value; return; } { # debugging helpers sub confess { require Carp; return Carp::confess(@_) } sub cluck { require Carp; return Carp::cluck(@_) } } { # tie logic and cache my $pwcacheistied = 0; my $pwcachetie; sub istied { return $pwcacheistied } sub deinit { $pwcacheistied = 0; return; } sub tiedto { my $v = shift; if ( !defined $v ) { # get return $pwcacheistied ? $pwcachetie : undef; } else { # set $pwcacheistied = 1; $pwcachetie = $v; } return; } } { my $SYSTEM_CONF_DIR = '/etc'; my $PRODUCT_CONF_DIR = '/var/cpanel'; sub default_conf_dir { return $SYSTEM_CONF_DIR } sub default_product_dir { return $PRODUCT_CONF_DIR; } } 1; } # --- END Cpanel/PwCache/Helpers.pm { # --- BEGIN Cpanel/PwCache/Cache.pm package Cpanel::PwCache::Cache; use strict; use warnings; my %_cache; my %_homedir_cache; use constant get_cache => \%_cache; use constant get_homedir_cache => \%_homedir_cache; our $pwcache_inited = 0; my $PWCACHE_IS_SAFE = 1; sub clear { # clear all %_cache = (); %_homedir_cache = (); $pwcache_inited = 0; return; } sub remove_key { my ($pwkey) = @_; return delete $_cache{$pwkey}; } sub replace { my $h = shift; %_cache = %$h if ref $h eq 'HASH'; return; } sub is_safe { $PWCACHE_IS_SAFE = $_[0] if defined $_[0]; return $PWCACHE_IS_SAFE; } sub pwmksafecache { return if $PWCACHE_IS_SAFE; $_cache{$_}{'contents'}->[1] = 'x' for keys %_cache; $PWCACHE_IS_SAFE = 1; return; } 1; } # --- END Cpanel/PwCache/Cache.pm { # --- BEGIN Cpanel/PwCache/Find.pm package Cpanel::PwCache::Find; use strict; # use Cpanel::LoadFile::ReadFast (); our $PW_CHUNK_SIZE = 1 << 17; sub field_with_value_in_pw_file { my ( $passwd_fh, $field, $value, $lc_flag ) = @_; return if ( $value =~ tr{\x{00}-\x{1f}\x{7f}:}{} ); my $needle = $field == 0 ? "\n${value}:" : ":${value}"; my $haystack; my $match_pos = 0; my $line_start; my $line_end; my $not_eof; my $data = "\n"; while ( ( $not_eof = Cpanel::LoadFile::ReadFast::read_fast( $passwd_fh, $data, $PW_CHUNK_SIZE, length $data ) ) || length($data) > 1 ) { $haystack = $not_eof ? substr( $data, 0, rindex( $data, "\n" ), '' ) : $data; if ( $lc_flag && $lc_flag == 1 ) { $haystack = lc $haystack; $needle = lc $needle; } while ( -1 < ( $match_pos = index( $haystack, $needle, $match_pos ) ) ) { $line_start = ( !$field ? $match_pos : rindex( $haystack, "\n", $match_pos ) ) + 1; if ( !$field || ( $field == ( substr( $haystack, $line_start, $match_pos - $line_start + 1 ) =~ tr{:}{} ) && ( length($haystack) == $match_pos + length($needle) || substr( $haystack, $match_pos + length($needle), 1 ) =~ tr{:\n}{} ) ) ) { $line_end = index( $haystack, "\n", $match_pos + length($needle) ); my $line = substr( $haystack, $line_start, ( $line_end > -1 ? $line_end : length($haystack) ) - $line_start ); return split( ':', $line ); } $match_pos += length($needle); } last unless $not_eof; } return; } 1; } # --- END Cpanel/PwCache/Find.pm { # --- BEGIN Cpanel/PwCache/Build.pm package Cpanel::PwCache::Build; use strict; use warnings; # use Cpanel::Debug (); # use Cpanel::JSON::FailOK (); # use Cpanel::FileUtils::Write::JSON::Lazy (); # use Cpanel::PwCache::Helpers (); # use Cpanel::PwCache::Cache (); # use Cpanel::LoadFile::ReadFast (); my ( $MIN_FIELDS_FOR_VALID_ENTRY, $pwcache_has_uid_cache ) = ( 0, 6 ); sub pwmksafecache { return if Cpanel::PwCache::Cache::is_safe(); my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); $pwcache_ref->{$_}{'contents'}->[1] = 'x' for keys %{$pwcache_ref}; Cpanel::PwCache::Cache::is_safe(1); return; } sub pwclearcache { # also known as clear_this_process_cache $pwcache_has_uid_cache = undef; Cpanel::PwCache::Cache::clear(); return; } sub init_pwcache { Cpanel::PwCache::Cache::is_safe(0); return _build_pwcache(); } sub init_passwdless_pwcache { return _build_pwcache( 'nopasswd' => 1 ); } sub fetch_pwcache { init_passwdless_pwcache() unless pwcache_is_initted(); my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); if ( scalar keys %$pwcache_ref < 3 ) { die "The password cache unexpectedly had less than 3 entries"; } return [ map { $pwcache_ref->{$_}->{'contents'} } grep { substr( $_, 0, 1 ) eq '0' } keys %{$pwcache_ref} ]; } sub _write_json_cache { my ($cache_file) = @_; if ( !Cpanel::PwCache::Helpers::istied() && exists $INC{'Cpanel/JSON.pm'} ) { my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); if ( !ref $pwcache_ref || scalar keys %$pwcache_ref < 3 ) { die "The system failed build the password cache"; } Cpanel::FileUtils::Write::JSON::Lazy::write_file( $cache_file, $pwcache_ref, 0600 ); } return; } sub _write_tied_cache { my ( $crypted_passwd_ref, $passwdmtime, $hpasswdmtime ) = @_; my $SYSTEM_CONF_DIR = Cpanel::PwCache::Helpers::default_conf_dir(); local $!; if ( open( my $pwcache_passwd_fh, '<:stdio', "$SYSTEM_CONF_DIR/passwd" ) ) { local $/; my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); my $data = ''; Cpanel::LoadFile::ReadFast::read_all_fast( $pwcache_passwd_fh, $data ); die "The file “$SYSTEM_CONF_DIR/passwd” was unexpectedly empty" if !length $data; my @fields; my $skip_uid_cache = Cpanel::PwCache::Helpers::skip_uid_cache(); foreach my $line ( split( /\n/, $data ) ) { next unless length $line; @fields = split( /:/, $line ); next if scalar @fields < $MIN_FIELDS_FOR_VALID_ENTRY || $fields[0] =~ tr/[A-Z][a-z][0-9]._-//c; $pwcache_ref->{ '0:' . $fields[0] } = { 'cachetime' => $passwdmtime, 'hcachetime' => $hpasswdmtime, 'contents' => [ $fields[0], $crypted_passwd_ref->{ $fields[0] } || $fields[1], $fields[2], $fields[3], '', '', $fields[4], $fields[5], $fields[6], -1, -1, $passwdmtime, $hpasswdmtime ] }; next if $skip_uid_cache || !defined $fields[2] || exists $pwcache_ref->{ '2:' . $fields[2] }; $pwcache_ref->{ '2:' . $fields[2] } = $pwcache_ref->{ '0:' . $fields[0] }; } close($pwcache_passwd_fh); } else { die "The system failed to read $SYSTEM_CONF_DIR/passwd because of an error: $!"; } return; } sub _cache_ref_is_valid { my ( $cache_ref, $passwdmtime, $hpasswdmtime ) = @_; my @keys = qw/0:root 0:cpanel 0:bin/; return $cache_ref && ( scalar keys %{$cache_ref} ) > 2 && scalar @keys == grep { # $cache_ref->{$_}->{'hcachetime'} && $cache_ref->{$_}->{'hcachetime'} == $hpasswdmtime && $cache_ref->{$_}->{'cachetime'} && $cache_ref->{$_}->{'cachetime'} == $passwdmtime } @keys; } sub _build_pwcache { my %OPTS = @_; if ( $INC{'B/C.pm'} ) { Cpanel::PwCache::Helpers::confess("Cpanel::PwCache::Build::_build_pwcache cannot be run under B::C (see case 162857)"); } my $SYSTEM_CONF_DIR = Cpanel::PwCache::Helpers::default_conf_dir(); my ( $cache_file, $passwdmtime, $cache_file_mtime, $crypted_passwd_ref, $crypted_passwd_file, $hpasswdmtime ) = ( "$SYSTEM_CONF_DIR/passwd.cache", ( stat("$SYSTEM_CONF_DIR/passwd") )[9] ); if ( $OPTS{'nopasswd'} ) { $hpasswdmtime = ( stat("$SYSTEM_CONF_DIR/shadow") )[9]; $cache_file = "$SYSTEM_CONF_DIR/passwd" . ( Cpanel::PwCache::Helpers::skip_uid_cache() ? '.nouids' : '' ) . '.cache'; } elsif ( -r "$SYSTEM_CONF_DIR/shadow" ) { Cpanel::PwCache::Cache::is_safe(0); $hpasswdmtime = ( stat(_) )[9]; $crypted_passwd_file = "$SYSTEM_CONF_DIR/shadow"; $cache_file = "$SYSTEM_CONF_DIR/shadow" . ( Cpanel::PwCache::Helpers::skip_uid_cache() ? '.nouids' : '' ) . '.cache'; } else { $hpasswdmtime = 0; } if ( !Cpanel::PwCache::Helpers::istied() && exists $INC{'Cpanel/JSON.pm'} ) { if ( open( my $cache_fh, '<:stdio', $cache_file ) ) { my $cache_file_mtime = ( stat($cache_fh) )[9] || 0; if ( $cache_file_mtime > $hpasswdmtime && $cache_file_mtime > $passwdmtime ) { my $cache_ref = Cpanel::JSON::FailOK::LoadFile($cache_fh); Cpanel::Debug::log_debug("[read pwcache from $cache_file]") if ( $Cpanel::Debug::level > 3 ); if ( _cache_ref_is_valid( $cache_ref, $passwdmtime, $hpasswdmtime ) ) { Cpanel::Debug::log_debug("[validated pwcache from $cache_file]") if ( $Cpanel::Debug::level > 3 ); my $memory_pwcache_ref = Cpanel::PwCache::Cache::get_cache(); @{$cache_ref}{ keys %$memory_pwcache_ref } = values %$memory_pwcache_ref; Cpanel::PwCache::Cache::replace($cache_ref); $Cpanel::PwCache::Cache::pwcache_inited = ( $OPTS{'nopasswd'} ? 1 : 2 ); return; } } } } if ($crypted_passwd_file) { $crypted_passwd_ref = _load_pws($crypted_passwd_file); } $Cpanel::PwCache::Cache::pwcache_inited = ( $OPTS{'nopasswd'} ? 1 : 2 ); $pwcache_has_uid_cache = ( Cpanel::PwCache::Helpers::skip_uid_cache() ? 0 : 1 ); _write_tied_cache( $crypted_passwd_ref, $passwdmtime, $hpasswdmtime ); _write_json_cache($cache_file) if $> == 0; return 1; } sub pwcache_is_initted { return ( $Cpanel::PwCache::Cache::pwcache_inited ? $Cpanel::PwCache::Cache::pwcache_inited : 0 ); } sub _load_pws { my $lookup_file = shift; if ( $INC{'B/C.pm'} ) { Cpanel::PwCache::Helpers::confess("Cpanel::PwCache::Build::_load_pws cannot be run under B::C (see case 162857)"); } my %PW; if ( open my $lookup_fh, '<:stdio', $lookup_file ) { my $data = ''; Cpanel::LoadFile::ReadFast::read_all_fast( $lookup_fh, $data ); die "The file “$lookup_file” was unexpectedly empty" if !length $data; %PW = map { ( split(/:/) )[ 0, 1 ] } split( /\n/, $data ); if ( index( $data, '#' ) > -1 ) { delete @PW{ '', grep { index( $_, '#' ) == 0 } keys %PW }; } else { delete $PW{''}; } close $lookup_fh; } return \%PW; } 1; } # --- END Cpanel/PwCache/Build.pm { # --- BEGIN Cpanel/PwCache.pm package Cpanel::PwCache; use strict; # use Cpanel::Debug (); # use Cpanel::NSCD::Check (); # use Cpanel::PwCache::Helpers (); # use Cpanel::PwCache::Cache (); # use Cpanel::PwCache::Find (); use constant DUMMY_PW_RETURNS => ( -1, -1, 0, 0 ); use constant DEBUG => 0; # Must set $ENV{'CPANEL_DEBUG_LEVEL'} = 5 as well our $VERSION = '4.2'; my %FIXED_KEYS = ( '0:root' => 1, '0:nobody' => 1, '0:cpanel' => 1, '0:cpanellogin' => 1, '0:mail' => 1, '2:0' => 1, '2:99' => 1 ); our $_WANT_ENCRYPTED_PASSWORD; sub getpwnam_noshadow { $_WANT_ENCRYPTED_PASSWORD = 0; goto &_getpwnam; } sub getpwuid_noshadow { $_WANT_ENCRYPTED_PASSWORD = 0; goto &_getpwuid; } sub getpwnam { $_WANT_ENCRYPTED_PASSWORD = !!wantarray; goto &_getpwnam; } sub getpwuid { $_WANT_ENCRYPTED_PASSWORD = !!wantarray; goto &_getpwuid; } sub gethomedir { my $uid_or_name = $_[0] // $>; my $hd = Cpanel::PwCache::Cache::get_homedir_cache(); unless ( exists $hd->{$uid_or_name} ) { $_WANT_ENCRYPTED_PASSWORD = 0; if ( $uid_or_name !~ tr{0-9}{}c ) { $hd->{$uid_or_name} = ( _getpwuid($uid_or_name) )[7]; } else { $hd->{$uid_or_name} = ( _getpwnam($uid_or_name) )[7]; } } return $hd->{$uid_or_name}; } sub getusername { my $uid = defined $_[0] ? $_[0] : $>; $_WANT_ENCRYPTED_PASSWORD = 0; return scalar _getpwuid($uid); } sub init_passwdless_pwcache { require Cpanel::PwCache::Build; *init_passwdless_pwcache = \&Cpanel::PwCache::Build::init_passwdless_pwcache; goto &Cpanel::PwCache::Build::init_passwdless_pwcache; } sub _getpwuid { ## no critic qw(Subroutines::RequireArgUnpacking) return unless ( length( $_[0] ) && $_[0] !~ tr/0-9//c ); my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); if ( !exists $pwcache_ref->{"2:$_[0]"} && $> != 0 && !Cpanel::PwCache::Helpers::istied() && Cpanel::NSCD::Check::nscd_is_running() ) { return CORE::getpwuid( $_[0] ) if !wantarray; my @ret = CORE::getpwuid( $_[0] ); return @ret ? ( @ret, DUMMY_PW_RETURNS() ) : (); } if ( my $pwref = _pwfunc( $_[0], 2 ) ) { return wantarray ? @$pwref : $pwref->[0]; } return; #important not to return 0 } sub _getpwnam { ## no critic qw(Subroutines::RequireArgUnpacking) return unless ( length( $_[0] ) && $_[0] !~ tr{\x{00}-\x{20}\x{7f}:/#}{} ); my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); if ( !exists $pwcache_ref->{"0:$_[0]"} && $> != 0 && !Cpanel::PwCache::Helpers::istied() && Cpanel::NSCD::Check::nscd_is_running() ) { return CORE::getpwnam( $_[0] ) if !wantarray; my @ret = CORE::getpwnam( $_[0] ); return @ret ? ( @ret, DUMMY_PW_RETURNS() ) : (); } if ( my $pwref = _pwfunc( $_[0], 0 ) ) { return wantarray ? @$pwref : $pwref->[2]; } return; #important not to return 0 } sub _pwfunc { ## no critic qw(Subroutines::RequireArgUnpacking) my ( $value, $field, $pwkey ) = ( $_[0], ( $_[1] || 0 ), $_[1] . ':' . ( $_[0] || 0 ) ); if ( Cpanel::PwCache::Helpers::istied() ) { Cpanel::Debug::log_debug("cache tie (tied) value[$value] field[$field]") if (DEBUG); my $pwcachetie = Cpanel::PwCache::Helpers::tiedto(); if ( ref $pwcachetie eq 'HASH' ) { my $cache = $pwcachetie->{$pwkey}; if ( ref $cache eq 'HASH' ) { return $pwcachetie->{$pwkey}->{'contents'}; } } return undef; } my $SYSTEM_CONF_DIR = Cpanel::PwCache::Helpers::default_conf_dir(); my $lookup_encrypted_pass = 0; if ($_WANT_ENCRYPTED_PASSWORD) { $lookup_encrypted_pass = $> == 0 ? 1 : 0; } my ( $passwdmtime, $hpasswdmtime ); my $pwcache_ref = Cpanel::PwCache::Cache::get_cache(); if ( my $cache_entry = $pwcache_ref->{$pwkey} ) { Cpanel::Debug::log_debug("exists in cache value[$value] field[$field]") if (DEBUG); if ( ( exists( $cache_entry->{'contents'} ) && $cache_entry->{'contents'}->[1] ne 'x' ) # Has shadow entry || !$lookup_encrypted_pass # Or we do not need it ) { # If we are root and missing the password field we could fail authentication if ( $FIXED_KEYS{$pwkey} ) { # We assume root, nobody, and cpanellogin will never change during execution Cpanel::Debug::log_debug("cache (never change) hit value[$value] field[$field]") if (DEBUG); return $cache_entry->{'contents'}; } $passwdmtime = ( stat("$SYSTEM_CONF_DIR/passwd") )[9]; $hpasswdmtime = $lookup_encrypted_pass ? ( stat("$SYSTEM_CONF_DIR/shadow") )[9] : 0; if ( ( $lookup_encrypted_pass && $hpasswdmtime && $hpasswdmtime != $cache_entry->{'hcachetime'} ) || ( $passwdmtime && $passwdmtime != $cache_entry->{'cachetime'} ) ) { #timewarp safe DEBUG && Cpanel::Debug::log_debug( "cache miss value[$value] field[$field] pwkey[$pwkey] " . qq{hpasswdmtime: $hpasswdmtime != $cache_entry->{hcachetime} } . qq{passwdmtime: $passwdmtime != $cache_entry->{cachetime} } ); if ( defined $cache_entry && defined $cache_entry->{'contents'} ) { Cpanel::PwCache::Cache::clear(); #If the passwd file mtime changes everything is invalid } } else { Cpanel::Debug::log_debug("cache hit value[$value] field[$field]") if (DEBUG); return $cache_entry->{'contents'}; } } elsif (DEBUG) { Cpanel::Debug::log_debug( "cache miss pwkey[$pwkey] value[$value] field[$field] passwdmtime[$passwdmtime] pwcacheistied[" . Cpanel::PwCache::Helpers::istied() . "] hpasswdmtime[$hpasswdmtime]" ); } } elsif (DEBUG) { Cpanel::Debug::log_debug( "cache miss (no entry) pwkey[$pwkey] value[$value] field[$field] pwcacheistied[" . Cpanel::PwCache::Helpers::istied() . "]" ); } my $pwdata = _getpwdata( $value, $field, $passwdmtime, $hpasswdmtime, $lookup_encrypted_pass ); _cache_pwdata( $pwdata, $pwcache_ref ) if $pwdata && @$pwdata; return $pwdata; } sub _getpwdata { my ( $value, $field, $passwdmtime, $shadowmtime, $lookup_encrypted_pass ) = @_; return if ( !defined $value || !defined $field || $value =~ tr/\0// ); if ($lookup_encrypted_pass) { return [ _readshadow( $value, $field, $passwdmtime, $shadowmtime ) ]; } return [ _readpasswd( $value, $field, $passwdmtime, $shadowmtime ) ]; } sub _readshadow { ## no critic qw(Subroutines::RequireArgUnpacking) my $SYSTEM_CONF_DIR = Cpanel::PwCache::Helpers::default_conf_dir(); my ( $value, $field, $passwdmtime, $shadowmtime ) = ( $_[0], ( $_[1] || 0 ), ( $_[2] || ( stat("$SYSTEM_CONF_DIR/passwd") )[9] ), ( $_[3] || ( stat("$SYSTEM_CONF_DIR/shadow") )[9] ) ); my @PW = _readpasswd( $value, $field, $passwdmtime, $shadowmtime ); return if !@PW; $value = $PW[0]; if ( open my $shadow_fh, '<', "$SYSTEM_CONF_DIR/shadow" ) { if ( my @SH = Cpanel::PwCache::Find::field_with_value_in_pw_file( $shadow_fh, 0, $value ) ) { ( $PW[1], $PW[9], $PW[10], $PW[11], $PW[12] ) = ( $SH[1], #encrypted pass $SH[5], #expire time $SH[2], #change time $passwdmtime, $shadowmtime ); close $shadow_fh; Cpanel::PwCache::Cache::is_safe(0); return @PW; } } else { Cpanel::PwCache::Helpers::cluck("Unable to open $SYSTEM_CONF_DIR/shadow: $!"); } Cpanel::PwCache::Helpers::cluck("Entry for $value missing in $SYSTEM_CONF_DIR/shadow"); return @PW; } sub _readpasswd { ## no critic qw(Subroutines::RequireArgUnpacking) my $SYSTEM_CONF_DIR = Cpanel::PwCache::Helpers::default_conf_dir(); my ( $value, $field, $passwdmtime, $shadowmtime, $block ) = ( $_[0], ( $_[1] || 0 ), ( $_[2] || ( stat("$SYSTEM_CONF_DIR/passwd") )[9] ), $_[3] ); if ( $INC{'B/C.pm'} ) { die("Cpanel::PwCache::_readpasswd cannot be run under B::C (see case 162857)"); } if ( open( my $passwd_fh, '<', "$SYSTEM_CONF_DIR/passwd" ) ) { if ( my @PW = Cpanel::PwCache::Find::field_with_value_in_pw_file( $passwd_fh, $field, $value ) ) { return ( $PW[0], $PW[1], $PW[2], $PW[3], '', '', $PW[4], $PW[5], $PW[6], -1, -1, $passwdmtime, ( $shadowmtime || $passwdmtime ) ); } close($passwd_fh); } else { Cpanel::PwCache::Helpers::cluck("open($SYSTEM_CONF_DIR/passwd): $!"); } return; } sub _cache_pwdata { my ( $pwdata, $pwcache_ref ) = @_; $pwcache_ref ||= Cpanel::PwCache::Cache::get_cache(); if ( $pwdata->[2] != 0 || $pwdata->[0] eq 'root' ) { # special case for multiple uid 0 users @{ $pwcache_ref->{ '2' . ':' . $pwdata->[2] } }{ 'cachetime', 'hcachetime', 'contents' } = ( $pwdata->[11], $pwdata->[12], $pwdata ); } @{ $pwcache_ref->{ '0' . ':' . $pwdata->[0] } }{ 'cachetime', 'hcachetime', 'contents' } = ( $pwdata->[11], $pwdata->[12], $pwdata ); return 1; } 1; } # --- END Cpanel/PwCache.pm { # --- BEGIN Cpanel/Locale/Utils/User.pm package Cpanel::Locale::Utils::User; use strict; # use Cpanel::Config::LoadCpUserFile (); # use Cpanel::Config::HasCpUserFile (); # use Cpanel::PwCache (); # use Cpanel::LoadModule (); our $DATASTORE_MODULE = 'Cpanel::DataStore'; our $LOCALE_LEGACY_MODULE = 'Cpanel::Locale::Utils::Legacy'; my $inited_cpdata_user; my $userlocale = {}; my $logger; sub _logger { require Cpanel::Logger; return ( $logger ||= Cpanel::Logger->new() ); } sub init_cpdata_keys { my $user = shift || $Cpanel::user || $ENV{'REMOTE_USER'} || ( $> == 0 ? 'root' : ( Cpanel::PwCache::getpwuid($>) )[0] ); return if ( defined $inited_cpdata_user && $inited_cpdata_user eq $user ); if ( !$Cpanel::CPDATA{'LOCALE'} && $user ne 'root' ) { require Cpanel::Server::Utils; if ( Cpanel::Server::Utils::is_subprocess_of_cpsrvd() && ( $> && $user ne 'cpanel' && $user ne 'cpanellogin' && !-e "/var/cpanel/users/$user" ) ) { _logger()->panic("get_handle() called before initcp()"); } if ( $> == 0 || ( $> && $> == Cpanel::PwCache::getpwnam($user) ) ) { $Cpanel::CPDATA{'LOCALE'} = get_user_locale($user); } } return ( $inited_cpdata_user = $user ); } sub clear_user_cache { my ($user) = @_; return delete $userlocale->{$user}; } sub get_user_locale { my $user = ( shift || $Cpanel::user || $ENV{'REMOTE_USER'} || ( $> == 0 ? 'root' : ( Cpanel::PwCache::getpwuid($>) )[0] ) ); my $cpuser_ref = shift; # not required, just faster if it is passed if ( !$user ) { require Cpanel::Locale; return Cpanel::Locale::get_server_locale() || 'en'; } return $userlocale->{$user} if exists $userlocale->{$user} && !shift; if ( $Cpanel::user && $user eq $Cpanel::user && $Cpanel::CPDATA{'LOCALE'} ) { return ( $userlocale->{$user} = $Cpanel::CPDATA{'LOCALE'} ); } my $locale; if ( $user eq 'root' ) { my $root_conf_yaml = ( Cpanel::PwCache::getpwnam('root') )[7] . '/.cpanel_config'; if ( -e $root_conf_yaml ) { Cpanel::LoadModule::load_perl_module($DATASTORE_MODULE); my $hr = $DATASTORE_MODULE->can('fetch_ref')->($root_conf_yaml); $locale = $hr->{'locale'}; } } elsif ( $user eq 'cpanel' ) { require Cpanel::Locale; $locale = Cpanel::Locale::get_locale_for_user_cpanel(); } else { if ( $cpuser_ref || ( Cpanel::Config::HasCpUserFile::has_readable_cpuser_file($user) && ( $cpuser_ref = Cpanel::Config::LoadCpUserFile::loadcpuserfile($user) ) ) ) { if ( defined $cpuser_ref->{'LOCALE'} ) { $locale = $cpuser_ref->{'LOCALE'}; } elsif ( defined $cpuser_ref->{'LANG'} ) { Cpanel::LoadModule::load_perl_module($LOCALE_LEGACY_MODULE); $locale = $LOCALE_LEGACY_MODULE->can('map_any_old_style_to_new_style')->( $cpuser_ref->{'LANG'} ); } } } if ( !$locale ) { require Cpanel::Locale; return $userlocale->{$user} = Cpanel::Locale::get_server_locale() || 'en'; } $userlocale->{$user} = $locale; return $userlocale->{$user}; } 1; } # --- END Cpanel/Locale/Utils/User.pm { # --- BEGIN Cpanel/Cookies.pm package Cpanel::Cookies; $Cpanel::Cookies::VERSION = '0.1'; sub get_cookie_hashref_from_string { return {} if !defined $_[0]; return { map { map { s/%([a-fA-F0-9][a-fA-F0-9])/pack('C', hex($1))/eg if -1 != index( $_, '%' ); $_; } split m<=>, $_, 2 } split( /; /, $_[0] ) }; } my $http_cookie_cached; sub get_cookie_hashref { if ( !defined $http_cookie_cached ) { $http_cookie_cached = get_cookie_hashref_from_string( $ENV{'HTTP_COOKIE'} ); } return $http_cookie_cached; } sub get_cookie_hashref_recache { $http_cookie_cached = get_cookie_hashref_from_string( $ENV{'HTTP_COOKIE'} ); return $http_cookie_cached; } 1; } # --- END Cpanel/Cookies.pm { # --- BEGIN Cpanel/SafeDir/Read.pm package Cpanel::SafeDir::Read; use strict; use warnings; sub read_dir { my ( $dir, $coderef ) = @_; my @contents; if ( opendir my $dir_dh, $dir ) { @contents = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); if ($coderef) { @contents = grep { $coderef->($_) } @contents; } closedir $dir_dh; return wantarray ? @contents : \@contents; } return; } 1; } # --- END Cpanel/SafeDir/Read.pm { # --- BEGIN Cpanel/ArrayFunc/Uniq.pm package Cpanel::ArrayFunc::Uniq; use cPstrict; sub uniq (@list) { if ( $INC{'List/Util.pm'} ) { no warnings 'redefine'; *uniq = *List::Util::uniq; return List::Util::uniq(@list); } my %seen; return grep { !$seen{$_}++ } @list; } 1; } # --- END Cpanel/ArrayFunc/Uniq.pm { # --- BEGIN Cpanel/Locale/Utils/Charmap.pm package Cpanel::Locale::Utils::Charmap; use cPstrict; # use Cpanel::ArrayFunc::Uniq (); sub get_charmap_list ( $root_says_to_make_symlinks = 0, $no_aliases = 0 ) { ## no critic(Subroutines::ProhibitManyArgs) my $args = { 'iconv' => 0, 'unpreferred_aliases' => ( $no_aliases ? 0 : 1 ) }; if ($root_says_to_make_symlinks) { make_symlinks(); } return @{ get_charmaps($args) }; } sub get_charmaps ( $args = {} ) { _validate_args( $args, { map { $_ => 1 } qw( iconv unpreferred_aliases ) } ); my ( $iconv, $unpreferred_aliases ) = @{$args}{ 'iconv', 'unpreferred_aliases' }; $iconv //= 1; # Provide iconv compatibility by default. my %charset_aliases = _get_charset_aliases(); my %excluded_charmaps = _get_excluded_charmaps( $iconv, $unpreferred_aliases ); my @raw_charmaps = ( qw(utf-8 us-ascii), _get_filesystem_charmaps(), ( $unpreferred_aliases ? %charset_aliases : ( values %charset_aliases ) ) ); my %charmaps; for my $cm (@raw_charmaps) { $cm =~ tr{A-Z}{a-z}; my $copy = $cm; my $stripped = ( $copy =~ tr{_.-}{}d ); #prefer "utf-8" over "utf8" if ( !exists( $excluded_charmaps{$cm} ) && ( !exists( $charmaps{$copy} ) || $stripped ) ) { $charmaps{$copy} = $cm; } } return [ sort ( Cpanel::ArrayFunc::Uniq::uniq( values %charmaps ) ) ]; } sub make_symlinks { return unless $> == 0; my %charset_aliases = _get_charset_aliases(); my $charmapsdir = _get_charmaps_dir(); for my $loop ( 1 .. 2 ) { for my $key ( keys %charset_aliases ) { lstat("$charmapsdir/$key.gz"); # unpreferred if ( -e _ ) { lstat("$charmapsdir/$charset_aliases{$key}.gz"); # preferred if ( !-e _ && !-l _ ) { symlink( "$charmapsdir/$key.gz", "$charmapsdir/$charset_aliases{$key}.gz" ); # unpreferred -> preferred } } elsif ( !-l _ && -e "$charmapsdir/$charset_aliases{$key}.gz" ) { # preferred symlink( "$charmapsdir/$charset_aliases{$key}.gz", "$charmapsdir/$key.gz" ); # preferred -> unpreferred } } } return 1; } sub _validate_args ( $args, $possible_args ) { if ( my @bad_args = grep { !$possible_args->{$_} } keys %{$args} ) { require Cpanel::Exception; die Cpanel::Exception::create_raw( 'InvalidParameters', 'The following arguments are invalid: ' . join ', ', @bad_args ); } } sub _get_charmaps_dir { state $charmaps_dir = -e '/usr/local/share/i18n/charmaps' ? '/usr/local/share/i18n/charmaps' : '/usr/share/i18n/charmaps'; return $charmaps_dir; } sub _get_charset_aliases { return ( # unpreferred => preferred 'ASCII' => 'US-ASCII', 'BIG5-ETEN' => 'BIG5', 'CP1251' => 'WINDOWS-1251', 'CP1252' => 'WINDOWS-1252', 'CP936' => 'GBK', 'CP949' => 'KS_C_5601-1987', # Note: same preferred as KS_C_5601 'EUC-CN' => 'GB2312', 'KS_C_5601' => 'KS_C_5601-1987', # Note: same preferred as CP949 'SHIFTJIS' => 'SHIFT_JIS', 'SHIFTJISX0213' => 'SHIFT_JISX0213', 'UNICODE-1-1-UTF-7' => 'UTF-7', # RFC 1642 (obs.) 'UTF8' => 'UTF-8', 'UTF-8-STRICT' => 'UTF-8', # Perl internal use 'HZ' => 'HZ-GB-2312', # RFC 1842 'GSM0338' => 'GSM03.38', ); } sub _get_iconv_blacklist { return ( 'big5-eten', 'bs_viewdata', 'csa_z243.4-1985-gr', 'gsm03.38', 'gsm0338', 'hz', 'hz-gb-2312', 'invariant', 'iso_10646', 'iso_646.basic', 'iso_646.irv', 'iso_6937-2-25', 'iso_6937-2-add', 'iso_8859-1,gl', 'iso_8859-supp', 'jis_c6220-1969-jp', 'jis_c6229-1984-a', 'jis_c6229-1984-b-add', 'jis_c6229-1984-hand', 'jis_c6229-1984-hand-add', 'jis_c6229-1984-kana', 'jis_x0201', 'jus_i.b1.003-mac', 'jus_i.b1.003-serb', 'ks_c_5601', 'ks_c_5601-1987', 'nats-dano-add', 'nats-sefi-add', 'nextstep', 'sami', 'sami-ws2', 't.101-g2', 't.61-7bit', 'unicode-1-1-utf-7', 'utf-8-strict', 'videotex-suppl', ); } sub _get_filesystem_charmaps { state @filesystem_charmaps; return @filesystem_charmaps if @filesystem_charmaps; my $charmapsdir = _get_charmaps_dir(); if ( opendir my $charmaps_dh, $charmapsdir ) { @filesystem_charmaps = map { m{\A([^.].*)[.]gz\z}xms ? $1 : () } readdir $charmaps_dh; closedir $charmaps_dh; } return @filesystem_charmaps; } sub _get_excluded_charmaps ( $iconv, $unpreferred_aliases ) { my %excluded; if ($iconv) { for my $bl ( _get_iconv_blacklist() ) { $excluded{$bl} = 1; } } if ( !$unpreferred_aliases ) { my %charset_aliases = _get_charset_aliases; for my $alias ( keys %charset_aliases ) { $alias =~ tr{A-Z}{a-z}; $excluded{$alias} = 1; } } return %excluded; } 1; } # --- END Cpanel/Locale/Utils/Charmap.pm { # --- BEGIN Cpanel/StringFunc/Case.pm package Cpanel::StringFunc::Case; use strict; use warnings; our $VERSION = '1.2'; sub ToUpper { return unless defined $_[0]; ( local $_ = $_[0] ) =~ tr/a-z/A-Z/; # avoid altering $_[0] by making a copy return $_; } sub ToLower { return unless defined $_[0]; ( local $_ = $_[0] ) =~ tr/A-Z/a-z/; # avoid altering $_[0] by making a copy return $_; } 1; } # --- END Cpanel/StringFunc/Case.pm { # --- BEGIN Cpanel/Locale/Utils/Legacy.pm package Cpanel::Locale::Utils::Legacy; use strict; use warnings; # use Cpanel::Locale::Utils::Normalize (); # use Cpanel::Locale::Utils::Paths (); my %oldname_to_locale; my $loc; sub _load_oldnames { %oldname_to_locale = ( 'turkish' => 'tr', 'traditional-chinese' => 'zh', 'thai' => 'th', 'swedish' => 'sv', 'spanish-utf8' => 'es', 'spanish' => 'es', 'slovenian' => 'sl', 'simplified-chinese' => 'zh_cn', 'russian' => 'ru', 'romanian' => 'ro', 'portuguese-utf8' => 'pt', 'portuguese' => 'pt', 'polish' => 'pl', 'norwegian' => 'no', 'korean' => 'ko', 'japanese-shift_jis' => 'ja', # see Cpanel::Locale::Utils::MkDB::compile_single_legacy_from_legacy_system() 'japanese-euc-jp' => 'ja', # see Cpanel::Locale::Utils::MkDB::compile_single_legacy_from_legacy_system() 'japanese' => 'ja', # see Cpanel::Locale::Utils::MkDB::compile_single_legacy_from_legacy_system() 'spanish_latinamerica' => 'es_419', 'iberian_spanish' => 'es_es', 'italian' => 'it', 'indonesian' => 'id', 'hungarian' => 'hu', 'german-utf8' => 'de', 'german' => 'de', 'french-utf8' => 'fr', 'french' => 'fr', 'finnish' => 'fi', 'english-utf8' => 'en', 'english' => 'en', 'dutch-utf8' => 'nl', 'dutch' => 'nl', 'chinese' => 'zh', 'bulgarian' => 'bg', 'brazilian-portuguese-utf8' => 'pt_br', 'brazilian-portuguese' => 'pt_br', 'arabic' => 'ar', ); { no warnings 'redefine'; *_load_oldnames = sub { }; } return; } sub get_legacy_to_locale_map { _load_oldnames(); return \%oldname_to_locale; } sub get_legacy_list_from_locale { my ($locale) = @_; return if !$locale; $locale = 'en' if $locale eq 'en_us' || $locale eq 'i_default'; _load_oldnames(); return grep { $oldname_to_locale{$_} eq $locale ? 1 : 0 } keys %oldname_to_locale; } sub get_best_guess_of_legacy_from_locale { my ( $locale, $always_return_useable ) = @_; return if !$locale && !$always_return_useable; $locale = 'en' if $locale eq 'en_us' || $locale eq 'i_default'; _load_oldnames(); my @legacy_locale_matches = grep { $oldname_to_locale{$_} eq $locale ? 1 : 0 } keys %oldname_to_locale; return $legacy_locale_matches[0] if @legacy_locale_matches; return 'english' if $always_return_useable; return; } sub get_legacy_name_list { _load_oldnames(); return sort { $a =~ m/\.local$/ ? $a cmp $b : $b cmp $a } keys %oldname_to_locale; } sub get_existing_filesys_legacy_name_list { require Cpanel::SafeDir::Read; my %args = @_; my @extras; if ( exists $args{'also_look_in'} && ref $args{'also_look_in'} eq 'ARRAY' ) { for my $path ( @{ $args{'also_look_in'} } ) { my $copy = $path; $copy =~ s/\/lang$//; next if !-d "$copy/lang"; push @extras, Cpanel::SafeDir::Read::read_dir("$copy/lang"); } } my @local_less_names; my %has_local; my @names; my $legacy_dir = Cpanel::Locale::Utils::Paths::get_legacy_lang_root(); for my $name ( grep { $_ !~ m/^\./ } ( $args{'no_root'} ? () : Cpanel::SafeDir::Read::read_dir($legacy_dir) ), @extras ) { my $copy = $name; if ( $copy =~ s/\.local$// ) { $has_local{$copy}++; } else { push @local_less_names, $copy; } } for my $name_localless ( sort { $b cmp $a } @local_less_names ) { push @names, exists $has_local{$name_localless} ? ( "$name_localless.local", $name_localless ) : $name_localless; } return @names; } sub get_legacy_root_in_locale_database_root { return Cpanel::Locale::Utils::Paths::get_locale_database_root() . '/legacy'; } sub get_legacy_file_cache_path { my ($legacy_file) = @_; $legacy_file .= 'cache'; my $legacy_dir = Cpanel::Locale::Utils::Paths::get_legacy_lang_root(); $legacy_file =~ s{$legacy_dir}{/var/cpanel/lang.cache}; return $legacy_file; } sub map_any_old_style_to_new_style { return wantarray ? map { get_new_langtag_of_old_style_langname($_) || $_ } @_ : get_new_langtag_of_old_style_langname( $_[0] ) || $_[0]; } my %charset_lookup; sub _determine_via_disassemble { my ( $lcl, $oldlang ) = @_; my ( $language, $territory, $encoding, $probable_ext ); my @parts = split( /[^A-Za-z0-9]+/, $oldlang ); # We can't use Cpanel::CPAN::Locales::normalize_tag since it breaks things into 8 character chunks return if @parts == 1; # we've already tried just $parts[0] if the split is only 1 item return if @parts > 4; # if there are more than 4 parts then there is unresolveable data if ( !ref($lcl) ) { $lcl = Cpanel::CPAN::Locales->new($lcl) or return; } for my $part (@parts) { my $found_part = 0; if ( $lcl->get_code_from_language($part) || $lcl->get_language_from_code($part) ) { if ($language) { if ( !$lcl->get_territory_from_code($part) ) { return; } } else { $found_part++; $language = $lcl->get_language_from_code($part) ? $part : $lcl->get_code_from_language($part); } } if ( !$found_part && ( $lcl->get_code_from_territory($part) || $lcl->get_territory_from_code($part) ) ) { if ($territory) { return; } else { $found_part++; $territory = $lcl->get_territory_from_code($part) ? $part : $lcl->get_code_from_territory($part); } } if ( !$found_part ) { if ( $part eq $parts[$#parts] ) { # && length($part) < $max_len_for_ext $probable_ext = $part; } else { if ( !%charset_lookup ) { require Cpanel::Locale::Utils::Charmap; @charset_lookup{ map { Cpanel::Locale::Utils::Normalize::normalize_tag($_) } Cpanel::Locale::Utils::Charmap::get_charmap_list() } = (); } if ( $charset_lookup{$part} ) { $found_part++; $encoding = $part; } else { return; } } } } if ($encoding) { } if ($probable_ext) { } if ($language) { if ($territory) { return "$language\_$territory"; } else { return $language; } } return; } sub real_get_new_langtag_of_old_style_langname { my ($oldlang) = @_; $oldlang = Cpanel::StringFunc::Case::ToLower($oldlang) || ""; # case 34321 item #3 $oldlang =~ s/\.legacy_duplicate\..+$//; # This '.legacy_duplicate. naming hack' is for copying legacy file into a name that maps back to it's new target locale if ( !defined $oldlang || $oldlang eq '' || $oldlang =~ m/^\s+$/ ) { return; # return a value ?, what is safe ... } elsif ( Cpanel::Locale::Utils::Normalize::normalize_tag($oldlang) eq 'default' ) { return; # return 'en' ? could be an incorrect assumption ... } elsif ( exists $oldname_to_locale{$oldlang} ) { return $oldname_to_locale{$oldlang}; } { local $@; $loc ||= Cpanel::CPAN::Locales->new('en') or die $@; } my $return; if ( $loc->get_language_from_code($oldlang) ) { $return = Cpanel::Locale::Utils::Normalize::normalize_tag($oldlang); # case 34321 item #4 } else { my $locale = $loc->get_code_from_language($oldlang); if ($locale) { $return = $locale; # case 34321 item #2 } else { $return = _determine_via_disassemble( $loc, $oldlang ); if ( !$return ) { local $SIG{'__DIE__'}; # may be made moot by case 50857 for my $nen ( grep { $_ ne 'en' } sort( $loc->get_language_codes() ) ) { my $loca = Cpanel::CPAN::Locales->new($nen) or next; # singleton my $locale = $loca->get_code_from_language($oldlang); if ($locale) { $return = $locale; # case 34321 item #2 last; } else { $return = _determine_via_disassemble( $loca, $oldlang ); last if $return; } } } } } if ( !$return ) { $return = Cpanel::CPAN::Locales::get_i_tag_for_string($oldlang); } return $return; } sub get_new_langtag_of_old_style_langname { _load_oldnames(); require Cpanel::StringFunc::Case; require Cpanel::CPAN::Locales; $loc = Cpanel::CPAN::Locales->new('en'); { no warnings 'redefine'; *get_new_langtag_of_old_style_langname = \&real_get_new_langtag_of_old_style_langname; } goto &real_get_new_langtag_of_old_style_langname; } my $legacy_lookup; sub phrase_is_legacy_key { my ($key) = @_; if ( !$legacy_lookup ) { require 'Cpanel/Locale/Utils/MkDB.pm'; ## no critic qw(Bareword) - hide from perlpkg $legacy_lookup = { %{ Cpanel::Locale::Utils::MkDB::get_hash_of_legacy_file( Cpanel::Locale::Utils::Paths::get_legacy_lang_root() . '/english-utf8' ) || {} }, %{ Cpanel::Locale::Utils::MkDB::get_hash_of_legacy_file('/usr/local/cpanel/base/frontend/jupiter/lang/english-utf8') || {} }, }; } return exists $legacy_lookup->{$key} ? 1 : 0; } sub fetch_legacy_lookup { return $legacy_lookup if $legacy_lookup; phrase_is_legacy_key(''); # ensure $legacy_lookup is loaded return $legacy_lookup; } sub get_legacy_key_english_value { my ($key) = @_; if ( phrase_is_legacy_key($key) ) { # inits $legacy_lookup cache return $legacy_lookup->{$key}; } return; } 1; } # --- END Cpanel/Locale/Utils/Legacy.pm { # --- BEGIN Cpanel/Config/LoadCpUserFile/CurrentUser.pm package Cpanel::Config::LoadCpUserFile::CurrentUser; use strict; use warnings; # use Cpanel::Config::LoadCpUserFile (); my $_cpuser_ref_singleton; my $_cpuser_user; sub load { my ($user) = @_; if ( $_cpuser_user && $_cpuser_user eq $user ) { return $_cpuser_ref_singleton; } $_cpuser_user = $user; return ( $_cpuser_ref_singleton = Cpanel::Config::LoadCpUserFile::load($user) ); } sub _reset { $_cpuser_ref_singleton = undef; $_cpuser_user = undef; return; } 1; } # --- END Cpanel/Config/LoadCpUserFile/CurrentUser.pm { # --- BEGIN Cpanel/YAML/Syck.pm package Cpanel::YAML::Syck; use YAML::Syck (); sub _init { $YAML::Syck::LoadBlessed = 0; { no warnings 'redefine'; *Cpanel::YAML::Syck::_init = sub { }; } return; } _init(); 1; } # --- END Cpanel/YAML/Syck.pm { # --- BEGIN Cpanel/PwUtils.pm package Cpanel::PwUtils; use strict; use warnings; # use Cpanel::Exception (); # use Cpanel::PwCache (); sub normalize_to_uid { my ($user) = @_; if ( !length $user ) { die Cpanel::Exception::create( 'MissingParameter', 'Supply a username or a user ID.' ); } return $user if $user !~ tr{0-9}{}c; # Only has numbers so its a uid my $uid = Cpanel::PwCache::getpwnam_noshadow($user); if ( !defined $uid ) { die Cpanel::Exception::create( 'UserNotFound', [ name => $user ] ); } return $uid; } 1; } # --- END Cpanel/PwUtils.pm { # --- BEGIN Cpanel/AccessIds/Normalize.pm package Cpanel::AccessIds::Normalize; use strict; use warnings; # use Cpanel::ArrayFunc::Uniq (); # use Cpanel::PwCache (); # use Cpanel::PwUtils (); # use Cpanel::Exception (); sub normalize_user_and_groups { my ( $user, @groups ) = @_; if ( ( scalar @groups == 1 && !defined $groups[0] ) || ( scalar @groups > 1 && scalar( grep { !defined } @groups ) ) ) { require Cpanel::Carp; # no load module for memory die Cpanel::Carp::safe_longmess("Undefined group passed to normalize_user_and_groups"); } my $uid; if ( defined $user && $user !~ tr{0-9}{}c ) { if ( scalar @groups == 1 && $groups[0] !~ tr{0-9}{}c ) { # we already have a gid return ( $user, $groups[0] ); } $uid = $user; if ( scalar @groups == 1 && $groups[0] !~ tr{0-9}{}c ) { # we already have a gid return ( $uid, $groups[0] ); } } elsif ( !scalar @groups ) { ( $uid, @groups ) = ( Cpanel::PwCache::getpwnam_noshadow($user) )[ 2, 3 ]; if ( !defined $uid ) { die Cpanel::Exception::create( 'UserNotFound', [ name => $user ] ); } return ( $uid, @groups ); } else { $uid = Cpanel::PwUtils::normalize_to_uid($user); } my @gids = @groups ? ( map { !tr{0-9}{}c ? $_ : scalar( ( getgrnam $_ )[2] ) } @groups ) : ( ( Cpanel::PwCache::getpwuid_noshadow($uid) )[3] ); if ( scalar @gids > 2 ) { return ( $uid, Cpanel::ArrayFunc::Uniq::uniq(@gids) ); } elsif ( scalar @gids == 2 && $gids[0] eq $gids[1] ) { return ( $uid, $gids[0] ); } return ( $uid, @gids ); } sub normalize_code_user_groups { my @args = @_; my $code_index; for my $i ( 0 .. $#args ) { if ( ref $args[$i] eq 'CODE' ) { $code_index = $i; last; } } die "No coderef found!" if !defined $code_index; my $code = splice( @args, $code_index, 1 ); return ( $code, normalize_user_and_groups( grep { defined } @args ) ); } 1; } # --- END Cpanel/AccessIds/Normalize.pm { # --- BEGIN Cpanel/AccessIds/Utils.pm package Cpanel::AccessIds::Utils; use strict; use warnings; # use Cpanel::ArrayFunc::Uniq (); # use Cpanel::Debug (); sub normalize_user_and_groups { require Cpanel::AccessIds::Normalize; goto \&Cpanel::AccessIds::Normalize::normalize_user_and_groups; } sub normalize_code_user_groups { require Cpanel::AccessIds::Normalize; goto \&Cpanel::AccessIds::Normalize::normalize_code_user_groups; } sub set_egid { my @gids = @_; if ( !@gids ) { Cpanel::Debug::log_die("No arguments passed to set_egid()!"); } if ( scalar @gids > 1 ) { @gids = Cpanel::ArrayFunc::Uniq::uniq(@gids); } _check_positive_int($_) for @gids; my $new_egid = join( q{ }, $gids[0], @gids ); return _set_var( \$), 'EGID', $new_egid ); } sub set_rgid { my ( $gid, @extra_gids ) = @_; if (@extra_gids) { Cpanel::Debug::log_die("RGID can only be set to a single value! (Do you want set_egid()?)"); } _check_positive_int($gid); return _set_var( \$(, 'RGID', $gid ); } sub set_euid { my ($uid) = @_; _check_positive_int($uid); return _set_var( \$>, 'EUID', $uid ); } sub set_ruid { my ($uid) = @_; _check_positive_int($uid); return _set_var( \$<, 'RUID', $uid ); } sub _check_positive_int { if ( !length $_[0] || $_[0] =~ tr{0-9}{}c ) { Cpanel::Debug::log_die("“$_[0] is not a positive integer!"); } return 1; } sub _set_var { my ( $var_r, $name, $desired_value ) = @_; my $old_value = $$var_r; $$var_r = $desired_value; return $desired_value eq $$var_r ? 1 : validate_var_set( $name, # The name of the value like 'RUID' $desired_value, # The value we wanted it to be set to $$var_r, # Deferenced variable being set, ex $< $old_value # The value before we set it. ); } sub validate_var_set { my ( $name, $desired_value, $new_value, $old_value ) = @_; my $error; if ( $new_value =~ tr/ // ) { my ( $desired_first, @desired_parts ) = split( ' ', $desired_value ); my ( $new_first, @new_parts ) = split( ' ', $new_value ); if ( $new_first != $desired_first ) { $error = 1; } elsif ( @desired_parts && @new_parts ) { if ( scalar @desired_parts == 1 && scalar @new_parts == 1 ) { if ( $new_parts[0] != $desired_parts[0] ) { $error = 1; } } else { @desired_parts = sort { $a <=> $b } Cpanel::ArrayFunc::Uniq::uniq(@desired_parts); @new_parts = sort { $a <=> $b } Cpanel::ArrayFunc::Uniq::uniq(@new_parts); for my $i ( 0 .. $#desired_parts ) { if ( $new_parts[$i] != $desired_parts[$i] ) { $error = 1; last; } } } } } else { if ( $new_value != $desired_value ) { $error = 1; } } return 1 if !$error; if ( defined $old_value ) { Cpanel::Debug::log_die("Failed to change $name from “$old_value” to “$desired_value”: $!"); } Cpanel::Debug::log_die("Failed to change $name to “$desired_value”: $!"); return 0; #not reached } 1; } # --- END Cpanel/AccessIds/Utils.pm { # --- BEGIN Cpanel/AccessIds/ReducedPrivileges.pm package Cpanel::AccessIds::ReducedPrivileges; use strict; use warnings; # use Cpanel::Debug (); # use Cpanel::AccessIds::Utils (); # use Cpanel::AccessIds::Normalize (); our $PRIVS_REDUCED = 0; sub new { ## no critic qw(Subroutines::RequireArgUnpacking) my $class = shift; if ( $class ne __PACKAGE__ ) { Cpanel::Debug::log_die("Attempting to drop privileges as '$class'."); } my ( $uid, @gids ) = Cpanel::AccessIds::Normalize::normalize_user_and_groups(@_); _allowed_to_reduce_privileges(); _prevent_dropping_to_root( $uid, @gids ); my $self = { 'uid' => $>, 'gid' => $), 'new_uid' => $uid, 'new_gid' => join( q< >, @gids ), }; _reduce_privileges( $uid, @gids ); $PRIVS_REDUCED = 1; return bless $self; } sub DESTROY { my ($self) = @_; _allowed_to_restore_privileges( $self->{'new_uid'}, $self->{'new_gid'} ); return _restore_privileges( $self->{'uid'}, $self->{'gid'} ); } sub call_as_user { ## no critic qw(Subroutines::RequireArgUnpacking) my ( $code, $uid, $gid, @supplemental_gids ) = Cpanel::AccessIds::Normalize::normalize_code_user_groups(@_); _prevent_dropping_to_root( $uid, $gid ); if ( !$code ) { Cpanel::Debug::log_die("No code reference supplied."); } _allowed_to_reduce_privileges(); my ( $saved_uid, $saved_gid ) = ( $>, $) ); _reduce_privileges( $uid, $gid, @supplemental_gids ); local $PRIVS_REDUCED = 1; my ( $scalar, @list ); if (wantarray) { #list context @list = eval { $code->(); }; } elsif ( defined wantarray ) { #scalar context $scalar = eval { $code->(); }; } else { #void context eval { $code->(); }; } my $ex = $@; _restore_privileges( $saved_uid, $saved_gid ); die $ex if $ex; return wantarray ? @list : $scalar; } sub _allowed_to_reduce_privileges { if ( $< != 0 ) { Cpanel::Debug::log_die("Attempting to drop privileges as a normal user with RUID $<"); } if ( $> != 0 ) { Cpanel::Debug::log_die("Attempting to drop privileges as a normal user with EUID $>"); } return 1; } sub _reduce_privileges { my ( $uid, $gid, @supplemental_gids ) = @_; Cpanel::AccessIds::Utils::set_egid( $gid, @supplemental_gids ); Cpanel::AccessIds::Utils::set_euid($uid); return 1; } sub _prevent_dropping_to_root { if ( grep { !$_ } @_ ) { Cpanel::Debug::log_die("Attempting to drop privileges to root."); } return 1; } sub _allowed_to_restore_privileges { my ( $uid, $gid ) = @_; if ( $< != 0 ) { Cpanel::Debug::log_die("Attempting to restore privileges as a normal user with RUID $<"); } if ( $> != $uid ) { Cpanel::Debug::log_warn("EUID ($>) does not match expected reduced user ($uid)"); } my ( $first_egid, $first_given_gid ) = ( $), $gid ); $_ = ( split m{ } )[0] for ( $first_egid, $first_given_gid ); if ( int $first_egid != int $first_given_gid ) { Cpanel::Debug::log_warn("EGID ($)) does not match expected reduced user ($gid)"); } } sub _restore_privileges { my ( $saved_uid, $saved_gid ) = @_; Cpanel::AccessIds::Utils::set_euid($saved_uid); Cpanel::AccessIds::Utils::set_egid( split m{ }, $saved_gid ); $PRIVS_REDUCED = 0; return 1; } 1; } # --- END Cpanel/AccessIds/ReducedPrivileges.pm { # --- BEGIN Cpanel/DataStore.pm package Cpanel::DataStore; use strict; use warnings; # use Cpanel::Debug (); sub store_ref { my ( $file, $outof_ref, $perm ) = @_; require Cpanel::YAML::Syck; $YAML::Syck::ImplicitTyping = 0; local $YAML::Syck::SingleQuote; local $YAML::Syck::SortKeys; $YAML::Syck::SingleQuote = 1; $YAML::Syck::SortKeys = 1; if ( ref($file) ) { my $yaml_string = YAML::Syck::Dump($outof_ref); print( {$file} _format($yaml_string) ) || return; return $file; } if ( ref($perm) eq 'ARRAY' && !-l $file && !-e $file ) { require Cpanel::FileUtils::TouchFile; # or use() ? my $touch_chmod = sub { if ( !Cpanel::FileUtils::TouchFile::touchfile($file) ) { Cpanel::Debug::log_info("Could not touch \xE2\x80\x9C$file\xE2\x80\x9D: $!"); return; } if ( $perm->[0] ) { if ( !chmod( oct( $perm->[0] ), $file ) ) { Cpanel::Debug::log_info("Could not chmod \xE2\x80\x9C$file\xE2\x80\x9D to \xE2\x80\x9C$perm->[0]\xE2\x80\x9D: $!"); return; } } return 1; }; if ( $> == 0 && $perm->[1] && $perm->[1] ne 'root' ) { require Cpanel::AccessIds::ReducedPrivileges; # or use() ? Cpanel::AccessIds::ReducedPrivileges::call_as_user( $perm->[1], $touch_chmod ) || return; } else { $touch_chmod->() || return; } } if ( open my $yaml_out, '>', $file ) { my $yaml_string = YAML::Syck::Dump($outof_ref); print {$yaml_out} _format($yaml_string); close $yaml_out; return 1; } else { Cpanel::Debug::log_warn("Could not open file '$file' for writing: $!"); return; } } sub fetch_ref { my ( $file, $is_array ) = @_; my $fetch_ref = load_ref($file); my $data_type = ref $fetch_ref; my $data = $data_type ? $fetch_ref : undef; $data_type ||= 'UNDEF'; if ( $is_array && $data_type ne 'ARRAY' ) { return []; } elsif ( !$is_array && $data_type ne 'HASH' ) { return {}; } return $data; } sub load_ref { my ( $file, $into_ref ) = @_; return if ( !-e $file || -z _ ); require Cpanel::YAML::Syck; $YAML::Syck::ImplicitTyping = 0; my $struct; if ( ref($file) ) { local $!; $struct = eval { local $/; local $SIG{__WARN__}; local $SIG{__DIE__}; ( YAML::Syck::Load(<$file>) )[0]; }; Cpanel::Debug::log_warn("Error loading YAML data: $!") if ( !$struct ); } elsif ( open my $yaml_in, '<', $file ) { local $!; $struct = eval { local $/; local $SIG{__WARN__}; local $SIG{__DIE__}; ( YAML::Syck::Load(<$yaml_in>) )[0]; }; Cpanel::Debug::log_warn("Error loading YAML data: $!") if ( !$struct ); close $yaml_in; } else { my $err = $!; Cpanel::Debug::log_warn("Could not open file '$file' for reading: $err"); return; } if ( !$struct ) { Cpanel::Debug::log_warn("Failed to load YAML data from file $file"); return; } if ( defined $into_ref ) { my $type = ref $into_ref; my $yaml_type = ref $struct; if ( $yaml_type ne $type ) { Cpanel::Debug::log_warn("Invalid data type from file $file! YAML type $yaml_type does not match expected type $type. Data ignored!"); return; # if we want an empty ref on failure use fetch_ref() } if ( $yaml_type eq 'HASH' ) { %{$into_ref} = %{$struct}; } elsif ( $yaml_type eq 'ARRAY' ) { @{$into_ref} = @{$struct}; } else { Cpanel::Debug::log_warn("YAML in '$file' is not a hash or array reference"); return; # if we want an empty ref on failure use fetch_ref() } return $into_ref; } return $struct; } sub edit_datastore { my ( $file, $editor_cr, $is_array ) = @_; if ( ref $editor_cr ne 'CODE' ) { Cpanel::Debug::log_warn('second arg needs to be a coderef'); return; } my $ref = $is_array ? [] : {}; if ( !-e $file ) { Cpanel::Debug::log_info("Data store file $file does not exist. Attempting to create empty datastore."); store_ref( $file, $ref ); } if ( load_ref( $file, $ref ) ) { if ( $editor_cr->($ref) ) { if ( !store_ref( $file, $ref ) ) { Cpanel::Debug::log_warn("Modifications to file $file could not be saved"); return; } } } else { Cpanel::Debug::log_warn("Could not load datastore $file"); return; } return 1; } sub _format { my ($s) = @_; $s =~ s/[ \t]+$//mg; return __grapheme_to_character($s); } sub __grapheme_to_character { my ($yaml_string) = @_; $yaml_string = quotemeta($yaml_string); $yaml_string =~ s/\\{2}x/\\x/g; $yaml_string = eval qq{"$yaml_string"}; return $yaml_string; } 1; } # --- END Cpanel/DataStore.pm { # --- BEGIN Cpanel/StringFunc/Trim.pm package Cpanel::StringFunc::Trim; use strict; use warnings; $Cpanel::StringFunc::Trim::VERSION = '1.02'; my %ws_chars = ( "\r" => undef, "\n" => undef, " " => undef, "\t" => undef, "\f" => undef ); sub trim { my ( $str, $totrim ) = @_; $str = rtrim( ltrim( $str, $totrim ), $totrim ); return $str; } sub ltrim { my ( $str, $totrim ) = @_; $str =~ s/^$totrim*//; return $str; } sub rtrim { my ( $str, $totrim ) = @_; $str =~ s/$totrim*$//; return $str; } sub endtrim { my ( $str, $totrim ) = @_; if ( substr( $str, ( length($totrim) * -1 ), length($totrim) ) eq $totrim ) { return substr( $str, 0, ( length($str) - length($totrim) ) ); } return $str; } sub begintrim { my ( $str, $totrim ) = @_; if ( defined $str && defined $totrim # . && substr( $str, 0, length($totrim) ) eq $totrim ) { return substr( $str, length($totrim) ); } return $str; } sub ws_trim { my ($this) = @_; return unless defined $this; my $fix = ref $this eq 'SCALAR' ? $this : \$this; return unless defined $$fix; if ( $$fix =~ tr{\r\n \t\f}{} ) { ${$fix} =~ s/^\s+// if exists $ws_chars{ substr( $$fix, 0, 1 ) }; ${$fix} =~ s/\s+$// if exists $ws_chars{ substr( $$fix, -1, 1 ) }; } return ${$fix}; } sub ws_trim_array { my $ar = ref $_[0] eq 'ARRAY' ? $_[0] : [@_]; # [@_] :: copy @_ w/ out unpack first: !! not \@_ in this case !! foreach my $idx ( 0 .. scalar( @{$ar} ) - 1 ) { $ar->[$idx] = ws_trim( $ar->[$idx] ); } return wantarray ? @{$ar} : $ar; } sub ws_trim_hash_values { my $hr = ref $_[0] eq 'HASH' ? $_[0] : {@_}; # {@_} :: copy @_ w/ out unpack first: foreach my $key ( keys %{$hr} ) { $hr->{$key} = ws_trim( $hr->{$key} ); } return wantarray ? %{$hr} : $hr; } 1; } # --- END Cpanel/StringFunc/Trim.pm { # --- BEGIN Cpanel/Locale/Utils/3rdparty.pm package Cpanel::Locale::Utils::3rdparty; use strict; use warnings; our %cpanel_provided = ( 'de' => 1, 'en' => 1, 'es_es' => 1, 'i_cpanel_snowmen' => 1, 'ru' => 1, 'ja' => 1, ); my %locale_to_3rdparty; sub _load_3rdparty { return if (%locale_to_3rdparty); %locale_to_3rdparty = ( 'ar' => { 'analog' => 'us', 'awstats' => 'ar', 'webalizer' => 'english' }, 'bg' => { 'analog' => 'bg', 'awstats' => 'bg', 'webalizer' => 'english' }, 'bn' => { 'analog' => 'us', 'awstats' => 'en', 'webalizer' => 'english' }, 'de' => { 'analog' => 'de', 'awstats' => 'de', 'webalizer' => 'german' }, 'en' => { 'analog' => 'us', 'awstats' => 'en', 'webalizer' => 'english' }, 'es' => { 'analog' => 'es', 'awstats' => 'es', 'webalizer' => 'spanish' }, 'es_es' => { 'analog' => 'es', 'awstats' => 'es', 'webalizer' => 'spanish' }, 'fi' => { 'analog' => 'fi', 'awstats' => 'fi', 'webalizer' => 'finnish' }, 'fr' => { 'analog' => 'fr', 'awstats' => 'fr', 'webalizer' => 'french' }, 'hi' => { 'analog' => 'us', 'awstats' => 'en', 'webalizer' => 'english' }, 'hu' => { 'analog' => 'hu', 'awstats' => 'hu', 'webalizer' => 'hungarian' }, 'id' => { 'analog' => 'us', 'awstats' => 'id', 'webalizer' => 'indonesian' }, 'it' => { 'analog' => 'it', 'awstats' => 'it', 'webalizer' => 'italian' }, 'ja' => { 'analog' => 'jpu', # appears to be the UTF-8 one 'awstats' => 'jp', 'webalizer' => 'japanese' }, 'ko' => { 'analog' => 'us', 'awstats' => 'ko', 'webalizer' => 'korean' }, 'nl' => { 'analog' => 'nl', 'awstats' => 'nl', 'webalizer' => 'dutch' }, 'no' => { 'analog' => 'no', 'awstats' => 'en', 'webalizer' => 'norwegian' }, 'pl' => { 'analog' => 'pl', 'awstats' => 'pl', 'webalizer' => 'polish' }, 'pt' => { 'analog' => 'pt', 'awstats' => 'pt', 'webalizer' => 'portuguese' }, 'pt_br' => { 'analog' => 'pt', 'awstats' => 'pt', 'webalizer' => 'portuguese_brazil' }, 'ro' => { 'analog' => 'ro', 'awstats' => 'ro', 'webalizer' => 'romanian' }, 'ru' => { 'analog' => 'ru', 'awstats' => 'ru', 'webalizer' => 'russian' }, 'sl' => { 'analog' => 'us', 'awstats' => 'en', 'webalizer' => 'slovene' }, 'sv' => { 'analog' => 'us', 'awstats' => 'en', 'webalizer' => 'swedish' }, 'th' => { 'analog' => 'us', 'awstats' => 'th', 'webalizer' => 'english' }, 'tr' => { 'analog' => 'tr', 'awstats' => 'tr', 'webalizer' => 'turkish' }, 'zh' => { 'analog' => 'cn', # the cn.lng does not say what it is so this is an assumption based on other pervasive bad practices 'awstats' => 'cn', 'webalizer' => 'chinese' }, 'zh_cn' => { 'analog' => 'cn', # the cn.lng does not say what it is so this is an assumption based on other pervasive bad practices 'awstats' => 'cn', 'webalizer' => 'simplified_chinese' }, ); } sub get_known_3rdparty_lang { my ( $locale, $_3rdparty ) = @_; _load_3rdparty(); my $locale_tag = ref $locale ? $locale->get_language_tag() : $locale; $locale_tag = 'en' if $locale_tag eq 'en_us' || $locale_tag eq 'i_default'; return if !exists $locale_to_3rdparty{$locale_tag}; return if !exists $locale_to_3rdparty{$locale_tag}{$_3rdparty}; return $locale_to_3rdparty{$locale_tag}{$_3rdparty}; } my %locale_lookup_cache; sub get_3rdparty_lang { my ( $locale, $_3rdparty ) = @_; my $known = get_known_3rdparty_lang( $locale, $_3rdparty ); return $known if $known; return if !ref($locale) && $locale =~ m/(?:\.\.|\/)/; return if $_3rdparty =~ m/(?:\.\.|\/)/; my $locale_tag = ref $locale ? $locale->get_language_tag() : $locale; $locale_tag = 'en' if $locale_tag eq 'en_us' || $locale_tag eq 'i_default'; if ( exists $locale_lookup_cache{$_3rdparty} ) { return $locale_lookup_cache{$_3rdparty}{$locale_tag} if exists $locale_lookup_cache{$_3rdparty}{$locale_tag}; return; } require Cpanel::DataStore; my $hr = Cpanel::DataStore::fetch_ref("/var/cpanel/locale/3rdparty/apps/$_3rdparty.yaml"); my %seen; %{ $locale_lookup_cache{$_3rdparty} } = map { ++$seen{ $hr->{$_} } == 1 ? ( $hr->{$_} => $_ ) : () } keys %{$hr}; return $locale_lookup_cache{$_3rdparty}{$locale_tag} if exists $locale_lookup_cache{$_3rdparty}{$locale_tag}; return; } my @list; sub get_3rdparty_list { return @list if @list; @list = qw(analog awstats webalizer); if ( -d "/var/cpanel/locale/3rdparty/apps" ) { require Cpanel::SafeDir::Read; push @list, sort map { my $f = $_; $f =~ s/\.yaml$// ? ($f) : () } Cpanel::SafeDir::Read::read_dir("/var/cpanel/locale/3rdparty/apps"); } return @list; } my %opt_cache; sub get_app_options { my ($_3rdparty) = @_; return if $_3rdparty =~ m/(?:\.\.|\/)/; return $opt_cache{$_3rdparty} if exists $opt_cache{$_3rdparty}; if ( $_3rdparty eq 'analog' || $_3rdparty eq 'awstats' || $_3rdparty eq 'webalizer' ) { _load_3rdparty(); my %seen; $opt_cache{$_3rdparty} = [ sort map { ++$seen{ $locale_to_3rdparty{$_}{$_3rdparty} } == 1 ? ( $locale_to_3rdparty{$_}{$_3rdparty} ) : () } keys %locale_to_3rdparty ]; } else { require Cpanel::DataStore; my $hr = Cpanel::DataStore::fetch_ref("/var/cpanel/locale/3rdparty/apps/$_3rdparty.yaml"); $opt_cache{$_3rdparty} = [ sort keys %{$hr} ]; } return $opt_cache{$_3rdparty}; } sub get_app_setting { my ( $locale, $_3rdparty ) = @_; return if !ref($locale) && $locale =~ m/(?:\.\.|\/)/; return if $_3rdparty =~ m/(?:\.\.|\/)/; require Cpanel::LoadFile; require Cpanel::StringFunc::Trim; my $locale_tag = ref $locale ? $locale->get_language_tag() : $locale; $locale_tag = 'en' if $locale_tag eq 'en_us' || $locale_tag eq 'i_default'; my $setting = Cpanel::StringFunc::Trim::ws_trim( Cpanel::LoadFile::loadfile("/var/cpanel/locale/3rdparty/conf/$locale_tag/$_3rdparty") ) || ''; if ( $_3rdparty eq 'analog' && $setting eq 'en' ) { $setting = 'us'; } return $setting; } sub set_app_setting { my ( $locale, $_3rdparty, $setting ) = @_; return if !ref($locale) && $locale =~ m/(?:\.\.|\/)/; return if $_3rdparty =~ m/(?:\.\.|\/)/; require Cpanel::SafeDir::MK; require Cpanel::FileUtils::Write; my $locale_tag = ref $locale ? $locale->get_language_tag() : $locale; $locale_tag = 'en' if $locale_tag eq 'en_us' || $locale_tag eq 'i_default'; Cpanel::SafeDir::MK::safemkdir("/var/cpanel/locale/3rdparty/conf/$locale_tag/"); Cpanel::FileUtils::Write::overwrite_no_exceptions( "/var/cpanel/locale/3rdparty/conf/$locale_tag/$_3rdparty", $setting, 0644 ); return; } 1; } # --- END Cpanel/Locale/Utils/3rdparty.pm { # --- BEGIN Cpanel/JS/Variations.pm package Cpanel::JS::Variations; use strict; sub lex_filename_for { my ( $filename, $locale ) = @_; return if !$filename || !$locale; return get_base_file( $filename, "-${locale}.js" ); } sub get_base_file { my ( $filename, $replace_extension ) = @_; return if !$filename; $replace_extension //= '.js'; $filename =~ s{/js2-min/}{/js2/}; $filename =~ s{(?:[\.\-]min|_optimized)?\.js$}{$replace_extension}; return $filename; } 1; } # --- END Cpanel/JS/Variations.pm { # --- BEGIN Cpanel/Locale/Utils/Display.pm package Cpanel::Locale::Utils::Display; use warnings; use strict; # use Cpanel::Locale::Utils::Paths (); sub get_locale_list { my ($lh) = @_; my @result = @{ $lh->{'_cached_get_locale_list'} ||= [ sort ( 'en', $lh->list_available_locales() ) ] }; if ( !-e "/var/cpanel/enable_snowmen" ) { @result = grep { !/i_cpanel_snowmen/ } @result; } return @result; } sub get_non_existent_locale_list { my ( $lh, $loc_obj ) = @_; $loc_obj ||= $lh->get_locales_obj('en'); my %have; @have{ get_locale_list($lh), 'en_us', 'i_default', 'und', 'zxx', 'mul', 'mis', 'art' } = (); return sort grep { !exists $have{$_} } $loc_obj->get_language_codes(); } sub get_locale_menu_hashref { my ( $lh, $omit_current_locale, $native_only, $skip_locales ) = @_; $skip_locales ||= {}; my %langs; my %dir; my @langs = get_locale_list($lh); my @wanted_langs = grep { !$skip_locales->{$_} } @langs; if ( !@wanted_langs ) { return ( {}, \@langs, {} ); } my $func = $native_only ? 'lang_names_hashref_native_only' : 'lang_names_hashref'; my ( $localized_name_for_tag, $native_name_for_tag, $direction_map ) = $lh->$func(@wanted_langs); my $current_tag = $lh->get_language_tag(); $current_tag = 'en' if $current_tag eq 'en_us' || $current_tag eq 'i_default'; my $i_locales_path = Cpanel::Locale::Utils::Paths::get_i_locales_config_path(); if ($omit_current_locale) { delete $localized_name_for_tag->{$current_tag}; delete $native_name_for_tag->{$current_tag}; @langs = grep { $_ ne $current_tag } @langs; } foreach my $tag ( keys %{$localized_name_for_tag} ) { if ( index( $tag, 'i_' ) == 0 ) { require Cpanel::DataStore; my $i_conf = Cpanel::DataStore::fetch_ref("$i_locales_path/$tag.yaml"); $langs{$tag} = exists $i_conf->{'display_name'} && defined $i_conf->{'display_name'} && $i_conf->{'display_name'} ne '' ? "$i_conf->{'display_name'} - $tag" : $tag; # slightly different format than real tags to visually indicate specialness $native_name_for_tag->{$tag} = $langs{$tag}; if ( exists $i_conf->{'character_orientation'} ) { $dir{$tag} = $lh->get_html_dir_attr( $i_conf->{'character_orientation'} ); } elsif ( exists $i_conf->{'fallback_locale'} && exists $direction_map->{ $i_conf->{'fallback_locale'} } ) { $dir{$tag} = $direction_map->{ $i_conf->{'fallback_locale'} }; } next; } if ( exists $direction_map->{$tag} ) { $dir{$tag} = $lh->get_html_dir_attr( $direction_map->{$tag} ); } next if $native_only; if ( $native_name_for_tag->{$tag} eq $localized_name_for_tag->{$tag} ) { if ( $tag eq $current_tag ) { $langs{$tag} = $native_name_for_tag->{$tag}; } else { $langs{$tag} = "$localized_name_for_tag->{$tag} ($tag)"; } } else { $langs{$tag} = "$localized_name_for_tag->{$tag} ($native_name_for_tag->{$tag})"; } } if ($native_only) { return wantarray ? ( $native_name_for_tag, \@langs, \%dir ) : $native_name_for_tag; } return wantarray ? ( \%langs, \@langs, \%dir ) : \%langs; } sub get_non_existent_locale_menu_hashref { my $lh = shift; $lh->{'Locales.pm'}{'_main_'} ||= $lh->get_locales_obj(); my %langs; my %dir; my @langs = get_non_existent_locale_list( $lh, $lh->{'Locales.pm'}{'_main_'} ); my $wantarray = wantarray() ? 1 : 0; for my $code (@langs) { if ($wantarray) { if ( my $orient = $lh->{'Locales.pm'}{'_main_'}->get_character_orientation_from_code_fast($code) ) { $dir{$code} = $lh->get_html_dir_attr($orient); } } my $current = $lh->{'Locales.pm'}{'_main_'}->get_language_from_code( $code, 1 ); my $native = $lh->{'Locales.pm'}{'_main_'}->get_native_language_from_code( $code, 1 ); $langs{$code} = $current eq $native ? "$current ($code)" : "$current ($native)"; } return wantarray ? ( \%langs, \@langs, \%dir ) : \%langs; } sub in_translation_vetting_mode { return ( -e '/var/cpanel/translation_vetting_mode' ) ? 1 : 0; } 1; } # --- END Cpanel/Locale/Utils/Display.pm { # --- BEGIN Cpanel/Locale/Utils/Api1.pm package Cpanel::Locale::Utils::Api1; use strict; use warnings; # use Cpanel::Locale (); my $_lh; sub _api1_maketext { ## no critic qw(Subroutines::RequireArgUnpacking) ## no extract maketext $_lh ||= Cpanel::Locale->get_handle(); $_[0] =~ s{\\'}{'}g; my $localized_str = $_lh->makevar(@_); if ($Cpanel::Parser::Vars::embtag) { # PPI NO PARSE -- module will already be there is we care about it require Cpanel::Encoder::Tiny; $localized_str = Cpanel::Encoder::Tiny::safe_html_encode_str($localized_str); } elsif ($Cpanel::Parser::Vars::javascript) { # PPI NO PARSE -- module will already be there is we care about it $localized_str =~ s/"/\\"/g; $localized_str =~ s/'/\\'/g; } return { status => 1, statusmsg => $localized_str, }; } 1; } # --- END Cpanel/Locale/Utils/Api1.pm { # --- BEGIN Cpanel/FileUtils/Lines.pm package Cpanel::FileUtils::Lines; use strict; # use Cpanel::Debug (); use IO::SigGuard (); use constant _ENOENT => 2; our $VERSION = '1.0'; my $MAX_LINE_SIZE = 32768; sub get_file_lines { my $cfgfile = shift; my $line_number = shift; return if ( !$line_number || $line_number !~ m/^\d+$/ ); my $numpadding = 7; my %ret; if ( open( my $cfg_fh, '<', $cfgfile ) ) { my $linecounter = 0; while ( readline($cfg_fh) ) { $linecounter++; if ( $linecounter < $line_number && $linecounter > ( $line_number - $numpadding ) ) { push @{ $ret{'previouslines'} }, { 'line' => $linecounter, 'data' => $_ }; } elsif ( $linecounter > $line_number && $linecounter < ( $line_number + $numpadding ) ) { push @{ $ret{'afterlines'} }, { 'line' => $linecounter, 'data' => $_ }; } elsif ( $linecounter == $line_number ) { push @{ $ret{'lines'} }, { 'line' => $linecounter, 'data' => $_ }; } elsif ( $linecounter > ( $line_number + $numpadding ) ) { last; } } close $cfg_fh; } return \%ret; } sub get_last_lines { my $cfgfile = shift; my $number = shift; if ( !$number || $number !~ m/^\d+$/ ) { $number = 10; } my @lines; if ( open( my $cfg_fh, '<', $cfgfile ) ) { my $size = ( stat($cfg_fh) )[7]; if ( $size > ( $MAX_LINE_SIZE * $number ) ) { seek( $cfg_fh, $size - ( $MAX_LINE_SIZE * $number ), 0 ); } my $linecounter = 0; while ( my $line = readline($cfg_fh) ) { chomp $line; if ( $linecounter >= $number ) { shift @lines; } push @lines, $line; $linecounter++; } close $cfg_fh; } else { Cpanel::Debug::log_warn("Unable to open $cfgfile: $!"); } return wantarray ? @lines : \@lines; } sub has_txt_in_file { my ( $file, $txt ) = @_; my $regex; eval { $regex = qr($txt); }; if ($@) { Cpanel::Debug::log_warn('Invalid regex'); return; } my $fh; if ( open $fh, '<', $file ) { while ( my $line = readline $fh ) { if ( $line =~ $regex ) { close $fh; return 1; } } close $fh; } return; } sub appendline { my ( $filename, $line ) = @_; my $fh; if ( open my $fh, '>>:stdio', $filename ) { IO::SigGuard::syswrite( $fh, $line . "\n" ) or do { warn "write($filename): $!"; }; close $fh; return 1; } else { warn "open($filename): $!" if $! != _ENOENT(); } return; } 1; } # --- END Cpanel/FileUtils/Lines.pm { # --- BEGIN Cpanel/SafeRun/Simple.pm package Cpanel::SafeRun::Simple; use strict; # use Cpanel::FHUtils::Autoflush (); # use Cpanel::LoadFile::ReadFast (); # use Cpanel::SV (); BEGIN { eval { require Proc::FastSpawn; }; } my $KEEP_STDERR = 0; my $MERGE_STDERR = 1; my $NULL_STDERR = 2; my $NULL_STDOUT = 3; sub saferun_r { return _saferun_r( \@_ ); } sub _saferun_r { ## no critic qw(Subroutines::ProhibitExcessComplexity) my ( $cmdline, $error_flag ) = @_; if ($Cpanel::AccessIds::ReducedPrivileges::PRIVS_REDUCED) { # PPI NO PARSE -- can't be reduced if the module isn't loaded eval "use Cpanel::Carp;"; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) die Cpanel::Carp::safe_longmess( __PACKAGE__ . " cannot be used with ReducedPrivileges. Use Cpanel::SafeRun::Object instead" ); } elsif ( scalar @$cmdline == 1 && $cmdline->[0] =~ tr{><*?[]`$()|;&#$\\\r\n\t }{} ) { eval "use Cpanel::Carp;"; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) die Cpanel::Carp::safe_longmess( __PACKAGE__ . " prevents accidental execution of a shell. If you intended to execute a shell use saferun(" . join( ',', '/bin/sh', '-c', @$cmdline ) . ")" ); } my $output; if ( index( $cmdline->[0], '/' ) == 0 ) { my ($check) = !-e $cmdline->[0] && $cmdline->[0] =~ /[\s<>&\|\;]/ ? split( /[\s<>&\|\;]/, $cmdline->[0], 2 ) : $cmdline->[0]; if ( !-x $check ) { $? = -1; return \$output; } } $error_flag ||= 0; local ($/); my ( $pid, $prog_fh, $did_fastspawn ); if ( $INC{'Proc/FastSpawn.pm'} ) { # may not be available yet due to upcp.static or updatenow.static my @env = map { exists $ENV{$_} && $_ ne 'IFS' && $_ ne 'CDPATH' && $_ ne 'ENV' && $_ ne 'BASH_ENV' ? ( $_ . '=' . ( $ENV{$_} // '' ) ) : () } keys %ENV; my ($child_write); pipe( $prog_fh, $child_write ) or warn "Failed to pipe(): $!"; my $null_fh; if ( $error_flag == $NULL_STDERR || $error_flag == $NULL_STDOUT ) { open( $null_fh, '>', '/dev/null' ) or die "Failed open /dev/null: $!"; } Cpanel::FHUtils::Autoflush::enable($_) for ( $prog_fh, $child_write ); $did_fastspawn = 1; my $stdout_fileno = fileno($child_write); my $stderr_fileno = -1; if ( $error_flag == $MERGE_STDERR ) { $stderr_fileno = fileno($child_write); } elsif ( $error_flag == $NULL_STDERR ) { $stderr_fileno = fileno($null_fh); } elsif ( $error_flag == $NULL_STDOUT ) { $stdout_fileno = fileno($null_fh); $stderr_fileno = fileno($child_write); } $pid = Proc::FastSpawn::spawn_open3( -1, # stdin $stdout_fileno, # stdout $stderr_fileno, # stderr $cmdline->[0], # program $cmdline, # args \@env, #env ); } else { if ( $pid = open( $prog_fh, '-|' ) ) { } elsif ( defined $pid ) { delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; $ENV{'PATH'} ||= ''; Cpanel::SV::untaint( $ENV{'PATH'} ); if ( $error_flag == $MERGE_STDERR ) { open( STDERR, '>&STDOUT' ) or die "Failed to redirect STDERR to STDOUT: $!"; } elsif ( $error_flag == $NULL_STDERR ) { open( STDERR, '>', '/dev/null' ) or die "Failed to open /dev/null: $!"; } elsif ( $error_flag == $NULL_STDOUT ) { open( STDERR, '>&STDOUT' ) or die "Failed to redirect STDERR to STDOUT: $!"; open( STDOUT, '>', '/dev/null' ) or die "Failed to redirect STDOUT to /dev/null: $!"; } exec(@$cmdline) or exit( $! || 127 ); } else { die "fork() failed: $!"; } } if ( !$prog_fh || !$pid ) { $? = -1; ## no critic qw(Variables::RequireLocalizedPunctuationVars) return \$output; } Cpanel::LoadFile::ReadFast::read_all_fast( $prog_fh, $output ); close($prog_fh); waitpid( $pid, 0 ) if $did_fastspawn; return \$output; } sub _call_saferun { my ( $args, $flag ) = @_; my $ref = _saferun_r( $args, $flag || 0 ); return $$ref if $ref; return; } sub saferun { return _call_saferun( \@_, $KEEP_STDERR ); } sub saferunallerrors { return _call_saferun( \@_, $MERGE_STDERR ); } sub saferunnoerror { return _call_saferun( \@_, $NULL_STDERR ); } sub saferunonlyerrors { return _call_saferun( \@_, $NULL_STDOUT ); } 1; } # --- END Cpanel/SafeRun/Simple.pm { # --- BEGIN Cpanel/SafeRun/Errors.pm package Cpanel::SafeRun::Errors; use strict; # use Cpanel::SafeRun::Simple (); sub saferunallerrors { my $output_ref = Cpanel::SafeRun::Simple::_saferun_r( \@_, 1 ); #1 = errors to stdout return wantarray ? split( /\n/, $$output_ref ) : $$output_ref; } sub saferunnoerror { my $output_ref = Cpanel::SafeRun::Simple::_saferun_r( \@_, 2 ); # 2 = errors to devnull return wantarray ? split( /\n/, $$output_ref ) : $$output_ref; } sub saferunonlyerrors { my $output_ref = Cpanel::SafeRun::Simple::_saferun_r( \@_, 3 ); return wantarray ? split( /\n/, $$output_ref ) : $$output_ref; } 1; } # --- END Cpanel/SafeRun/Errors.pm { # --- BEGIN Cpanel/Timezones.pm package Cpanel::Timezones; use cPstrict; # use Cpanel::OS (); # use Cpanel::Debug (); use Cwd (); use constant _ENOENT => 2; our $timezones_cachefile = '/var/cpanel/timezones.cache'; my $cache_ttl = 3_600 * 24 * 7 * 6; #6 WEEKS our $timezones_cache; our $timezonedir = '/usr/share/zoneinfo'; our $zonetabfile = '/usr/share/zoneinfo/zone.tab'; our $etc_localtime = '/etc/localtime'; sub gettimezones() { if ( !$timezones_cache ) { my $timezones; if ( open my $fh, '<', $timezones_cachefile ) { $timezones = -f $fh; $timezones &&= ( time - ( stat $fh )[9] ) < $cache_ttl; $timezones &&= [<$fh>]; chomp @$timezones if $timezones; } elsif ( $! != _ENOENT ) { warn "open($timezones_cachefile): $!"; } if ( !$timezones || !scalar @{$timezones} || !grep { $_ eq "UTC" } @$timezones ) { $timezones = []; get_timezones_from_zonetab($timezones); if ( !scalar @{$timezones} ) { require Cpanel::FileUtils::Lines; require File::Find; File::Find::find( sub { if ( Cpanel::FileUtils::Lines::has_txt_in_file( $File::Find::name, '\ATZif' ) ) { ( my $stripped_name = $File::Find::name ) =~ s{\A$timezonedir/}{}; push @{$timezones}, $stripped_name; } }, $timezonedir ); } push @$timezones, "UTC" unless grep { $_ eq "UTC" } @$timezones; if ( _running_as_root() ) { require Cpanel::FileUtils::Write; Cpanel::FileUtils::Write::overwrite_no_exceptions( $timezones_cachefile, join( "\n", @{$timezones} ), 0644 ); } } $timezones_cache = $timezones; } return wantarray ? @$timezones_cache : $timezones_cache; } sub _running_as_root { return $> == 0 ? 1 : 0; } sub is_valid ($tz) { my $is_valid; require Cpanel::FileUtils::Lines; if ($timezones_cache) { $is_valid = grep { $_ eq $tz } @$timezones_cache; } elsif ( -f $timezones_cachefile && ( time() - ( stat $timezones_cachefile )[9] <= $cache_ttl ) ) { $is_valid = Cpanel::FileUtils::Lines::has_txt_in_file( $timezones_cachefile, qr(\A\Q$tz\E\Z) ); } else { $is_valid = grep { $_ eq $tz } ( gettimezones() ); } return $is_valid; } sub set ($tz) { return unless is_valid($tz); my $method = Cpanel::OS::setup_tz_method(); if ( $method eq 'timedatectl' ) { _setup_clock_using_timedatectl($tz); } elsif ( $method eq 'sysconfig' ) { _setup_clock_using_sysconfig($tz); } else { Cpanel::Debug::log_die("Invalid setup_tz_method method: $method"); } return; } sub _setup_clock_using_timedatectl ($tz) { require Cpanel::SafeRun::Errors; Cpanel::SafeRun::Errors::saferunallerrors( '/usr/bin/timedatectl', 'set-timezone', $tz ); return; } sub _setup_clock_using_sysconfig ($tz) { my @CLOCK; if ( open( my $fh, '<', '/etc/sysconfig/clock' ) ) { @CLOCK = (<$fh>); close($fh); } else { Cpanel::Debug::log_warn(qq[Could not open file "/etc/sysconfig/clock" for reading ($!)]); } if ( open( my $fh, '>', '/etc/sysconfig/clock' ) ) { my $ok; foreach my $line (@CLOCK) { if ( $line =~ m{\AZONE=}i ) { $ok = 1; print {$fh} qq[ZONE="$tz"\n]; } else { print {$fh} $line; } } print {$fh} qq[ZONE="$tz"\n] unless $ok; close($fh); unlink $etc_localtime; symlink "/usr/share/zoneinfo/$tz", $etc_localtime; } else { Cpanel::Debug::log_warn(qq[Could not open file "/etc/sysconfig/clock" for writing ($!)]); } return; } sub get_timezones_from_zonetab ($timezones_ar) { my $fh; if ( !open $fh, '<', $zonetabfile ) { Cpanel::Debug::log_warn("Could not open file \"$zonetabfile\" for reading ($!), will attempt to determine the time zone list by other (probably less precise) method(s)"); return; } while (<$fh>) { next unless m{ ^ [A-Z]+ \s+ [-+][-+\d]+ \s+ (\S+) }ax; push @{$timezones_ar}, $1; } close $fh; return 1; } sub _hash_file ($file) { require Digest::SHA; return Digest::SHA->new(512)->addfile($file)->hexdigest; } sub get_current_timezone (%opts) { my $tz = 'UTC'; # default value when none found if ( -l $etc_localtime ) { # check file being symlinked, if symlinked $tz = Cwd::abs_path($etc_localtime); $tz =~ s{^.*/usr/share/zoneinfo/}{}; } else { my $localhash = _hash_file($etc_localtime); my @TZS = gettimezones(); foreach my $tzfile (@TZS) { my $check = $timezonedir . '/' . $tzfile; next unless -f $check; my $digest = _hash_file($check); if ( $localhash eq $digest ) { $tz = $tzfile; last; } } } $tz =~ s{^posix/}{} if $opts{noposix}; return $tz; } sub calculate_TZ_env() { return get_current_timezone( noposix => 1 ); } sub set_zonetabfile ($file) { $zonetabfile = $file; return; } sub clear_caches() { undef $timezones_cache; unlink $timezones_cachefile; return; } 1; } # --- END Cpanel/Timezones.pm { # --- BEGIN Cpanel/Locale/Utils/DateTime.pm package Cpanel::Locale::Utils::DateTime; use strict; # use Cpanel::LoadModule (); # use Cpanel::Locale (); our $ENCODE_MODULE = 'Encode'; our $DATETIME_MODULE = 'DateTime'; our $DATETIME_LOCALE_MODULE = 'DateTime::Locale'; my %known_ids = (); sub datetime { my ( $lh, $epoch, $format, $timezone ) = @_; if ( $epoch && ref $epoch eq 'ARRAY' ) { $epoch = $epoch->[0]; } elsif ( !$epoch ) { $epoch = time; } $format ||= 'date_format_long'; my $encoding = $lh->encoding(); if ( _can_use_cpanel_date_format( $encoding, $timezone ) ) { Cpanel::LoadModule::load_perl_module('Cpanel::Date::Format'); return Cpanel::Date::Format::translate_for_locale( $epoch, $format, $lh->language_tag() ); } my $locale = _get_best_locale_for_datetime_obj( $lh->language_tag() ); return _get_formatted_datetime( $locale, $encoding, $format, $epoch, $timezone ); } sub _can_use_cpanel_date_format { my ( $encoding, $timezone ) = @_; return ( $encoding eq 'utf-8' ) && ( !$timezone || $timezone eq 'UTC' ); } sub get_lookup_hash_of_multi_epoch_datetime { my ( $lh, $epochs_ar, $format, $timezone ) = @_; $format ||= 'date_format_long'; my %lookups; my $encoding = $lh->encoding(); my $can_use_cpanel_date_format = _can_use_cpanel_date_format( $encoding, $timezone ); my $locale; if ($can_use_cpanel_date_format) { Cpanel::LoadModule::load_perl_module('Cpanel::Date::Format'); $locale = $lh->language_tag(); } else { $locale = _get_best_locale_for_datetime_obj( $lh->language_tag() ); } foreach my $epoch ( @{$epochs_ar} ) { $lookups{$epoch} ||= do { if ($can_use_cpanel_date_format) { Cpanel::Date::Format::translate_for_locale( $epoch, $format, $locale ); } else { _get_formatted_datetime( $locale, $encoding, $format, $epoch, $timezone ); } }; } return \%lookups; } sub _get_formatted_datetime { my ( $locale, $encoding, $format, $epoch, $timezone ) = @_; if ( !$timezone ) { $timezone = 'UTC'; } elsif ( $timezone !~ m{^[\.0-9A-Za-z\/_\+\-]+$} ) { die "Invalid timezone “$timezone”"; } my $datetime_obj = $DATETIME_MODULE->from_epoch( 'epoch' => $epoch, 'locale' => $locale, 'time_zone' => $timezone ); if ( $format && $format !~ m{_format$} && $datetime_obj->{'locale'}->can($format) ) { return $ENCODE_MODULE->can('encode')->( $encoding, $datetime_obj->format_cldr( $datetime_obj->{'locale'}->$format ) ); } die 'Invalid datetime format: ' . $format; } sub _get_best_locale_for_datetime_obj { my ($language_tag) = @_; my ( $fallback, $locale ) = _get_fallback_locale($language_tag); Cpanel::LoadModule::load_perl_module($ENCODE_MODULE) if !$INC{'Encode.pm'}; Cpanel::LoadModule::load_perl_module($DATETIME_MODULE); foreach my $try_locale ( $locale, $fallback, 'en_US', 'en' ) { next if !$try_locale; return $try_locale if $known_ids{$try_locale} || $Cpanel::Locale::known_locales_character_orientation{$try_locale}; if ( eval { $DATETIME_MODULE->load($try_locale) } ) { $known_ids{$try_locale} = 1; return $try_locale; } } die "Could not locale any working DateTime locale"; } sub _get_fallback_locale { my ($locale) = @_; my $fallback; if ( substr( $locale, 0, 2 ) eq 'i_' ) { require Cpanel::Locale::Utils::Paths; my $dir = Cpanel::Locale::Utils::Paths::get_i_locales_config_path(); if ( -e "$dir/$locale.yaml" ) { require Cpanel::DataStore; my $hr = Cpanel::DataStore::fetch_ref("$dir/$locale.yaml"); if ( exists $hr->{'fallback_locale'} && $hr->{'fallback_locale'} ) { $fallback = $hr->{'fallback_locale'}; } } } else { my ( $pre, $pst ) = split( /[\_\-]/, $locale, 2 ); if ($pst) { $fallback = $pre; $locale = $pre . '_' . uc($pst); } } $fallback ||= 'en'; return ( $fallback, $locale ); } 1; } # --- END Cpanel/Locale/Utils/DateTime.pm { # --- BEGIN Cpanel/Time/ISO.pm package Cpanel::Time::ISO; use strict; use warnings; # use Cpanel::Debug (); # use Cpanel::LoadModule (); sub unix2iso { Cpanel::LoadModule::load_perl_module('Cpanel::Time') unless $INC{'Cpanel/Time.pm'}; return sprintf( '%04d-%02d-%02dT%02d:%02d:%02dZ', reverse( ( Cpanel::Time::gmtime( $_[0] || time() ) )[ 0 .. 5 ] ) ); } sub iso2unix { my ($iso_time) = @_; if ( rindex( $iso_time, 'Z' ) != length($iso_time) - 1 ) { die "Only UTC times, not “$iso_time”!"; } my @smhdmy = reverse split m<[^0-9.]>, $iso_time; Cpanel::LoadModule::load_perl_module('Cpanel::Time') unless $INC{'Cpanel/Time.pm'}; return Cpanel::Time::timegm(@smhdmy); } sub unix2iso_date { Cpanel::LoadModule::load_perl_module('Cpanel::Time') unless $INC{'Cpanel/Time.pm'}; Cpanel::Debug::log_deprecated('This function will be removed, please use locale datetime'); return sprintf( '%04d-%02d-%02d', reverse( ( Cpanel::Time::gmtime( $_[0] || time() ) )[ 3 .. 5 ] ) ); } sub unix2iso_time { Cpanel::LoadModule::load_perl_module('Cpanel::Time') unless $INC{'Cpanel/Time.pm'}; Cpanel::Debug::log_deprecated('This function will be removed, please use locale datetime'); return sprintf( '%02d:%02d:%02d', reverse( ( Cpanel::Time::gmtime( $_[0] || time() ) )[ 0 .. 2 ] ) ); } 1; } # --- END Cpanel/Time/ISO.pm { # --- BEGIN Cpanel/Config/LoadUserDomains/Count.pm package Cpanel::Config::LoadUserDomains::Count; use strict; use warnings; # use Cpanel::Autodie qw(exists); INIT { Cpanel::Autodie->import(qw{exists}); } # use Cpanel::LoadFile::ReadFast (); # use Cpanel::ConfigFiles (); sub counttrueuserdomains { if ( !Cpanel::Autodie::exists( _trueuserdomains() ) ) { return 0; } return _count_file_lines( _trueuserdomains() ); } sub countuserdomains { if ( !Cpanel::Autodie::exists( _userdomains() ) ) { return 0; } return _count_file_lines( _userdomains() ) - 1; # -1 for *: nobody } sub _count_file_lines { my ($file) = @_; open( my $ud_fh, '<', $file ) or die "open($file): $!"; my $buffer = ''; Cpanel::LoadFile::ReadFast::read_all_fast( $ud_fh, $buffer ); my $num_ud = ( $buffer =~ tr/\n// ); close($ud_fh) or warn "close($file): $!"; $num_ud++ if length($buffer) && substr( $buffer, -1 ) ne "\n"; return $num_ud; } sub _userdomains { return $Cpanel::ConfigFiles::USERDOMAINS_FILE; } sub _domainusers { return $Cpanel::ConfigFiles::DOMAINUSERS_FILE; } sub _trueuserdomains { return $Cpanel::ConfigFiles::TRUEUSERDOMAINS_FILE; } 1; } # --- END Cpanel/Config/LoadUserDomains/Count.pm { # --- BEGIN Cpanel/Server/Type.pm package Cpanel::Server::Type; use cPstrict; use constant NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE => 1; sub _get_license_file_path { return q{/usr/local/cpanel/cpanel.lisc} } sub _get_dnsonly_file_path { return q{/var/cpanel/dnsonly} } use constant _ENOENT => 2; my @server_config; our %PRODUCTS; our $MAXUSERS; our %FIELDS; our ( $DNSONLY_MODE, $NODE_MODE ); sub is_dnsonly { return $DNSONLY_MODE if defined $DNSONLY_MODE; return 1 if -e _get_dnsonly_file_path(); return 0 if $! == _ENOENT(); my $err = $!; if ( _read_license() ) { return $PRODUCTS{'dnsonly'} ? 1 : 0; } die sprintf( 'stat(%s): %s', _get_dnsonly_file_path(), "$err" ); } sub get_producttype { return $NODE_MODE if defined $NODE_MODE; return 'DNSONLY' unless _read_license(); return 'STANDARD' if $PRODUCTS{'cpanel'}; foreach my $product (qw/dnsnode mailnode databasenode dnsonly/) { return uc($product) if $PRODUCTS{$product}; } return 'DNSONLY'; } sub get_max_users { return $MAXUSERS if defined $MAXUSERS; return NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE unless _read_license(); return $MAXUSERS // NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE; } sub has_els { return $FIELDS{els} if defined $FIELDS{els}; return 0 unless _read_license(); return $FIELDS{els} // 0; } sub get_license_expire_gmt_date { return $FIELDS{'license_expire_gmt_date'} if defined $FIELDS{'license_expire_gmt_date'}; return 0 unless _read_license(); return $FIELDS{'license_expire_gmt_date'} // 0; } sub is_licensed_for_product ($product) { return unless $product; $product = lc $product; return unless _read_license(); return exists $PRODUCTS{$product}; } sub get_features { return unless _read_license(); my @features = split( ",", $FIELDS{'features'} // '' ); return @features; } sub has_feature ( $feature = undef ) { length $feature or return; return ( grep { $_ eq $feature } get_features() ) ? 1 : 0; } sub get_products { return unless _read_license(); return keys %PRODUCTS; } sub _read_license { my $LICENSE_FILE = _get_license_file_path(); my @new_stat = stat($LICENSE_FILE) if @server_config; if ( @server_config && @new_stat && $new_stat[9] == $server_config[9] && $new_stat[7] == $server_config[7] ) { return 1; } open( my $fh, '<', $LICENSE_FILE ) or do { if ( $! != _ENOENT() ) { warn "open($LICENSE_FILE): $!"; } return; }; _reset_cache(); my $content; read( $fh, $content, 1024 ) // do { warn "read($LICENSE_FILE): $!"; $content = q<>; }; return _parse_license_contents_sr( $fh, \$content ); } sub _parse_license_contents_to_hashref ($content_sr) { my %vals = map { ( split( m{: }, $_ ) )[ 0, 1 ] } split( m{\n}, $$content_sr ); return \%vals; } sub _parse_license_contents_sr ( $fh, $content_sr ) { my $vals_hr = _parse_license_contents_to_hashref($content_sr); if ( length $vals_hr->{'products'} ) { %PRODUCTS = map { ( $_ => 1 ) } split( ",", $vals_hr->{'products'} ); } else { return; } if ( length $vals_hr->{'maxusers'} ) { $MAXUSERS //= int $vals_hr->{'maxusers'}; } else { return; } foreach my $field (qw/license_expire_time license_expire_gmt_date support_expire_time updates_expire_time/) { $FIELDS{$field} = $vals_hr->{$field} // 0; } foreach my $field (qw/client features/) { $FIELDS{$field} = $vals_hr->{$field} // ''; } if ( length $vals_hr->{'fields'} ) { foreach my $field ( split( ",", $vals_hr->{'fields'} ) ) { my ( $k, $v ) = split( '=', $field, 2 ); $FIELDS{$k} = $v; } } else { return; } @server_config = stat($fh); return 1; } sub _reset_cache { undef %PRODUCTS; undef %FIELDS; undef @server_config; undef $MAXUSERS; undef $DNSONLY_MODE; return; } 1; } # --- END Cpanel/Server/Type.pm { # --- BEGIN Cpanel/Config/LoadUserDomains.pm package Cpanel::Config::LoadUserDomains; use strict; use warnings; # use Cpanel::Config::LoadConfig (); # use Cpanel::Config::LoadUserDomains::Count (); # use Cpanel::Server::Type (); sub loaduserdomains { my ( $conf_ref, $reverse, $usearr ) = @_; $conf_ref = Cpanel::Config::LoadConfig::loadConfig( Cpanel::Config::LoadUserDomains::Count::_userdomains(), $conf_ref, ': ', # We write the file so there is no need to match stray spaces '0E0', # Avoid looking for comments since there will not be any 0, # reverse 1, # allow_undef_values since there will not be any { 'use_reverse' => $reverse ? 0 : 1, 'skip_keys' => ['nobody'], 'use_hash_of_arr_refs' => ( $usearr || 0 ), } ); if ( !defined($conf_ref) ) { $conf_ref = {}; } return wantarray ? %{$conf_ref} : $conf_ref; } sub loadtrueuserdomains { my ( $conf_ref, $reverse, $ignore_limit ) = @_; $conf_ref = Cpanel::Config::LoadConfig::loadConfig( ( $reverse ? Cpanel::Config::LoadUserDomains::Count::_domainusers() : Cpanel::Config::LoadUserDomains::Count::_trueuserdomains() ), $conf_ref, ': ', # We write the file so there is no need to match stray spaces '0E0', # Avoid looking for comments since there will not be any 0, # reverse 1, # allow_undef_values since there will not be any { 'limit' => ( $ignore_limit ? 0 : Cpanel::Server::Type::get_max_users() ) } ); if ( !defined($conf_ref) ) { $conf_ref = {}; } return wantarray ? %{$conf_ref} : $conf_ref; } *counttrueuserdomains = *counttrueuserdomains = *Cpanel::Config::LoadUserDomains::Count::counttrueuserdomains; 1; } # --- END Cpanel/Config/LoadUserDomains.pm { # --- BEGIN Cpanel/Config/CpUser.pm package Cpanel::Config::CpUser; use strict; # use Cpanel::Debug (); # use Cpanel::LoadModule (); # use Cpanel::Config::LoadUserDomains (); # use Cpanel::Config::LoadCpUserFile (); # use Cpanel::ConfigFiles (); # use Cpanel::FileUtils::Write::JSON::Lazy (); our $cpuser_dir; *cpuser_dir = \$Cpanel::ConfigFiles::cpanel_users; our $cpuser_cache_dir = "$cpuser_dir.cache"; our $header = <{$_} } required_cpuser_keys(); if (@missing) { $user = q{} if !defined $user; Cpanel::Debug::log_warn( "The following keys are missing from supplied '$user' cPanel user data: " . join( ', ', @missing ) . ", to prevent data loss, the data was not saved." ); return; } } if ( grep { $_ && index( $_, "\n" ) != -1 } %$cpuser_ref ) { Cpanel::Debug::log_warn("The cpuser data contains newlines. This is not allowed as it would corrupt the file."); return; } my $domain = $cpuser_ref->{'DOMAIN'}; if ( !$domain ) { # Try to lookup main domain in /etc/trueuserdomains my $trueuserdomains_ref = Cpanel::Config::LoadUserDomains::loadtrueuserdomains( undef, 1 ); $domain = $trueuserdomains_ref->{$user} || ''; if ( !$domain ) { Cpanel::Debug::log_info("Unable to determine user ${user}'s main domain"); } } my %clean_data = ( %$cpuser_ref, DNS => $domain, ); delete @clean_data{ q{}, 'DOMAIN', 'DBOWNER', '__CACHE_DATA_VERSION', ( keys %memory_file_list_key ), }; if ( defined $clean_data{'DISK_BLOCK_LIMIT'} && $clean_data{'DISK_BLOCK_LIMIT'} eq 'unlimited' ) { $clean_data{'DISK_BLOCK_LIMIT'} = 0; } while ( my ( $memkey, $filekey ) = each %memory_file_list_key ) { if ( exists $cpuser_ref->{$memkey} && scalar @{ $cpuser_ref->{$memkey} } ) { my $doms_ar = $cpuser_ref->{$memkey}; my $count = 0; @clean_data{ ( map { $filekey . ++$count } @$doms_ar ) } = @$doms_ar; } } my $homedirs_key_in_file = $memory_file_list_key{'HOMEDIRLINKS'}; if ( exists $clean_data{ $homedirs_key_in_file . 1 } ) { $clean_data{$homedirs_key_in_file} = delete $clean_data{ $homedirs_key_in_file . 1 }; } return wantarray ? %clean_data : \%clean_data; } sub get_cpgid { my ($user) = @_; my $cpgid = 0; if ( exists $INC{'Cpanel/PwCache.pm'} || Cpanel::LoadModule::load_perl_module('Cpanel::PwCache') ) { $cpgid = ( Cpanel::PwCache::getpwnam_noshadow($user) )[3]; } return $cpgid; } sub recache { my ( $cpuser_ref, $user, $cpgid ) = @_; my $user_cache_file = $cpuser_cache_dir . '/' . $user; Cpanel::Config::LoadCpUserFile::create_users_cache_dir(); $cpuser_ref->{'__CACHE_DATA_VERSION'} = $Cpanel::Config::LoadCpUserFile::VERSION; # set this before the cache is written so that it will be included in the cache if ( Cpanel::FileUtils::Write::JSON::Lazy::write_file( $user_cache_file, $cpuser_ref, 0640 ) ) { chown 0, $cpgid, $user_cache_file if $cpgid; # this is ok if the chown happens after as we fall though to reading the non-cache on a failed open } else { unlink $user_cache_file; #outdated } } sub required_cpuser_keys { my @keys = qw( FEATURELIST HASCGI MAXSUB MAXADDON DEMO RS USER MAXFTP MAXLST MAXPARK STARTDATE BWLIMIT IP MAXSQL DOMAIN MAXPOP PLAN OWNER ); return wantarray ? @keys : \@keys; } 1; } # --- END Cpanel/Config/CpUser.pm { # --- BEGIN Cpanel/Config/FlushConfig.pm package Cpanel::Config::FlushConfig; use strict; use warnings; # use Cpanel::FileUtils::Write (); # use Cpanel::Debug (); # use Cpanel::Exception (); our $VERSION = '1.4'; my $DEFAULT_DELIMITER = '='; sub flushConfig { my ( $filename_or_fh, $conf, $delimiter, $header, $opts ) = @_; if ( !$filename_or_fh ) { Cpanel::Debug::log_warn('flushConfig requires valid filename or fh as first argument'); return; } elsif ( !$conf || ref $conf ne 'HASH' ) { Cpanel::Debug::log_warn('flushConfig requires HASH reference as second argument'); return; } if ( ref $opts && $opts->{'no_overwrite'} ) { die Cpanel::Exception::create( 'Unsupported', 'Function ”flushConfig” called with an unsupported option “no_overwrite”.' ); } my $contents_sr = serialize( $conf, do_sort => $opts && $opts->{'sort'}, delimiter => $delimiter, header => $header, allow_array_values => $opts && $opts->{'allow_array_values'}, ); my $perms = 0644; # default permissions when unset if ( defined $opts->{'perms'} ) { $perms = $opts->{'perms'}; } elsif ( !ref $filename_or_fh && -e $filename_or_fh ) { $perms = ( stat(_) )[2] & 0777; } if ( ref $filename_or_fh ) { return Cpanel::FileUtils::Write::write_fh( $filename_or_fh, ref $contents_sr eq 'SCALAR' ? $$contents_sr : $contents_sr ); } return Cpanel::FileUtils::Write::overwrite_no_exceptions( $filename_or_fh, ref $contents_sr eq 'SCALAR' ? $$contents_sr : $contents_sr, $perms, ); } sub serialize { my ( $conf, %opts ) = @_; my ( $do_sort, $delimiter, $header, $allow_array_values ) = @opts{qw(do_sort delimiter header allow_array_values)}; $delimiter ||= $DEFAULT_DELIMITER; if ($allow_array_values) { my $contents = ''; $contents .= $header . "\n" if $header; foreach my $key ( $do_sort ? ( sort keys %{$conf} ) : ( keys %{$conf} ) ) { if ( ref( $conf->{$key} ) eq 'ARRAY' ) { $contents .= join( "\n", map { $key . $delimiter . $_ } ( @{ $conf->{$key} } ) ) . "\n"; } else { $contents .= $key . $delimiter . ( defined $conf->{$key} ? $conf->{$key} : '' ) . "\n"; } } return \$contents; } my $contents = ( $header ? ( $header . "\n" ) : '' ) . join( "\n", map { $_ . ( defined $conf->{$_} ? ( $delimiter . $conf->{$_} ) : '' ) } ( $do_sort ? ( sort keys %{$conf} ) : ( keys %{$conf} ) ) ) . "\n"; return \$contents; } 1; } # --- END Cpanel/Config/FlushConfig.pm { # --- BEGIN Cpanel/Config/CpUser/Write.pm package Cpanel::Config::CpUser::Write; use cPstrict; # use Cpanel::Config::CpUser (); # use Cpanel::Config::FlushConfig (); sub serialize ($cpuser_data) { die 'Pass data through clean_cpuser_hash() first!' if grep { ref } values %$cpuser_data; return ${ Cpanel::Config::FlushConfig::serialize( $cpuser_data, do_sort => 1, delimiter => '=', 'header' => $Cpanel::Config::CpUser::header, ) }; } 1; } # --- END Cpanel/Config/CpUser/Write.pm { # --- BEGIN Cpanel/LinkedNode/Worker/Storage.pm package Cpanel::LinkedNode::Worker::Storage; use strict; use warnings; sub read { my ( $cpuser_hr, $worker_type ) = @_; my $str = $cpuser_hr->{ _get_key($worker_type) }; return _parse($str); } sub set { my ( $cpuser_hr, $worker_type, $alias, $token ) = @_; $cpuser_hr->{ _get_key($worker_type) } = "$alias:$token"; return; } sub unset { my ( $cpuser_hr, $worker_type ) = @_; return _parse( delete $cpuser_hr->{ _get_key($worker_type) } ); } sub _get_key { my ($worker_type) = @_; substr( $worker_type, 0, 1 ) =~ tr<> or do { die "Worker type names always begin with a capital! (given: “$worker_type”)"; }; return "WORKER_NODE-$worker_type"; } sub _parse { my ($str) = @_; return $str ? [ split m<:>, $str, 2 ] : undef; } 1; } # --- END Cpanel/LinkedNode/Worker/Storage.pm { # --- BEGIN Cpanel/SafeFile/Replace.pm package Cpanel::SafeFile::Replace; use strict; use warnings; # use Cpanel::Fcntl::Constants (); # use Cpanel::FileUtils::Open (); use File::Basename (); use constant { WRONLY_CREAT_EXCL => $Cpanel::Fcntl::Constants::O_WRONLY | $Cpanel::Fcntl::Constants::O_CREAT | $Cpanel::Fcntl::Constants::O_EXCL, _EEXIST => 17 }; sub safe_replace_content { my ( $fh, $safelock, @content ) = @_; return locked_atomic_replace_contents( $fh, $safelock, sub { local $!; @content = @{ $content[0] } if scalar @content == 1 && ref $content[0] eq 'ARRAY'; print { $_[0] } @content; if ($!) { my $length = 0; $length += length for @content; my $err = $!; require Cpanel::Exception; die Cpanel::Exception::create( 'IO::WriteError', [ length => $length, error => $err ] ); } return 1; } ); } my $_lock_ex_nb; sub locked_atomic_replace_contents { my ( $fh, $safelock, $coderef ) = @_; $_lock_ex_nb //= $Cpanel::Fcntl::Constants::LOCK_EX | $Cpanel::Fcntl::Constants::LOCK_NB; if ( !flock $fh, $_lock_ex_nb ) { my $err = $!; require Cpanel::Exception; die Cpanel::Exception::create_raw( 'IOError', "locked_atomic_replace_contents could not lock the file handle because of an error: $err" ); } if ( !ref $safelock ) { local $@; if ( !eval { $safelock->isa('Cpanel::SafeFileLock') } ) { die "locked_atomic_replace_contents requires a Cpanel::SafeFileLock object"; } } my $locked_path = $safelock->get_path_to_file_being_locked(); die "locked_path must be valid" if !length $locked_path; my ( $temp_file, $temp_fh, $created_temp_file, $attempts ); my $current_perms = ( stat($fh) )[2] & 07777; while ( !$created_temp_file && ++$attempts < 100 ) { $temp_file = sprintf( '%s-%x-%x-%x', $locked_path, substr( rand, 2 ), scalar( reverse time ), scalar( reverse $$ ), ); my ( $basename, $dirname ); $basename = File::Basename::basename($temp_file); if ( length $basename >= 255 ) { $basename = substr( $basename, 255 ); $dirname = File::Basename::dirname($temp_file); $temp_file = "$dirname/$basename"; } $created_temp_file = Cpanel::FileUtils::Open::sysopen_with_real_perms( $temp_fh, $temp_file, WRONLY_CREAT_EXCL, $current_perms ) or do { last if $! != _EEXIST; }; } if ( !$created_temp_file ) { my $lasterr = $!; die Cpanel::Exception::create( 'TempFileCreateError', [ path => $temp_file, error => $lasterr ] ); } if ( !flock $temp_fh, $Cpanel::Fcntl::Constants::LOCK_EX ) { my $err = $!; require Cpanel::Exception; die Cpanel::Exception::create( 'IO::FlockError', [ path => $temp_file, error => $err, operation => $Cpanel::Fcntl::Constants::LOCK_EX ] ); } select( ( select($temp_fh), $| = 1 )[0] ); ##no critic qw(ProhibitOneArgSelect Variables::RequireLocalizedPunctuationVars) #aka $fd->autoflush(1); if ( $coderef->( $temp_fh, $temp_file, $current_perms ) ) { rename( $temp_file, $locked_path ); return $temp_fh; } local $!; close $temp_fh; unlink $temp_file; die "locked_atomic_replace_contents coderef returns false"; } 1; } # --- END Cpanel/SafeFile/Replace.pm { # --- BEGIN Cpanel/Config/CpUserGuard.pm package Cpanel::Config::CpUserGuard; use strict; use warnings; # use Cpanel::Destruct (); # use Cpanel::Config::CpUser (); # use Cpanel::Config::CpUser::Write (); # use Cpanel::Config::LoadCpUserFile (); # use Cpanel::Debug (); sub new { my ( $class, $user ) = @_; my ( $data, $file, $lock, $is_locked ) = ( undef, undef, undef, 0 ); my $cpuser = Cpanel::Config::LoadCpUserFile::_load_locked($user); if ( $cpuser && ref $cpuser eq 'HASH' ) { $data = $cpuser->{'data'}; $file = $cpuser->{'file'}; $lock = $cpuser->{'lock'}; $is_locked = defined $lock; } else { Cpanel::Debug::log_warn("Failed to load user file for '$user': $!"); return; } my $path = "$Cpanel::Config::CpUser::cpuser_dir/$user"; return bless { user => $user, data => $data, path => $path, _file => $file, _lock => $lock, _pid => $$, is_locked => $is_locked, }; } sub set_worker_node { my ( $self, $worker_type, $worker_alias, $token ) = @_; require Cpanel::LinkedNode::Worker::Storage; Cpanel::LinkedNode::Worker::Storage::set( $self->{'data'}, $worker_type, $worker_alias, $token ); return $self; } sub unset_worker_node { my ( $self, $worker_type ) = @_; require Cpanel::LinkedNode::Worker::Storage; return Cpanel::LinkedNode::Worker::Storage::unset( $self->{'data'}, $worker_type ); } sub save { my ($self) = @_; my $user = $self->{'user'}; my $data = $self->{'data'}; if ( $self->{'_pid'} != $$ ) { Cpanel::Debug::log_die('Locked in parent, cannot save'); return; } if ( !UNIVERSAL::isa( $data, 'HASH' ) ) { Cpanel::Debug::log_die( __PACKAGE__ . ': hash reference required' ); return; } my $clean_data = Cpanel::Config::CpUser::clean_cpuser_hash( $self->{'data'}, $user ); if ( !$clean_data ) { Cpanel::Debug::log_warn("Data for user '$user' was not saved."); return; } if ( !$self->{'_file'} || !$self->{'_lock'} ) { Cpanel::Debug::log_warn("Unable to save user file for '$user': file not open and locked for writing"); return; } require Cpanel::SafeFile::Replace; require Cpanel::Autodie; my $newfh = Cpanel::SafeFile::Replace::locked_atomic_replace_contents( $self->{'_file'}, $self->{'_lock'}, sub { my ($fh) = @_; chmod( 0640, $fh ) or do { warn sprintf( "Failed to set permissions on “%s” to 0%o: %s", $self->{'path'}, 0640, $! ); }; return Cpanel::Autodie::syswrite_sigguard( $fh, Cpanel::Config::CpUser::Write::serialize($clean_data), ); } ) or do { Cpanel::Debug::log_warn("Failed to save user file for “$user”: $!"); }; $self->{'_file'} = $newfh; my $cpgid = Cpanel::Config::CpUser::get_cpgid($user); if ($cpgid) { chown 0, $cpgid, $self->{'path'} or do { Cpanel::Debug::log_warn("Failed to chown( 0, $cpgid, $self->{'path'}): $!"); }; } if ( $INC{'Cpanel/Locale/Utils/User.pm'} ) { Cpanel::Locale::Utils::User::clear_user_cache($user); } Cpanel::Config::CpUser::recache( $data, $user, $cpgid ); require Cpanel::SafeFile; Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} ) or do { Cpanel::Debug::log_warn("Failed to safeclose $self->{'path'}: $!"); }; $self->{'_file'} = $self->{'_lock'} = undef; $self->{'is_locked'} = 0; return 1; } sub abort { my ($self) = @_; my $user = $self->{'user'}; my $data = $self->{'data'}; if ( $self->{'_pid'} != $$ ) { Cpanel::Debug::log_die('Locked in parent, cannot save'); return; } require Cpanel::SafeFile; Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} ); $self->{'_file'} = $self->{'_lock'} = undef; $self->{'is_locked'} = 0; return 1; } sub DESTROY { my ($self) = @_; return unless $self->{'is_locked'}; return if Cpanel::Destruct::in_dangerous_global_destruction(); return unless $self->{'_pid'} == $$; Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} ); $self->{'is_locked'} = 0; return; } 1; } # --- END Cpanel/Config/CpUserGuard.pm { # --- BEGIN Cpanel/Locale/Utils/User/Modify.pm package Cpanel::Locale::Utils::User::Modify; use strict; use warnings; # use Cpanel::PwCache (); sub save_user_locale { my ( $locale, undef, $user ) = @_; $locale ||= 'en'; $user ||= $Cpanel::user || $ENV{'REMOTE_USER'} || ( $> == 0 ? 'root' : ( Cpanel::PwCache::getpwuid_noshadow($>) )[0] ); if ( $user eq 'root' ) { require Cpanel::LoadModule; Cpanel::LoadModule::load_perl_module('Cpanel::DataStore'); my $root_conf_yaml = Cpanel::PwCache::gethomedir('root') . '/.cpanel_config'; my $hr = Cpanel::DataStore::fetch_ref($root_conf_yaml); return 2 if exists $hr->{'locale'} && $hr->{'locale'} eq $locale; $hr->{'locale'} = $locale; return 1 if Cpanel::DataStore::store_ref( $root_conf_yaml, $hr ); return; } elsif ( $> == 0 ) { require Cpanel::Config::CpUserGuard; my $cpuser_guard = Cpanel::Config::CpUserGuard->new($user) or return; $cpuser_guard->{'data'}->{'LOCALE'} = $locale; delete $cpuser_guard->{'data'}->{'LANG'}; delete $cpuser_guard->{'data'}{'__LOCALE_MISSING'}; return $cpuser_guard->save(); } else { require Cpanel::LoadModule; Cpanel::LoadModule::load_perl_module('Cpanel::AdminBin'); return Cpanel::AdminBin::run_adminbin_with_status( 'lang', 'SAVEUSERSETTINGS', $locale, 0, $user )->{'status'}; } return 1; } 1; } # --- END Cpanel/Locale/Utils/User/Modify.pm { # --- BEGIN Cpanel/Version/Tiny.pm package Cpanel::Version::Tiny; use strict; our $VERSION = '11.110.0'; our $VERSION_BUILD = '11.110.0.50'; our $VERSION_TEXT = '110.0 (build 50)'; our $VERSION_DISPLAY = '110.0.50'; our $parent_version = 11; our $major_version = 110; our $minor_version = 0; our $build_number = 50; our $build_time_text = 'Mon Dec 9 15:33:38 2024'; our $buildtime = 1733780018; 1; } # --- END Cpanel/Version/Tiny.pm { # --- BEGIN Cpanel/Version/Full.pm package Cpanel::Version::Full; use strict; my $full_version; our $VERSION_FILE = '/usr/local/cpanel/version'; sub getversion { if ( !$full_version ) { if ( open my $ver_fh, '<', $VERSION_FILE ) { if ( read $ver_fh, $full_version, 32 ) { chomp($full_version); } elsif ($!) { warn "read($VERSION_FILE): $!"; } } else { warn "open($VERSION_FILE): $!"; } if ( !$full_version || $full_version =~ tr{.}{} < 3 ) { require Cpanel::Version::Tiny; $full_version = $Cpanel::Version::Tiny::VERSION_BUILD; } } return $full_version; } sub _clear_cache { undef $full_version; return; } 1; } # --- END Cpanel/Version/Full.pm { # --- BEGIN Cpanel/Version/Compare.pm package Cpanel::Version::Compare; use cPstrict; my %modes = ( '>' => sub ( $check, $against ) { return if $check eq $against; # no need to continue if they are the same return ( cmp_versions( $check, $against ) > 0 ); }, '<' => sub ( $check, $against ) { return if $check eq $against; # no need to continue if they are the same return ( cmp_versions( $check, $against ) < 0 ); }, '==' => sub ( $check, $against ) { return ( $check eq $against || cmp_versions( $check, $against ) == 0 ); }, '!=' => sub ( $check, $against ) { return ( $check ne $against && cmp_versions( $check, $against ) != 0 ); }, '>=' => sub ( $check, $against ) { return 1 if $check eq $against; # no need to continue if they are the same return ( cmp_versions( $check, $against ) >= 0 ); }, '<=' => sub ( $check, $against ) { return 1 if $check eq $against; # no need to continue if they are the same return ( cmp_versions( $check, $against ) <= 0 ); }, '<=>' => sub ( $check, $against ) { return cmp_versions( $check, $against ); }, ); sub compare ( $check, $mode, $against ) { if ( !defined $mode || !exists $modes{$mode} ) { return; } foreach my $ver ( $check, $against ) { $ver //= ''; if ( $ver !~ m{ ^((?:\d+[._]){0,}\d+[a-z]?).*?$ }axms ) { return; } $ver = $1; } $check =~ s/_/\./g; $against =~ s/_/\./g; $check =~ s/([a-z])$/'.' . ord($1)/e; $against =~ s/([a-z])$/'.' . ord($1)/e; my @check_len = split( /[_\.]/, $check ); my @against_len = split( /[_\.]/, $against ); if ( @check_len > 4 ) { return; } elsif ( @check_len < 4 ) { for ( 1 .. 4 - @check_len ) { $check .= '.0'; } } if ( @against_len > 4 ) { return; } elsif ( @against_len < 4 ) { for ( 1 .. 4 - @against_len ) { $against .= '.0'; } } return if $check !~ m { \A \d+\.\d+\.\d+\.\d+ \z }axms; return if $against !~ m { \A \d+\.\d+\.\d+\.\d+ \z }axms; return $modes{$mode}->( $check, $against ); } sub cmp_versions ( $left, $right ) { my ( $maj, $min, $rev, $sup ) = split /[\._]/, $left; my ( $mj, $mn, $rv, $sp ) = split /[\._]/, $right; return $maj <=> $mj || $min <=> $mn || $rev <=> $rv || $sup <=> $sp; } sub get_major_release ( $version = '' ) { $version =~ s/\s*//g; my ( $major, $minor ); if ( $version =~ m/^([0-9]+)\.([0-9]+)/ ) { $major = int $1; $minor = int $2; } else { return; } $minor++ if $minor % 2; return "$major.$minor"; } sub compare_major_release ( $check, $mode, $against ) { return unless defined $check && defined $mode && defined $against; my $maj1 = get_major_release($check); return unless defined $maj1; my $maj2 = get_major_release($against); return unless defined $maj2; return $modes{$mode}->( $maj1, $maj2 ); } 1; } # --- END Cpanel/Version/Compare.pm { # --- BEGIN Cpanel/Version.pm package Cpanel::Version; use strict; use warnings; # use Cpanel::Version::Full (); our ( $VERSION, $MAJORVERSION, $LTS ) = ( '4.0', '11.110', '11.110' ); sub get_version_text { return sprintf( "%d.%d (build %d)", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 1, 2, 3 ] ); } sub get_version_parent { return _ver_key('parent_version'); } sub get_version_display { return sprintf( "%d.%d.%d", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 1, 2, 3 ] ); } { no warnings 'once'; # for updatenow *get_version_full = *Cpanel::Version::Full::getversion; } sub getversionnumber { return sprintf( "%d.%d.%d", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 0, 1, 2 ] ); } sub get_lts { return $LTS; } sub get_short_release_number { my $current_ver = ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[1]; if ( $current_ver % 2 == 0 ) { return $current_ver; } return $current_ver + 1; } sub _ver_key { require Cpanel::Version::Tiny if !$INC{'Cpanel/Version/Tiny.pm'}; return ${ $Cpanel::Version::Tiny::{ $_[0] } }; } sub compare { require Cpanel::Version::Compare; goto &Cpanel::Version::Compare::compare; } sub is_major_version { my ( $ver, $major ) = @_; require Cpanel::Version::Compare; return ( $ver eq $major || Cpanel::Version::Compare::get_major_release($ver) eq $major ) ? 1 : 0; } sub is_development_version { return substr( $MAJORVERSION, -1 ) % 2 ? 1 : 0; } sub display_version { my ($ver) = @_; if ( defined $ver && $ver =~ tr{\.}{} >= 2 ) { my @v = split( m{\.}, $ver ); if ( $v[0] == 11 && $v[1] >= 54 ) { return join( '.', (@v)[ 1, 2, 3 ] ); } return $ver; } return; } 1; } # --- END Cpanel/Version.pm { # --- BEGIN Cpanel/Locale.pm package Cpanel::Locale; use strict; BEGIN { $ENV{'IGNORE_WIN32_LOCALE'} = 1; } # use Cpanel::CPAN::Locale::Maketext::Utils(); our @ISA; BEGIN { push @ISA, qw(Cpanel::CPAN::Locale::Maketext::Utils); } # use Cpanel::Locale::Utils (); # Individual Locale modules depend on this being brought in here, if it is removed they will all need updated. Same for cpanel.pl # use Cpanel::Locale::Utils::Paths (); # use Cpanel::CPAN::Locale::Maketext (); # use Cpanel::Exception (); use constant _ENOENT => 2; BEGIN { local $^H = 0; # cheap no warnings without importing it local $^W = 0; *Cpanel::CPAN::Locale::Maketext::Utils::remove_key_from_lexicons = sub { }; # PPI NO PARSE - loaded above - disabled } our $SERVER_LOCALE_FILE = '/var/cpanel/server_locale'; our $LTR = 1; our $RTL = 2; our %known_locales_character_orientation = ( ar => $RTL, bn => $LTR, bg => $LTR, cs => $LTR, da => $LTR, de => $LTR, el => $LTR, en => $LTR, en_US => $LTR, en_GB => $LTR, es_419 => $LTR, es => $LTR, es_es => $LTR, fi => $LTR, fil => $LTR, fr => $LTR, he => $RTL, hi => $LTR, hu => $LTR, i_cpanel_snowmen => $LTR, i_cp_qa => $LTR, id => $LTR, it => $LTR, ja => $LTR, ko => $LTR, ms => $LTR, nb => $LTR, nl => $LTR, no => $LTR, pl => $LTR, pt_br => $LTR, pt => $LTR, ro => $LTR, ru => $LTR, sl => $LTR, sv => $LTR, th => $LTR, tr => $LTR, uk => $LTR, vi => $LTR, zh => $LTR, zh_tw => $LTR, zh_cn => $LTR, ); my $logger; sub _logger { require Cpanel::Logger; return ( $logger ||= Cpanel::Logger->new() ); } *get_lookup_hash_of_mutli_epoch_datetime = *get_lookup_hash_of_multi_epoch_datetime; sub preinit { if ( exists $INC{'Cpanel.pm'} && !$Cpanel::CPDATA{'LOCALE'} ) { require Cpanel::Locale::Utils::User if !exists $INC{'Cpanel/Locale/Utils/User.pm'}; Cpanel::Locale::Utils::User::init_cpdata_keys(); } if ( $ENV{'HTTP_COOKIE'} ) { require Cpanel::Cookies unless $INC{'Cpanel/Cookies.pm'}; if ( !keys %Cpanel::Cookies ) { %Cpanel::Cookies = %{ Cpanel::Cookies::get_cookie_hashref() }; } } %Cpanel::Grapheme = %{ Cpanel::Locale->get_grapheme_helper_hashref() }; return 1; } sub makevar { return $_[0]->maketext( ref $_[1] ? @{ $_[1] } : @_[ 1 .. $#_ ] ); ## no extract maketext } *maketext = *Cpanel::CPAN::Locale::Maketext::maketext; ## no extract maketext my %singleton_stash = (); BEGIN { no warnings; ## no critic(ProhibitNoWarnings) CHECK { if ( ( $INC{'O.pm'} || $INC{'Cpanel/BinCheck.pm'} || $INC{'Cpanel/BinCheck/Lite.pm'} ) && %singleton_stash ) { die("If you use a locale at begin time, you are responsible for deleting it too. Try calling _reset_singleton_stash\n"); } } } sub _reset_singleton_stash { foreach my $class ( keys %singleton_stash ) { foreach my $args_sig ( keys %{ $singleton_stash{$class} } ) { $singleton_stash{$class}{$args_sig}->cpanel_detach_lexicon(); } } %singleton_stash = (); return 1; } sub get_handle { preinit(); no warnings 'redefine'; *get_handle = *_real_get_handle; goto &_real_get_handle; } sub _map_any_old_style_to_new_style { my (@locales) = @_; if ( grep { !$known_locales_character_orientation{$_} && index( $_, 'i_' ) != 0 } @locales ) { require Cpanel::Locale::Utils::Legacy; goto \&Cpanel::Locale::Utils::Legacy::map_any_old_style_to_new_style; } return @locales; } our $IN_REAL_GET_HANDLE = 0; sub _setup_for_real_get_handle { ## no critic qw(RequireArgUnpacking) if ($IN_REAL_GET_HANDLE) { _load_carp(); if ( $IN_REAL_GET_HANDLE > 1 ) { die 'Cpanel::Carp'->can('safe_longmess')->("Attempted to call _setup_for_real_get_handle from _setup_for_real_get_handle"); } warn 'Cpanel::Carp'->can('safe_longmess')->("Attempted to call _setup_for_real_get_handle from _setup_for_real_get_handle"); if ($Cpanel::Exception::IN_EXCEPTION_CREATION) { # PPI NO PARSE - Only care about this check if the module is loaded $Cpanel::Exception::LOCALIZE_STRINGS = 0; # PPI NO PARSE - Only care about this check if the module is loaded } } local $IN_REAL_GET_HANDLE = $IN_REAL_GET_HANDLE + 1; if ( defined $Cpanel::App::appname && defined $ENV{'REMOTE_USER'} ) { # PPI NO PARSE - Only care about this check if the module is loaded if ( $Cpanel::App::appname eq 'whostmgr' # PPI NO PARSE - Only care about this check if the module is loaded && $ENV{'REMOTE_USER'} ne 'root' ) { require Cpanel::Config::HasCpUserFile; if ( Cpanel::Config::HasCpUserFile::has_readable_cpuser_file( $ENV{'REMOTE_USER'} ) ) { require Cpanel::Config::LoadCpUserFile::CurrentUser; my $cpdata_ref = Cpanel::Config::LoadCpUserFile::CurrentUser::load( $ENV{'REMOTE_USER'} ); if ( scalar keys %{$cpdata_ref} ) { *Cpanel::CPDATA = $cpdata_ref; } } } } my ( $class, @langtags ) = ( $_[0], ( defined $_[1] ? _map_any_old_style_to_new_style( (@_)[ 1 .. $#_ ] ) : exists $Cpanel::Cookies{'session_locale'} && $Cpanel::Cookies{'session_locale'} ? _map_any_old_style_to_new_style( $Cpanel::Cookies{'session_locale'} ) : ( exists $Cpanel::CPDATA{'LOCALE'} && $Cpanel::CPDATA{'LOCALE'} ) ? ( $Cpanel::CPDATA{'LOCALE'} ) : ( exists $Cpanel::CPDATA{'LANG'} && $Cpanel::CPDATA{'LANG'} ) ? ( _map_any_old_style_to_new_style( $Cpanel::CPDATA{'LANG'} ) ) : ( get_server_locale() ) ) ); if ( !$Cpanel::Locale::CDB_File_Path ) { $Cpanel::Locale::CDB_File_Path = Cpanel::Locale::Utils::init_lexicon( 'en', \%Cpanel::Locale::Lexicon, \$Cpanel::Locale::VERSION, \$Cpanel::Locale::Encoding ); } _make_alias_if_needed( @langtags ? @langtags : 'en_us' ); return @langtags; } my %_made_aliases; sub _make_alias_if_needed { foreach my $tag ( grep { ( $_ eq 'en' || $_ eq 'i_default' || $_ eq 'en_us' ) && !$_made_aliases{$_} } ( 'en', @_ ) ) { Cpanel::Locale->make_alias( [$tag], 1 ); $_made_aliases{$tag} = 1; } return 0; } sub _real_get_handle { my ( $class, @arg_langtags ) = @_; my @langtags = _setup_for_real_get_handle( $class, @arg_langtags ); @langtags = map { my $l = $_; $l = 'en' if ( $l eq 'en_us' || $l eq 'i_default' ); $l } grep { $class->cpanel_is_valid_locale($_) } @langtags; @langtags = ('en') unless scalar @langtags; my $args_sig = join( ',', @langtags ) || 'no_args'; return ( ( defined $singleton_stash{$class}{$args_sig} && ++$singleton_stash{$class}{$args_sig}->{'_singleton_reused'} ) ? $singleton_stash{$class}{$args_sig} : ( $singleton_stash{$class}{$args_sig} = Cpanel::CPAN::Locale::Maketext::get_handle( $class, @langtags ) ) ); } sub get_non_singleton_handle { my ( $class, @arg_langtags ) = @_; my @langtags = _setup_for_real_get_handle( $class, @arg_langtags ); return Cpanel::CPAN::Locale::Maketext::get_handle( $class, @langtags ); } sub init { my ($lh) = @_; $lh->SUPER::init(); $lh->_initialize_unknown_phrase_logging(); $lh->_initialize_bracket_notation_whitelist(); return $lh; } sub _initialize_unknown_phrase_logging { my $lh = shift; if ( defined $Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT ) { # PPI NO PARSE - Only needed if loaded my $setter_cr = $lh->can("set_context_${Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT}") or do { # PPI NO PARSE - Only needed if loaded die "Invalid \$Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT: “$Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT”!"; # PPI NO PARSE - Only needed if loaded }; $setter_cr->($lh); } elsif ( defined $Cpanel::Carp::OUTPUT_FORMAT ) { # issafe if ( $Cpanel::Carp::OUTPUT_FORMAT eq 'xml' ) { # issafe $lh->set_context_plain(); # no HTML markup or ANSI escape sequences } elsif ( $Cpanel::Carp::OUTPUT_FORMAT eq 'html' ) { # issafe $lh->set_context_html(); # HTML } } $lh->{'use_external_lex_cache'} = 1; if ( exists $Cpanel::CPDATA{'LOCALE_LOG_MISSING'} && $Cpanel::CPDATA{'LOCALE_LOG_MISSING'} ) { $lh->{'_log_phantom_key'} = sub { my ( $lh, $key ) = @_; my $chain = ''; my $base_class = $lh->get_base_class(); foreach my $class ( $lh->get_language_class, $base_class ) { my $lex_path = $lh->get_cdb_file_path( $class eq $base_class ? 1 : 0 ); next if !$lex_path; $chain .= "\tLOCALE: $class\n\tPATH: $lex_path\n"; last if $class eq 'Cpanel::Locale::en' || $class eq 'Cpanel::Locale::en_us' || $class eq 'Cpanel::Locale::i_default'; } my $pkg = $lh->get_language_tag(); _logger->info( ( $Cpanel::Parser::Vars::file ? "$Cpanel::Parser::Vars::file ::" : '' ) . qq{ Could not find key via '$pkg' locale:\n\tKEY: '$key'\n$chain} ); # PPI NO PARSE -- module will already be there is we care about it }; } return $lh; } our @DEFAULT_WHITELIST = qw(quant asis output current_year list_and list_or comment boolean datetime local_datetime format_bytes get_locale_name get_user_locale_name is_defined is_future join list_and_quoted list_or_quoted numerate numf); sub _initialize_bracket_notation_whitelist { my $lh = shift; my @whitelist = @DEFAULT_WHITELIST; my $custom_whitelist_file = Cpanel::Locale::Utils::Paths::get_custom_whitelist_path(); if ( open( my $fh, '<', $custom_whitelist_file ) ) { while ( my $ln = readline($fh) ) { chomp $ln; push @whitelist, $ln if length($ln); } close $fh; } $lh->whitelist(@whitelist); return $lh; } sub output_cpanel_error { my ( $lh, $position ) = @_; if ( $lh->context_is_ansi() ) { return "\e[1;31m" if $position eq 'begin'; return "\e[0m" if $position eq 'end'; return ''; } elsif ( $lh->context_is_html() ) { return qq{

} if $position eq 'begin'; return '

' if $position eq 'end'; return ''; } else { return ''; # e.g. $lh->context_is_plain() } } sub cpanel_get_3rdparty_lang { my ( $lh, $_3rdparty ) = @_; require Cpanel::Locale::Utils::3rdparty; return Cpanel::Locale::Utils::3rdparty::get_app_setting( $lh, $_3rdparty ) || Cpanel::Locale::Utils::3rdparty::get_3rdparty_lang( $lh, $_3rdparty ) || $lh->get_language_tag() || 'en'; } sub cpanel_is_valid_locale { my ( $lh, $locale ) = @_; my %valid_locales = map { $_ => 1 } ( qw(en en_us i_default), $lh->list_available_locales ); return $valid_locales{$locale} ? 1 : 0; } sub cpanel_get_3rdparty_list { my ($lh) = @_; require Cpanel::Locale::Utils::3rdparty; return Cpanel::Locale::Utils::3rdparty::get_3rdparty_list($lh); } sub cpanel_get_lex_path { my ( $lh, $path, $rv ) = @_; return if !defined $path || $path eq '' || substr( $path, -3 ) ne '.js'; require Cpanel::JS::Variations; my $query = $path; $query = Cpanel::JS::Variations::get_base_file( $query, '-%s.js' ); if ( defined $rv && index( $rv, '%s' ) == -1 ) { substr( $rv, -3, 3, '-%s.js' ); } my $asset_path = $lh->get_asset_file( $query, $rv ); return $asset_path if $asset_path && substr( $asset_path, -3 ) eq '.js' && index( $asset_path, '-' ) > -1; # Only return a value if there is a localized js file here return; } sub tag_is_default_locale { my $tag = $_[1] || $_[0]->get_language_tag(); return 1 if $tag eq 'en' || $tag eq 'en_us' || $tag eq 'i_default'; return; } sub get_cdb_file_path { my ( $lh, $core ) = @_; my $class = $core ? $lh->get_base_class() : $lh->get_language_class(); no strict 'refs'; return $class eq 'Cpanel::Locale::en' || $class eq 'Cpanel::Locale::en_us' || $class eq 'Cpanel::Locale::i_default' ? $Cpanel::Locale::CDB_File_Path : ${ $class . '::CDB_File_Path' }; } sub _slurp_small_file_if_exists_no_exception { my ($path) = @_; local ( $!, $^E ); open my $rfh, '<', $path or do { if ( $! != _ENOENT() ) { warn "open($path): $!"; } return undef; }; read $rfh, my $buf, 8192 or do { warn "read($path): $!"; }; return $buf; } my $_server_locale_file_contents; sub get_server_locale { if ( exists $ENV{'CPANEL_SERVER_LOCALE'} ) { return $ENV{'CPANEL_SERVER_LOCALE'} if $ENV{'CPANEL_SERVER_LOCALE'} !~ tr{A-Za-z0-9_-}{}c; return undef; } if (%main::CPCONF) { return $main::CPCONF{'server_locale'} if exists $main::CPCONF{'server_locale'}; } return ( $_server_locale_file_contents //= ( _slurp_small_file_if_exists_no_exception($SERVER_LOCALE_FILE) || '' ) ); } sub _clear_cache { $_server_locale_file_contents = undef; return; } sub get_locale_for_user_cpanel { if (%main::CPCONF) { return $main::CPCONF{'cpanel_locale'} if exists $main::CPCONF{'cpanel_locale'}; return $main::CPCONF{'server_locale'} if exists $main::CPCONF{'server_locale'}; } require Cpanel::Config::LoadCpConf; my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf_not_copy(); # safe since we do not modify cpconf return $cpconf->{'cpanel_locale'} if $cpconf->{'cpanel_locale'}; # will not be autovivified, 0 and "" are invalid, if the value is invalid they will get 'en' return $cpconf->{'server_locale'} if $cpconf->{'server_locale'}; # will not be autovivified, 0 and "" are invalid, if the value is invalid they will get 'en' return; } sub cpanel_reinit_lexicon { my ($lh) = @_; $lh->cpanel_detach_lexicon(); $lh->cpanel_attach_lexicon(); } my $detach_locale_lex; sub cpanel_detach_lexicon { my ($lh) = @_; my $locale = $lh->get_language_tag(); no strict 'refs'; undef $Cpanel::Locale::CDB_File_Path; if ( $locale ne 'en' && $locale ne 'en_us' && $locale ne 'i_default' ) { $detach_locale_lex = ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' }; undef ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' }; } untie( %{ 'Cpanel::Locale::' . $locale . '::Lexicon' } ); untie %Cpanel::Locale::Lexicon; } sub cpanel_attach_lexicon { my ($lh) = @_; my $locale = $lh->get_language_tag(); $Cpanel::Locale::CDB_File_Path = Cpanel::Locale::Utils::init_lexicon( 'en', \%Cpanel::Locale::Lexicon, \$Cpanel::Locale::VERSION, \$Cpanel::Locale::Encoding ); _make_alias_if_needed($locale); no strict 'refs'; if ( defined $detach_locale_lex ) { ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' } = $detach_locale_lex; } else { ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' } = $Cpanel::Locale::CDB_File_Path; } my $file_path = $lh->get_cdb_file_path(); return if !$file_path; return Cpanel::Locale::Utils::get_readonly_tie( $lh->get_cdb_file_path(), \%{ 'Cpanel::Locale::' . $locale . '::Lexicon' } ); } sub is_rtl { my ($lh) = @_; return 'right-to-left' eq $lh->get_language_tag_character_orientation() ? 1 : 0; } sub get_language_tag_character_orientation { if ( my $direction = $known_locales_character_orientation{ $_[1] || $_[0]->{'fallback_locale'} || $_[0]->get_language_tag() } ) { return 'right-to-left' if $direction == $RTL; return 'left-to-right'; } $_[0]->SUPER::get_language_tag_character_orientation( @_[ 1 .. $#_ ] ); } my $menu_ar; sub get_locale_menu_arrayref { return $menu_ar if $menu_ar; require Cpanel::Locale::Utils::Display; $menu_ar = [ Cpanel::Locale::Utils::Display::get_locale_menu_hashref(@_) ]; # always array context to get all structs, properly uses other args besides object return $menu_ar; } my $non_existent; sub get_non_existent_locale_menu_arrayref { return $non_existent if $non_existent; require Cpanel::Locale::Utils::Display; $non_existent = [ Cpanel::Locale::Utils::Display::get_non_existent_locale_menu_hashref(@_) ]; # always array context to get all structs, properly uses other args besides object return $non_existent; } sub _api1_maketext { require Cpanel::Locale::Utils::Api1; goto \&Cpanel::Locale::Utils::Api1::_api1_maketext; ## no extract maketext } our $api1 = { 'maketext' => { ## no extract maketext 'function' => \&_api1_maketext, ## no extract maketext 'internal' => 1, 'legacy_function' => 2, 'modify' => 'inherit', }, }; sub current_year { return (localtime)[5] + 1900; # we override datetime() so we can't use the internal current_year() } sub local_datetime { my ( $lh, $epoch, $format ) = @_; my $timezone = $ENV{'TZ'} // do { require Cpanel::Timezones; Cpanel::Timezones::calculate_TZ_env(); }; return $lh->datetime( $epoch, $format, $timezone ); } sub datetime { my ( $lh, $epoch, $format, $timezone ) = @_; require Cpanel::Locale::Utils::DateTime; if ( $epoch && $epoch =~ tr<0-9><>c ) { require # do not include it in updatenow.static Cpanel::Validate::Time; Cpanel::Validate::Time::iso_or_die($epoch); require Cpanel::Time::ISO; $epoch = Cpanel::Time::ISO::iso2unix($epoch); } return Cpanel::Locale::Utils::DateTime::datetime( $lh, $epoch, $format, $timezone ); } sub get_lookup_hash_of_multi_epoch_datetime { my ( $lh, $epochs_ar, $format, $timezone ) = @_; require Cpanel::Locale::Utils::DateTime; return Cpanel::Locale::Utils::DateTime::get_lookup_hash_of_multi_epoch_datetime( $lh, $epochs_ar, $format, $timezone ); } sub get_locale_name_or_nothing { my ( $locale, $name, $in_locale_tongue ) = @_; $name ||= $locale->get_language_tag(); if ( index( $name, 'i_' ) == 0 ) { require Cpanel::DataStore; my $i_locales_path = Cpanel::Locale::Utils::Paths::get_i_locales_config_path(); my $i_conf = Cpanel::DataStore::fetch_ref("$i_locales_path/$name.yaml"); return $i_conf->{'display_name'} if $i_conf->{'display_name'}; } else { my $real = $locale->get_language_tag_name( $name, $in_locale_tongue ); return $real if $real; } return; } sub get_locale_name_or_tag { return $_[0]->get_locale_name_or_nothing( $_[1], $_[2] ) || $_[1] || $_[0]->get_language_tag(); } *get_locale_name = *get_locale_name_or_tag; # for shorter BN sub get_user_locale { return $Cpanel::CPDATA{'LOCALE'} if $Cpanel::CPDATA{'LOCALE'}; require Cpanel::Locale::Utils::User; # probably a no-op but just in case since its loading is conditional return Cpanel::Locale::Utils::User::get_user_locale(); } sub get_user_locale_name { require Cpanel::Locale::Utils::User; # probably a no-op but just in case since its loading is conditional return $_[0]->get_locale_name_or_tag( Cpanel::Locale::Utils::User::get_user_locale( $_[1] ) ); } sub set_user_locale { my ( $locale, $country_code ) = @_; if ($country_code) { my $language_name = $locale->lang_names_hashref(); if ( exists $language_name->{$country_code} ) { require Cpanel::Locale::Utils::Legacy; require Cpanel::Locale::Utils::User::Modify; my $language = Cpanel::Locale::Utils::Legacy::get_best_guess_of_legacy_from_locale($country_code); if ( Cpanel::Locale::Utils::User::Modify::save_user_locale( $country_code, $language, $Cpanel::user ) ) { return 1; } } } die Cpanel::Exception::create_raw( "Empty", $locale->maketext("Unable to set locale, please specify a valid country code.") ); } sub get_locales { my $locale = shift; my @listing; my ( $names, $local_names ) = $locale->lang_names_hashref(); foreach ( keys %{$names} ) { push @listing, { locale => $_, name => $names->{$_}, local_name => $local_names->{$_}, direction => ( !defined $known_locales_character_orientation{$_} || $known_locales_character_orientation{$_} == $LTR ) ? 'ltr' : 'rtl' }; } return \@listing; } my $api2_lh; sub api2_get_user_locale { $api2_lh ||= Cpanel::Locale->get_handle(); return ( { 'locale' => $api2_lh->get_user_locale() } ); } sub api2_get_user_locale_name { $api2_lh ||= Cpanel::Locale->get_handle(); return ( { 'name' => $api2_lh->get_user_locale_name() } ); } sub api2_get_locale_name { $api2_lh ||= Cpanel::Locale->get_handle(); my $tag = ( scalar @_ > 2 ) ? {@_}->{'locale'} : $_[1]; return ( { 'name' => $api2_lh->get_locale_name_or_tag($tag) } ); } sub api2_get_encoding { $api2_lh ||= Cpanel::Locale->get_handle(); return ( { 'encoding' => $api2_lh->encoding() } ); } sub api2_numf { my %args = @_; $api2_lh ||= Cpanel::Locale->get_handle(); return ( { 'numf' => $api2_lh->numf( $args{number}, $args{max_decimal_places} ) } ); } sub api2_get_html_dir_attr { $api2_lh ||= Cpanel::Locale->get_handle(); return ( { 'dir' => $api2_lh->get_html_dir_attr() } ); } my $allow_demo = { allow_demo => 1 }; our %API = ( get_locale_name => $allow_demo, get_encoding => $allow_demo, get_html_dir_attr => $allow_demo, get_user_locale => $allow_demo, get_user_locale_name => $allow_demo, numf => $allow_demo, ); sub api2 { my ($func) = @_; return { %{ $API{$func} } } if $API{$func}; return; } my $global_lh; sub lh { return ( $global_lh ||= Cpanel::Locale->get_handle() ); } sub import { my ( $package, @args ) = @_; my ($namespace) = caller; if ( @args == 1 && $args[0] eq 'lh' ) { no strict 'refs'; ## no critic(ProhibitNoStrict) my $exported_name = "${namespace}::lh"; *$exported_name = \*lh; } } sub _load_carp { if ( !$INC{'Cpanel/Carp.pm'} ) { local $@; eval 'require Cpanel::Carp; 1;' or die $@; # hide from perlcc } return; } sub user_feedback_text_for_more_locales { require Cpanel::Version; my $locale = Cpanel::Locale->get_handle(); my $version = Cpanel::Version::get_version_full(); my $survey_url = 'https://cpanel.typeform.com/changeLng?utm_source=cpanel-changelanguage&cpanel_productversion=' . $version; return $locale->maketext( "Don’t see your language of choice? Take our [output,url,_1,Language Support Feedback Survey,class,externalLink,target,Language Survey] to let us know your preferences.", $survey_url ); } 1; } # --- END Cpanel/Locale.pm { # --- BEGIN Cpanel/Sys/Uname.pm package Cpanel::Sys::Uname; use strict; our $SYS_UNAME = 63; our $UNAME_ELEMENTS = 6; our $_UTSNAME_LENGTH = 65; my $UNAME_PACK_TEMPLATE = ( 'c' . $_UTSNAME_LENGTH ) x $UNAME_ELEMENTS; my $UNAME_UNPACK_TEMPLATE = ( 'Z' . $_UTSNAME_LENGTH ) x $UNAME_ELEMENTS; my @uname_cache; sub get_uname_cached { return ( @uname_cache ? @uname_cache : ( @uname_cache = syscall_uname() ) ); } sub clearcache { @uname_cache = (); return; } sub syscall_uname { my $uname; if ( syscall( $SYS_UNAME, $uname = pack( $UNAME_PACK_TEMPLATE, () ) ) == 0 ) { return unpack( $UNAME_UNPACK_TEMPLATE, $uname ); } else { die "The uname() system call failed because of an error: $!"; } return; } 1; } # --- END Cpanel/Sys/Uname.pm { # --- BEGIN Cpanel/Sys/Hostname/Fallback.pm package Cpanel::Sys::Hostname::Fallback; use strict; use warnings; use Socket (); # use Cpanel::Sys::Uname (); sub get_canonical_hostname { my @uname = Cpanel::Sys::Uname::get_uname_cached(); my ( $err, @results ) = Socket::getaddrinfo( $uname[1], 0, { flags => Socket::AI_CANONNAME() } ); if ( @results && $results[0]->{'canonname'} ) { return $results[0]->{'canonname'}; } return undef; } 1; } # --- END Cpanel/Sys/Hostname/Fallback.pm { # --- BEGIN Cpanel/Sys/Hostname.pm package Cpanel::Sys::Hostname; use strict; use warnings; our $VERSION = 2.0; # use Cpanel::Sys::Uname (); our $cachedhostname = ''; sub gethostname { my $nocache = shift || 0; if ( !$nocache && length $cachedhostname ) { return $cachedhostname } my $hostname = _gethostname($nocache); if ( length $hostname ) { $hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn) $cachedhostname = $hostname; } return $hostname; } sub _gethostname { my $nocache = shift || 0; my $hostname; Cpanel::Sys::Uname::clearcache() if $nocache; my @uname = Cpanel::Sys::Uname::get_uname_cached(); if ( $uname[1] && index( $uname[1], '.' ) > -1 ) { $hostname = $uname[1]; $hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn) return $hostname; } eval { require Cpanel::Sys::Hostname::Fallback; $hostname = Cpanel::Sys::Hostname::Fallback::get_canonical_hostname(); }; if ($hostname) { $hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn) return $hostname; } require Cpanel::LoadFile; chomp( $hostname = Cpanel::LoadFile::loadfile( '/proc/sys/kernel/hostname', { 'skip_exists_check' => 1 } ) ); if ($hostname) { $hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn) $hostname =~ tr{\r\n}{}d; # chomp is not enough (not sure if this is required, however we cannot test all kernels so its safer to leave it in) return $hostname; } require Cpanel::Debug; Cpanel::Debug::log_warn('Unable to determine correct hostname'); return; } sub shorthostname { my $hostname = gethostname(); return $hostname if index( $hostname, '.' ) == -1; # Hostname is not a FQDN (this should never happen) return substr( $hostname, 0, index( $hostname, '.' ) ); } 1; } # --- END Cpanel/Sys/Hostname.pm { # --- BEGIN Cpanel/Hostname.pm package Cpanel::Hostname; use strict; use warnings; # use Cpanel::Sys::Hostname (); our $VERSION = 2.0; { no warnings 'once'; *gethostname = *Cpanel::Sys::Hostname::gethostname; *shorthostname = *Cpanel::Sys::Hostname::shorthostname; } 1; } # --- END Cpanel/Hostname.pm { # --- BEGIN Cpanel/Config/CpConfGuard/CORE.pm package Cpanel::Config::CpConfGuard::CORE; use strict; use warnings; # use Cpanel::ConfigFiles (); # use Cpanel::Debug (); # use Cpanel::FileUtils::Write::JSON::Lazy (); # use Cpanel::LoadModule (); # use Cpanel::Config::CpConfGuard (); our $SENDING_MISSING_FILE_NOTICE = 0; my $FILESYS_PERMS = 0644; sub find_missing_keys { my ($self) = @_; _verify_called_as_object_method($self); Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Default'); my $default = 'Cpanel::Config::CpConfGuard::Default'->new( current_config => $self->{data}, current_changes => $self->{changes}, ); if ( $self->{'is_missing'} ) { if ( UNIVERSAL::isa( $self->{'cache'}, 'HASH' ) && %{ $self->{'cache'} } ) { $self->{'data'} = {}; %{ $self->{'data'} } = %{ $self->{'cache'} }; my $config = $self->{'data'}; foreach my $key ( $default->get_keys() ) { next if exists $config->{$key}; $config->{$key} = $default->get_default_for($key); } } else { $self->{'data'} = $default->get_all_defaults(); } $self->{'modified'} = 1; # Mark as save needed. return; } my $cache = $self->{'cache'}; undef( $self->{'cache'} ); # we do not need the cache after the first pass my $config = $self->{'data'}; my $changes = $self->{'changes'}; # used for notifications $config->{'tweak_unset_vars'} ||= ''; foreach my $key ( $default->get_keys() ) { next if exists $config->{$key}; $self->{'modified'} = 1; # Mark as save needed. if ( exists $cache->{$key} ) { $config->{$key} = $cache->{$key}; $changes->{'from_cache'} ||= []; push @{ $changes->{'from_cache'} }, $key; $changes->{'changed_keys'} ||= {}; $changes->{'changed_keys'}{$key} = 'from_cache'; next; } my $changes_type = $default->is_dynamic($key) ? 'from_dynamic' : 'from_default'; $changes->{'changed_keys'} ||= {}; $changes->{'changed_keys'}{$key} = $changes_type; $changes->{$changes_type} ||= []; push @{ $changes->{$changes_type} }, $key; $config->{$key} = $default->get_default_for($key); } foreach my $key ( @{ $default->dead_variables() } ) { next unless exists $config->{$key}; $self->{'modified'} = 1; # Mark as save needed. delete( $config->{$key} ); $changes->{'dead_variable'} ||= []; push @{ $changes->{'dead_variable'} }, $key; } return; } sub validate_keys { my ($self) = @_; _verify_called_as_object_method($self); Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Validate'); my $invalid = 'Cpanel::Config::CpConfGuard::Validate'->can('patch_cfg')->( $self->{'data'} ); if (%$invalid) { $self->{modified} = 1; $self->{'changes'}->{'invalid'} = $invalid; } return; } sub notify_and_save_if_changed { my ($self) = @_; _verify_called_as_object_method($self); return if !$self->{'use_lock'}; return if !$self->{'modified'}; my $config = $self->{'data'}; if ( $ENV{'CPANEL_BASE_INSTALL'} ) { ; # Do nothing for notification. } elsif ( $self->{'is_missing'} ) { $config->{'tweak_unset_vars'} = ''; Cpanel::Debug::log_warn("Missing cpanel.config regenerating …"); $self->notify_missing_file; } elsif ( %{ $self->{'changes'} } ) { my $changes = $self->{'changes'}; my %uniq = map { $_ => 1 } @{ $changes->{'from_default'} || [] }, @{ $changes->{'from_dynamic'} || [] }, split( /\s*,\s*/, $config->{'tweak_unset_vars'} ); $config->{'tweak_unset_vars'} = join ",", sort keys %uniq; $self->log_missing_values(); } return $self->save( keep_lock => 1 ); } sub _server_locale { my ($self) = @_; _verify_called_as_object_method($self); my $locale_name = $self->{'data'}->{'server_locale'} || 'en'; require Cpanel::Locale; return Cpanel::Locale->_real_get_handle($locale_name); } sub _longest { my @array = @_; return length( ( sort { length $b <=> length $a } @array )[0] ); } sub _stringify_undef { my $value = shift; return defined $value ? $value : ''; } sub log_missing_values { my ($self) = @_; require Cpanel::Hostname; my $changes = $self->{'changes'}; my $locale = $self->_server_locale(); my $hostname = Cpanel::Hostname::gethostname(); my $prev = $locale->set_context_plain(); my $message = ''; $message .= $locale->maketext( 'One or more key settings for “[_1]” were either not found in [asis,cPanel amp() WHM]’s server configuration file ([_2]), or were present but did not pass validation.', $hostname, $self->{'path'} ) . "\n"; if ( $changes->{'from_dynamic'} ) { $message .= $locale->maketext('The following settings were absent and have been selected based on the current state of your installation.'); $message .= "\n"; my @keys = @{ $changes->{'from_dynamic'} }; my $max_len = _longest(@keys) + 2; foreach my $key (@keys) { $message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) ); } $message .= "\n"; } if ( $changes->{'from_cache'} ) { $message .= $locale->maketext('The following settings were absent, but were restored from your [asis,cpanel.config.cache] file:'); $message .= "\n"; my @keys = @{ $changes->{'from_cache'} }; my $max_len = _longest(@keys) + 2; foreach my $key (@keys) { $message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) ); } $message .= "\n"; } if ( $changes->{'from_default'} or $changes->{'invalid'} ) { $message .= $locale->maketext('The following settings were absent or invalid. Your server has copied the defaults for them from the configuration defaults file ([asis,/usr/local/cpanel/etc/cpanel.config]).'); $message .= "\n"; if ( $changes->{'from_default'} ) { my @keys = @{ $changes->{'from_default'} }; my $max_len = _longest(@keys) + 2; foreach my $key (@keys) { $message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) ); } } if ( $changes->{'invalid'} ) { my $invalid = $changes->{'invalid'}; my @keys = keys %$invalid; my $max_len = _longest(@keys) + 2; foreach my $key (@keys) { $message .= sprintf( " %-${max_len}s= %s (Previously set to '%s')\n", $key, _stringify_undef( $invalid->{$key}->{'to'} ), _stringify_undef( $invalid->{$key}->{'from'} ) ); } } $message .= "\n"; } if ( $changes->{'dead_variable'} ) { $message .= $locale->maketext('The following settings are obsolete and have been removed from the server configuration file:'); $message .= "\n"; $message .= ' ' . join( ', ', @{ $changes->{'dead_variable'} } ); $message .= "\n\n"; } $message .= $locale->maketext( 'Read the [asis,cpanel.config] file [output,url,_1,documentation] for important information about this file.', 'https://go.cpanel.net/cpconfig' ); $message .= "\n\n"; Cpanel::Debug::logger(); # initialize the logger local $Cpanel::Logger::ENABLE_BACKTRACE = 0; foreach my $chunk ( split( /\n+/, $message ) ) { Cpanel::Debug::log_warn($chunk); } $locale->set_context($prev); return; } sub notify_missing_file { my ($self) = @_; if ($SENDING_MISSING_FILE_NOTICE) { return; #Already sending notification, don't double up } require Cpanel::Hostname; local $SENDING_MISSING_FILE_NOTICE = 1; my $locale = $self->_server_locale(); my $prev = $locale->set_context_plain(); my @to_log; my %critical_values; my $hostname = Cpanel::Hostname::gethostname(); push @to_log, $locale->maketext('Your server has copied the defaults from your cache and the configuration defaults file ([asis,/usr/local/cpanel/etc/cpanel.config]) to [asis,/var/cpanel/cpanel.config], and it has generated the following critical values:'); Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Default'); my $critical = Cpanel::Config::CpConfGuard::Default::critical_values(); my $max_len = _longest(@$critical) + 2; my $critical_value; foreach my $key ( sort @$critical ) { $critical_value = _stringify_undef( $self->{'data'}->{$key} ); $critical_values{$key} = $critical_value; push @to_log, sprintf( " %-${max_len}s= %s\n", $key, $critical_value ); } push @to_log, $locale->maketext( 'Read the [asis,cpanel.config] file [output,url,_1,documentation] for more information about this file.', 'https://go.cpanel.net/cpconfig' ) . ' '; Cpanel::Debug::logger(); # initialize the logger local $Cpanel::Logger::ENABLE_BACKTRACE = 0; foreach my $chunk (@to_log) { chomp $chunk; Cpanel::Debug::log_warn($chunk); } _icontact( \%critical_values ); $locale->set_context($prev); return; } sub _icontact { my $critical_values = shift; Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::Config::CpConfGuard"); Cpanel::LoadModule::load_perl_module('Cpanel::Notify'); 'Cpanel::Notify'->can('notification_class')->( 'class' => 'Config::CpConfGuard', 'application' => 'Config::CpConfGuard', 'constructor_args' => [ 'origin' => 'cpanel.config', 'critical_values' => $critical_values, ] ); return; } sub save { my ( $self, %opts ) = @_; _verify_called_as_object_method($self); return unless ( $self->{'use_lock'} ); return if ( $] > 5.007 && $] < 5.014 ); return 1 if $Cpanel::Config::CpConfGuard::memory_only; if ( !$self->{'rw'} ) { Cpanel::LoadModule::load_perl_module('Cpanel::SafeFile'); $self->{'fh'} = 'Cpanel::SafeFile'->can('safereopen')->( $self->{'fh'}, '+>', $Cpanel::ConfigFiles::cpanel_config_file ); return $self->abort('Cannot reopen file for rw') unless $self->{'fh'}; $self->{'rw'} = 1; } return $self->abort('Locked in parent, cannot save') if $self->{'pid'} != $$; return $self->abort('hash reference required') if !UNIVERSAL::isa( $self->{'data'}, 'HASH' ); Cpanel::LoadModule::load_perl_module('Cpanel::Config::FlushConfig'); Cpanel::LoadModule::load_perl_module('Cpanel::Config::SaveCpConf'); 'Cpanel::Config::FlushConfig'->can('flushConfig')->( $self->{'fh'}, $self->{'data'}, '=', 'Cpanel::Config::SaveCpConf'->can('header_message')->(), { sort => 1, perms => $FILESYS_PERMS, }, ); %{$Cpanel::Config::CpConfGuard::MEM_CACHE} = %{ $self->{'data'} }; return 1 if $opts{keep_lock}; $self->release_lock; return 1; } sub _update_cache { my ($self) = @_; _verify_called_as_object_method($self); return 0 if Cpanel::Config::CpConfGuard::_cache_is_valid() && $self->{'cache_is_valid'}; # Don't re-write the file if it looks correct. $Cpanel::Config::CpConfGuard::MEM_CACHE_CPANEL_CONFIG_MTIME = ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0; return unless $self->{'use_lock'}; # never update the cache when not root local $@; my $ok = eval { Cpanel::FileUtils::Write::JSON::Lazy::write_file( $Cpanel::ConfigFiles::cpanel_config_cache_file, $Cpanel::Config::CpConfGuard::MEM_CACHE, $FILESYS_PERMS ) || 0 }; if ( !$ok ) { if ( !defined $ok ) { Cpanel::Debug::log_warn("Cannot update cache file: $Cpanel::ConfigFiles::cpanel_config_cache_file $@"); unlink $Cpanel::ConfigFiles::cpanel_config_cache_file; return -1; } return; } my $past = ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] - 1; return _adjust_timestamp_for( $Cpanel::ConfigFiles::cpanel_config_file => $past ); } sub _adjust_timestamp_for { my ( $f, $time ) = @_; return unless defined $f && defined $time; return 1 if utime( $time, $time, $f ); my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time); my $stamp = sprintf( "%04d%02d%02d%02d%02d.%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec ); unless ( _touch( $f => $stamp ) ) { Cpanel::Debug::log_warn("Cannot update mtime on $f: $@"); return; } return 1; } sub _touch { # mainly created to easily mock that part during the tests my ( $f, $stamp ) = @_; return system( 'touch', '-t', $stamp, $f ) == 0 ? 1 : 0; } sub _verify_called_as_object_method { if ( ref( $_[0] ) ne "Cpanel::Config::CpConfGuard" ) { die '' . ( caller(0) )[3] . " was not called as an object method [" . ref( $_[0] ) . "]\n"; } return; } sub abort { my ( $self, $msg ) = @_; _verify_called_as_object_method($self); if ( $self->{'pid'} != $$ ) { Cpanel::Debug::log_die('Locked in parent, cannot release lock'); return; } $self->release_lock(); Cpanel::Debug::log_die($msg) if $msg; return 1; } sub set { my ( $self, $k, $v ) = @_; _verify_called_as_object_method($self); return unless defined $k; my $config = $self->{'data'}; $config->{$k} = $v; if ( $config->{'tweak_unset_vars'} && index( $config->{'tweak_unset_vars'}, $k ) > -1 ) { my %unset = map { ( $_ => 1 ) } split( /\s*,\s*/, $config->{'tweak_unset_vars'} ); delete( $unset{$k} ); $config->{'tweak_unset_vars'} = join( ',', sort keys %unset ); } return 1; } 1; } # --- END Cpanel/Config/CpConfGuard/CORE.pm { # --- BEGIN Cpanel/Config/CpConfGuard.pm package Cpanel::Config::CpConfGuard; use strict; use warnings; # use Cpanel::JSON::FailOK (); # use Cpanel::ConfigFiles (); # use Cpanel::Debug (); # use Cpanel::Destruct (); use constant { _ENOENT => 2, }; our $IN_LOAD = 0; our $SENDING_MISSING_FILE_NOTICE = 0; my $FILESYS_PERMS = 0644; my $is_daemon; BEGIN { $is_daemon = 0; # initialize the value in the begin block if ( index( $0, 'updatenow' ) > -1 || index( $0, 'cpsrvd' ) > -1 || index( $0, 'cpdavd' ) > -1 || index( $0, 'queueprocd' ) > -1 || index( $0, 'tailwatchd' ) > -1 || index( $0, 'cpanellogd' ) > -1 || ( length $0 > 7 && substr( $0, -7 ) eq '.static' ) ) { $is_daemon = 1; } } my $module_file; our ( $MEM_CACHE_CPANEL_CONFIG_MTIME, $MEM_CACHE ) = ( 0, undef ); our $memory_only; sub _is_daemon { $is_daemon }; # for testing sub clearcache { $MEM_CACHE_CPANEL_CONFIG_MTIME = 0; $MEM_CACHE = undef; return; } sub new { my ( $class, %opts ) = @_; Cpanel::JSON::FailOK::LoadJSONModule() if !$is_daemon && !$INC{'Cpanel/JSON.pm'}; my $self = bless { %opts, # to be improved 'path' => $Cpanel::ConfigFiles::cpanel_config_file, 'pid' => $$, 'modified' => 0, 'changes' => {}, }, $class; $self->{'use_lock'} //= ( $> == 0 ) ? 1 : 0; if ($memory_only) { $self->{'data'} = ref($memory_only) eq 'HASH' ? $memory_only : {}; return $self; } ( $self->{'cache'}, $self->{'cache_is_valid'} ) = get_cache(); return $self if $self->{'loadcpconf'} && $self->{'cache_is_valid'}; $self->load_cpconf_file(); return $self if $is_daemon || $opts{'no_validate'} || !$self->{'use_lock'}; $self->find_missing_keys(); $self->validate_keys(); $self->notify_and_save_if_changed(); return $self; } sub set { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::set; } sub config_copy { my ($self) = @_; _verify_called_as_object_method($self); my $config = $self->{'data'} || $self->{'cache'} || {}; return {%$config}; } sub find_missing_keys { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::find_missing_keys; } sub validate_keys { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::validate_keys; } sub notify_and_save_if_changed { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::notify_and_save_if_changed; } sub log_missing_values { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::log_missing_values; } sub notify_missing_file { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::notify_missing_file; } sub save { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::save; } sub release_lock { my ($self) = @_; _verify_called_as_object_method($self); return unless $self->{'use_lock'} && defined $self->{'pid'} && $self->{'pid'} eq $$ && $self->{'lock'}; require Cpanel::SafeFile; Cpanel::SafeFile::safeclose( $self->{'fh'}, $self->{'lock'}, sub { return $self->_update_cache() } ); $self->{'fh'} = $self->{'lock'} = undef; $self->{'is_locked'} = 0; return; } sub abort { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::abort; } sub _update_cache { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::_update_cache; } sub _server_locale { require Cpanel::Config::CpConfGuard::CORE; goto \&Cpanel::Config::CpConfGuard::CORE::_server_locale; } sub get_cache { my $cpanel_config_mtime = ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0; my $verbose = ( defined $Cpanel::Debug::level ? $Cpanel::Debug::level : 0 ) >= 5; if ( $MEM_CACHE && ref($MEM_CACHE) eq 'HASH' && $cpanel_config_mtime && $cpanel_config_mtime == $MEM_CACHE_CPANEL_CONFIG_MTIME ) { Cpanel::Debug::log_info("loadcpconf memory cache hit") if $verbose; return ( $MEM_CACHE, 1 ); } clearcache(); # Invalidate the memory cache. Cpanel::Debug::log_info("loadcpconf memory cache miss") if $verbose; my $mtime_before_read; if ( !$INC{'Cpanel/JSON.pm'} ) { Cpanel::Debug::log_info("Cpanel::JSON not loaded. Skipping cache load.") if $verbose; return ( undef, 0 ); } elsif ( -e $Cpanel::ConfigFiles::cpanel_config_cache_file ) { # No need to do -r (costs 5 additional syscalls) since we write this 0644 $mtime_before_read = ( stat _ )[9] || 0; } else { Cpanel::Debug::log_info("The cache file “$Cpanel::ConfigFiles::cpanel_config_cache_file” could not be read. Skipping cache load.") if $verbose; return ( undef, 0 ); } my ( $mtime_after_read, $cpconf_ref ) = (0); my $loop_count = 0; while ( $mtime_after_read != $mtime_before_read && $loop_count++ < 10 ) { sleep 1 if ( $mtime_after_read == time ); # If it was just written to, give it a second in case it's being written to. Cpanel::Debug::log_info( "loadcpconf cache_filesys_mtime = $mtime_before_read , filesys_mtime: $cpanel_config_mtime , memory_mtime: $MEM_CACHE_CPANEL_CONFIG_MTIME , now: " . time ) if $verbose; $cpconf_ref = Cpanel::JSON::FailOK::LoadFile($Cpanel::ConfigFiles::cpanel_config_cache_file); $mtime_after_read = ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] || 0; sleep 1 if ( $mtime_after_read != $mtime_before_read ); } if ( $cpconf_ref && scalar keys %{$cpconf_ref} ) { if ( _cache_is_valid( $cpanel_config_mtime, $mtime_after_read ) ) { Cpanel::Debug::log_info("loadcpconf file system cache hit") if $verbose; ( $MEM_CACHE, $MEM_CACHE_CPANEL_CONFIG_MTIME ) = ( $cpconf_ref, $cpanel_config_mtime ); return ( $cpconf_ref, 1 ); } Cpanel::Debug::log_info("loadcpconf cpanel.config.cache miss.") if $verbose; return ( $cpconf_ref, 0 ); } Cpanel::Debug::log_info("loadcpconf cpanel.config.cache miss.") if $verbose; return ( undef, 0 ); } sub _cache_is_valid { my ( $config_mtime, $cache_mtime ) = @_; $cache_mtime ||= ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] || 0; return 0 unless $cache_mtime; $config_mtime ||= ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0; return 0 unless $config_mtime; return ( $config_mtime + 1 == $cache_mtime ) ? 1 : 0; } sub load_cpconf_file { my ($self) = @_; if ($IN_LOAD) { require Cpanel::Carp; die Cpanel::Carp::safe_longmess("Load loop detected"); } local $IN_LOAD = 1; _verify_called_as_object_method($self); my $config = {}; my $config_file = $Cpanel::ConfigFiles::cpanel_config_file; $self->{'is_missing'} = ( -e $config_file ) ? 0 : 1; return if ( !$self->{'use_lock'} && $self->{'is_missing'} ); # We can't do anything if the file is missing and we're not root. ABORT! if ( $self->{'use_lock'} && $self->{'is_missing'} ) { if ( open( my $touch_fh, '>>', $config_file ) ) { print {$touch_fh} ''; close $touch_fh; chown 0, 0, $config_file; # avoid pulling in Cpanel::PwCache for memory reasons chmod 0644, $config_file; } } $self->{'rw'} = 0; $self->{'rw'} = 1 if ( $self->{'use_lock'} && !$self->{'cache_is_valid'} ); require Cpanel::Config::LoadConfig; my ( $ref, $fh, $conflock, $err ) = Cpanel::Config::LoadConfig::loadConfig( $Cpanel::ConfigFiles::cpanel_config_file, $config, (undef) x 4, { 'keep_locked_open' => !!$self->{'use_lock'}, 'nocache' => 1, 'rw' => $self->{'rw'}, 'allow_undef_values' => 1, }, ); if ( !$ref && !$fh && $! != _ENOENT() ) { $err ||= '(unknown error)'; require Cpanel::Carp; die Cpanel::Carp::safe_longmess("Can’t read “$Cpanel::ConfigFiles::cpanel_config_file” ($err)"); } $self->{'fh'} = $fh; $self->{'lock'} = $conflock; $self->{'data'} = $config; if ( $self->{'use_lock'} ) { Cpanel::Debug::log_warn("Failed to establish lock on $Cpanel::ConfigFiles::cpanel_config_file") unless $self->{'lock'}; Cpanel::Debug::log_warn("Failed to get file handle for $Cpanel::ConfigFiles::cpanel_config_file") unless $self->{'fh'}; } $self->{'is_locked'} = defined $self->{'lock'} ? 1 : 0; # alias for external usage if ( !$MEM_CACHE ) { $MEM_CACHE = {}; %$MEM_CACHE = %$config; } return; } sub _verify_called_as_object_method { if ( ref( $_[0] ) ne __PACKAGE__ ) { die '' . ( caller(0) )[3] . " was not called as an object method [" . ref( $_[0] ) . "]\n"; } return; } sub DESTROY { ## no critic(RequireArgUnpacking) return 1 if ( $> || $memory_only ); # Special modes we don't or won't write to cpanel.config files. return 2 if ( !$_[0] || !keys %{ $_[0] } ); # Nothing to cleanup if we're just a blessed empty hash. return if !$_[0]->{'lock'}; return if Cpanel::Destruct::in_dangerous_global_destruction(); $_[0]->release_lock(); # Close the file so we can update the cache properly. return; } 1; } # --- END Cpanel/Config/CpConfGuard.pm { # --- BEGIN Cpanel/Config/LoadCpConf.pm package Cpanel::Config::LoadCpConf; use strict; use warnings; # use Cpanel::Config::CpConfGuard (); sub loadcpconf { my $cpconf = Cpanel::Config::CpConfGuard->new( 'loadcpconf' => 1 )->config_copy; return wantarray ? %$cpconf : $cpconf; } sub loadcpconf_not_copy { if ( !defined $Cpanel::Config::CpConfGuard::memory_only && $Cpanel::Config::CpConfGuard::MEM_CACHE_CPANEL_CONFIG_MTIME ) { my ( $cache, $cache_is_valid ) = Cpanel::Config::CpConfGuard::get_cache(); if ($cache_is_valid) { return wantarray ? %$cache : $cache; } } my $cpconf_obj = Cpanel::Config::CpConfGuard->new( 'loadcpconf' => 1 ); my $cpconf = $cpconf_obj->{'data'} || $cpconf_obj->{'cache'} || {}; return wantarray ? %$cpconf : $cpconf; } sub clearcache; *clearcache = *Cpanel::Config::CpConfGuard::clearcache; 1; } # --- END Cpanel/Config/LoadCpConf.pm { # --- BEGIN Cpanel/Maxmem.pm package Cpanel::Maxmem; use strict; use warnings; # use Cpanel::Config::LoadUserDomains::Count (); use constant _INITIAL_DEFAULT => 4096; sub _count_domains { return eval { Cpanel::Config::LoadUserDomains::Count::countuserdomains() } // 1; } sub minimum { return _INITIAL_DEFAULT() * ( 1 + int( _count_domains() / 10_000 ) ); } *default = *minimum; 1; } # --- END Cpanel/Maxmem.pm { # --- BEGIN Cpanel/OSSys/Bits.pm package Cpanel::OSSys::Bits; use strict; use warnings; our $MAX_32_BIT_SIGNED; our $MAX_32_BIT_UNSIGNED; our $MAX_64_BIT_SIGNED; our $MAX_64_BIT_UNSIGNED; our $MAX_NATIVE_SIGNED; our $MAX_NATIVE_UNSIGNED; sub getbits { return length( pack( 'l!', 1000 ) ) * 8; } BEGIN { $MAX_32_BIT_UNSIGNED = ( 1 << 32 ) - 1; $MAX_32_BIT_SIGNED = ( 1 << 31 ) - 1; $MAX_64_BIT_UNSIGNED = ~0; #true on both 32- and 64-bit systems $MAX_64_BIT_SIGNED = -1 >> 1; #true on both 32- and 64-bit systems if ( getbits() == 32 ) { $MAX_NATIVE_SIGNED = $MAX_32_BIT_SIGNED; $MAX_NATIVE_UNSIGNED = $MAX_32_BIT_UNSIGNED; } else { $MAX_NATIVE_SIGNED = $MAX_64_BIT_SIGNED; $MAX_NATIVE_UNSIGNED = $MAX_64_BIT_UNSIGNED; } } 1; } # --- END Cpanel/OSSys/Bits.pm { # --- BEGIN Cpanel/Sys/Rlimit.pm package Cpanel::Sys::Rlimit; use strict; use warnings; # use Cpanel::OSSys::Bits (); # use Cpanel::Pack (); # use Cpanel::Syscall (); my $SYS_getrlimit; my $SYS_setrlimit; our $RLIM_INFINITY; # denotes no limit on a resource our %RLIMITS = ( 'CPU' => 0, # CPU time limit in seconds. 'DATA' => 2, # The maximum size of the process's data segment 'CORE' => 4, # Maximum size of a core file 'RSS' => 5, # Specifies the limit (in pages) of the process's resident set 'NPROC' => 6, # The maximum number of processes 'NOFILE' => 7, # The maximum number of file descriptors 'AS' => 9, # The maximum size of the process's virtual memory 'FSIZE' => 1, 'STACK' => 3, 'MEMLOCK' => 8, 'LOCKS' => 10, 'SIGPENDING' => 11, 'MSGQUEUE' => 12, 'NICE' => 13, 'RTPRIO' => 14, 'RTTIME' => 15, ); BEGIN { $RLIM_INFINITY = $Cpanel::OSSys::Bits::MAX_NATIVE_UNSIGNED; } our $PACK_TEMPLATE = 'L!L!'; our @TEMPLATE = ( rlim_cur => 'L!', # unsigned long rlim_max => 'L!', # unsigned long ); sub getrlimit { my ($rlimit) = @_; local $!; die "getrlimit requires an rlimit constant" if !defined $rlimit; my $buffer = pack( $PACK_TEMPLATE, 0 ); my $rlimit_num = _rlimit_to_num($rlimit); Cpanel::Syscall::syscall( 'getrlimit', $rlimit_num, $buffer ); my $getrlimit_hr = Cpanel::Pack->new( \@TEMPLATE )->unpack_to_hashref($buffer); return ( $getrlimit_hr->{'rlim_cur'}, $getrlimit_hr->{'rlim_max'} ); } sub setrlimit { my ( $rlimit, $soft, $hard ) = @_; local $!; die "setrlimit requires an rlimit constant" if !defined $rlimit; die "setrlimit requires a soft limit" if !defined $soft; die "setrlimit requires a hard limit" if !defined $hard; my $buffer = pack( $PACK_TEMPLATE, $soft, $hard ); my $rlimit_num = _rlimit_to_num($rlimit); Cpanel::Syscall::syscall( 'setrlimit', $rlimit_num, $buffer ); return 1; } sub _rlimit_to_num { my ($rlimit) = @_; if ( length($rlimit) && $rlimit !~ tr<0-9><>c ) { return $rlimit; } elsif ( exists $RLIMITS{$rlimit} ) { return $RLIMITS{$rlimit}; } die "Unknown RLIMIT: $rlimit"; } 1; } # --- END Cpanel/Sys/Rlimit.pm { # --- BEGIN Cpanel/Rlimit.pm package Cpanel::Rlimit; use strict; # use Cpanel::Config::LoadCpConf (); # use Cpanel::Maxmem (); # use Cpanel::Sys::Rlimit (); sub set_rlimit { my ( $limit, $limit_names ) = @_; my ( $default_rlimit, $coredump_are_enabled ) = _get_server_setting_or_default(); $limit ||= $default_rlimit || $Cpanel::Sys::Rlimit::RLIM_INFINITY; $limit_names ||= [qw/RSS AS/]; my $core_limit = $coredump_are_enabled ? $limit : 0; if ( $limit > $Cpanel::Sys::Rlimit::RLIM_INFINITY ) { require Cpanel::Logger; Cpanel::Logger->new->warn("set_rlimit adjusted the requested limit of “$limit” to infinity because it exceeded the maximum allowed value."); $limit = $Cpanel::Sys::Rlimit::RLIM_INFINITY; } my $error = ''; foreach my $lim (@$limit_names) { local $@; eval { Cpanel::Sys::Rlimit::setrlimit( $lim, $limit, $limit ) } or do { my $limit_human_value = ( $limit == $Cpanel::Sys::Rlimit::RLIM_INFINITY ? 'INFINITY' : $limit ); $error .= "$$: Unable to set RLIMIT_$lim to $limit_human_value: $@\n"; } } local $@; eval { Cpanel::Sys::Rlimit::setrlimit( 'CORE', $core_limit, $core_limit ) } or $error .= "$$: Unable to set RLIMIT_CORE to $core_limit: $@\n"; if ($error) { $error =~ s/\n$//; require Cpanel::Logger; Cpanel::Logger->new->warn($error); return 0; } return 1; } sub set_min_rlimit { my ($min) = @_; my $error = ''; foreach my $lim (qw(RSS AS)) { my ( $current_soft, $current_hard ) = Cpanel::Sys::Rlimit::getrlimit($lim); if ( $current_soft < $min || $current_hard < $min ) { local $@; eval { Cpanel::Sys::Rlimit::setrlimit( $lim, $min, $min ) } or $error .= "$$: Unable to set RLIMIT_$lim to $min: $@\n"; } } if ($error) { $error =~ s/\n$//; require Cpanel::Logger; Cpanel::Logger->new->warn($error); return 0; } return 1; } sub get_current_rlimits { return { map { $_ => [ Cpanel::Sys::Rlimit::getrlimit($_) ] } (qw(RSS AS CORE)) }; } sub restore_rlimits { my $limit_hr = shift; my $error = ''; if ( ref $limit_hr eq 'HASH' ) { foreach my $resource_name ( keys %{$limit_hr} ) { my $values = $limit_hr->{$resource_name}; if ( ref $values ne 'ARRAY' || scalar @{$values} != 2 ) { $error .= "Invalid limit arguments, could not restore resource limit for $resource_name.\n"; next; } local $@; eval { Cpanel::Sys::Rlimit::setrlimit( $resource_name, $values->[0], $values->[1] ) } or $error .= "$$: Unable to set $resource_name to $values->[0] and $values->[1]: $@\n"; } } else { $error .= "Invalid arguments, could not restore resource limits.\n"; } if ($error) { $error =~ s/\n$//; require Cpanel::Logger; Cpanel::Logger->new->warn($error); return 0; } return 1; } sub set_rlimit_to_infinity { return set_rlimit($Cpanel::Sys::Rlimit::RLIM_INFINITY); } sub set_open_files_to_maximum { my $limit = 1048576; if ( open( my $fh, '<', '/proc/sys/fs/nr_open' ) ) { $limit = <$fh>; chomp($limit); close($fh); } return set_rlimit( $limit, [qw/NOFILE/] ); } sub _get_server_setting_or_default { my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf_not_copy(); my $default_maxmem = Cpanel::Maxmem::default(); my $core_dumps_enabled = $cpconf->{'coredump'}; my $configured_maxmem = exists $cpconf->{'maxmem'} ? int( $cpconf->{'maxmem'} || 0 ) : $default_maxmem; if ( $configured_maxmem && $configured_maxmem < $default_maxmem ) { return ( _mebibytes_to_bytes($default_maxmem), $core_dumps_enabled ); } elsif ( $configured_maxmem == 0 ) { return ( $Cpanel::Sys::Rlimit::RLIM_INFINITY, $core_dumps_enabled ); } else { return ( _mebibytes_to_bytes($configured_maxmem), $core_dumps_enabled ); } } sub _mebibytes_to_bytes { my $mebibytes = shift; return ( $mebibytes * 1024**2 ); } 1; } # --- END Cpanel/Rlimit.pm package main; # cpanel - scripts/upcp 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 scripts::upcp; BEGIN { unshift @INC, q{/usr/local/cpanel}; # if we are being called with a compile check flag ( perl -c ), skip the begin block # so we don't actually call upcp.static when just checking syntax and such is OK return if $^C; # static never gets --use-checked and should pass all the begin block checks return if $0 =~ /\.static$/; # let the '--use-check' instance compiled if ( grep { $_ eq '--use-check' } @ARGV ) { no warnings; # dynamic definition of the INIT block eval "INIT { exit(0); }"; return; } system("$0 --use-check >/dev/null 2>&1"); # compilation is ok with '--use-check', we will continue the non static version return if $? == 0; my $static = $0 . ".static"; if ( -f $static ) { print STDERR "We determined that $0 had compilation issues..\n"; print STDERR "Trying to exec $static " . join( ' ', @ARGV ) . "\n"; exec( $^X, $static, @ARGV ); } } use cPstrict; no warnings; ## no critic qw(ProhibitNoWarnings) use Try::Tiny; # use Cpanel::OS::All (); # PPI USE OK -- make sure Cpanel::OS is embedded # use Cpanel::HiRes ( preload => 'perl' ); # use Cpanel::Env (); # use Cpanel::Update::IsCron (); # use Cpanel::Update::Logger (); # use Cpanel::FileUtils::TouchFile (); # use Cpanel::LoadFile (); # use Cpanel::LoadModule (); # use Cpanel::Usage (); # use Cpanel::UPID (); use IO::Handle (); use POSIX (); # use Cpanel::Unix::PID::Tiny (); my $pidfile = '/var/run/upcp.pid'; my $lastlog = '/var/cpanel/updatelogs/last'; my $upcp_disallowed_path = '/root/.upcp_controlc_disallowed'; my $version_upgrade_file = '/usr/local/cpanel/upgrade_in_progress.txt'; our $logger; # Global for logger object. our $logfile_path; my $now; my $forced = 0; my $fromself = 0; my $sync_requested = 0; my $bg = 0; my $from_version; my $pbar_starting_point; exit( upcp() || 0 ) unless caller(); sub usage { print <&STDOUT" ) or die $!; local $| = 1; umask(0022); $now = time(); setupenv(); unset_rlimits(); ############################################################################# # Record the arguments used when started, check for certain flags my $update_is_available_exit_code = 42; my @retain_argv = @ARGV; foreach my $arg (@ARGV) { if ( $arg =~ m/^--log=(.*)/ ) { $logfile_path = $1; } elsif ( $arg eq '--fromself' ) { $fromself = 1; } elsif ( $arg eq '--force' ) { $forced = 1; $ENV{'FORCEDCPUPDATE'} = 1; } elsif ( $arg eq '--sync' ) { $sync_requested = 1; } elsif ( $arg eq '--bg' ) { $bg = 1; } } if ( $sync_requested && $forced ) { print "FATAL: --force and --sync are mutually exclusive commands.\n"; print " Force is designed to update your installed version, regardless of whether it's already up to date.\n"; print " Sync is designed to update the version already installed, regardless of what is available.\n"; return 1; } if ( $> != 0 ) { die "upcp must be run as root"; } ############################################################################# # Make sure easyapache isn't already running my $upid = Cpanel::Unix::PID::Tiny->new(); if ( $upid->is_pidfile_running('/var/run/easyapache.pid') ) { print "EasyApache is currently running. Please wait for EasyApache to complete before running cPanel Update (upcp).\n"; return 1; } ############################################################################# # Make sure we aren't already running && make sure everyone knows we are running my $curpid = $upid->get_pid_from_pidfile($pidfile) || 0; if ( $curpid && $curpid != $$ && !$fromself && -e '/var/cpanel/upcpcheck' ) { my $pidfile_mtime = ( stat($pidfile) )[9]; my $pidfile_age = ( time - $pidfile_mtime ); if ( $pidfile_age > 21600 ) { # Running for > 6 hours _logger()->warning("previous PID ($curpid) has been running more than 6 hours. Killing processes."); kill_upcp($curpid); # the pid_file_no_cleanup() will exit if it is still stuck after this sleep 1; # Give the process group time to die. } elsif ( my $logpath = _determine_logfile_path_if_running($curpid) ) { print _message_about_already_running( $curpid, $logpath ) . "\n"; return 1; } } if ( $curpid && $curpid != $$ && !$upid->is_pidfile_running($pidfile) ) { print "Stale PID file '$pidfile' (pid=$curpid)\n"; } if ( !$fromself && !$upid->pid_file_no_cleanup($pidfile) ) { print "process is already running\n"; return 1; } # to indicate re-entry into upcp $pbar_starting_point = $fromself ? 17 : 0; # record current version $from_version = fetch_cpanel_version(); ############################################################################# # Set up the upcp log directory and files setup_updatelogs(); ############################################################################# # Fork a child to the background. The child does all the heavy lifting and # logs to a file; the parent just watches, reads, and parses the log file, # displaying what it gets. # # Note that the parent reads the log in proper line-oriented -- and buffered! # -- fashion. An earlier version of this script did raw sysread() calls here, # and had to deal with all the mess that that entailed. The current approach # reaps all the benefits of Perl's and Linux's significant file read # optimizations without needing to re-invent any of them. The parent loop # below becomes lean, mean, and even elegant. # # Note in particular that we do not need to explicitly deal with an # end-of-file condition (other than avoiding using undefined data). For # exiting the read loop we merely need to test that the child has expired, # which in any case is the only situation that can cause an eof condition for # us on the file the child is writing. # # Note, too, that the open() needs to be inside this loop, in case the child # has not yet created the file. if ( !$fromself ) { # we need to be sure that log an pid are the current one when giving back the end unlink $lastlog if $bg; if ( my $updatepid = fork() ) { $logfile_path ||= _determine_logfile_path_if_running($updatepid); if ($logger) { # Close if logged about killing stale process. $logger->{'brief'} = 1; # Don't be chatty about closing $logger->close_log; } if ($bg) { print _message_about_newly_started( $updatepid, $logfile_path ) . "\n"; my $progress; select undef, undef, undef, .10; while ( !-e $lastlog ) { print '.'; select undef, undef, undef, .25; $progress = 1; } print "\n" if $progress; } else { monitor_upcp($updatepid); } return; } else { $logfile_path ||= _determine_logfile_path_if_running($$); } } local $0 = 'cPanel Update (upcp) - Slave'; open( my $RNULL, '<', '/dev/null' ) or die "Cannot open /dev/null: $!"; chdir '/'; _logger(); # Open the log file. ############################################################################# # Set CPANEL_IS_CRON env var based on detection algorithm my $cron_reason = set_cron_env(); $logger->info("Detected cron=$ENV{'CPANEL_IS_CRON'} ($cron_reason)"); my $set_cron_method = $ENV{'CPANEL_IS_CRON'} ? 'set_on' : 'set_off'; Cpanel::Update::IsCron->$set_cron_method(); my $openmax = POSIX::sysconf( POSIX::_SC_OPEN_MAX() ); if ( !$openmax ) { $openmax = 64; } foreach my $i ( 0 .. $openmax ) { POSIX::close($i) unless $i == fileno( $logger->{'fh'} ); } POSIX::setsid(); open( STDOUT, '>', '/dev/null' ) or warn $!; open( STDERR, '>', '/dev/null' ) or warn $!; $logger->update_pbar($pbar_starting_point); ############################################################################## # Symlink /var/cpanel/updatelogs/last to the current log file unlink $lastlog; symlink( $logfile_path, $lastlog ) or $logger->error("Could not symlink $lastlog: $!"); ############################################################################# # now that we have sporked: update our pidfile and ensure it is removed unlink $pidfile; # so that pid_file() won't see it as running. if ( !$upid->pid_file($pidfile) ) { # re-verifies (i.e. upcp was not also started after the unlink() and here) and sets up cleanup of $pidfile for sporked proc $logger->error("Could not update pidfile “$pidfile” with BG process: $!\n"); return 1; } # Assuming we didn't get re-executed from a upcp change after updatenow (!$fromself). # If the file is still there from a failed run, remove it. unlink($upcp_disallowed_path) if !$fromself && -f $upcp_disallowed_path; # make sure that the pid file is going to be removed when killed by a signal $SIG{INT} = $SIG{HUP} = $SIG{TERM} = sub { ## no critic qw(Variables::RequireLocalizedPunctuationVars) unlink $pidfile; if ($logger) { $logger->close_log; $logger->open_log; $logger->error("User hit ^C or killed the process ( pid file '$pidfile' removed )."); $logger->close_log; } return; }; ############################################################################# # Get variables needed for update my $gotSigALRM = 0; my $connecttimeout = 30; my $liveconnect = 0; my $connectedhost = q{}; my @HOST_IPs = (); ## Case 46528: license checks moved to updatenow and Cpanel::Update::Blocker $logger->debug("Done getting update config variables.."); $logger->increment_pbar; ############################################################################# # Run the preupcp hook if ( -x '/usr/local/cpanel/scripts/preupcp' ) { $logger->info("Running /usr/local/cpanel/scripts/preupcp"); system '/usr/local/cpanel/scripts/preupcp'; } if ( -x '/usr/local/cpanel/scripts/hook' ) { $logger->info("Running Standardized hooks"); system '/usr/local/cpanel/scripts/hook', '--category=System', '--event=upcp', '--stage=pre'; } $logger->increment_pbar(); ############################################################################# # Check mtime on ourselves before sync # This is the target for a goto in the case that the remote TIERS file is # changed sometime during the execution of this upcp run. It prevents the # need for a new script argument and re-exec. STARTOVER: my $mtime = ( stat('/usr/local/cpanel/scripts/upcp') )[9]; $logger->info( "mtime on upcp is $mtime (" . scalar( localtime($mtime) ) . ")" ); # * If no fromself arg is passed, it's either the first run from crontab or called manually. # * --force is passed to updatenow, has no bearing on upcp itself. # * Even if upcp is changed 3 times in a row during an update (fastest builds ever?), we # would never actually update more than once unless the new upcp script changed the logic below if ( !$fromself ) { # run updatenow to sync everything # updatenow expects --upcp to be passed or will error out my @updatenow_args = ( '/usr/local/cpanel/scripts/updatenow', '--upcp', "--log=$logfile_path" ); # if --forced was received, pass it on to updatenow if ($forced) { push( @updatenow_args, '--force' ); } # if --sync was received, pass it on to updatenow. --force makes --sync meaningless. if ( !$forced && $sync_requested ) { push( @updatenow_args, '--sync' ); } # This is the point of no return, we are upgrading # and its no longer abortable. # set flag to disallow ^C during updatenow Cpanel::FileUtils::TouchFile::touchfile($upcp_disallowed_path) or $logger->warn("Failed to create: $upcp_disallowed_path: $!"); # call updatenow, if we get a non-zero status, die. my $exit_code = system(@updatenow_args); $logger->increment_pbar(15); if ( $exit_code != 0 ) { my $signal = $exit_code % 256; $exit_code = $exit_code >> 8; analyze_and_report_error( #success_msg => undef, error_msg => "Running `@updatenow_args` failed, exited with code $exit_code (signal = $signal)", type => 'upcp::UpdateNowFailed', exit_status => $exit_code, extra => [ 'signal' => $signal, 'updatenow_args' => \@updatenow_args, ], ); return ($exit_code); } # get the new mtime and compare it, if upcp changed, let's run ourselves again. # this should be a fairly rare occasion. my $newmtime = ( stat('/usr/local/cpanel/scripts/upcp') )[9]; if ( $newmtime ne $mtime ) { #----> Run our new self (and never come back). $logger->info("New upcp detected, restarting ourself"); $logger->close_log(); exec '/usr/local/cpanel/scripts/upcp', @retain_argv, '--fromself', "--log=$logfile_path"; } } ############################################################################# # Run the maintenance script my $last_logfile_position; my $save_last_logfile_position = sub { $last_logfile_position = int( qx{wc -l $logfile_path 2>/dev/null} || 0 ); }; $logger->close_log(); # Allow maintenance to write to the log $save_last_logfile_position->(); # remember how many lines has the logfile before starting the maintenance script my $exit_status; my $version_change_happened = -e $version_upgrade_file; if ($version_change_happened) { $exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--pre', '--log=' . $logfile_path, '--pbar-start=20', '--pbar-stop=30' ); } else { $exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--log=' . $logfile_path, '--pbar-start=20', '--pbar-stop=95' ); } $logger->open_log(); # Re-open the log now maintenance is done. analyze_and_report_error( success_msg => "Pre Maintenance completed successfully", error_msg => "Pre Maintenance ended, however it did not exit cleanly ($exit_status). Please check the logs for an indication of what happened", type => 'upcp::MaintenanceFailed', exit_status => $exit_status, logfile => $logfile_path, last_logfile_position => $last_logfile_position, ); # Run post-sync cleanup only if updatenow did a sync # Formerly run after layer2 did a sync. if ($version_change_happened) { # post_sync pbar range: 30%-55% $logger->close_log(); # Yield the log to post_sync_cleanup $save_last_logfile_position->(); # remember how many lines has the logfile before starting the post_sync_cleanup script my $post_exit_status = system( '/usr/local/cpanel/scripts/post_sync_cleanup', '--log=' . $logfile_path, '--pbar-start=30', '--pbar-stop=55' ); $logger->open_log; # reopen the log to continue writing messages analyze_and_report_error( success_msg => "Post-sync cleanup completed successfully", error_msg => "Post-sync cleanup has ended, however it did not exit cleanly. Please check the logs for an indication of what happened", type => 'upcp::PostSyncCleanupFailed', exit_status => $post_exit_status, logfile => $logfile_path, last_logfile_position => $last_logfile_position, ); unlink $version_upgrade_file; unlink($upcp_disallowed_path) if -f ($upcp_disallowed_path); # Maintenance pbar range: 55-95% $logger->close_log(); # Allow maintenance to write to the log $save_last_logfile_position->(); # remember how many lines has the logfile before starting the maintenance --post $exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--post', '--log=' . $logfile_path, '--pbar-start=55', '--pbar-stop=95' ); $logger->open_log(); # Re-open the log now maintenance is done. analyze_and_report_error( success_msg => "Post Maintenance completed successfully", error_msg => "Post Maintenance ended, however it did not exit cleanly ($exit_status). Please check the logs for an indication of what happened", type => 'upcp::MaintenanceFailed', exit_status => $exit_status, logfile => $logfile_path, last_logfile_position => $last_logfile_position, ); # Check for new version... used when updating to next LTS version $logger->info("Polling updatenow to see if a newer version is available for upgrade"); $logger->close_log(); # Yield the log to updatenow my $update_available = system( '/usr/local/cpanel/scripts/updatenow', "--log=$logfile_path", '--checkremoteversion' ); $logger->open_log; # reopen the log to continue writing messages if ( !$sync_requested && $update_available && ( $update_available >> 8 ) == $update_is_available_exit_code ) { $logger->info("\n\n/!\\ - Next LTS version available, restarting upcp and updating system. /!\\\n\n"); $fromself = 0; goto STARTOVER; } } else { unlink($upcp_disallowed_path) if -f ($upcp_disallowed_path); } ############################################################################# # Run the post upcp hook $logger->update_pbar(95); if ( -x '/usr/local/cpanel/scripts/postupcp' ) { $logger->info("Running /usr/local/cpanel/scripts/postupcp"); system '/usr/local/cpanel/scripts/postupcp'; } if ( -x '/usr/local/cpanel/scripts/hook' ) { $logger->info("Running Standardized hooks"); system '/usr/local/cpanel/scripts/hook', '--category=System', '--event=upcp', '--stage=post'; } close($RNULL); ############################################################################# # All done. ############################################################################# $logger->update_pbar(100); $logger->info( "\n\n\tcPanel update completed\n\n", 1 ); $logger->info("A log of this update is available at $logfile_path\n\n"); # this happens on exit so it shouldn't be necessary $logger->info("Removing upcp pidfile"); unlink $pidfile if -f $pidfile || $logger->warn("Could not delete pidfile $pidfile : $!"); my $update_blocks_fname = '/var/cpanel/update_blocks.config'; if ( -s $update_blocks_fname ) { $logger->warning("NOTE: A system upgrade was not possible due to the following blockers:\n"); if ( open( my $blocks_fh, '<', $update_blocks_fname ) ) { while ( my $line = readline $blocks_fh ) { my ( $level, $message ) = split /,/, $line, 2; # Not using the level in the log, cause the logger can emit additional messages # on some of the levels used (fatal emits an 'email message', etc) # Remove URL from log output. Make sure message is defined. if ($message) { $message =~ s///ig; $message =~ s{}{}ig; } $logger->warning( uc("[$level]") . " - $message" ); } } else { $logger->warning("Unable to open blocks file! Please review '/var/cpanel/update_blocks.config' manually."); } } else { $logger->info("\n\nCompleted all updates\n\n"); } $logger->close_log(); return 0; } ############################################################################# ######[ Subroutines ]######################################################## ############################################################################# sub analyze_and_report_error { my %info = @_; my $type = $info{type} or die; my $exit_status = $info{exit_status}; if ( $exit_status == 0 ) { if ( defined $info{success_msg} ) { $logger->info( $info{success_msg} ); } return; } my $msg = $info{error_msg} or die; my @extra; if ( ref $info{extra} ) { @extra = @{ $info{extra} }; } my $logfile_content = Cpanel::LoadFile::loadfile_r($logfile_path); # add events to the end of the error log if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::Logs::ErrorEvents") } ) ) { my ($events) = Cpanel::Logs::ErrorEvents::extract_events_from_log( log => $logfile_content, after_line => $info{last_logfile_position} ); if ( $events && ref $events && scalar @$events ) { my $events_str = join ', ', map { qq["$_"] } @$events; $events_str = qq[The following events were logged: ${events_str}.]; $msg =~ s{(Please check)}{${events_str} $1} or $msg .= ' ' . $events_str; } } $logger->error( $msg, 1 ); if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::$type") } ) ) { require Cpanel::Notify; Cpanel::Notify::notification_class( 'class' => $type, 'application' => $type, 'constructor_args' => [ 'exit_code' => $exit_status, 'events_after_line' => $info{last_logfile_position}, @extra, 'attach_files' => [ { 'name' => 'update_log.txt', 'content' => $logfile_content, 'number_of_preview_lines' => 25 } ] ] ); } elsif ( !try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact"); Cpanel::iContact::icontact( 'application' => 'upcp', 'subject' => 'cPanel & WHM update failure (upcp)', 'message' => $msg, ); } ) ) { $logger->error('Failed to send contact message'); } return 1; } ############################################################################# sub kill_upcp { my $pid = shift or die; my $status = shift || 'hanging'; my $msg = shift || "/usr/local/cpanel/scripts/upcp was running as pid '$pid' for longer than 6 hours. cPanel will kill this process and run a new upcp in its place."; # Attempt to notify admin of the kill. if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::upcp::Killed") } ) ) { require Cpanel::Notify; Cpanel::Notify::notification_class( 'class' => 'upcp::Killed', 'application' => 'upcp::Killed', 'constructor_args' => [ 'upcp_path' => '/usr/local/cpanel/scripts/upcp', 'pid' => $pid, 'status' => $status, 'attach_files' => [ { 'name' => 'update_log.txt', 'content' => Cpanel::LoadFile::loadfile_r($logfile_path), 'number_of_preview_lines' => 25 } ] ] ); } else { try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact"); Cpanel::iContact::icontact( 'application' => 'upcp', 'subject' => "cPanel update $status", 'message' => $msg, ); } ); } print "Sending kill signal to process group for $pid\n"; kill -1, $pid; # Kill the process group for ( 1 .. 60 ) { print "Waiting for processes to die\n"; waitpid( $pid, POSIX::WNOHANG() ); last if ( !kill( 0, $pid ) ); sleep 1; } if ( kill( 0, $pid ) ) { print "Could not kill upcp nicely. Doing kill -9 $pid\n"; kill 9, $pid; } else { print "Done!\n"; } return; } ############################################################################# sub setupenv { Cpanel::Env::clean_env(); delete $ENV{'DOCUMENT_ROOT'}; delete $ENV{'SERVER_SOFTWARE'}; if ( $ENV{'WHM50'} ) { $ENV{'GATEWAY_INTERFACE'} = 'CGI/1.1'; } ( $ENV{'USER'}, $ENV{'HOME'} ) = ( getpwuid($>) )[ 0, 7 ]; $ENV{'PATH'} .= ':/sbin:/usr/sbin:/usr/bin:/bin:/usr/local/bin'; $ENV{'LANG'} = 'C'; $ENV{'LC_ALL'} = 'C'; } sub unset_rlimits { # This is required if upcp started running from a pre-1132 eval { local $SIG{__DIE__}; require Cpanel::Rlimit; Cpanel::Rlimit::set_rlimit_to_infinity(); }; } ############################################################################# sub setup_updatelogs { return if ( -d '/var/cpanel/updatelogs' ); unlink('/var/cpanel/updatelogs'); mkdir( '/var/cpanel/updatelogs', 0700 ); } sub set_cron_env { # Do not override the env var if set. return 'env var CPANEL_IS_CRON was present before this process started.' if ( defined $ENV{'CPANEL_IS_CRON'} ); if ( grep { $_ eq '--cron' } @ARGV ) { $ENV{'CPANEL_IS_CRON'} = 1; return 'cron mode set from command line'; } if ( $ARGV[0] eq 'manual' ) { $ENV{'CPANEL_IS_CRON'} = 0; return 'manual flag passed on command line'; } if ($forced) { $ENV{'CPANEL_IS_CRON'} = 0; return '--force passed on command line'; } if ( -t STDOUT ) { $ENV{'CPANEL_IS_CRON'} = 0; return 'Terminal detected'; } if ( $ENV{'SSH_CLIENT'} ) { $ENV{'CPANEL_IS_CRON'} = 0; return 'SSH connection detected'; } # cron sets TERM=dumb if ( $ENV{'TERM'} eq 'dumb' ) { $ENV{'CPANEL_IS_CRON'} = 1; return 'TERM detected as set to dumb'; } # Check if parent is whostmgr if ( readlink( '/proc/' . getppid() . '/exe' ) =~ m/whostmgrd/ ) { $ENV{'CPANEL_IS_CRON'} = 0; return 'parent process is whostmgrd'; } # Default to cron enabled. $ENV{'CPANEL_IS_CRON'} = 1; return 'default'; } ############################################################################# sub fetch_cpanel_version { my $version; my $version_file = '/usr/local/cpanel/version'; return if !-f $version_file; my $fh; local $/ = undef; return if !open $fh, '<', $version_file; $version = <$fh>; close $fh; $version =~ s/^\s+|\s+$//gs; return $version; } ############################################################################# sub monitor_upcp { my $updatepid = shift or die; $0 = 'cPanel Update (upcp) - Master'; $SIG{INT} = $SIG{TERM} = sub { print "User hit ^C\n"; if ( -f $upcp_disallowed_path ) { print "Not allowing upcp slave to be killed during updatenow, just killing monitoring process.\n"; exit; } print "killing upcp\n"; kill_upcp( $updatepid, "aborted", "/usr/local/cpanel/scripts/upcp was aborted by the user hitting Ctrl-C." ); exit; }; $SIG{HUP} = sub { print "SIGHUP detected; closing monitoring process.\n"; print "The upcp slave has not been affected\n"; exit; }; # Wait till the file shows up. until ( -e $logfile_path ) { select undef, undef, undef, .25; # sleep just a bit } # Wait till we're allowed to open it. my $fh; until ( defined $fh && fileno $fh ) { $fh = IO::Handle->new(); if ( !open $fh, '<', $logfile_path ) { undef $fh; select undef, undef, undef, .25; # sleep just a bit next; } } # Read the file until the pid dies. my $child_done = 0; while (1) { # Read all the available lines. while (1) { my $line = <$fh>; last if ( !defined $line || $line eq '' ); print $line; } # Once the child is history, we need to do yet one more final read, # on the off chance (however remote) that she has written one last # hurrah after we last checked. Hence the following. last if $child_done; # from prev. pass $child_done = 1 if -1 == waitpid( $updatepid, 1 ); # and loop back for one more read select undef, undef, undef, .25; # Yield idle time to the cpu } close $fh if $fh; exit; } sub _logger { return $logger if $logger; $logger = Cpanel::Update::Logger->new( { 'logfile' => $logfile_path, 'stdout' => 1, 'log_level' => 'info' } ); # do not set the pbar in the constructor to do not display the 0 % in bg mode $logger->{pbar} = $pbar_starting_point; return $logger; } sub _determine_logfile_path_if_running ($pid) { my $upid = Cpanel::UPID::get($pid); return $upid ? "/var/cpanel/updatelogs/update.$upid.log" : undef; } #---------------------------------------------------------------------- # HANDLE WITH CARE!! This string is parsed # in at least one place. (cf. Cpanel::Update::Start) sub _message_about_newly_started ( $updatepid, $logfile_path ) { return "upcp is going into background mode (PID $updatepid). You can follow “$logfile_path” to watch its progress."; } #---------------------------------------------------------------------- # HANDLE WITH CARE!! This string is parsed # in at least one place. (cf. Cpanel::Update::Start) sub _message_about_already_running ( $curpid, $logpath ) { return "cPanel Update (upcp) is already running. Please wait for the previous upcp (PID $curpid, log file “$logpath”) to complete, then try again. You can use the command 'ps --pid $curpid' to check if the process is running. You may wish to use '--force'."; } 1;