PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i5h-3LL

HOME


5h-3LL 1.0
DIR: /srv/http/vyvoj.adent.cz/egroupware/pfi/calendar/inc
/srv/http/vyvoj.adent.cz/egroupware/pfi/calendar/inc/
Upload File:
Current File : /srv/http/vyvoj.adent.cz/egroupware/pfi/calendar/inc/class.socal.inc.php
<?php
/**
 * eGroupWare - Calendar's storage-object
 *
 * @link http://www.egroupware.org
 * @package calendar
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
 * @copyright (c) 2005-7 by RalfBecker-At-outdoor-training.de
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @version $Id: class.socal.inc.php 24806 2007-12-13 16:06:41Z ralfbecker $
 */

/**
 * some necessary defines used by the calendar
 */
if(extension_loaded('mcal') == False)
{
	define('MCAL_RECUR_NONE',0);
	define('MCAL_RECUR_DAILY',1);
	define('MCAL_RECUR_WEEKLY',2);
	define('MCAL_RECUR_MONTHLY_MDAY',3);
	define('MCAL_RECUR_MONTHLY_WDAY',4);
	define('MCAL_RECUR_YEARLY',5);
	define('MCAL_RECUR_SECONDLY',6);
	define('MCAL_RECUR_MINUTELY',7);
	define('MCAL_RECUR_HOURLY',8);

	define('MCAL_M_SUNDAY',1);
	define('MCAL_M_MONDAY',2);
	define('MCAL_M_TUESDAY',4);
	define('MCAL_M_WEDNESDAY',8);
	define('MCAL_M_THURSDAY',16);
	define('MCAL_M_FRIDAY',32);
	define('MCAL_M_SATURDAY',64);

	define('MCAL_M_WEEKDAYS',62);
	define('MCAL_M_WEEKEND',65);
	define('MCAL_M_ALLDAYS',127);
}

define('REJECTED',0);
define('NO_RESPONSE',1);
define('TENTATIVE',2);
define('ACCEPTED',3);

/**
 * Class to store all calendar data (storage object)
 *
 * Tables used by socal:
 *	- egw_cal: general calendar data: cal_id, title, describtion, locations, ...
 *	- egw_cal_dates: start- and enddates (multiple entry per cal_id for recuring events!)
 *	- egw_cal_users: participant info including status (multiple entries per cal_id AND startdate for recuring events)
 * 	- egw_cal_repeats: recur-data: type, optional enddate, etc.
 *  - egw_cal_extra: custom fields (multiple entries per cal_id possible)
 *
 * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
 *  UI only operates in user-time, so there have to be no conversation at all !!!
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
 *  SO operates only on server-time
 */
class socal
{
	/**
	 * name of the main calendar table and prefix for all other calendar tables
	 */
	var $cal_table = 'egw_cal';
	var $extra_table,$repeats_table,$user_table,$dates_table,$all_tables;

	/**
	 * internal copy of the global db-object
	 * 
	 * @var egw_db
	 */
	var $db;
	/**
	 * instance of the async object
	 *
	 * @var asyncservice
	 */
	var $async;

	/**
	 * Constructor of the socal class
	 */
	function socal()
	{
		foreach(array(
			'async' => 'phpgwapi.asyncservice',
		) as $my => $app_class)
		{
			list(,$class) = explode('.',$app_class);

			if (!is_object($GLOBALS['egw']->$class))
			{
				//echo "<p>calling CreateObject($app_class)</p>\n".str_repeat(' ',4096);
				$GLOBALS['egw']->$class =& CreateObject($app_class);
			}
			$this->$my =& $GLOBALS['egw']->$class;
		}
		$this->db = clone($GLOBALS['egw']->db);
		$this->db->set_app('calendar');

		$this->all_tables = array($this->cal_table);
		foreach(array('extra','repeats','user','dates') as $name)
		{
			$vname = $name.'_table';
			$this->all_tables[] = $this->$vname = $this->cal_table.'_'.$name;
		}
	}

