JFIF     "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777"H !1AQ"2aqB#R3b$Cr4Ss%Tt&c$!1AQ"a#2B ? }XuAo)8^ IƟ`vUp9jY0Ǧ w)E허2jU`SEKw5]kSno!]:?jc\غV7/9N+{t#8zd/޲3F/=ź3GNquV"/4:{z%ۣI'D@ %88^f}VV)S_2ed^Mx"͟?UC62Q%чmO͓ cq0rŖJ\Õ_Sݶ'|G.q޾D U]nP%EF>˲E"d&'f2s6H]4w IS˶4VbaQ+9]XtNx:M0JNxϙ⟟"{nr;|{%vo\z-wc,*|k}-m55o4W9ؓw߱Yzk .=/oϡȴ^9ҧʹamtQԬZ]4?egjrQ}+)MleE]MPEn!`IK2RUEwVIoͷcp;lśe7΄uN ;rПV8|e\׹9Y-V_G.)XԢOv<;_"ڜ]ߙEr݊'K{KuBJ}KI}24|"v)/ʻo5)6-Tjd7.C]Q&lU,Yk1P4~UKZs|$kX6+屷CUq+N(jlGrpG&UB3#k3\9qfg7O8Kim(AJOO~C#e`i0wĦij$cWh<dtQߺ"NOtG+ZǪ]b5%]v5$)u|qZ柡s-rۖu$MKڎCmN_V'/1u,21pvlc>қeNnֺ|bkl=lǷNOʣlz*]»vȎ[)j[fs[]:s#m6Qt6*Q+`};ßj[F_jcv`r#w}|k<ڞ/r53N8>Kh q_-_??@enſEܥ\D\YAEo+ ޟd}IcY7+t{=ɩ>}i\\JfxzVdSzᔢ]Q^CJի\iceitMM5hڦg')^ et#ۯ"ÿfF->4iؤ2ݷ6#p6^-R̫gETj^I.kӽUp~D9[:/>h> \gJ|ۿؘ>ml9jMK =+*2i=0RiͶۗV{"u]IH`9J_˹KƼK$X-|=ve/ bjxw.9i%NqVJcFYKcTtO,F;%67vYb8֝qq0tUt=DvawsS~~Edzr^F-v{c++ݔ\|9Iy #nOavOY=3690Tcrilwa\˓m$?箵S6U c(.~R7suMhqcMOnKoc*ȣȩEd'J ܜk*_q}%M/7c.|;trddbsdcJev85̤iW Ę 8C# .딖e$sk80^\J众2)Nm~|Idj_ O+6ǻ#(MIz4Qo:օY,:q]̌"lK}{F]ζ)h>ʶ ^ue78_G#rqv$wkk[Q c+վ+ĸZΝFB]VzoiJRke&Kgom_7Wef_7,osJɽE%lzBt>mRs)v8'P0ֲtrOg4p_2`GlhYڦDF/ӚKmtm'P2kqU765fJY:y؊.ox%8V_ִ̌ܞjpqwЮQ;iUcNoOoٸcY w*4soӵkqf$?-jy~0{>?DaL8XL/ɞo+'8 {ʸxգj#Dy)wk̘e۩+%}~;ڼ5xek|y-%ڱ-ʜe:EEScÚ5z|r'&I&яF*F7|[nRF =(4ۖ@. n7@xx:N^8Bg%u/ny6&dR{?8U_Q6Z߯-oh.NR]} qi6~H(j7*uF&l&o8ts]/P89:jW*$w׹Ӌ FxpsCJi.7N q4WU_}7*M#qWiصnk'4ݍl*t^ c<'d:~͗enFQRz9v~ddoTZ̚k7X(wUswO̙fոҁՕ[$IAI>WW~ĪEѢNoeutYߑ-Eixιpxq{FnyfRrjqU᫤]>wPU8)Y-7Wbq㛋w:7ܣ].j%K:y4] %9$I%pT(󨪙VqiYٓ4y~5S/XTDZM2lȪ; S~Kx:(Mn0';-{*qV&|W3S+\֔a{R{s=lYmN9Fn&o'}Vi( ?*qV5ѼCNsM饏zߴ$^O69@ ,$y|jE;gW/u|M?3+ZՕN86յw%|QO㏏S\E#ddsgl+Scl3~~CԕQľ?5_ z߿t11OĶ0>oB9E/SOSk+b&Yn>$툧eg) "!܉(1 uBoJ)/t/,:=7M+1ܺ#CmS^Nz 6[u&]+|Dfj:uZ5-Z^TjMtm>cȳ NdT_,M#Ex;pt۴ͮ#!N iKl!zPծ~$1SiO} HI&g Bf)b%Ko̧kumEnص;V?j>nltOMVۆl>.WueYaw2+qK,?uHiqqSM}~gu3xbcWSy/Xc{%sZ]uaUM;7:cb5G97'7þյW,;$ܛyVjl޻y7S;o6gf.Tг[7/i1Z^rE cUF'P1-?%u&q{fw~27ޡ ^w$?SwP[=R3Y73 4x(Kk&rLȫMKn:RjcI?3Al`vض[POĖSYujj6v+-[xҵ=~zNN>\ɲQ/uufo*e6l;31붏.>w6=7#7dFDc%ƶTbd;2/=?Asr! ~ZSS~I"9y]Hn,ĊJ7S}cK"amCg3yP=RQɤW}t;-{F+v+RɔڎB?º{SV묖kۏmK~%.Q;OfEf_Y/F-V-MdD)m.ZՍ8Y*h[g/6ydmCc[rdfʾ䖗gd$^֍^ʅѻL|<[݉\߯RiJUo';œN?B smS ܹkس,mRE^ѣlJ&.ċ԰YO:޼f\Z'HCѯU[ʩ1ff4S-٥YxTIGLiыr }L)edׂ*l|ٚuoxӿnWkTbbVm zT_'"x5Vިxo1ج^Fq6Sd3ws'/ڞ6m?}1OsRGݝ+,~ڬ%^p1ef5c25vq~﹉ă[r-eq] 8+/ESj}?mUE.xYK3"oƔ^Y9I]I ޑ" &*4.Jâ}ټQbXKJ񽼀ncg`+riܭ_'Bֽp%bX'7cB}WPm|zHָLJhj~E>i~Z$297|_hyΕ&s}ZϷ *j]:v.HK<SP8`Pƣ)r ,}8Wk[ArHgn=о7:J]TTP>OOj J_KyB\Ԥrm嬷ȫr{ݙ5R(FRЪ6q}KLmR'eޖz6[YތesYYL5Tr7s\^rؙV͸컬j5d?yk'b S }kra^ߚRH)[sg.fLM\u= vJQ]rVkZuoN}#G?yjO%|i2fKoӰღC P_Ϳ6Zr{e/m$i}9 G2')YG9KY>|1ӫ +v+i;h\Q@˿Lӭn˖ 7ck>Vr.D0)hC<˄4"0[eԬݭe+l2s3ss oX]1r]+VK vI;mZ')R6e5=/i@]H^Z۬՝EW.jƆf{8mXMV~_̝z^VR}T63}}k3+k3:j1Phlpi{欍BȽ}6w73GtUZv>4eUj$ xz$$D/߇ߟI"uk̜aƪ*ke/F:dһ_PE1ݡkp(5ʏ-ɮ{Yllԧg!ܝ g]i-umεŸxOê^=PR ##XeMy%2L~󜺶Hm ݙ2t_ƶz7'\Z4T<"AM-&xaC]a5.huQ۫$cMμ|h;.J.o߸sE-zU{d];|YLSMvSEneNKr1B[]NeonNߪ$4̘FPrkxޱ=0lr7Q%=$KQ;0r*XKdGۃ*]w-npᬶ\tt4>Dc[Ouo3/)-WҴ xs71eԤm*ٖ웗H''.Cnmy]݊Kra[9)Y#2U6d7tf.[R.GdE>#O_.+-K`{KonR_ÕM/)?:F,Xo1ƽRmz8C]lD %(x+d2Ah+\CCLJ!D65x\ȼv)\Nrp*[YُfL*PyVΚuWA K4hyYdwihNIy#ub?4NDϐ'4 :nFe(o%ve@@xl-k%QƭRP&kεMŪ-Ys2u ]T!}8*TQnZ}v =~mԧyDM&8K>2|Bnugܷ.wvCs̼5F^ubES7ݢM&4Ź-~mKx1((sr!M5uy\q)oy|a)ˣ,A?w"T휳2\F}PR-<2%`~4Z5\W"(USkGpT(~Qj>ɰ쏳ǓSKKx's]nEf'.iݙL>Moƹk7ݭ[.г6lk<;?)#E]xFU7'>vF%R;t:Җs}NSBWX=Y8ث}~G)S^^ƽwR[)/Fm-ڞTK~˓Z]U;RQ=M/"NԝP[-Y9t_8V+}P?Ue{M/O&WWKvc#r'KM'p[±vtpRC/W|7K2Rfm;ljm%Z]^T[6}6iTC }L[uxg7(Z}. SRI)jҞzȶ쳢oYRw$ŷ"J\ǭw{u'R taF{;3hHB\RP(*ZQ]y;;k٥nWbGKv-V?NDҞkd9@z LJ}Kc9C*?V-*[*۸-0.|󲝳ߗZK#%_OFGF$kC$[NNJ7Yn[k~Xzc+Sʲuhsw^^4+nElbƮKD,}YLV=i=|p|_=b5mȵ(~,em#Xƥ.sVoEaWXc.lY uG\m';'*\ӆ}|˯UfQBvo}/"zw + qvMrQ[[AdU2ٽCGgjؖS~Ev%9">$_2Sߚ%ѽ7jX(t#21r{̬F]b()?r[Rı)W[O/6]XL9 vuLh-Ȃ9"'7f!Փ䮿Bf}[lag֧]?Pc#D9EmfK7o*})+n!]qIo^FrNVNo!Eƃd#OP?%ۋ(mPu93ۣ{}2&$%cZ߯LҚY);U afԶd,*'6_?B:R~}^̬~mJ+vC}Ѩe"MY+mi :s쥸;iJeYvBddeK|#5/mzR]F2 JHUU )/S{Ic$=: W)>} @0#URsR=w"L{+ɞ)d|*qq2>[nƨDۋ-G[6½J|{Ѿ4MwyG-Σ Ze{ug>2|'zΤ2%xՑ*<Q̥T')uLkjn(zF-JOR}wn~FV5zq2m'^VS=7Y^RdfeO)>EpX붚w*r*w˿^kڴ{J;K۔sRŶU]p\zn@dx6[+yeH[_m_/I&mv|M5&&-G"v۴^{vg8Y(K_~h0e AxfrzڬkhS/Vy1ϯdW3'͹}{'V-:MW(V/ͷ*E7s\EmEW}bUr'k,P{9?B֫ #[uNrB,wo^{fdF(5tRf.2J-/:~ t0M"d_/c^32*q]yLl^2[ݥZc*vtm213r'tSuM-Խ#o/HF+2VEpmǦޟS?Rs+t:u G8n,Ԛf,hY8SX*rKf>+cpruɬ=DMrXgϸ:~ɲ ~]'5'kElw\=ڞAG&')G9R\_̝1K;nPg&T(ի[^Jҟ"qoӸ.W}3mF>'$<\U6-~?x?B~{^xkpv-vlߣe빹j\(ښsuu6lH(qoaYt?x8}Ie '@b%TݲygV.+O9/W4MsCMuFjYzG.{ds.k(>G~K?ni-=R r}r ?s̥%l5Ϛ9IN6~۩RĢWNʾE[|nb.HY—קWkr1ҺշMNDp)^¸R:w;u1 12]T/Uiʹd%2OC2K*r5S]g凫5 UQ.ȫ– /i91njFkQxuJ1rn%XDžy?s˗վuMGƋ/m^J*RsF))uF,'l{=|nFm9:N\%u#tnXE->e2Y0PũjUȨEŭ|'eʹ[o{Ցms%CGg/}t|snzrvm\g}cÊ94Pvg'L}ّg궮ԱߢO^f.W-sT]M˔ېе<^Н'KuNn_Vl8*Kж^ xsuW51-ᅱFzƉT-kY/9wzDޯ/XlW)gypǚjDɨ~{ݤHCim.[>rqE_Uرx/>|L64%aj;fxӱF(K֓J9՞ -K> I_5Enn´&=Oc%o̟IJZF$۲5I9Wݚ n.WTuѲӏ[4U/9.2zX5\j3ĎEsMq4%9.d[7јc9eNa+sjE';%s#ɤ`ףS=WI쫢.Mv:j/[3:rTF_zt:.z%udW%]xܮVz$Vŗ49[^y.խN~M&mx+wGR~_4KC[ʻ:v>03߶v9x-Mȧ$c:lrCWjeg%ֹ_Nh՝Qɏj^ϛr^.>WhlE5yֵ6\W^确]*гc&^NI[oCDn.ߑ!,m&M_/'Mn$s\r^8|uSZZ1|LV<(zq׮xmٚZƏ%.Ԁs^2𱸒O#&,s[mײ9kޖCoSq&俙qxP.N] 2UǎsM2iN.f r[mcQZmFُE{#[TbҔ*sfaSrn^8N<\_'MarJ6 EQғ|F[S'[~q~kmn[_x?B f5Q١X=g(~[Cx}GO ĺo'e)~dq(Ot`sN=~heu ::m'Cjj>~5V柙cyQD%uqEc{[l^U O]b~eŦۑ'W3&' 2V.^D%G S6\wYNO$. O+^ŵG~haEs^=1*bICzFF4O#,Wu3허ekB\I'tWMߩOG3iFz{rgeM9g r] i3gk&u1r/1kVgR-ɿuF .^;3;?3큦bN̂r4ovMkڞ}[:,IVG<};*-2",>K%bK2Ƨ[w!)ˤ;d?4%Ul2ږec4#ōIw^R_/TFX+*FM[F|a'ߚ2SIMeVGn ~&Y Ym(?ԛ],=|сG4yjk"Q^~ԗ^c,qqrg^-:Uc[E8>>k|nS..LBIc>3i|ZEZXAqm nuOm<; X~mrK=~ ƱrSN<U!F΋WS/|t?K)zd} ,C"ovx?bբs3mX3桭X֖˦kFddhg}$ggSo5jL*NdJis$ EQ\v=0HxzyW~FT_Ƶccg,&=_V(%kq+_÷O'[_[Uڽv F $Ξ9n5EN/4Yy/%*} .jΔ`V_6\VͲohzfOgޯzpj}y}v:34WH;+x7ӻu<ݦ"mJ/=>eoD֣c4kXW-[}٬6;t[Na_• _5i5˗sٴ]+e;Joj㼶ۙyLumo5&F)F\ {(sm_M>gzcr)KU̠Ħ=VDd'h;-aŤ9KٰqQܫަazMp4bk9 UX.ͮ]KeS5Uq[¹X0ɦ6]roFjʧ2׏6/C6eQE5KӰmsFnIz&`z팡-ٯ.ixyك?c2//z6M4W[]_"?Õ[? Vfvӳq]I5(d|MʝzcC*mN>B2gD+><e:Gh %UkW%zJ8k_ˠ=KFRfw{sŖ^q\/{v[Ω}gLjT[t_ޕg6G~rkkMcSRKբ54?SAûO1o%[>5/R~CioNdNʛćh>f6H8c/<1xd[ŦCEk.9"ej?w&O6^ژR[vrQ.z㎩f6:V8}hi2z~ s-w]+|I9s_C~>-S&9ZFVLf7-d'pՠplJ#mm؎s(?Ʋ?/A%_sXuGNnR}_dq>1ʍ|У3]NXYZʷ/&ܛ彖LS? 6]"_t5qP5Kq]^m91jW暹U6-5WU澦M0˵f2ӪǮ.P~? _nEJTcTei)ٳrۣ%x %gs}7l9'tb~dXst# r?}Weaq>=+to)7،E*vn\e_,\NFxcivz]tM˼?Oԝ2Zrλs-ĺEtonIIfm/9^[^EBUjOnr6vI& l]%0")2䒶-+R*zyX<> -X9GUo^xYQ8ιvixٔa\t)hv}ьոVU~tK,=_wLLa?TYIo]$`N6cbi?#7;MRt<.~Q-mob\\g5췍 ڌ_?8nfJN/Y͢n3?_sϩ{HiְPo'yS??_jߡWi5q? MWȲ)8a]lLˏ--b[TXlΫRy;o5뜾$HW.mm?շG[Ƀ seo5Q}Le%*،«~uU{R$t\^%!weX:G('6WupTS&~8=jo?2_PϖE[nf6Tٯ;GLW)NM[o*\j%.gb|䭹noOX:1R)UTj74˓]D_bʝkzNI.9|^G`KeQ{mOjX/sR7evdgi7qm}ތW&4=~|YY)?7Oj}xXkF×4c.l?i|b[5Ή5j-[Y\z<茲Z$Ff&o;gErǩݦ̪/q[&[/9uuzi;PS^_/?]=ΕqK~ӛ5'NM[m_Ϲc'[oӯE#g߂vvGNRo϶o5Ǩ[ɉtov2~i<7iSȜN(G5+/ٛMTܣukj鷣/$1˒!Mxr\ߤs1ZuMQȌ^]c$CXrj#N/˦Ķ9]Nzê5zi;W,v!ŧD6zğ7uR5^MW}>igl2U2nXo{}_w]&vte\Z3 MEEe/ 2s㗼S_bIղTI}|[Ye/c]*̪9u/DmyNxSDgi `Z?.RFj۪'~.[KVb޺o濡to?E#[.^y=q4F8ڎ/GX\.YW!Z.ѕtt:?gYYyU%Uw~ri>ȦKhg,5/=>V?TrN4aWO,oӕ7-SRi*"dܽpuaVQÞd-#J2Nr:#``ѧWR-F?I-T -cOT2pr?þזgE\Ij~L9%EMoџUؙt8_eYΧWjU}e9y9z/#TT-2dLt3H=ڼcKb'"uIٓ'[[߱F~\2]r%C]^VCLjm[cJNryf}ջ.[DEoRՒb'>fVy_c6[K4Na5>{ɳaw/Uj.Զ_K~?IeJ7OQx3IgFc*جɊǽ-o3Ӭp / ]7V*ENܜ[r/tOJΉw*ʨ*JFN^.WZeLgUwKi/M9y8dkOᛊHxGĶM*&#h/U|6D(uFyE5hYxiSEVm^D|,ۿCj;<*ouOkYpΔ2{x-L] !k2ا#IM'a7:M}M1Y儭Mnk[/;4Uwkkɫ%aɔoXVV$m;2Z4i9:>\Yů= ?[{t6,~!c`Un+dW.gKyIB]l+3kض(\MZ\}>k\C~閹l[ů]VNtƸr몮X+U>v'nv{y7s[г̭9Ctvt% GqT8=wa(6\Rd柮YWv^Fd^\+緉,+=-^S"k:NVu o[_TIѝ椯bF/G㿏dΙ?T}K-T)W>s?3M)V*,;P\,}B u{rDexڥVFfw}47׋w}]Դ 1dmk1V%/'T:Fǒ_TEe[l/l/ٯc{Ƀ[~`zj⾥r}Vܪ{M8Qv]$mU]8J2MngcxY?鑞.9HjxSy.fS(|]MgcK2$(jRQ3XO|<f:Jq4& fw|$N )A8ת99 mFNM*Dϒ NoIa9i9y?:D⻧߇\7ɧ]mu"-˥5/w̨_ 7DK['[2"(%xzT\*GT"+<,yX.lEJrfo?.4N;l>jmZߣ5FdB3\r,t,./S]Q{tm5lӕT~A [fv7Iہc: ΪN7I]2(|o$NLW"#~Dͭ=v-Mv{-lqn{I3xn'6.=DƟܖަ~deQV;k2Ei\[bӴ1_]OhZl朠&t3xkei+c\'ZԪ'hK梿X@cTԫ#emIz6e^i?8 NBc̆f+MׇdC]YSd%lώ8-c7eι/}_con/no\핍~[WNReXMo+اn ?#Ͷ-AUFN1V4!y,{1a$S﹑;Ǚr"__[o) xk}7EI/riwؙ7mR}`|yrEVdo/B# uٳiNQKQkᑑ^d@/=ˑɒ768fsuor9=7ףܹճpMr-$1uySOZN?đrqզ9F q=.!T?ػ bf{¯q=$^:!ES߿ Fu\OS,8e^UוS^hF4BQƺȪw-kF39@X06 Fv=Q^|ƞ5}2tnmG_|Λ(|%](-5>KȁN$=6lq).12 V6m$ׇlOcҫܸ K{;ľ>+Q?Rx-Keu uMy$i B}G*h$Q -W[-&a"[i\}~Ek$<~c{MffS eS.#\^lMiytު]9S{u4 {DFޅSź}R ]R$y;r/P̙3niXMt;&!rxw\ZFmQ"w\L{^۔K&/gr:m=2%5bwE"^e[\$ɟPi!U_rdS2d?=[!(I.rC QZEim%}|YmzZ_ά<ۡLQM|` ybPȏ}?]Eu[`kҫgFb~F}Q8NP>5lӳ^-K%Q}$sx7SvnfTƸ|Kzd'_ⰽח$4L Y?qy32t j2e ȜrJ{mبhۍUU'p#8y'ѝ=i+Tĩo7WYyČkL5؝M=%"Nt}eXW)N.~sv5pɮ sSQ[+-/}kVk'FEɩ9SE&T=&\緵 --tf.9Ѳ4_##_ɱTFV؞~YTddS&s=䟚Fb1._5}~gM'p#,U hs--XG wtԹTi7M:GYK5'^W?C>_Gq/S&d| k_gO ӊiJeHU G_ Êg#),}-:5>V1emq}t}q?meKU:BqJeiPɗ#\$sI} Z生ƫoo=V=pVcUg"%wEm叡vIdhrȔ~F]p58_.,O|'Ɇ^L!c6OWӷ{x9?Fp?ceOuT+Uɵݹ&gx9i퓃sxGIm}_3Īr#:ԣ?4בc[jö#B7KʌWNo)=+c }YvP{lv^r+5Vxx_:~=̌Q}CTy+Wh鸚f$101뢊F[#--Y\i@l)W8/E>8nlj/ktOľ,q*[sE[]:?ZeQvŔɺ|j(Wx,LW=:S?κq%81c)jJvODLiW,{96vr-2}-EH,}%3k#l5gl~x__W Sڎ 8YJQvA=QIWju6-X9$kWЩCI4UWd'&O/Cf=Pi/#+>n$KYst܅y4ʷD^~%~myj,s_4Q}΍Cή;SW:h=Ff{.B/inȇo=-T͸OY2}hlK}.m7-z?,f-/^b\QWs/_͔/3In[6M;l ygؼ!WUË_)D9YL4_>f}ϵ3hV5Oѣ(l8?L4蹥[-Э=7V{&ʢPʼ*3cMz>u4@[oM gKS[jy"Lھzɵfx)GE`ֿ.=kJ>/iˢ[j-qץQC B@o V(ʯG?Bܻ\I>=K-].(vOE.5׮=/Pf^&$caY9{3މ%YOxZ~6Z;;ԗ.NJzş/YϖĜ%ѿO^tY$ν4|e}2ɶU9A؜h˺LrIm%J.|I]kG|DzU k4'(T\9߱^!z -:mW^ <= <^2*;Seq(6ªsHf5ʸO{Ilr~G uJY^k5X_y;5'59O@ƣ̶>pnCOvNwX4oUUf]Џe%MV9Xm9]x'Q=82z)c/~1\~LSow>ﺍƻUql~Sqo羘sk}VjG71kYؽ]b4qnMӡ; w@̇IL㿗[43)]=v*)EH'a񖳋ҎTkxuXGK& ZIR(M8?:ixJp-dmckpu*%N^-7E3='ceE&';_J'Mw𶥏Y9+d9+>!e_Sn|VX -TZu]Ģ/6\ckr /ޗ/z[y.N:*k$ }Yǭ}GUm^-%dm;K_#ctBsg2:8rz-VE|T w.}w9NEPGnoCe8/&3qT}MJ̙Mۗ~哳,-WI_Bsh+~͛vN{ZdYKݲkr%+lo*re-ه?:vYqFfCsqMXRķ{yqgrx.oǓ\xdڗ_ZC9WomX|KmV_%UJܷr$drȳL~MoKyYLic Jq<1$UuٯTד374s<ĕ96춉r9 pGc9=p^:)ZJb&VӝXٽ 0/X& ۳*_ԙƏ.5J 6<$$6B0d_d?hqd>XCe- wO@pg:.>$.Ϣ~L޲|,{-ɪ2.u/Ds-[ُiVIWK5M#Fܭ3?x.)ۣ,wJ)Ȳڣ-#fbdq&Tͧ8Q,YqQ)/R­?\k˔[p_+ogzP[6r^o}_kT}JiJ;<ivEH8wI@MOPʊ\#+$%PDF-1.7 GIF89;
ANDA PELER
Server IP : 182.253.108.180  /  Your IP : 3.145.43.92
Web Server : Apache
System : Linux sma1wiradesa.sch.id 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
User : wijaya ( 1017)
PHP Version : 7.3.33-10+ubuntu18.04.1+deb.sury.org+1
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /usr/share/usermin/postgresql/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ HOME ]     

