From 79a207a6f2fbc7ce335d5e0de28a30deabc57c94 Mon Sep 17 00:00:00 2001 From: Adrian Stephens Date: Fri, 3 Apr 2026 09:16:09 +0100 Subject: [PATCH] Privacy model added, plus some helper scripts --- .../calendar-plugin-0.1.15.manifest.sha256 | 20 +++ package/calendar-plugin-0.1.15.zip | Bin 0 -> 52177 bytes requirements/api.md | 13 +- requirements/authorization.md | 8 +- requirements/caldav.md | 8 + requirements/caldav_endpoints.md | 1 + requirements/data.md | 2 + requirements/data_schema.md | 6 + requirements/editor.md | 3 + requirements/ics.md | 12 +- requirements/ics_endpoint.md | 3 +- requirements/web-ui.md | 13 ++ scripts/deploy_remote.sh | 137 ++++++++++++++++++ scripts/package_plugin.sh | 71 +++++++++ 14 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 package/calendar-plugin-0.1.15.manifest.sha256 create mode 100644 package/calendar-plugin-0.1.15.zip create mode 100755 scripts/deploy_remote.sh create mode 100755 scripts/package_plugin.sh diff --git a/package/calendar-plugin-0.1.15.manifest.sha256 b/package/calendar-plugin-0.1.15.manifest.sha256 new file mode 100644 index 0000000..fb05654 --- /dev/null +++ b/package/calendar-plugin-0.1.15.manifest.sha256 @@ -0,0 +1,20 @@ +ca4806cd3e7827b9666e6929cbcae8f3e2eba87e499f7dd7008d8edf2fcc774e staging/calendar-plugin/calendar-plugin.php +1daa5861c0d10258c0d16c5c88c1a18fb3c8df7b590f7facfe7c28f678643bdb staging/calendar-plugin/src/Contracts/AuthAdapterInterface.php +25cff4b0fc2ee292b53c152edd083c6af66200a25eec28f8dfce37d126a8892d staging/calendar-plugin/src/Contracts/DatabaseAdapterInterface.php +4f0f4caa5ac98499854336f5b74af55ce889653f3956e5df10910f869a23fdba staging/calendar-plugin/src/Contracts/HttpAdapterInterface.php +15e8f58c7360d6cd0c76c945abfeb026f8278d40b330e99b67955eb2f85f5563 staging/calendar-plugin/src/Contracts/OptionsAdapterInterface.php +47d55e845b8a696c55fa86f597bb2760b6e35d974b70f365593d38697ef398c8 staging/calendar-plugin/src/Domain/CalDavService.php +4dc3337761c97aa550896fcc377aaab8338c598f39e489742dacbfd20e1a71b1 staging/calendar-plugin/src/Domain/EventService.php +412a22ecd910535c7ace2549a86eacf08cc9cd824f1767e60116d8593355f57f staging/calendar-plugin/src/Domain/IcsService.php +5f7fb1f8c00136ad2c4a2dc8b909dabf6494a194b09aa73557a4bf68d73f4ed2 staging/calendar-plugin/src/Domain/RecurrenceExpander.php +20ee26671fd934f36e97606cfd2ec1d5101ac8064594989bc955bf805fae2502 staging/calendar-plugin/src/Domain/SettingsService.php +9956790a5d62f5798f3ec15fc507fb04f33e01fd27969db09e91a6d1b41cd33f staging/calendar-plugin/src/Domain/UserService.php +5aaac066919b60461bde2e96cbfb4de66a5a28e89d2a61b78f0e2d346f23395b staging/calendar-plugin/src/Infrastructure/ServiceContainer.php +70334df8ca06c8fe61d81f24cb0d8f19285180f9950054bd86af0083adf8b4c8 staging/calendar-plugin/src/Infrastructure/WordPress/MigrationManager.php +8e6c95e9e1c051606e66d95cf0bcf92b2ca087bc491f32ab4921e0898cf77b81 staging/calendar-plugin/src/Infrastructure/WordPress/WordPressAuthAdapter.php +68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c staging/calendar-plugin/src/Infrastructure/WordPress/WordPressDatabaseAdapter.php +8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b staging/calendar-plugin/src/Infrastructure/WordPress/WordPressHttpAdapter.php +cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea staging/calendar-plugin/src/Infrastructure/WordPress/WordPressOptionsAdapter.php +ffc7c3eef8f3ed0873b5f614765219925d07c24ffd6ffa7422a3d862a7342450 staging/calendar-plugin/src/Plugin.php +4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 staging/calendar-plugin/src/bootstrap.php +893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c staging/calendar-plugin/uninstall.php diff --git a/package/calendar-plugin-0.1.15.zip b/package/calendar-plugin-0.1.15.zip new file mode 100644 index 0000000000000000000000000000000000000000..1e69edd1d3435e67dd679f24e2d68283bafd93d9 GIT binary patch literal 52177 zcmaI-Q><`Jz;1~y+qP}nwr$(CZQHi>F59+k+vsoa-RaIb`Om3~LT2izn^9w?QYi&# zU=S#P|5b(<{B{0!=l`w{0B`_|4Q)*AObnf99c*09E$!%4RUiR?`P8N?vCZc!T|J-y z071@y0RaAYrSM-8%71eq0Jfz3b>^=jyPg358~*p1_=#67{4$8k*wnn;@fy#u>xi_#WXsXr`+N2Fd zLRdbPX0DbfO@h@8>=4d(U1cBHA_i`1SZOtf1Nk?ow;0~59wj8hUjcES7H1Pgn%$74 zu)o(W(1hL=3JG)jwoHHUl($hZyQI+y)0}cgOyj9bMWHvQp$xY}bLs>UES>{HazOsI zq(eUF`q8f6T$y8|Be3;@p!az4cxk5uEbT71q-|?MlEo5*HXbL&k8;xbpUtA54H?@a ziMvDZse?Usim~D!<+h1?d$DB5@2G(M9*e(ZpSYrS8AU9FzfC%IY^IQqF-lHMJD5kB zTI(Tmp2Jn~Yfrd#uZ|w3ngnrEk5g7Z|7bRT;s0Nr5*Yn;Y_7>3Pyge|9s~dY_kZBY z)y~q+*~QSt=Kmq7U;Te1DK^>A{y&n!`o7e+%cSJ079!_Lg@M7URHQ_?VIc|UTI(xW z#~|C3xn#fYI2^NFwYbO0$^GtjyUytDcGItta^Ai`KJOE)S(3CfE~u2jsET8rbgVU; ziCH%V6>^b@6fNu+9e1e?GW+NkVt31gg_fa`kRz#0708~=M(fItE_@t0GaELJKc_sT zmQe5@LcrT6;U|a*GwITFgzw}j(}ywyIm7uZdtJPudz;E4&mm*CRZiA~h++yVhfF9< z&NUUiR5L=N+R)Y5dK&7`SHYTjM3$Hf+#^V3i=UdO$%_#cqCFc0y(*y27~x+Bc;tVp z!RFGjG;!6wpvK&QvclX~Q4x{XZgcNuuQgS4#p+uC-`wzphi+y>yCH-xSbOOt{6=+b z#|vh-Aa*+hk8pXpsTwG#tQR0mI#AbL-m`k0EdE^Qd>q5;%??sk@-B#=v-As7RSgrP zT@99BhjE9E!Kv8(x`e#*bNoJkZNZ|a;FGB_{KKm8k3Njx+Tj_GA6{8vlVgCriJpy| z4s;)7#q%)pjJ02{Af>hyNa9OOtd0KOuD}G4_NAMiI;u@_ zbhG1c#ftX;0H+@$8KCoc8Y?c660j6;xXgIs)B$6`Zp8j3kmIKL}6g0^AP9umxZ(aJY3}kQLk!W32}2dbmjl3_vsM zg*?Z@`&snBTB7_sjAn*`fIfzKm`DwU8<$lIQ1H+#`p~CJn1F1M;lO?diqP%kOpL^zia}SHiZ~Dz|j^+1S5HBZ$$97e`l5 zJ#4J3^wFclID+^9Ah%kqt<{eer<<#%pAq--o_BLtv}*O&#ULsfedKj5I;EA^ih3?l zi6+lzZWTp2rW2XC)!K5NVqHe5TWXhGnv_x`Wm;=PZ=6aGFcx_#+d|V!l@@2rB7e}3 z)frp84l*llYii%)7I6nH(8*;|AGpy|>oCE7|Q zB*fAUfCHju&>~II$R$ipHUI{efYJs;rGe&Ni|VBYtJ<2E`E>f01Em!|m7|2R?-NF+iKf!Tr>c)Dm~!?em(gRXKDo{lwM zdEjp_k9$4O0XfNqy-n)I1OX9B3T^jmFqc;{to``HBBzDV4~NQ}egq6JH8q!PhRlvH zJ_ySawy7_=Y?__QmC{DNN6$jEnP`^7Os%`|kne)ko8JiO2n}^ewXwi2&cq)rvsWapT~UqnLW4y%mkqS;QWtW<;Y7>r-xD?X zB)G9`j71EmU{hR}LISS4d`t>MjGs|3nub-hWMPR=HqIL*kqbQ~7W1G{2;8xSh|HShTp#!=Zb<~Y8zkUlsUzS6mwP;bt281?@__FC~ByFgta6G~sqM+16t|-}+ zqfiHGs`8>;)*gM=VxTc!0}_1bK?_^kqS7dZkS{W32!i*vivNTY)Fo4=nS#3Epp;6U-uP%n@Do zj*~B7$PD`(Z1$4BIb&;_>E#1S!&DDyI1xzzQ{&w#ZU6bB>8HF=MHai$V1HiMF?AL? z!j|gcbIh$=b0KgP;Y~hN=-?*>w8vSpJiM zT7v*fr<{J^lSr*h)L*7-5u4?Voso<%it<&kfM!5~tvZAd>ZCzF;m_N}Yte7{v>J0B zpDvwjsDBWptrEANN+HXOeSC%4xk;EqTz^$-88Zn%gzT?p%;2*p3(mo$JxFk; zO58UBtq$nDfHhFhGqyjO7{gcY5?m#c|6LejtN;i`uEb zD#qQm())>4$_{C4a%2mr$zVP&FJwxFxQ9V7WE-(r{gySmbOkvL3(J1ty=PtiYu+2T zuK8oulxmd)jR8nw5I|3E#wO>PI}m3fsoGV4QX?OIJ_QED*%62ZsDo4vSkE z)1NUJ1(+0eXM8Kt+k#_#_t!EVx2qiX*9#g*SanQJptcJrkTmawM#4O&|7ia4Vgg*A zV<#92QF5zC@TSJI9(}?cXC#TiLb_PK#M`hf+bG!sU8>o~54+aT9s3o}D$B#5%EmeC zBg!y-{Ez^onG%^X3$l=;GjUPFO13H_(p~c}^{J^tefptQnS-b3k$&XCUr8hP`n3Zl z+CRMi@<(H#swLbb3`onE(()?15^w1=H-GOG+f-fgM$*44Zvmuc|<-Qsxf zCH+}!wmO#Ky`q-TR5ZGtc2?FaeYpEd;cI`gHrKO0qbOnld^N1RJf{fSqv?loTVp)z(&OG^KgO}Pg);hvsZK2ZLaDGe>6WfWG=5OL9YRUB|s zS7mtT1q|eC=u=g=HUHTB$q7N|B>x3bEPK&P@;5ljo=3UX}G(FuiwcycdYe2fZ`Z= zs&$!4fo$=`oYPnfzolsU;!pq&&r^MQ^Pga0sQ2(LhKjA|Z_bD0Ube^@UbKF1J1Wzm zfH~g>XpVvJ++tH927JOwTMX{Ox)UY`>t~!KVF5IjUV_9}5yIC|r>a9a2TK8>8{kTU zDMu>-&xQ0Qx*<>#xRAf%q6PWX7IwWGkE}Z83#`_Kx+#CQxAWt)EL=4V_i|niFn=q9 zGzr>26BvJntQ91xXvCwPGNW6F6#WYie%>fFN7$>#VV49Yb830trz3|Z0~_tLZ0G|d zM-I~#Nz$>m_=J#KuX^#?u~ZmXACA`Zqa90HrvEsig!i(8=9BTP?1iJIJo+0ys4=