	/**
	 * reads one or more calendar entries
	 *
	 * All times (start, end and modified) are returned as timesstamps in servertime!
	 *
	 * @param int/array/string $ids id or array of id's of the entries to read, or string with a single uid
	 * @param int $recur_date=0 if set read the next recurrance at or after the timestamp, default 0 = read the initital one
	 * @return array/boolean array with id => data pairs or false if entry not found
	 */
	function read($ids,$recur_date=0)
	{
		//echo "<p>socal::read(".print_r($ids,true).",$recur_date)<br />\n".function_backtrace()."<p>\n";

		$table_def = $this->db->get_table_definitions('calendar',$this->cal_table);
		$group_by_cols = $this->cal_table.'.'.implode(','.$this->cal_table.'.',array_keys($table_def['fd']));
		$table_def = $this->db->get_table_definitions('calendar',$this->repeats_table);
		$group_by_cols .= ','.$this->repeats_table.'.'.implode(','.$this->repeats_table.'.',array_keys($table_def['fd']));

		$where = array();
		if (is_array($ids))
		{
			array_walk($ids,create_function('&$val,$key','$val = (int) $val;'));

			$where[] = $this->cal_table.'.cal_id IN ('.implode(',',$ids).')';
		}
		elseif (is_numeric($ids))
		{
			$where[] = $this->cal_table.'.cal_id = '.(int) $ids;
		}
		else
		{
			$where['cal_uid'] = $ids;
		}
		if ((int) $recur_date)
		{
			$where[] = 'cal_start >= '.(int)$recur_date;
		}
		$this->db->select($this->cal_table,"$this->repeats_table.*,$this->cal_table.*,MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end",
			$where,__LINE__,__FILE__,false,'GROUP BY '.$group_by_cols,false,0,
			",$this->dates_table LEFT JOIN $this->repeats_table ON $this->dates_table.cal_id=$this->repeats_table.cal_id".
			" WHERE $this->cal_table.cal_id=$this->dates_table.cal_id");

		$events = false;
		while (($row = $this->db->row(true,'cal_')))
		{
			$row['recur_exception'] = $row['recur_exception'] ? explode(',',$row['recur_exception']) : array();
			if (!$row['recur_type']) $row['recur_type'] = MCAL_RECUR_NONE;
			$row['alarm'] = array();
			$events[$row['id']] = $row;

			// if a uid was supplied, convert it for the further code to an id
			if (!is_array($ids) && !is_numeric($ids)) $ids = $row['id'];
		}
		if (!$events) return false;

		// check if we have a real recurance, if not set $recur_date=0
		if (is_array($ids) || $events[(int)$ids]['recur_type'] == MCAL_RECUR_NONE)
		{
			$recur_date = 0;
		}
		else	// adjust the given recurance to the real time, it can be a date without time(!)
		{
			if ($recur_date)
			{
				// also remember recur_date, maybe we need it later, duno now
				$recur_date = $events[$ids]['recur_date'] = $events[$ids]['start'];
			}
		}

		// participants, if a recur_date give, we read that recurance, else the one users from the default entry with recur_date=0
		$this->db->select($this->user_table,'*',array(
			'cal_id'      => $ids,
			'cal_recur_date' => $recur_date,
		),__LINE__,__FILE__,false,'ORDER BY cal_user_type DESC');	// DESC puts users before resources and contacts
		while (($row = $this->db->row(true)))
		{
			// if the type is not an ordinary user (eg. contact or resource)...
			if ($row['cal_user_type'] && $row['cal_user_type'] != 'u')
			{
				// prefix the id with the type
				$user_id = $row['cal_user_type'].$row['cal_user_id'];
				// and append quantity
				$row['cal_status'] .= $row['cal_quantity'] == 1 ? '' : $row['cal_quantity'];
			}
			else
			{
				$user_id = (int) $row['cal_user_id'];
			}
			$events[$row['cal_id']]['participants'][$user_id] = $row['cal_status'];
			$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][$row['cal_user_id']] = $row['cal_status'];
		}

		// custom fields
		$this->db->select($this->extra_table,'*',array('cal_id'=>$ids),__LINE__,__FILE__);
		while (($row = $this->db->row(true)))
		{
			$events[$row['cal_id']]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
		}