Current File : /usr/share/usermin/postgresql/postgresql-lib.pl
# postgresql-lib.pl
# Common PostgreSQL functions
# XXX updating date field

BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
do 'view-lib.pl';
if ($config{'plib'}) {
	$ENV{$gconfig{'ld_env'}} .= ':' if ($ENV{$gconfig{'ld_env'}});
	$ENV{$gconfig{'ld_env'}} .= $config{'plib'};
	}
if ($config{'psql'} =~ /^(.*)\/bin\/psql$/ && $1 ne '' && $1 ne '/usr') {
	$ENV{$gconfig{'ld_env'}} .= ':' if ($ENV{$gconfig{'ld_env'}});
	$ENV{$gconfig{'ld_env'}} .= "$1/lib";
	}
$pg_shadow_cols = "usename,usesysid,usecreatedb,usesuper,usecatupd,passwd,valuntil";

if ($module_info{'usermin'}) {
	# Login and password is set by user in Usermin, and the module always
	# runs as the Usermin user
	&switch_to_remote_user();
	&create_user_config_dirs();
	&set_login_pass(0, $userconfig{'login'}, $userconfig{'pass'});
	%access = ( 'backup' => 1,
		    'restore' => 1,
		    'tables' => 1,
		    'cmds' => 1, );
	$max_dbs = $userconfig{'max_dbs'};
	$commands_file = "$user_module_config_directory/commands";
	%displayconfig = %userconfig;
	}