!-pmTqLSFaEs7$v>P2OU*cU>T!{J}m)tM1L|H~|*>wuvX@`bYsj?s63nt}$I>>Ll z{g1guJ{xp&7R$1_K`D`GKxy}$dAA_XOrzBXMjiCySb5C%v$l;7;H?MMIl1?WS#y@g zVR8jNCZjva3NB?XDP=U1`b~R`T&3H+oM=j7`bc4o$4Ka&_Dc$LxE3==)CN-O*NvqO z?)N@Xz4c8l(hSO+YFjj-o8A5`6E#qwX_vcw@inj&l5`vPfY+3!Jjd1^e?D4w&|o2t z#s2!!9CCdEMR#buM0c##0Ibgb<4FP4_OI{%G^1`na;DLYd}&4A-ji-|;-b*6dZAsp z7p^~$T2|;yyRx@`Z<3b0bnWkD{J8*^t!(p}Z*d8wy zfm0<;lx+`fW(38UKP`@A`BP2X1{tk(?NXVvvZHG=^h!ur*dIf^5B}+fVE?BVH)`BN z&r24_%n+p8$B41X!e$=k2prn#-<(bo%FFLkpziM8xx3Ez{F7bqkw50rYkk>;QtKW%2@2{t;5oQc&fRSxL$%pL$y*7h4V|g1WUGxq_ zeg@X}M;V8Ka5L{Q3>FS-+$&uOod5o`C1@h?^?~Jk;|8zlGy1}sVXS{osC@ zke{R`eRmK*gk>RGU|JmEab=94i<)7*c!WDiRCdQ zV@>b$@XUoV*@9x6HFJfyd9&lDJ`ixv$dHywD#WV9z$rg`%j$W~=p`_M5 z)`lO8dvyzEcMpFqCw7#5z#l?l05*1`wQE~1(2UBM?qPSo@BX7dtc`%ddwtnAFG)eS zmoT3*u(w3uqT?1X|JQm?@pW{#b72I~qIDMHFew;a+DO(X+GnoUJG^mL#5T`T?M-KiPSnIT!>p~`u z9N9dfc-HVjbVRdbROhe;dP~OSZhHlKGwF1-DelGvmsh&ox^;5~KIi0=9k2~Y zdaL!?Y?p4f{UCb{WsU0{q{5ua*Ir>cRm0oNowD3)R&>s^YJ-rQYCHqBuz_S9{>pq+ zklL7bovfaLOj|llI-11jE|9r;4JRb< zmKIM(;eiZ|vR&^tNBryQKDKvV3eu$Jv&5qvf=D9BgYl@twFA9J79MY+4})M-aD^!W zFbn4xD_5&n%S#vRe@trb?jF(Zq)j`tN58)okTIOB8BQ@bY7oq9K(Vn z=xaDlSx=*}Xz}cFQbDYH!s6bcIM`URoye!ZxUe+h)(DAbn@LQRYp)_EFmUbYMFn^P zJTL@-A`;Q&9w@AuxWwrjEge@+%cw+c7KzC&jS*|+^@Fa^*JN8@%JREmJ0K5ia;qOx zC*r0SO-Gsu-EIUIpe3Wt6y>wMW4fy%i%%7CrB*%mTYmaXQ3hx~?j-cjE;$209C2Ry z0Ph-uctB9!*#;Pj-H%T3D7lp!gaVz7TcXFmUXi_sl83vX0_-4v4vNC3vabICG)aj% zm31hN3W>tQRVtP|IAV*lTcE_ZsXWAqpR7CuRKhMWe@CFXbL zdW*|ouFT7Rw8EZ5p2;+|4k1^=%sO7&6iRhzQY=sQbVnPWg`_w5e-e;zPFogI zk`X6IFD6i5Rw|Yh-X@Pp;~KRILSn)jhl+yY_g$2=!E<3Gc&c?AQyy;3X7)C11gJ@!D$Wmt?@+`q0 z>^kSfy=1u_T4EqojIs3F3pg}P-<)w4gm&Zr-kqyUE{z!}n7$>XXD-&tmx|wa&>Bc1 z7V`EbGjA<1{&`iL{;}8FheGWd%N8b2S-~+PkS_!i~my3~z1^*M*m zhy2ZRM2Z_7Sn94izA{JD`X`EbI>`&*Ce*9FIX%Y@vXodR6e3>Yc3_&5n<&-^hI>1u zFck&ar#hABH4al}`>x*-5*K-&PAEA5foE~4XpwAzjZr2Jmg@6+qWGm#2LzxAG)h!B zOXga2gx~cN<6{lv!PPBWMX231gvs}b51#Jn1(2urO&&7ee{F9!U$)z$gMV+7Y&A$p ztfW$Mhb=7>_@YV1{;jmQl5$y8RKk(e*49Z=PLfkq#d~+%L!LuVBzrmlG<99lx;UJ$ z*;>bR3;j+1%V zvBbD1(HFe zZD1kd4mCk>EIJ_alO(^08b+ag3KH8cP?N}gqIVA*Rt^@Qk|L@WD^$m~sB*%uZTlz= zoUQ0$6#^S-T2_s42s!%qk27_Nweud6~W zybj_gBK20P$ITA3t%??{|Eqb#x;gKv2!p$E#2{wZU`oRH9H*6uN}v{#44{KG4Al{0p+`%) z3g3>r@bQW|YNQbfrW2PscPQ8}DQM-&*1J@yVGwftLjMU7YQt(}E6258!y^e^4R?my zxUe|jjy+9Xo!M@fW+DtrldyUQ2#>qhnFQv6UQBNc0A>rTU^PbAs#x?k0oMQ7Ntx=-_gI&m@|s-=l?;ncWE*N>8#9t(b_ z!5j-dMNTjR{4%MmNS#SI8truo3ri?`W@?(`Jk;QM6>x5Wr6?VGjJPAb4m$*b z@h>2!D+tna-tq$2su!YB`*6y_pI^y)v%2DuW$u)Jn`|$nr76iNU0TrlL&ApCWbbM> z-_V&a@oWd16s1XCYp5E0X8MFVy9j@_4M&+?jj*4%Sw6ouNHO`BinL^d~P{jb>l+UT#3L%B{ zRg$FFC5Wj6(uMJ^3CE|dw&$imkhrhbYp}be*jjlQjO)4HueOzJ(>=&5Ku<901-I|H z9zFHZXN>;!N)*oARq_<*yk+uF@-W@$gTMr2m6`j2dJf@M!5tQ@_`E;MxhKNfBGWT zBGhpjQ7}0WWrF4^dSZ;Rg>Vf%Wzqo5xh>|5D5u#}e2uUoxQ@qQr9VGR+N;X;E)0>@ zg5F!f+8Pi=zM`Ijdp~rp{w?3Wv>0-lTmga=7HSf`1*GXVy;Vs$J7!O{XJ0U9I{Bcc zTJo*)0(OLrsv)VDwR(cK~sDmww(OSJW*~MYw!ymk>UhTc@_V~K#A+TLxv<3l0 zi=z}bYYk1_>THVI_Y-CjT2tC_u#82st*?qvGqQZi<#Po6a4ITcEUOO?}Vfj=cp#v6EvQ zuoAH;H1)}E6(eA}5^sMYNN_3?Z1X;~P~E%L6TPEPM;Ttf3XMx;@iHiOa(=d@nhK~7 zN_E*b5H*d&hn`N?fI&HM5wF)7y-=G>^KgS<=N_zNwE%g0XJxGTg&sd1kH;aQLUj6w zsO1&-LqhB|zGX%Lm+)3g97t<{;^q<@=o-s>0%bMc>J15vuisYmVnefCkp5%4pS0z@ z;^Tw8)*>Nh`VoFrt9s_!MQ|4=7Cn21$mE3%(X(Zp5M>h@@cG^1rVa^LZL2R!(7-t<>Ak6IH(YCh>-t3aHW~Q!DVbAMZ(+SoM z+g@-Lsz#zcYtR$;^aK#>p^jDeI=Z$ie`K%s!fKq0Tbnt-wFguClb{Ql;e)`gC-|WF z{%L{zA%E^SNVdOaHCr9kOwDYs|L&Y$yR4;*sj!mqPOB?8xx*i=+U-cWAf(wVd~BP9 zs-JOxRro;@_^?eHe^>9sT`K_u+yHWzg!BZylP_ZK^XULaBJwdz$3ee!wNF#`#;3J2 zDwgIT@3_kfi9kb^qe(`}bR6E=82!0d9o7qqZlDEfKs|@?$JDQfE zjw5$!0gnLZsGW3vH41Vyw&X6TxwffU=a+OMDBz`X*PUE(6$jDrV{rC7r5@^RfLwAw z_L%ZLU?APRl=8+R_Bx_?kGEV~c3LD3dM!%Ka_h9LDWc8G#;NmO6Shf-^d_Sb;rc?z zxfWyft{tqQ*0(!ND4($5GCDk&*6A^OF_BV8Ui!UO|edXZ};;29#xO#t5t^D{i;6KMo>3vq7#FH z_~l8y8v~G8x-$8n6N~+w>hlus1-vx~8z*?Hzc>CrZSVc=k%(L~xYdY9=2zh%GAJDT zAhY+{Cld|}7-Ze&h2FESV!3m-{fb5oL5_>bX-hIwtYM0i6NEfGao7Nt=hu!Ye+-Zd z`Icr4nnHd)P~TChE<-HrrRZD58-utp9lb@v^dZi?hz~&F3G47P=jjnvS%zjcf3@Tf z2bjD8OfH;6eGeVp1v0)of;CqrFmFAT$@NX~ZrGpqhPX3Wv3^%1@(uIuisFLV zLm0k*Kb2B<4u^@^S%$AdO($X9v>_wa?G(z9n)s0d)0hZwNk^{O|q;HM}s;4Rsu~Ds_{`JkC%&&Yi#K56g#{?{;kCG+!7i67+d5&1c3|^E`c3zV)mrJ`p)ely!rhzOwv0=tnGew%|u<$#Ko`NHNxa|E+ zQNlRC&bNY2kKUQf-sQ-DmQ36reT1LCCbZ05Pp;}|>B#jbV@rw|^R8?tg>b)_OB<;- z^inS0D=uzXKJ}0*PlkX#!k;!Q);};k>7UIB*0<(r|NEmel8LL+)@`F^x}t73n?Nwn zXSlJKS)ki5?hf=7&~BB4=WA~P@NepkocDc7Yz2%Tx#Kd2O<4M32hqltJ!!W4@sca~ z`R)quy?DB2Cj%dy`Ap_WXD(4VBboAsGMOu!f=uS{Wu|RP789c%H{Fi5 zTXMyG9fb|xhd;c-G7z`}A20;9hO6NW5OLRF@;k75Q_ZblTk|ushST@Csf5S>BQ?!X zSlh%BGaF!E)Rvvbbv+d-tK-gwR&TkAob}{zi^h0Q*WcnZdObYBGg|VEefjRj)op{) ztpl@t)Ys5I;rr}bRySC|jg9j?4Y=xvbxd5s@~6ldv7f}f3<=^wuLy4kq)6*q=7H3q zJ4OJbqPj;<@qnT659G?S#?gP;{GRB4o}w28E5*sPvqVorYR*X1R3$G7XqMgaUOuK5DwUUU4~7!-8fexUR&QpL0XeE%o|y%nR&Us-}6z_Odl3GMP*dXv)`5=E+*v}@Htox z%99!u3D2XJcH4P{yhP36X6}KY;RBtdppo!2c3J3Q9^W#N^nzivJ?-XHLD-i`iS4OY zFD4HiQ#dhY5M%vdaA>KyDorx+0cSj1|4X}$!-MJt0AExmBVXqPuIMKv32(%Ri7!H} z{%}~)86X^DzuZ#95o(rRGVC@mj~{J zOogzIUcIw0y7S>-~DE8hud(xRrOK3x!?ufz;rtP^)wnaho&i5VGI z(=?c=Z01ldCbPL8Ig?>y3;j^BNxqCRX>pgbD-GACtbFZ<6#rI@R4oZm2I=zbc)3EjwSOj0VtE-PWkb%5Z2NLc@n6Zn}9b>=jHgHdk)PwP%- zY^2#aBRYcTa)yq|E_L*NW=Xh<%@awACW+G26Tb7CNa}6!KE@oXeK{W(x zC-cJgXX>u)KdxD)CB|+ZNkQzKQ;DCUBlD9`+55O9EkLlsbTO2MnFl|2ZN(QeSQWb6 zi4u{Mf8`>uTDu6GBVpS`E_|qrkfbLc?ju`&L(wNOia|)83^s@~k}fO7v^ZX$YA$Uq zuc7;ocG{$B!2Z-3p=rtO<#EMVe03(KluXyeQ;7qI?5Ld@k$`fFh{_8yF>|qvelrnK zdpZ-Y01v>8m6Dj*Z(WEd>ZM`S2s?Dr#~xz7Q##J6`C?>Tr za0;S(-)8N;?M{4Un6sOqgz;+yRcx?(bcLm$5??fCw7V#^@)PF`h$(3E;da2 zrvpdAZ0;?`Y-lAM*aOCH0eko;`2 zjc2gS1MoU6(tHgYgN|r`7p)zhO^vT9ZPQx-jeq z<0%11tqcNO)`kK9nB6hTY32_Ob%-$ir;`D-LJ8_hF)D0se6+$rW+C4k-&_Lqing4rMQeCDHC9!ZYzF6ViMKnLk7$1N|~AeFX~K5@^| z14BuO1|!pK=wltqJ644dKA68U(yeTnBX-{T)36_9485)e{2wMv*7VH|5tO(J2WGFu z1^?_Rq4`)i9I~Ik-!p&9$VtFYn$hUx8X5K&x}p@#44GNRD!?{f-E7NtX4`P2n8p%dxYCO<`*zQ^<@3RdXy=*PhK(NCCGUaDhYJe$5GR z1s3Tl7hljbaInqnXYh|%f-D4FLeFfB$VVo; zIYk`bbTVLTD9NmpRxg}Ikaav5FX9RiU2o)}xMSRZ=|7mPva@=7NHz?c#B3IgcTH=> zzZe3qN=9RuKdVNKt5?K_Pyq^PE0C1n3=tD}ad#T2wtQ^jL5)6v11Rho{;^&TrQy)A z)}FV32iUl*uI9zW+p1n2>*q}H5%K))Tate>95ZGG@^trBjfrqU0X|17S^ph!>rBll zo9E{H!|VmQL$UsQT{6sxH!TFak<$qvtObOX0Z6`#V)7w!G463~!LckWAw3x7$WVMU zgwj^nk&))3z>6msKU*6Vg&A)VT=;s9DP%R6;O(ld#gO$VwW(%r)6jckXz9?z9y^!x z*2zXq1t(`Q{=Lh7DI8K$bylDjlZU}g%eg|!>lDdzdsxV5wi$Pp3~4i#1+~PzzVYX0 z{#Vi4_q12<4hQE=?+%xLV+SpI+LbFz!ZL^u_V7;wtNeVKt>q0bji>5unFUe&TyY`+ zO`aaU!2QAnt(di66Br#x=#C~b9QYifVx(7SaC@qjv;3vdTTL0Dj8J^Q3S`%x&Ma1@ z3mfJUH4Ra;)s#eR3{6xZP2E^InW&zrNQ$JserU<{9H|3yAj?6N4gYX}si zZJ}&E8G3#+axgk1Qeyu;3hxxm7+Ogq9z79*P#L2yEOpkryH!~L;031r)0t_SqudI27sS}I>V2@4}uz_su`H&H5CLjRVJ<1%ap?;ZhcSc#i zx;W5u#dvX|kymcC;0Q>6pgLx(zY;18fnu826MHOq-XabdrYuNf(5IVlf_b=Vm@>el zzw6oWZ&1aWb zaKZ1TE$+%vd&lOWZt-nqlQms*R)K^OTn#Gra?xQWdRk*oWa&6y9O6KHhd+}A&Ro`7 zE7~((!VT%R&cSZ+C-GZ1LXe1mw8L?E^WrJA0^;W4&d&?{uYRX~e91~V1*5iCkosJU zsZI9mBC^}@22+4$Jz_#^qOgZ6P(2d?ckL+{mN;ob8&D6ho{l!Kez`;j#1VT}>yrsX z#8@i?X^7p-R;d=rkx{YRxM@`L3svq06b7v(8BHHFd3}Uj#)s)LE)>w*P%Nz=8R!bB$+7fHSM z|3IgAV?We2LWfQ&bz<()Ia@dL`?O9#bipd?&v6F4l+iIaLyFWV`*cw;!Z|{Eh{`g!G zD6Klj9BA4rpTJ6Ow(()xK#+y0+khBArkyH>S_OR=GMLK3p@xioAW%IC4?G?V-OZUP z^WUIVX$>PK*ead!Nin$lssz5a#%!wF9=-fVp9A!YcK@jh#Z}SX6S@A|8V)VSKRwEq znExal$L@M==TB*Z!zDA(44=uwILl})@;I51YKaq+uu&Xe-FhbWO74zkeh|w8Oj2$H zx)w=6X5?qj2i8^86%%c3GX$Y4!o~P+N2zBaqi1E{^zr#*E2~yGQlq5J5yY`(nWuaX zje`t)08jr@cCh!>iV&t{Mik7?aM6g;Gl;}c7?wGgx@3L0Q!S5b?8MJ6D@OdpK&YRf znMju)n!r7lS*Np&5!1#njajg(2rbM9TDqHD5h&w(&yfSUbVax|)EK2;tQ?&{yP3(Y9*_!A7D! z$OtGk0Wn~EX-ZpRi?>=!po|=w7HXlgHQ#N%0LSm*NbTo+z74KSutgjxSw+5ZMdDi@ z;UjA>MtolA*w%w+TPmETUkj3mrCEd7{R(Fvj8U8mDa^8BVb^Ob_0L{2La)JpQhWi8 zze+5Vj^tmum#B2OlA-|$NAs|B3B|Wv-4fdY)Ft&h;iM_%Vo0uZE<~=sYIwcH`dq<1 z1k`x&xRd}|Y&p<9(=>tI-n7}La>lMDSLXOrwt~`YfJd_1`%}0JSHbzO65Fy_qO#e! zE4081GL$iT$kazP$`IP!JG{e}$#TBlWacuiSkydhx}xn7fS=f+T;r~p0CwV5;$_cx zz#cI={e}2#exP1~xsf=8Fn&FJ9CHoYgtI*>A|lCb1G^tmT(!m12(;V6{GtCNGKE(E z&@i_~2vIU)=g_i(gsxa{sfxWXe_?DG!W$nY#` z|9K?ATwY8=x~0bE$NTBDpJd8 z3xbs&K{3;PTi_d&IOQ}%nL@#MIr6Z8pmyH|0T^Cx1Xcysus~`E?P=Q(Mhx7?gDgSA z_ETV#H0$LXlA&_5ydeck)aF&*`23+^G!JCn!YQ#^5Ufa0g98ny;B1fNz4QmEsIHDe z)0XHgTNp^};=e8_KYx%|%o%~SocJNT2vHtv5T#Pl>f81EVtEbN;C1OeFo!Wz>Y`ib zfTt@W8HmW}!|D`2{dndPhUe0&(*3VHZ6=P81MNl47v2Y2OWmqN?Zb-)W#%ZfmU=A3 zkVAlcKG+XHKE^Ff0Jm4Z@W9#lOTymBF=Vr+X_IvYJ(H#DhpYM543q32)QDnNkSGjd z8KN4S^6xhyeBzWp8a!FEbl7IyruG+;)lofvG#-D_421j_QvUeM7~wB@8{8m&4K5Mb z<@cBxP;8{6q~j~NX}CNz<)a5Wm2%C>PKZ!#Vs}v?BMs81ylVr&zmE9mSnX|pZQ?T4 ztfQYUA1U4AiPC2^>}(GM99X-~1&+H&=Z2V!{H*d3MWP!ZQz(6nP-Hh?CNd*rA@8Fx z&pBJDVmp&mUbRO(sA)@icR8M{Zhx6IADSgkZO@JHn~6dB_3RP*Xpba>7?`eF6QDt% zE8BSu!Cs)xY0s|`7ER~DM90Lx4MyMbsOEvN6=|k46f`w1dAuz`Ot8^88ZcpoYSC&{ zGqsg{DkN$;SuzrPk55e;`1{S&P+~Nd5q(~x!VdCw%*FsT_H7E2Xgu#ogK8$l9E2(* z$^pX~Iw`^LZR?{3ojG*Ku(#Ryu^4gv6W$E$x%SZ1SD#>}fC~K_$zdPN30*PXU6am( zy~9y)z`d(UyZfSPd61>KI2Hl;0Cl)#N&JqDHP%VCfTN}B^cf{!v$#hf<1`+t5og7X zQ-j=QNKL$uN`CaFB$;YT9hk6)P_wV@cS`KfRxa?PUlMM4b6Ci&3L2Q7`f>~;LE@X4 za$%mbaP{W{vPLG(d>+KF5Z_@@wL43{P&n++x-Lw7)VK1 zI`sHqh?q#9!LU=mi&Zba*g2d3R>=?z=Ndua&HSdL_Sfs;axI>N6X_#9bq5tEb5k#-v=`^oiwYGE(oD^voY( z@&jJ*?ZS;^b+zu54z1B)%q`HN-fxJ3dSkH_Fgl60_;chiZJcP{e(mru2ZrEHH9$Pl-Tj?CIEc4D z`=}P?*b9W7Y!i6u-)T_wM@3e0K3;XumxOy_45@`*qA~V@XUEKKYW7ty`odS;(N)5PpBBAx*s4xW7Nn9gW>IO*@NlPnN;b3i zKrFf=C$|%6{k$Ddtn~ET0(G|Iaee(E_YxGKMSxtc5u_3*YaIp?xBqc8Zwkf;MSB=Q ztwRc9buJSj*YVeZS=~2f&w(&Xq2z0?hxE6l>eVxSFt7m@h7F6%%*+$TkR(~PMD^5% zkx8v6o3@YdHv5cNCTztLnKFTPE>zBiwW~gw2FltQ2pc*b%p_W9?!KRxSLS07XPnd{j`r+WdAMm!x!KliS<0_LBJ0f$L92_ZXlT z>}4rwXZu(Drzds3+5sl~{TK5l+SdeY(lL|nnr3&mBVgv-!zM#r1Z!s?HV2mKCo?Xw zl6pQtsRGr9qsz6YPTB)t0{oU|=Bn%_%CAOgikMB!G57OE>)v; zN9Le`=X|)ZLSDl6c6aHlo=t1PcXs#ow@_}O;L7&DhzO~=nQ&N?eP&e4gmutjZ~bO| zz_0DX$t2C(YzdqcP)Q{&o#j%8*xxoINi`XRf>_F#`(;7BM&=;6v>o3 z@^f(g-8X_w6O0|y4Ob!R2e1Z=%4#Gz3fDbrP1VrB1gMt)hK0Q@XvzUB6&?*Ao|&1NFgA2fc}lG9g4QWO0jH_U1ZE_coL9u z1G_+w!k;2SY`Rq~9Stpw&yKRUJz?DZLv8;a;Fn}LFx_;L=qi|}p5PT@Wf%lmd!r5= zM5|Ic*tqI+7U&+2p&&vKR%%4{NYR5o|Q+RURQW9U><^_?or=ZXfdd z-$^;Xt7q4-4>WuH6S8KJRC(m&*GF=D&>3^kyg)w95_Lqg^Lo8#&T|gzRv9_@4C&yP zAVGU#P5)Cp9&hz`|Be-1P^cz3uw6j9{IWVjQO6CuUwWCdC5*VpAzxznOZix8+QGu7 z_m0YA!Uc*g3OhyjHil9Mrq&7WKfPd;?Wi~8W#_VSuUxBlPSFWT_=E;=9T`}>vYd=$?DqoL+3N(sIt7eFme^T@P1%M=Er zy$WV$jDH>c)aR0GyEoeLmPHB%wry#zM-scAjqC0g_XHk$tQXkU%o4a=B~(j1&SmaW z{({j)%5$Q?&e3U^&j}i|;j3t+WsRIyi8%zdQ8|bylM~pASL$smN0H<~LzTa#JfN-= z-*8=j)8fwOw6V$Tl4S8-!wwe#g%2Vxcj+`9m4SU*iY9x0AQ+bj(vs40#z$45B>$Cp zbt3A$-C9LuCOJnQiKinqc{rjWl%cKbj_(q`_1krN;0dc0|E96w{D6ei%b2kQlDmKZ zt0%Uh(3R-BKf&J9YHY5(962lF+UxKX`*MFf)B#fF-|={E51)9;6ie_3$mnqP=7F-* zKoZxe)9*OV<@-F_tR@tHRIC#Pqh5@=6QS~{q=Wyvj!Ah8Ne`sy4N#|?&FX*>)Wofc z8;l0H{hDcE=Ibrr4nO$K2vYup$-JTXAVLC)apfrGS|mP=@JCy1yxof>lXH!bF^MV~ zR%qVsgA^>l0hw7zIT`^!J}Or9`!%{*m05D+Pe7{b(_N+MV@|b_gUS2D^Xgf_O@tLV z?-p<=X!fA1v&!we<@@{EM8^V>HE0%FZeB;VE?>BIvU(TwoF7k@%yNl|;KKlPnXm}yD zOIfim+otUSLJ*7bDmR|OHzM0qS&Rh@lnwiF0}4_@zbE`H&4{JctyNz1!y2Dgb(oau z0iZA%NQ{@IevU`b#bbSMy%!;DI19}KAkFS&vrbEioojmi&-)q3aBANx{W)T{-sgqu zSm;iwH8FbhD?=+nPpZzqyUxYPZ1-!3z;Cq_;nWf{clynD`@>;k>@dU20+x=q3)wey zr+DeR&&JLM=o+f!fbv#+-y5ece{p*3{ZT$Ypk6b6KPUqhJnli_d*p%&s zT%@p;i#+>F!f!X`%V-!!oVfUCg+Yg&fiYl0mKf;j)~;*Z6*3A6e!*l*vCEm36fn;@ zV}GWGJp6kb?Z3PasIy8FROJUp?PXTvkEGhb%ybgog7{vEv43ZPc?EF!nch2L)TjsFb7)N%5UP{v zyvIUjW)6X?wTaxc(y?V@yG~Tx>zwTGykAvr@9soy_06namtO=BKry>D_#-mLA6W`A zwJbhZ!)4ri1t`GAa4mP$U{y_@)6$v4Qt{Lm#UEdsGh&8dSMY0;`S}`$Ax0HJcLi*M zW-xXPp(V3EF-N}3bIGNs**wPprf#ud#Ij~@bd~^+_Qp!WJ`EkTykD7}eg_QY!XC7F zEY0N>%ZUg6g+a$(y*ob0vso{7$;K1qT)KNdqNzZ$r3~-W*Gq5@O6l1KaJMKl<1)=O zb6$Gu8@GY!936)B*aatvLp%NKKPr%(C(n zm4wHan)h@gHOmFS1aXpGYld4lGSS_)i28Z4F(zO+7Z9#Vy+j7H#;^$cU48e3ac#%CCmsuAZ zDAX|zDyk!bpa!-6aU_3!44{<`j%fs&nU;juZ-H^v6>4AxDv)dBQa7*$qn3dQReMmK zf>s^x5uadL@v~-j+I@^=I4(R%pDD#VhQJ{F86muG0KR zZ;G%s0<<{0BFSD_@OBQ4G0R%lG6dJ+(ZV&DuhVyVp~M-5_Qw*sM@(zEo-yP|Y&YgG z;1FLB3n(*|RTK~iul_8~Bo%Ba3?!$P$v3JH8^rQl&I`9CI z{>GG;`d91Fxdzp9o0G`3-f?508^^WYEvcR-ey5;w_u-?mTdQ*I^$|{ef$&7`;dGia z7n@JxgN?zky5l4Cb~b7TcaCg%|Ixh+|3$15qV3{CCv25Dm?~`8Ev;Y ze9aDZ1sr4cOth0z$0dDQ4&Yy@NLTMl-#(kxyd{;U*HEjrC7KZJpU?Fx6V5oc%*iOQ z&1!S_?kL}=9G8TcHeT9~n_~l=%`C~<;D^OCYF|F9$$jNESi94@-x8$x>_K)>xo|Z4 zP!kRa1!cU{S}FHPJRMIqnJ^rObuDNnKWbnNw^iI4PPZKbTq=9!>lX^ree&Mm2;ci0 zOy(@2klq?>U#nsN#gvjk2=oh7Lk7ODyTON&5dg0F)^C^<%({QBOe^sce0AsF)g(*i zc#>1aYoP&(sEQuhNmD&-8k~m*N~?z5GwXW z9$xyLO*06zU6j&oqwBWXALvj0DslssT#0S`zSVgiR}Hh4*}<{Syq&b;@9$}7=-Lup zF(^RqVw9E1d~@#p_JN!AnUfYI1PdJwaUOvB318vECfzh6mYwi*Cdt3hU)1RDZ`}Y( zRu1c1e{6THM0T(H_`GWO@m74b5DJG-if$fMc{ATU0N0!l+u~+3yd(C93+A{Q^>Lzs ze1p#=UnKYnKc$9sl5Oqm`t=mn)%Hz7hA~9A8SU*EYg=E}|74t86;HfC=`D6AHOBdTCxU> zZ`~y0$7yZS#P2C3Kz?z6^)^~QGl)7 z^2f*1z-;r(f?meCkIR^aprL;E z!K)0bX~?witX@B6{sjz(Y3CCFCc&EVO0Qt4tDm+F;uPZYqSjkw7;xN#`?|G?v>Su~ z(}6lT*0qkO)ygJ-{tQy?3k*u~x;dF;4Mw$ayVnr%UKG_-y0tmHyT3~7=!Zg@o68`# zxR9OmFO~FfZLE=u(f1j|&czr-4_Hh0;W%iZeUjoJ-oV1cP_6RFcrDYUO%R=wMBT;- zQe8h+XRK|6o9++h=d?`OHdlq$tlE;YNiKj;`^}8A%uvM%<^HoSo$&1gG=yKTiomU| zQmm@)`@c(sPNexM)2*1J&yxp^znGJCliT^^%P%ai7F9H(;qRGqMaJzxwtrYQWVvE7 z2lqzLB{_W-am}u=s)u$%ZvPGX7CwS~ugg5(>ztVnyRYt(x2w3+8bh)tQSK|wOu0ER zbhs20G9DQ*e!{Z9NUx#m{@@5P`C6a8r(Wh2E2KMGnya>R*pXG!x{~T>V_=!NV6QKh zq@}na#EDER2hK!Nb$+m?L2oA9oPem&PnbgLXeSZLU7)c&rZ zwnnkSSoptT`cgt-&N;Fki6TJj!I;yq`3*`;qcFrr9@`S2m9V=Qxr~KX+`UuBHm2AG zc_b&50NuipZwO(~^tM?mlkrlITcD)|G-HCO1L@l!s3LS)_L#{Ac>ngJfliCxl%T)~ z^)!>5kWozQkjp_XHtou%j0XV~PYo0i_|2Dh$W3KxAed^UH0@Jx${Yk1idz1l)HJL9(g}6SO|=fRvqd^ka83w7ReS< zgL09~#Olwxirf{2jG)PcQF=ZBXlaAE<-4Y|{O6_gE`Y%UlAeV>MFB$)KricQLNfAs zaW?zuSBU5NDl$n7Gii}pm9a5+o%@Y#^q=GCZ9$mmcR*Gz+yo6PDr^19f1toMV`~b~ zmu3!d(*^v+fgo6czPz$c`F8Ay6A;ZFI65WHemX4Sg&(ua!41gTX?wo9FRO2Oagj2!|x#qJfS8+!bn5t z%uNfa%E91}`huQmrz6Ff>I!FSs*25Bn8~hm{n)LSe%pMXVxtfF@~Z^(STV(Z)xkG( z?J7^P{F=tHD!3wLOS*R&>@z7V_cu}x)t5s~PAkRzACE;5q#+~gH_g= z(QauXR|;_110d{cvOt@CH|0f)D{efaAi;HwMH1h4 z2eje6eK53is!!?CHP7q{KVPhb3C^52-E(#B+ge6Z1THxrX1R)x(va3l$h}68W@IQq zqg@=#pekXY>%Oa_#eSw+aiFBMpw^>)Zj0l$zb`kT$C%kAHp&QuHE}T9^r&J)4}s5s zE^$09`NQ{D&9jo#hBotc0b%3!eo-z6&aa1ah%dJ0rzbdnt{^2yAmPS$BlHG`wD9fS zoH_SdF4Upa2?~im^8RO|^c)$V?Se0cKZDOZ#hCB=$qlp~CHgQDyKi^VVGF=>+i^xn zLI6G~L&MJ-v(zld)CYz$^=8Z}==e9f+I?d?eJ~+*lWyVO|EcjfnrZ%3$?LV_LD~_L z&<-bdSiA|`3H(eOg3A%I>ialz`FNS-?bhCw+JXY$L`A8TKpLx(BC~JwG5hS>21WtUzWu9O45X4hByZoTEJt zp#LYauX?$`?vT+`DHfdPw}u@rS8)R)#$I`J?Z;jbTfWh*C+U1Yq(}EVcUv}oy(|pK zQmcE>YJMsB-5h1~wy2CO9rUa)DQjbueZ!#u4pn`5dhc|_u$h9VJ8E$%%u~k9wZSa- z@bdf1Iw6F2Zzb5#+Lj%kQsb312z|Vmy8~DPW6t#?8S4UbE^#E7gebm$m=XMjcf-MS zejom~rjZ}7xM>&!30h~B1!%kQY(lD>zVdDT&>|6ex50;~9DeGzmH7Y&5K#1>q@gy~ znn!)33p${YTq(_$P>Her&0L7;5Of8~J?Y<}4MH=TD^Q*(BTSy{T^|gexe*7H>l?e< zf88~H&`E;Ky-xM1Jnz4b9|zAK1*WqUvSdU`^9mUe=37PMrkL?> z4B?jGv^cKf zm|d0SL;@X{0X8Kf(qy+1z~5fsueBLW`}L2m~7gpT!`jyeos{9RdEs1$rA?Kz~bI}js>pATLoSSNELTE0IcJM~uJa;|^iacq4*4Nn1H z8hUSpa}!U(jP5L40!@yh9iSX*?-%gioR2SG3_P*2bBc>~c%?JvLwh?>&^T>4y8u3f zq<4HV7|iQS%*vjhWKr%){Xo~PyY3ZMpldQb0mIGeCeKaTb!f3rfH(0|qSc(;FX^nJK7^6{SfFbr`*gh4mEAYq2GQ#Ody1z6oW z+qU3p3T@-jI1G82{`GVz>5MJG5Gadhk0XGH!md?k?x=!;S8ha$$fb? zX|7*#h2pDXNsVgh?9h~aRl}+)oCYOOSY?|W7@P#=dkuEKkg?EG)u|&^cM6x3Ww`@+ zSs#g_i;Y|8&k$g8Fm6QV8T-|`^`w?G!RNeK&0?sfhl~$bjH}C^LY=-M$K-&44wEVM}Tkz`% zu2q<4LJP6`v(z%A_FjO6fj>DEF_lDN%-2j04atElH{6z5JTXsKX)fSnEmKf~F3U|T zuIdR-81}6cN4WXVz|;MDxxCYLAF{?rVGBPMC5AOF>(F};4alO9*Uo3@ooiwaHNGVo zS-p+W_|^p}L6IMloIO^Lc^YV{Qg)JSu7?Q%R}Rs5XaBj2WE*@c;)_b5B@IH7;y{xH z@h-d>loXk$`y%+5JUqWrOyKGUdV-#G_?x)W4ZBzzZY9R=USx<~p@KU+m{B}))08IG z+fcSF;=%w2(J-!08eNpAZJ-=4S@r@MvQqhAz;naxIO^t()Q%}t-yy8qc0e*IV{e^g zUXyjCyj%HsEy;RRP)*y!#&^FS7vXBgJoTA0nCH^eQ&*g8sWaGDdl4}PxFAADx&rD+ zuqZD5damFe?290;SB%0wCz+>sD~`Y_w@C1DE|9`thbQPGGG6h)W9|tj{kG$y9rUds za8;svS5{L%+wned9gdiDlo)|?LO<|S)q_hj|76(VCvQl*aK5)T#G&IB%!Y5%j~h04 z3)e`=PFANVe=7YB)4P_3_CQECT`4m~O5Nwi09otdhc$z56G;`&E1L?m7+kCE5^tI0 z5aXB>#{PE|V!9lKr=YsUhGHLUjkI(@dGp`4h{e$g>?9seg(@J^$RtoKtHL?st8*67 zc@B|k|9!P$Gb3BY{qcFfar^t!Z+A#IAt!CJ;DjC9pVb2t8Q2BE^p=JD9t2c8=khX6 zMeV#b#N^dio@DUFvT>8#bu9<@`wJpfboe8-806%|?Pm!foJj#+Uw?en5E#J1e)>+F7?Nktyei+nBNfDD!ytmL0HU z_iaNXwFkT9(*HOt&-Fn@HELEFFmH42PhjhRf0r(k$lRo_m{^w%VeR(xr>dkX@YS|U z|F%xJ;yovuaRq}bCHAeH*!Gkn4^!UJMs8Rgz!OLw&d<2?l!MO_F$H-se@-Jns@Fm5 z$c*a`Ww0LN0ctp_m;aTFGY@+)IvJE z>Kb3dv!-|iW}?Ze2QVjYc)*m68N+VVXlfoVXokT!4-d$-eGxy-zPZ4puelZznz8!d z6|auY-@%igml;<(JHJBQ{jB*IZZMwS zG&5f=-t=Aq0UBudW&m{Nbh{q+`cEM{uhY%d-Dg<0-wvTyexT~q)WY^@F_ay*5nvPj zcitRQvjaky=6i_YmIL0Wbf$H*MBH>YAjN#Q_3ptK?VcRi?jn5kGp_2^Gb1xPbWQMF za+-8N2WBZW9rwkbVeFuwH&qDSC{eXv5>Kmb)fskr3|xXs#tx8U?d@cYXE~0vDQd0F zpl`y+LN`#A0*}zQ;e3%nfziwY(_5kp@q^Z{ue3`@3uqy>> z@`WWG3IhS(Z7>vY2VLicxa!>jxF53|KW)YB+#yD)pEgXAE}Lm$38+2`6=A`~Y;Q7K zMaTQ_9O@6h2em+K#Hk4nHNhG^0I$K*=6qmB_2rMN+CEBGqUQI@o9`C|fo=_L%4-n~ z=L%Xc9UDzMVrX^0$6sWRk&=)@z38*W~r(`@e*$YaOq8?^K%)?>2kltb% z7toka5|D9k3JUtduNSh}=z3ax8Jz!PN{G6~_9(}j_& zB9-3-bVJ^|@La}nH=j4Z*k{K2urE#Nd#}NdRONmR70!i(DNn*4%m>tP1JChCVk5_= zRg(h(3hp>8hYOM|9aSBki)a)P@SoHzol~oVoqN^v>wNG30{;AM>7ipT;-+9ol8Ai|`x74ekAy85A_~RQfc7lx(6C*hw+}f%t zP&-es#lD_K+QUddsx0JAfO07P0l&qdoH|%9q>u^s5QqJen}<)Fb9*}6UH%{PBmWPc%zwya%9}E_ z{qHOApG>U(txV$1&i4O*HS&pLkQ<_h33>g3g2Q1i2W|x-Q{D!`;#4I*;_PAVh!c*9 zR3`Z0x(TO(-W&Jz^xT!YxtUbYdXz>8sjZO2_gQYY-?m|Eogwi))Qm%aLFe#DDrNse z(0%2kD6n8z5|?z`mWHb=by~mn?T)Y=2As+G%m9yFDvap}^wh&;XJ(0+hvCHA%#Tj= z5}&Wpf;-L(%&$aj49PhN+p@ZgiCEw7O(vxCH2tURbgz05Z|syIQt_or5hZLl@0Sb` zE`Q?_R{J@Q>^oeGuPP*xI5Hm;xqqEfYnkP1(FgE9VJiP2W?L!3PRBnnCI3H(Dd6I4 z{{L6ZkQkoGe+w@3^D6=wl~&v65}a@nWfhy%HE?QX=7K-#Ut${dfgfAvfkQOsHW&Bo zWT$qR5`77YGrUCxGvkH8C%*+tCF%%776mAO0}Fcs>cPq}(PYm{0fM|zfO?W48-He1 zsSI@fQSRxYb~Fu|;~~0DDrj=I`V>RL87)dvwyKksrsOJk6Bvft#Jns#5c8G#ry<); zC)!5{q1$&YS`nXG=#xnfR_hxo9)iqTvZzXu_deJ#G-eJ)Ni<$(`!%2(YL%ZP)epxai4G0AF2MM-=M9KnJ z+f+q+X|}{bB8L$(#>7{2pTVL_LbzA+rUx zeA=s#bvjAPox%Olp=oUWyCh>`^wVi_>+2SoSOcdt;7#=IHSu$*aN5qt!;?<>0!_p@(bA0#+n){b`-aY)@H7MB3 zC^X@-7iG@ep{bAlk?-aEK$Y{hndzRQ2jD-AZd~a96WoIJU&t?PXJhbxp{e+?{u_%s z`H$c7VgiMu1`YsVivs|_@V}o_(ZtBb(b2@#$V9~5-oVz_#PNSxTAu$k(xq9$#%_}p z{^v$d0N9pMzjF2nvq0Y8M}7cZ}(mVkwVhff{o}e z%KgoL_ij7~kKFBoE*-va6w|>8b)DTk-n7vUtt z^Z2k(gav=YPAYDoT!zSNU+7ox35-tnn~?)Yr~9j+>-$OTt9e9@TnvB&JSv;RQDK$= zn4W=v4=M>V0^sQZ^`I3q{%bJfYzo{TRfInY%U+?4GM8w~cX0zy;?n4lx`{5Zf!ecS zqmCltL?NfSfS=$g6dD*8oiifwBorK9EP&b<{>@aOSD8?Yo04AG&p09O{ z)%{H`NAz_@6*f3?3~`n+B_>Im#+UJ5V9n7+wJ5R{>)#IzBTA;}>m7o-84{xHYG9D$ z3}?c?RMhvBs!7l7}$|yZDmJ2*Q zf22hHeWneax>cGFKa}?q-3bnDsmvAYilpmUy^t>KEX{6O<8)qM+3?X7EZHnR7&?4b zOX~7#co{&FH{kwev0j};zzcKa@+5D)eK34DLr-yE-qw74^0}rH*vd7mp+}r?F6?vO z=JOiRxpZIjrp*x!5U-Y4+Y0sPU3h9Wxm;*t;%Xc5qY(u z?0uOr%It*a1zjByUCZ4Tb!~*~a!mcyXl}P9l$aX--J<_^4GZLUbT76PKBzr8;HC754obZ_)!hN zvu@Wk64vYUiC`QA$~jV-U!`3^vZY`x-d3ri$8^p6H?W&~RX?a^?*Nzz-zyU@;qnb0 z5C|Nk&8J#-(ziXDb%%TPoy^NUyNc;eh1)AsYh8~I$-5R!yEQ4P7$^MHFY3Yp=;2su zR(GmVL5C;EI!R9}>>5ejU#LsQh6s@o;F;Q&`Y_@*W(pJ0OZ_!U&i+H;v26ItlQ>s( zlD3J~+x@yUZgq$3CrXEcaZ*v4#NjL;iGqE>>@yNb*ma);VnFHA*$djWBoC*s8Tv~x+I9YBf~<0U zT`JujuOCAA;+5|P3GMiU#JUhD-)wIzCoO$pYS@`XTOB=wn_PbP)36^+V#Uifc2a9N zOz=!K4>vz2^;#srh)>zC{&wT1jRD)uKUhBkW3z>x@nTGOeYHXZa^{RYa?)IX{rb$Q`QPDAN^wiX=HgD#_;O=#!lS^sRBw>=& zC4tT;m3Vip=be0b{TyZ-cl1|RlijQ&terGgRl9vncut-3C zKZwYKf5%uafY8Em=(vu8dUpC0qho~?_PE@kpBn|!W{!BHd-7+3Y= zRK{T-*j;v(aFt-LEMa1>o%eqp109%MsVI89BJv0PIUB~P-faNC-}3-gauDSA!eA>3 zBhC=zRQJhIDMZ^a$5#>fjn8|4h+`K%GR!DM>Z3ik)=6Q`owo?W|Eu!}?i{bek~v3- zdlgqiok-<{K5@`60LwvUNDgb!tP8LZtR(YYL+O^-!Qq8hM50sje+bCNwjWr-G^<8IEY?A=A=; z46^Yksr=nV4;omny}xluG#%^5%g)WO;5v?W9z<|?*3O`LqmPox;CcZy)Avf;M(Y$M z1Kc3=@N76w`2vH=FVlV`tvwfgWT`-GVJ_-4a=6Y2+QpuMkY+(pTB^oUeK77$&rkAIs4Z8B!XylKpP)M2ku`4`e3WQ6Y4|Rb zGP2fNZ{Ltn&ZVW#TZ0a8O7%ft_A6Bw&@PFn0W0$7MOL1%4a{NSY;G-L zYK-k+W!McgN|W~bl1r39Ra*1gmB{ggxyf4AaQ%`tt?lpEV{Rbo=Pmyt$k)_l@O=U(FP$dh6WWtnz6aLDU7Y2B-PCpP=;5U075VrsInjo;_8Vk*^7<>y6 zU?aa-x_1E@1=K{5{U66M6j%fXspiNzqA5al?QX|Idl237-z_Wi?Dlq$6VhC{Dm=EUhH!$Jx-CLdg@{**xrHMpi&D#0A#T zA*(rG6z8h;(EVkpP0NhCn3@f+aS*qs)z%gnSHWfv-d%(17oqf`7h1 zP5#i}g7%Fb>+7IWeUP}}@JKntlcFh|2Yq_91_)kp+2aQ|zFmW=keLY}s1q6KosBTQ z^TX{CK*NTDY>`IkXRX)NJgh$jrX)AWwm6yaL(ERF+DWJs8k#&tmB4REucFYD4xDXV zAc4G8D57uht#a2$l|ruSB&`Rk*DlZxh4-Wzb3B3OnVL`Jj@Tw~9c{+qs4y${fu@l~ zMzpjYhn0;WgQ& zujkV1+oGDry6VVAxub-g_W-qui9wif@@f{%qozJi!Jg9$}Ts!j;%L zWK8j5yM`X^&?+P zPT@Zp>*qOoVY`+xHMk+~(s7gZjE;MT>~g={l5oS_afR6Rc|y&?|9iq)A^jeS1!@J% z8vwF_4FX@VVM3Oi_%md;1llLb>;BORAIOk1sswV8J6WL*$`3<#$!GKCSvGhXKJ(&j zFnJjW4Bj*v^0~zJX~mF8n-K4TE9>1N28Tn3k#bRo2kZ%`okM(53EpR?;@VD>!p>&0 zK-`dE3&=7V9;!>sqqLZ4t}_FGw(jd_F*_IN1#&q44m-Kch&pa1cic14VDi!A8?EWB z+e=yCU|PjQ$a^@%S)}YyzN26MP6!qE4~t@}&`fI*zoZdq;MA z7QsE3-zCMMR67pZ!@-K_4Bl+i2{_&gJW|278H6pJEUI-obXHH~Ibh#MUW>yPdl(~; zlH*3f?)R3*hf7`nZGMms!U@D`%674AJ7TZzMTgKZj*|P1vURYgdNqq*0=-R8U+AQ3 z^yfM9VM;8P&^BM$zigtIq(tu1J5$l;M^B&wCo5|YXZOY(8?eZ7x=~@XEVb!SjNLi2 z9zk7IB^50s3M`h<5Mb2cHL27rWK%{OHv{^p>7kLdq7U7huPsk(M({D*9FZ7XVQ#=k zcvD&S=WwO=FCJBt&~7u`nqPA`Gj->7Cc%sT#0c-tWNjjZGc`la5opf9{Z~L(WF}B< zaNSy3UUd`Qsm$N7h(XY7%tksB2*%=j}rUJ7YgtxD?-&mONMqA&~r zRU3*kR$O>(wQPH%g+}o(uB1fj=te%-0oPlb#6xm28)u>ivWs9|2}@|XbY!H1GK*yV zEYlLw*iyM#TNRe{Liz|<86+8O38TrPMcblB$+4=Ie+2q554o=#PJgZgEUsO(SugpC zurRe_A#;@Nduc}x!BdUMU5%*i{e7quTb^Ac&|1-%lN|+n%uXj(t_$l;r+WW?41>%p zH!|RpasQ|HszSj1&`lMm1`q~BtOlifyeg@yid{FTB;QGvNVoL2-zn)YyGGUdh$}&l zb}W$)KGVCe2R$q-Rv%01Eqd9d?=}yG1!GrPLydP+bxtmWfX{|92=>r&Ny-LLIcqnE zpkP&|WxObFRNtzsm2ZAoDYTn3n;NLmqSdyljK>ZKm?<&eGMNcG65N#Xbi10PehKPB zJrB-D4m*BLZ~=FmhJnktire2ssrb2mUNs=0{%LLvd@Oy_7p?_18t%P#SvDleetQ}? zjh##zxkzDUj~WL|x(Mp#wWNDOAWo^D5i{_J${vnIBs=om6j4>ynDDRKX3czQZeLUV z!bWYm$V^5J;7chlEBxwmyRWu$yRRpi0H(D+i+fo=GD`a}oobv}0PQ%Zv^XLpqeEiot z)R9x0qA)&zbG`&PvX5q0NH&jm#d_2yUi24*seSF2zvR(QR`2}On1-xmD~;P z&Mqg&zz1EY~?)DI1DI|1`wI!uwt z*-k@^9|lLSIBW)_LWHANUXj3o^ooT3v|PUbYO&mMnsqAKXzbrR*6%$=*Y1aNM7B`A zDkG-!1CYOHU4(pc;y4fZ>MlM|`qsfIOwI?OG^W9>JqJxwm@CAdRJbt{&+Z+C>yekH01vrYFO#$aaYOX$2+B zA$}Drg-$N^#Co@hmauSAN04}&S69p-Rm|r`Kx>{z+`nKSNS z9QKACLVi6V*ee!*eHfoQaCKzU97A$fb`>@@3^O@xV$E!2zH<+z`qM;2lysZMaE%{z zsT-fAO#>id_u(?Vp7y%TZe*g+ApJy8z*S}5kBGY#p8G&4IWza%KLLF#;-2N&c;eZy zcYjKNNx^VeaO>21%G@4Nlu)d)n3RX&E<)A#oj{E4O_lQ$j5{6{zg?Ms>E+fTV|*N) zn>r;Qq&4$nf}g_% z{PTRa8Fk(3?Q~u&auwwrtN?6Fl~grhC|X$&hB@Bz=DSiY1?0;l zp@wCLE2Ya}4_#rLl_%PkjQU8yCu%8{V3rj-nyVm}8;!dOTyS3QTedpnHZ}f7%#Cok zvU^${;NyXE02B=YNU_E7{@^%o0*NgKA!5;QPGcGtb7L0Z3k892i28nL*5gm2c9hK6 z@ZQGGs&Rnr(+g=#0n2NJ&z^ciwOTW@S3C3t z^2OR-J%DlYHpvj41Y&G#5a*|0nx6JS@;X~*E`;0I66Snr1C+G@>C9iEPVyX5?Q9C-qBHxEu%Qs?Dw$(#-7?4R=m zhJrAXZyjgcuvgn(&*jalrW)lRpSB6AU?qSnqRsPAX9U_@cmjj=*V%WSs^JSZuH>~j z-!GCzdA~B?SE65YLWbdPc5@m%RmAA3(u}QR_Qi3;QsaF%kH_%aG3qk}B^0s-M5Oq4E7sp6$=tO`8N|Bc622s zCK0d0?bIQqe^xZoizC!=y3Q}2)w?C+q2FVVwr&$ZL$!>ED z(Or~uGq2LS7pgT=WJg%diW8%)(t0tSm~Lb5YOw#0(%u3n&m{XEesO|BaCaxT2X_b_ zg1dWgcXxLS?hxGF-Q6`1+#No$v;W!5Om=qn_dyl)zE#w7Zujk{yYK0HPutgaa8e0y zWNOqB)Ofbk#<8>Qxjv^oq)P^&8eLlFUR!w(k~(`1YL-d^k(Z6eym7&x9-V>HM1j>l zA|M+6w6~2xrRIoGQ(X0QEFtNQ;Im0WOFkEI+ML>+W^O5JhOCMUd>+4)Tay)#-DfZ;IJaL51M4vTqkH8>$VP=MlteJ%+&UrV}th8{1>Mpk5Z&R zrC5y&m`t7Ai5V`*Grgn5sDh3^A1AaOtH;1$$JTHdsV+qeOQ`a#H`|8J(;V~VB-{~rhy>qXt^7@ zFwcY9sAWJ7^jo%zC4ITwH= z(=*o$vUtnxwWb*}!18c?sx3#vvxpLbTp{fFLlc?)j$I?S^;>nKgs! zZo;ra@}gt|B`P@)0psJMnN=fA3!}+>Ff#cOdOnFHw=Z8=3(c{-CBdVCS`F&n#cdLc zRVlLyt6E_&{SwpAv*7Cyz}PhXr+DsI;XC8IdvH&Qy z?&h*?KIEz)9ePkH!Ez2;er;(dRrfQ{9j@~;6qwOk_Fl2IG#r9~UmuqXCzWFIZFpK= zRque@zIq?tiq0b@q8VBzPrW)_m3H5V<#^*b0rap!EI2QZ+?e>bn)z5k{qggVk)ACZ zhTD*#DydV21nJ{}(e_$B7AgNHqN`*WY`zI66TPVU$RaT)YD1Juv&LXf&0XfP!7)}l zcb)({j8mJh*u;h|iOxCF8Hw2ES7m%DJa()c-0E3v&lf1^#Sb_(GY6Q2!MCTGeFNK- zx@h&oT-!b->rD|yAKdzJnK|83aK!hsr1(Vu%IR{CrcJJ&2TnR}+F8E@tKRB$YX%M) z8^yqi-^3g%kD!5pI(jcAh<1a13MS@lMR@u^Es@L1C*2z^S1=p!MP_L%O}KZ`NHPC= z==TaxY?jN9jaT=pFTnreI+6X{)!2pVK?A`609`Zyfco#bP62H*L2dg#lbw!9zdRGI zQBt)eWJPxWrpQ)oWN0J?EV~#WgH$M~7+hqps9s2m95x(EU;z$a$z?PV{Ia?=9O;C)JnV8goeWxK41Gv}<3h{nS5@Cj8AkY`!l^qKEhe zv;GUW9@Ym8nG*6Y3hxVk@??nShYz>my?Up1p6gX32^3)yl!g^K7y`1$hA^Nz6)?gN-vYK`V;q>)Wa8c6ESGJ}? zu2%u?84L(LxX9#UFU6Z~97YpQS@gdL1&aWT9E)RQvuwO2qmo2c>tT~Tm2%tf52o76 zE(WMySN0ocu^Tmiy+Tm-`{r!ub$lX=0K*A~6f~C}<6QjBs8tewQ_MG4+E#45OZZS3 z`v?*Z0Mr#&guwo;#%;uKQcE7Fv*T$~a5r@ULGnSXj`V!%oCkQF6*V>-9CFn`N2d5w z78ugI3A#Hw&2?7lq}as?6sWOAdZQVAr(}EOY`KD12#xvJO+h z6k{qocrk=+OdJQsAh=HW6khXgp@!ZeM?N#~-&j~(k;TjS7;psEgtajUW2SH*HQ+ze zSA?`W*D*(9POR};;bYq1Io5jVV124K;VsjnugYrdfN0%)zqP1;fj^4$w#u^-N6Hw9 z!XaD+a3r#B=LSCk9MlgbAIq3)He|5hSf*Jh zGn}&4PFgL^MWYeDlt{x!p8Ti7C~QmPA&YIZ#f{{>jJ87Y+{vn9m8vs!vVEJo6HS2U zLxC0=A4JM@iaI(sDx;=q?QgwR82spEgj1+9si0aM8zQC$M5V#1vOw*~40N0Uk$#N~ zutRbT@P`Jm4RDBD0wHwtiL{i;v5b7%6lBFMf=hdn#=@=rV{~Yby1l@;nJJjq#Eswq z1ow!NdAQQdaKkFrt`Ic0gZE%B+SVn6GZT$YC0WMi!lq*8aA{#1+2rvCiwsjmR>DHw zNOabitZdhzBxE^3Q3nzTRiS$RFgSy@Hs(2l(py#@tE^@UQQy;LpoT^cwMJ?KvG^qW z2ZD~NouwiAGT&sV6yVqH76qHnZDG-|_@Bd4?!}=Huia$L##l*essGjVBIlcZR#wsCZ#O~)sC+kWP`RVZ6Fe~h>()G|26C&0pX zVUDNLl!SyCBpzF(nsWyGvi4Ka#_7bx*NSfMg8b}CINOe*K!KecOk1C_RzMeGH$7R4 z-BRoW68esQlcKBDFyjjHGrcrzE0qCs{gmRO45V`xmNe}9t=0f^Qy1BOPSKT`+AklW zL(rOoJo#E=%v;=*`;!qjW>W>*7R`*>$RWD>+`lKRG*10ZKxVcv)gP+8b5^4g=lBb^% zf258*ReicOY^e9U5)PBsM(}wMHiB7sFf9GvMC5GGoG8E6vU)vSB(U_-T=aHTb+o9W zF0G}}?zHQR>nBbaIG>lFGP4wUJ=7@K1Ay~D0Dv8E%HH9{up4f2CI-qmjJYZ z#)TQ5w;`#FHoMqxPd5q;@5o|OW%-yS!e&xc%p6DG)#n^V0NuZF}LlyT{HhVL)EdsB?T0j&UP0unqRb?ei5S zTUdprMXLsno6Dt_`wZ6`5<)W754uP5vXbMENF*T$YE|L}4`0Ng%{=wz=QS1>MxdpF zh(aYQbxyVCwAbF?cx_H=@l!B@TYkiq4`{3~AhtXiVOJK@FA33(Zib2vcrr}eB!*}d zg!}+TR7t7YZW$V9WjU2C<#U1>QRnco+4D3qxAc-JFE*7pSsWZ=VHzSX*N38g2xKc{ zt|HKQABW6o+YT@)>g3-g@L9*(x}`EG_Y>BwOK@N$VSqax+a96=x^w zy6k!Da5L{do}|j~YC}aHYC~BQ-Zt)Mjt@n_>b9S_8*vwrWGmb<{Y5SA;AgwwG4<7R zTIqHjI&an7BIk=+qMpiXjUoI43Dljov@I^p6GQJCF%>5C>jbzbxonmnNKFt z7f*dDwqY{3Q%0+1Eba3&*n6w8n5*`EXaD@^G9yM>8Gf^0DMCjqmsOBg74<7!K{W&` z%cXX$Gc8<*%ke4%IYFE{<5zq;9lttS#_h5Wvvmbl6b?rp93Nb`!2{OIaV})lj*)DC zC~LrV8(eJ$fvDC5Aq6sJRwzd%&vh%cdncXgKO#hN7=Knws%p%T7gFNKGc zq(+Sz;^t%z)Z)tAmTYO}%M|z&+DJW?g@1FtlUmu5))5jG)}0~Caswg`_moJQnb8sw zGDLpo7(4n%4ixOoyo!@2mVU=K%8h&1z;lMd1^MJ8RTTsJoCI8l=8E2&i#8FOK5PIs z-qXAFlJn;MLZWaG-dlu7`t$UO!&HMgSzgxXdHmBP!wKf`n9zIeUWy)I3Ecpd(oM~35BEyPH-#&&S!76l>uBXn(dS-6#gml7l%yE zRL0B+0iu$l%;qTdNPUrz$IZ?jjNdIjX|~%p;jvH8?a*T?WIPdDzmv!{PZjWZN4AhE zOdq(e{*a_P`f|U0xp_PMaJ7E9y|=zO&*^US*_s2LT>h0#bc!O`*=8D=9JVXt^BnJ+ zae52_;UvooMGw7g31q>_Jw3f9wP#?Zdg;r1_WjhNPZcqW>;yz;sN}k<}G6FPGrlI+>2!NB%qlp zL)}X5NX*<&;QV;~@v*MXP-J!D?7-(H9}NVWlRL~ovdRc^V*J8plz~ELX<04?cj&2E zD|IqSz!vo8`tk0)M28N}P(1)SV{Tct1+_>A*IquXxNVqD=H!i_cOf3*;?O6;4?vO2 zCaM%jxDXeykV!#m49zZ3Qd)JW%j<88xl(;qSk@8i^Fq+6_Q1GpNFp+82_zEjZrcJ+<(f6&n*236iDCb91oL}1-H2%fv_e=H7~|}_cHPmEcC7ov<5$M z6wf(_(WMy4A}7MZSq%%nRn-@Rras?i1+%4sTH-Df^0n{)mWJXU1l&h^MXY+Bw5=CH zF%vP&z^+H#goG>91$BZSaYst%EzB4IVe6cQ6sdtcSgKuX@!Ru-N68`%$Wd_Q{Jt1s zpdehyuG#CdWgBrM;jy$3+YMJVD&mU1Y@&g9qglSxS#01KR9qEJ3g~We`{~yr|*PRz?NUgq=J?0 zw97+n2i}}*&=s>R9|EB?crM!NBSa!o zk?+7)LKL_@s#d*OKC(!(>dtp;FAp?oWS7-BD-V z=aH=G`nC^{2*J5%OfqMTyjm4DZ`#B?<3G8zt(sa>U^GK6;(4Oi z%$*&&>n#4rV|q|~CBsL1v&`Gl#Pq@^pdqa7XBT}~5*+?L@^t2HrkI9>B)K+1RhkvF znb}bQGn8WP)zO(TFJgqqw#&Jhkl_cs8k#G|oBmj7hir{cerLXzxjC$R24_~e0pEej zdyC(wHy@OV%m_fo-Iuf1)ytv{(?zuv!C%9h_^wng)SNT%)8j9B31$<`^7xe+g+85v zu#0VT%P|CiY58D#&v;7(4sV3s%F}UXxvbVoauS45^y?4w<*f1)+v-;(WPFE~rX}bO zwogTD^tNRYA()L&SKnx!^jbzR6zYPm4yqkqu5E@#@Yy#c)Oz;B3T87r_PN-a+UkN1 zTq_@2Z6)wvf;plB;$-J0;(1B&9K%V)zal%2oC z5HM{~ovLwGjkzc>7KvdBd%d?Is+Ub_x+|g*t8w$N54}Y9pF z@zZ5kRAF{QJJ|RPwdLJMXBOFVXyc|7dg z9-@QX;NB_TYKg+!On*OQV9bF=`&NC6AKgZ1x9!hk4rlo8(^_{MUwkJ*5J4u1@(gsf zA*qdqQJ*np2FvR8rH$ha(Ylf@%=C@KbF2 zofB@iU6`TSV1BaC44nO5^I*jD&7T9??SOP=5*&}l?N&9hb%<^!o~dCgLJr?=E7VMm zE|bCm*q>K`8Yx&%t(_80p?z=^Z5JP?V8^CG@}`|)20dMM7#I5~PluUJn{tVem2t2X zgKN~YwW&_d&~L0_OO~xR@?N!C7MdhK7Eh``$xRQJHr9G~9dXvqv0rvlXf6#5OugdA;5{7G?F*KJQg3{q0eN-oP_CTEgP%Fam+w035RscZgJBWLZ#(xW?2C@u@x`Zwtejm zZ4oy9CFT-5KwO`QE~vfPeqGNpzyxtbA^Gj*hoL?9Sw%!ee8Z*J`KWj{8ohyuF|*YA z$l>cu{kaL0(e@m;;*Q%8Qdna{2cOg0Hr?^6;RHQ3BVVL{^(b^D39)vz(iIf+XTbpE z9^W`1)(R}|@>$)Y2F~Eo9qKINKB?cU)GX z{Xkt`r-PMZr)Fo=CTM{d8Uz{*Ajk1Lvfb>;Isi*LoR60%^;<6z!CX`joYw-}JK|Kb z@P0!G^c3D>=nRfVJBH2Y55V{?jRnxQDfBB)NQ!C*oSP?$r-?}!a#cFOn+>G6Xj$Hb zr>81hON~~FqFqy+P^NlRq2q!JBf(`@0}@+Bg+dQ^k$93PQ4=^`*EFlElVzvVqJ8*6 zmsE}T;J0O36(2Tx>x)Ktwn&oLRfHL!PzRgb2r(~EMwO5&cBOfBA} z>0H3$)jEV;acJ1ZK5=2hItwpi5k}f^nq^zgsxs=RnPyDgQ?X~i#A{8CW5zgUS{WMk zo`>V^aM>RZme&fzmSg=^8Tnh3(w^t8HY*0@U?iDDv0BKyZl||` z#OYxBPSuady<1y)xW4o<)Qp?h5bS4N?E7QihThkQ_wyOw>LPXZ&q|_0M1e5sCrxD- zpbGw%QJ6)nC&b-dSi< z6`Jpq*gbm15NhDP3O&i>_!)DXG3cUX=YtP<)*yQrMv$nKL0fdd;rrKH@ED_Ou^Vr1 zW2O?VuYsu3Oz^uKM&R;03X>Kurvf_@^>{fb{rCEw8g3*!#+_dBTqd!w9Re(hA8*|( z9Vd9OY_Hqa6!0o`9MrzKkP%Ht=68W3QzE$X`8l{_T)VSlT{nR!jGi2=`J3Zo*h;&Q zmqxAmD?cy~dV_EqtfPpRe&cps(`pHTY~`~Atw9IJ3?`T?j_v(Eyt$ubJ8FKH6i3#Gw)ms z*c5AhxLKh{gB%eQw-i#FdBk+cj?c+_UvW10XS7s3-xe(R2DNS*(~Yccd5;zPOuo-4 zlEFAY31HPi9(8uy>mDMP!|6m5!%L#6fia|`Rjj1g4qG02uHeKt%F0w%OsJjm=s4mjQ?@!v)46)82hn$Gsg9p+q@_$PL>{~(t`Bpsx6hH zSB$GTH8enK*}erD`i6S!9DgLBqea>7rSk;Njf(M5oRiA8APEtNW(;Q4Uj5q`kA4)} zwb6^f{d>3q8r}@5{wa2BKvKS`Lyw}p-71c~w8X42-|L+kYS(ZP9gaIPnNa0jm^a{N zZ>)U#O-W*^XqH^H(D@Z`38fG@*haJXATUv!~xlz20}5&cL>ppodX zeTXe!AO)Gx$h<*7JVg-*Hqw(wM=i^DBkz*vLh!{wp@lFp(d>(aqG!G|l5Ja5y$_6) z7L+T~$(_<0;L<{aK-SjF&Ln+-kBeZO@d62BQWx${?DFp#iqQ~)(@mufEfil=C%J47 zP|bZ$m*ZXk9t#p*(@2!SKc$2MicX$<yB0$Pn&@SpRZk_fDa5{(*o)EAgUx_=e6-``0{-Buu#f1ZIx2~vFPK#P z88YF#Ga6Arl`v=XLu2<${9WCZbY~J#!d7#!9*24!MNt0scR0F?khiA`wqHqahY&&) zF~Gp72tG7u7_mRHzR?FJW#ngoMYpoKM}ktCu!D~lnHwx!76DhS2g4BPQGQFX9Oy{GJ- zGtFMpwJulez}4GwnV&otP=R(i&V2z7ldJPTY&n5-WiHh`N1BkC=3^?X!R}i^g>%b= zkt`Tji=*5)S&Y_4mqVA0qVjYy&jiz|5L4^AT4aEB)r|?_y}49FO(aXWi=DM5{8}An zNBek95qn|z(6ZH-dY&{GO%GDho1BbNh3$)1a>kMZQ>XD@Cv2U8_bnYaW2dTRAxFr9 z1KxV-CNTzXb!vd8S(xo4t#2 zn8@N~ferG&8l4iEp;*@)I|3u!5MI)3@Dk9ta7mn&+=k$qJwsSiVvBB;&#PEbBW7KQ z0imex8HY;0h24%5vW6yLoeeoFz+4Tt3c1GR>%ro1V^fr4~lkKyq&77QvYl&o z`*hDramTs`JlZv$yZI60m>QDk(?CPt%JTrk6?uLQE<*K}JPiXU2E&}TE9bWF)i#iW zssu-uX6tQJ#<-%9P|@%$$1>T8Q7Kv&JIP_l9x?zYc_L{TMoji)If@e z9L0SxjG|et>ssZ3r@xaaZC85oTEptp?gICV44Ucav{e!I-jJUM6Gkq-2G|PH3eq89 z8hpKyT&_+GNIq*i!CvYjgb)yFhtbbHB>#lQkxHf<2UmTiH3Nht+w3%H8XXiA+7#~>AwFvxzq9o$@MSlJnbO#el?-GRFiS`^qfzpNBIN1u3aX6)F$cr{0p8nF0nEmdPac`5$r;&LZAA?&h zkZFeCtS~5#7yWNDYa1EKjIu(cjmNDZ#g|Tvb48?-1#tGV78lDPD0lOo!P8R6tac^E zM*Gl7pDnzsLh|s^aDA*Alz8bT)3qi_%@#RbvU_m~o}A>W(a<`~Xf7kmf*h`{Q%>7* z@sy38IVD0A7+kmD_>R7De2sKkGd%skOGpYHak6>&5fRBp2vP}MNe%(Fk6#pjTrP>9 zmw*_NtcAa;0wMZys9ukd36-0j%r|F|7UxK6xvghDAhG4oz`MwMdoOj!yQ)y7l7s=e zD1e-9LK5rja%o2a{%&;9GkesV;#?m~B<965C8=d0ac=dE^t0tOkD7?ysx~J3J#MLe z6wjx}9dm1({36~yXWRoM^hA!2Ua&lVGMTTpO?lZf4ShmK@#^~8s$U$ssDzI`3q-&@ zOo5OUB>6$Pj|#UjcR#H0C?baET2XSBfB@J%Rk0pwW`iX);+hmS06BR?Hf|%2U_r!p8M6xy6`G++KFmp9wi~D`syY4Z5wXA{ zvTH30qnrMiakBx99jkJ}=wP7$_gLd9Yn@f*!hQLPQyOSjObahIF7`599k-M2-&-gFa6 zv-tRWyO3h7Y*iei{!>?lM+)gpo`T9`Ts1Jwgm~$JTyQRhf@*5CdPXvLkF@hOGR&C4 zf~7vws@_~|&U4k1`p2i05$li*9LVHY7N#AN&tEvI#i{ODVPxEeI$|C5k!!ecE$dQ` zK<;ye+D_Z97j`wLj>IIkwBB!>0X_8IvUT03boL|D__I(~`}(At7pm3qiCMNnDaE#a z96&DZl;YUgn$6Rc@wd{j^(y%sKr6G%2x(2S?ohGgYE{UTdUFenJb20t_dt0i@ht4m z@CcIao)6R3xl#9h?eVFfX`lOCKNR*;OtLE>wNg3!Ft|ZUb&|gwM!x-}?1k);eT@X# z)|%vPsIz8McgIEoxcZ!tHi@k;tT&q=Egi~#G;c9!S}>HxKvQcyFjM=u?FdTW?c~%a8YQLS7|3l zaL=E?yv=tMC4*f6VxbjF~1;Ya_xak#g%;i0aB6@ol2AIQWjR}g5*{6<6 zAyqeLsJ=;wc+eqX3mZz6KnHTIc0n`-!@Q#oAr;PdM-~hN2XQQ`202RQmNLb(4A5fL zG}43fk=>%iQBe8H%r{o2j%)B$O+-rfO^P?nwpUoB{wcZG5;)gjW3L1Yj(|ReY1UD+ z3fDrEN20^iP@W_c5s#1xe!%>ug%pckZM6$JX| zY-dSJ`R!Z0vCe3+rwenhQeF!XL*4qDC+#YtG`0KDE>reU&2HAUC9d-v_kW}syyg|W z{``O8vouWBFel`i! zRu`50$Se4(+uQV^x*9L zb!N591?H0)h%+>8ibBiCS~}UuK{+g(YZ#iP*+;3TUb#aaR*jw3?5V5g1` z^o&YHjUA799Y{)NO9tUkjrswGg50y)HKcURLdm&Hx*jn5Ng62SzLpZ_!#gvMqgTZi z0hH*~1b(rHFQe zkyyD|rhVxf)6>oZ67w~|%}Fif&A2huxCLc-!Rr z7EVLvk7083u%oLuCOs~kQrb`wr~#NT9fJa1vVqjUSfJ!^Mi<}bRKMTGNywD z1_O2`2UQ}sP_wrFE#^&?3PCM%3){h5)*);4u05sKImfvkYY?b7Fkv8&vvucd@NvR| zB(jV^)S4d50dG_W35a3r+NMNH;?xB zPk&N3`rUbB3BP3Ua$`HfQ|J)pQRn6Yr`ZkdDR-y6wWFuAHko;|U3W+$-rv#Pt&< z0^{1x`)E+b0`{iN(s~WSqIH@$12SzZy;uDs&uni@f6J09vWA&o7yRp;-PJ6vREcA5 zehmB|WL9v_tIAvjgC-^8ju3M)6XlS%l_Q^uv4eZJHD!`_&5eKg@&CDYuZriSQ_Zu5Q4EGR^JbfR`z^jOaNG&!MaA(FaU z-D}OfiuAcnpmUmWB*brnRxE)PykAU*sgUkUUAK08rFEFF+=CCjSGC7tZhsz+j zN-tlZwokCRQT07J%2y((f;Y2!6zId8nks0?l3y{3Ta74^k`od~-o3R+qj;2V3Fl)l z*)T}y?r9$bkC9pRy`K***PR8sl<}eK@_nFzm?M$K0n@Grmdk)L$+bR}@}c3oVK5T& zKjMe3V&!<3X@jSyWzes422AyJFA4QVS63@Er4e%{X<$%Qrkk?@TZ*?u;MSX2}hJwZI)`0 z+;7U;&)86ywJNxyMekx~Z155jS7eHU1N5m%pC6O~&E7fh(%#3y z*9soKLwz_xx!;kj4waHp+q_Lg^0E5PzPy_f_;vK)dSpP%Q_)vwo}eJW4XHkn`~IvG zI{m&QRp(0t4zB$9B|*+H5lJYJ^f2Xg3*9<0XDZiZp(SlZ- z(rWOfVln!j>(P}*Ctx*oxCQV4`OyG|%Mm)b0Sz3UtsRq70dwauaY1vQ>03xL?j132%EOC_doHIo;@stG@76(_#Vh0G> zZeg>T-%!!J1qQy&XLrMa$Yed(pU>AEPmq{!iJ;A^J9WgNvAG{2*`tWy_mL#5)9g?5 zRGoHV;9Ff|H&mq@9wk+E$0DvmyJ@BtWJA8QK=WN1z?>I*hj|37DLAm~f_Uk=UlFq8 z`%Sj>oh*Ys+}H6z(LhGVwCwm56SV60M5aieNw-Nk)ku-nOKMfjP<&(10t7on@NJ7<`;q-o1Q%FT1W4|*y8tL|gYf9S-$+nj=i>Z59Fh(W$qd8k* zK4d>Lo(V>RjBD|1Hl@hQ-oE64H@fZa-`?@A-!(+MOqbc6pxgncOnavsX!kOO_RrwM z;P2PjMqH^k`5>YhfvMrKV9L8|Ru%<6fHj?yU~ZaSf!O z)F*tNs1bsFx|#aa&1AQAymj?K(mPq8$!_s)$w1#-37Cy~ZLy!Ha=OMd6IOZ@1>F2X zoXQ&FMaF}8xvBOE#lt7NOIgGuugj3+q?STU~~ zp9R4>jns&$bD{YPIGbbAa@Hl(N`kf%R1rQWgScmB@IdZEJ*=V;o=KM1B%%}nwTq5w z3i2x8bH;jNh2CBq9CUKcG$cPltHxnaCMXRn^RI~Id(N_p3&xlABU!=LiOU?l*-^i( zjMu#Z9ZM-ogljAu2<-aiSsw!&kom4-QB)0wIRzjyrSD{;hrIn%O!QS~&DEzmVbB>Y ztfwo1$yFH0NmyE!{;*UO)!ILtO9!!_|Jw}MS@mRyOpXPA6hzck2xlwoIdH`5VWQgQ z;TehC88MCheQ=sa7QL62*NMfm9Hgg}nbeo2?F1E5d1k$?Mg&D=3K#c|_$BpE3XsrU zl(tr1EA&R|x{VX>Ht8+MsSXUdF9v`)Q-Uu0Zw8baLGGSfVZvp`Ij8FF@%9lL#W&fZ z+|CX!0cRhZ0frT?=`4G$gvWL&6uEEdm;(4yebO6Y5NDeOTlCQxygwtHlVl3DDZ8Lw zHG&u?XHrA9g;UXG>CNu;g5wq74!e#_Z4$B^H|xEGP&! zb$d&VqS?NEOrD+*0~QgK2xApv{UcI5ON#pP3G(mo(<(BZ`3SX8+9bw4S!k2)wjU~s z+7j(7eYrUA87tqK5SZQTZ>fXD=|Y=V*2~;1*Yvm7oiFpzFro1bfJd3nl-%bHz&fqX zYOeQVwOj@8G6%_DGNm~m3hEw#jkFJptz__sF!=$S9bb{F_Db5 zSNDa`ZcI?Cso=8%A&1~!yIsy{4x|r+v{@$Q?G*=3rReT3R#MC!PJ8jrG)p#%V`{&7 zq`IkA0iGr>UG*tQt`keliGT}Q-5)~!!ur-lK+%T|c7gd?H?TlQRCK#+0!t`M3DVo>MR$BL4&d*N?txvRIXwwOHD5>Ehgjr}ip%27K!(>JW$ZjtT@qx zH^!flyWFVUCy+Lt^@7}U-}RixxVuzx-SPY@3jGI3g)+8z_sUx|zV15-{>?r|$I{aF zm7~)B{GT?H`mf5X-LDbmtMV!lHIrB6_ZgM2c)!2N^qBtHs2e-W(9{3CAv-FoeR*FnEo|7U9V z|6u?CKm3cO|7(nYiH`rH9Y3P6ziG!0j6X`5|2Bqx^R~@@#Q0+j_$OiW*D>&4!2eMq z{Ac)Nga6dTpGc|y6ZRh!wSUI`X8515f8sX(XY9XfRR4^PX#AhB|HSG1760E3-`|GT zUqzaK!~bn)MVkB?|98peSI7R4Z2p^Z^;f~>pBuRI_W=J+zWEjQhkWzj!2VTb@@Lq5 z>%VQ+Uo|IR0e@&t{te(?OTB*vj0yN#K!Lv&e!n9ADE$5##J`ps{*34q_P22L7U@fGezvf?k{em&9t zza{Utx8bGu*KmJgUjN(K_K)XjKetP*CnsPgC#ji|LPJrC<8^w*~u$DStc&`PmfQum4w5ep-S4 zTT}i@?f-1bT*d#@ls_#Bznk-e=>MCZ`70s(vpICt|5tPVxHb6QntwY=fAiB{Dbb&; zX|DMnt@$-U`uei`AV~j1rvUyCvHWaCPV2AD_*4Ay@7_tU*Yg|z0Q2>?3mO2pZTs=< F{{s=K=`a8Q literal 0 HcmV?d00001 diff --git a/requirements/api.md b/requirements/api.md index 075a6d6..43c28de 100644 --- a/requirements/api.md +++ b/requirements/api.md @@ -36,7 +36,7 @@ This document covers: ### Create Event - `POST /events` - Requires calendar write capability. -- Request body supports fields in `requirements/editor.md`. +- Request body supports fields in `requirements/editor.md`, including `visibility` (`public`/`private`). - Response: `201` with created event payload and identifiers. ### List Events @@ -70,6 +70,16 @@ This document covers: - Must not split the underlying recurring series. - Response: `204` on success. + +## Public Read Endpoints and Privacy Redaction + +Public-read endpoints (for example `/public/events`, `/public/sidebar-events`, and ICS feed generation paths) must apply privacy masking for `private` events: + +- Event date/time placement remains unchanged. +- Title is replaced with `Private Event`. +- Description/location/category and other non-time content are omitted from public responses. +- Logged-in write-capable UI and authenticated CalDAV/API reads continue to receive full event details. + ## CalDAV User Workflow Endpoints ### Register @@ -139,3 +149,4 @@ Acceptance should verify: - Validation and error payloads are consistent. - Single-occurrence delete creates recurrence exception rather than split series. - Authz rules enforce role/access constraints. +- Privacy redaction rules are enforced consistently across public API payloads and ICS output. diff --git a/requirements/authorization.md b/requirements/authorization.md index 85e826e..14d3a25 100644 --- a/requirements/authorization.md +++ b/requirements/authorization.md @@ -40,9 +40,15 @@ This document covers: ### ICS Access - Public ICS endpoint visibility is governed by setup configuration. -- If endpoint is public: `public` may read only approved public events. +- If endpoint is public: `public` may read public events and redacted private events. - If endpoint requires auth: enforce configured auth policy consistently. +### Privacy Visibility Rules +- `public` viewers (not logged in) see full details for public events. +- `public` viewers see private events in normal date/time positions with title `Private Event` and no other event content. +- Logged-in write-capable users and authenticated CalDAV users may view full details for both public and private events. +- Privacy masking applies consistently across public calendar page views, sidebar, and public ICS output. + ## Ownership and Scope Rules - CalDAV users operate against the single shared `public` calendar collection. - Access control is enforced by account approval state (`pending_approval` vs `active`) rather than per-user private calendars. diff --git a/requirements/caldav.md b/requirements/caldav.md index 7d267dc..9284547 100644 --- a/requirements/caldav.md +++ b/requirements/caldav.md @@ -74,6 +74,7 @@ The endpoint must support these operations at minimum. ### Create and Update - `PUT`: create new event resource or replace an existing event resource. - `PUT` updates must preserve recurrence/exception semantics from iCalendar input. +- CalDAV clients (including Thunderbird) must be able to read current event privacy and update it on edit (for example via `CLASS`). - Write operations must require approved authenticated access. ### Delete @@ -101,6 +102,12 @@ Minimum mapping expectations: - Recurrence rules -> `RRULE` - Recurrence exceptions -> `EXDATE` and/or additional `VEVENT` with matching `UID` plus `RECURRENCE-ID` +Privacy visibility mapping: + +- Event visibility must round-trip through CalDAV/iCalendar semantics (for example `CLASS:PUBLIC` and `CLASS:PRIVATE` where supported). +- Authenticated CalDAV users must receive full event content for both public and private events. +- CalDAV feeds are not subject to the public redaction model used by anonymous web/ICS surfaces. + Recurrence behavior: - Series-level recurrence remains a single logical event sequence keyed by `UID`. @@ -188,3 +195,4 @@ Acceptance should verify: - Sync collection reports provide incremental changes. - Recurring-event single-occurrence delete results in exception representation, not sequence split. - Returned iCalendar validates against RFC 5545 expectations and is accepted by target clients. +- Privacy values round-trip through CalDAV edit flows (including Thunderbird) without losing event detail fidelity. diff --git a/requirements/caldav_endpoints.md b/requirements/caldav_endpoints.md index 07770e0..f89084a 100644 --- a/requirements/caldav_endpoints.md +++ b/requirements/caldav_endpoints.md @@ -96,6 +96,7 @@ If a report is unsupported for a resource, server returns standards-appropriate ## Content Handling - Event objects use `text/calendar` payloads with RFC 5545-compatible `VCALENDAR`. +- Authenticated CalDAV object responses (`GET`, `REPORT`, `calendar-multiget`) return full event details for both public and private events. - Unsupported component types should be rejected unless explicitly mapped. - Server should normalize line endings/content as required by iCalendar compatibility. diff --git a/requirements/data.md b/requirements/data.md index 3c838a5..8b0e186 100644 --- a/requirements/data.md +++ b/requirements/data.md @@ -33,6 +33,7 @@ Requirements: The stored model must support these fields (direct columns or normalized equivalents): - Event identifier (internal primary key) +- `visibility` (`public` or `private`) - `title` - `location` - `category` @@ -55,6 +56,7 @@ The stored model must support these fields (direct columns or normalized equival - Data model must support efficient retrieval by date range for day/week/month/year UI windows. - Data model must support generation of recurrence occurrences for both web UI and ICS output. - Queries for public UI must return only intended public event data. +- Queries for public UI/ICS/sidebar must include private events with privacy redaction (time/date shown, non-time content masked). ## Local Testing Data Requirements The local compatibility-harness test environment must support deterministic data setup and teardown. diff --git a/requirements/data_schema.md b/requirements/data_schema.md index fda8f31..a5a913b 100644 --- a/requirements/data_schema.md +++ b/requirements/data_schema.md @@ -22,6 +22,7 @@ This document covers: Required fields: - `id` (PK) - `uid` (stable iCalendar UID) +- `visibility` (`public`, `private`; default `public`) - `title` - `description` - `location` @@ -42,6 +43,11 @@ Constraints: - `title`, `start_datetime` required - `end_datetime >= start_datetime` - recurrence fields internally consistent +- `visibility` must be one of the supported enum values + +Privacy semantics: +- `private` events are stored with full details. +- Public-facing renderers (public ICS/public calendar/sidebar) must apply redaction rules at read time, not by destructively altering stored event data. ### Recurrence Exceptions Required fields: diff --git a/requirements/editor.md b/requirements/editor.md index 34ec166..4d4d94b 100644 --- a/requirements/editor.md +++ b/requirements/editor.md @@ -35,6 +35,7 @@ Behavior requirements: Supported fields: - `title` (required) +- `visibility` (`public` or `private`; default `public`) - `description` - `location` - `category` @@ -55,6 +56,7 @@ UI behavior requirements: - Time controls hide when `all_day_event` is selected. - Floating/persistent labels keep field purpose visible. - Internal event ID is hidden from users. +- Editor must provide a control to mark events as `public` or `private`. ## Recurrence Model Supported `repeat_type` values: @@ -91,3 +93,4 @@ Acceptance should verify: - Deleting one occurrence creates an exception while preserving the series. - Monthly ordinal patterns save and reload correctly. - Validation blocks invalid start/end ranges. +- Privacy visibility setting saves and reloads correctly. diff --git a/requirements/ics.md b/requirements/ics.md index d07fbde..9d96f8d 100644 --- a/requirements/ics.md +++ b/requirements/ics.md @@ -49,6 +49,7 @@ Field mapping requirements: - Plugin end date/time -> `DTEND` - Plugin category -> `CATEGORIES` - Stable unique event identifier -> `UID` +- Plugin `visibility` controls public-output masking for private events - Last update timestamp -> `DTSTAMP` Additional mapping guidance: @@ -57,6 +58,15 @@ Additional mapping guidance: - Timed events must use `DTSTART;TZID=Europe/London` and `DTEND;TZID=Europe/London`. - `DTEND` must represent a valid end boundary and not precede `DTSTART`. + +Privacy behavior: + +- `public` events are exported with normal mapped content. +- `private` events remain present in the feed at normal date/time positions, but with redaction: + - `SUMMARY` must be `Private Event` + - `DTSTART`/`DTEND` remain populated + - `DESCRIPTION`, `LOCATION`, and `CATEGORIES` must be omitted from public ICS output + ## Recurrence Mapping Recurring entries should be represented with `RRULE` where possible. @@ -84,7 +94,7 @@ If specific recurrence shapes cannot be represented in a single RRULE, plugin ma - Generation errors should fail safely with observable error handling. ## Security and Privacy -- Export must include only events intended for public/user-facing distribution. +- Export must enforce visibility policy: private events are included with redacted content in public ICS output. - Internal/admin-only metadata must not leak into ICS properties. ## Verification Requirements diff --git a/requirements/ics_endpoint.md b/requirements/ics_endpoint.md index b252bbf..dad6a48 100644 --- a/requirements/ics_endpoint.md +++ b/requirements/ics_endpoint.md @@ -26,13 +26,14 @@ This document covers: ## Access Policy - Access mode is configurable in `Setup`: - - `public_read`: endpoint accessible without auth, only public events included + - `public_read`: endpoint accessible without auth; includes both public and private events, with private events redacted - `authenticated_read`: endpoint requires authenticated access - Default mode: `public_read` unless overridden by policy requirements. ## Data Scope - ICS output includes only events intended for the selected endpoint audience. - Internal/admin-only metadata must never appear in output. +- In `public_read` mode, private events remain in timeline position but expose only date/time and the title `Private Event`. - Recurrence and exception semantics follow `requirements/ics.md` and `requirements/recurrence_exceptions.md`. ## Stability and Subscription diff --git a/requirements/web-ui.md b/requirements/web-ui.md index c4f5589..efbc96b 100644 --- a/requirements/web-ui.md +++ b/requirements/web-ui.md @@ -83,6 +83,16 @@ Minimum display requirements: - Category (if provided) - Description excerpt/summary (if configured for display) +Privacy display rules: + +- Public events render full details per normal display rules. +- Private events remain visible in chronological position for all viewers. +- In public (not logged-in) UI and sidebar rendering, private events must display: + - Title as `Private Event` + - Date/time fields as normal + - No location/category/description content +- In logged-in write-capable UI, private events display full details. + ## Web Event Editing UX (Authorized Users) Where the viewer has write permission, the web UI must support event creation/editing. @@ -94,6 +104,7 @@ Requirements: - Irrelevant recurrence fields must be hidden based on current recurrence selections. - Form controls use persistent inline/floating labels so field purpose remains visible after input. - Event ID must not be shown in the UI. +- Create/edit panel must show current privacy (`public`/`private`) and allow changing it before save. - `location` and `category` should be rendered on one row. - `start date/time` and `end date/time` should be rendered on one row. - `repeat type` and `every` interval should be rendered on one row. @@ -112,6 +123,7 @@ Requirements: ## Login and Access Modes - `/calendar` must support two user modes: public (not logged in) and logged-in. - Public mode is read-only. +- In public mode, private events are redacted (title `Private Event`; time preserved; non-time fields hidden). - A `Login` action must open an in-page dialog. - Login dialog must support credential login. - Login dialog must support registration (registration is a request for approval/write access). @@ -149,3 +161,4 @@ Acceptance should verify: - All five view modes are selectable and render expected event windows. - Empty-state behavior is clear and user-friendly. - ICS link is present and returns valid calendar payload. +- Privacy redaction behavior is correct in public views, sidebar, and logged-in views. diff --git a/scripts/deploy_remote.sh b/scripts/deploy_remote.sh new file mode 100755 index 0000000..58aac1f --- /dev/null +++ b/scripts/deploy_remote.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ROOT_DIR}/credentials/.env" + +if [[ -f "${ENV_FILE}" ]]; then + # shellcheck disable=SC1090 + source "${ENV_FILE}" +fi + +ARTIFACT="" +WP_ROOT="${REMOTE_WP_PATH:-/var/www/wordpress}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --artifact) + ARTIFACT="${2:-}" + shift 2 + ;; + --wp-root) + WP_ROOT="${2:-}" + shift 2 + ;; + --help|-h) + cat <<'USAGE' +Deploy a plugin artifact to remote WordPress. + +Usage: + scripts/deploy_remote.sh [--artifact /abs/or/relative/path.zip] [--wp-root /var/www/wordpress] + +Defaults: +- Artifact: latest ./package/calendar-plugin-*.zip +- Remote host settings from credentials/.env + +This script enforces ownership: +- chown -R www-data:www-data +USAGE + exit 0 + ;; + *) + echo "[deploy] unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +if [[ -z "${ARTIFACT}" ]]; then + ARTIFACT="$(ls -1 "${ROOT_DIR}/package/calendar-plugin-"*.zip 2>/dev/null | sort -V | tail -n1 || true)" +fi + +if [[ -z "${ARTIFACT}" ]]; then + echo "[deploy] no artifact found; run scripts/package_plugin.sh first" >&2 + exit 1 +fi + +if [[ "${ARTIFACT}" != /* ]]; then + ARTIFACT="${ROOT_DIR}/${ARTIFACT#./}" +fi + +if [[ ! -f "${ARTIFACT}" ]]; then + echo "[deploy] artifact not found: ${ARTIFACT}" >&2 + exit 1 +fi + +MANIFEST="${ARTIFACT%.zip}.manifest.sha256" +if [[ ! -f "${MANIFEST}" ]]; then + echo "[deploy] warning: manifest not found beside artifact (${MANIFEST}); continuing" >&2 +fi + +required=(REMOTE_HOST REMOTE_PORT REMOTE_USER REMOTE_SSH_KEY_PATH REMOTE_APP_DIR REMOTE_WP_CLI WP_URL) +for key in "${required[@]}"; do + if [[ -z "${!key:-}" ]]; then + echo "[deploy] missing required env var: ${key}" >&2 + exit 1 + fi +done + +SSH_KEY_PATH="${REMOTE_SSH_KEY_PATH}" +if [[ "${SSH_KEY_PATH}" != /* ]]; then + SSH_KEY_PATH="${ROOT_DIR}/${SSH_KEY_PATH}" +fi + +if [[ ! -f "${SSH_KEY_PATH}" ]]; then + echo "[deploy] ssh key not found: ${SSH_KEY_PATH}" >&2 + exit 1 +fi + +artifact_base="$(basename -- "${ARTIFACT}")" +stage_name="${artifact_base%.zip}" +STAGE_DIR="/tmp/${stage_name}" + +SSH=(ssh -F /dev/null -i "${SSH_KEY_PATH}" -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}") +RSYNC=(rsync -avz --delete -e "ssh -F /dev/null -i ${SSH_KEY_PATH} -p ${REMOTE_PORT} -o StrictHostKeyChecking=accept-new") + +echo "[deploy] staging on remote: ${STAGE_DIR}" +"${SSH[@]}" "mkdir -p '${STAGE_DIR}'" + +if [[ -f "${MANIFEST}" ]]; then + "${RSYNC[@]}" "${ARTIFACT}" "${MANIFEST}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/" +else + "${RSYNC[@]}" "${ARTIFACT}" "${REMOTE_USER}@${REMOTE_HOST}:${STAGE_DIR}/" +fi + +"${SSH[@]}" "set -euo pipefail; rm -rf '${STAGE_DIR}/extracted'; mkdir -p '${STAGE_DIR}/extracted'; unzip -q '${STAGE_DIR}/${artifact_base}' -d '${STAGE_DIR}/extracted'; test -f '${STAGE_DIR}/extracted/calendar-plugin/calendar-plugin.php'" +"${SSH[@]}" "set -euo pipefail; rsync -a --delete '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/'" +"${SSH[@]}" "set -euo pipefail; chown -R www-data:www-data '${REMOTE_APP_DIR}'" +"${SSH[@]}" "set -euo pipefail; '${REMOTE_WP_CLI}' --path='${WP_ROOT}' plugin activate calendar-plugin --allow-root >/dev/null 2>&1 || true" + +# Exact-match style checksum dry-run check +"${SSH[@]}" "set -euo pipefail; rsync -avznc --delete '${STAGE_DIR}/extracted/calendar-plugin/' '${REMOTE_APP_DIR}/' >/tmp/codex_rsync_check.out; if grep -Eq '^[^./]|^\./' /tmp/codex_rsync_check.out; then cat /tmp/codex_rsync_check.out; exit 1; fi" + +# Ownership sanity check: must be zero mismatches +non_owned="$("${SSH[@]}" "set -euo pipefail; find '${REMOTE_APP_DIR}' \( ! -user www-data -o ! -group www-data \) | wc -l")" +if [[ "${non_owned}" != "0" ]]; then + echo "[deploy] ownership check failed: ${non_owned} path(s) not owned by www-data:www-data" >&2 + exit 1 +fi + +# Resolve slug for endpoint checks +url_slug="$("${SSH[@]}" "'${REMOTE_WP_CLI}' --path='${WP_ROOT}' option get calendar_plugin_url_slug --allow-root 2>/dev/null || true" | tr -d '\r' | tail -n1)" +if [[ -n "${url_slug}" ]]; then + ICS_URL="${WP_URL%/}/${url_slug}/calendar.ics" + CALDAV_URL="${WP_URL%/}/${url_slug}/caldav/" +else + ICS_URL="${WP_URL%/}/calendar.ics" + CALDAV_URL="${WP_URL%/}/caldav/" +fi + +health_code="$(curl -sS -o /tmp/codex_health.out -w '%{http_code}' "${WP_URL%/}/wp-json/calendar/v1/health" || true)" +ics_code="$(curl -sS -o /tmp/codex_ics.out -w '%{http_code}' "${ICS_URL}" || true)" +caldav_code="$(curl -sS -o /tmp/codex_caldav.out -w '%{http_code}' "${CALDAV_URL}" || true)" + +echo "[deploy] plugin deployed: ${artifact_base}" +echo "[deploy] ownership: www-data:www-data (verified)" +echo "[deploy] checks: health=${health_code} ics=${ics_code} caldav_unauth=${caldav_code}" +echo "[deploy] urls: ${ICS_URL} ${CALDAV_URL}" diff --git a/scripts/package_plugin.sh b/scripts/package_plugin.sh new file mode 100755 index 0000000..054db76 --- /dev/null +++ b/scripts/package_plugin.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ROOT_DIR}/credentials/.env" + +if [[ -f "${ENV_FILE}" ]]; then + # shellcheck disable=SC1090 + source "${ENV_FILE}" +fi + +VERSION="" +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + VERSION="${2:-}" + shift 2 + ;; + --help|-h) + cat <<'USAGE' +Create a plugin package artifact from ./code. + +Usage: + scripts/package_plugin.sh [--version X.Y.Z] + +Notes: +- If --version is not provided, reads Version from code/calendar-plugin.php. +- Writes zip + manifest to ./package. +USAGE + exit 0 + ;; + *) + echo "[package] unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +if [[ -z "${VERSION}" ]]; then + VERSION="$(sed -n 's/^ \* Version: \(.*\)$/\1/p' "${ROOT_DIR}/code/calendar-plugin.php" | head -n1 | tr -d '[:space:]')" +fi + +if [[ -z "${VERSION}" ]]; then + echo "[package] unable to detect plugin version from code/calendar-plugin.php" >&2 + exit 1 +fi + +SLUG="calendar-plugin" +PACKAGE_DIR="${ROOT_DIR}/package" +STAGING_DIR="${PACKAGE_DIR}/staging/${SLUG}" +ARTIFACT="${PACKAGE_DIR}/${SLUG}-${VERSION}.zip" +MANIFEST="${PACKAGE_DIR}/${SLUG}-${VERSION}.manifest.sha256" + +mkdir -p "${PACKAGE_DIR}/staging" +rm -rf "${STAGING_DIR}" +mkdir -p "${STAGING_DIR}" + +rsync -a --delete "${ROOT_DIR}/code/" "${STAGING_DIR}/" + +( + cd "${PACKAGE_DIR}/staging" + zip -qr "${ARTIFACT}" "${SLUG}" +) + +( + cd "${STAGING_DIR}" + find . -type f -print0 | sort -z | xargs -0 sha256sum +) > "${MANIFEST}" + +echo "[package] created artifact: ${ARTIFACT}" +echo "[package] created manifest: ${MANIFEST}"