		// alarms, atm. we read all alarms in the system, as this can be done in a single query
		foreach((array)$this->async->read('cal'.(is_array($ids) ? '' : ':'.(int)$ids).':%') as $id => $job)
		{
			list(,$cal_id) = explode(':',$id);
			if (!isset($events[$cal_id])) continue;	// not needed

			$alarm         = $job['data'];	// text, enabled
			$alarm['id']   = $id;
			$alarm['time'] = $job['next'];

			$events[$cal_id]['alarm'][$id] = $alarm;
		}
		//echo "<p>socal::read(".print_r($ids,true).")=<pre>".print_r($events,true)."</pre>\n";
		return $events;
	}

	/**
	 * generate SQL to filter after a given category (evtl. incl. subcategories)
	 *
	 * @param array/int $cat_id cat-id or array of cat-ids, or !$cat_id for none
	 * @return string SQL to include in the query
	 */
	function cat_filter($cat_id)
	{
		$sql = '';
		if ($cat_id)
		{
			if (!is_array($cat_ids) && !@$GLOBALS['egw_info']['user']['preferences']['common']['cats_no_subs'])
			{
				if (!is_object($GLOBALS['egw']->categories))
				{
					$GLOBALS['egw']->categories =& CreateObject('phpgwapi.categories');
				}
				$cats = $GLOBALS['egw']->categories->return_all_children($cat_id);
			}
			else
			{
				$cats = is_array($cat_id) ? $cat_id : array($cat_id);
			}
			array_walk($cats,create_function('&$val,$key','$val = (int) $val;'));

			$sql = '(cal_category'.(count($cats) > 1 ? " IN ('".implode("','",$cats)."')" : '='.$this->db->quote((int)$cat_id));
			foreach($cats as $cat)
			{
				$sql .= ' OR '.$this->db->concat("','",'cal_category',"','").' LIKE '.$this->db->quote('%,'.$cat.',%');
			}
			$sql .= ') ';
		}
		return $sql;
	}

	/**
	 * Searches / lists calendar entries, including repeating ones
	 *
	 * @param int $start startdate of the search/list (servertime)
	 * @param int $end enddate of the search/list (servertime)
	 * @param int/array $users user-id or array of user-id's, !$users means all entries regardless of users
	 * @param int $cat_id=0 mixed category-id or array of cat-id's, default 0 = all
	 *		Please note: only a single cat-id, will include all sub-cats (if the common-pref 'cats_no_subs' is False)
	 * @param string $filter='' string filter-name, atm. all or hideprivate
	 * @param string $query='' pattern so search for, if unset or empty all matching entries are returned (no search)
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
	 * @param int/bool $offset=False offset for a limited query or False (default)
	 * @param int $num_rows=0 number of rows to return if offset set, default 0 = use default in user prefs
	 * @param string $order='cal_start' column-names plus optional DESC|ASC separted by comma
	 * @param boolean $show_rejected=true should the search return rejected invitations
	 * @return array of cal_ids, or false if error in the parameters
	 *
	 * ToDo: search custom-fields too
	 */
	function &search($start,$end,$users,$cat_id=0,$filter='',$query='',$offset=False,$num_rows=0,$order = 'cal_start',$show_rejected=true)
	{
		//echo '<p>socal::search('.($start ? date('Y-m-d H:i',$start) : '').','.($end ? date('Y-m-d H:i',$end) : '').','.print_r($users,true).','.print_r($cat_id,true).",'$filter',".print_r($query,true).",$offset,$num_rows)</p>\n";

		$where = array();
		if (is_array($query))
		{
			$where = $query;
		}
		elseif ($query)
		{
			foreach(array('cal_title','cal_description','cal_location') as $col)
			{
				$to_or[] = $col . ' LIKE ' . $this->db->quote('%'.$query.'%');
			}
			$where[] = '('.implode(' OR ',$to_or).')';
		}
		if ($users)
		{
			$users_by_type = array();
			foreach(is_array($users) ? $users : array($users) as $user)
			{
				if (is_numeric($user))
				{
					$users_by_type['u'][] = (int) $user;
				}
				elseif (is_numeric(substr($user,1)))
				{
					$users_by_type[$user{0}][] = (int) substr($user,1);
				}
			}
			$to_or = array();
			foreach($users_by_type as $type => $ids)
			{
				$to_or[] = $this->db->expression($this->user_table,array(
					'cal_user_type' => $type,
					'cal_user_id'   => $ids,
				));
			}
			$where[] = '('.implode(' OR ',$to_or).')';

			if (!$show_rejected) $where[] = "cal_status != 'R'";
		}
		if ($cat_id)
		{
			$where[] = $this->cat_filter($cat_id);
		}
		if ($start) $where[] = (int)$start.' < cal_end';
		if ($end)   $where[] = 'cal_start < '.(int)$end;

		if (!preg_match('/^[a-z_ ,]+$/i',$order)) $order = 'cal_start';		// gard against SQL injunktion

		if ($this->db->capabilities['distinct_on_text'] && $this->db->capabilities['union'])
		{
			// changed the original OR in the query into a union, to speed up the query execution under MySQL 5
			$select = array(
				'table' => $this->cal_table,
				'join'  => "JOIN $this->dates_table ON $this->cal_table.cal_id=$this->dates_table.cal_id JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id",
				'cols'  => "$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date",
				'where' => $where,
			);
			$selects = array($select,$select);
			$selects[0]['where'][] = 'recur_type IS NULL AND cal_recur_date=0';
			$selects[1]['where'][] = 'cal_recur_date=cal_start';

			if (is_numeric($offset))	// get the total too
			{
				// we only select cal_table.cal_id (and not cal_table.*) to be able to use DISTINCT (eg. MsSQL does not allow it for text-columns)
				$selects[0]['cols'] = $selects[1]['cols'] = "DISTINCT $this->repeats_table.*,$this->cal_table.cal_id,cal_start,cal_end,cal_recur_date";

				$this->db->union($selects,__LINE__,__FILE__);
				$this->total = $this->db->num_rows();

				$selects[0]['cols'] = $selects[1]['cols'] = $select['cols'];	// restore the original cols
			}
			$this->db->union($selects,__LINE__,__FILE__,$order,$offset,$num_rows);
		}
		else	// MsSQL oder MySQL 3.23
		{
			$where[] = '(recur_type IS NULL AND cal_recur_date=0 OR cal_recur_date=cal_start)';

			//_debug_array($where);
			if (is_numeric($offset))	// get the total too
			{
				// we only select cal_table.cal_id (and not cal_table.*) to be able to use DISTINCT (eg. MsSQL does not allow it for text-columns)
				$this->db->select($this->cal_table,"DISTINCT $this->repeats_table.*,$this->cal_table.cal_id,cal_start,cal_end,cal_recur_date",
					$where,__LINE__,__FILE__,false,'',false,0,
					"JOIN $this->dates_table ON $this->cal_table.cal_id=$this->dates_table.cal_id JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id");

				$this->total = $this->db->num_rows();
			}
			$this->db->select($this->cal_table,($this->db->capabilities['distinct_on_text'] ? 'DISTINCT ' : '').
				"$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date",
				$where,__LINE__,__FILE__,$offset,'ORDER BY '.$order,false,$num_rows,
				"JOIN $this->dates_table ON $this->cal_table.cal_id=$this->dates_table.cal_id JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id");
		}
		$events = $ids = $recur_dates = $recur_ids = array();
		while (($row =& $this->db->row(true,'cal_')))
		{
			$ids[] = $id = $row['id'];
			if ($row['recur_date'])
			{
				$id .= '-'.$row['recur_date'];
				$recur_dates[] = $row['recur_date'];
			}
			$row['alarm'] = array();
			$row['recur_exception'] = $row['recur_exception'] ? explode(',',$row['recur_exception']) : array();

			$events[$id] = $row;
		}

		if (count($events))
		{
			// now ready all users with the given cal_id AND (cal_recur_date=0 or the fitting recur-date)
			// This will always read the first entry of each recuring event too, we eliminate it later
			$recur_dates[] = 0;
			$this->db->select($this->user_table,'*',array(
				'cal_id' => array_unique($ids),
				'cal_recur_date' => $recur_dates,
			),__LINE__,__FILE__,false,'ORDER BY cal_id,cal_user_type DESC');	// DESC puts users before resources and contacts

			while (($row = $this->db->row(true)))
			{
				$id = $row['cal_id'];
				if ($row['cal_recur_date']) $id .= '-'.$row['cal_recur_date'];

				if (!isset($events[$id])) continue;		// not needed first entry of recuring event

				$events[$id]['participants'][$this->combine_user($row['cal_user_type'],$row['cal_user_id'])] = $row['cal_status'];
			}
/*			custom fields are not shown in the regular views, so we can ignore them here for the moment
			$this->db->select($this->extra_table,'*',array('cal_id'=>$ids),__LINE__,__FILE__);
			while (($row = $this->db->row(true)))
			{
				$set_ids = array($row['cal_id']);
				if (isset($recur_ids[$row['cal_id']])) $set_ids += $recur_ids[$row['cal_id']];

				foreach($set_ids as $id)
				{
					if (isset($events[$cal_id]))
					{
						$events[$id]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
					}
				}
			}
*/
			// alarms, atm. we read all alarms in the system, as this can be done in a single query
			foreach((array)$this->async->read('cal'.(is_array($ids) ? '' : ':'.(int)$ids).':%') as $id => $job)
			{
				list(,$cal_id) = explode(':',$id);

				$alarm         = $job['data'];	// text, enabled
				$alarm['id']   = $id;
				$alarm['time'] = $job['next'];

				$event_start = $alarm['time'] + $alarm['offset'];

				if (isset($events[$cal_id]))	// none recuring event
				{
					$events[$cal_id]['alarm'][$id] = $alarm;
				}
				elseif (isset($events[$cal_id.'-'.$event_start]))	// recuring event
				{
					$events[$cal_id.'-'.$event_start]['alarm'][$id] = $alarm;
				}
			}
		}
		//echo "<p>socal::search\n"; _debug_array($events);
		return $events;
	}

	/**
	 * Checks for conflicts
	 */