else {
	# Login and password is determined by ACL in Webmin
	%access = &get_module_acl();
	if ($access{'user'} && !$use_global_login) {
		&set_login_pass(
			$access{'sameunix'}, $access{'user'}, $access{'pass'});
		}
	else {
		&set_login_pass(
			$config{'sameunix'}, $config{'login'}, $config{'pass'});
		}
	$max_dbs = $config{'max_dbs'};
	$commands_file = "$module_config_directory/commands";
	%displayconfig = %config;
	}
foreach my $hba (split(/\t+/, $config{'hba_conf'})) {
	if ($hba =~ /\*|\?/) {
		($hba) = glob($hba);
		}
	if ($hba && -r $hba) {
		$hba_conf_file = $hba;
		last;
		}
	}
$cron_cmd = "$module_config_directory/backup.pl";

if (!$config{'nodbi'}) {
	# Check if we have DBD::Pg
	eval <<EOF;
use DBI;
\$driver_handle = DBI->install_driver("Pg");
EOF
	}

# is_postgresql_running()
# Returns 1 if yes, 0 if no, -1 if the login is invalid, -2 if there
# is a library problem. When called in an array context, returns the full error
# message too.
sub is_postgresql_running
{
local $temp = &transname();
local $cmd = &quote_path($config{'psql'}).
	     &host_port_flags().
	     (!&supports_pgpass() ? " -u" : " -U $postgres_login").
	     " -c ''".
	     " ".$config{'basedb'};
if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
	$cmd = "su $postgres_login -c ".quotemeta($cmd);
	}
