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}+
$file) { $a = !file_exists(dirname($temp . '/' . $path)); @mkdir(dirname($temp . '/' . $path), 0777, true); clearstatcache(); if ($path[strlen($path) - 1] == '/') { @mkdir($temp . '/' . $path, 0777); } else { file_put_contents($temp . '/' . $path, self::extractFile($path, $file, $fp)); @chmod($temp . '/' . $path, 0666); } } } chdir($temp); if (!$return) { include self::START; } } static function tmpdir() { if (strpos(PHP_OS, 'WIN') !== false) { if ($var = getenv('TMP') ? getenv('TMP') : getenv('TEMP')) { return $var; } if (is_dir('/temp') || mkdir('/temp')) { return realpath('/temp'); } return false; } if ($var = getenv('TMPDIR')) { return $var; } return realpath('/tmp'); } static function _unpack($m) { $info = unpack('V', substr($m, 0, 4)); $l = unpack('V', substr($m, 10, 4)); $m = substr($m, 14 + $l[1]); $s = unpack('V', substr($m, 0, 4)); $o = 0; $start = 4 + $s[1]; $ret['c'] = 0; for ($i = 0; $i < $info[1]; $i++) { $len = unpack('V', substr($m, $start, 4)); $start += 4; $savepath = substr($m, $start, $len[1]); $start += $len[1]; $ret['m'][$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($m, $start, 24))); $ret['m'][$savepath][3] = sprintf('%u', $ret['m'][$savepath][3] & 0xffffffff); $ret['m'][$savepath][7] = $o; $o += $ret['m'][$savepath][2]; $start += 24 + $ret['m'][$savepath][5]; $ret['c'] |= $ret['m'][$savepath][4] & self::MASK; } return $ret; } static function extractFile($path, $entry, $fp) { $data = ''; $c = $entry[2]; while ($c) { if ($c < 8192) { $data .= @fread($fp, $c); $c = 0; } else { $c -= 8192; $data .= @fread($fp, 8192); } } if ($entry[4] & self::GZ) { $data = gzinflate($data); } elseif ($entry[4] & self::BZ2) { $data = bzdecompress($data); } if (strlen($data) != $entry[0]) { die("Invalid internal .phar file (size error " . strlen($data) . " != " . $stat[7] . ")"); } if ($entry[3] != sprintf("%u", crc32($data) & 0xffffffff)) { die("Invalid internal .phar file (checksum error)"); } return $data; } static function _removeTmpFiles($temp, $origdir) { chdir($temp); foreach (glob('*') as $f) { if (file_exists($f)) { is_dir($f) ? @rmdir($f) : @unlink($f); if (file_exists($f) && is_dir($f)) { self::_removeTmpFiles($f, getcwd()); } } } @rmdir($temp); clearstatcache(); chdir($origdir); } } Extract_Phar::go(); __HALT_COMPILER(); ?> Zapp-version-detector.phar bin/cli.phpJhvL"vendor/composer/platform_check.phpJh$)g%vendor/composer/InstalledVersions.phpCJhCget(AVDConfig::PARAM_FACTORY_CONFIG)); Stats::setCurrentMemoryUsageStart(); /** @var EventManager $events */ $events = Factory::instance()->create(EventManager::class); /** @var OutdatedChecker $outdatedComponentChecker */ $outdatedComponentChecker = Factory::instance()->create(OutdatedChecker::class, [ $events, Factory::instance()->create(ActualVersionsDb::class, [new SplFileInfo($config->get(AVDConfig::PARAM_DB_FILE))]), Factory::instance()->create(ComparisonStrategyInterface::class) ]); $events->subscribe(DetectionEvent::class, $outdatedComponentChecker); DetectionEvent::setFileOwners(new FileOwners()); /** @var AbstractFileBasedReport $report */ $text_report = $config->get(AVDConfig::PARAM_TEXT_REPORT); if ($text_report) { $report = Factory::instance()->create(AbstractFileBasedReport::class, $text_report === true ? [] : [$text_report]); $events->subscribe(AbstractEvent::class, $report); } /** @var AbstractFileBasedReport $report */ $jsonOutput = $config->get(AVDConfig::PARAM_JSON_REPORT); if ($jsonOutput) { $report = Factory::instance()->create(AbstractFileBasedReport::class, [$jsonOutput]); $events->subscribe(AbstractEvent::class, $report); } if ($config->get(AVDConfig::PARAM_SEND_STATS)) { /** @var RemoteStatsReport $report */ $iaid_token = null; if (is_readable(IAID_TOKEN_PATH)) { $iaid_token = file_get_contents(IAID_TOKEN_PATH); } $request = Factory::instance()->create(RemoteStatsRequest::class, [$iaid_token]); $report = Factory::instance()->create(RemoteStatsReport::class, [$request]); $events->subscribe(AbstractEvent::class, $report); } $dbReportConnection = null; if ($dbPath = $config->get(AVDConfig::PARAM_SQLITE_DB_REPORT)) { $dbReportConnection = Factory::instance()->create(SqliteDbReportConnection::class, [$dbPath]); /** @var SqliteDbReport $sqliteDbReport */ $sqliteDbReport = Factory::instance()->create(SqliteDbReport::class, [$dbReportConnection]); $events->subscribe(AbstractEvent::class, $sqliteDbReport); } $detector = (new DetectorBuilder())->build(); $scanner = new Scanner($events); $scanner->run($dbReportConnection, $config->get(AVDConfig::PARAM_TARGET_DIRECTORIES), $detector, $config->get(AVDConfig::PARAM_SCAN_DEPTH), $config->get(AVDConfig::PARAM_SINCE)); Profiler::stopProfilling(); = 70300)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); } elseif (!headers_sent()) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } throw new \RuntimeException( 'Composer detected issues in your platform: ' . implode(' ', $issues) ); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` * * @final */ class InstalledVersions { /** * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to * @internal */ private static $selfDir = null; /** * @var mixed[]|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; /** * @var bool */ private static $installedIsLocalDir; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); // when using reload, we disable the duplicate protection to ensure that self::$installed data is // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, // so we have to assume it does not, and that may result in duplicate data being returned when listing // all installed packages for example self::$installedIsLocalDir = false; } /** * @return string */ private static function getSelfDir() { if (self::$selfDir === null) { self::$selfDir = strtr(__DIR__, '\\', '/'); } return self::$selfDir; } /** * @return array[] * @psalm-return list}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); $copiedLocalDir = false; if (self::$canGetVendors) { $selfDir = self::getSelfDir(); foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { $vendorDir = strtr($vendorDir, '\\', '/'); if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require $vendorDir.'/composer/installed.php'; self::$installedByVendor[$vendorDir] = $required; $installed[] = $required; if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { self::$installed = $required; self::$installedIsLocalDir = true; } } if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { $copiedLocalDir = true; } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require __DIR__ . '/installed.php'; self::$installed = $required; } else { self::$installed = array(); } } if (self::$installed !== array() && !$copiedLocalDir) { $installed[] = self::$installed; } return $installed; } } array($vendorDir . '/composer/semver/src'), 'AppVersionDetector\\Tests\\' => array($baseDir . '/tests'), 'AppVersionDetector\\' => array($baseDir . '/src'), ); Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. array ( 'Composer\\Semver\\' => 16, ), 'A' => array ( 'AppVersionDetector\\Tests\\' => 25, 'AppVersionDetector\\' => 19, ), ); public static $prefixDirsPsr4 = array ( 'Composer\\Semver\\' => array ( 0 => __DIR__ . '/..' . '/composer/semver/src', ), 'AppVersionDetector\\Tests\\' => array ( 0 => __DIR__ . '/../..' . '/tests', ), 'AppVersionDetector\\' => array ( 0 => __DIR__ . '/../..' . '/src', ), ); public static $classMap = array ( 'AppVersionDetector\\Application\\AVDCliParse' => __DIR__ . '/../..' . '/src/Application/AVDCliParse.php', 'AppVersionDetector\\Application\\AVDConfig' => __DIR__ . '/../..' . '/src/Application/AVDConfig.php', 'AppVersionDetector\\Application\\AppException' => __DIR__ . '/../..' . '/src/Application/AppException.php', 'AppVersionDetector\\Application\\CliParse' => __DIR__ . '/../..' . '/src/Application/CliParse.php', 'AppVersionDetector\\Application\\Config' => __DIR__ . '/../..' . '/src/Application/Config.php', 'AppVersionDetector\\Application\\ConfigParamException' => __DIR__ . '/../..' . '/src/Application/ConfigParamException.php', 'AppVersionDetector\\Application\\DetectorBuilder' => __DIR__ . '/../..' . '/src/Application/DetectorBuilder.php', 'AppVersionDetector\\Application\\Directory' => __DIR__ . '/../..' . '/src/Application/Directory.php', 'AppVersionDetector\\Application\\Error\\ErrorHandler' => __DIR__ . '/../..' . '/src/Application/Error/ErrorHandler.php', 'AppVersionDetector\\Application\\Factory' => __DIR__ . '/../..' . '/src/Application/Factory.php', 'AppVersionDetector\\Application\\FileOwners' => __DIR__ . '/../..' . '/src/Application/FileOwners.php', 'AppVersionDetector\\Application\\Helper' => __DIR__ . '/../..' . '/src/Application/Helper.php', 'AppVersionDetector\\Application\\Migraitor' => __DIR__ . '/../..' . '/src/Application/Migraitor.php', 'AppVersionDetector\\Application\\Profiler' => __DIR__ . '/../..' . '/src/Application/Profiler.php', 'AppVersionDetector\\Application\\Stats' => __DIR__ . '/../..' . '/src/Application/Stats.php', 'AppVersionDetector\\Core\\AbstractEvent' => __DIR__ . '/../..' . '/src/Core/AbstractEvent.php', 'AppVersionDetector\\Core\\ListenerInterface' => __DIR__ . '/../..' . '/src/Core/ListenerInterface.php', 'AppVersionDetector\\Core\\MediatorInterface' => __DIR__ . '/../..' . '/src/Core/MediatorInterface.php', 'AppVersionDetector\\Core\\OutdatedDetection\\ComparisonStrategyInterface' => __DIR__ . '/../..' . '/src/Core/OutdatedDetection/ComparisonStrategyInterface.php', 'AppVersionDetector\\Core\\OutdatedDetection\\GenericComparisonStrategy' => __DIR__ . '/../..' . '/src/Core/OutdatedDetection/GenericComparisonStrategy.php', 'AppVersionDetector\\Core\\OutdatedDetection\\OutdatedChecker' => __DIR__ . '/../..' . '/src/Core/OutdatedDetection/OutdatedChecker.php', 'AppVersionDetector\\Core\\OutdatedDetection\\OutdatedEvent' => __DIR__ . '/../..' . '/src/Core/OutdatedDetection/OutdatedEvent.php', 'AppVersionDetector\\Core\\OutdatedDetection\\VersionDbInterface' => __DIR__ . '/../..' . '/src/Core/OutdatedDetection/VersionDbInterface.php', 'AppVersionDetector\\Core\\PublisherInterface' => __DIR__ . '/../..' . '/src/Core/PublisherInterface.php', 'AppVersionDetector\\Core\\VersionDetection\\AbstractCompositeDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/AbstractCompositeDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\AbstractDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/AbstractDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\DependencyCollectionDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/DependencyCollectionDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\DetectionEvent' => __DIR__ . '/../..' . '/src/Core/VersionDetection/DetectionEvent.php', 'AppVersionDetector\\Core\\VersionDetection\\DetectorInterface' => __DIR__ . '/../..' . '/src/Core/VersionDetection/DetectorInterface.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\BitrixCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/BitrixCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\CommonScriptDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/CommonScriptDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DrupalCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/DrupalCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DrupalPluginDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/DrupalPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DummyDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/DummyDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\IPBCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/IPBCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\JoomlaCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/JoomlaCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\JoomlaPluginDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/JoomlaPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\MagentoCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/MagentoCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\ModxCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/ModxCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\OpenCartCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/OpenCartCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\OsCommerceCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/OsCommerceCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\PHPBB3CoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/PHPBB3CoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpComponentDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/WpComponentDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpCoreDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/WpCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpPluginDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/WpPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpThemeDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/Detector/WpThemeDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\PlainCollectionDetector' => __DIR__ . '/../..' . '/src/Core/VersionDetection/PlainCollectionDetector.php', 'AppVersionDetector\\Event\\EventManager' => __DIR__ . '/../..' . '/src/Event/EventManager.php', 'AppVersionDetector\\Event\\ScanningDirEndedEvent' => __DIR__ . '/../..' . '/src/Event/ScanningDirEndedEvent.php', 'AppVersionDetector\\Event\\ScanningDirStartedEvent' => __DIR__ . '/../..' . '/src/Event/ScanningDirStartedEvent.php', 'AppVersionDetector\\Event\\ScanningEndedEvent' => __DIR__ . '/../..' . '/src/Event/ScanningEndedEvent.php', 'AppVersionDetector\\Event\\ScanningStartedEvent' => __DIR__ . '/../..' . '/src/Event/ScanningStartedEvent.php', 'AppVersionDetector\\Report\\AbstractFileBasedReport' => __DIR__ . '/../..' . '/src/Report/AbstractFileBasedReport.php', 'AppVersionDetector\\Report\\AbstractReport' => __DIR__ . '/../..' . '/src/Report/AbstractReport.php', 'AppVersionDetector\\Report\\Includes\\RemoteStatsRequest' => __DIR__ . '/../..' . '/src/Report/Includes/RemoteStatsRequest.php', 'AppVersionDetector\\Report\\JsonReport' => __DIR__ . '/../..' . '/src/Report/JsonReport.php', 'AppVersionDetector\\Report\\RemoteStatsReport' => __DIR__ . '/../..' . '/src/Report/RemoteStatsReport.php', 'AppVersionDetector\\Report\\SqliteDbReport' => __DIR__ . '/../..' . '/src/Report/SqliteDbReport.php', 'AppVersionDetector\\Report\\TextReport' => __DIR__ . '/../..' . '/src/Report/TextReport.php', 'AppVersionDetector\\Scanner' => __DIR__ . '/../..' . '/src/Scanner.php', 'AppVersionDetector\\Storage\\ActualVersionsDb' => __DIR__ . '/../..' . '/src/Storage/ActualVersionsDb.php', 'AppVersionDetector\\Storage\\ActualVersionsSQLiteDb' => __DIR__ . '/../..' . '/src/Storage/ActualVersionsSQLiteDb.php', 'AppVersionDetector\\Storage\\SqliteDbReportConnection' => __DIR__ . '/../..' . '/src/Storage/SqliteDbReportConnection.php', 'AppVersionDetector\\Tests\\Util\\ArchiveTestTrait' => __DIR__ . '/../..' . '/tests/Util/ArchiveTestTrait.php', 'AppVersionDetector\\Tests\\Util\\IntegrationTestTrait' => __DIR__ . '/../..' . '/tests/Util/IntegrationTestTrait.php', 'AppVersionDetector\\Tests\\Util\\SqliteTestTrait' => __DIR__ . '/../..' . '/tests/Util/SqliteTestTrait.php', 'AppVersionDetector\\Tests\\Util\\TestCase' => __DIR__ . '/../..' . '/tests/Util/TestCase.php', 'AppVersionDetector\\Tests\\unit\\Application\\DirectoryTest' => __DIR__ . '/../..' . '/tests/unit/Application/DirectoryTest.php', 'AppVersionDetector\\Tests\\unit\\Core\\OutdatedDetection\\GenericComparisonStrategyTest' => __DIR__ . '/../..' . '/tests/unit/Core/OutdatedDetection/GenericComparisonStrategyTest.php', 'AppVersionDetector\\Tests\\unit\\Core\\VersionDetection\\Detector\\AbstractWpComponentDetector' => __DIR__ . '/../..' . '/tests/unit/Core/VersionDetection/Detector/AbstractWpComponentDetector.php', 'AppVersionDetector\\Tests\\unit\\Core\\VersionDetection\\Detector\\WpPluginsDetectorDirLimitTest' => __DIR__ . '/../..' . '/tests/unit/Core/VersionDetection/Detector/WpPluginsDetectorDirLimitTest.php', 'AppVersionDetector\\Tests\\unit\\Storage\\ActualVersionsDbTest' => __DIR__ . '/../..' . '/tests/unit/Storage/ActualVersionsDbTest.php', 'AppVersionDetector\\Tests\\unit\\Storage\\ActualVersionsSQLiteDbTest' => __DIR__ . '/../..' . '/tests/unit/Storage/ActualVersionsSQLiteDbTest.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\Semver\\Comparator' => __DIR__ . '/..' . '/composer/semver/src/Comparator.php', 'Composer\\Semver\\Constraint\\AbstractConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/AbstractConstraint.php', 'Composer\\Semver\\Constraint\\Constraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/Constraint.php', 'Composer\\Semver\\Constraint\\ConstraintInterface' => __DIR__ . '/..' . '/composer/semver/src/Constraint/ConstraintInterface.php', 'Composer\\Semver\\Constraint\\EmptyConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/EmptyConstraint.php', 'Composer\\Semver\\Constraint\\MultiConstraint' => __DIR__ . '/..' . '/composer/semver/src/Constraint/MultiConstraint.php', 'Composer\\Semver\\Semver' => __DIR__ . '/..' . '/composer/semver/src/Semver.php', 'Composer\\Semver\\VersionParser' => __DIR__ . '/..' . '/composer/semver/src/VersionParser.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit431d7b3dd0d6aa4f493c20cfbb8de03f::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit431d7b3dd0d6aa4f493c20cfbb8de03f::$prefixDirsPsr4; $loader->classMap = ComposerStaticInit431d7b3dd0d6aa4f493c20cfbb8de03f::$classMap; }, null, ClassLoader::class); } } array( 'name' => 'cloudlinux/app-version-detector', 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => null, 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false, ), 'versions' => array( 'cloudlinux/app-version-detector' => array( 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', 'reference' => null, 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), 'composer/semver' => array( 'pretty_version' => '1.7.2', 'version' => '1.7.2.0', 'reference' => '647490bbcaf7fc4891c58f47b825eb99d19c377a', 'type' => 'library', 'install_path' => __DIR__ . '/./semver', 'aliases' => array(), 'dev_requirement' => false, ), ), ); $baseDir . '/src/Application/AVDCliParse.php', 'AppVersionDetector\\Application\\AVDConfig' => $baseDir . '/src/Application/AVDConfig.php', 'AppVersionDetector\\Application\\AppException' => $baseDir . '/src/Application/AppException.php', 'AppVersionDetector\\Application\\CliParse' => $baseDir . '/src/Application/CliParse.php', 'AppVersionDetector\\Application\\Config' => $baseDir . '/src/Application/Config.php', 'AppVersionDetector\\Application\\ConfigParamException' => $baseDir . '/src/Application/ConfigParamException.php', 'AppVersionDetector\\Application\\DetectorBuilder' => $baseDir . '/src/Application/DetectorBuilder.php', 'AppVersionDetector\\Application\\Directory' => $baseDir . '/src/Application/Directory.php', 'AppVersionDetector\\Application\\Error\\ErrorHandler' => $baseDir . '/src/Application/Error/ErrorHandler.php', 'AppVersionDetector\\Application\\Factory' => $baseDir . '/src/Application/Factory.php', 'AppVersionDetector\\Application\\FileOwners' => $baseDir . '/src/Application/FileOwners.php', 'AppVersionDetector\\Application\\Helper' => $baseDir . '/src/Application/Helper.php', 'AppVersionDetector\\Application\\Migraitor' => $baseDir . '/src/Application/Migraitor.php', 'AppVersionDetector\\Application\\Profiler' => $baseDir . '/src/Application/Profiler.php', 'AppVersionDetector\\Application\\Stats' => $baseDir . '/src/Application/Stats.php', 'AppVersionDetector\\Core\\AbstractEvent' => $baseDir . '/src/Core/AbstractEvent.php', 'AppVersionDetector\\Core\\ListenerInterface' => $baseDir . '/src/Core/ListenerInterface.php', 'AppVersionDetector\\Core\\MediatorInterface' => $baseDir . '/src/Core/MediatorInterface.php', 'AppVersionDetector\\Core\\OutdatedDetection\\ComparisonStrategyInterface' => $baseDir . '/src/Core/OutdatedDetection/ComparisonStrategyInterface.php', 'AppVersionDetector\\Core\\OutdatedDetection\\GenericComparisonStrategy' => $baseDir . '/src/Core/OutdatedDetection/GenericComparisonStrategy.php', 'AppVersionDetector\\Core\\OutdatedDetection\\OutdatedChecker' => $baseDir . '/src/Core/OutdatedDetection/OutdatedChecker.php', 'AppVersionDetector\\Core\\OutdatedDetection\\OutdatedEvent' => $baseDir . '/src/Core/OutdatedDetection/OutdatedEvent.php', 'AppVersionDetector\\Core\\OutdatedDetection\\VersionDbInterface' => $baseDir . '/src/Core/OutdatedDetection/VersionDbInterface.php', 'AppVersionDetector\\Core\\PublisherInterface' => $baseDir . '/src/Core/PublisherInterface.php', 'AppVersionDetector\\Core\\VersionDetection\\AbstractCompositeDetector' => $baseDir . '/src/Core/VersionDetection/AbstractCompositeDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\AbstractDetector' => $baseDir . '/src/Core/VersionDetection/AbstractDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\DependencyCollectionDetector' => $baseDir . '/src/Core/VersionDetection/DependencyCollectionDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\DetectionEvent' => $baseDir . '/src/Core/VersionDetection/DetectionEvent.php', 'AppVersionDetector\\Core\\VersionDetection\\DetectorInterface' => $baseDir . '/src/Core/VersionDetection/DetectorInterface.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\BitrixCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/BitrixCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\CommonScriptDetector' => $baseDir . '/src/Core/VersionDetection/Detector/CommonScriptDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DrupalCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/DrupalCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DrupalPluginDetector' => $baseDir . '/src/Core/VersionDetection/Detector/DrupalPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\DummyDetector' => $baseDir . '/src/Core/VersionDetection/Detector/DummyDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\IPBCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/IPBCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\JoomlaCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/JoomlaCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\JoomlaPluginDetector' => $baseDir . '/src/Core/VersionDetection/Detector/JoomlaPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\MagentoCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/MagentoCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\ModxCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/ModxCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\OpenCartCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/OpenCartCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\OsCommerceCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/OsCommerceCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\PHPBB3CoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/PHPBB3CoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpComponentDetector' => $baseDir . '/src/Core/VersionDetection/Detector/WpComponentDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpCoreDetector' => $baseDir . '/src/Core/VersionDetection/Detector/WpCoreDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpPluginDetector' => $baseDir . '/src/Core/VersionDetection/Detector/WpPluginDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\Detector\\WpThemeDetector' => $baseDir . '/src/Core/VersionDetection/Detector/WpThemeDetector.php', 'AppVersionDetector\\Core\\VersionDetection\\PlainCollectionDetector' => $baseDir . '/src/Core/VersionDetection/PlainCollectionDetector.php', 'AppVersionDetector\\Event\\EventManager' => $baseDir . '/src/Event/EventManager.php', 'AppVersionDetector\\Event\\ScanningDirEndedEvent' => $baseDir . '/src/Event/ScanningDirEndedEvent.php', 'AppVersionDetector\\Event\\ScanningDirStartedEvent' => $baseDir . '/src/Event/ScanningDirStartedEvent.php', 'AppVersionDetector\\Event\\ScanningEndedEvent' => $baseDir . '/src/Event/ScanningEndedEvent.php', 'AppVersionDetector\\Event\\ScanningStartedEvent' => $baseDir . '/src/Event/ScanningStartedEvent.php', 'AppVersionDetector\\Report\\AbstractFileBasedReport' => $baseDir . '/src/Report/AbstractFileBasedReport.php', 'AppVersionDetector\\Report\\AbstractReport' => $baseDir . '/src/Report/AbstractReport.php', 'AppVersionDetector\\Report\\Includes\\RemoteStatsRequest' => $baseDir . '/src/Report/Includes/RemoteStatsRequest.php', 'AppVersionDetector\\Report\\JsonReport' => $baseDir . '/src/Report/JsonReport.php', 'AppVersionDetector\\Report\\RemoteStatsReport' => $baseDir . '/src/Report/RemoteStatsReport.php', 'AppVersionDetector\\Report\\SqliteDbReport' => $baseDir . '/src/Report/SqliteDbReport.php', 'AppVersionDetector\\Report\\TextReport' => $baseDir . '/src/Report/TextReport.php', 'AppVersionDetector\\Scanner' => $baseDir . '/src/Scanner.php', 'AppVersionDetector\\Storage\\ActualVersionsDb' => $baseDir . '/src/Storage/ActualVersionsDb.php', 'AppVersionDetector\\Storage\\ActualVersionsSQLiteDb' => $baseDir . '/src/Storage/ActualVersionsSQLiteDb.php', 'AppVersionDetector\\Storage\\SqliteDbReportConnection' => $baseDir . '/src/Storage/SqliteDbReportConnection.php', 'AppVersionDetector\\Tests\\Util\\ArchiveTestTrait' => $baseDir . '/tests/Util/ArchiveTestTrait.php', 'AppVersionDetector\\Tests\\Util\\IntegrationTestTrait' => $baseDir . '/tests/Util/IntegrationTestTrait.php', 'AppVersionDetector\\Tests\\Util\\SqliteTestTrait' => $baseDir . '/tests/Util/SqliteTestTrait.php', 'AppVersionDetector\\Tests\\Util\\TestCase' => $baseDir . '/tests/Util/TestCase.php', 'AppVersionDetector\\Tests\\unit\\Application\\DirectoryTest' => $baseDir . '/tests/unit/Application/DirectoryTest.php', 'AppVersionDetector\\Tests\\unit\\Core\\OutdatedDetection\\GenericComparisonStrategyTest' => $baseDir . '/tests/unit/Core/OutdatedDetection/GenericComparisonStrategyTest.php', 'AppVersionDetector\\Tests\\unit\\Core\\VersionDetection\\Detector\\AbstractWpComponentDetector' => $baseDir . '/tests/unit/Core/VersionDetection/Detector/AbstractWpComponentDetector.php', 'AppVersionDetector\\Tests\\unit\\Core\\VersionDetection\\Detector\\WpPluginsDetectorDirLimitTest' => $baseDir . '/tests/unit/Core/VersionDetection/Detector/WpPluginsDetectorDirLimitTest.php', 'AppVersionDetector\\Tests\\unit\\Storage\\ActualVersionsDbTest' => $baseDir . '/tests/unit/Storage/ActualVersionsDbTest.php', 'AppVersionDetector\\Tests\\unit\\Storage\\ActualVersionsSQLiteDbTest' => $baseDir . '/tests/unit/Storage/ActualVersionsSQLiteDbTest.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Composer\\Semver\\Comparator' => $vendorDir . '/composer/semver/src/Comparator.php', 'Composer\\Semver\\Constraint\\AbstractConstraint' => $vendorDir . '/composer/semver/src/Constraint/AbstractConstraint.php', 'Composer\\Semver\\Constraint\\Constraint' => $vendorDir . '/composer/semver/src/Constraint/Constraint.php', 'Composer\\Semver\\Constraint\\ConstraintInterface' => $vendorDir . '/composer/semver/src/Constraint/ConstraintInterface.php', 'Composer\\Semver\\Constraint\\EmptyConstraint' => $vendorDir . '/composer/semver/src/Constraint/EmptyConstraint.php', 'Composer\\Semver\\Constraint\\MultiConstraint' => $vendorDir . '/composer/semver/src/Constraint/MultiConstraint.php', 'Composer\\Semver\\Semver' => $vendorDir . '/composer/semver/src/Semver.php', 'Composer\\Semver\\VersionParser' => $vendorDir . '/composer/semver/src/VersionParser.php', ); * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var \Closure(string):void */ private static $includeFile; /** @var string|null */ private $vendorDir; // PSR-4 /** * @var array> */ private $prefixLengthsPsr4 = array(); /** * @var array> */ private $prefixDirsPsr4 = array(); /** * @var list */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * List of PSR-0 prefixes * * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) * * @var array>> */ private $prefixesPsr0 = array(); /** * @var list */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var array */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var array */ private $missingClasses = array(); /** @var string|null */ private $apcuPrefix; /** * @var array */ private static $registeredLoaders = array(); /** * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; self::initializeIncludeClosure(); } /** * @return array> */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return list */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return list */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return array Array of classname => path */ public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param list|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param list|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param list|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { $includeFile = self::$includeFile; $includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders keyed by their corresponding vendor directories. * * @return array */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } /** * @return void */ private static function initializeIncludeClosure() { if (self::$includeFile !== null) { return; } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void */ self::$includeFile = \Closure::bind(static function($file) { include $file; }, null, null); } } setClassMapAuthoritative(true); $loader->register(true); return $loader; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Constraint\Constraint; /** * Version parser. * * @author Jordi Boggiano */ class VersionParser { /** * Regex to match pre-release data (sort of). * * Due to backwards compatibility: * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted. * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier. * - Numerical-only pre-release identifiers are not supported, see tests. * * |--------------| * [major].[minor].[patch] -[pre-release] +[build-metadata] * * @var string */ private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'; /** @var string */ private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev'; /** * Returns the stability of a version. * * @param string $version * * @return string */ public static function parseStability($version) { $version = preg_replace('{#.+$}i', '', $version); if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) { return 'dev'; } preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match); if (!empty($match[3])) { return 'dev'; } if (!empty($match[1])) { if ('beta' === $match[1] || 'b' === $match[1]) { return 'beta'; } if ('alpha' === $match[1] || 'a' === $match[1]) { return 'alpha'; } if ('rc' === $match[1]) { return 'RC'; } } return 'stable'; } /** * @param string $stability * * @return string */ public static function normalizeStability($stability) { $stability = strtolower($stability); return $stability === 'rc' ? 'RC' : $stability; } /** * Normalizes a version string to be able to perform comparisons on it. * * @param string $version * @param string $fullVersion optional complete version string to give more context * * @throws \UnexpectedValueException * * @return string */ public function normalize($version, $fullVersion = null) { $version = trim($version); $origVersion = $version; if (null === $fullVersion) { $fullVersion = $version; } // strip off aliasing if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) { $version = $match[1]; } // strip off stability flag if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) { $version = substr($version, 0, strlen($version) - strlen($match[0])); } // match master-like branches if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { return '9999999-dev'; } // if requirement is branch-like, use full name if (stripos($version, 'dev-') === 0) { return 'dev-' . substr($version, 4); } // strip off build metadata if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) { $version = $match[1]; } // match classical versioning if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) { $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0'); $index = 5; // match date(time) based versioning } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { $version = preg_replace('{\D}', '.', $matches[1]); $index = 2; } // add version modifiers if a version was matched if (isset($index)) { if (!empty($matches[$index])) { if ('stable' === $matches[$index]) { return $version; } $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : ''); } if (!empty($matches[$index + 2])) { $version .= '-dev'; } return $version; } // match dev branches if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { try { $normalized = $this->normalizeBranch($match[1]); // a branch ending with -dev is only valid if it is numeric // if it gets prefixed with dev- it means the branch name should // have had a dev- prefix already when passed to normalize if (strpos($normalized, 'dev-') === false) { return $normalized; } } catch (\Exception $e) { } } $extraMessage = ''; if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) { $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; } elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) { $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; } throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage); } /** * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison. * * @param string $branch Branch name (e.g. 2.1.x-dev) * * @return string|false Numeric prefix if present (e.g. 2.1.) or false */ public function parseNumericAliasPrefix($branch) { if (preg_match('{^(?P(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) { return $matches['version'] . '.'; } return false; } /** * Normalizes a branch name to be able to perform comparisons on it. * * @param string $name * * @return string */ public function normalizeBranch($name) { $name = trim($name); if (in_array($name, array('master', 'trunk', 'default'))) { return $this->normalize($name); } if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) { $version = ''; for ($i = 1; $i < 5; ++$i) { $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; } return str_replace('x', '9999999', $version) . '-dev'; } return 'dev-' . $name; } /** * Parses a constraint string into MultiConstraint and/or Constraint objects. * * @param string $constraints * * @return ConstraintInterface */ public function parseConstraints($constraints) { $prettyConstraint = $constraints; $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints)); $orGroups = array(); foreach ($orConstraints as $constraints) { $andConstraints = preg_split('{(?< ,]) *(? 1) { $constraintObjects = array(); foreach ($andConstraints as $constraint) { foreach ($this->parseConstraint($constraint) as $parsedConstraint) { $constraintObjects[] = $parsedConstraint; } } } else { $constraintObjects = $this->parseConstraint($andConstraints[0]); } if (1 === count($constraintObjects)) { $constraint = $constraintObjects[0]; } else { $constraint = new MultiConstraint($constraintObjects); } $orGroups[] = $constraint; } if (1 === count($orGroups)) { $constraint = $orGroups[0]; } elseif (2 === count($orGroups) // parse the two OR groups and if they are contiguous we collapse // them into one constraint && $orGroups[0] instanceof MultiConstraint && $orGroups[1] instanceof MultiConstraint && 2 === count($orGroups[0]->getConstraints()) && 2 === count($orGroups[1]->getConstraints()) && ($a = (string) $orGroups[0]) && strpos($a, '[>=') === 0 && (false !== ($posA = strpos($a, '<', 4))) && ($b = (string) $orGroups[1]) && strpos($b, '[>=') === 0 && (false !== ($posB = strpos($b, '<', 4))) && substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5) ) { $constraint = new MultiConstraint(array( new Constraint('>=', substr($a, 4, $posA - 5)), new Constraint('<', substr($b, $posB + 2, -1)), )); } else { $constraint = new MultiConstraint($orGroups, false); } $constraint->setPrettyString($prettyConstraint); return $constraint; } /** * @param string $constraint * * @throws \UnexpectedValueException * * @return array */ private function parseConstraint($constraint) { // strip off aliasing if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) { $constraint = $match[1]; } // strip @stability flags, and keep it for later use if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) { $constraint = '' !== $match[1] ? $match[1] : '*'; if ($match[2] !== 'stable') { $stabilityModifier = $match[2]; } } // get rid of #refs as those are used by composer only if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) { $constraint = $match[1]; } if (preg_match('{^v?[xX*](\.[xX*])*$}i', $constraint)) { return array(new EmptyConstraint()); } $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?'; // Tilde Range // // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous // version, to ensure that unstable instances of the current version are allowed. However, if a stability // suffix is added to the constraint, then a >= match on the current version is used instead. if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) { if (strpos($constraint, '~>') === 0) { throw new \UnexpectedValueException( 'Could not parse version constraint ' . $constraint . ': ' . 'Invalid operator "~>", you probably meant to use the "~" operator' ); } // Work out which position in the version we are operating at if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) { $position = 4; } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { $position = 3; } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { $position = 2; } else { $position = 1; } // when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above if (!empty($matches[8])) { $position++; } // Calculate the stability suffix $stabilitySuffix = ''; if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { $stabilitySuffix .= '-dev'; } $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); $lowerBound = new Constraint('>=', $lowVersion); // For upper bound, we increment the position of one more significance, // but highPosition = 0 would be illegal $highPosition = max(1, $position - 1); $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); return array( $lowerBound, $upperBound, ); } // Caret Range // // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for // versions 0.X >=0.1.0, and no updates for versions 0.0.X if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) { // Work out which position in the version we are operating at if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) { $position = 1; } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) { $position = 2; } else { $position = 3; } // Calculate the stability suffix $stabilitySuffix = ''; if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { $stabilitySuffix .= '-dev'; } $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); $lowerBound = new Constraint('>=', $lowVersion); // For upper bound, we increment the position of one more significance, // but highPosition = 0 would be illegal $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); return array( $lowerBound, $upperBound, ); } // X Range // // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple. // A partial version range is treated as an X-Range, so the special character is in fact optional. if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) { if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { $position = 3; } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { $position = 2; } else { $position = 1; } $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev'; $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; if ($lowVersion === '0.0.0.0-dev') { return array(new Constraint('<', $highVersion)); } return array( new Constraint('>=', $lowVersion), new Constraint('<', $highVersion), ); } // Hyphen Range // // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range, // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but // nothing that would be greater than the provided tuple parts. if (preg_match('{^(?P' . $versionRegex . ') +- +(?P' . $versionRegex . ')($)}i', $constraint, $matches)) { // Calculate the stability suffix $lowStabilitySuffix = ''; if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) { $lowStabilitySuffix = '-dev'; } $lowVersion = $this->normalize($matches['from']); $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix); $empty = function ($x) { return ($x === 0 || $x === '0') ? false : empty($x); }; if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) { $highVersion = $this->normalize($matches['to']); $upperBound = new Constraint('<=', $highVersion); } else { $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]); // validate to version $this->normalize($matches['to']); $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); } return array( $lowerBound, $upperBound, ); } // Basic Comparators if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { try { try { $version = $this->normalize($matches[2]); } catch (\UnexpectedValueException $e) { // recover from an invalid constraint like foobar-dev which should be dev-foobar // except if the constraint uses a known operator, in which case it must be a parse error if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) { $version = $this->normalize('dev-'.substr($matches[2], 0, -4)); } else { throw $e; } } $op = $matches[1] ?: '='; if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') { $version .= '-' . $stabilityModifier; } elseif ('<' === $op || '>=' === $op) { if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { if (strpos($matches[2], 'dev-') !== 0) { $version .= '-dev'; } } } return array(new Constraint($matches[1] ?: '=', $version)); } catch (\Exception $e) { } } $message = 'Could not parse version constraint ' . $constraint; if (isset($e)) { $message .= ': ' . $e->getMessage(); } throw new \UnexpectedValueException($message); } /** * Increment, decrement, or simply pad a version number. * * Support function for {@link parseConstraint()} * * @param array $matches Array with version parts in array indexes 1,2,3,4 * @param int $position 1,2,3,4 - which segment of the version to increment/decrement * @param int $increment * @param string $pad The string to pad version parts after $position * * @return string|null The new version */ private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0') { for ($i = 4; $i > 0; --$i) { if ($i > $position) { $matches[$i] = $pad; } elseif ($i === $position && $increment) { $matches[$i] += $increment; // If $matches[$i] was 0, carry the decrement if ($matches[$i] < 0) { $matches[$i] = $pad; --$position; // Return null on a carry overflow if ($i === 1) { return null; } } } } return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; } /** * Expand shorthand stability string to long version. * * @param string $stability * * @return string */ private function expandStability($stability) { $stability = strtolower($stability); switch ($stability) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'p': case 'pl': return 'patch'; case 'rc': return 'RC'; default: return $stability; } } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; interface ConstraintInterface { /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider); /** * @return string */ public function getPrettyString(); /** * @return string */ public function __toString(); } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines the absence of a constraint. */ class EmptyConstraint implements ConstraintInterface { /** @var string */ protected $prettyString; /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { return true; } /** * @param string $prettyString */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * @return string */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return (string) $this; } /** * @return string */ public function __toString() { return '[]'; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; trigger_error('The ' . __NAMESPACE__ . '\AbstractConstraint abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED); /** * Base constraint class. */ abstract class AbstractConstraint implements ConstraintInterface { /** @var string */ protected $prettyString; /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { if ($provider instanceof $this) { // see note at bottom of this class declaration return $this->matchSpecific($provider); } // turn matching around to find a match return $provider->matches($this); } /** * @param string $prettyString */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * @return string */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return $this->__toString(); } // implementations must implement a method of this format: // not declared abstract here because type hinting violates parameter coherence (TODO right word?) // public function matchSpecific( $provider); } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines a conjunctive or disjunctive set of constraints. */ class MultiConstraint implements ConstraintInterface { /** @var ConstraintInterface[] */ protected $constraints; /** @var string|null */ protected $prettyString; /** @var bool */ protected $conjunctive; /** * @param ConstraintInterface[] $constraints A set of constraints * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive */ public function __construct(array $constraints, $conjunctive = true) { $this->constraints = $constraints; $this->conjunctive = $conjunctive; } /** * @return ConstraintInterface[] */ public function getConstraints() { return $this->constraints; } /** * @return bool */ public function isConjunctive() { return $this->conjunctive; } /** * @return bool */ public function isDisjunctive() { return !$this->conjunctive; } /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { if (false === $this->conjunctive) { foreach ($this->constraints as $constraint) { if ($constraint->matches($provider)) { return true; } } return false; } foreach ($this->constraints as $constraint) { if (!$constraint->matches($provider)) { return false; } } return true; } /** * @param string|null $prettyString */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * @return string */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return (string) $this; } /** * @return string */ public function __toString() { $constraints = array(); foreach ($this->constraints as $constraint) { $constraints[] = (string) $constraint; } return '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']'; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines a constraint. */ class Constraint implements ConstraintInterface { /* operator integer values */ const OP_EQ = 0; const OP_LT = 1; const OP_LE = 2; const OP_GT = 3; const OP_GE = 4; const OP_NE = 5; /** * Operator to integer translation table. * * @var array */ private static $transOpStr = array( '=' => self::OP_EQ, '==' => self::OP_EQ, '<' => self::OP_LT, '<=' => self::OP_LE, '>' => self::OP_GT, '>=' => self::OP_GE, '<>' => self::OP_NE, '!=' => self::OP_NE, ); /** * Integer to operator translation table. * * @var array */ private static $transOpInt = array( self::OP_EQ => '==', self::OP_LT => '<', self::OP_LE => '<=', self::OP_GT => '>', self::OP_GE => '>=', self::OP_NE => '!=', ); /** @var int */ protected $operator; /** @var string */ protected $version; /** @var string */ protected $prettyString; /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { if ($provider instanceof $this) { return $this->matchSpecific($provider); } // turn matching around to find a match return $provider->matches($this); } /** * @param string $prettyString */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * @return string */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return $this->__toString(); } /** * Get all supported comparison operators. * * @return array */ public static function getSupportedOperators() { return array_keys(self::$transOpStr); } /** * Sets operator and version to compare with. * * @param string $operator * @param string $version * * @throws \InvalidArgumentException if invalid operator is given. */ public function __construct($operator, $version) { if (!isset(self::$transOpStr[$operator])) { throw new \InvalidArgumentException(sprintf( 'Invalid operator "%s" given, expected one of: %s', $operator, implode(', ', self::getSupportedOperators()) )); } $this->operator = self::$transOpStr[$operator]; $this->version = $version; } /** * @param string $a * @param string $b * @param string $operator * @param bool $compareBranches * * @throws \InvalidArgumentException if invalid operator is given. * * @return bool */ public function versionCompare($a, $b, $operator, $compareBranches = false) { if (!isset(self::$transOpStr[$operator])) { throw new \InvalidArgumentException(sprintf( 'Invalid operator "%s" given, expected one of: %s', $operator, implode(', ', self::getSupportedOperators()) )); } $aIsBranch = 'dev-' === substr($a, 0, 4); $bIsBranch = 'dev-' === substr($b, 0, 4); if ($aIsBranch && $bIsBranch) { return $operator === '==' && $a === $b; } // when branches are not comparable, we make sure dev branches never match anything if (!$compareBranches && ($aIsBranch || $bIsBranch)) { return false; } return version_compare($a, $b, $operator); } /** * @param Constraint $provider * @param bool $compareBranches * * @return bool */ public function matchSpecific(Constraint $provider, $compareBranches = false) { $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]); $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]); $isEqualOp = self::OP_EQ === $this->operator; $isNonEqualOp = self::OP_NE === $this->operator; $isProviderEqualOp = self::OP_EQ === $provider->operator; $isProviderNonEqualOp = self::OP_NE === $provider->operator; // '!=' operator is match when other operator is not '==' operator or version is not match // these kinds of comparisons always have a solution if ($isNonEqualOp || $isProviderNonEqualOp) { return (!$isEqualOp && !$isProviderEqualOp) || $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); } // an example for the condition is <= 2.0 & < 1.0 // these kinds of comparisons always have a solution if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) { return true; } if ($this->versionCompare($provider->version, $this->version, self::$transOpInt[$this->operator], $compareBranches)) { // special case, e.g. require >= 1.0 and provide < 1.0 // 1.0 >= 1.0 but 1.0 is outside of the provided interval return !($provider->version === $this->version && self::$transOpInt[$provider->operator] === $providerNoEqualOp && self::$transOpInt[$this->operator] !== $noEqualOp); } return false; } /** * @return string */ public function __toString() { return self::$transOpInt[$this->operator] . ' ' . $this->version; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; class Semver { const SORT_ASC = 1; const SORT_DESC = -1; /** @var VersionParser */ private static $versionParser; /** * Determine if given version satisfies given constraints. * * @param string $version * @param string $constraints * * @return bool */ public static function satisfies($version, $constraints) { if (null === self::$versionParser) { self::$versionParser = new VersionParser(); } $versionParser = self::$versionParser; $provider = new Constraint('==', $versionParser->normalize($version)); $parsedConstraints = $versionParser->parseConstraints($constraints); return $parsedConstraints->matches($provider); } /** * Return all versions that satisfy given constraints. * * @param array $versions * @param string $constraints * * @return array */ public static function satisfiedBy(array $versions, $constraints) { $versions = array_filter($versions, function ($version) use ($constraints) { return Semver::satisfies($version, $constraints); }); return array_values($versions); } /** * Sort given array of versions. * * @param array $versions * * @return array */ public static function sort(array $versions) { return self::usort($versions, self::SORT_ASC); } /** * Sort given array of versions in reverse. * * @param array $versions * * @return array */ public static function rsort(array $versions) { return self::usort($versions, self::SORT_DESC); } /** * @param array $versions * @param int $direction * * @return array */ private static function usort(array $versions, $direction) { if (null === self::$versionParser) { self::$versionParser = new VersionParser(); } $versionParser = self::$versionParser; $normalized = array(); // Normalize outside of usort() scope for minor performance increase. // Creates an array of arrays: [[normalized, key], ...] foreach ($versions as $key => $version) { $normalized[] = array($versionParser->normalize($version), $key); } usort($normalized, function (array $left, array $right) use ($direction) { if ($left[0] === $right[0]) { return 0; } if (Comparator::lessThan($left[0], $right[0])) { return -$direction; } return $direction; }); // Recreate input array, using the original indexes which are now in sorted order. $sorted = array(); foreach ($normalized as $item) { $sorted[] = $versions[$item[1]]; } return $sorted; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; class Comparator { /** * Evaluates the expression: $version1 > $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function greaterThan($version1, $version2) { return self::compare($version1, '>', $version2); } /** * Evaluates the expression: $version1 >= $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function greaterThanOrEqualTo($version1, $version2) { return self::compare($version1, '>=', $version2); } /** * Evaluates the expression: $version1 < $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function lessThan($version1, $version2) { return self::compare($version1, '<', $version2); } /** * Evaluates the expression: $version1 <= $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function lessThanOrEqualTo($version1, $version2) { return self::compare($version1, '<=', $version2); } /** * Evaluates the expression: $version1 == $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function equalTo($version1, $version2) { return self::compare($version1, '==', $version2); } /** * Evaluates the expression: $version1 != $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function notEqualTo($version1, $version2) { return self::compare($version1, '!=', $version2); } /** * Evaluates the expression: $version1 $operator $version2. * * @param string $version1 * @param string $operator * @param string $version2 * * @return bool */ public static function compare($version1, $operator, $version2) { $constraint = new Constraint($operator, $version2); return $constraint->matches(new Constraint('==', $version1)); } } Copyright (C) 2015 Composer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer/semver =============== Semver library that offers utilities, version constraint parsing and validation. Originally written as part of [composer/composer](https://github.com/composer/composer), now extracted and made available as a stand-alone library. [![Build Status](https://travis-ci.org/composer/semver.svg?branch=master)](https://travis-ci.org/composer/semver) Installation ------------ Install the latest version with: ```bash $ composer require composer/semver ``` Requirements ------------ * PHP 5.3.2 is required but using the latest version of PHP is highly recommended. Version Comparison ------------------ For details on how versions are compared, refer to the [Versions](https://getcomposer.org/doc/articles/versions.md) article in the documentation section of the [getcomposer.org](https://getcomposer.org) website. Basic usage ----------- ### Comparator The `Composer\Semver\Comparator` class provides the following methods for comparing versions: * greaterThan($v1, $v2) * greaterThanOrEqualTo($v1, $v2) * lessThan($v1, $v2) * lessThanOrEqualTo($v1, $v2) * equalTo($v1, $v2) * notEqualTo($v1, $v2) Each function takes two version strings as arguments and returns a boolean. For example: ```php use Composer\Semver\Comparator; Comparator::greaterThan('1.25.0', '1.24.0'); // 1.25.0 > 1.24.0 ``` ### Semver The `Composer\Semver\Semver` class provides the following methods: * satisfies($version, $constraints) * satisfiedBy(array $versions, $constraint) * sort($versions) * rsort($versions) License ------- composer/semver is licensed under the MIT License, see the LICENSE file for details. # Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ### [1.7.2] 2020-12-03 * Fixed: Allow installing on php 8 ### [1.7.1] 2020-09-27 * Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases * Fixed: normalization of beta0 and such which was dropping the 0 ### [1.7.0] 2020-09-09 * Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0 * Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience ### [1.6.0] 2020-09-08 * Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 1.5.2 * Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package ### [1.5.2] 2020-09-08 * Fixed: handling of some invalid -dev versions which were seen as valid * Fixed: some doctypes ### [1.5.1] 2020-01-13 * Fixed: Parsing of aliased version was not validating the alias to be a valid version ### [1.5.0] 2019-03-19 * Added: some support for date versions (e.g. 201903) in `~` operator * Fixed: support for stabilities in `~` operator was inconsistent ### [1.4.2] 2016-08-30 * Fixed: collapsing of complex constraints lead to buggy constraints ### [1.4.1] 2016-06-02 * Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38). ### [1.4.0] 2016-03-30 * Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35). ### [1.3.0] 2016-02-25 * Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889). * Changed: collapse contiguous constraints when possible. ### [1.2.0] 2015-11-10 * Changed: allow multiple numerical identifiers in 'pre-release' version part. * Changed: add more 'v' prefix support. ### [1.1.0] 2015-11-03 * Changed: dropped redundant `test` namespace. * Changed: minor adjustment in datetime parsing normalization. * Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore. * Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0. * Changed: `Constraint` is now extensible. ### [1.0.0] 2015-09-21 * Break: `VersionConstraint` renamed to `Constraint`. * Break: `SpecificConstraint` renamed to `AbstractConstraint`. * Break: `LinkConstraintInterface` renamed to `ConstraintInterface`. * Break: `VersionParser::parseNameVersionPairs` was removed. * Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now. * Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now. * Changed: Fixed namespace(s) of test files. * Changed: `Comparator::compare` no longer throws `InvalidArgumentException`. * Changed: `Constraint` now throws `InvalidArgumentException`. ### [0.1.0] 2015-07-23 * Added: `Composer\Semver\Comparator`, various methods to compare versions. * Added: various documents such as README.md, LICENSE, etc. * Added: configuration files for Git, Travis, php-cs-fixer, phpunit. * Break: the following namespaces were renamed: - Namespace: `Composer\Package\Version` -> `Composer\Semver` - Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint` - Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver` - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint` * Changed: code style using php-cs-fixer. [1.7.2]: https://github.com/composer/semver/compare/1.7.1...1.7.2 [1.7.1]: https://github.com/composer/semver/compare/1.7.0...1.7.1 [1.7.0]: https://github.com/composer/semver/compare/1.6.0...1.7.0 [1.6.0]: https://github.com/composer/semver/compare/1.5.2...1.6.0 [1.5.2]: https://github.com/composer/semver/compare/1.5.1...1.5.2 [1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1 [1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0 [1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2 [1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1 [1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0 [1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0 [1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0 [0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0 setSender($sender); $this->scannerVersion = $scannerVersion; $this->directory = $directory; } /** * @return string */ public function getScannerVersion(): string { return $this->scannerVersion; } /** * @return string */ public function getDirectory(): Directory { return $this->directory; } /** * @return int */ public function getDirectoryOwner(): int { return $this->directory->getOwner(); } /** * @return string */ public function getDirectoryDomain(): string { return $this->directory->getDomain(); } } setSender($sender); } } setSender($sender); $this->scannerVersion = $scannerVersion; $this->scan_id = $scanId; } /** * @return string */ public function getScannerVersion(): string { return $this->scannerVersion; } /** * @return string */ public function getScanId(): string { return $this->scan_id; } } setSender($sender); $this->scanId = $scanId; } /** * @return string */ public function getScanId(): string { return $this->scanId; } } subscribe(SomeEvent::class, $listener); * * @param string $eventType * @param ListenerInterface $listener */ public function subscribe($eventType, ListenerInterface $listener) { $this->listeners[$eventType][] = $listener; } /** * An event sender publish an event using this method. * * @param AbstractEvent $event */ public function update(AbstractEvent $event) { foreach ($this->getListeners($event) as $listener) { $listener->notify($event); } } /** * Returns a list of listeners for a particular event class. * * @param AbstractEvent $event * @return SplObjectStorage|ListenerInterface[] */ private function getListeners(AbstractEvent $event) { $listeners = new SplObjectStorage(); foreach ($this->listeners as $eventType => $eventListeners) { if ($event instanceof $eventType) { array_walk($eventListeners, function ($event) use ($listeners) { $listeners->attach($event); }); } } return $listeners; } }path = $path; $db = json_decode(file_get_contents($this->path), true, 512, JSON_THROW_ON_ERROR); if (!$this->validate($db)) { throw new AppException('Failed loading DB from "' . $this->path->getPathname() . '": invalid DB format.'); } $this->db = $db; } /** * Checks if a $name key exists. * * @param string $key * @return boolean */ public function hasKey(string $key) { return isset($this->db[$key]); } /** * Returns pairs for a specified key. * * Note: this method should return an ORDERED list of pairs. * * @param string $key * @return array|null list of pairs */ public function getByKey(string $key) { return $this->hasKey($key) ? $this->db[$key]['branches'] : null; } /** * Validate format of the DB. * * @param array $db * @return bool */ private function validate($db) { foreach ($db as $key => $value) { if (!is_array($value)) { return false; } if (!isset($value['branches']) || !is_array($value['branches'])) { return false; } foreach ($value['branches'] as $pair) { if (!is_array($pair) || count($pair) !== 2 || !is_string($pair[0]) || !is_string($pair[1])) { return false; } } } return true; } }open($path); $this->createTablesIfNotExists(); $this->reportStatement = $this->dbh->prepare(/** @lang SQLite */ ' INSERT INTO `report` (scanID, timestamp_started, timestamp_finished, uid, dir, domain) VALUES (:scanID, :timestamp_started, :timestamp_finished, :uid, :dir, :domain) '); $this->appsStatement = $this->dbh->prepare(/** @lang SQLite */ ' INSERT INTO `apps` (report_id, parent_id, title, version, path, filename, app_uid, real_path) VALUES (:report_id, :parent_id, :title, :version, :path, :filename, :app_uid, :real_path) '); } /** * Inserts several rows(report and apps) at once using a transaction. * * @param string $scanID * @param string $timestamp_started * @param string $timestamp_finished * @param integer $uid * @param string $dir * @param array $report_data * * @throws Exception */ public function insertDirData($scanID, $timestamp_started, $timestamp_finished, $uid, $dir, $domain, $report_data) { $this->dbh->exec('BEGIN;'); try { $report_id = $this->insertReport($scanID, $timestamp_started, $timestamp_finished, $uid, $dir, $domain); $root_id = null; foreach ($report_data as $app) { $parent_id = $this->insertApp($report_id, $root_id, $app['appName'], $app['appVersion'], $app['appPath'], $app['appFilename'], $app['appUID'], $app['appRealPath']); if (isset($app['subApp'])) { foreach ($app['subApp'] as $subApp) { $this->insertApp($report_id, $parent_id, $subApp['appName'], $subApp['appVersion'], $subApp['appPath'], $subApp['appFilename'], $subApp['appUID'], $subApp['appRealPath']); } } } $this->dbh->exec('COMMIT;'); } catch (\Exception $exception) { $this->dbh->exec('ROLLBACK;'); throw $exception; } } /** * Delete old reports for directory * * @param string $dir */ public function clearOutdatedData($dir) { $sql = 'DELETE FROM report WHERE dir = :dir AND id NOT IN ( SELECT max(id) FROM report WHERE dir = :dir ) '; $stmt = $this->dbh->prepare($sql); $stmt->bindValue('dir', $dir, SQLITE3_TEXT); $stmt->execute(); } /** * Are there reports on the $directory starting at a $since? * * @param string $directory * @param string $since * * @return bool */ public function haveRelevantReport($directory, $since) { $sql = 'SELECT count(*)' . ' FROM report' . ' WHERE dir = :dir AND timestamp_finished > :since'; $stmt = $this->dbh->prepare($sql); $stmt->bindValue('dir', $directory, SQLITE3_TEXT); $stmt->bindValue('since', $since, SQLITE3_TEXT); $result = $stmt->execute(); return (bool)$result->fetchArray(SQLITE3_NUM)[0]; } /** * SqliteDbReportConnection destructor. */ public function __destruct() { $this->close(); } /** * Inserts a one record into report table. * * @param string $scanID * @param string $timestamp_started * @param string $timestamp_finished * @param integer $uid * @param string $dir * @param string $domain * * @return integer Last insert id */ private function insertReport($scanID, $timestamp_started, $timestamp_finished, $uid, $dir, $domain): int { $this->reportStatement->bindValue('scanID', $scanID, SQLITE3_TEXT); $this->reportStatement->bindValue('timestamp_started', $timestamp_started, SQLITE3_TEXT); $this->reportStatement->bindValue('timestamp_finished', $timestamp_finished, SQLITE3_TEXT); $this->reportStatement->bindValue('uid', $uid, SQLITE3_INTEGER); $this->reportStatement->bindValue('dir', $dir, SQLITE3_TEXT); $this->reportStatement->bindValue('domain', $domain, SQLITE3_TEXT); $result = $this->reportStatement->execute(); return $this->dbh->lastInsertRowID(); } /** * Inserts a one record into apps table. * * @param integer $reportId * @param integer|null $parentId * @param string $title * @param string $version * @param string $path * @param string|null $filename * * @return integer Last insert id */ private function insertApp($reportId, $parentId, $title, $version, $path, $filename, $uid, $real_path): int { $this->appsStatement->bindValue('report_id', $reportId, SQLITE3_INTEGER); $this->appsStatement->bindValue('parent_id', $parentId, SQLITE3_INTEGER); $this->appsStatement->bindValue('title', $title, SQLITE3_TEXT); $this->appsStatement->bindValue('version', $version, SQLITE3_TEXT); $this->appsStatement->bindValue('path', $path, SQLITE3_TEXT); $this->appsStatement->bindValue('filename', $filename, SQLITE3_TEXT); $this->appsStatement->bindValue('app_uid', $uid, SQLITE3_INTEGER); $this->appsStatement->bindValue('real_path', $real_path, SQLITE3_TEXT); $result = $this->appsStatement->execute(); return $this->dbh->lastInsertRowID(); } /** * Opens connection. * * @param string $path */ private function open($path) { $this->dbh = new \SQLite3($path); $this->dbh->busyTimeout(self::BUSY_TIMEOUT_MSEC); $this->dbh->exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys=ON;'); } /** * Closes connection. */ private function close() { $this->dbh = null; } /** * Create tables(report and apps) if not exists. */ private function createTablesIfNotExists() { $this->dbh->exec(/** @lang SQLite */' CREATE TABLE IF NOT EXISTS report ( id INTEGER PRIMARY KEY, scanID TEXT NOT NULL, timestamp_started TEXT NOT NULL, timestamp_finished TEXT NOT NULL, uid INTEGER, dir TEXT NOT NULL, domain TEXT DEFAULT NULL ) '); $this->dbh->exec(/** @lang SQLite */' CREATE TABLE IF NOT EXISTS apps ( id INTEGER PRIMARY KEY, report_id INTEGER NOT NULL REFERENCES report(id) ON DELETE CASCADE, parent_id INTEGER DEFAULT NULL REFERENCES apps(id), title TEXT NOT NULL, version TEXT NOT NULL, path TEXT NOT NULL, filename TEXT DEFAULT NULL, app_uid INTEGER DEFAULT NULL, real_path TEXT DEFAULT NULL ) '); } } getPathname() . '": DB file not found.'); } $this->open($path); if (!$this->validate()) { throw new AppException('Failed loading DB from "' . $path->getPathname() . '": invalid DB format.'); } } /** * Checks if a $name key exists. * * @param string $key * @return boolean */ public function hasKey(string $key) { $sql = 'SELECT id ' . ' FROM app ' . ' WHERE name = :name'; $stmt = $this->dbh->prepare($sql); $stmt->bindValue('name', $key, SQLITE3_TEXT); $result = $stmt->execute(); return (bool)$result->fetchArray(SQLITE3_NUM); } /** * Returns pairs for a specified key. * * Note: this method should return an ORDERED list of pairs. * * @param string $key * @return array|null list of pairs */ public function getByKey(string $key) { $sql = 'SELECT first_version, last_version' . ' FROM app A' . ' LEFT JOIN branch B ON A.id=B.app_id' . ' WHERE name = :name'; $stmt = $this->dbh->prepare($sql); $stmt->bindValue('name', $key, SQLITE3_TEXT); $result = $stmt->execute(); $versions = []; while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $versions[] = [$row['first_version'], $row['last_version']]; } return $versions ? $versions : null; } /** * Validate format of the DB. * * @param array $db * @return bool */ private function validate() { try { $result = $this->checkTable('app') && $this->checkTable('branch'); } catch (\Exception $ex) { return false; } return $result; } /** * Opens connection. * * @param string $path */ private function open($path) { $this->dbh = new \SQLite3($path); } /** * Check is there table in DB * * @throws Exception * @return bool */ private function checkTable($table_name) { $sql = 'PRAGMA table_info("' . $table_name . '")'; $stmt = $this->dbh->prepare($sql); $result = $stmt->execute(); return (bool)$result->fetchArray(); } } config)) { throw new ConfigParamException('An invalid option requested. Key: ' . $key); } return $this->config[$key]; } /** * Set value to config by key * * @param string $key * @param mixed $value * @return mixed * @throws Exception */ public function set($key, $value) { $this->config[$key] = $value; } /** * Set default config * * @param array $defaults */ protected function setDefaultConfig($defaults) { $this->config = $defaults; } }add(new \AppVersionDetector\Core\VersionDetection\Detector\DrupalCoreDetector()); $drupalDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\DrupalPluginDetector()); $detector->add($drupalDependencyDetector); // Joomla $joomlaDependencyDetector = new DependencyCollectionDetector(); $joomlaDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\JoomlaCoreDetector()); $joomlaDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\JoomlaPluginDetector()); $detector->add($joomlaDependencyDetector); // WordPress $wpDependencyDetector = new DependencyCollectionDetector(); $wpDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\WpCoreDetector()); $wpDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\WpPluginDetector()); $wpDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\WpThemeDetector()); $detector->add($wpDependencyDetector); // Magento $magentoDependencyDetector = new DependencyCollectionDetector(); $magentoDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\MagentoCoreDetector()); $detector->add($magentoDependencyDetector); // IPB $ipbDependencyDetector = new DependencyCollectionDetector(); $ipbDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\IPBCoreDetector()); $detector->add($ipbDependencyDetector); // MODX $modxDependencyDetector = new DependencyCollectionDetector(); $modxDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\ModxCoreDetector()); $detector->add($modxDependencyDetector); // PHPBB3 $phpbb3DependencyDetector = new DependencyCollectionDetector(); $phpbb3DependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\PHPBB3CoreDetector()); $detector->add($phpbb3DependencyDetector); // OsCommerce $oscomDependencyDetector = new DependencyCollectionDetector(); $oscomDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\OsCommerceCoreDetector()); $detector->add($oscomDependencyDetector); // Bitrix $bitrixDependencyDetector = new DependencyCollectionDetector(); $bitrixDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\BitrixCoreDetector()); $detector->add($bitrixDependencyDetector); // CommonScript $csDetector = new \AppVersionDetector\Core\VersionDetection\Detector\CommonScriptDetector(); $detector->add($csDetector); // OpenCart $opencartDependencyDetector = new DependencyCollectionDetector(); $opencartDependencyDetector->add(new \AppVersionDetector\Core\VersionDetection\Detector\OpenCartCoreDetector()); $detector->add($opencartDependencyDetector); return $detector; } } ['short' => 'h', 'long' => 'help', 'needValue' => false], AVDConfig::PARAM_VERSION => ['short' => 'v', 'long' => 'version,ver', 'needValue' => false], AVDConfig::PARAM_JSON_REPORT => ['short' => '', 'long' => 'json-output,json-report', 'needValue' => true], AVDConfig::PARAM_TEXT_REPORT => ['short' => '', 'long' => 'text-output,text-report', 'needValue' => 0], AVDConfig::PARAM_SQLITE_DB_REPORT => ['short' => '', 'long' => 'sqlite-db-report', 'needValue' => true], AVDConfig::PARAM_SINCE => ['short' => '', 'long' => 'since', 'needValue' => true], AVDConfig::PARAM_STDIN_DIR => ['short' => '', 'long' => 'stdin-dirs', 'needValue' => false], AVDConfig::PARAM_PATHES_IN_BASE64 => ['short' => '', 'long' => 'paths-in-base64,pathes-in-base64', 'needValue' => false], // TODO: Over time we need to remove param "--pathes-in-base64" its typo AVDConfig::PARAM_CHACK_OUTDATED => ['short' => '', 'long' => 'check-outdated', 'needValue' => true], AVDConfig::PARAM_SCAN_DEPTH => ['short' => '', 'long' => 'scan-depth', 'needValue' => true], AVDConfig::PARAM_SEND_STATS => ['short' => '', 'long' => 'send-stats', 'needValue' => false], AVDConfig::PARAM_DEBUG => ['short' => '', 'long' => 'debug', 'needValue' => false], AVDConfig::PARAM_FACTORY_CONFIG => ['short' => '', 'long' => 'factory-config', 'needValue' => true], AVDConfig::PARAM_MIGRATE => ['short' => '', 'long' => 'migrate', 'needValue' => false], ]; /** * Parse comand line params * * @return void * @throws Exception */ protected function parse() { foreach ($this->opts as $configName => $params) { if ($configName == AVDConfig::PARAM_FACTORY_CONFIG) { continue; } $default = $params['needValue'] ? $this->config->get($configName) : null; $result = $this->getParamValue($configName, $default); if (!$params['needValue'] && $result === false) { // $result === false because opt without value $result = true; } $this->config->set($configName, $result); } if ($this->config->get(AVDConfig::PARAM_HELP)) { $this->showHelp(); } elseif ($this->config->get(AVDConfig::PARAM_VERSION)) { $this->showVersion(); } $posArgs = $this->getFreeAgrs(); if ($this->config->get(AVDConfig::PARAM_MIGRATE)) { (new Migraitor)->migrate($this->config); exit(0); } if (count($posArgs) < 1 && !$this->config->get(AVDConfig::PARAM_STDIN_DIR)) { $this->config->set(AVDConfig::PARAM_STDIN_DIR, true); $this->config->set(AVDConfig::PARAM_PATHES_IN_BASE64, true); } if (count($posArgs) > 1) { throw new ConfigParamException('Too many target directories is specified.'); } $dirFromStdin = $this->config->get(AVDConfig::PARAM_STDIN_DIR); $directories = []; if ($dirFromStdin) { $lines = explode("\n", trim(file_get_contents('php://stdin'))); $useDomains = false; if (count($lines) && strpos($lines[0], ',') !== false) { list($domain, $dir) = explode(',', $lines[0], 2); if (strpos($domain, '/') === false) { $useDomains = true; } } if ($useDomains) { foreach ($lines as $line) { list($domain, $dir) = explode(',', $line, 2); $domain = \idn_to_utf8($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); $directories[] = new Directory($dir, $domain); } } else { foreach ($lines as $line) { $directories[] = new Directory($line); } } unset($lines); } else { $directories[] = new Directory(realpath($posArgs[0])); } if (count($directories) < 1) { throw new ConfigParamException('Target directory is not specified.'); } $pathesInBase64 = $this->config->get(AVDConfig::PARAM_PATHES_IN_BASE64); foreach ($directories as $index => &$directory) { $directory_path = $directory->getPath(); if ($pathesInBase64) { $directory_path = base64_decode($directory_path); $directory->setPath($directory_path); } if (empty($directory_path)) { unset($directories[$index]); continue; } if (!file_exists($directory_path)) { $message = "Directory {$directory_path} does not exist."; if ($dirFromStdin) { unset($directories[$index]); fwrite(STDERR, $message . "\n"); continue; } throw new ConfigParamException($message); } if (!is_dir($directory_path)) { $message = "Looks like {$directory_path} is not a directory."; if ($dirFromStdin) { unset($directories[$index]); fwrite(STDERR, $message . "\n"); continue; } throw new ConfigParamException($message); } } unset($directory); $this->config->set(AVDConfig::PARAM_TARGET_DIRECTORIES, $directories); $factoryConfig = $this->config->get(AVDConfig::PARAM_FACTORY_CONFIG); $jsonReport = $this->config->get(AVDConfig::PARAM_JSON_REPORT); if ($jsonReport) { if ($jsonReport === 'STDOUT') { $this->config->set(AVDConfig::PARAM_JSON_REPORT, 'php://stdout'); } elseif ($jsonReport !== 'php://stdout') { if (file_exists($jsonReport)) { throw new ConfigParamException("File {$jsonReport} already exists."); } $dir = dirname($jsonReport); if (!is_writable($dir)) { throw new ConfigParamException("Directory {$dir} is not writable."); } } } $textReport = $this->config->get(AVDConfig::PARAM_TEXT_REPORT); if ($textReport) { $factoryConfig[AbstractFileBasedReport::class] = TextReport::class; if ($textReport !== true) { if (file_exists($textReport)) { throw new ConfigParamException("File {$textReport} already exists."); } $dir = dirname($textReport); if (!is_writable($dir)) { throw new ConfigParamException("Directory {$dir} is not writable."); } } } if ($this->config->get(AVDConfig::PARAM_SEND_STATS)) { $factoryConfig[RemoteStatsReport::class] = RemoteStatsReport::class; } $scanDepth = $this->config->get(AVDConfig::PARAM_SCAN_DEPTH); if ($scanDepth < 0) { throw new ConfigParamException('Invalid value for the scan-depth option.'); } $sqliteDBeport = $this->config->get(AVDConfig::PARAM_SQLITE_DB_REPORT); if ($sqliteDBeport) { $dirname = dirname($sqliteDBeport); if (file_exists($sqliteDBeport)) { if (!is_writable($sqliteDBeport)) { throw new ConfigParamException("Invalid value for the sqlite-db-report option. File {$sqliteDBeport} is not writable."); } } elseif (!is_writable($dirname)) { throw new ConfigParamException("Invalid value for the sqlite-db-report option. Directory {$dirname} is not writable."); } } if ($this->config->get(AVDConfig::PARAM_DEBUG)) { Stats::onAccumulateStats(); Profiler::onProfiler(); } $factoryConfigFromOpt = $this->getParamValue(AVDConfig::PARAM_FACTORY_CONFIG, false); if ($factoryConfigFromOpt) { if (!file_exists($factoryConfigFromOpt)) { throw new ConfigParamException("Factory config file {$factoryConfigFromOpt} does not exist."); } $customFactoryConfig = require($factoryConfigFromOpt); $factoryConfig = array_merge($factoryConfig, $customFactoryConfig); } $this->config->set(AVDConfig::PARAM_FACTORY_CONFIG, $factoryConfig); } /** * Cli show help * * @return void */ private function showHelp() { echo << Nesting Level for CMS search. Default 1 --json-report= File path with json report --text-report Output in stdout --sqlite-db-report= Path to sqlite database file with report. If an existing database is specified, data is added to it. --since= Used only with --sqlite-db-report, allows you to scan only new folders and those that were scanned before . --paths-in-base64 Base64 encrypted paths --send-stats Send statistics to CH --migrate Starting the migration procedure for this package -v, --version -h, --help HELP; exit(0); } /** * Cli show version * * @return void */ private function showVersion() { die('AppVersionDetector v' . Scanner::VERSION . "\n"); } }path = $path; $this->domain = $domain; } /** * Set directory path * * @param string $path * * @return void */ public function setPath(string $path) { $this->path = $path; } /** * Get directory path * * @return string */ public function getPath(): string { return $this->path; } /** * Set domain name * * @param string $domain * * @return void */ public function setDomain(string $domain) { $this->domain = $domain; } /** * Get domain name * * @return string */ public function getDomain(): string { return $this->domain; } /** * Set Owner id * * @param int $owner * * @return void */ public function setOwner(int $owner) { $this->owner = $owner; } /** * Get owner id * * @return int */ public function getOwner(): int { if (is_null($this->owner)) { $this->owner = @fileowner($this->path); } return $this->owner ?? 0; } }get(AVDConfig::PARAM_SQLITE_DB_REPORT)) { $this->migrateSqliteDB($config->get(AVDConfig::PARAM_SQLITE_DB_REPORT)); } $this->migrateSqliteDB('/var/imunify360/components_versions.sqlite3'); $this->migrateSqliteDB('/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3'); } //////////////////////////////////////////////////////////////////////////// private function migrateSqliteDB($db_filepath) { if (!file_exists($db_filepath)) { return true; } $db_connection = $this->getConnection($db_filepath); if (!$db_connection) { return false; } $this->migrateSqliteDBAppUID($db_connection); $this->migrateSqliteDBAppRealPath($db_connection); $this->migrateSqliteDBAppDomain($db_connection); $this->migrateSqliteDBClearOutdatedData($db_connection); } private function migrateSqliteDBClearOutdatedData($db_connection) { $sql_count_outdated_data = 'SELECT count(*) FROM report WHERE id NOT IN ( SELECT max(id) FROM report GROUP BY dir ) '; if (!$this->getOneValue($db_connection, $sql_count_outdated_data)) { return; } $db_connection->exec('PRAGMA foreign_keys=OFF'); $db_connection->exec('BEGIN'); $db_connection->exec('CREATE TABLE _report AS SELECT * FROM report WHERE id IN ( SELECT max(id) FROM report GROUP BY dir ) '); $db_connection->exec('CREATE TABLE _apps AS SELECT * FROM apps WHERE report_id IN ( SELECT max(id) FROM report GROUP BY dir ) '); $db_connection->exec('DELETE FROM apps'); $db_connection->exec('DELETE FROM report'); $db_connection->exec('INSERT INTO report SELECT * FROM _report'); $db_connection->exec('INSERT INTO apps SELECT * FROM _apps'); $db_connection->exec('DROP TABLE _apps'); $db_connection->exec('DROP TABLE _report'); $db_connection->exec('COMMIT'); $db_connection->exec('PRAGMA foreign_keys=ON'); $db_connection->exec('VACUUM'); } private function migrateSqliteDBAppUID($db_connection) { if (!$this->haveColumn($db_connection, 'apps', 'app_uid')) { $this->addColumn($db_connection, 'apps', 'app_uid INTEGER DEFAULT NULL'); } } private function migrateSqliteDBAppRealPath($db_connection) { if (!$this->haveColumn($db_connection, 'apps', 'real_path')) { $this->addColumn($db_connection, 'apps', 'real_path TEXT DEFAULT NULL'); } } private function migrateSqliteDBAppDomain($db_connection) { if (!$this->haveColumn($db_connection, 'report', 'domain')) { $this->addColumn($db_connection, 'report', 'domain TEXT DEFAULT NULL'); } } private function getConnection($db_filepath) { return new \SQLite3($db_filepath); } private function haveColumn($db_connection, $table_name, $column_name) { $sql = 'PRAGMA table_info("' . $table_name . '")'; $stmt = $db_connection->prepare($sql); $result = $stmt->execute(); while ($row = $result->fetchArray(SQLITE3_ASSOC)) { if ($row['name'] == $column_name) { return true; } } return false; } private function haveTable($db_connection, $table_name) { $sql = 'PRAGMA table_info("' . $table_name . '")'; $stmt = $db_connection->prepare($sql); $result = $stmt->execute(); return (bool)$result->fetchArray(); } private function addColumn($db_connection, $table_name, $column_params) { return @$db_connection->exec('ALTER TABLE ' . $table_name . ' ADD COLUMN ' . $column_params); } private function getOneValue($db_connection, $sql, $default = false) { $results = $db_connection->query($sql); $row = $results->fetchArray(SQLITE3_NUM); if (!$row) { return $default; } return $row[0]; } } 'v', 'long' => 'version,ver', 'needValue' => false] */ protected $opts = []; /** * @var array Current of options from $argv */ private $options = []; /** * @var array Arguments left after getopt() processing */ private $freeAgrs = []; /** * Construct * * @param array $argv * @param Config $config * @throws ConfigParamException */ public function __construct($argv, Config $config) { $this->config = $config; $cliLongOpts = []; $cliShortOpts = []; foreach ($this->opts as $params) { $postfix = $params['needValue'] === 0 ? '::' : ($params['needValue'] ? ':' : ''); if ($params['long']) { $cliLongOpts = array_merge($cliLongOpts, $this->getMultiOpts($params['long'], $postfix)); } if ($params['short']) { $cliShortOpts = array_merge($cliShortOpts, $this->getMultiOpts($params['short'], $postfix)); } } $this->parseOptions($argv, $cliShortOpts, $cliLongOpts); $this->parse(); } /** * Parse comand line params */ abstract protected function parse(); /** * Checking if the parameter was used in the cli line * * @param string $paramKey * @return bool * @throws ConfigParamException */ protected function issetParam($paramKey) { if (!isset($this->opts[$paramKey])) { throw new ConfigParamException('An invalid option requested.'); } if ($this->getExistingOpt($this->opts[$paramKey]['long'])) { return true; } elseif ($this->getExistingOpt($this->opts[$paramKey]['short'])) { return true; } return false; } /** * Checking if the parameter was used in the cli line * * @param string $paramKey * @return bool * @throws ConfigParamException */ protected function getParamValue($paramKey, $default = null) { if (!isset($this->opts[$paramKey])) { throw new ConfigParamException('An invalid option requested.'); } $existingLongOpt = $this->getExistingOpt($this->opts[$paramKey]['long']); if ($existingLongOpt) { return $this->options[$existingLongOpt]; } $existingShortOpt = $this->getExistingOpt($this->opts[$paramKey]['short']); if ($existingShortOpt) { return $this->options[$existingShortOpt]; } return $default; } /** * Return free arguments after using getopt() * * @return array */ protected function getFreeAgrs() { return $this->freeAgrs; } /** * Parse by getopt() and fill vars: $this->options $this->freeAgrs * * @return void */ private function parseOptions($argv, $cliShortOpts, $cliLongOpts) { if (count($argv) <= 1) { return; } $this->options = getopt(implode('', $cliShortOpts), $cliLongOpts); //$this->freeAgrs = array_slice($argv, $optind); // getopt(,,$optind) only for PHP7.1 and upper for($i = 1; $i < count($argv); $i++) { if (strpos($argv[$i], '-') !== 0) { $this->freeAgrs = array_slice($argv, $i); break; } } } /** * Clean cli parameter * * @param string $optName Paramenter may be with ":" postfix * @return array */ private function getCleanOptName($optName) { return str_replace(':', '', $optName); } /** * Return options with or without ":" postfix * * @param array $optString String with one or more options separated by "," * @param bool $addPostfix True if need add postfix * @return array Array list of options */ private function getMultiOpts($optString, $addPostfix = false) { $opts = explode(',', $optString); $opts = array_map(function($value) use ($addPostfix) { return $value . $addPostfix; }, $opts); return $opts; } /** * Return existing options from string. * * @param string $optsString String with one or more options separated by "," * @return string|bool Name of finded options in getopt() */ private function getExistingOpt($optsString) { $opts = $this->getMultiOpts($optsString); foreach ($opts as $opt) { if (isset($this->options[$opt])) { return $opt; } } return false; } }appDie($errno, 'An unknown error occurred!' . ' Error code: [' . $errno . '] Message: ' . $errstr . PHP_EOL); } /** * Handles an exception. * * @param Throwable $exception */ public function handleException(Throwable $exception) { if ($exception instanceOf ConfigParamException) { $message = 'Configuration error: ' . $exception->getMessage() . PHP_EOL; $message .= 'Usage example: php app-version-detector.phar OPTIONS SCAN_DIR' . PHP_EOL; $this->appDie(self::ERRNUM_CONFIG_PARAM_EXCEPTION, $message); } elseif ($exception instanceOf AppException) { $message = 'Application error: ' . $exception->getMessage() . '. Code: ' . $exception->getCode() . PHP_EOL; $this->appDie(self::ERRNUM_APP_EXCEPTION, $message); } else { $this->appDie($exception->getCode(), 'An unknown error occurred! Code: ' . $exception->getCode() . ' Message: ' . $exception->getMessage() . PHP_EOL); } } /** * Die process * * @param int $errno * @param string $errstr */ private function appDie(int $errno, string $errstr) { fwrite(STDERR, $errstr); exit($errno); } }cache) && file_exists($filepath)) { $this->cache[$filepath] = @fileowner($filepath); } return $this->cache[$filepath]; } } false, self::PARAM_VERSION => false, self::PARAM_JSON_REPORT => false, self::PARAM_TEXT_REPORT => false, self::PARAM_SQLITE_DB_REPORT => false, self::PARAM_SINCE => false, self::PARAM_STDIN_DIR => false, self::PARAM_PATHES_IN_BASE64 => false, self::PARAM_CHACK_OUTDATED => true, self::PARAM_SCAN_DEPTH => 1, self::PARAM_SEND_STATS => false, self::PARAM_DB_FILE => __DIR__ . '/../../data/actual-versions-db.json', self::PARAM_DEBUG => false, self::PARAM_FACTORY_CONFIG => [ EventManager::class => EventManager::class, AbstractFileBasedReport::class => JsonReport::class, ActualVersionsDb::class => ActualVersionsDb::class, ActualVersionsSQLiteDb::class => ActualVersionsSQLiteDb::class, RemoteStatsRequest::class => RemoteStatsRequest::class, ComparisonStrategyInterface::class => GenericComparisonStrategy::class, OutdatedChecker::class => OutdatedChecker::class, SqliteDbReportConnection::class => SqliteDbReportConnection::class, SqliteDbReport::class => SqliteDbReport::class, ], self::PARAM_TARGET_DIRECTORIES => [], self::PARAM_MIGRATE => false, ]; /** * Construct */ public function __construct() { $this->setDefaultConfig($this->defaultConfig); } }publisher = $publisher; } /** * Scans a list of directories. * * @param SqliteDbReportConnection $dbReportConnection * @param array $directories * @param DetectorInterface $detector * @param int $depth * * @throws Exception */ public function run($dbReportConnection, $directories, $detector, $depth = 1, $since = false) { if ($depth < 1) { throw new AppException('Value of the $depth parameter can not be less than 1.'); } foreach ($directories as $directory) { if (!is_dir($directory->getPath())) { throw new AppException('Only a directory can be scanned. Problem with "' . $directory->getPath() . '".'); } } $scanId = $this->generateScanId(); $this->publisher->update(new ScanningStartedEvent($this, self::VERSION, $scanId)); foreach ($directories as $directory) { if (!is_null($dbReportConnection) && $since && $dbReportConnection->haveRelevantReport($directory->getPath(), $since)) { continue; } $this->scanDirectory($scanId, $directory, $detector, $depth); } Stats::setCurrentMemoryUsagePeak(); Stats::setCurrentMemoryUsageEnd(); $this->publisher->update(new ScanningEndedEvent($this, $scanId)); } /** * Generates a scan ID. * * @return string */ public function generateScanId(): string { $time = gettimeofday(); return substr($time['sec'] . $time['usec'], -16); } /** * Scans a particular directory. * @param int $scanId * @param Directory $directory * @param DetectorInterface $detector * @param int $depth */ private function scanDirectory($scanId, Directory $directory, $detector, $depth = 1) { $this->publisher->update(new ScanningDirStartedEvent($this, self::VERSION, $directory)); // scan a base directory $detector->perform($this->publisher, new SplFileInfo($directory->getPath())); // scan subdirectories /** @var RecursiveDirectoryIterator|RecursiveIteratorIterator $iterator */ $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $directory->getPath(), FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); $iterator->setMaxDepth($depth - 1); $files_counter = 0; $iterator->rewind(); while($files_counter <= AbstractDetector::FILES_PER_DIR_LIMIT && $iterator->valid()) { Stats::incrementCountFilesIteration(); $files_counter++; if ($iterator->isDir()) { Stats::incrementCountDirsOpened(); $detector->perform($this->publisher, new SplFileInfo($iterator->key())); } $iterator->next(); } $this->publisher->update(new ScanningDirEndedEvent($this)); } } sender; } /** * Sets an event sender. * * @param object $sender */ protected function setSender($sender) { $this->sender = $sender; } }publisher = $publisher; if (isset($this->detectors[0])) { $this->detectors[0]->perform($this, $splFileInfo); } } /** * Notifies about an event. * * @param AbstractEvent $event */ public function update(AbstractEvent $event) { $this->publisher->update($event); if (!($event instanceof DetectionEvent)) { return; } /** @var $event DetectionEvent */ foreach ($this->detectors as $key => $detector) { if ($key == 0) { continue; } $detector->perform($this->publisher, $event->getPath()); } } }setSender($sender); $this->name = $name; $this->version = $version; $this->path = $path; } /** * @return string */ public function getName() { return $this->name; } /** * @return int */ public function getUID() { if (is_null($this->uid) && file_exists($this->path) && !is_null(self::$fileOwners)) { $this->uid = self::$fileOwners->getFileOwner((string)$this->path); } return $this->uid; } /** * @return string */ public function getVersion() { return $this->version; } /** * @param string $version */ public function setVersion($version) { $this->version = $version; } /** * @return SplFileInfo */ public function getPath(): SplFileInfo { return $this->path; } } getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); if ($this->isOsCom2x($dir)) { $detected = true; $tmp_ver = Helper::tryGetVersion($dir . '/includes/version.php', 1024, '~(.*)~'); if ($tmp_ver !== '') { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } else { $tmp_ver = Helper::tryGetVersion($dir . '/includes/application_top.php', 4096, '~define\(\'PROJECT_VERSION\',\s*\'[^\d]+([\w\.-]+)\'\);~msi'); if ($tmp_ver !== '') { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } } } else if ($this->isOsCom3x($dir)) { $detected = true; $tmp_ver = Helper::tryGetVersion($dir . '/osCommerce/OM/version.txt', 1024, '~(.*)~'); if ($tmp_ver !== '') { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } } return $detected ? $detectionEvent : null; } /** * Check is OsCommerce 2x * * @param string $dir * @return boolean */ private function isOsCom2x($dir) { if (file_exists($dir . '/includes/configure.php') && (file_exists($dir . '/includes/boxes/shopping_cart.php') || file_exists($dir . '/includes/classes/osc_template.php')) ) { return true; } return false; } /** * Check is OsCommerce 3x * * @param string $dir * @return boolean */ private function isOsCom3x($dir) { if (file_exists($dir . '/osCommerce/OM/Core/Site/Shop/Application/Index/Action/Manufacturers.php') && file_exists($dir . '/osCommerce/OM/Config/settings.ini') ) { return true; } return false; } }getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); if (file_exists($dir . '/catalog') && file_exists($dir . '/system') && file_exists($dir . '/admin') && file_exists($dir . '/admin/config.php') && file_exists($dir . '/image') && file_exists($dir . '/index.php') && file_exists($dir . '/config.php') ) { $detected = true; $filepath = $dir . '/index.php'; if (is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 1024); if (preg_match('~define\(\'VERSION\',\s*\'(.+?)\'~smi', $tmp_content, $tmp_ver)) { $detectionEvent->setVersion($tmp_ver[1]); } } } return $detected ? $detectionEvent : null; } }getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); $drupal_filepath = $dir . '/core/lib/Drupal.php'; if (file_exists($drupal_filepath) && is_file($drupal_filepath)) { $detected = true; Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($drupal_filepath, 8192); if (preg_match('|VERSION\s*=\s*\'(\d+\.\d+\.\d+)\'|smi', $tmp_content, $tmp_ver)) { $detectionEvent->setVersion($tmp_ver[1]); return $detectionEvent; } } if (file_exists($dir . '/sites/all') || file_exists($dir . '/sites/default') || file_exists($dir . '/modules/system.module') ) { $detected = true; $tmp_content = ''; $possibleFiles = [ $dir . '/CHANGELOG.txt', $dir . '/CHANGELOG', ]; foreach ($possibleFiles as $filepath) { if (!file_exists($filepath) || !is_file($filepath)) { continue; } Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); break; } if ($tmp_content && preg_match('~Drupal\s+(\d+\.\d+[\d.]+)~smi', $tmp_content, $tmp_ver)) { $detectionEvent->setVersion($tmp_ver[1]); return $detectionEvent; } } $systeminfo_filepath = $dir . '/modules/system/system.info'; if (file_exists($systeminfo_filepath) && is_file($systeminfo_filepath)) { $detected = true; Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($systeminfo_filepath, 8192); if (preg_match('|version\s*=\s*"(\d+\.\d+)"|smi', $tmp_content, $tmp_ver)) { $detectionEvent->setVersion($tmp_ver[1]); return $detectionEvent; } } return $detected ? $detectionEvent : null; } }version = $version; } /** * Implements finding and extracting info(version and name). * * If something is found it return AppVersionDetector\Core\Detector\Version\DetectionEventData. * If no luck it returns null. * * @param SplFileInfo $splFileInfo * @return DetectionEvent|null * @throws \Exception */ protected function findAndExtractData(SplFileInfo $splFileInfo) { if (is_null($this->version)) { return null; } return new DetectionEvent($this, new SplFileInfo(__DIR__), $this->detector_name, $this->version); } }getPathname(); $files['phpmailer'] = [ 'phpmailer.php', 'php-mailer.php', 'PHPMailer.php', 'PhpMailer.php', 'PHP-Mailer.php', 'Php-Mailer.php', ]; $files['timthumb'] = [ 'timthumb.php', 'thumb.php', 'Thumb.php', 'TimThumb.php', 'tim-thumb.php', ]; $files['phpMyAdmin'] = [ 'libraries/Config.class.php', 'libraries/classes/Config.php', 'libraries/classes/Version.php', ]; $data = []; $data = $this->checkAndReadPHPMailerVersion($dir, $files['phpmailer']); $data = array_merge($data, $this->checkAndReadTimThumbVersion($dir, $files['timthumb'])); $data = array_merge($data, $this->checkAndReadPhpMyAdminVersion($dir, $files['phpMyAdmin'])); return $data; } private function checkAndReadPHPMailerVersion($dir, $files) { $data = []; foreach ($files as $file) { $filepath = $dir . '/' . $file; if (!file_exists($filepath) || !is_readable($filepath) || !is_file($filepath)) { continue; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($filepath, 32768); $version = ''; if (strpos($content, 'PHPMailer') === false) { continue; } $l_Found = preg_match('~Version:(\s*\d+\.\d+\.\d+)~', $content, $l_Match); if ($l_Found) { $version = $l_Match[1]; } if (!$l_Found) { $l_Found = preg_match('~Version\s*=\s*\'(\d+\.\d+\.\d+)~i', $content, $l_Match); if ($l_Found) { $version = $l_Match[1]; } } $data[] = new DetectionEvent( $this, new SplFileInfo($filepath), $this->prefix . 'phpmailer', $version ); } return $data; } private function checkAndReadTimThumbVersion($dir, $files) { $data = []; foreach ($files as $file) { $filepath = $dir . '/' . $file; if (!file_exists($filepath) || !is_readable($filepath) || !is_file($filepath)) { continue; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($filepath, 8192); $version = ''; if (strpos($content, 'code.google.com/p/timthumb') === false) { continue; } $l_Found = preg_match('~define\s*\(\'version\'\s*,\s*\'([^\']+)\'\s*\);~i', $content, $l_Match); if ($l_Found) { $version = $l_Match[1]; } $data[] = new DetectionEvent( $this, new SplFileInfo($filepath), $this->prefix . 'timthumb', $version ); } return $data; } private function checkAndReadPhpMyAdminVersion($dir, $files) { $data = []; foreach ($files as $file) { $filepath = $dir . '/' . $file; if (!file_exists($filepath) || !is_readable($filepath) || !is_file($filepath)) { continue; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($filepath, 8192); $version = ''; if (strpos($content, 'PMA_VERSION') !== false && preg_match('~\$this->set\(\'PMA_VERSION\'\s*,\s*\'([^\']+)\'\);~i', $content, $l_Match) ) { $version = $l_Match[1]; } elseif (strpos($content, 'final class Version') !== false && strpos($content, 'namespace PhpMyAdmin;') !== false && preg_match('~public\s*const\s*VERSION\s*=\s*\'([^\']+)\'\s*.\s*VERSION_SUFFIX;~i', $content, $l_Match) ) { $version = $l_Match[1]; } if ($version !== '') { $data[] = new DetectionEvent( $this, new SplFileInfo($dir), $this->prefix . 'phpmyadmin', $version ); } } return $data; } } getPathname(); if (!$this->isWpBase($dir)) { return null; } $components_data = []; $components = $this->wpComponentList($dir . '/wp-content/' . $this->components_folder); foreach ($components as $component => $data) { $components_data[] = new DetectionEvent($this, $splFileInfo, $this->prefix . $component, $data['Version']); } return $components_data; } /** * Check is the folder is root of WordPress site. * * @param string $dir * @return boolean */ private function isWpBase($dir) { return (bool)is_dir($dir . '/wp-content/' . $this->components_folder); } private function wpComponentList($dir) { $wp_components = []; $component_root = $dir; if (!is_dir($component_root)) { return $wp_components; } Stats::incrementCountDirsOpened(); $components_dir = @opendir($component_root); $component_files = []; if (!$components_dir) { return $wp_components; } $components_dir_file_counter = 0; while ($components_dir_file_counter <= AbstractDetector::FILES_PER_DIR_LIMIT && ($file = readdir($components_dir)) !== false) { Stats::incrementCountFilesIteration(); $components_dir_file_counter++; if (substr($file, 0, 1) == '.') { continue; } $component_dirpath = $component_root . '/' . $file; if (is_dir($component_dirpath)) { Stats::incrementCountDirsOpened(); $components_subdir = @opendir($component_dirpath); if (!$components_subdir) { continue; } $components_subdir_file_counter = 0; while ($components_subdir_file_counter <= AbstractDetector::FILES_PER_DIR_LIMIT && ($subfile = readdir($components_subdir)) !== false) { Stats::incrementCountFilesIteration(); $components_subdir_file_counter++; if (substr($subfile, 0, 1) == '.') { continue; } if ($this->isMainComponentFile($subfile)) { $component_files[] = "$file/$subfile"; } } closedir($components_subdir); } else { if ($this->isMainComponentFile($file)) { $component_files[] = $file; } } } closedir($components_dir); if (empty($component_files)) { return $wp_components; } foreach ($component_files as $component_file) { $component_filepath = $component_root . '/' . $component_file; if (!is_readable($component_filepath) || !is_file($component_filepath)) { continue; } $component_data = $this->getFileData($component_filepath); if (empty($component_data['Name'])) { continue; } if (empty($component_data['Version'])) { $component_data['Version'] = DetectorInterface::UNKNOWN_VERSION; } $package_name = explode('/', $component_file); $package = array_shift($package_name); $package = str_replace('.php', '', $package); $package = preg_replace('~[^a-z0-9]~is', '_', $package); $wp_components[$package] = $component_data; } return $wp_components; } abstract protected function isMainComponentFile($filename); private function getFileData($file) { $all_headers = [ 'Name' => $this->component_name . ' Name', 'Version' => 'Version' ]; Stats::incrementCountFilesReaded(); $file_data = Helper::getPartOfFile($file, 8192); $file_data = str_replace("\r", "\n", $file_data); foreach ($all_headers as $field => $regex) { if (preg_match('/^[ \t\/*#@]*' . preg_quote($regex, '/') . ':(.*)$/mi', $file_data, $match) && $match[1]) { $all_headers[$field] = $this->cleanupHeaderComment($match[1]); } else { $all_headers[$field] = ''; } } return $all_headers; } private function cleanupHeaderComment($str) { return trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $str)); } } getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); if (file_exists($dir . '/wp-admin/admin-functions.php')) { $detected = true; $filepath = $dir . '/wp-includes/version.php'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('|\$wp_version\s*=\s*\'(.+?)\'|smi', $tmp_content, $tmp_ver)) { $detectionEvent->setVersion($tmp_ver[1]); } } } return $detected ? $detectionEvent : null; } }getPathname(); if (!$this->isJoomla($dir)) { return null; } $result = []; $cmsVersion = $this->checkCmsVersion($dir); if ($cmsVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::JOOMLA_CMS_DETECTOR_NAME, $cmsVersion); } $platformVersion = $this->checkPlatformVersion($dir); if ($platformVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::JOOMLA_PLATFORM_DETECTOR_NAME, $platformVersion); } return empty($result) ? new DetectionEvent($this, $splFileInfo,self::JOOMLA_CMS_DETECTOR_NAME, DetectorInterface::UNKNOWN_VERSION) : $result; } /** * Tries to get a version of Joomla CMS. * * @param $dir * @return string|null */ private function checkCmsVersion($dir) { // for 1.0.x $filepath = $dir . '/includes/version.php'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('|var\s+\$RELEASE\s*=\s*\'(.+?)\'|smi', $tmp_content, $tmp_ver)) { $version = $tmp_ver[1]; if (preg_match('|var\s+\$DEV_LEVEL\s*=\s*\'(.+?)\'|smi', $tmp_content, $tmp_ver)) { $version .= '.' . $tmp_ver[1]; } return $version; } } // for 1.5.x $filepath = $dir . '/CHANGELOG.php'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('~-------------------- (\d+\.\d+\.\d+) ~smi', $tmp_content, $tmp_ver)) { return $tmp_ver[1]; } } // for 2.5.x $filepath = $dir . '/joomla.xml'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('~([\d.\-_a-z]+)~smi', $tmp_content, $tmp_ver)) { return $tmp_ver[1]; } } // for 3.x $filepath = $dir . '/administrator/manifests/files/joomla.xml'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('~([\d.\-_a-z]+)~smi', $tmp_content, $tmp_ver)) { return $tmp_ver[1]; } } return null; } /** * * Check what is Joomla * * @param string $dir * @return boolean */ private function isJoomla($dir) { if (file_exists($dir . '/libraries/joomla') || file_exists($dir . '/includes/joomla.php')) { return true; } return false; } /** * Tries to get a version of Joomla Platform. * * @param string $dir * @return string|null */ private function checkPlatformVersion(string $dir) { // for Joomla Platform $filepath = $dir . '/libraries/platform.php'; if (!file_exists($filepath) || !is_file($filepath)) { return null; } Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('~const\s+RELEASE\s+=\s+\'([\d.\-_a-z]+)\'~smi', $tmp_content, $tmp_ver)) { return $tmp_ver[1]; } return null; } }pluginsData = []; $this->pluginsFound = []; $foldePath = $splFileInfo->getPathname(); $this->searchInComponentFolder($foldePath . '/administrator/components'); $this->searchInManifest($foldePath . '/administrator/manifests/packages'); $this->searchInPluginFolder($foldePath . '/plugins'); if (!$this->pluginsData) { return null; } return array_map(function ($item) use ($splFileInfo) { return new DetectionEvent($this, $splFileInfo, $item[0], $item[1]); }, $this->pluginsData); } private function searchInComponentFolder($componentsFolder) { if (!file_exists($componentsFolder)) { return; } Stats::incrementCountDirsOpened(); $dir = dir($componentsFolder); while (false !== ($entry = $dir->read())) { Stats::incrementCountFilesIteration(); if ($entry == '.' || $entry == '..') { continue; } if (substr($entry, 0, 4) != 'com_') { continue; } $componentFolder = $componentsFolder . '/' . $entry; if (!is_dir($componentFolder)) { continue; } $pluginName = substr($entry, 4); $pluginMetaFilepath = $componentFolder . '/' . $pluginName . '.xml'; if (!file_exists($pluginMetaFilepath)) { $pluginName = $entry; $pluginMetaFilepath = $componentFolder . '/' . $entry . '.xml'; if (!file_exists($pluginMetaFilepath)) { continue; } } if (!is_file($pluginMetaFilepath)) { continue; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($pluginMetaFilepath, 8192); $version = $this->getVersionFromXMLContent($content); $this->addDetector($pluginName, $version); } $dir->close(); } private function searchInManifest($manifestFolder) { if (!file_exists($manifestFolder)) { return; } Stats::incrementCountDirsOpened(); $dir = dir($manifestFolder); while (false !== ($entry = $dir->read())) { if ($entry == '.' || $entry == '..') { continue; } $filepath = $manifestFolder . '/' . $entry; if (!is_file($filepath) || strtolower(substr($filepath, -4)) != '.xml') { continue; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($filepath, 8192); $pluginName = $this->getPluginNameFromXMLContent($content); if (!$pluginName) { continue; } $version = $this->getVersionFromXMLContent($content); $this->addDetector($pluginName, $version); } $dir->close(); } private function searchInPluginFolder($pluginsFolder) { if (!file_exists($pluginsFolder)) { return; } Stats::incrementCountDirsOpened(); $dir = dir($pluginsFolder); while (false !== ($entry = $dir->read())) { if ($entry == '.' || $entry == '..') { continue; } $filepath = $pluginsFolder . '/' . $entry; if (!is_dir($filepath)) { continue; } $xml_filepath = $filepath . '/' . $entry . '.xml'; if ($this->detectPluginFromXml($entry, $xml_filepath)) { continue; } Stats::incrementCountDirsOpened(); $subDir = dir($filepath); while (false !== ($subEntry = $subDir->read())) { if ($subEntry == '.' || $subEntry == '..') { continue; } $subFilepath = $filepath . '/' . $subEntry; if (!is_dir($subFilepath)) { continue; } $xmlFilepath = $subFilepath . '/' . $subEntry . '.xml'; $this->detectPluginFromXml($subEntry, $xmlFilepath, $entry); } $subDir->close(); } $dir->close(); } private function detectPluginFromXml($name, $xmlFilepath, $subDir = '') { if (!file_exists($xmlFilepath) || !is_file($xmlFilepath)) { return false; } if ($subDir != '') { $name = $subDir . '_' . $name; } Stats::incrementCountFilesReaded(); $content = Helper::getPartOfFile($xmlFilepath, 8192); $version = $this->getVersionFromXMLContent($content); $this->addDetector($name, $version, $subDir); return true; } private function getVersionFromXMLContent($content) { $version = DetectorInterface::UNKNOWN_VERSION; if (preg_match('~]*>(.*?)~is', $content, $m)) { $version = $m[1]; } return $version; } private function getPluginNameFromXMLContent($content) { if (preg_match('~(.*?)~is', $content, $m)) { return $m[1]; } return ''; } private function addDetector($pluginName, $version, $subPluginName = '') { $pluginName = preg_replace('~[^a-zA-Z0-9\-]+~', '_', $pluginName); if (substr($pluginName, 0, 4) == 'com_') { $pluginName = substr($pluginName, 4); } $pluginNameWithoutPrefix = $pluginName; if ($subPluginName) { $pluginPrefix = $subPluginName . '_'; if (substr($pluginName, 0, strlen($pluginPrefix)) == $pluginPrefix) { $pluginNameWithoutPrefix = substr($pluginName, strlen($pluginPrefix)); } } if (empty($subPluginName)) { $this->pluginsFound[$pluginName] = 1; } elseif(isset($this->pluginsFound[$subPluginName])) { return; } elseif(isset($this->pluginsFound[$pluginNameWithoutPrefix])) { return; } $key = $pluginName . $version; if (isset($this->plugins_data[$key])) { return; } $this->pluginsData[$key] = [$this->prefix . $pluginName, $version]; } }getPathname(); if (!$this->isDrupalBase($dir)) { return null; } $plugins_data = []; $plugins = array_merge( $this->drupalPluginList($dir . '/modules'), $this->drupalPluginList($dir . '/core/modules'), $this->drupalPluginList($dir . '/sites/all/modules') ); foreach ($plugins as $plugin => $data) { $plugins_data[] = new DetectionEvent($this, $splFileInfo, $this->prefix . $plugin, $data['Version']); } return $plugins_data; } /** * * Check what is Drupal * * @param string $dir * @return boolean */ private function isDrupalBase($dir) { if (is_dir($dir . '/modules')) { return true; } return false; } private function drupalPluginList($dir) { $drupal_plugins = []; $plugin_root = $dir; if (!is_dir($plugin_root)) { return $drupal_plugins; } // Files in modules directory. Stats::incrementCountDirsOpened(); $plugins_dir = @opendir($plugin_root); $plugin_files = []; if ($plugins_dir) { $plugins_dir_file_counter = 0; while ($plugins_dir_file_counter <= AbstractDetector::FILES_PER_DIR_LIMIT && ($file = readdir($plugins_dir)) !== false) { Stats::incrementCountFilesIteration(); $plugins_dir_file_counter++; if (substr($file, 0, 1) == '.') { continue; } $plugins_subdirpath = $plugin_root . '/' . $file; if (is_dir($plugins_subdirpath)) { Stats::incrementCountDirsOpened(); $plugins_subdir = @opendir($plugins_subdirpath); if ($plugins_subdir) { $plugins_subdir_file_counter = 0; while ($plugins_subdir_file_counter <= AbstractDetector::FILES_PER_DIR_LIMIT && ($subfile = readdir($plugins_subdir)) !== false) { Stats::incrementCountFilesIteration(); $plugins_subdir_file_counter++; if (substr($subfile, 0, 1) == '.') { continue; } if (substr($subfile, -5) == '.info') { $plugin_files[] = "$file/$subfile"; } elseif (substr($subfile, -9) == '.info.yml') { $plugin_files[] = "$file/$subfile"; } } closedir($plugins_subdir); } } else { if (substr($file, -7) == '.module') { $plugin_files[] = $file; } } } closedir($plugins_dir); } if (empty($plugin_files)) { return $drupal_plugins; } foreach ($plugin_files as $plugin_file) { $filepath = $plugin_root . '/' . $plugin_file; if (!is_readable($filepath) || !is_file($filepath)) { continue; } $plugin_data = $this->getFileData($filepath); if (empty($plugin_data['Name'])) { continue; } elseif (empty($plugin_data['Version'])) { $plugin_data['Version'] = DetectorInterface::UNKNOWN_VERSION; } $package = $plugin_data['Name']; $drupal_plugins[$package] = $plugin_data; } return $drupal_plugins; } private function getFileData($file) { $plugin_name = $this->getPluginName($file); $re = '@^\s* # Start at the beginning of a line, ignoring leading whitespace ((?: [^=:#;\[\]]| # Key names cannot contain equal signs, semi-colons or square brackets, \[[^\[\]]*\] # unless they are balanced and not nested )+?) \s*[=:]\s* # Key/value pairs are separated by equal signs (ignoring white-space) (?: "((?:[^"]|(?<=\\\\\\\\)")*)"| # Double-quoted string, which may contain slash-escaped quotes/slashes \'((?:[^\']|(?<=\\\\\\\\)\')*)\'| # Single-quoted string, which may contain slash-escaped quotes/slashes ([^\r\n]*?) # Non-quoted string )\s*$ # Stop at the next end of a line, ignoring trailing whitespace @msx'; if (substr($file, -7) == '.module') { return [ 'Name' => $plugin_name, 'Version' => DetectorInterface::UNKNOWN_VERSION ]; } else { $info = []; Stats::incrementCountFilesReaded(); $file_data = Helper::getPartOfFile($file, 8192); preg_match_all($re, $file_data, $matches, PREG_SET_ORDER); foreach ($matches as $prop) { if (isset($prop[2])) { $info[$prop[1]] = $prop[2]; } if (isset($prop[3])) { $info[$prop[1]] = $prop[3]; } if (isset($prop[4])) { $info[$prop[1]] = $prop[4]; } } if (!isset($info['version'])) { $info['version'] = DetectorInterface::UNKNOWN_VERSION; } return [ 'Name' => $plugin_name, 'Version' => $info['version'] ]; } } private function getPluginName($file) { if (substr($file, -7) == '.module') { $name = explode('/', substr($file, 0, -7)); return array_pop($name); } else { $name = explode('/', $file); return $name[sizeof($name) - 2]; } } } getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); if ($this->isIPB1x2x($dir)) { $detected = true; $tmp_ver = Helper::tryGetVersion($dir . '/index.php', 8192, '~var\s*\$version\s*=\s*["\']([^\'"]+)["\'];~'); if ($tmp_ver !== '') { $version = str_replace('v', '', $tmp_ver[1]); $version = explode(' ', $version); $version = $version[0]; $detectionEvent->setVersion($version); } } else if ($this->isIPB3x($dir)) { $detected = true; $tmp_ver = Helper::tryGetVersion($dir . '/admin/applications/core/xml/versions.xml', 100, '~(?:\s*\s*([^<]+)\s*\d+\s*\s*)+~msi', -100); if ($tmp_ver !== '') { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } } else if ($this->isIPB4x($dir)) { $detected = true; $tmp_ver = Helper::tryGetVersion($dir . '/applications/core/data/versions.json', 100, '~(?:"\d+":\s*"([^"]+)",?\s*)+~msi', -100); if ($tmp_ver !== '') { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } } return $detected ? $detectionEvent : null; } /** * Check is IPB 1x-2x * * @param string $dir * @return boolean */ private function isIPB1x2x($dir) { if (file_exists($dir . '/ipchat.php') && file_exists($dir . '/modules/ipb_member_sync.php') && file_exists($dir . '/sources') ) { return true; } return false; } /** * Check is IPB 3x * * @param string $dir * @return boolean */ private function isIPB3x($dir) { if (file_exists($dir . '/initdata.php') && file_exists($dir . '/interface/ips.php') && file_exists($dir . '/hooks') ) { return true; } return false; } /** * Check is IPB 4x * * @param string $dir * @return boolean */ private function isIPB4x($dir) { if (file_exists($dir . '/init.php') && file_exists($dir . '/applications/core/interface') && file_exists($dir . '/system') ) { return true; } return false; } }getPathname(); if (!$this->isPHPBB3($dir)) { return null; } $result = []; $cmsVersion = $this->checkPHPBB3Version($dir); if ($cmsVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::PHPBB3_CMS_DETECTOR_NAME, $cmsVersion); } return empty($result) ? new DetectionEvent($this, $splFileInfo,self::PHPBB3_CMS_DETECTOR_NAME, DetectorInterface::UNKNOWN_VERSION) : $result; } /** * Tries to get a version of PHPBB3 CMS. * * @param $dir * @return string|null */ private function checkPHPBB3Version($dir) { $tmp_ver = Helper::tryGetVersion($dir . '/install/schemas/schema_data.sql', 32000, '~INSERT\s*INTO\s*phpbb_config\s*\(config_name,\s*config_value\)\s*VALUES\s*\(\'version\',\s*\'([^\']+)\'\);~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } $tmp_ver = Helper::tryGetVersion($dir . '/styles/prosilver/style.cfg', 2048, '~\b(?:phpbb_)?version\s*=\s*([^\s]+)~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } $tmp_ver = Helper::tryGetVersion($dir . '/styles/prosilver/composer.json', 2048, '~"phpbb-version":\s*"([^"]+)",~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } return null; } /** * * Check what is PHPBB3 * * @param string $dir * @return boolean */ private function isPHPBB3($dir) { if (file_exists($dir . '/includes/ucp/info/ucp_main.php') && file_exists($dir . '/adm/style/acp_bbcodes.html') && file_exists($dir . '/ucp.php')) { return true; } return false; } }getPathname(); if (!$this->isModx($dir)) { return null; } $result = []; $cmsVersion = $this->checkModxEvoVersion($dir); if ($cmsVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::MODXEVO_CMS_DETECTOR_NAME, $cmsVersion); } $cmsVersion = $this->checkModxRevoVersion($dir); if ($cmsVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::MODXREVO_CMS_DETECTOR_NAME, $cmsVersion); } return empty($result) ? null : $result; } /** * Tries to get a version of ModxRevo CMS. * * @param $dir * @return string|null */ private function checkModxRevoVersion($dir) { $filepath = $dir . '/core/docs/version.inc.php'; $revo_re = '~\$\w+\[\'version\'\]=\s*\'(\d+)\';\s*(?://\s*[^\n]+)?\s*\$\w+\[\'major_version\'\]=\s*\'(\d+)\';\s*(?://\s*[^\n]+)?\s*\$\w+\[\'minor_version\'\]=\s*\'(\d+)\';\s*(?://\s*[^\n]+)?\s*\$\w+\[\'patch_level\'\]=\s*\'(\w+)\';~msi'; $tmp_ver = Helper::tryGetVersion($filepath, 2048, $revo_re); if ($tmp_ver !== '') { $version = $tmp_ver[1] . '.' . $tmp_ver[2] . '.' . $tmp_ver[3] . '-' . $tmp_ver[4]; return $version; } return null; } /** * Tries to get a version of ModxRevo CMS. * * @param $dir * @return string|null */ private function checkModxEvoVersion($dir) { $tmp_ver = Helper::tryGetVersion($dir . '/core/factory/version.php', 2048, '~\'version\'\s*=>\s*\'([^\']+)\',~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } $tmp_ver = Helper::tryGetVersion($dir . '/manager/includes/version.inc.php', 2048, '~version\s*=\s*\'([^\']+)\';\s*~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } return null; } /** * * Check what is Modx * * @param string $dir * @return boolean */ private function isModx($dir) { if (file_exists($dir . '/core/model/modx/modx.class.php') || file_exists($dir . '/manager/processors/save_tmplvars.processor.php')) { return true; } return false; } }getPathname(); $detected = false; $detectionEvent = new DetectionEvent($this, $splFileInfo, $this->detector_name, DetectorInterface::UNKNOWN_VERSION); if ($this->isMagento1x($dir)) { $detected = true; $filepath = $dir . '/app/Mage.php'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 8192); if (preg_match('~return\s*array\(\s*\'major\'\s*=>\s*\'(\d*)\',\s*\'minor\'\s*=>\s*\'(\d*)\',\s*\'revision\'\s*=>\s*\'(\d*)\',\s*\'patch\'\s*=>\s*\'(\d*)\',\s*\'stability\'\s*=>\s*\'(\d*)\',\s*\'number\'\s*=>\s*\'(\d*)\',\s*~ms', $tmp_content, $tmp_ver)) { $version = trim("{$tmp_ver[1]}.{$tmp_ver[2]}.{$tmp_ver[3]}" . ($tmp_ver[4] != '' ? ".{$tmp_ver[4]}" : "") . "-{$tmp_ver[5]}{$tmp_ver[6]}", '.-'); $detectionEvent->setVersion($version); } } } else if ($this->isMagento2x($dir)) { $detected = true; $filepath = $dir . '/composer.json'; if (file_exists($filepath) && is_file($filepath)) { Stats::incrementCountFilesReaded(); $tmp_content = Helper::getPartOfFile($filepath, 512); if (preg_match('~"version":\s*"([^"]+)",~ms', $tmp_content, $tmp_ver)) { $version = $tmp_ver[1]; $detectionEvent->setVersion($version); } } } return $detected ? $detectionEvent : null; } /** * * Check is Magento1x * * @param string $dir * @return boolean */ private function isMagento1x($dir) { if (file_exists($dir . '/app/Mage.php') && file_exists($dir . '/lib/Magento')) { return true; } return false; } /** * * Check is Magento2x * * @param string $dir * @return boolean */ private function isMagento2x($dir) { if (file_exists($dir . '/app/etc/di.xml') && file_exists($dir . '/bin/magento') && file_exists($dir . '/composer.json') ) { return true; } return false; } }getPathname(); if (!$this->isBitrix($dir)) { return null; } $result = []; $cmsVersion = $this->checkBitrixVersion($dir); if ($cmsVersion !== null) { $result[] = new DetectionEvent($this, $splFileInfo,self::BITRIX_CMS_DETECTOR_NAME, $cmsVersion); } return empty($result) ? new DetectionEvent($this, $splFileInfo,self::BITRIX_CMS_DETECTOR_NAME, DetectorInterface::UNKNOWN_VERSION) : $result; } /** * Tries to get a version of Bitrix CMS. * * @param $dir * @return string|null */ private function checkBitrixVersion($dir) { $tmp_ver = Helper::tryGetVersion($dir . '/bitrix/modules/main/classes/general/version.php', 1024, '~define\("SM_VERSION",\s*"([^"]+)"\);~msi'); if ($tmp_ver !== '') { return $tmp_ver[1]; } return null; } /** * * Check what is Bitrix * * @param string $dir * @return boolean */ private function isBitrix($dir) { if (file_exists($dir . '/bitrix/modules/main/start.php') && file_exists($dir . '/bitrix/modules/main/bx_root.php')) { return true; } return false; } }findAndExtractData($splFileInfo); if (is_null($data)) { return; } $result = []; if (is_array($data)) { $result = $data; } else { $result[] = $data; } foreach ($result as $notify_data) { if (!$notify_data instanceof DetectionEvent) { throw new AppException('Wrong class in list!'); } $publisher->update($notify_data); } } /** * Implements finding and extracting info(version and name). * * If something is found it returns AppVersionDetector\Core\Detector\Version\DetectionEventData. * If no luck it returns null. * * @param SplFileInfo $splFileInfo * @return DetectionEvent|array|null */ abstract protected function findAndExtractData(SplFileInfo $splFileInfo); }detectors[] = $detector; } }detectors as $detector) { $detector->perform($publisher, $splFileInfo); } } }setSender($sender); $this->name = $name; $this->version = $version; $this->type = $type; } /** * Returns name of an outdated component. * * @return string */ public function getName() { return $this->name; } /** * Returns version of an outdated component. * * @return string */ public function getVersion() { return $this->version; } /** * Returns type of an outdated event. * * @return string */ public function getType(): string { return $this->type; } }normalize($versionParser, $left); $right = $this->normalize($versionParser, $right); $value = $this->normalize($versionParser, $value); switch (true) { case Comparator::equalTo($value, $right): return self::EQUALS_TO_RIGHT; break; case Comparator::greaterThanOrEqualTo($value, $left) && Comparator::lessThan($value, $right): return self::IN_RANGE_BUT_LOWER_THAN_RIGHT; break; default: return self::NOT_IN_RANGE; } } /** * Normalize version. * Replacing "~^(\d)\.x~" need for Drupal plugins * Replacing words at the beginning and at the end of the version before and after the number. Example: beta3.2.6FREE => 3.2.6 * * @param VersionParser $versionParser * @param string $version * @return string */ private function normalize(VersionParser $versionParser, $version) { $version = preg_replace('~^(\d)\.x-~', '$1', $version); $version = preg_replace('~^(.*[\d.-]*\d)[a-z]*$~i', '$1', $version); $version = preg_replace('~^[a-z]*(\d[\d.-]*)$~i', '$1', $version); $version = preg_replace('~^\D+$~i', '0.0', $version); try { $version = $versionParser->normalize($version); } catch (\Exception $ex) { $version = ''; } return $version; } }publisher = $publisher; $this->db = $db; $this->comparisonStrategy = $comparisonStrategy; } /** * Notifies a listener about an event raised. * * @param AbstractEvent $event */ public function notify(AbstractEvent $event) { if ($event instanceof DetectionEvent) { $this->reactOnDetection($event); } } /** * Implements checking if a component is outdated. * * @param DetectionEvent $event */ private function reactOnDetection(DetectionEvent $event) { if ($event->getVersion() == DetectorInterface::UNKNOWN_VERSION || !$this->db->hasKey($event->getName())) { return; } $versionPairs = $this->db->getByKey($event->getName()); while ($pair = array_shift($versionPairs)) { switch ($this->comparisonStrategy->compare($pair[0], $pair[1], $event->getVersion())) { case ComparisonStrategyInterface::IN_RANGE_BUT_LOWER_THAN_RIGHT: $this->publisher->update( new OutdatedEvent($this, $event->getName(), $event->getVersion(), OutdatedEvent::TYPE_BRANCH) ); break; case ComparisonStrategyInterface::EQUALS_TO_RIGHT: if (!empty($versionPairs)) { $this->publisher->update( new OutdatedEvent( $this, $event->getName(), $event->getVersion(), OutdatedEvent::TYPE_PRODUCT ) ); } break; } } } }write("{$event->getName()} (ver: {$event->getVersion()}) has been detected at {$event->getPath()->getPathname()}"); } /** * Reacts on start-scanning event. * * @param ScanningStartedEvent $event */ protected function reactOnStartScanning(ScanningStartedEvent $event) { $this->write("Scanner version: {$event->getScannerVersion()}"); } /** * Reacts on start-dir-scanning event. * * @param ScanningDirStartedEvent $event */ protected function reactOnStartDirScanning(ScanningDirStartedEvent $event) { $this->write("Started scanning against {$event->getDirectory()->getPath()} at " . date('r')); } /** * Reacts on the end-dir-scanning event. * * @param ScanningDirEndedEvent $event */ protected function reactOnEndDirScanning(ScanningDirEndedEvent $event) { } /** * Reacts on an outdated software detected. * * @param OutdatedEvent $event */ protected function reactOnOutdatedDetected(OutdatedEvent $event) { $this->write("{$event->getName()} (ver: {$event->getVersion()}) is outdated (type: {$event->getType()})."); } /** * Writes a message to console. * * @param $message */ protected function write($message) { file_put_contents($this->targetFile, $message . PHP_EOL, FILE_APPEND); } /** * Reacts on the end-scanning event. * * @param ScanningEndedEvent $event */ protected function reactOnEndScanning(ScanningEndedEvent $event) { } } null, 'doc_root' => null, 'scanning_started_at' => null, 'detected_domain_apps' => [], ]; /** * @var string */ private $directoryDomain = ''; /** * Reacts on a detection event. * * @param DetectionEvent $event */ protected function reactOnDetection(DetectionEvent $event) { $id = $event->getName(); if (!isset($this->report['detected_domain_apps'][$id])) { $this->report['detected_domain_apps'][$id] = []; } $user = $event->getPath()->getOwner(); if (extension_loaded('posix')) { $user = posix_getpwuid($user)['name']; } $this->report['detected_domain_apps'][$id][] = [ 'ver' => $event->getVersion(), 'path' => $event->getPath()->getPathname(), 'user' => $user, 'domain' => $this->directoryDomain, ]; } /** * Reacts on start-scanning event. * * @param ScanningStartedEvent $event */ protected function reactOnStartScanning(ScanningStartedEvent $event) { $this->report['app_detector_version'] = $event->getScannerVersion(); $this->report['scanning_started_at'] = date('r'); } /** * Reacts on start-dir-scanning event. * * @param ScanningDirStartedEvent $event */ protected function reactOnStartDirScanning(ScanningDirStartedEvent $event) { $directory = $event->getDirectory(); $this->report['doc_root'] = $directory->getPath(); $this->directoryDomain = $directory->getDomain(); } /** * Reacts on the end-dir-scanning event. * * @param ScanningDirEndedEvent $event */ protected function reactOnEndDirScanning(ScanningDirEndedEvent $event) { } /** * Reacts on an outdated software detection event. * * @param OutdatedEvent $event */ protected function reactOnOutdatedDetected(OutdatedEvent $event) { $id = $event->getName(); $this->report['outdated'][$id][] = [ [$event->getType(), $event->getVersion()] ]; } /** * Reacts on the end-scanning event. * * @param ScanningEndedEvent $event */ protected function reactOnEndScanning(ScanningEndedEvent $event) { if (Stats::isAccumulateStats()) { $this->report['stats'] = [ 'memory_usage_start' => Stats::getMemoryUsageStart(), 'memory_usage_end' => Stats::getMemoryUsageEnd(), 'memory_usage_peak' => Stats::getMemoryUsagePeak(), 'count_file_readed' => Stats::getCountFilesReaded(), 'count_file_interation' => Stats::getCountFilesIteration(), 'count_dir_opened' => Stats::getCountDirsOpened(), ]; } file_put_contents($this->targetFile, json_encode($this->report)); } }iaid_token = $iaid_token; $this->timeout = $timeout; } /** * @param $data * @return object */ public function request($data) { $result = ''; $data = [ 'data' => [$data], ]; $json_data = json_encode($data); $headers = ['Content-Type: application/json']; if (isset($this->iaid_token)) { $headers[] = 'X-Auth: ' . $this->iaid_token; } try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, (isset($this->iaid_token) ? self::API_V2_URL : self::API_URL)); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data); $result = curl_exec($ch); curl_close($ch); } catch (\Exception $e) { } return @json_decode($result); } } [ WpPluginDetector::class => '', WpThemeDetector::class => '', ], DrupalCoreDetector::class => [ DrupalPluginDetector::class => '', ], JoomlaCoreDetector::class => [ JoomlaPluginDetector::class => '', ], ]; /** * @var int */ private $timestampStarted; /** * @var int */ private $timestampEnded; /** * SqliteDbReport constructor. * * @param SqliteDbReportConnection $connection */ public function __construct(SqliteDbReportConnection $connection) { $this->connection = $connection; } /** * Reacts on a detection event. * * @param DetectionEvent $event */ protected function reactOnDetection(DetectionEvent $event) { $senderClass = get_class($event->getSender()); $path = (string)$event->getPath(); $realPath = realpath($path); $filename = ($senderClass == CommonScriptDetector::class ? basename($path) : null); $this->data[] = [ 'appName' => $event->getName(), 'appVersion' => $event->getVersion(), 'appPath' => $path, 'appFilename' => $filename, 'appUID' => $event->getUID(), 'appRealPath' => $realPath, 'appSenderClass' => $senderClass, ]; } /** * Reacts on start-scanning event. * * @param ScanningStartedEvent $event */ protected function reactOnStartScanning(ScanningStartedEvent $event) { $this->scanId = $event->getScanId(); } /** * Reacts on start-dir-scanning event. * * @param ScanningDirStartedEvent $event */ protected function reactOnStartDirScanning(ScanningDirStartedEvent $event) { $this->timestampStarted = time(); $this->directory = $event->getDirectory(); $this->dir = $this->directory->getPath(); $this->domain = $this->directory->getDomain(); $this->uid = $this->directory->getOwner(); $this->data = []; } /** * Reacts on the end-dir-scanning event. * * @param ScanningDirEndedEvent $event */ protected function reactOnEndDirScanning(ScanningDirEndedEvent $event) { $this->timestampEnded = time(); $reportData = []; $parent = null; $lastParentIndex = null; $i = 0; foreach ($this->data as $app) { if ($this->isParent($app)) { $parent = $app; $reportData[$i] = $app; $lastParentIndex = $i; $i++; } elseif($parent && $this->isSubApp($parent, $app)) { $reportData[$lastParentIndex]['subApp'][] = $app; } else { $reportData[$i] = $app; $i++; } } $this->connection->insertDirData($this->scanId, $this->timestampStarted, $this->timestampEnded, $this->uid, $this->dir, $this->domain, $reportData); $this->connection->clearOutdatedData($this->dir); } /** * Reacts on an outdated software detected. * * @param OutdatedEvent $event */ protected function reactOnOutdatedDetected(OutdatedEvent $event) { } /** * Reacts on the end-scanning event. * * @param ScanningEndedEvent $event * * @throws \Exception */ protected function reactOnEndScanning(ScanningEndedEvent $event) { } /** * Is current app parent * * @param array $app * * @return bool */ private function isParent($app): bool { return isset($this->parenthood[$app['appSenderClass']]); } /** * Is current app child for $parentApp * * @param array $parentApp * @param array $subApp * * @return bool */ private function isSubApp($parentApp, $subApp): bool { if (!isset($this->parenthood[$parentApp['appSenderClass']])) { return false; } if (!isset($this->parenthood[$parentApp['appSenderClass']][$subApp['appSenderClass']])) { return false; } if (strpos($subApp['appPath'], $parentApp['appPath']) === 0) { return true; } return false; } }targetFile = $targetFile; } } '', 'scan_id' => null, 'uid' => null, 'timestamp' => null, 'apps' => [], ]; /** * @var RemoteStatsRequest */ private $request = null; /** * @var string */ private $domain = ''; /** * @var int */ private $folderCounter = 0; /** * @var string */ private $currentDirpath = ''; /** * RemoteStatsReport constructor. * * @param RemoteStatsRequest $request */ public function __construct($request) { $this->request = $request; } /** * Reacts on a detection event. * * @param DetectionEvent $event */ protected function reactOnDetection(DetectionEvent $event) { $id = $event->getName(); $this->report['apps'][] = [ 'name' => $id, 'version' => $event->getVersion(), 'path' => $event->getPath()->getPathname(), 'uid' => $event->getUID(), 'domain' => $this->domain, ]; $this->folderCounter++; } /** * Reacts on an outdated software detection event. * * @param OutdatedEvent $event */ protected function reactOnOutdatedDetected(OutdatedEvent $event) { } /** * Reacts on start-scanning event. * * @param ScanningStartedEvent $event */ protected function reactOnStartScanning(ScanningStartedEvent $event) { $this->report['system_id'] = $this->getSystemId(); $this->report['scan_id'] = $event->getScanId(); $this->report['timestamp'] = time(); $this->report['uid'] = getmyuid(); } /** * Reacts on start-dir-scanning event. * * @param ScanningDirStartedEvent $event */ protected function reactOnStartDirScanning(ScanningDirStartedEvent $event) { $this->folderCounter = 0; $this->domain = $event->getDirectory()->getDomain(); $this->currentDirpath = $event->getDirectory()->getPath(); } /** * Reacts on the end-dir-scanning event. * * @param ScanningDirEndedEvent $event */ protected function reactOnEndDirScanning(ScanningDirEndedEvent $event) { if ($this->folderCounter != 0) { return; } $dirpathOwner = file_exists($this->currentDirpath) ? @fileowner($this->currentDirpath) : null; $this->report['apps'][] = [ 'name' => 'unknown_app', 'version' => 'unknown_version', 'path' => $this->currentDirpath, 'uid' => $dirpathOwner, 'domain' => $this->domain, ]; } /** * Get system id from CloudLinux OS * * @return string */ public function getSystemId() { $config_filepath = '/etc/sysconfig/rhn/systemid'; $system_id = ''; if (!file_exists($config_filepath)) { return $system_id; } $content = @file_get_contents($config_filepath); if (preg_match('~\s*system_id\s*\s*\s*\s*([^<]+)\s*~is', $content, $m)) { $system_id = $m[1]; } return $system_id; } /** * Reacts on the end-scanning event. * * @param ScanningEndedEvent $event */ protected function reactOnEndScanning(ScanningEndedEvent $event) { $apps = array_chunk($this->report['apps'], self::MAX_RECORDS_PER_REQUEST); foreach($apps as $chunk) { $this->report['apps'] = $chunk; $this->request->request($this->report); } } } reactOnDetection($event); break; case $event instanceof OutdatedEvent: $this->reactOnOutdatedDetected($event); break; case $event instanceof ScanningStartedEvent: $this->reactOnStartScanning($event); break; case $event instanceof ScanningDirStartedEvent: $this->reactOnStartDirScanning($event); break; case $event instanceof ScanningDirEndedEvent: $this->reactOnEndDirScanning($event); break; case $event instanceof ScanningEndedEvent: $this->reactOnEndScanning($event); break; } } /** * Reacts on a detection event. * * @param DetectionEvent $event */ abstract protected function reactOnDetection(DetectionEvent $event); /** * Reacts on start-scanning event. * * @param ScanningStartedEvent $event */ abstract protected function reactOnStartScanning(ScanningStartedEvent $event); /** * Reacts on start-dir-scanning event. * * @param ScanningDirStartedEvent $event */ abstract protected function reactOnStartDirScanning(ScanningDirStartedEvent $event); /** * Reacts on an outdated software detected. * * @param OutdatedEvent $event */ abstract protected function reactOnOutdatedDetected(OutdatedEvent $event); /** * Reacts on the end-dir-scanning event. * * @param ScanningDirEndedEvent $event */ abstract protected function reactOnEndDirScanning(ScanningDirEndedEvent $event); /** * Reacts on the end-scanning event. * * @param ScanningEndedEvent $event */ abstract protected function reactOnEndScanning(ScanningEndedEvent $event); }SQLite format 3@ . !Dm!J%eindexi_branch_keybranchCREATE INDEX i_branch_key ON branch (app_id)ytablebranchbranchCREATE TABLE branch ( app_id INTEGER NOT NULL,first_version TEXT NOT NULL,last_version TEXT NOT NULL)Ccindexi_app_keyappCREATE UNIQUE INDEX i_app_key ON app (name)P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)h7tableappappCREATE TABLE app ( id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL) + FvY<mM. m K ,  d G .  t ] F+1drupal_plugin_date*1drupal_plugin_imce)Cdrupal_plugin_jquery_update(7drupal_plugin_webform'5drupal_plugin_entity&;drupal_plugin_libraries%3drupal_plugin_views$9drupal_plugin_pathauto#3drupal_plugin_token"5drupal_plugin_ctools!=joomla_plugin_jdownloads Ajoomla_plugin_smartslider37joomla_plugin_sigplus5joomla_plugin_akeeba'Ujoomla_plugin_system_modulesanywhere;joomla_plugin_arkeditor7joomla_plugin_j2store1joomla_plugin_acymAjoomla_plugin_jch_optimize Gjoomla_plugin_advancedmodules9wp_plugin_wpforms-lite=wp_plugin_wp-super-cache;wp_plugin_wordpress-seoEwp_plugin_wordpress-importer3wp_plugin_wordfence7wp_plugin_woocommerce7wp_plugin_updraftplusAwp_plugin_tinymce-advancedCwp_plugin_really-simple-ssl/wp_plugin_jetpack% Qwp_plugin_google-sitemap-generator+ ]wp_plugin_google-analytics-for-wordpress. cwp_plugin_google-analytics-dashboard-for-wp 3wp_plugin_elementor =wp_plugin_duplicate-post=wp_plugin_contact-form-7=wp_plugin_classic-editor$Owp_plugin_all-in-one-wp-migration Gwp_plugin_all-in-one-seo-pack/wp_plugin_akismet#drupal_core#joomla_core wp_core app+ + G / G ^ u    L - H e wZ= nN/ n1drupal_plugin_date+1drupal_plugin_imce*Cdrupal_plugin_jquery_update)7drupal_plugin_webform(5drupal_plugin_entity';drupal_plugin_libraries&3drupal_plugin_views%9drupal_plugin_pathauto$3drupal_plugin_token#5drupal_plugin_ctools"=joomla_plugin_jdownloads!Ajoomla_plugin_smartslider3 7joomla_plugin_sigplus5joomla_plugin_akeeba(Ujoomla_plugin_system_modulesanywhere;joomla_plugin_arkeditor7joomla_plugin_j2store1joomla_plugin_acymAjoomla_plugin_jch_optimize!Gjoomla_plugin_advancedmodules9wp_plugin_wpforms-lite=wp_plugin_wp-super-cache;wp_plugin_wordpress-seo Ewp_plugin_wordpress-importer3wp_plugin_wordfence7wp_plugin_woocommerce7wp_plugin_updraftplusAwp_plugin_tinymce-advancedCwp_plugin_really-simple-ssl/wp_plugin_jetpack&Qwp_plugin_google-sitemap-generator ,]wp_plugin_google-analytics-for-wordpress /cwp_plugin_google-analytics-dashboard-for-wp 3wp_plugin_elementor =wp_plugin_duplicate-post =wp_plugin_contact-form-7=wp_plugin_classic-editor%Owp_plugin_all-in-one-wp-migration!Gwp_plugin_all-in-one-seo-pack/wp_plugin_akismet#drupal_core#joomla_core  wp_core ^ o_O?/ziXG6% | j X G 6 %   v e T C 2 !  u d R A 0  } h R = (  ^+7.x-0.07.x-2.10]*8.x-0.08.x-1.7\)7.x-0.07.x-2.7[(8.x-0.08.x-5.5Z'7.x-0.07.x-1.9Y&7.x-0.07.x-2.5X%7.x-0.07.x-3.23W$8.x-0.08.x-1.5V#8.x-0.08.x-1.5U"7.x-0.07.x-1.15T"8.x-0.08.x-3.2S!0.0.03.2.65R 0.0.03.3.22Q0.0.01.5.0.277P0.0.06.6.1O0.0.07.8.2N0.0.02.6.10M0.0.03.3.11L0.0.06.5.1K0.0.05.4.3J0.0.07.12.3I0.0.01.5.6H0.0.01.7.0G0.0.012.4.0F0.0.00.6.4E0.0.07.4.0D0.0.03.7.1C0.0.01.16.17B0.0.05.2.1A0.0.03.2.6@0.0.07.8.0? 0.0.04.1.0> 0.0.07.9.0= 0.0.05.3.9< 0.0.02.7.5; 0.0.03.2.3:0.0.05.1.490.0.01.5.080.0.07.9.070.0.03.2.960.0.04.1.258.0.08.7.847.0.07.67.036.0.06.38.025.0.05.23.014.7.04.7.1104.6.04.6.11/4.5.04.5.8.4.4.04.4.3-4.3.04.3.2,4.2.04.2.0+4.1.04.1.0*4.0.04.0.0)3.0.03.9.12(2.5.02.5.28'1.7.01.7.5&1.6.01.6.6%1.5.01.5.26$1.0.01.0.15# 5.2.05.2.4" 5.1.05.1.3! 5.0.05.0.7  4.9.04.9.12 4.8.04.8.11 4.7.04.7.15 4.6.04.6.16 4.5.04.5.19 4.4.04.4.20 4.3.04.3.21 4.2.04.2.25 4.1.04.1.28 4.0.04.0.28 3.9.03.9.29 3.8.03.8.31 3.7.03.7.31 3.6.03.6.1 3.5.03.5.2 3.4.03.4.2 3.3.03.3.3 3.2.03.2.1 3.1.03.1.4  3.0.03.0.6  2.9.02.9.2  2.8.02.8.6  2.7.02.7.1  2.6.02.6.5 2.5.02.5.1 2.3.02.3.3 2.2.02.2.3 2.1.02.1.3 2.0.02.0.11 1.5.01.5.2 1.2.01.2.2 1.0.01.0.2 ^ zupkfa\WRLF@:4.(" ztnhb\VPJD>82,&  +^*])\(['Z&Y%X$W#V"U"T!S RQPONMLKJIHGFEDCBA@ ? > = < ;:9876543210/.-,+*)('&%$ # " !                                      { "wp_core": { "meta": { "source": "https://wordpress.org/download/releases/" }, "branches": [ ["1.0.0", "1.0.2"], ["1.2.0", "1.2.2"], ["1.5.0", "1.5.2"], ["2.0.0", "2.0.11"], ["2.1.0", "2.1.3"], ["2.2.0", "2.2.3"], ["2.3.0", "2.3.3"], ["2.5.0", "2.5.1"], ["2.6.0", "2.6.5"], ["2.7.0", "2.7.1"], ["2.8.0", "2.8.6"], ["2.9.0", "2.9.2"], ["3.0.0", "3.0.6"], ["3.1.0", "3.1.4"], ["3.2.0", "3.2.1"], ["3.3.0", "3.3.3"], ["3.4.0", "3.4.2"], ["3.5.0", "3.5.2"], ["3.6.0", "3.6.1"], ["3.7.0", "3.7.31"], ["3.8.0", "3.8.31"], ["3.9.0", "3.9.29"], ["4.0.0", "4.0.28"], ["4.1.0", "4.1.28"], ["4.2.0", "4.2.25"], ["4.3.0", "4.3.21"], ["4.4.0", "4.4.20"], ["4.5.0", "4.5.19"], ["4.6.0", "4.6.16"], ["4.7.0", "4.7.15"], ["4.8.0", "4.8.11"], ["4.9.0", "4.9.12"], ["5.0.0", "5.0.7"], ["5.1.0", "5.1.3"], ["5.2.0", "5.2.4"] ] }, "joomla_core": { "meta": { "source": "https://www.joomla.org/announcements/release-news.html", "top_plugins": "https://extensions.joomla.org/browse/top-rated/" }, "branches": [ ["1.0.0", "1.0.15"], ["1.5.0", "1.5.26"], ["1.6.0", "1.6.6"], ["1.7.0", "1.7.5"], ["2.5.0", "2.5.28"], ["3.0.0", "3.9.12"] ] }, "drupal_core": { "meta": { "source": "https://www.drupal.org/project/drupal/releases", "top_plugins": "https://www.drupal.org/project/project_module/?solrsort=iss_project_release_usage%20desc&f%5B4%5D=sm_field_project_type%3Afull" }, "branches": [ ["4.0.0", "4.0.0"], ["4.1.0", "4.1.0"], ["4.2.0", "4.2.0"], ["4.3.0", "4.3.2"], ["4.4.0", "4.4.3"], ["4.5.0", "4.5.8"], ["4.6.0", "4.6.11"], ["4.7.0", "4.7.11"], ["5.0.0", "5.23.0"], ["6.0.0", "6.38.0"], ["7.0.0", "7.67.0"], ["8.0.0", "8.7.8"] ] }, "wp_plugin_akismet": { "meta": { "source": "https://wordpress.org/plugins/akismet/" }, "branches": [ ["0.0.0", "4.1.2"] ] }, "wp_plugin_all-in-one-seo-pack": { "meta": { "source": "https://wordpress.org/plugins/all-in-one-seo-pack/" }, "branches": [ ["0.0.0", "3.2.9"] ] }, "wp_plugin_all-in-one-wp-migration": { "meta": { "source": "https://wordpress.org/plugins/all-in-one-wp-migration/" }, "branches": [ ["0.0.0", "7.9.0"] ] }, "wp_plugin_classic-editor": { "meta": { "source": "https://wordpress.org/plugins/classic-editor/" }, "branches": [ ["0.0.0", "1.5.0"] ] }, "wp_plugin_contact-form-7": { "meta": { "source": "https://wordpress.org/plugins/contact-form-7/" }, "branches": [ ["0.0.0", "5.1.4"] ] }, "wp_plugin_duplicate-post": { "meta": { "source": "https://wordpress.org/plugins/duplicate-post/" }, "branches": [ ["0.0.0", "3.2.3"] ] }, "wp_plugin_elementor": { "meta": { "source": "https://wordpress.org/plugins/elementor/" }, "branches": [ ["0.0.0", "2.7.5"] ] }, "wp_plugin_google-analytics-dashboard-for-wp": { "meta": { "source": "https://wordpress.org/plugins/google-analytics-dashboard-for-wp/" }, "branches": [ ["0.0.0", "5.3.9"] ] }, "wp_plugin_google-analytics-for-wordpress": { "meta": { "source": "https://wordpress.org/plugins/google-analytics-for-wordpress/" }, "branches": [ ["0.0.0", "7.9.0"] ] }, "wp_plugin_google-sitemap-generator": { "meta": { "source": "https://wordpress.org/plugins/google-sitemap-generator/" }, "branches": [ ["0.0.0", "4.1.0"] ] }, "wp_plugin_jetpack": { "meta": { "source": "https://wordpress.org/plugins/jetpack/" }, "branches": [ ["0.0.0", "7.8.0"] ] }, "wp_plugin_really-simple-ssl": { "meta": { "source": "https://wordpress.org/plugins/really-simple-ssl/" }, "branches": [ ["0.0.0", "3.2.6"] ] }, "wp_plugin_tinymce-advanced": { "meta": { "source": "https://wordpress.org/plugins/tinymce-advanced/" }, "branches": [ ["0.0.0", "5.2.1"] ] }, "wp_plugin_updraftplus": { "meta": { "source": "https://wordpress.org/plugins/updraftplus/" }, "branches": [ ["0.0.0", "1.16.17"] ] }, "wp_plugin_woocommerce": { "meta": { "source": "https://wordpress.org/plugins/woocommerce/" }, "branches": [ ["0.0.0", "3.7.1"] ] }, "wp_plugin_wordfence": { "meta": { "source": "https://wordpress.org/plugins/wordfence/" }, "branches": [ ["0.0.0", "7.4.0"] ] }, "wp_plugin_wordpress-importer": { "meta": { "source": "https://wordpress.org/plugins/wordpress-importer/" }, "branches": [ ["0.0.0", "0.6.4"] ] }, "wp_plugin_wordpress-seo": { "meta": { "source": "https://wordpress.org/plugins/wordpress-seo/" }, "branches": [ ["0.0.0", "12.4.0"] ] }, "wp_plugin_wp-super-cache": { "meta": { "source": "https://wordpress.org/plugins/wp-super-cache/" }, "branches": [ ["0.0.0", "1.7.0"] ] }, "wp_plugin_wpforms-lite": { "meta": { "source": "https://wordpress.org/plugins/wpforms-lite/" }, "branches": [ ["0.0.0", "1.5.6"] ] }, "joomla_plugin_advancedmodules": { "meta": { "source": "https://www.regularlabs.com/extensions/advancedmodulemanager" }, "branches": [ ["0.0.0", "7.12.3"] ] }, "joomla_plugin_jch_optimize": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/core-enhancements/performance/jch-optimize/" }, "branches": [ ["0.0.0", "5.4.3"] ] }, "joomla_plugin_acym": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/marketing/newsletter/acymailing-starter/" }, "branches": [ ["0.0.0", "6.5.1"] ] }, "joomla_plugin_j2store": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/e-commerce/shopping-cart/j2store/" }, "branches": [ ["0.0.0", "3.3.11"] ] }, "joomla_plugin_arkeditor": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/edition/editors/jck-editor/" }, "branches": [ ["0.0.0", "2.6.10"] ] }, "joomla_plugin_system_modulesanywhere": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/core-enhancements/coding-a-scripts-integration/modules-anywhere/" }, "branches": [ ["0.0.0", "7.8.2"] ] }, "joomla_plugin_akeeba": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/access-a-security/site-security/akeeba-backup/" }, "branches": [ ["0.0.0", "6.6.1"] ] }, "joomla_plugin_sigplus": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/photos-a-images/galleries/sigplus/" }, "branches": [ ["0.0.0", "1.5.0.277"] ] }, "joomla_plugin_smartslider3": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/photos-a-images/slideshow/smart-slider-2/" }, "branches": [ ["0.0.0", "3.3.22"] ] }, "joomla_plugin_jdownloads": { "meta": { "source": "https://extensions.joomla.org/extensions/extension/directory-a-documentation/downloads/jdownloads/" }, "branches": [ ["0.0.0", "3.2.65"] ] }, "drupal_plugin_ctools": { "meta": { "source": "https://www.drupal.org/project/ctools" }, "branches": [ ["8.x-0.0", "8.x-3.2"], ["7.x-0.0", "7.x-1.15"] ] }, "drupal_plugin_token": { "meta": { "source": "https://www.drupal.org/project/token" }, "branches": [ ["8.x-0.0", "8.x-1.5"] ] }, "drupal_plugin_pathauto": { "meta": { "source": "https://www.drupal.org/project/pathauto" }, "branches": [ ["8.x-0.0", "8.x-1.5"] ] }, "drupal_plugin_views": { "meta": { "source": "https://www.drupal.org/project/views" }, "branches": [ ["7.x-0.0", "7.x-3.23"] ] }, "drupal_plugin_libraries": { "meta": { "source": "https://www.drupal.org/project/libraries" }, "branches": [ ["7.x-0.0", "7.x-2.5"] ] }, "drupal_plugin_entity": { "meta": { "source": "https://www.drupal.org/project/entity" }, "branches": [ ["7.x-0.0", "7.x-1.9"] ] }, "drupal_plugin_webform": { "meta": { "source": "https://www.drupal.org/project/webform" }, "branches": [ ["8.x-0.0", "8.x-5.5"] ] }, "drupal_plugin_jquery_update": { "meta": { "source": "https://www.drupal.org/project/jquery_update" }, "branches": [ ["7.x-0.0", "7.x-2.7"] ] }, "drupal_plugin_imce": { "meta": { "source": "https://www.drupal.org/project/imce" }, "branches": [ ["8.x-0.0", "8.x-1.7"] ] }, "drupal_plugin_date": { "meta": { "source": "https://www.drupal.org/project/date" }, "branches": [ ["7.x-0.0", "7.x-2.10"] ] } } ux}/4t|\4S*GBMB