/* folowing SQL checks for conflicts completly on DB level

SELECT cal_user_type, cal_user_id, SUM( cal_quantity )
FROM egw_cal, egw_cal_dates, egw_cal_user
LEFT JOIN egw_cal_repeats ON egw_cal.cal_id = egw_cal_repeats.cal_id
WHERE egw_cal.cal_id = egw_cal_dates.cal_id
AND egw_cal.cal_id = egw_cal_user.cal_id
AND (
recur_type IS NULL
AND cal_recur_date =0
OR cal_recur_date = cal_start
)
AND (
(
cal_user_type = 'u'			# user of the checked event
AND cal_user_id
IN ( 7, 5 )
)
AND 1118822400 < cal_end	# start- and end-time of the checked event
AND cal_start <1118833200
)
AND egw_cal.cal_id !=26		# id of the checked event
AND cal_non_blocking !=1
AND cal_status != 'R'
GROUP BY cal_user_type, cal_user_id
ORDER BY cal_user_type, cal_usre_id

*/

	/**
	 * Saves or creates an event
	 *
	 * We always set cal_modified and cal_modifier and for new events cal_uid.
	 * All other column are only written if they are set in the $event parameter!
	 *
	 * @param array $event
	 * @param boolean &$set_recurrences on return: true if the recurrences need to be written, false otherwise
	 * @param int $change_since=0 time from which on the repetitions should be changed, default 0=all
	 * @return boolean/int false on error, cal_id otherwise
	 */
	function save($event,&$set_recurrences,$change_since=0)
	{
		//echo "<p>socal::save(,$change_since) event="; _debug_array($event);

		$cal_id = (int) $event['id'];
		unset($event['id']);

		$set_recurrences = !$cal_id && $event['recur_type'] != MCAL_RECUR_NONE;

		// add colum prefix 'cal_' if there's not already a 'recur_' prefix
		foreach($event as $col => $val)
		{
			if ($col{0} != '#' && substr($col,0,6) != 'recur_' && $col != 'alarm')
			{
				$event['cal_'.$col] = $val;
				unset($event[$col]);
			}
		}
		if (is_array($event['cal_category'])) $event['cal_category'] = implode(',',$event['cal_category']);

		if ($cal_id)
		{
			$this->db->update($this->cal_table,$event,array('cal_id' => $cal_id),__LINE__,__FILE__);
		}
		else
		{
			if (!$event['cal_owner']) $event['cal_owner'] = $GLOBALS['egw_info']['user']['account_id'];

			if (!$event['cal_id'] && !isset($event['cal_uid'])) $event['cal_uid'] = '';	// uid is NOT NULL!

			$this->db->insert($this->cal_table,$event,false,__LINE__,__FILE__);
			if (!($cal_id = $this->db->get_last_insert_id($this->cal_table,'cal_id')))
			{
				return false;
			}
			// new event (without uid) or new created referencing event => create new uid
			if (!$event['cal_uid'] || $event['cal_reference'] && strpos($event['cal_uid'],'cal-'.$event['calreference'].'-') !== false)
			{
				$event['cal_uid'] = $GLOBALS['egw']->common->generate_uid('calendar',$cal_id);
				$this->db->update($this->cal_table,array('cal_uid' => $event['cal_uid']),array('cal_id' => $cal_id),__LINE__,__FILE__);
			}
			// new events need to have at least one participant, default to the owner
			if (!isset($event['cal_participants']))
			{
				$event['cal_participants'] = array($event['cal_owner'] => 'A');
			}
		}
		// write information about recuring event, if recur_type is present in the array
		if (isset($event['recur_type']))
		{
			if (isset($event['recur_exception']) && is_array($event['recur_exception']) && count($event['recur_exception']))
			{
				// delete execeptions from the user and dates table, it could be the first time
				$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__);
				$this->db->delete($this->dates_table,array('cal_id' => $cal_id,'cal_start' => $event['recur_exception']),__LINE__,__FILE__);

				$event['recur_exception'] = implode(',',$event['recur_exception']);
			}
			else
			{
				$event['recur_exception'] = null;
			}
			if (!$set_recurrences)
			{
				// check if the recure-information changed
				$this->db->select($this->repeats_table,'*',array('cal_id' => $cal_id),__LINE__,__FILE__);
				$old_recur = $this->db->row(true);
				$old_exceptions = $old_recur['recur_exception'] ? explode(',',$old_recur['recur_exception']) : array();
				$exceptions = $event['recur_exception'] ? explode(',',$event['recur_exception']) : array();
				$set_recurrences = $event['recur_type'] != $old_recur['recur_type'] || $event['recur_data'] != $old_recur['recur_data'] ||
					$event['recur_interval'] != $old_recur['recur_interval'] || $event['recur_enddate'] != $old_recur['recur_enddate'] ||
					count(array_diff($old_exceptions,$exceptions));	// exception deleted
			}
			if($event['recur_type'] != MCAL_RECUR_NONE)
			{
				$this->db->insert($this->repeats_table,$event,array('cal_id' => $cal_id),__LINE__,__FILE__);
			}
			else
			{
				$this->db->delete($this->repeats_table,array('cal_id' => $cal_id),__LINE__,__FILE__);
			}
			if ($set_recurrences)
			{
				// delete all, but the lowest dates record
				$this->db->select($this->dates_table,'MIN(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__);
				$min = $this->db->next_record() ? $this->db->f(0) : 0;

				$this->db->delete($this->dates_table,array(
					'cal_id' => $cal_id,
					'cal_start > '.(int)$min,
				),__LINE__,__FILE__);
				// delete all user-records, with recur-date != 0
				$this->db->delete($this->user_table,array(
					'cal_id' => $cal_id,
					'cal_recur_date != 0',
				),__LINE__,__FILE__);
			}
		}
		// update start- and endtime if present in the event-array, evtl. we need to move all recurrences
		if (isset($event['cal_start']) && isset($event['cal_end']))
		{
			$this->move($cal_id,$event['cal_start'],$event['cal_end'],!$cal_id ? false : $change_since);
		}
		// update participants if present in the event-array
		if (isset($event['cal_participants']))
		{
			$this->participants($cal_id,$event['cal_participants'],!$cal_id ? false : $change_since);
		}
		// Custom fields
		foreach($event as $name => $value)
		{
			if ($name[0] == '#')
			{
				if (strlen($value))
				{
					$this->db->insert($this->extra_table,array(
						'cal_extra_value'	=> $value,
					),array(
						'cal_id'			=> $cal_id,
						'cal_extra_name'	=> substr($name,1),
					),__LINE__,__FILE__);
				}
				else
				{
					$this->db->delete($this->extra_table,array(
						'cal_id'			=> $cal_id,
						'cal_extra_name'	=> substr($name,1),
					),__LINE__,__FILE__);
				}
			}
		}
		// updating or saving the alarms, new alarms have a temporary numeric id!
		// ToDo: recuring events !!!
		if (is_array($event['alarm']))
		{
			foreach ($event['alarm'] as $id => $alarm)
			{
				if (is_numeric($id)) unset($alarm['id']);	// unset the temporary id, to add the alarm

				$alarm['time'] = $event['cal_start'] - $alarm['offset'];	// recalculate the offset, as the start-time might have changed

				$this->save_alarm($cal_id,$alarm);
			}
		}
		return $cal_id;
	}

	/**
	 * moves an event to an other start- and end-time taken into account the evtl. recurrences of the event(!)
	 *
	 * @param int $cal_id
	 * @param int $start new starttime
	 * @param int $end new endtime
	 * @param int/boolean $change_since=0 false=new entry, > 0 time from which on the repetitions should be changed, default 0=all
	 * @param int $old_start=0 old starttime or (default) 0, to query it from the db
	 * @param int $old_end=0 old starttime or (default) 0
	 * @return int/boolean number of moved recurrences or false on error
	 */
	function move($cal_id,$start,$end,$change_since=0,$old_start=0,$old_end=0)
	{
		//echo "<p>socal::move($cal_id,$start,$end,$change_since,$old_start,$old_end)</p>\n";

		if (!(int) $cal_id) return false;

		if (!$old_start)
		{
			if ($change_since !== false) $this->db->select($this->dates_table,'MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end',
				array('cal_id'=>$cal_id),__LINE__,__FILE__);
			// if no recurrence found, create one with the new dates
			if ($change_since === false || !($row = $this->db->row(true)) || !$row['cal_start'] || !$row['cal_end'])
			{
				$this->db->insert($this->dates_table,array(
					'cal_id'    => $cal_id,
					'cal_start' => $start,
					'cal_end'   => $end,
				),false,__LINE__,__FILE__);

				return 1;
			}
			$move_start = (int) ($start-$row['cal_start']);
			$move_end   = (int) ($end-$row['cal_end']);
		}
		else
		{
			$move_start = (int) ($start-$old_start);
			$move_end   = (int) ($end-$old_end);
		}
		$where = $this->db->expression($this->cal_table,array('cal_id' => $cal_id));

		if ($move_start)
		{
			// move the recur-date of the participants
			$this->db->query("UPDATE $this->user_table SET cal_recur_date=cal_recur_date+$move_start WHERE $where AND cal_recur_date ".
				((int)$change_since ? '>= '.(int)$change_since : '!= 0'));
		}
		if ($move_start || $move_end)
		{
			// move the event and it's recurrences
			$this->db->query("UPDATE $this->dates_table SET cal_start=cal_start+$move_start,cal_end=cal_end+$move_end WHERE $where".
				((int) $change_since ? ' AND cal_start >= '.(int) $change_since : ''));
		}
		return $this->db->affected_rows();
	}

	/**
	 * combines user_type and user_id into a single string or integer (for users)
	 *
	 * @param string $user_type 1-char type: 'u' = user, ...
	 * @param int $user_id id
	 * @return string/int combined id
	 */
	function combine_user($user_type,$user_id)
	{
		if (!$user_type || $user_type == 'u')
		{
			return (int) $user_id;
		}
		return $user_type.$user_id;
	}

	/**
	 * splits the combined user_type and user_id into a single values
	 *
	 * @param string $user_type 1-char type: 'u' = user, ...
	 * @param int $user_id id
	 * @return string/int
	 */
	function split_user($uid,&$user_type,&$user_id)
	{
		if (is_numeric($uid))
		{
			$user_type = 'u';
			$user_id = (int) $uid;
		}
		else
		{
			$user_type = $uid[0];
			$user_id = (int) substr($uid,1);
		}
	}

	/**
	 * updates the participants of an event, taken into account the evtl. recurrences of the event(!)
	 *
	 * @param int $cal_id
	 * @param array $participants id => status pairs
	 * @param int/boolean $change_since=0 false=new entry, > 0 time from which on the repetitions should be changed, default 0=all
	 * @param int $recur_date=0 time of which repetitions should be updated, default 0=all
	 * @return int/boolean number of updated recurrences or false on error
	 */
	function participants($cal_id,$participants,$change_since=0)
	{
		//echo "<p>socal::participants($cal_id,".print_r($participants,true).",$change_since)</p>\n";

		// remove group-invitations, they are NOT stored in the db
		foreach($participants as $uid => $status)
		{
			if ($status == 'G')
			{
				unset($participants[$uid]);
			}
		}
		$where = array('cal_id' => $cal_id);

		if ((int) $change_since)
		{
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.(int)$change_since.')';
		}
		if ($change_since !== false)	// existing entries only
		{
			// delete not longer set participants
			$deleted = array();
			$this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id,cal_quantity',$where,__LINE__,__FILE__);
			while (($row = $this->db->row(true)))
			{
				$uid = $this->combine_user($row['cal_user_type'],$row['cal_user_id']);
				if (!isset($participants[$uid]))	// delete group-invitations
				{
					$deleted[$row['cal_user_type']][] = $row['cal_user_id'];
				}
				elseif($row['cal_quantity'] == (substr($participants[$uid],1) ? substr($participants[$uid],1) : 1))
				{
					unset($participants[$uid]);	// we dont touch them
				}
			}

			if (count($deleted))
			{
				$to_or = array();
				foreach($deleted as $type => $ids)
				{
					$to_or[] = $this->db->expression($this->user_table,array(
						'cal_user_type' => $type,
						'cal_user_id'   => $ids,
					));
				}
				$this->db->delete($this->user_table,$where + array('('.implode(' OR ',$to_or).')'),__LINE__,__FILE__);
			}
		}
		if (count($participants))	// these are NEW participants now
		{
			// find all recurrences, as they all need the new parts to be added
			$recurrences = array();
			if ($change_since !== false)	// existing entries only
			{
				$this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__);
				while(($row = $this->db->row(true)))
				{
					$recurrences[] = $row['cal_recur_date'];
				}
			}
			if (!count($recurrences)) $recurrences[] = 0;	// insert the default one

			foreach($participants as $uid => $status)
			{
				$this->split_user($uid,$type,$id);
				foreach($recurrences as $recur_date)
				{
					$this->db->insert($this->user_table,array(
						'cal_status'	  => $status !== true ? $status{0} : 'U',
						'cal_quantity'	  => substr($status,1) ? substr($status,1) : 1,
					),array(
						'cal_id'	      => $cal_id,
						'cal_recur_date'  => $recur_date,
						'cal_user_type'   => $type,
						'cal_user_id' 	  => $id,
					),__LINE__,__FILE__);
				}
			}
		}
		return true;
	}

	/**
	 * set the status of one participant for a given recurrence or for all recurrences since now (includes recur_date=0)
	 *
	 * @param int $cal_id
	 * @param char $user_type 'u' regular user, 'r' resource, 'c' contact
	 * @param int $user_id
	 * @param int/char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A'
	 * @param int $recur_date=0 date to change, or 0 = all since now
	 * @return int number of changed recurrences
	 */
	function set_status($cal_id,$user_type,$user_id,$status,$recur_date=0)
	{
		static $status_code_short = array(
			REJECTED 	=> 'R',
			NO_RESPONSE	=> 'U',
			TENTATIVE	=> 'T',
			ACCEPTED	=> 'A'
		);
		if (!(int)$cal_id || !(int)$user_id) return false;

		if (is_numeric($status)) $status = $status_code_short[$status];

		$where = array(
			'cal_id'		=> $cal_id,
			'cal_user_type'	=> $user_type ? $user_type : 'u',
			'cal_user_id'   => $user_id,
		);
		if ((int) $recur_date)
		{
			$where['cal_recur_date'] = $recur_date;
		}
		else
		{
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.time().')';
		}
		if ($status == 'G')		// remove group invitations, as we dont store them in the db
		{
			$this->db->delete($this->user_table,$where,__LINE__,__FILE__);
		}
		else
		{
			$this->db->insert($this->user_table,array(
				'cal_status'     => $status,
			),$where,__LINE__,__FILE__);
		}
		return $this->db->affected_rows();
	}

	/**
	 * creates or update a recurrence in the dates and users table
	 *
	 * @param int $cal_id
	 * @param int $start
	 * @param int $end
	 * @param array $participants uid => status pairs
	 */
	function recurrence($cal_id,$start,$end,$participants)
	{
		//echo "<p>socal::recurrence($cal_id,$start,$end,".print_r($participants,true).")</p>\n";
		$this->db->insert($this->dates_table,array(
			'cal_end' => $end,
		),array(
			'cal_id' => $cal_id,
			'cal_start'  => $start,
		),__LINE__,__FILE__);

		foreach($participants as $uid => $status)
		{
			if ($status == 'G') continue;	// dont save group-invitations

			$this->split_user($uid,$type,$id);
			$this->db->insert($this->user_table,array(
				'cal_status'	 => $status !== true ? $status{0} : 'U',
				'cal_quantity'	=> substr($status,1) ? substr($status,1) : 1,
			),array(
				'cal_id'		 => $cal_id,
				'cal_recur_date' => $start,
				'cal_user_type'  => $type,
				'cal_user_id' 	 => $id,
			),__LINE__,__FILE__);
		}
	}

	/**
	 * Get all unfinished recuring events (or all users) after a given time
	 *
	 * @param int $time
	 * @return array with cal_id => max(cal_start) pairs
	 */
	function unfinished_recuring($time)
	{
		$this->db->select($this->repeats_table,"$this->repeats_table.cal_id,MAX(cal_start) AS cal_start",array(
			"$this->repeats_table.cal_id = $this->dates_table.cal_id",
			'(recur_enddate = 0 OR recur_enddate IS NULL OR recur_enddate > '.(int)$time.')',
		),__LINE__,__FILE__,false,"GROUP BY $this->repeats_table.cal_id",false,0,','.$this->dates_table);
		$ids = array();
		while (($row = $this->db->Row(true)))
		{
			$ids[$row['cal_id']] = $row['cal_start'];
		}
		return $ids;
	}

	/**
	 * deletes an event incl. all recurrences, participants and alarms
	 *
	 * @param int $cal_id
	 */
	function delete($cal_id)
	{
		//echo "<p>socal::delete($cal_id)</p>\n";

		$this->delete_alarms($cal_id);

		foreach($this->all_tables as $table)
		{
			$this->db->delete($table,array('cal_id'=>$cal_id),__LINE__,__FILE__);
		}
	}

	/**
	 * read the alarms of a calendar-event specified by $cal_id
	 *
	 * alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
	 *
	 * @param int $cal_id
	 * @return array of alarms with alarm-id as key
	 */
	function read_alarms($cal_id)
	{
		$alarms = array();

		if ($jobs = $this->async->read('cal:'.(int)$cal_id.':%'))
		{
			foreach($jobs as $id => $job)
			{
				$alarm         = $job['data'];	// text, enabled
				$alarm['id']   = $id;
				$alarm['time'] = $job['next'];

				$alarms[$id] = $alarm;
			}
		}
		return $alarms;
	}

	/**
	 * read a single alarm specified by it's $id
	 *
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
	 * @return array with data of the alarm
	 */
	function read_alarm($id)
	{
		if (!($jobs = $this->async->read($id)))
		{
			return False;
		}
		list($id,$job) = each($jobs);
		$alarm         = $job['data'];	// text, enabled
		$alarm['id']   = $id;
		$alarm['time'] = $job['next'];

		//echo "<p>read_alarm('$id')="; print_r($alarm); echo "</p>\n";
		return $alarm;
	}

	/**
	 * saves a new or updated alarm
	 *
	 * @param int $cal_id Id of the calendar-entry
	 * @param array $alarm array with fields: text, owner, enabled, ..
	 * @return string id of the alarm
	 */
	function save_alarm($cal_id,$alarm)
	{
		//echo "<p>save_alarm(cal_id=$cal_id, alarm="; print_r($alarm); echo ")</p>\n";
		if (!($id = $alarm['id']))
		{
			$alarms = $this->read_alarms($cal_id);	// find a free alarm#
			$n = count($alarms);
			do
			{
				$id = 'cal:'.(int)$cal_id.':'.$n;
				++$n;
			}
			while (@isset($alarms[$id]));
		}
		else
		{
			$this->async->cancel_timer($id);
		}
		$alarm['cal_id'] = $cal_id;		// we need the back-reference

		if (!$this->async->set_timer($alarm['time'],$id,'calendar.bocalupdate.send_alarm',$alarm))
		{
			return False;
		}
		return $id;
	}

	/**
	 * delete all alarms of a calendar-entry
	 *
	 * @param int $cal_id Id of the calendar-entry
	 * @return int number of alarms deleted
	 */
	function delete_alarms($cal_id)
	{
		$alarms = $this->read_alarms($cal_id);

		foreach($alarms as $id => $alarm)
		{
			$this->async->cancel_timer($id);
		}
		return count($alarms);
	}

	/**
	 * delete one alarms identified by its id
	 *
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
	 * @return int number of alarms deleted
	 */
	function delete_alarm($id)
	{
		return $this->async->cancel_timer($id);
	}

	function change_delete_user($old_user,$new_user=false)
	{
		if (!(int)$new_user)
		{
			$this->split_user($old_user,$user_type,$user_id);

			if ($user_type == 'u')	// only accounts can be owners of events
			{
				$ids = array();
				$this->db->select($this->cal_table,'cal_id',array('cal_owner' => $old_user),__LINE__,__FILE__);
				while(($row = $this->db->row(true)))
				{
					$ids[] = $row['cal_id'];
				}
				foreach($ids as $cal_id)
				{
					$this->delete($cal_id);
				}
			}
			$this->db->delete($this->user_table,array(
				'cal_user_type' => $user_type,
				'cal_user_id'   => $user_id,
			),__LINE__,__FILE__);

			// delete calendar entries without participants (can happen if the deleted user is the only participants, but not the owner)
			$ids = array();
			$this->db->select($this->cal_table,"DISTINCT $this->cal_table.cal_id",'cal_user_id IS NULL',__LINE__,__FILE__,
				False,'',False,0,"LEFT JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id");
			while(($row = $this->db->row(true)))
			{
				$ids[] = $row['cal_id'];
			}
			foreach($ids as $cal_id)
			{
				$this->delete($cal_id);
			}
		}
		else
		{
			$this->db->update($this->cal_table,array('cal_owner' => $new_user),array('cal_owner' => $old_user),__LINE__,__FILE__);
			// delete participation of old user, if new user is already a participant
			$this->db->select($this->user_table,'cal_id',array(		// MySQL does NOT allow to run this as delete!
				'cal_user_type' => 'u',
				'cal_user_id' => $old_user,
				"cal_id IN (SELECT cal_id FROM $this->user_table other WHERE other.cal_id=cal_id AND other.cal_user_id=".(int)$new_user." AND cal_user_type='u')",
			),__LINE__,__FILE__);
			$ids = array();
			while(($row = $this->db->row(true)))
			{
				$ids[] = $row['cal_id'];
			}
			if ($ids) $this->db->delete($this->user_table,array(
				'cal_user_type' => 'u',
				'cal_user_id' => $old_user,
				'cal_id' => $ids,
			),__LINE__,__FILE__);
			// now change participant in the rest to contain new user instead of old user
			$this->db->update($this->user_table,array('cal_user_id' => $new_user),array('cal_user_type' => 'u','cal_user_id' => $old_user),__LINE__,__FILE__);
		}
	}
}