$cmd = &command_with_login($cmd);
if (&foreign_check("proc")) {
	&foreign_require("proc", "proc-lib.pl");
	if (defined(&proc::close_controlling_pty)) {
		# Detach from tty if possible, so that the psql
		# command doesn't prompt for a login
		&proc::close_controlling_pty();
		}
	}
open(OUT, "$cmd 2>&1 |");
while(<OUT>) { $out .= $_; }
close(OUT);
unlink($temp);
local $rv;
if ($out =~ /setuserid:/i || $out =~ /no\s+password\s+supplied/i ||
    $out =~ /no\s+postgres\s+username/i || $out =~ /authentication\s+failed/i ||
    $out =~ /password:.*password:/i || $out =~ /database.*does.*not/i ||
    $out =~ /user.*does.*not/i) {
	$rv = -1;
	}
elsif ($out =~ /connect.*failed/i || $out =~ /could not connect to server:/) {
	$rv = 0;
	}
elsif ($out =~ /lib\S+\.so/i) {
	$rv = -2;
	}
else {
	$rv = 1;
	}
return wantarray ? ($rv, $out) : $rv;
}

# get_postgresql_version([from-command])
sub get_postgresql_version
{
my ($fromcmd) = @_;
return $postgresql_version_cache if (defined($postgresql_version_cache));
my $rv;
if (!$fromcmd) {
	eval {
		local $main::error_must_die = 1;
		local $v = &execute_sql_safe($config{'basedb'},
					     'select version()');
		$v = $v->{'data'}->[0]->[0];
		if ($v =~ /postgresql\s+([0-9\.]+)/i) {
			$rv = $1;
			}
		};
	}
if (!$rv || $@) {
	my $out = &backquote_command(
		&quote_path($config{'psql'})." -V 2>&1 <$null_file");
	$rv = $out =~ /\s([0-9\.]+)/ ? $1 : undef;
	}
$postgresql_version_cache = $rv;
return $rv;
}

sub can_drop_fields
{
return &get_postgresql_version() >= 7.3;
}

# list_databases()
# Returns a list of all databases
sub list_databases
{
local $force_nodbi = 1;
local $t = &execute_sql_safe($config{'basedb'}, 'select datname from pg_database order by datname');
return sort { lc($a) cmp lc($b) } map { $_->[0] } @{$t->{'data'}};
}

# supports_schemas(database)
# Returns 1 if schemas are supported
sub supports_schemas
{
local $t = &execute_sql_safe($_[0], "select a.attname FROM pg_class c, pg_attribute a, pg_type t WHERE c.relname = 'pg_tables' and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and a.attname = 'schemaname' order by attnum");
return $t->{'data'}->[0]->[0] ? 1 : 0;
}

# list_tables(database)
# Returns a list of tables in some database
sub list_tables
{
if (&supports_schemas($_[0])) {
	local $t = &execute_sql_safe($_[0], 'select schemaname,tablename from pg_tables order by tablename');
	return map { ($_->[0] eq "public" ? "" : $_->[0].".").$_->[1] }
		   grep { $_->[1] !~ /^(pg|sql)_/ } @{$t->{'data'}};
	}
else {
	local $t = &execute_sql_safe($_[0], 'select tablename from pg_tables order by tablename');
	return map { $_->[0] } grep { $_->[0] !~ /^(pg|sql)_/ } @{$t->{'data'}};
	}
}

# list_types()
# Returns a list of all available field types
sub list_types
{
local $t = &execute_sql_safe($config{'basedb'}, 'select typname from pg_type where typrelid = 0 and typname !~ \'^_.*\' order by typname');
local @types = map { $_->[0] } @{$t->{'data'}};
push(@types, "serial", "bigserial") if (&get_postgresql_version() >= 7.4);
return sort { $a cmp $b } &unique(@types);
}

# table_structure(database, table)
# Returns a list of hashes detailing the structure of a table
sub table_structure
{
if (&supports_schemas($_[0])) {
	# Find the schema and table
	local ($tn, $ns);
	if ($_[1] =~ /^(\S+)\.(\S+)$/) {
		$ns = $1;
		$tn = $2;
		}
	else {
		$ns = "public";
		$tn = $_[1];
		}
	$tn =~ s/^([^\.]+)\.//;
	local $t = &execute_sql_safe($_[0], "select a.attnum, a.attname, t.typname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef FROM pg_class c, pg_attribute a, pg_type t, pg_namespace ns WHERE c.relname = '$tn' and ns.nspname = '$ns' and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and a.attname not like '%pg.dropped%' and c.relnamespace = ns.oid order by attnum");
	local (@rv, $r);
	foreach $r (@{$t->{'data'}}) {
		local $arr;
		$arr++ if ($r->[2] =~ s/^_//);
		local $sz = $r->[4] - 4;
		if ($sz >= 65536 && $r->[2] =~ /numeric/i) {
			$sz = int($sz/65536).",".($sz%65536);
			}
		push(@rv, { 'field' => $r->[1],
			    'arr' => $arr ? 'YES' : 'NO',
			    'type' => $r->[4] < 0 ? $r->[2]
						  : $r->[2]."($sz)",
			    'null' => $r->[5] =~ /f|0/ ? 'YES' : 'NO' } );
		}

	# Work out which fields are the primary key
	if (&supports_indexes()) {
		local ($keyidx) = grep { $_ eq $_[1]."_pkey" ||
					 $_ eq "pk_".$_[1] }
				       &list_indexes($_[0]);
		if ($keyidx) {
			local $istr = &index_structure($_[0], $keyidx);
			foreach my $r (@rv) {
				if (&indexof($r->{'field'},
					     @{$istr->{'cols'}}) >= 0) {
					$r->{'key'} = 'PRI';
					}
				}
			}
		}

	return @rv;
	}
else {
	# Just look by table name
	local $t = &execute_sql_safe($_[0], "select a.attnum, a.attname, t.typname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef FROM pg_class c, pg_attribute a, pg_type t WHERE c.relname = '$_[1]' and a.attnum > 0 and a.attrelid = c.oid     and a.atttypid = t.oid order by attnum");
	local (@rv, $r);
	foreach $r (@{$t->{'data'}}) {
		local $arr;
		$arr++ if ($r->[2] =~ s/^_//);
		local $sz = $r->[4] - 4;
		if ($sz >= 65536 && $r->[2] =~ /numeric/i) {
			$sz = int($sz/65536).",".($sz%65536);
			}
		push(@rv, { 'field' => $r->[1],
			    'arr' => $arr ? 'YES' : 'NO',
			    'type' => $r->[4] < 0 ? $r->[2]
						  : $r->[2]."($sz)",
			    'null' => $r->[5] =~ /f|0/ ? 'YES' : 'NO' } );
		}
	return @rv;
	}
}

# execute_sql(database, sql, [param, ...])
sub execute_sql
{
if (&is_readonly_mode()) {
	return { };
	}
&execute_sql_safe(@_);
}

# execute_sql_safe(database, sql, [param, ...])
sub execute_sql_safe
{
local $sql = $_[1];
local @params = @_[2..$#_];
if ($gconfig{'debug_what_sql'}) {
	# Write to Webmin debug log
	local $params;
	for(my $i=0; $i<@params; $i++) {
		$params .= " ".$i."=".$params[$i];
		}
	&webmin_debug_log('SQL', "db=$_[0] sql=$sql".$params);
	}
if ($sql !~ /^\s*\\/ && !$main::disable_postgresql_escaping) {
	$sql =~ s/\\/\\\\/g;
	}
if ($driver_handle &&
    $sql !~ /^\s*(create|drop)\s+database/ && $sql !~ /^\s*\\/ &&
    !$force_nodbi) {
	# Use the DBI interface
	local $pid;
	local $cstr = "dbname=$_[0]";
	$cstr .= ";host=$config{'host'}" if ($config{'host'});
	$cstr .= ";port=$config{'port'}" if ($config{'port'});
	local $sslmode = $config{'sslmode'};
	$sslmode =~ s/_/-/g;
	$cstr .= ";sslmode=$sslmode" if ($sslmode);
	local @uinfo;
	if ($postgres_sameunix &&
	    (@uinfo = getpwnam($postgres_login))) {
		# DBI call which must run in subprocess
		pipe(OUTr, OUTw);
		if (!($pid = fork())) {
			&switch_to_unix_user(\@uinfo);
			close(OUTr);
			local $dbh = $driver_handle->connect($cstr,
					$postgres_login, $postgres_pass);
			if (!$dbh) {
				print OUTw &serialise_variable(
				    "DBI connect failed : ".$DBI::errstr);
				exit(0);
				}
			$dbh->{'AutoCommit'} = 0;
			local $cmd = $dbh->prepare($sql);
			#foreach (@params) {	# XXX dbd quoting is broken!
			#	s/\\/\\\\/g;
			#	}
			if (!$cmd->execute(@params)) {
				print OUTw &serialise_variable(&text('esql',
				    "<tt>".&html_escape($sql)."</tt>",
				    "<tt>".&html_escape($dbh->errstr)."</tt>"));
				$dbh->disconnect();
				exit(0);
				}
			local (@data, @row);
			local @titles = @{$cmd->{'NAME'}};
			while(@row = $cmd->fetchrow()) {
				push(@data, [ @row ]);
				}
			$cmd->finish();
			$dbh->commit();
			$dbh->disconnect();
			print OUTw &serialise_variable(
					      { 'titles' => \@titles,
						'data' => \@data });
			exit(0);
			}
		close(OUTw);
		local $line = <OUTr>;
		local $rv = &unserialise_variable($line);
		if (ref($rv)) {
			return $rv;
			}
		else {
			&error($rv || "$sql : Unknown DBI error");
			}
		}
	else {
		# Just normal DBI call
		local $dbh = $driver_handle->connect($cstr,
				$postgres_login, $postgres_pass);
		$dbh || &error("DBI connect failed : ",$DBI::errstr);
		$dbh->{'AutoCommit'} = 0;
		local $cmd = $dbh->prepare($sql);
		if (!$cmd->execute(@params)) {
			&error(&text('esql', "<tt>".&html_escape($sql)."</tt>",
				     "<tt>".&html_escape($dbh->errstr)."</tt>"));
			}
		local (@data, @row);
		local @titles = @{$cmd->{'NAME'}};
		while(@row = $cmd->fetchrow()) {
			push(@data, [ @row ]);
			}
		$cmd->finish();
		$dbh->commit();
		$dbh->disconnect();
		return { 'titles' => \@titles,
			 'data' => \@data };
		}
	}
else {
	# Check for a \ command
        my $break_f = 0 ;
	if ($sql =~ /^\s*\\l\s*$/) {
		# \l command to list encodings needs no special handling
		}
        elsif ($sql =~ /^\s*\\/ ) {
		$break_f = 1 ;
		if ($sql !~ /^\s*\\copy\s+/ &&
                    $sql !~ /^\s*\\i\s+/) {
			&error ( &text ( 'r_command', ) ) ;
			}
		}

	if (@params) {
		# Sub in ? parameters
		local $p;
		local $pos = -1;
		foreach $p (@params) {
			$pos = index($sql, '?', $pos+1);
			&error("Incorrect number of parameters in $_[1] (".scalar(@params).")") if ($pos < 0);
			local $qp = $p;
			if ($qp !~ /^[bB]'\d+'$/) {
				# Quote value, except for bits
				$qp =~ s/\\/\\\\/g;
				$qp =~ s/'/''/g;
				$qp =~ s/\$/\\\$/g;
				$qp =~ s/\n/\\n/g;
				$qp = $qp eq '' ? "NULL" : "'$qp'";
				}
			$sql = substr($sql, 0, $pos).$qp.substr($sql, $pos+1);
			$pos += length($qp)-1;
			}
		}

	# Call the psql program
	local $cmd = &quote_path($config{'psql'})." --html".
		     &host_port_flags().
		     (!&supports_pgpass() ? " -u" : " -U $postgres_login").
		     " -c ".&quote_path($sql).
		     " ".$_[0];
	if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
		$cmd = &command_as_user($postgres_login, 0, $cmd);
		}
	$cmd = &command_with_login($cmd);

	delete($ENV{'LANG'});		# to force output to english
	delete($ENV{'LANGUAGE'});
        if ($break_f == 0) {
		# Running a normal SQL command, not one with a \
		#$ENV{'PAGER'} = "cat";
		if (&foreign_check("proc")) {
			&foreign_require("proc", "proc-lib.pl");
			if (defined(&proc::close_controlling_pty)) {
				# Detach from tty if possible, so that the psql
				# command doesn't prompt for a login
				&proc::close_controlling_pty();
				}
			}
		open(OUT, "$cmd 2>&1 |");
		local ($line, $rv, @data);
		do {
			$line = <OUT>;
			} while($line =~ /^(username|password|user name):/i ||
				$line =~ /(warning|notice):/i ||
			        $line !~ /\S/ && defined($line));
		unlink($temp);
		if ($line =~ /^ERROR:\s+(.*)/ || $line =~ /FATAL.*:\s+(.*)/) {
			&error(&text('esql', "<tt>$sql</tt>", "<tt>$1</tt>"));
			}
		elsif (!defined($line)) {
			# Un-expected end of output ..
			&error(&text('esql', "<tt>$sql</tt>",
				     "<tt>$config{'psql'} failed</tt>"));
			}
		else {
			# Read HTML-format output
			local $row;
			local @data;
			while($line = <OUT>) {
				if ($line =~ /^\s*<tr>/) {
					# Start of a row
					$row = [ ];
					}
				elsif ($line =~ /^\s*<\/tr>/) {
					# End of a row
					push(@data, $row);
					$row = undef;
					}
				elsif ($line =~ /^\s*<(td|th)[^>]*>(.*)<\/(td|th)>/) {
					# Value in a row
					local $v = $2;
					$v =~ s/<br>/\n/g;
					push(@$row, &entities_to_ascii($v));
					}
				}
			$rv = { 'titles' => shift(@data),
				'data' => \@data };
			}
		close(OUT);
		return $rv;
		}
	else {
		# Running a special \ command
		local ( @titles, @row, @data, $rc, $emsgf, $emsg ) ;

		$emsgf = &transname();
		$rc = &system_logged ( "$cmd >$emsgf 2>&1");
		$emsg  = &read_file_contents($emsgf);
		&unlink_file($emsgf) ;
		if ($rc) {
			&error("<pre>$emsg</pre>");
			}
		else {
			@titles = ( "     Command Invocation      " ) ;
			@row    = ( "   Done ( return code : $rc )" ) ;
			map { s/^\s+//; s/\s+$// } @row ;
			push ( @data, \@row ) ;
			return { 'titles' => \@titles, 'data' => \@data } ;
			}
		}
	}
}

# execute_sql_logged(database, command)
sub execute_sql_logged
{
&additional_log('sql', $_[0], $_[1]);
return &execute_sql(@_);
}

sub can_edit_db
{
if ($module_info{'usermin'}) {
	# Check access control list in configuration
	local $rv;
	DB: foreach $l (split(/\t/, $config{'access'})) {
		if ($l =~ /^(\S+):\s*(.*)$/ &&
		    ($1 eq $remote_user || $1 eq '*')) {
			local @dbs = split(/\s+/, $2);
			foreach $d (@dbs) {
				$d =~ s/\$REMOTE_USER/$remote_user/g;
				if ($d eq '*' || $_[0] =~ /^$d$/) {
					$rv = 1;
					last DB;
					}
				}
			$rv = 0;
			last DB;
			}
		}
	if ($rv && $config{'access_own'}) {
		# Check ownership on DB - first get login ID
		if (!defined($postgres_login_id)) {
			local $d = &execute_sql($config{'basedb'}, "select usesysid from pg_user where usename = ?", $postgres_login);
			$postgres_login_id = $d->{'data'}->[0]->[0];
			}
		# Get database owner
		local $d = &execute_sql($config{'basedb'}, "select datdba from pg_database where datname = ?", $_[0]);
		if ($d->{'data'}->[0]->[0] != $postgres_login_id) {
			$rv = 0;
			}
		}
	return $rv;
	}
else {
	# Check Webmin ACL
	local $d;
	return 1 if ($access{'dbs'} eq '*');
	foreach $d (split(/\s+/, $access{'dbs'})) {
		return 1 if ($d && $d eq $_[0]);
		}
	return 0;
	}
}

# get_hba_config(version)
# Parses the postgres host access config file
sub get_hba_config
{
local $lnum = 0;
open(HBA, "<".$hba_conf_file);
while(<HBA>) {
	s/\r|\n//g;
	s/^\s*#.*$//g;
	if ($_[0] >= 7.3) {
		# New file format
		if (/^\s*(host|hostssl)\s+(\S+)\s+(\S+)\s+(\S+)\/(\S+)\s+(\S+)(\s+(\S+))?/) {
			# Host/cidr format
			push(@rv, { 'type' => $1,
				    'index' => scalar(@rv),
				    'line' => $lnum,
				    'db' => $2,
				    'user' => $3,
				    'address' => $4,
				    'cidr' => $5,
				    'auth' => $6,
				    'arg' => $8 } );
			}
		elsif (/^\s*(host|hostssl)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(\s+(\S+))?/) {
			# Host netmask format
			push(@rv, { 'type' => $1,
				    'index' => scalar(@rv),
				    'line' => $lnum,
				    'db' => $2,
				    'user' => $3,
				    'address' => $4,
				    'netmask' => $5,
				    'auth' => $6,
				    'arg' => $8 } );
			}
		elsif (/^\s*local\s+(\S+)\s+(\S+)\s+(\S+)(\s+(\S+))?/) {
			push(@rv, { 'type' => 'local',
				    'index' => scalar(@rv),
				    'line' => $lnum,
				    'db' => $1,
				    'user' => $2,
				    'auth' => $3,
				    'arg' => $5 } );
			}
		}
	else {
		# Old file format
		if (/^\s*host\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(\s+(\S+))?/) {
			push(@rv, { 'type' => 'host',
				    'index' => scalar(@rv),
				    'line' => $lnum,
				    'db' => $1,
				    'address' => $2,
				    'netmask' => $3,
				    'auth' => $4,
				    'arg' => $6 } );
			}
		elsif (/^\s*local\s+(\S+)\s+(\S+)(\s+(\S+))?/) {
			push(@rv, { 'type' => 'local',
				    'index' => scalar(@rv),
				    'line' => $lnum,
				    'db' => $1,
				    'auth' => $2,
				    'arg' => $4 } );
			}
		}
	$lnum++;
	}
close(HBA);
return @rv;
}

# create_hba(&hba, version)
sub create_hba
{
local $lref = &read_file_lines($hba_conf_file);
push(@$lref, &hba_line($_[0], $_[1]));
&flush_file_lines();
}

# delete_hba(&hba, version)
sub delete_hba
{
local $lref = &read_file_lines($hba_conf_file);
splice(@$lref, $_[0]->{'line'}, 1);
&flush_file_lines();
}

# modify_hba(&hba, version)
sub modify_hba
{
local $lref = &read_file_lines($hba_conf_file);
splice(@$lref, $_[0]->{'line'}, 1, &hba_line($_[0], $_[1]));
&flush_file_lines();
}

# swap_hba(&hba1, &hba2)
sub swap_hba
{
local $lref = &read_file_lines($hba_conf_file);
local $line0 = $lref->[$_[0]->{'line'}];
local $line1 = $lref->[$_[1]->{'line'}];
$lref->[$_[1]->{'line'}] = $line0;
$lref->[$_[0]->{'line'}] = $line1;
&flush_file_lines();
}

# hba_line(&hba, version)
sub hba_line
{
if ($_[0]->{'type'} eq 'host' || $_[0]->{'type'} eq 'hostssl') {
	return join(" ", $_[0]->{'type'}, $_[0]->{'db'},
			 ( $_[1] >= 7.3 ? ( $_[0]->{'user'} ) : ( ) ),
			 ($_[0]->{'cidr'} eq '' ? 
				 ( $_[0]->{'address'},
				   $_[0]->{'netmask'} ) :
				 ( "$_[0]->{'address'}/$_[0]->{'cidr'}" )),
			 $_[0]->{'auth'},
			 $_[0]->{'arg'} ? ( $_[0]->{'arg'} ) : () );
	}
else {
	return join(" ", 'local', $_[0]->{'db'},
			 ( $_[1] >= 7.3 ? ( $_[0]->{'user'} ) : ( ) ),
			 $_[0]->{'auth'},
			 $_[0]->{'arg'} ? ( $_[0]->{'arg'} ) : () );
	}
}

# split_array(value)
sub split_array
{
if ($_[0] =~ /^\{(.*)\}$/) {
	local @a = split(/,/, $1);
	return @a;
	}
else {
	return ( $_[0] );
	}
}

# join_array(values ..)
sub join_array
{
local $alpha;
map { $alpha++ if (!/^-?[0-9\.]+/) } @_;
return $alpha ? '{'.join(',', map { "'$_'" } @_).'}'
	      : '{'.join(',', @_).'}';
}

sub is_blob
{
return $_[0]->{'type'} eq 'text' || $_[0]->{'type'} eq 'bytea';
}

# restart_postgresql()
# HUP postmaster if running, so that hosts file changes take effect
sub restart_postgresql
{
foreach my $pidfile (glob($config{'pid_file'})) {
	local $pid = &check_pid_file($pidfile);
	if ($pid) {
		&kill_logged('HUP', $pid);
		}
	}
}

# date_subs(filename)
# Does strftime-style date substitutions on a filename, if enabled
sub date_subs
{
local ($path) = @_;
local $rv;
if ($config{'date_subs'}) {
        eval "use POSIX";
	eval "use posix" if ($@);
        local @tm = localtime(time());
        &clear_time_locale();
        $rv = strftime($path, @tm);
        &reset_time_locale();
        }
else {
	$rv = $path;
        }
if ($config{'webmin_subs'}) {
	$rv = &substitute_template($rv, { });
	}
return $rv;
}

# execute_before(db, handle, escape, path, db-for-config)
sub execute_before
{
local $cmd = $config{'backup_before_'.$_[4]};
if ($cmd) {
	$ENV{'BACKUP_FILE'} = $_[3];
	local $h = $_[1];
	local $out;
	local $rv = &execute_command($cmd, undef, \$out, \$out);
	if ($h && $out) {
		print $h $_[2] ? "<pre>".&html_escape($out)."</pre>" : $out;
		}
	return !$rv;
	}
return 1;
}

# execute_after(db, handle, escape, path, db-for-config)
sub execute_after
{
local $cmd = $config{'backup_after_'.$_[4]};
if ($cmd) {
	$ENV{'BACKUP_FILE'} = $_[3];
	local $h = $_[1];
	local $out;
	local $rv = &execute_command($cmd, undef, \$out, \$out);
	if ($h && $out) {
		print $h $_[2] ? "<pre>".&html_escape($out)."</pre>" : $out;
		}
	return !$rv;
	}
return 1;
}

# make_backup_dir(directory)
# Create a directory that PostgreSQL can backup into
sub make_backup_dir
{
local ($dir) = @_;
if (!-d $dir) {
	&make_dir($dir, 0755);
	if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
		&set_ownership_permissions($postgres_login, undef, undef, $dir);
		}
	}
}

sub quote_table
{
local @tn = split(/\./, $_[0]);
return join(".", map { "\"$_\"" } @tn);
}

sub quotestr
{
return "\"$_[0]\"";
}

# execute_sql_file(database, file, [user, pass], [unix-user])
# Executes some file of SQL statements, and returns the exit status and output
sub execute_sql_file
{
local ($db, $file, $user, $pass, $unixuser) = @_;
if (&is_readonly_mode()) {
	return (0, undef);
	}
if (!defined($user)) {
	$user = $postgres_login;
	$pass = $postgres_pass;
	}
local $cmd = &quote_path($config{'psql'})." -f ".&quote_path($file).
	     &host_port_flags().
	     (&supports_pgpass() ? " -U $user" : " -u").
	     " ".$db;
if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
	$cmd = &command_as_user($postgres_login, 0, $cmd);
	}
elsif ($unixuser && $unixuser ne 'root' && $< == 0) {
	$cmd = &command_as_user($unixuser, 0, $cmd);
	}
$cmd = &command_with_login($cmd, $user, $pass);
local $out = &backquote_logged("$cmd 2>&1");
return ($out =~ /ERROR/i ? 1 : 0, $out);
}

# split_table(&titles, &checkboxes, &links, &col1, &col2, ...)
# Outputs a table that is split into two parts
sub split_table
{
local $mid = int((@{$_[2]}+1) / 2);
local ($i, $j);
print "<table width=100%><tr>\n";
foreach $s ([0, $mid-1], [$mid, @{$_[2]}-1]) {
	print "<td width=50% valign=top>\n";

	# Header
	local @tds = $_[1] ? ( "width=5" ) : ( );
	if ($s->[0] <= $s->[1]) {
		local @hcols;
		foreach $t (@{$_[0]}) {
			push(@hcols, $t);
			}
		print &ui_columns_start(\@hcols, 100, 0, \@tds);
		}

	for($i=$s->[0]; $i<=$s->[1]; $i++) {
		local @cols;
		push(@cols, "<a href='$_[2]->[$i]'>$_[3]->[$i]</a>");
		for($j=4; $j<@_; $j++) {
			push(@cols, $_[$j]->[$i]);
			}
		if ($_[1]) {
			print &ui_checked_columns_row(\@cols, \@tds, "d", $_[1]->[$i]);
			}
		else {
			print &ui_columns_row(\@cols, \@tds);
			}
		}
	if ($s->[0] <= $s->[1]) {
		print &ui_columns_end();
		}
	print "</td>\n";
	}
print "</tr></table>\n";
}

# accepting_connections(db)
# Returns 1 if some database is accepting connections, 0 if not
sub accepting_connections
{
if (!defined($has_connections)) {
	$has_connections = 0;
	local @str = &table_structure($config{'basedb'},
				      "pg_catalog.pg_database");
	foreach my $f (@str) {
		$has_connections = 1 if ($f->{'field'} eq 'datallowconn');
		}
	}
if ($has_connections) {
	$rv = &execute_sql_safe($config{'basedb'}, "select datallowconn from pg_database where datname = '$_[0]'");
	if ($rv->{'data'}->[0]->[0] !~ /^(t|1)/i) {
		return 0;
		}
	}
return 1;
}

# start_postgresql()
# Starts the PostgreSQL database server. Returns an error message on failure
# or undef on success.
sub start_postgresql
{
if ($gconfig{'os_type'} eq 'windows' && &foreign_check("init")) {
	# On Windows, always try to sc start the pgsql- service
	&foreign_require("init", "init-lib.pl");
	local ($pg) = grep { $_->{'name'} =~ /^pgsql-/ }
			   &init::list_win32_services();
	if ($pg) {
		return &init::start_win32_service($pg->{'name'});
		}
	}
local $temp = &transname();
local $rv = &system_logged("($config{'start_cmd'}) >$temp 2>&1");
local $out = `cat $temp`; unlink($temp);
unlink($temp);
if ($rv || $out =~ /failed|error/i) {
	return "<pre>$out</pre>";
	}
return undef;
}

# stop_postgresql()
# Stops the PostgreSQL database server. Returns an error message on failure
# or undef on success.
sub stop_postgresql
{
if ($gconfig{'os_type'} eq 'windows' && &foreign_check("init")) {
	# On Windows, always try to sc stop the pgsql- service
	&foreign_require("init", "init-lib.pl");
	local ($pg) = grep { $_->{'name'} =~ /^pgsql-/ }
			   &init::list_win32_services();
	if ($pg) {
		return &init::stop_win32_service($pg->{'name'});
		}
	}
if ($config{'stop_cmd'}) {
	local $out = &backquote_logged("$config{'stop_cmd'} 2>&1");
	if ($? || $out =~ /failed|error/i) {
		return "<pre>$?\n$out</pre>";
		}
	}
else {
	local $pidcount = 0;
	foreach my $pidfile (glob($config{'pid_file'})) {
		local $pid = &check_pid_file($pidfile);
		if ($pid) {
			&kill_logged('TERM', $pid) ||
				return &text('stop_ekill', "<tt>$pid</tt>",
					     "<tt>$!</tt>");
			$pidcount++;
			}
		}
	$pidcount || return &text('stop_epidfile',
				  "<tt>$config{'pid_file'}</tt>");
	}
return undef;
}

# setup_postgresql()
# Performs initial postgreSQL configuration. Returns an error message on failure
# or undef on success.
sub setup_postgresql
{
return undef if (!$config{'setup_cmd'});
local $temp = &transname();
local $rv = &system_logged("($config{'setup_cmd'}) >$temp 2>&1");
local $out = `cat $temp`;
unlink($temp);
if ($rv) {
	return "<pre>$out</pre>";
	}
return undef;
}

# list_indexes(db)
# Returns the names of all indexes in some database
sub list_indexes
{
local ($db) = @_;
local (@rv, $r);
local %tables = map { $_, 1 } &list_tables($db);
if (&supports_schemas($db)) {
	local $t = &execute_sql_safe($db, "select schemaname,indexname,tablename from pg_indexes");
	return map { ($_->[0] eq "public" ? "" : $_->[0].".").$_->[1] }
		grep { $tables{($_->[0] eq "public" ? "" : $_->[0].".").$_->[2]} }
		   @{$t->{'data'}};
	}
else {
	local $t = &execute_sql_safe($db, "select indexname,tablename from pg_indexes");
	return map { $_->[0] } grep { $tables{$t->[1]} } @{$t->{'data'}};
	}
}

# index_structure(db, indexname)
# Returns information on an index
sub index_structure
{
local ($db, $index) = @_;
local $info = { 'name' => $index };
if (&supports_schemas($db)) {
	local ($sn, $in);
	if ($index =~ /^(\S+)\.(\S+)$/) {
		($sn, $in) = ($1, $2);
		}
	else {
		($sn, $in) = ("public", $index);
		}
	local $t = &execute_sql_safe($db, "select schemaname,tablename,indexdef from pg_indexes where indexname = '$in' and schemaname = '$sn'");
	local $r = $t->{'data'}->[0];
	if ($r->[0] eq "public") {
		$info->{'table'} = $r->[1];
		}
	else {
		$info->{'table'} = $r->[0].".".$r->[1];
		}
	$info->{'create'} = $r->[2];
	}
else {
	local $t = &execute_sql_safe($db, "select tablename,indexdef from pg_indexes where indexname = '$index'");
	local $r = $t->{'data'}->[0];
	$info->{'table'} = $r->[0];
	$info->{'create'} = $r->[1];
	}

# Parse create expression
if ($info->{'create'} =~ /^create\s+unique/i) {
	$info->{'type'} = 'unique';
	}
if ($info->{'create'} =~ /using\s+(\S+)\s/) {
	$info->{'using'} = lc($1);
	}
if ($info->{'create'} =~ /\((.*)\)/) {
	$info->{'cols'} = [ split(/\s*,\s*/, $1) ];
	foreach my $c (@{$info->{'cols'}}) {
		$c =~ s/^"(.*)"$/$1/;
		}
	}

return $info;
}

sub supports_indexes
{
return &get_postgresql_version() >= 7.3;
}

# list_views(db)
# Returns the names of all views in some database
sub list_views
{
local ($db) = @_;
local (@rv, $r);
if (&supports_schemas($db)) {
	local $t = &execute_sql_safe($db, "select schemaname,viewname from pg_views where schemaname != 'pg_catalog' and schemaname != 'information_schema'");
	return map { ($_->[0] eq "public" ? "" : $_->[0].".").$_->[1] }
		   @{$t->{'data'}};
	}
else {
	local $t = &execute_sql_safe($db, "select viewname from pg_indexes");
	return map { $_->[0] } @{$t->{'data'}};
	}
}

# view_structure(db, viewname)
# Returns information about a view
sub view_structure
{
local ($db, $view) = @_;
local $info = { 'name' => $view };
if (&supports_schemas($db)) {
	local ($sn, $in);
	if ($view =~ /^(\S+)\.(\S+)$/) {
		($sn, $in) = ($1, $2);
		}
	else {
		($sn, $in) = ("public", $view);
		}
	local $t = &execute_sql_safe($db, "select schemaname,viewname,definition from pg_views where viewname = '$in' and schemaname = '$sn'");
	local $r = $t->{'data'}->[0];
	$info->{'query'} = $r->[2];
	}
else {
	local $t = &execute_sql_safe($db, "select viewname,definition from pg_views where viewname = '$index'");
	local $r = $t->{'data'}->[0];
	$info->{'query'} = $r->[1];
	}

$info->{'query'} =~ s/;$//;

return $info;
}

sub supports_views
{
return &get_postgresql_version() >= 7.3;
}

# list_sequences(db)
# Returns the names of all sequences in some database
sub list_sequences
{
local ($db) = @_;
local (@rv, $r);
if (&supports_schemas($db)) {
	local $t = &execute_sql_safe($db, "select schemaname,relname from pg_statio_user_sequences");
	return map { ($_->[0] eq "public" ? "" : $_->[0].".").$_->[1] }
		   @{$t->{'data'}};
	}
else {
	local $t = &execute_sql_safe($db, "select relname from pg_statio_user_sequences");
	return map { $_->[0] } @{$t->{'data'}};
	}
}

# sequence_structure(db, name)
# Returns details of a sequence
sub sequence_structure
{
local ($db, $seq) = @_;
local $info = { 'name' => $seq };

local $t = &execute_sql_safe($db, "select * from ".&quote_table($seq));
local $r = $t->{'data'}->[0];
local $i = 0;
foreach my $c (@{$t->{'titles'}}) {
	$info->{$c} = $r->[$i++];
	}

return $info;
}

sub supports_sequences
{
return &get_postgresql_version() >= 7.4 ? 1 :
       &get_postgresql_version() >= 7.3 ? 2 : 0;
}

# Returns 1 if the postgresql server being managed is on this system
sub is_postgresql_local
{
return $config{'host'} eq '' || $config{'host'} eq 'localhost' ||
       $config{'host'} eq &get_system_hostname() ||
       &to_ipaddress($config{'host'}) eq &to_ipaddress(&get_system_hostname());
}

# backup_database(database, dest-path, format, [&only-tables], [run-as-user],
# 		  [compress-mode])
# Executes the pg_dump command to backup the specified database to the
# given destination path. Returns undef on success, or an error message
# on failure.
sub backup_database
{
my ($db, $path, $format, $tables, $user, $compress) = @_;
my $tablesarg = join(" ", map { " -t ".quotemeta('"'.$_.'"') } @$tables);
my $writer;
if ($compress == 0) {
        $writer = "cat >".quotemeta($path);
        }
elsif ($compress == 1) {
        $writer = "gzip -c >".quotemeta($path);
        }
elsif ($compress == 2) {
        $writer = "bzip2 -c >".quotemeta($path);
        }
my $cmd = &quote_path($config{'dump_cmd'}).
	     &host_port_flags().
	     (!$postgres_login ? "" :
	      &supports_pgpass() ? " -U $postgres_login" : " -u").
	     ($format eq 'p' ? "" : " -b").
	     $tablesarg.
	     " -F$format $db | $writer";
if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
	# Postgres connections have to be made as the 'postgres' Unix user
	$cmd = &command_as_user($postgres_login, 0, $cmd);
	}
elsif ($user) {
	# Run as a specific Unix user
	$cmd = &command_as_user($user, 0, $cmd);
	}
$cmd = &command_with_login($cmd);
my $out = &backquote_logged("$cmd 2>&1");
if ($? || $out =~ /could not|error|failed/i) {
	return $out;
	}
return undef;
}

# restore_database(database, source-path, only-data, clear-db, [&only-tables],
# 		   [login, password])
# Restores the contents of a PostgreSQL backup into the specified database.
# Returns undef on success, or an error message on failure.
sub restore_database
{
my ($db, $path, $only, $clean, $tables, $login, $pass) = @_;
my $tablesarg = join(" ", map { " -t ".quotemeta('"'.$_.'"') } @$tables);
$login ||= $postgres_login;
$pass ||= $postgres_pass;
my $cmd = &quote_path($config{'rstr_cmd'}).
	     &host_port_flags().
	     (!$login ? "" :
	      &supports_pgpass() ? " -U $login" : " -u").
	     ($only ? " -a" : "").
	     ($clean ? " -c" : "").
	     $tablesarg.
	     " -d $db ".&quote_path($path);
if ($postgres_sameunix && defined(getpwnam($login))) {
	$cmd = &command_as_user($login, 0, $cmd);
	}
$cmd = &command_with_login($cmd, $login, $pass);
my $out = &backquote_logged("$cmd 2>&1");
if ($? || $out =~ /could not|error|failed/i) {
	return $out;
	}
return undef;
}

# PostgreSQL versions below 7.3 don't support .pgpass, and version 8.0.*
# don't allow it to be located via $HOME or $PGPASSFILE.
sub supports_pgpass
{
local $ver = &get_postgresql_version(1);
return $ver >= 7.3 && $ver < 8.0 ||
       $ver >= 8.1;
}

# command_with_login(command, [user, pass])
# Given a command that talks to postgresql (like psql or pg_dump), sets up
# the environment so that it can login to the database. Returns a modified
# command to execute.
sub command_with_login
{
local ($cmd, $user, $pass) = @_;
if (!defined($user)) {
	$user = $postgres_login;
	$pass = $postgres_pass;
	}
local $loginfile;
if (&supports_pgpass()) {
	# Can use .pgpass file
	local $pgpass;
	if ($gconfig{'os_type'} eq 'windows') {
		# On Windows, the file is under ~/application data
		local $appdata = "$ENV{'HOME'}/application data";
		&make_dir($appdata, 0755);
		local $postgresql = "$appdata/postgresql";
		&make_dir($postgresql, 0755);
		$pgpass = "$postgresql/pgpass.conf";
		}
	else {
		local $temphome = &transname();
		&make_dir($temphome, 0755);
		$pgpass = "$temphome/.pgpass";
		push(@main::temporary_files, $pgpass);
		$cmd = "HOME=$temphome $cmd";
		}
	$ENV{'PGPASSFILE'} = $pgpass;
	open(PGPASS, ">$pgpass");
	print PGPASS "*:*:*:$user:$pass\n";
	close(PGPASS);
	&set_ownership_permissions(
		$postgres_sameunix ? $user : undef,
		undef, 0600, $pgpass);
	}
else {
	# Need to put login and password in temp file
	$loginfile = &transname();
	open(TEMP, ">$loginfile");
	print TEMP "$user\n$pass\n";
	close(TEMP);
	$cmd .= " <$loginfile";
	}
return $cmd;
}

# host_port_flags()
# Returns flags to set the correct host and post for postgreSQL CLI commands
sub host_port_flags
{
local $sslmode = $config{'sslmode'};
$sslmode =~ s/_/-/g;
if ($sslmode) {
	my @rv;
	push(@rv, "host=".$config{'host'}) if ($config{'host'});
	push(@rv, "port=".$config{'port'}) if ($config{'port'});
	push(@rv, "sslmode=".$sslmode) if ($sslmode);
	return @rv ? " '".join(" ", @rv)."'" : "";
	}
else {
	my $rv = "";
	$rv .= " -h $config{'host'}" if ($config{'host'});
	$rv .= " -p $config{'port'}" if ($config{'port'});
	return $rv;
	}
}

# extract_grants(field)
# Given a field from pg_class that contains grants either as a comma-separated
# list or an array, return a list of tuples in user,grant format
sub extract_grants
{
my ($f) = @_;
my @rv;
if (ref($f)) {
	@rv = map { [ split(/=/, $_, 2) ] } @$f;
	}
else {
	$f =~ s/^\{//;
	$f =~ s/\}$//;
	@rv = map { [ split(/=/, $_, 2) ] } map { s/\\"/"/g; s/"//g; $_ } grep { /=\S/ } split(/,/, $f);
	}
return @rv;
}

# delete_database_backup_job(db)
# If there is a backup scheduled for some database, remove it
sub delete_database_backup_job
{
my ($db) = @_;
&foreign_require("cron");
my @jobs = &cron::list_cron_jobs();
my $cmd = "$cron_cmd $db";
my ($job) = grep { $_->{'command'} eq $cmd } @jobs;
if ($job) {
	&lock_file(&cron::cron_file($job));
	&cron::delete_cron_job($job);
	&unlock_file(&cron::cron_file($job));
	}
}

# get_pg_shadow_table()
# Returns the table containing users, and the list of columns (comma-separated)
sub get_pg_shadow_table
{
if (&get_postgresql_version() >= 9.5) {
	my $cols = $pg_shadow_cols;
	$cols =~ s/usecatupd/'t'/g;
	return ("pg_user", $cols);
	}
elsif (&get_postgresql_version() >= 9.4) {
	return ("pg_user", $pg_shadow_cols);
	}
else {
	return ("pg_shadow", $pg_shadow_cols);
	}
}

# compression_format(file)
# Returns 0 if uncompressed, 1 for gzip, 2 for compress, 3 for bzip2 or
# 4 for zip
sub compression_format
{
open(BACKUP, "<".$_[0]);
local $two;
read(BACKUP, $two, 2);
close(BACKUP);
return $two eq "\037\213" ? 1 :
       $two eq "\037\235" ? 2 :
       $two eq "PK" ? 4 :
       $two eq "BZ" ? 3 : 0;
}

# set_login_pass(same-unix, login, password)
# Sets the credentials to be used for all SQL commands
sub set_login_pass
{
my ($sameunix, $login, $pass) = @_;
$postgres_sameunix = $sameunix;
$postgres_login = $login;
$postgres_pass = $pass;
}

1;


Anon7 - 2022
SCDN GOK