From e8641028ccabec943898a0f344ae8706401f7191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 8 Feb 2012 10:37:23 +0100 Subject: [PATCH] Initial content --- Makefile | 21 + WebSocketMain.swf | Bin 0 -> 13304 bytes p1pp.js | 6519 +++++++++++++++++++++++++++++ p1pp.js.min | 163 + src/flash-websocket/swfobject.js | 4 + src/flash-websocket/web_socket.js | 389 ++ src/p1pp.js | 479 +++ src/p1pp_defs.js | 213 + src/strophe/strophe.bosh.js | 1824 ++++++++ src/strophe/strophe.js | 1710 ++++++++ src/strophe/strophe.pubsub.js | 289 ++ src/strophe/strophe.roster.js | 186 + src/strophe/strophe.websocket.js | 1435 +++++++ 13 files changed, 13232 insertions(+) create mode 100644 Makefile create mode 100644 WebSocketMain.swf create mode 100644 p1pp.js create mode 100644 p1pp.js.min create mode 100644 src/flash-websocket/swfobject.js create mode 100644 src/flash-websocket/web_socket.js create mode 100644 src/p1pp.js create mode 100644 src/p1pp_defs.js create mode 100644 src/strophe/strophe.bosh.js create mode 100644 src/strophe/strophe.js create mode 100644 src/strophe/strophe.pubsub.js create mode 100644 src/strophe/strophe.roster.js create mode 100644 src/strophe/strophe.websocket.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..35ab01b --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +all: p1pp.js + +JS_FILES=\ + src/p1pp_defs.js \ + src/flash-websocket/swfobject.js \ + src/flash-websocket/web_socket.js \ + src/strophe/strophe.js \ + src/strophe/strophe.bosh.js \ + src/strophe/strophe.pubsub.js \ + src/strophe/strophe.roster.js \ + src/strophe/strophe.websocket.js \ + src/p1pp.js + +GOOGLE_CC = compiler.jar +JAVA = java + +p1pp.js: $(JS_FILES) + @cat $(JS_FILES) > p1pp.js + +p1pp.js.min: p1pp.js + @$(JAVA) -jar $(GOOGLE_CC) p1pp.js >p1pp.js.min \ No newline at end of file diff --git a/WebSocketMain.swf b/WebSocketMain.swf new file mode 100644 index 0000000000000000000000000000000000000000..1d272b3b1d18e9711764d356248b58cbc1be3fc8 GIT binary patch literal 13304 zcmV_T1r>CT5?I+na)giI^8>6rZaT# z?|bfhlI=8|`TYO+_`G+QbI(2J+;h)4_dda1LH@BIl${WSnM_z;F-s7H12f)Xf^d0i zAlkBdz1O{WIG#wiptfdVERz{&S+Zo$o;?kFmNq0)gG-uLuU@^Rv3W^zb3HKX)B6&c z(BAq)`jUlfIf3;?I+7Y1$qXeEZmNgE$!uoL!i5&qfk=_)NH!JcBnKi(jJPpuBr@qG zO$|*TG!SWtCR4+q%-Yb%NPH*~B63Ui)~931$gVx1-9~*h9!ke9UoweEV=_aTxUsf% zAQ?8?-ng;Xz0}=0Ii6#iD1`cCVuomRLnJx8WF(aw$VR|Z6r}jzsY8gwNH!cF zO2>@U+H7K1BDu$$VzQNRA}J%3NlwQov{8RNlo-s02934t-Mp_@=RAfo#@gn_rsn!a z`ZJlJw#%2Cqid@L%ePio@A`#wxzHxw_Q+G$I3%pE3Xs&uI)VRdXua}^kas7C2Grh6YA7+N ztxG23Mkt~B!q*v*OcmH~FxhV?vR_tYzkN3hN*YRJl-5)#w6By~OoKtKX@z&nS@g2Y zT%E-+UCF^gBc(>-L+CE;^tGo_$rQ(}>@_0U)KF&MWRtTel^jeN=`^pEnDn!mp?KQ9 zZePaWvre+(CH2|7*+hhvOYP1MhmF*N^NG-MEshSDSP&_1*(tdn!`U zSh_lnIo2D|P&S^KoYU?gdsf<%lozznmP{ldqbqa{f*RrGT#-yz7}$+mQ$@SuT96Q&inKbD978o1(X?mI zU1KT4G1ua=+Iy(-({&bUU8oXp=ByUHKLLgpUAkdm-Y@B zY3F8dTl1=>l_n#XoEN8M8cy|q0d1#@0lw5^7}%(3C)PN!YqOCaNhZ>U7BRJBFY3t^ z8ZZZR4Z#Sp=5`{!uLpK)mO5Y!7l7&zJVjg|YNUl3DEIe$!~uB?iajHl~{8LQ@<}lnp_eRHob5(*;85GPA2- zluG}eWNMc(kb+yRBGnD1lksE&kW%}2T$Ot}TAOB_ucw82{tu7CjYN$biNQ=viVh{T zkl<2%KdP>TEg6|<(pc!_pjUH^>8`K8%eSq4b6aa~yAuQ^QEXJBf)a_P(l?ZHg1d+Nq0-EZ zb)9{^{XJltv(Vbx*4b(Dfax^Lhe)Yp+=luTes*ORuERv&v8Q!CAKTj22P*5^y{%ii zAhvH)PgiGacU!xi^Ls9d8Lylu5Kei?r@+KM@!4|E?FYCtXj6zIl@yzcNDo{ zi`z>5)B6y&hHZuPsEi!(WC~e6bz0U;v-lgV-3TKDn8AO|D$$N{LyzA2&*PCzC$01l|5T%UNMbnYE5<|#i zb{i(*od1Q$g;P&KE$5`HCBc+|LFlTgd;TWr;SF7%=wwqIT}VD5NkyYf*;M-Jx!=VTl@v zg;K4V5-^|XG%+=6Wo`6Mqae26uSK&WkR3TkC{Bs7Ovc9=;B2$VUqbP;p+J}+WZTBM zhUE#=?%cC@QOnLHJD1d68`!y|#lLt_ZAfO=Z z2k(fm(Bd9S@GRdA*IBf7Zj$K|z8e!6xSF&bWpm-BJ?XTKXR%QEMX0#uqm?Y<)Y->b$9ZqMawwVAxF= ze(fTaUM+6t9s+#A0t~DDI9FvwC-U zpZmtf{Vi@%_nI4<_Pfm!-|IUhTDuCQCo7y~omb?S>YT3<+(THMSdUzZ$8h)zf4hW0+<*WJ^?FDOGbAl=>{>kNzHBQ*B#2Z zOWd{A`KiS%XAlF}BGWiAa(FnK;J0&$LDK}!OQFJDJMB!1-Mz@sGmNt=>@a1xGclup z4dYE^6A7Y+V~v}89k;11_mr4GcnDH-28r);^Nl-Tr^70@t%{U&2r9~2wz+q!dpHZ} z!-l)DVFAzU-O*HXxG*vABy|9Rl6zyIVR?_4?7Ndux3ve|+)#^@;KS13K9q1bHLrlV zV(x|&o6UQc0X}9Zk>z)n=0J~oPi!a>D{RrB;Sn>(!en*Dy#*8q$fFmjfs~Ufkfcr1 zP+yz@p+A(`$6L=KUEJRdYI|Z}D3oxl>)g=a-n||%ABQYzv5jC$5N{Ryl%Gfs4JM2M zzL#o>6I%PmWPet1V(T*0ZJvHw-1fq)M@r$FNdj$oXKH7nJV3i)Cv69JQ)836EDU`X zmIr(UL_Ss6F&OZcPoc}TfC_N^O&-WuHXeOPB?)tG#@xzEWer*536JG2|>qhQ)lGr;eK z5ZH}$VqwP3BMx@2d;S`C&2%vfQ_U*QdCs7Pj)kVNCfp0zLOXZ3w98zk%eTx1acEXA z?8@38X=Hj>#-uw=DV3u*C_QJYjZ^F>8W%jbxysszdERD2PIDt#+pv79nPR?ra}%4$ zZkz%PV<7Eb*1USz>J=-SS1)gHuY*2_J~Ea0OprH)GBMS>qEe#qWHP0s;ZO|bW_IRe z4pu7eyiwe_-&MTbYB86?7Pl@;99aa1&Sxa(Y$^Lo;{P#AhbZz7H+8v6*+UnRhZp zu)WD@{+-qQiq-rHwF^|+|FGHfW`DqDPq5hs#o4!rvk!^074)o{-GYJ7h&7)UYn~Nn zgRUaXj-ewyJ2Cr736*`+bir+G_8qK*iDfn>UgVyyU3`hY;L?S)_StN{V^Lkn;#%gY zcFGPm-y^!!dTDOiyoL*xlr{pR2^h_Frxk?kOHCCgs3p8bx zrp(sFIhs1xUdA*wTVoe#Hn+y+YqpCu+a(&)HQNG>&C=MVnr)$GtJQ3aG*+kC7HiC- z+3GdcpdoE$jT&p#*iwxx)7Wy2t?f- zSi9!*YR(OsvqN)sYR-)syGnC*Y0gcWvs-ieG-r?IyjpW^)||bXvrjAAsyVl5&h46W zhvvLSb6l&joto3HIRl!rUvmaEXGn8~HD^R~4roq8b4E4Cpys+xs~py<)@oHrt!kTA zHKMWWwW^fH(pr^ItIB9qS*>cf#`b7cd$p>lR&#^KZq(RKTFri~=4Oo@(AYty)!YKz z90J^m@`vy~%(M$`$NMh8hXEe}+zt3B;A4P$0QUku4)_G%lYsjG4*(uy+U$q$ei-lw z;8DP1fXC53iuX~xkKz3U;7OE^<9!0~6yPM_6yRy5IX?w>2JmSB=5am?_zd8AzzcvE z0WSew2D}1z74TWWYZ(7J;0>nD`5fMF0lt9p+jxHw5Z2~=39m1s=_`1D74S8P{SMw= z$NQUje+%$!z;^&+fE*wX7zcb8@GjsC;4I*KfbRqT3GmN=9{~OZ@UMV>1N;#1BfyUV zKLPv{@H4>A0l#3{g}-Fl++U;o8^G_FHt)}P{{`?@!25u|G5g<`Xunky?VCmWhwxe^ z+M7lDO3}VloFi)UZv)&RYVuv8rhXXk5y0KpDjx-W41MK1Z@I zm$dmOMf*t+`|6ZvUoC6%Pm6ODjeQ0KUI4rVcp2~t;5EP-fHy_W{W-wr0dE1mfVM9I zz6y8;^=8ezRC90A++Rm!rRM$yUf;xoW9a`^z)!*Kukrp3;J1L^0e%mhKLY*)__Js~ zEh_fAm}0+81TS}kxsL+wku>+m0Y?Cz0DKZaMtL9ldSQ2XJp_1IvR}p^sx(Kn56brI zv^h5Wami*sA;Doh1%Xck=~H+=BiWxv`wNn7j$NDoB3fPoye!$TmFGCL`Ja_OMBjl0 z#zgCJ?mw$Xnu;^!AuEEQGF8+>RTOMeX(%`Q8?_9eipX0O!Tj)S{gVYzlq5dR zfq#O{L@v%P7Dw9g5Jg^;iVv~ah=;wh%7qQz&KDKV4EL8TlMGig#e zA~9K}qA1$!_H$NXig-nFOyE^fg=n-EA`^)?e_EV6tp{HRfWkzVq|zcd z987gM&LIVc!5hdiaL)OYSXr@wPt7bA*m+k@{JWJZK53B<&!8QxiIvlf^P(Fv3E@Z-txlE{- zCkUEw5fe(?Owf=UR$fd}UBU#X&V=d(Ot4?dgsO#vRm%j|A|^QMm{6wJpp2R7ViLwf zBkJkVFkhIxWWF$~k?f~QwwIu}d8xfrXslgEYqp#T>sHXtkyaA=DkgYW6J@ZqtVIwU z!evZox||8C*D&GowM66!!n=|Qt*uOGUPqNSdaP%{nszF9nb5X@3LTtmCll6h6ojjU zE4FW1GNt2Bv|h} z1$M4_nkavsvD1*K>r;?&qfdJVyG9m1%_^myXIZ7P`8g&?cHuLuQoj0moR_eMg;86C zpz;E+!Rd?O)b5mC0vDR_GWCChu~)!vMVYi~&a066#?NBH#A{GU_H}G8EYcfL@t!w< zi}L57zSQTzMe;3dGrLXt0yLX`8{|;_BCD8j)0e<>{L4`PzOR7K;jcpJyT3+j$k;p3 zXXfjW)vik4fC4e(o2;U0|F;f=62Hfg=~URRKZxBfGl$HC z8dfDUHis!PgFQ6Kg0z7(%Yxj^cFKa%$NaLO_OpP@wCh-pEZ7dPeKNBjLKMLGTN#oj zIM_M1Tezo}*aiwTaF&j-MniCK7h;YL_KWEIZ`*dQcnr5yghN3sI?oP`QGE}Sc9EmyOefDP;Lw^Z4CZ`H3t73@Jm27{XV zSUp;2DWam*fq!KTmNFKtAB+A9uV1r`ihNLcHy2zSTAT|uh8lB0Psl^MoGmg%oq-K- zk4QsCJ@<;E7&nTpIbdF%QDEMNzwIJ8RWQ$mGLz)X!0BP;xu3-jv%mw)bDG5-V1Xmd ze=qZVT=X0fH{Q#{*bx@Ehc|tq*mMsGFjtZ#c?C?|^GVVB8^&~TG<}9DdDO)8*>Qd1 zbS0K3m-qgbvBNGK6~|E)4}-UPG`bUl-ou-%7Eh<^$m-GXc%cCUE~Wu93j_YX{}PgU+vL*I zx{ppO`q{clWc!Q8^Ushn3fy$$JV*ae9KMNlNs?r^nVkP_EF|1R-uoZu6NCk_Cdt2n zu5}@zdf3@qVR`E!GBm}NVXbIHreav8b)@C4izu*wC4%s_LR@1BsWqZvW|T=;-aj&7 zG#6H(Nve+?KI=MQYBwxfBvgB$7Ltk$U=c-GDAT!EVU>u1mC^G};50e5 z`p(lpZv9e{`fHHdeK;Zm6tYz{=#M_j+@~wy={?MaCvP}>_)IQ%n3+pe){ItIA~cAR zo+reJ0#V>OJx_`=9A@8b)TiWvN65mV<-_nX53s^A31yp1v`;XWA^C+e5;u4cVu*?D z!Z{*0euOLIL9wdRT+1UQV2+lQjS|Isn1@-^A<#lv-$22q=Tv$z)y|yvr0Lxc zzZ=|{FILXvptU2G%4etrtIy}Bx8|w!OfFAGT8l%UTmn~bD(XHoEm9Oou7uOMgXQw> zaqk^WJ`B@APaU#O`CXCgSaTngBQ_N59u`q9C8;Bh+@VN`iw;)dlCWbQkisej*DlxWSfisNc8RL&t>|3`_kc6&Yy8N455GRLXnU3Z|%Y`?>7 zZ!U3EeD{M1TZzpPR-oO*$nfPiNxNXm4$V3&&0e z-U(-z_x=U4XmEq1ax!Vnbv3D6;Wi5iy#Hw;*mMC6pgzuZ{8uv7zgniMGjlB*<6KSX zbC8<;%GKn6B2I`Dkfg6{_O$DFLRfT;^W48*SKQYP?sIrAB7DZy=Z6~K!efPu!TFp7b4&c14h2a>g zpTdffSE)z-rKrg9Iw|*TKsqKNN%sF2qkK%>F3pWKp?wKujR)c11CKJ#b1e2K3mk{{ zjvZ%#M-b^_kFdaF$gX0KvA{8=OR-}t@DTMs!~#!I|5GgRIQ2iy0!Nu{iydWw2bpe< zJ;(wNGu;t;m<66>3x1)+o@9Yhw%|8fY?K8~um!)>VkcPO3AW&lg|R0nqidAQ6!k7{ zV1Yb86kS>e;-I_CXvb2S`1KU(Ga z6jBf0DmE5&z+m2Iv2|1usni}^=Xp*%O>45?b;%_n*!QF(C1d`-L4mpq6z|_C)KpM% z5H88YmeEiJ?T6|rC{n<|A3}?2wh)$w9U6^o7z@j>wz2Wp>anmE>l+K(Vk^T6k1u@r zVep7BZHO>5f0d21`Md}w{eg`E;b5RHQi`ph`mPhhr9o%(e^~HZoPE%@BV1Y+amIFp zoi=A|qpML60vr8TZM;f|Z8T|>a#}AEEf>OHaO0HWJAxZyfS@gU3kza1Ejn@cuXX_dl53^v4=T*^H3B&4` zYm-p>EXK#hQQr(2OO|l3Y*<1Qf#|zMWWkQ$BdiKr3jO69%LQ|;xEy?ng$_`GkV7~5 zyQ%E$W|eTi`a}n{MsMhFH9=P$!N(XfP+B6Ck1=i-76O#|Iw=(lv6;1=L*lKX{2@`0 z%7nwBAd13mqQFGqc2SVah2^TMmi$N};UjXRDYs&}B^g|ke7_ua#4fYvBRE2vh& z)ctpge#J9uqr!Z5ipfv!S-QM&?Mm_WDAVl4Z{%2*+)e|m4uIp^aVt^M-*%- z8HiFY+zT_2gcUYTQGbLz0=nNKy65voXK453c_Um4Wgi|`EtFkAG2QcRF}T|E9nt%75hwGU$WwygN|HZ} zH?QsV;9bBOz*)fe0N)4vlgNh#H;iMdwqPF;_0vo{j3Ypc=bv%zpe-8Q z0pCS?a)&QLv1X+#D(YM==vOQS(#rX-TnN2;7}N`-RidP*t(5bg;VWJ>6SDq53~n3s z{EIl6iypzm|Au^;@)OSw|GwS7CGXopfnzlhYM(;r!Xlv`6$bnm@RRApeso@9Eh1Q- z1J;RU^M_Vn6ECzQ3ySROQ zLKI!il3)+6(B;w}!qQgov#?5@XoW6f`#Gt|kr8Wj=V`hS^slHp8(r~MM8;BtR@h{l z+Fk&c+15=6rq{*3z`T4?_oDtXti z3et>!xrOOl4jJ`{VKG2nC)P2Bjd_F>cIFi#QNG4|P0>eJdPIQ_mZLWm$MWjUx-bJf z&tJRXmzwA2qOVqT2{asC`zwnb@9~cJnAveaw>?+f?^G@Sw0Uj7t2bdk-29w1_Sc%{ z7kun#Cb)DYplJJ*=J|Ktc0cN*0&|!y6ozv>C?wM|q~_%4C^O%vh+MjJBN-_EPV@Yd zD9)t?Do*k%K1nm51Wq+gCgNdaA{LioxmU0z4SrIL-Y16c7Yn?E9uT<$L&f``DDc5W zPQ^(QM>(?^7VQtT@%**qEy=YZEcj|A*9%Bf{p(G9-+JbH0(IA80_PEm{STAWfl*@h zc0q96YH@VnZ15pAJ{u;Z3$D#Dx~HrfbFD}1ajPb}R-ks&s!6U|)E=~IvWsHb!&Xgk zVTH&9b(MQQ)h5lkX<|Z6e=u?D2NU1>VB!}aObkv;?3tKAE1B6_23yRHM5N7B{r2A` zHn87Gx5;aR-KBLC}q{c@@{+-po#q(Fu>>7oiSlGeu)6K(;4zKG!39iK=Fi{BP0hH%X3%HJ-0}9~W_4=Lu`*eHNDgzL|>XdRxIE6{`3U z&H=LLed_W2O`K85Y5$Kn?)gA05(mNrM|i)0a8Mfe+#*dyI7GFI8s2-W^>NyO! zO|tki5f0bE^oy#Ma1Ik*FX<8-s(laQZo^;Pw zC`d(rCKcQ>_rDb8X@g|Qf~KUCaMZGEu0UHdF8Md?{`+2Sro9Tf%mv>7EFJ;|JgdC zL{3YrFMp+jiZd&^;5#~HJjyQf2=Xxr z8LrhX`HqQGa&6Ue99(Uo8!S~do2pU^`L(HXU@8=5`=O6Koz{J?)bmQoD@kQt8g|fBaE7d;rnTcX%NPbk6MDE6uKd2 zW&FXeJg1SPI5UpaM50^LRuP(7K%as<4G8216&!7Hp~Yc2A3{by9`Ydl4$Bk)vE|m$ zeg$&Q`lw|Y5xCBA(s@v2fv$Y43&?E>kgo(~6Y1TY?h3(YoN>V^+H-l@h57+(Lh^88 zJ_nn54>sfTC550b=S#7m&)YyB=+?`~!}>8Pv5(YO&Zpss8;A(^s^A6QCM=_ti#~wJ zcuHiR2P8^@sSRlh#&I7yK3e?_`B2hJ5p~qMn{6j{FTymzr$zjEPmA=0##_O1M?vD4 z_ftRxnwRoM2c^7}E#}}0HqiI4D#-baXnnm~x?P?-H`WXtbkOZ^=~!dnY3BJni#<(B z?386m-a+@$RT3rt9i$VK>~D+yQ!JOKi)QaB#wvb2F+tbS>9cgzXCMEQ( zVmx2YKYj7* z<9SrdTZsf_^E_tN7^M!L$E})3se@R_Oh?=w`3+H1B?9YmHOIpJ}hw;ko6scuGq;q1P#&cCHAOoWhlE~Xnjz?bV8jJRidQKtjz{zzC;fJOG>NPS@*TQb$uI(Ta8q*nf2%!)v{L)cEF88vCQ_BoH z@!<W4u74 zxco&L)N+0r0f(w;?c`Ev9-Lh%gURs$rz^sDvFdavH6 zZ_&5v+w|@F4*eQEq=)s0KA;LdE~dP-0089l4-*7xXp z^?mvcdZXT?H|tCFW%_b`g}zc>rLWc(>r1v?8GDI#d`QMVre7WH(S^FYtx{|ccC~Ink*|DO-JrwJbiJwVXVRH^U>E$*L5}*6r*p5?s4y%qBwf^-Tb{7Sljgore`N6 z0@rU9V%G=pq#vh{hl^YePk+Xj5dtH+5CaCp z!+5+Aug!wU3t{dCv_#TKBwJVu+aQw7rcD<~4J-fOL{cCUg-o^j;|L64 z8T^3fs3>y5f*4+=ShW0my?cej@A4I=1sarK4=Z60)0WPhF))nh@IRgxbHpet*r2?G zH$l16kOF1?nXV$QC!;u0FdO$KsTTGwsIXYn=W!6PP7YY3>psPi6{$risE6f@ymdz>-C2akQ zljlRs($A0s!@*%)9<4hQ8_wHAy{oE>Fkavorkk47<^Hx~2&5iY-eHTUGE-3j; z`oQ%j3w)hEaDANxzQ#N(_B9sB(U;0tjs@PPFI{i5z&Ggq4HkHvtDFVC1w^`-eGBcO zx5y&n^kbG1%=HzP^L~X3d8TDI=6}*O%xcZMp6$oq<#}T%HS~HVP&%4P~Eb z@_(7(sY^%EN(CDgo~BY+vEBHssLJsiIQ@w0rf=J3? zhdElNni4KCYYNrWaH&~SsiuXUW=-R)gv-pDEo|3aqflVjPG_yx=;N`;eAu>?KKy4T zIEyuOxT`9q-@T2W%-9wUAH9Q@bpGNKS(lWD~ddE46)h7tMX&W?}L@=hl zI?I1hW>ToH$Ok7ASL&%jX?%-TGc=&mxl#d?R0Pcd_J!B||} zSfGw#QK7|GT?=XP<*+&)R4v!2G5kubomZxU9)-@j56K>&i9hqY9Vp`oRfLLX!a~lx+n60(Q$fY>1SiE@rk-3jvvIS^KW z@&v`@@sqk6<)i2-Y%kt9N`@p`imQHOVuF69ccY4#t{!XjuyZHz&?G@3Y|b#1=u_h- zy>HN2`OI|6y8Br5*G$4UkqCPcXF^m3v!>{z<7J*^5cv#9`E>co zqq^-FeEA843Od}Lx4+PdQ~g<_48{57W7Vf|4#xcZRZWtA$oNk)Ya^=X4QX=T*Z5^Y zfvT-Q_t*&@23PupK|ehfhZlr=Xn57R+xgH$Sip>r#)6no24|Iy)Zx z7NZ~ET|mcY?0OYMR18O$p}Zuko|CG&)t#7E{ohryczcP>d@tq{Z@f``p+* zG#_MiTr`hk5jJ%+W!NeLMAIe C{R4{t literal 0 HcmV?d00001 diff --git a/p1pp.js b/p1pp.js new file mode 100644 index 0000000..dd7f0f2 --- /dev/null +++ b/p1pp.js @@ -0,0 +1,6519 @@ +var WEB_SOCKET_DEBUG = false; +var WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + +var P1PP = function(params){ + this.jid= null; //for non-anonymous connections + this.password= null; // + this.connection= null; // the strophe connection + this.params= null; // merge between default params and user provided params + this.timeout_id= null; // points to the timeout function triggering the BOSH connection + //if WS connection failed + this.retries=0; // How many retries already ? + this.closing=false; + this.defaults= { + flash_location: "WebSocketMain.swf", + domain: "p1pp.net", + ws_url: "ws://p1pp.net:5280/xmpp", + bosh_url: "ws://p1pp.net:5280/http-bind", + connect_timeout: 15000, //How long should we wait before trying BOSH ? + connect_delay: 0, //Connection will not be done before this number of ms + connect_retry: 10, //Connection max attempts + rebind: true, // attempt to reuse previous connection + debug: false, // Dump traffic in console + num_old: 0, // How many old items should be fetch ? + on_strophe_event: function(){}, // Connect callback. if additional XMPP exchanges are to be done with the server. + on_login_required: null, + publish: function(){}, // User provided call back.Called everytime an event is published. + retract: function(){}, // User provided call back. Called when an item is retracted. + on_disconnected: function(){}, // User Provided callback + on_connected: function(){}, + cookie_opts: {}, + nodes: [] + }; + var merge = function (o,ob) {var i = 0;for (var z in ob) {if (ob.hasOwnProperty(z)) {o[z] = ob[z];}}return o;} + this.params = merge(this.defaults, params); + if(!this.params.pubsub_domain){ + this.params.pubsub_domain = "pubsub."+this.params.domain; + } + + var nodes = this.params.nodes; + if(nodes.length > 0){ + this.scope = MD5.hexdigest(nodes.join("-")); + } + + if (!window.console || !window.console.log || !window.console.error) { + this.console = {log: function(){ }, error: function(){ }}; + } else { + if(this.params.debug){ + this.console = window.console; + } + } + if(this.params.debug){ + window.WEB_SOCKET_DEBUG = true; + } + window.WEB_SOCKET_SWF_LOCATION=this.params.flash_location; + //initializing flash websockets (if necessary) + if(window.WebSocket && window.WebSocket.__initialize){ + window.WebSocket.__initialize(); + } + return this; +} +/** + * Connects to server and subscribes to select channels. + * @param {Object} params JSON object with configuration options (see below for attributes) + * @returns {Object} the P1PP instance used for the connection + * + *

Parameters and their default value

+ * jid: "" if set, will connect with this JID and password instead anonymous + * password: "" see above + * ws_url: "ws://gitlive.com:5280/xmpp", websocket URL + * bosh_url: "http://gitlive.com:5280/http-bind", BOSH URL + * domain: "gitlive.com", Domain to logon to + * rebind: true, should use rebind if possible + * nodes: [], list of nodes to subscribe to + * num_old: 0, maximum number of old items to fetch + * flash_location: "WebSocketMain.swf", Location of the WebSocket flash file. Can be an URL + * connect_delay: 0 The client will attempt connection after this milliseconds + * connect_timeout: 3000 How long should we wait before fallback to BOSH , + * connect_retry: 10, How many times should we try connecting + * pubsub_domain: "pubsub.gitlive.com", pubsub service url. defaults to pubsub.domain + * debug: false, Will dump traffic in console if true.. + * publish: function(){}, publish callback + * retract: function(){} retract callback + * on_strophe_event: function(){} Access to StropheJS API events + * cookie_opts: {path: false, domain: false, expire:false, secure: false} cookies options if cookies are used + * + */ +P1PP.connect = function(params){ + if(!this.push_client){ + this.push_client = new P1PP(params); + this.push_client.connect(); + } else if(this.push_client.connection.connected == false){ + this.push_client.connect(); + } + return this.push_client; + } + +/** + * Disconnect client + */ +P1PP.disconnect = function(){ + var that = this; + this.push_client.disconnect() + } +/** + * Subscribe to a channel or channels + * @param channels a string or an array of string each being a node to subscribe to + */ +P1PP.addChannel = function(channels){ + if(this.push_client){ + if(typeof channels === "string"){ + channels = [channels]; + } + var nodes = this.push_client.params.nodes; + this.merge(nodes, channels) + if(nodes.length > 0){ + this.push_client.scope = MD5.hexdigest(nodes.join("-")); + } + this.push_client.subscribe(channels) + } +} +/** + * Unsubscribe from a channel or channels + * @param channels a string or an array of string each being a node to unsubscribe from + */ +P1PP.removeChannel = function(channels){ + if(this.push_client){ + if(typeof channels === "string"){ + channels = [channels]; + } + var nodes = P1PP.diff(this.push_client.params.nodes, channel); + if(nodes.length > 0){ + this.push_client.scope = MD5.hexdigest(nodes.join("-")); + } + this.push_client.unsubscribe(channels) + } +} + +/** + * Publishes data under given node + * + * @param {String} node - Name of node which should be used for + * publishing + * @param {String} id - Id of published data, if null random name is + * generated + * @param {DOMElement} data - Data to store + * @param {Function} callback - Callback called with (id, status_code) + * after receiving response from server, status_code "ok" is used for + * successfull operation + * @returns null if connection is not yet established, Id of published data otherwise + */ +P1PP.publish = function(node, id, value, callback) { + if (this.push_client) + return this.push_client._publish(node, id, value, callback); + return null; +}, + +/** + * Publishes data under given node + * + * @param {String} node - Name of node which should be used for + * publishing + * @param {String} id - Id of published data, if null random name is + * generated + * @param {DOMElement} data - Data to store + * @param {Function} callback - Callback called with (id, status_code) + * after receiving response from server, status_code "ok" is used for + * successfull operation + * @returns Id of published data + */ +P1PP.deleteNode = function(node, callback) { + if (this.push_client) + return this.push_client._deleteNode(node, callback); + return null; +}, + +P1PP.COOKIE = "session"; // Cookie or sessionstorage key used to store attach or fast rebind data. +P1PP.couldRebind = function(){ + var protocols = ["WEBSOCKET", "BOSH"]; + for(p in protocols){ + var key = P1PP.COOKIE+ "_" + protocols[p]; + if(window.sessionStorage){ + return !!sessionStorage[key]; + } else { + return !!this.cookie(key); + } + } + } + +/** + * merges two arrays + * Taken from jQuery 1.5 + */ +P1PP.merge = function( first, second ) { + var i = first.length, + j = 0; + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + first.length = i; + return first; + } +/** + * Array diff + */ +P1PP.diff = function(first, second){ + return first.filter(function(i) {return !(second.indexOf(i) > -1);}); + } +/* SWFObject v2.2 + is released under the MIT License +*/ +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/rfc6455 + +(function() { + + if (window.WEB_SOCKET_FORCE_FLASH) { + // Keeps going. + } else if (window.WebSocket) { + return; + } else if (window.MozWebSocket) { + // Firefox. + window.WebSocket = MozWebSocket; + return; + } + + var logger; + if (window.WEB_SOCKET_LOGGER) { + logger = WEB_SOCKET_LOGGER; + } else if (window.console && window.console.log && window.console.error) { + // In some environment, console is defined but console.log or console.error is missing. + logger = window.console; + } else { + logger = {log: function(){ }, error: function(){ }}; + } + + // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. + if (swfobject.getFlashPlayerVersion().major < 9) { + logger.error("Flash Player >= 9.0.0 is required."); + return; + } + if (location.protocol == "file:") { + logger.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * Our own implementation of WebSocket class using Flash. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + self.__createTask = setTimeout(function() { + WebSocket.__addTask(function() { + self.__createTask = null; + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.__createTask) { + clearTimeout(this.__createTask); + this.__createTask = null; + this.readyState = WebSocket.CLOSED; + return; + } + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link
DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler.apply(this, [event]); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + jsEvent = this.__createSimpleEvent("close"); + jsEvent.wasClean = flashEvent.wasClean ? true : false; + jsEvent.code = flashEvent.code; + jsEvent.reason = flashEvent.reason; + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__initialized = false; + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + + if (WebSocket.__initialized) return; + WebSocket.__initialized = true; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && + !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && + WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { + var swfHost = RegExp.$1; + if (location.host != swfHost) { + logger.error( + "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + + "('" + location.host + "' != '" + swfHost + "'). " + + "See also 'How to host HTML file and SWF file in different domains' section " + + "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + + "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); + } + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "9.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + logger.error("[WebSocket] swfobject.embedSWF failed"); + } + } + ); + + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + logger.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + logger.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + logger.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + // NOTE: + // This fires immediately if web_socket.js is dynamically loaded after + // the document is loaded. + swfobject.addDomLoadEvent(function() { + WebSocket.__initialize(); + }); + } + +})(); +// This code was written by Tyler Akins and has been placed in the +// public domain. It would be nice if you left this header intact. +// Base64 code from Tyler Akins -- http://rumkin.com + +var Base64 = (function () { + var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + var obj = { + /** + * Encodes a string in base64 + * @param {String} input The string to encode in base64. + */ + encode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + do { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + keyStr.charAt(enc4); + } while (i < input.length); + + return output; + }, + + /** + * Decodes a base64 string. + * @param {String} input The string to decode. + */ + decode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + + return output; + } + }; + + return obj; +})(); +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +var MD5 = (function () { + /* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + var safe_add = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + + /* + * Bitwise rotate a 32-bit number to the left. + */ + var bit_rol = function (num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + }; + + /* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ + var str2binl = function (str) { + var bin = []; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + { + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + } + return bin; + }; + + /* + * Convert an array of little-endian words to a string + */ + var binl2str = function (bin) { + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + { + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + } + return str; + }; + + /* + * Convert an array of little-endian words to a hex string. + */ + var binl2hex = function (binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; + }; + + /* + * Convert an array of little-endian words to a base-64 string + */ + var binl2b64 = function (binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for(var i = 0; i < binarray.length * 4; i += 3) + { + triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + } + } + return str; + }; + + /* + * These functions implement the four basic operations the algorithm uses. + */ + var md5_cmn = function (q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b); + }; + + var md5_ff = function (a, b, c, d, x, s, t) { + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); + }; + + var md5_gg = function (a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); + }; + + var md5_hh = function (a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); + }; + + var md5_ii = function (a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); + }; + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ + var core_md5 = function (x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + var olda, oldb, oldc, oldd; + for (var i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return [a, b, c, d]; + }; + + + /* + * Calculate the HMAC-MD5, of a key and some data + */ + var core_hmac_md5 = function (key, data) { + var bkey = str2binl(key); + if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); } + + var ipad = new Array(16), opad = new Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); + }; + + var obj = { + /* + * These are the functions you'll usually want to call. + * They take string arguments and return either hex or base-64 encoded + * strings. + */ + hexdigest: function (s) { + return binl2hex(core_md5(str2binl(s), s.length * chrsz)); + }, + + b64digest: function (s) { + return binl2b64(core_md5(str2binl(s), s.length * chrsz)); + }, + + hash: function (s) { + return binl2str(core_md5(str2binl(s), s.length * chrsz)); + }, + + hmac_hexdigest: function (key, data) { + return binl2hex(core_hmac_md5(key, data)); + }, + + hmac_b64digest: function (key, data) { + return binl2b64(core_hmac_md5(key, data)); + }, + + hmac_hash: function (key, data) { + return binl2str(core_hmac_md5(key, data)); + }, + + /* + * Perform a simple self-test to see if the VM is working + */ + test: function () { + return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72"; + } + }; + + return obj; +})(); + +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* jslint configuration: */ +/*global document, window, setTimeout, clearTimeout, console, + XMLHttpRequest, ActiveXObject, + Base64, MD5, + Strophe, $build, $msg, $iq, $pres */ + +/** File: strophe.js + * A JavaScript library for XMPP BOSH. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * has no facilities for persistent TCP connections, this library uses + * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate + * a persistent, stateful, two-way connection to an XMPP server. More + * information on BOSH can be found in XEP 124. + */ + +/** PrivateFunction: Function.prototype.bind + * Bind a function to an instance. + * + * This Function object extension method creates a bound method similar + * to those in Python. This means that the 'this' object will point + * to the instance you want. See + * Bound Functions and Function Imports in JavaScript + * for a complete explanation. + * + * This extension already exists in some browsers (namely, Firefox 3), but + * we provide it to support those that don't. + * + * Parameters: + * (Object) obj - The object that will become 'this' in the bound function. + * + * Returns: + * The bound function. + */ +if (!Function.prototype.bind) { + Function.prototype.bind = function (obj) + { + var func = this; + return function () { return func.apply(obj, arguments); }; + }; +} + +/** PrivateFunction: Function.prototype.prependArg + * Prepend an argument to a function. + * + * This Function object extension method returns a Function that will + * invoke the original function with an argument prepended. This is useful + * when some object has a callback that needs to get that same object as + * an argument. The following fragment illustrates a simple case of this + * > var obj = new Foo(this.someMethod); + * + * Foo's constructor can now use func.prependArg(this) to ensure the + * passed in callback function gets the instance of Foo as an argument. + * Doing this without prependArg would mean not setting the callback + * from the constructor. + * + * This is used inside Strophe for passing the Strophe.Request object to + * the onreadystatechange handler of XMLHttpRequests. + * + * Parameters: + * arg - The argument to pass as the first parameter to the function. + * + * Returns: + * A new Function which calls the original with the prepended argument. + */ +if (!Function.prototype.prependArg) { + Function.prototype.prependArg = function (arg) + { + var func = this; + + return function () { + var newargs = [arg]; + for (var i = 0; i < arguments.length; i++) { + newargs.push(arguments[i]); + } + return func.apply(this, newargs); + }; + }; +} + +/** PrivateFunction: Array.prototype.indexOf + * Return the index of an object in an array. + * + * This function is not supplied by some JavaScript implementations, so + * we provide it if it is missing. This code is from: + * http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf + * + * Parameters: + * (Object) elt - The object to look for. + * (Integer) from - The index from which to start looking. (optional). + * + * Returns: + * The index of elt in the array or -1 if not found. + */ +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt /*, from*/) + { + var len = this.length; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && this[from] === elt) { + return from; + } + } + + return -1; + }; +} + +/* All of the Strophe globals are defined in this special function below so + * that references to the globals become closures. This will ensure that + * on page reload, these references will still be available to callbacks + * that are still executing. + */ + +(function (callback) { +var Strophe; + +/** Function: $build + * Create a Strophe.Builder. + * This is an alias for 'new Strophe.Builder(name, attrs)'. + * + * Parameters: + * (String) name - The root element name. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $build(name, attrs) { return new Strophe.Builder(name, attrs); } +/** Function: $msg + * Create a Strophe.Builder with a element as the root. + * + * Parmaeters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $msg(attrs) { return new Strophe.Builder("message", attrs); } +/** Function: $iq + * Create a Strophe.Builder with an element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $iq(attrs) { return new Strophe.Builder("iq", attrs); } +/** Function: $pres + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $pres(attrs) { return new Strophe.Builder("presence", attrs); } + +/** Class: Strophe + * An object container for all Strophe library functions. + * + * This class is just a container for all the objects and constants + * used in the library. It is not meant to be instantiated, but to + * provide a namespace for library objects, constants, and functions. + */ +Strophe = { + /** Constant: VERSION + * The version of the Strophe library. Unreleased builds will have + * a version of head-HASH where HASH is a partial revision. + */ + VERSION: "2a276a4", + + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. + * + * NS.HTTPBIND - HTTP BIND namespace from XEP 124. + * NS.BOSH - BOSH namespace from XEP 206. + * NS.CLIENT - Main XMPP client namespace. + * NS.AUTH - Legacy authentication namespace. + * NS.ROSTER - Roster operations namespace. + * NS.PROFILE - Profile namespace. + * NS.DISCO_INFO - Service discovery info namespace from XEP 30. + * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. + * NS.MUC - Multi-User Chat namespace from XEP 45. + * NS.SASL - XMPP SASL namespace from RFC 3920. + * NS.STREAM - XMPP Streams namespace from RFC 3920. + * NS.BIND - XMPP Binding namespace from RFC 3920. + * NS.SESSION - XMPP Session namespace from RFC 3920. + */ + NS: { + HTTPBIND: "http://jabber.org/protocol/httpbind", + BOSH: "urn:xmpp:xbosh", + CLIENT: "jabber:client", + AUTH: "jabber:iq:auth", + ROSTER: "jabber:iq:roster", + PROFILE: "jabber:iq:profile", + DISCO_INFO: "http://jabber.org/protocol/disco#info", + DISCO_ITEMS: "http://jabber.org/protocol/disco#items", + MUC: "http://jabber.org/protocol/muc", + SASL: "urn:ietf:params:xml:ns:xmpp-sasl", + STREAM: "http://etherx.jabber.org/streams", + BIND: "urn:ietf:params:xml:ns:xmpp-bind", + SESSION: "urn:ietf:params:xml:ns:xmpp-session", + VERSION: "jabber:iq:version", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas" + }, + + /** Function: addNamespace + * This function is used to extend the current namespaces in + * Strophe.NS. It takes a key and a value with the key being the + * name of the new namespace, with its actual value. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + * + * Parameters: + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function (name, value) + { + Strophe.NS[name] = value; + }, + + /** Constants: Connection Status Constants + * Connection status constants for use by the connection handler + * callback. + * + * Status.ERROR - An error has occurred + * Status.CONNECTING - The connection is currently being made + * Status.CONNFAIL - The connection attempt failed + * Status.AUTHENTICATING - The connection is authenticating + * Status.AUTHFAIL - The authentication attempt failed + * Status.CONNECTED - The connection has succeeded + * Status.DISCONNECTED - The connection has been terminated + * Status.DISCONNECTING - The connection is currently being terminated + * Status.ATTACHED - The connection has been attached + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7, + ATTACHED: 8 + }, + + /** Constants: Log Level Constants + * Logging level indicators. + * + * LogLevel.DEBUG - Debug output + * LogLevel.INFO - Informational output + * LogLevel.WARN - Warnings + * LogLevel.ERROR - Errors + * LogLevel.FATAL - Fatal errors + */ + LogLevel: { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + FATAL: 4 + }, + + /** PrivateConstants: DOM Element Type Constants + * DOM element types. + * + * ElementType.NORMAL - Normal element. + * ElementType.TEXT - Text data element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3 + }, + + /** PrivateConstants: Timeout Values + * Timeout values for error states. These values are in seconds. + * These should not be changed unless you know exactly what you are + * doing. + * + * TIMEOUT - Timeout multiplier. A waiting request will be considered + * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. + * This defaults to 1.1, and with default wait, 66 seconds. + * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where + * Strophe can detect early failure, it will consider the request + * failed if it doesn't return after + * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. + * This defaults to 0.1, and with default wait, 6 seconds. + */ + TIMEOUT: 1.1, + SECONDARY_TIMEOUT: 0.1, + + /** Function: forEachChild + * Map a function over some or all child elements of a given element. + * + * This is a small convenience function for mapping a function over + * some or all of the children of an element. If elemName is null, all + * children will be passed to the function, otherwise only children + * whose tag names match elemName will be passed. + * + * Parameters: + * (XMLElement) elem - The element to operate on. + * (String) elemName - The child element tag name filter. + * (Function) func - The function to apply to each child. This + * function should take a single argument, a DOM element. + */ + forEachChild: function (elem, elemName, func) + { + var i, childNode; + + for (i = 0; i < elem.childNodes.length; i++) { + childNode = elem.childNodes[i]; + if (childNode.nodeType == Strophe.ElementType.NORMAL && + (!elemName || this.isTagEqual(childNode, elemName))) { + func(childNode); + } + } + }, + + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case insensitive. + * + * Parameters: + * (XMLElement) el - A DOM element. + * (String) name - The element name. + * + * Returns: + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function (el, name) + { + return el.tagName.toLowerCase() == name.toLowerCase(); + }, + + /** PrivateVariable: _xmlGenerator + * _Private_ variable that caches a DOM document to + * generate elements. + */ + _xmlGenerator: null, + + /** PrivateFunction: _makeGenerator + * _Private_ function that creates a dummy XML DOM document to serve as + * an element and text node generator. + */ + _makeGenerator: function () { + var doc; + + if (window.ActiveXObject) { + doc = new ActiveXObject("Microsoft.XMLDOM"); + doc.appendChild(doc.createElement('strophe')); + } else { + doc = document.implementation + .createDocument('jabber:client', 'strophe', null); + } + + return doc; + }, + + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Note that these are not HTML DOM elements, which + * aren't appropriate for XMPP stanzas. + * + * Parameters: + * (String) name - The name for the element. + * (Array|Object) attrs - An optional array or object containing + * key/value pairs to use as element attributes. The object should + * be in the format {'key': 'value'} or {key: 'value'}. The array + * should have the format [['key1', 'value1'], ['key2', 'value2']]. + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function (name) + { + if (!name) { return null; } + + var node = null; + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + node = Strophe._xmlGenerator.createElement(name); + + // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + var a, i, k; + for (a = 1; a < arguments.length; a++) { + if (!arguments[a]) { continue; } + if (typeof(arguments[a]) == "string" || + typeof(arguments[a]) == "number") { + node.appendChild(Strophe.xmlTextNode(arguments[a])); + } else if (typeof(arguments[a]) == "object" && + typeof(arguments[a].sort) == "function") { + for (i = 0; i < arguments[a].length; i++) { + if (typeof(arguments[a][i]) == "object" && + typeof(arguments[a][i].sort) == "function") { + node.setAttribute(arguments[a][i][0], + arguments[a][i][1]); + } + } + } else if (typeof(arguments[a]) == "object") { + for (k in arguments[a]) { + if (arguments[a].hasOwnProperty(k)) { + node.setAttribute(k, arguments[a][k]); + } + } + } + } + + return node; + }, + + /* Function: xmlescape + * Excapes invalid xml characters. + * + * Parameters: + * (String) text - text to escape. + * + * Returns: + * Escaped text. + */ + xmlescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(//g, ">"); + return text; + }, + + /** Function: xmlTextNode + * Creates an XML DOM text node. + * + * Provides a cross implementation version of document.createTextNode. + * + * Parameters: + * (String) text - The content of the text node. + * + * Returns: + * A new XML DOM text node. + */ + xmlTextNode: function (text) + { + //ensure text is escaped + text = Strophe.xmlescape(text); + + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + return Strophe._xmlGenerator.createTextNode(text); + }, + + /** Function: getText + * Get the concatenation of all text children of an element. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A String with the concatenated text of all text element children. + */ + getText: function (elem) + { + if (!elem) { return null; } + + var str = ""; + if (elem.childNodes.length === 0 && elem.nodeType == + Strophe.ElementType.TEXT) { + str += elem.nodeValue; + } + + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } + } + + return str; + }, + + /** Function: copyElement + * Copy an XML DOM element. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + copyElement: function (elem) + { + var i, el; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + el = Strophe.xmlElement(elem.tagName); + + for (i = 0; i < elem.attributes.length; i++) { + el.setAttribute(elem.attributes[i].nodeName.toLowerCase(), + elem.attributes[i].value); + } + + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[i])); + } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: escapeNode + * Escape the node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An escaped node (or local part). + */ + escapeNode: function (node) + { + return node.replace(/^\s+|\s+$/g, '') + .replace(/\\/g, "\\5c") + .replace(/ /g, "\\20") + .replace(/\"/g, "\\22") + .replace(/\&/g, "\\26") + .replace(/\'/g, "\\27") + .replace(/\//g, "\\2f") + .replace(/:/g, "\\3a") + .replace(//g, "\\3e") + .replace(/@/g, "\\40"); + }, + + /** Function: unescapeNode + * Unescape a node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An unescaped node (or local part). + */ + unescapeNode: function (node) + { + return node.replace(/\\20/g, " ") + .replace(/\\22/g, '"') + .replace(/\\26/g, "&") + .replace(/\\27/g, "'") + .replace(/\\2f/g, "/") + .replace(/\\3a/g, ":") + .replace(/\\3c/g, "<") + .replace(/\\3e/g, ">") + .replace(/\\40/g, "@") + .replace(/\\5c/g, "\\"); + }, + + /** Function: getNodeFromJid + * Get the node portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the node. + */ + getNodeFromJid: function (jid) + { + if (jid.indexOf("@") < 0) { return null; } + return jid.split("@")[0]; + }, + + /** Function: getDomainFromJid + * Get the domain portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the domain. + */ + getDomainFromJid: function (jid) + { + var bare = Strophe.getBareJidFromJid(jid); + if (bare.indexOf("@") < 0) { + return bare; + } else { + var parts = bare.split("@"); + parts.splice(0, 1); + return parts.join('@'); + } + }, + + /** Function: getResourceFromJid + * Get the resource portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the resource. + */ + getResourceFromJid: function (jid) + { + var s = jid.split("/"); + if (s.length < 2) { return null; } + s.splice(0, 1); + return s.join('/'); + }, + + /** Function: getBareJidFromJid + * Get the bare JID from a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the bare JID. + */ + getBareJidFromJid: function (jid) + { + return jid ? jid.split("/")[0] : null; + }, + + /** Function: log + * User overrideable logging function. + * + * This function is called whenever the Strophe library calls any + * of the logging functions. The default implementation of this + * function does nothing. If client code wishes to handle the logging + * messages, it should override this with + * > Strophe.log = function (level, msg) { + * > (user code here) + * > }; + * + * Please note that data sent and received over the wire is logged + * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput(). + * + * The different levels and their meanings are + * + * DEBUG - Messages useful for debugging purposes. + * INFO - Informational messages. This is mostly information like + * 'disconnect was called' or 'SASL auth succeeded'. + * WARN - Warnings about potential problems. This is mostly used + * to report transient connection errors like request timeouts. + * ERROR - Some error occurred. + * FATAL - A non-recoverable fatal error occurred. + * + * Parameters: + * (Integer) level - The log level of the log message. This will + * be one of the values in Strophe.LogLevel. + * (String) msg - The log message. + */ + log: function (level, msg) + { + return; + }, + + /** Function: debug + * Log a message at the Strophe.LogLevel.DEBUG level. + * + * Parameters: + * (String) msg - The log message. + */ + debug: function(msg) + { + this.log(this.LogLevel.DEBUG, msg); + }, + + /** Function: info + * Log a message at the Strophe.LogLevel.INFO level. + * + * Parameters: + * (String) msg - The log message. + */ + info: function (msg) + { + this.log(this.LogLevel.INFO, msg); + }, + + /** Function: warn + * Log a message at the Strophe.LogLevel.WARN level. + * + * Parameters: + * (String) msg - The log message. + */ + warn: function (msg) + { + this.log(this.LogLevel.WARN, msg); + }, + + /** Function: error + * Log a message at the Strophe.LogLevel.ERROR level. + * + * Parameters: + * (String) msg - The log message. + */ + error: function (msg) + { + this.log(this.LogLevel.ERROR, msg); + }, + + /** Function: fatal + * Log a message at the Strophe.LogLevel.FATAL level. + * + * Parameters: + * (String) msg - The log message. + */ + fatal: function (msg) + { + this.log(this.LogLevel.FATAL, msg); + }, + + /** Function: serialize + * Render a DOM element and all descendants to a String. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The serialized element tree as a String. + */ + serialize: function (elem) + { + var result; + + if (!elem) { return null; } + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + + var nodeName = elem.nodeName; + var i, child; + + if (elem.getAttribute("_realname")) { + nodeName = elem.getAttribute("_realname"); + } + + result = "<" + nodeName; + for (i = 0; i < elem.attributes.length; i++) { + if(elem.attributes[i].nodeName != "_realname") { + result += " " + elem.attributes[i].nodeName.toLowerCase() + + "='" + elem.attributes[i].value + .replace("&", "&") + .replace("'", "'") + .replace("<", "<") + "'"; + } + } + + if (elem.childNodes.length > 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeType == Strophe.ElementType.NORMAL) { + // normal element, so recurse + result += Strophe.serialize(child); + } else if (child.nodeType == Strophe.ElementType.TEXT) { + // text element + result += child.nodeValue; + } + } + result += ""; + } else { + result += "/>"; + } + + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0, + + /** PrivateVariable: Strophe.connectionPlugins + * _Private_ variable Used to store plugin names that need + * initialization on Strophe.Connection construction. + */ + _connectionPlugins: {}, + + /** Function: addConnectionPlugin + * Extends the Strophe.Connection object with the given plugin. + * + * Paramaters: + * (String) name - The name of the extension. + * (Object) ptype - The plugin's prototype. + */ + addConnectionPlugin: function (name, ptype) + { + Strophe._connectionPlugins[name] = ptype; + } +}; + +/** Class: Strophe.Builder + * XML DOM builder. + * + * This object provides an interface similar to JQuery but for building + * DOM element easily and rapidly. All the functions except for toString() + * and tree() return the object, so calls can be chained. Here's an + * example using the $iq() builder helper. + * > $iq({to: 'you', from: 'me', type: 'get', id: '1'}) + * > .c('query', {xmlns: 'strophe:example'}) + * > .c('example') + * > .toString() + * The above generates this XML fragment + * > + * > + * > + * > + * > + * The corresponding DOM manipulations to get a similar fragment would be + * a lot more tedious and probably involve several helper variables. + * + * Since adding children makes new operations operate on the child, up() + * is provided to traverse up the tree. To add two children, do + * > builder.c('child1', ...).up().c('child2', ...) + * The next operation on the Builder will be relative to the second child. + */ + +/** Constructor: Strophe.Builder + * Create a Strophe.Builder object. + * + * The attributes should be passed in object notation. For example + * > var b = new Builder('message', {to: 'you', from: 'me'}); + * or + * > var b = new Builder('messsage', {'xml:lang': 'en'}); + * + * Parameters: + * (String) name - The name of the root element. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder. + */ +Strophe.Builder = function (name, attrs) +{ + // Set correct namespace for jabber:client elements + if (name == "presence" || name == "message" || name == "iq") { + if (attrs && !attrs.xmlns) { + attrs.xmlns = Strophe.NS.CLIENT; + } else if (!attrs) { + attrs = {xmlns: Strophe.NS.CLIENT}; + } + } + + // Holds the tree being built. + this.nodeTree = Strophe.xmlElement(name, attrs); + + // Points to the current operation node. + this.node = this.nodeTree; +}; + +Strophe.Builder.prototype = { + /** Function: tree + * Return the DOM tree. + * + * This function returns the current DOM tree as an element object. This + * is suitable for passing to functions like Strophe.Connection.send(). + * + * Returns: + * The DOM tree as a element object. + */ + tree: function () + { + return this.nodeTree; + }, + + /** Function: toString + * Serialize the DOM tree to a String. + * + * This function returns a string serialization of the current DOM + * tree. It is often used internally to pass data to a + * Strophe.Request object. + * + * Returns: + * The serialized DOM tree in a String. + */ + toString: function () + { + return Strophe.serialize(this.nodeTree); + }, + + /** Function: up + * Make the current parent element the new current element. + * + * This function is often used after c() to traverse back up the tree. + * For example, to add two children to the same element + * > builder.c('child1', {}).up().c('child2', {}); + * + * Returns: + * The Stophe.Builder object. + */ + up: function () + { + this.node = this.node.parentNode; + return this; + }, + + /** Function: attrs + * Add or modify attributes of the current element. + * + * The attributes should be passed in object notation. This function + * does not move the current element pointer. + * + * Parameters: + * (Object) moreattrs - The attributes to add/modify in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + attrs: function (moreattrs) + { + for (var k in moreattrs) { + if (moreattrs.hasOwnProperty(k)) { + this.node.setAttribute(k, moreattrs[k]); + } + } + return this; + }, + + /** Function: c + * Add a child to the current element and make it the new current + * element. + * + * This function moves the current element pointer to the child. If you + * need to add another child, it is necessary to use up() to go back + * to the parent in the tree. + * + * Parameters: + * (String) name - The name of the child. + * (Object) attrs - The attributes of the child in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + c: function (name, attrs) + { + var child = Strophe.xmlElement(name, attrs); + this.node.appendChild(child); + this.node = child; + return this; + }, + + /** Function: cnode + * Add a child to the current element and make it the new current + * element. + * + * This function is the same as c() except that instead of using a + * name and an attributes object to create the child it uses an + * existing DOM element object. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The Strophe.Builder object. + */ + cnode: function (elem) + { + this.node.appendChild(elem); + this.node = elem; + return this; + }, + + /** Function: t + * Add a child text element. + * + * This *does not* make the child the new current element since there + * are no children of text elements. + * + * Parameters: + * (String) text - The text data to append to the current element. + * + * Returns: + * The Strophe.Builder object. + */ + t: function (text) + { + var child = Strophe.xmlTextNode(text); + this.node.appendChild(child); + return this; + } +}; + + +/** PrivateClass: Strophe.Handler + * _Private_ helper class for managing stanza handlers. + * + * A Strophe.Handler encapsulates a user provided callback function to be + * executed when matching stanzas are received by the connection. + * Handlers can be either one-off or persistant depending on their + * return value. Returning true will cause a Handler to remain active, and + * returning false will remove the Handler. + * + * Users will not use Strophe.Handler objects directly, but instead they + * will use Strophe.Connection.addHandler() and + * Strophe.Connection.deleteHandler(). + */ + +/** PrivateConstructor: Strophe.Handler + * Create and initialize a new Strophe.Handler. + * + * Parameters: + * (Function) handler - A function to be executed when the handler is run. + * (String) ns - The namespace to match. + * (String) name - The element name to match. + * (String) type - The element type to match. + * (String) id - The element id attribute to match. + * (String) from - The element from attribute to match. + * (Object) options - Handler options + * + * Returns: + * A new Strophe.Handler object. + */ +Strophe.Handler = function (handler, ns, name, type, id, from, options) +{ + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.options = options || {matchbare: false}; + + // default matchBare to false if undefined + if (!this.options.matchBare) { + this.options.matchBare = false; + } + + if (this.options.matchBare) { + this.from = from ? Strophe.getBareJidFromJid(from) : null; + } else { + this.from = from; + } + + // whether the handler is a user handler or a system handler + this.user = true; +}; + +Strophe.Handler.prototype = { + /** PrivateFunction: isMatch + * Tests if a stanza matches the Strophe.Handler. + * + * Parameters: + * (XMLElement) elem - The XML element to test. + * + * Returns: + * true if the stanza matches and false otherwise. + */ + isMatch: function (elem) + { + var nsMatch; + var from = null; + + if (this.options.matchBare) { + from = Strophe.getBareJidFromJid(elem.getAttribute('from')); + } else { + from = elem.getAttribute('from'); + } + + nsMatch = false; + if (!this.ns) { + nsMatch = true; + } else { + var that = this; + Strophe.forEachChild(elem, null, function (elem) { + if (elem.getAttribute("xmlns") == that.ns) { + nsMatch = true; + } + }); + + nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; + } + + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || elem.getAttribute("type") == this.type) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || from == this.from)) { + return true; + } + + return false; + }, + + /** PrivateFunction: run + * Run the callback on a matching stanza. + * + * Parameters: + * (XMLElement) elem - The DOM element that triggered the + * Strophe.Handler. + * + * Returns: + * A boolean indicating if the handler should remain active. + */ + run: function (elem) + { + var result = null; + try { + result = this.handler(elem); + } catch (e) { + if (e.sourceURL) { + Strophe.fatal("error: " + this.handler + + " " + e.sourceURL + ":" + + e.line + " - " + e.name + ": " + e.message); + } else if (e.fileName) { + if (typeof(console) != "undefined") { + console.trace(); + console.error(this.handler, " - error - ", e, e.message); + } + Strophe.fatal("error: " + this.handler + " " + + e.fileName + ":" + e.lineNumber + " - " + + e.name + ": " + e.message); + } else { + Strophe.fatal("error: " + this.handler); + } + + throw e; + } + + return result; + }, + + /** PrivateFunction: toString + * Get a String representation of the Strophe.Handler object. + * + * Returns: + * A String. + */ + toString: function () + { + return "{Handler: " + this.handler + "(" + this.name + "," + + this.id + "," + this.ns + ")}"; + } +}; + +/** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. + * + * A Strophe.TimedHandler encapsulates a user provided callback that + * should be called after a certain period of time or at regular + * intervals. The return value of the callback determines whether the + * Strophe.TimedHandler will continue to fire. + * + * Users will not use Strophe.TimedHandler objects directly, but instead + * they will use Strophe.Connection.addTimedHandler() and + * Strophe.Connection.deleteTimedHandler(). + */ + +/** PrivateConstructor: Strophe.TimedHandler + * Create and initialize a new Strophe.TimedHandler object. + * + * Parameters: + * (Integer) period - The number of milliseconds to wait before the + * handler is called. + * (Function) handler - The callback to run when the handler fires. This + * function should take no arguments. + * + * Returns: + * A new Strophe.TimedHandler object. + */ +Strophe.TimedHandler = function (period, handler) +{ + this.period = period; + this.handler = handler; + + this.lastCalled = new Date().getTime(); + this.user = true; +}; + +Strophe.TimedHandler.prototype = { + /** PrivateFunction: run + * Run the callback for the Strophe.TimedHandler. + * + * Returns: + * true if the Strophe.TimedHandler should be called again, and false + * otherwise. + */ + run: function () + { + this.lastCalled = new Date().getTime(); + return this.handler(); + }, + + /** PrivateFunction: reset + * Reset the last called time for the Strophe.TimedHandler. + */ + reset: function () + { + this.lastCalled = new Date().getTime(); + }, + + /** PrivateFunction: toString + * Get a string representation of the Strophe.TimedHandler object. + * + * Returns: + * The string representation. + */ + toString: function () + { + return "{TimedHandler: " + this.handler + "(" + this.period +")}"; + } +}; + +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (XMLElement) elem - The XML data to be sent in the request. + * (Function) func - The function that will be called when the + * XMLHttpRequest readyState changes. + * (Integer) rid - The BOSH rid attribute associated with this request. + * (Integer) sends - The number of times this same request has been + * sent. + */ +Strophe.Request = function (elem, func, rid, sends) +{ + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); + // save original function in case we need to make a new request + // from this one. + this.origFunc = func; + this.func = func; + this.rid = rid; + this.date = NaN; + this.sends = sends || 0; + this.abort = false; + this.dead = null; + this.age = function () { + if (!this.date) { return 0; } + var now = new Date(); + return (now - this.date) / 1000; + }; + this.timeDead = function () { + if (!this.dead) { return 0; } + var now = new Date(); + return (now - this.dead) / 1000; + }; + this.xhr = this._newXHR(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function () + { + var node = null; + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + if (node.tagName == "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + throw "parsererror"; + } + } else if (this.xhr.responseText) { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function () + { + var xhr = null; + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } + + xhr.onreadystatechange = this.func.prependArg(this); + + return xhr; + } +}; + + + +if (callback) { + callback(Strophe, $build, $msg, $iq, $pres); +} + +})(function () { + window.Strophe = arguments[0]; + window.$build = arguments[1]; + window.$msg = arguments[2]; + window.$iq = arguments[3]; + window.$pres = arguments[4]; +}); +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The BOSH service URL. + * + * Returns: + * A new Strophe.Connection object. + */ +Strophe.Connection = function (service) +{ + /* The path to the httpbind service. */ + this.service = service; + /* The connected JID. */ + this.jid = ""; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this.paused = false; + + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); + } + } +}; + +Strophe.Connection.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function () + { + this.rid = Math.floor(Math.random() * 4294967295); + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this._requests = []; + this._uniqueId = Math.round(Math.random()*10000); + }, + + /** Function: pause + * Pause the request manager. + * + * This will prevent Strophe from sending any more requests to the + * server. This is very useful for temporarily pausing while a lot + * of send() calls are happening quickly. This causes Strophe to + * send the data in a single request, saving many request trips. + */ + pause: function () + { + this.paused = true; + }, + + /** Function: resume + * Resume the request manager. + * + * This resumes after pause() has been called. + */ + resume: function () + { + this.paused = false; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + */ + connect: function (jid, pass, callback, wait, hold) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + + // build the body tag + var body = this._buildBody().attrs({ + to: this.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._connect_cb.bind(this)), + body.tree().rid)); + this._throttledRequestHandler(); + }, + + /** Function: attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + attach: function (jid, sid, rid, callback, wait, hold, wind) + { + this.jid = jid; + this.sid = sid; + this.rid = rid; + this.connect_callback = callback; + + this.domain = Strophe.getDomainFromJid(this.jid); + + this.authenticated = true; + this.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + xmlInput: function (elem) + { + return; + }, + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + xmlOutput: function (elem) + { + return; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function (elem) + { + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + this._queueData(elem[i]); + } + } else if (typeof(elem.tree) === "function") { + this._queueData(elem.tree()); + } else { + this._queueData(elem); + } + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: flush + * Immediately send any pending outgoing data. + * + * Normally send() queues outgoing data until the next idle period + * (100ms), which optimizes network use in the common cases when + * several send()s are called in succession. flush() can be used to + * immediately send all pending data. + */ + flush: function () + { + // cancel the pending idle period and run the idle function + // immediately + clearTimeout(this._idleTimeout); + this._onIdle(); + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); + + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) { + callback(stanza); + } + } else if (iqtype == 'error') { + if (errback) { + errback(stanza); + } + } else { + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; + } + }, null, 'iq', null, id); + + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + + this.send(elem); + + return id; + }, + + /** PrivateFunction: _queueData + * Queue outgoing data for later sending. Also ensures that the data + * is a DOMElement. + */ + _queueData: function (element) { + if (element === null || + !element.tagName || + !element.childNodes) { + throw { + name: "StropheError", + message: "Cannot queue non-DOMElement." + }; + } + + this._data.push(element); + }, + + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); + } + } + } + } + + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); + } + } + }, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function (req) + { + Strophe.debug("removing request"); + + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); + } + } + + // IE6 fails on setting to null, so set to empty function + req.xhr.onreadystatechange = function () {}; + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _restartRequest + * _Private_ function to restart a request that is presumed dead. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _restartRequest: function (i) + { + var req = this._requests[i]; + if (req.dead === null) { + req.dead = new Date(); + } + + this._processRequest(i); + }, + + /** PrivateFunction: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function (i) + { + var req = this._requests[i]; + var reqStatus = -1; + + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); + } + req.abort = true; + req.xhr.abort(); + // setting to null fails on IE6, so set to empty function + req.xhr.onreadystatechange = function () {}; + this._requests[i] = new Strophe.Request(req.xmlData, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; + } + + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + req.date = new Date(); + try { + req.xhr.open("POST", this.service, true); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this.connected) { + this._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); + } + this.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.xhr.send(req.data); + }; + + // Implement progressive backoff for reconnects -- + // First retry (send == 1) should also be instantaneous + if (req.sends > 1) { + // Using a cube of the retry number creats a nicely + // expanding retry window + var backoff = Math.pow(req.sends, 3) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } + + req.sends++; + + this.xmlOutput(req.xmlData); + this.rawOutput(req.data); + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function () + { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window - 1) { + this._processRequest(1); + } + }, + + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function (func, req) + { + Strophe.debug("request id " + req.id + + "." + req.sends + " state changed to " + + req.xhr.readyState); + + if (req.abort) { + req.abort = false; + return; + } + + // request complete + var reqStatus; + if (req.xhr.readyState == 4) { + reqStatus = 0; + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. works + // around a browser bug + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; + } + + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } + + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); + + if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { + // remove from internal queue + this._removeRequest(req); + Strophe.debug("request id " + + req.id + + " should now be removed"); + } + + // request succeeded + if (reqStatus == 200) { + // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + if (reqIs1 || + (reqIs0 && this._requests.length > 0 && + this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { + this._restartRequest(0); + } + // call handler + Strophe.debug("request id " + + req.id + "." + + req.sends + " got 200"); + func(req); + this.errors = 0; + } else { + Strophe.error("request id " + + req.id + "." + + req.sends + " error " + reqStatus + + " happened"); + if (reqStatus === 0 || + (reqStatus >= 400 && reqStatus < 600) || + reqStatus >= 12000) { + this._hitError(reqStatus); + if (reqStatus >= 400 && reqStatus < 500) { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._doDisconnect(); + } + } + } + + if (!((reqStatus > 0 && reqStatus < 10000) || + req.sends > 5)) { + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function (reqStatus) + { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + + ", number of errors: " + this.errors); + if (this.errors > 4) { + this._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + this.rid = Math.floor(Math.random() * 4294967295); + + // tell the parent we disconnected + if (this.connected) { + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (req) + { + try { + var elem = req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this.disconnect("strophe-parsererror"); + } + if (elem === null) { return; } + + this.xmlInput(elem); + this.rawInput(Strophe.serialize(elem)); + + // remove handlers scheduled for deletion + var i, hand; + while (this.removeHandlers.length > 0) { + hand = this.removeHandlers.pop(); + i = this.handlers.indexOf(hand); + if (i >= 0) { + this.handlers.splice(i, 1); + } + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting && this._requests.length === 0) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + return; + } + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = elem.getAttribute("condition"); + conflict = elem.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this.disconnect(); + return; + } + + // send each incoming stanza through the handler chain + var that = this; + Strophe.forEachChild(elem, null, function (child) { + var i, newList; + // process handlers + newList = that.handlers; + that.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { + that.handlers.push(hand); + } + } else { + that.handlers.push(hand); + } + } + }); + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (this.authenticated) { + body.c('presence', { + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().getAttribute("rid")); + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (req) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + var bodyWrap = req.getResponse(); + if (!bodyWrap) { return; } + + this.xmlInput(bodyWrap); + this.rawInput(Strophe.serialize(bodyWrap)); + + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + return; + } + + // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + if (!this.stream_id) { + this.stream_id = bodyWrap.getAttribute("authid"); + } + var wind = bodyWrap.getAttribute('requests'); + if (wind) { this.window = parseInt(wind, 10); } + var hold = bodyWrap.getAttribute('hold'); + if (hold) { this.hold = parseInt(hold, 10); } + var wait = bodyWrap.getAttribute('wait'); + if (wait) { this.wait = parseInt(wait, 10); } + + + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } else { + // we didn't get stream:features yet, so we need wait for it + // by sending a blank poll request + var body = this._buildBody(); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._connect_cb.bind(this)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + return; + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.getBareJidFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = Base64.encode(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); + + this.send($iq({ + type: "get", + to: this.domain, + id: "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = Base64.decode(Strophe.getText(elem)); + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + this.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + + this._quote(Strophe.getNodeFromJid(this.jid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + this._quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + _quote: function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }, + + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .up() + .c('password').t(this.pass); + + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + + // we must send an xmpp:restart now + this._sendRestart(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, + null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + if (resource) { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .c('resource', {}).t(resource).tree()); + } else { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .tree()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } else { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; + } + + // actually disconnect + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function () + { + var i, thand, since, newList; + + // remove timed handlers that have been scheduled for deletion + while (this.removeTimeds.length > 0) { + thand = this.removeTimeds.pop(); + i = this.timedHandlers.indexOf(thand); + if (i >= 0) { + this.timedHandlers.splice(i, 1); + } + } + + // add timed handlers scheduled for addition + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } + + // call ready timed handlers + var now = new Date().getTime(); + newList = []; + for (i = 0; i < this.timedHandlers.length; i++) { + thand = this.timedHandlers[i]; + if (this.authenticated || !thand.user) { + since = thand.lastCalled + thand.period; + if (since - now <= 0) { + if (thand.run()) { + newList.push(thand); + } + } else { + newList.push(thand); + } + } + } + this.timedHandlers = newList; + + var body, time_elapsed; + + // if no requests are in progress, poll + if (this.authenticated && this._requests.length === 0 && + this._data.length === 0 && !this.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + this._data.push(null); + } + + if (this._requests.length < 2 && this._data.length > 0 && + !this.paused) { + body = this._buildBody(); + for (i = 0; i < this._data.length; i++) { + if (this._data[i] !== null) { + if (this._data[i] === "restart") { + body.attrs({ + to: this.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(this._data[i]).up(); + } + } + } + delete this._data; + this._data = []; + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().rid)); // TODO Fix that. + this._processRequest(this._requests.length - 1); + } + + if (this._requests.length > 0) { + time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } + } + + if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { + Strophe.warn("Request " + + this._requests[0].id + + " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + + " seconds since last activity"); + this._throttledRequestHandler(); + } + } + + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +};/* + Copyright 2008, Stanziq Inc. +*/ + +Strophe.addConnectionPlugin('pubsub', { +/* + Extend connection object to have plugin name 'pubsub'. +*/ + _connection: null, + + //The plugin must have the init function. + init: function(conn) { + + this._connection = conn; + + /* + Function used to setup plugin. + */ + + /* extend name space + * NS.PUBSUB - XMPP Publish Subscribe namespace + * from XEP 60. + * + * NS.PUBSUB_SUBSCRIBE_OPTIONS - XMPP pubsub + * options namespace from XEP 60. + */ + Strophe.addNamespace('PUBSUB',"http://jabber.org/protocol/pubsub"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_OPTIONS', + Strophe.NS.PUBSUB+"#subscribe_options"); + Strophe.addNamespace('PUBSUB_ERRORS',Strophe.NS.PUBSUB+"#errors"); + Strophe.addNamespace('PUBSUB_EVENT',Strophe.NS.PUBSUB+"#event"); + Strophe.addNamespace('PUBSUB_OWNER',Strophe.NS.PUBSUB+"#owner"); + Strophe.addNamespace('PUBSUB_AUTO_CREATE', + Strophe.NS.PUBSUB+"#auto-create"); + Strophe.addNamespace('PUBSUB_PUBLISH_OPTIONS', + Strophe.NS.PUBSUB+"#publish-options"); + Strophe.addNamespace('PUBSUB_NODE_CONFIG', + Strophe.NS.PUBSUB+"#node_config"); + Strophe.addNamespace('PUBSUB_CREATE_AND_CONFIGURE', + Strophe.NS.PUBSUB+"#create-and-configure"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_AUTHORIZATION', + Strophe.NS.PUBSUB+"#subscribe_authorization"); + Strophe.addNamespace('PUBSUB_GET_PENDING', + Strophe.NS.PUBSUB+"#get-pending"); + Strophe.addNamespace('PUBSUB_MANAGE_SUBSCRIPTIONS', + Strophe.NS.PUBSUB+"#manage-subscriptions"); + Strophe.addNamespace('PUBSUB_META_DATA', + Strophe.NS.PUBSUB+"#meta-data"); + + }, + /***Function + + Create a pubsub node on the given service with the given node + name. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Dictionary) options - The configuration options for the node. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + createNode: function(jid,service,node,options, call_back) { + + var iqid = this._connection.getUniqueId("pubsubcreatenode"); + + var iq = $iq({from:jid, to:service, type:'set', id:iqid}); + + var c_options = Strophe.xmlElement("configure",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB+"#node_config"); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + for (var i in options) + { + var val = options[i]; + x.appendChild(val); + } + + if(options.length && options.length != 0) + { + c_options.appendChild(x); + } + + iq.c('pubsub', + {xmlns:Strophe.NS.PUBSUB}).c('create', + {node:node}).up().cnode(c_options); + + this._connection.addHandler(call_back, + null, + 'iq', + null, + iqid, + null); + this._connection.send(iq.tree()); + return iqid; + }, + /***Function + Subscribe to a node in order to receive event items. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) options - The configuration options for the node. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + subscribe: function(jid,service,node,options, call_back) { + var subid = this._connection.getUniqueId("subscribenode"); + //create subscription options + var sub_options = Strophe.xmlElement("options",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB_SUBSCRIBE_OPTIONS); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + + if(options && options.length && options.length !== 0) + { + for (var i = 0; i < options.length; i++) + { + var val = options[i]; + x.appendChild(val); + } + sub_options.appendChild(x); + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}).up().cnode(sub_options); + } + else + { + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}); + } + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + return subid; + + }, + /***Function + Unsubscribe from a node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Function) call_back - Used to determine if node + creation was sucessful. + + */ + unsubscribe: function(jid,service,node, call_back) { + + var subid = this._connection.getUniqueId("unsubscribenode"); + + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('unsubscribe', + {node:node,jid:jid}); + + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + + + return subid; + + }, + /***Function + + Publish and item to the given pubsub node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) items - The list of items to be published. + (Function) call_back - Used to determine if node + creation was sucessful. + */ + publish: function(jid, service, node, items, call_back) { + var pubid = this._connection.getUniqueId("publishnode"); + + + var publish_elem = Strophe.xmlElement("publish", + [["node", + node]/*, + ["jid", + jid]*/]); + for (var i in items) + { + var item = Strophe.xmlElement("item",[["id", items[i].id]]); + item.appendChild(items[i].value[0]); + publish_elem.appendChild(item); + } + + var pub = $iq({from:jid, to:service, type:'set', id:pubid}) + pub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).cnode(publish_elem); + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + pubid, + null); + this._connection.send(pub.tree()); + + + return pubid; + }, + + deleteNode: function(jid,service,node,call_back) { + + var subid = this._connection.getUniqueId("deletenode"); + + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB_OWNER }).c('delete', + {node:node}); + + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + + + return subid; + + }, + /*Function: items + Used to retrieve the persistent items from the pubsub node. + + */ + items: function(jid,service,node, maxitems, ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('pubsub', + { xmlns:Strophe.NS.PUBSUB }).c('items',{node:node, 'max_items': maxitems}); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + }, + item: function(jid,service,node, itemid, ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('pubsub', + { xmlns:Strophe.NS.PUBSUB }).c('items',{node:node}).c('item', {id: itemid}); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + } +}); +// Contact object +function Contact() { + this.name = ""; + this.resources = {}; + this.subscription = "none"; + this.ask = ""; + this.groups = []; +} + +Contact.prototype = { + // compute whether user is online from their + // list of resources + online: function () { + var result = false; + for (var k in this.resources) { + result = true; + break; + } + return result; + } +}; + +// example roster plugin +Strophe.addConnectionPlugin('roster', { + init: function (connection) { + this.connection = connection; + this.contacts = {}; + + Strophe.addNamespace('ROSTER', 'jabber:iq:roster'); + }, + + // called when connection status is changed + statusChanged: function (status) { + if (status === Strophe.Status.CONNECTED) { + this.contacts = {}; + + // set up handlers for updates + this.connection.addHandler(this.rosterChanged.bind(this), + Strophe.NS.ROSTER, "iq", "set"); + this.connection.addHandler(this.presenceChanged.bind(this), + null, "presence"); + + // build and send initial roster query + var roster_iq = $iq({type: "get"}) + .c('query', {xmlns: Strophe.NS.ROSTER}); + + var that = this; + this.connection.sendIQ(roster_iq, function (iq) { + $(iq).find("item").each(function () { + // build a new contact and add it to the roster + var contact = new Contact(); + contact.name = $(this).attr('name') || ""; + contact.subscription = $(this).attr('subscription') || + "none"; + contact.ask = $(this).attr('ask') || ""; + $(this).find("group").each(function () { + contact.groups.push($(this).text()); + }); + that.contacts[$(this).attr('jid')] = contact; + }); + + // let user code know something happened + $(document).trigger('roster_changed', that); + }); + } else if (status === Strophe.Status.DISCONNECTED) { + // set all users offline + for (var contact in this.contacts) { + this.contacts[contact].resources = {}; + } + + // notify user code + $(document).trigger('roster_changed', this); + } + }, + + // called when roster udpates are received + rosterChanged: function (iq) { + var item = $(iq).find('item'); + var jid = item.attr('jid'); + var subscription = item.attr('subscription') || ""; + + if (subscription === "remove") { + // removing contact from roster + delete this.contacts[jid]; + } else if (subscription === "none") { + // adding contact to roster + var contact = new Contact(); + contact.name = item.attr('name') || ""; + item.find("group").each(function () { + contact.groups.push(this.text()); + }); + this.contacts[jid] = contact; + } else { + // modifying contact on roster + var contact = this.contacts[jid]; + contact.name = item.attr('name') || contact.name; + contact.subscription = subscription || contact.subscription; + contact.ask = item.attr('ask') || contact.ask; + contact.groups = []; + item.find("group").each(function () { + contact.groups.push(this.text()); + }); + } + + // acknowledge receipt + this.connection.send($iq({type: "result", id: $(iq).attr('id')})); + + // notify user code of roster changes + $(document).trigger("roster_changed", this); + + return true; + }, + + // called when presence stanzas are received + presenceChanged: function (presence) { + var from = $(presence).attr("from"); + var jid = Strophe.getBareJidFromJid(from); + var resource = Strophe.getResourceFromJid(from); + var ptype = $(presence).attr("type") || "available"; + + if (!this.contacts[jid] || ptype === "error") { + // ignore presence updates from things not on the roster + // as well as error presence + return true; + } + + if (ptype === "unavailable") { + // remove resource, contact went offline + delete this.contacts[jid].resources[resource]; + } else { + // contact came online or changed status + this.contacts[jid].resources[resource] = { + show: $(presence).find("show").text() || "online", + status: $(presence).find("status").text() + }; + } + + // notify user code of roster changes + $(document).trigger("roster_changed", this); + + return true; + }, + + // add a contact to the roster + addContact: function (jid, name, groups) { + var iq = $iq({type: "set"}) + .c("query", {xmlns: Strophe.NS.ROSTER}) + .c("item", {name: name || "", jid: jid}); + if (groups && groups.length > 0) { + $.each(groups, function () { + iq.c("group").t(this).up(); + }); + } + this.connection.sendIQ(iq); + }, + + // delete a contact from the roster + deleteContact: function (jid) { + var iq = $iq({type: "set"}) + .c("query", {xmlns: Strophe.NS.ROSTER}) + .c("item", {jid: jid, subscription: "remove"}); + this.connection.sendIQ(iq); + }, + + + // modify a roster contact + modifyContact: function (jid, name, groups) { + this.addContact(jid, name, groups); + }, + + // subscribe to a new contact's presence + subscribe: function (jid, name, groups) { + this.addContact(jid, name, groups); + + var presence = $pres({to: jid, "type": "subscribe"}); + this.connection.send(presence); + }, + + // unsubscribe from a contact's presence + unsubscribe: function (jid) { + var presence = $pres({to: jid, "type": "unsubscribe"}); + this.connection.send(presence); + + this.deleteContact(jid); + } +}); +/** Class: Strophe.WebSocket + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The WebSocket service URL. + * + * Returns: + * A new Strophe.WebSocket object. + */ +Strophe.Status.REBINDFAILED = 9; +Strophe.WebSocket = function (service) +{ + /* The websocket url. */ + this.service = service; + this.ws = null; + this.connect_timeout=300; + + /* The connected JID. */ + this.jid = ""; + /* The current stream ID. */ + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + this._keep_alive_timer = 20000 + this.errors = 0; + + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + this._rebind_success_handler = null; + this._rebind_failure_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); + } + } +}; + +Strophe.WebSocket.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function () + { + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + }, + + /** Function: pause + * UNUSED with websockets + */ + pause: function () + { + return; + }, + + /** Function: resume + * UNUSED with websockets + */ + resume: function () + { + return; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback The connect callback function. + */ + connect: function (jid, pass, callback) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + if(window.WebSocket){ + try{ + this.ws = new WebSocket(this.service); + this.ws.onopen = this._send_initial_stream.bind(this); + this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this)); + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._connect_cb.bind(this)); + this.ws.onerror = function(e){ Strophe.error("error : " + e)}; + this.ws.onclose = this._ws_on_close.bind(this); + } catch(e){ + //console.log("exception "+e); + } + + } else{ + throw "no websocket support" + } + + + }, + + /** Function: attach + * UNUSED, use rebind + */ + attach: function(){ return }, + + rebind: function (jid, sid, callback) + { + this.jid = jid; + this.connect_callback = callback; + this.domain = Strophe.getDomainFromJid(this.jid); + this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this)); + this.ws = new WebSocket(this.service); + this.ws.onopen = this._send_initial_stream.bind(this); + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._rebind_cb.bind(this) + .prependArg(jid).prependArg(sid)); + this.ws.onclose = this._ws_on_close.bind(this); + }, + + save: function(success, failure){ + var push = $iq({type: "set"}).c("push", {xmlns: "p1:push"}) + .c("keepalive", {max: "30"}) + .up() + .c("session", {duration:"1"}); + this.sendIQ(push, success,failure ); + }, + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + xmlInput: function (elem) + { + return; + }, + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + xmlOutput: function (elem) + { + return; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function (elem) + { + var toSend = ""; + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + toSend += Strophe.serialize(elem[i]); + this.xmlOutput(elem); + } + } else if (typeof(elem.tree) === "function") { + toSend = Strophe.serialize(elem.tree()); + this.xmlOutput(elem.tree()); + } else { + toSend = Strophe.serialize(elem); + this.xmlOutput(elem); + } + + this.rawOutput(toSend); + this.ws.send(toSend); + }, + + /** Function: flush + * UNUSED + */ + flush: function () + { + return + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); + + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) {callback(stanza);} + } else if (iqtype == 'error') { + if (errback) { errback(stanza);} + } else { + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; + } + }, null, 'iq', null, id); + + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + + this.send(elem); + + return id; + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); + } + } + } + } + + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); + } + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + + // tell the parent we disconnected + if (this.connected) { + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + if(this.ws.readyState != this.ws.CLOSED) + { + this.ws.close(); + } + }, + + _ws_on_close: function(ev){ + Strophe.info("websocket closed"); + this._doDisconnect(); + }, + + _keep_alive_handler: function(){ + this.ws.send("\n"); + return true; + }, + + _send_initial_stream: function(){ + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + var stream = '' + this.rawOutput(stream); + this.ws.send(stream); + }, + + _get_stream_id: function(onmessage,event){ + elem = event.data + this.rawInput(elem); + if (event.data.match(/id=[\'\"]([^\'\"]+)[\'\"]/)) + this.streamId = RegExp.$1; + this.ws.onmessage = onmessage; + }, + + _parseTree: function(elem){ + try { + if(this._xml_parse == undefined){ + if (window.DOMParser){ + this._xml_parser=new DOMParser(); + this._xml_parse= function(text){ + // Because FF wants valid XML, with correct namespaces ! + return this._xml_parser.parseFromString("" + text + + "", "text/xml") + .documentElement.firstChild; + } + } + else{ // Internet Explorer + this._xml_parse= function(text){ + var _xml_parser=new ActiveXObject("MSXML2.DomDocument"); + _xml_parser.async="false"; + _xml_parser.loadXML("" + text+ ""); + return _xml_parser.documentElement.firstChild; + } + } + } + return this._xml_parse(elem); + } catch (e) {Strophe.error("Error : " + e.message) } + return null; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (event) + { + var elem; + try { + elem = this._parseTree(event.data); + } catch (e) { + if (e != "parsererror") { throw e; } + this.disconnect("strophe-parsererror"); + } + if (elem === null) { return; } + this.xmlInput(elem); + this.rawInput(event.data); + + // remove handlers scheduled for deletion + var i, hand; + while (this.removeHandlers.length > 0) { + hand = this.removeHandlers.pop(); + i = this.handlers.indexOf(hand); + if (i >= 0) { + this.handlers.splice(i, 1); + } + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + return; + } + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = elem.getAttribute("condition"); + conflict = elem.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this.disconnect(); + return; + } + + // send each incoming stanza through the handler chain + var i, newList; + // process handlers + newList = this.handlers; + this.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + if (hand.isMatch(elem) && + (this.authenticated || !hand.user)) { + if (hand.run(elem)) { + this.handlers.push(hand); + } + } else { + this.handlers.push(hand); + } + } + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var stanza = {} + if (this.authenticated) { + stanza = $pres({ + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + this.send(stanza); + this.ws.send(""); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (event) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + this.ws.onmessage=this._dataRecv.bind(this); + var strStanza = event.data; + if (!strStanza) { return; } + stanza = this._parseTree(strStanza) + this.xmlInput(stanza); + this.rawInput(Strophe.serialize(stanza)); + + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = stanza.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.getBareJidFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = Base64.encode(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); + + this.send($iq({ + type: "get", + to: this.domain, + id: "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + + _rebind_cb: function (jid, sid, event){ + this.connected = true; + this.ws.onmessage=this._dataRecv.bind(this); + var strStanza = event.data; + if (!strStanza) { return; } + stanza = this._parseTree(strStanza) + this.xmlInput(stanza); + this.rawInput(Strophe.serialize(stanza)); + var rebinds = stanza.getElementsByTagName("rebind"); + if (rebinds.length > 0){ + this.send($build('rebind',{ + xmlns:"p1:rebind" + }).c("jid", {}).t(jid) + .up() + .c("sid", {}).t(sid).tree()); + this._rebind_success_handler = this._addSysHandler( + this._rebind_success_cb.bind(this), null, + "rebind", null, null); + this._rebind_failure_handler = this._addSysHandler( + this._rebind_failure_cb.bind(this), null, + "failure", null, null); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-rebind-not-supported'); + } + + + }, + + _rebind_success_cb: function(elem){ + Strophe.info("Rebinding succeeded."); + // remove old handlers + this.authenticated=true; + this.connected=true; + this.deleteHandler(this._rebind_failure_handler); + this._rebind_failure_handler = null; + this._changeConnectStatus(Strophe.Status.ATTACHED, null); + return false; + }, + + _rebind_failure_cb: function(elem){ + Strophe.info("Rebinding failed."); + // delete unneeded handlers + if (this._rebind_success_handler) { + this.deleteHandler(this._rebind_success_handler); + this._rebind_success_handler = null; + } + this._changeConnectStatus(Strophe.Status.REBINDFAILED, null); + return false; + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = Base64.decode(Strophe.getText(elem)); + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + this.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + + this._quote(Strophe.getNodeFromJid(this.jid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + this._quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + _quote: function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }, + + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .up() + .c('password').t(this.pass); + + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + // We need the new stream_id + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._dataRecv.bind(this)); + // we must send an xmpp:restart now + this._send_initial_stream(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, + null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + if (resource) { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .c('resource', {}).t(resource).tree()); + } else { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .tree()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } else { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + // actually disconnect + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function () + { + var i, thand, since, newList; + + // remove timed handlers that have been scheduled for deletion + while (this.removeTimeds.length > 0) { + thand = this.removeTimeds.pop(); + i = this.timedHandlers.indexOf(thand); + if (i >= 0) { + this.timedHandlers.splice(i, 1); + } + } + + // add timed handlers scheduled for addition + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } + + // call ready timed handlers + var now = new Date().getTime(); + newList = []; + for (i = 0; i < this.timedHandlers.length; i++) { + thand = this.timedHandlers[i]; + if (this.authenticated || !thand.user) { + since = thand.lastCalled + thand.period; + if (since - now <= 0) { + if (thand.run()) { + newList.push(thand); + } + } else { + newList.push(thand); + } + } + } + this.timedHandlers = newList; + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +};/** + * P1PP library + * Author: Eric Cestari + * + * This library will open an XMPP socket using the best transport available. + * Once connection has been established, it will suscribe to PubSub nodes on the given service + * For each publication event, it will call the user provided `publish(id, item, delayed)` function. + * + */ + +/** + * P1PP. The main class + * + **/ +P1PP.prototype = { + /** + * @param {Object} params the configuration parameters. See Readme for usage + * @param {Boolean} fallback Used internally. if connection fails and timeout code is triggered it will set this to true. + */ + connect: function(fallback){ + var self = this, + params = this.params; + if(!!this.connection && this.connection.connected){ + return; + } + + this._check_rebind(); + var _connect = function(){ + self.retries ++; + if (self.retries > params.connectretry){ + self.console.log(params.connectretry + " connect retry exceeded"); + return; + } + self.timeout_id = setTimeout(function(){ + self.connect(true); + }, params.connect_timeout); + if(!fallback && params.ws_url && window.WebSocket){ + self.current_protocol = "WEBSOCKET"; + self.websocket(); + } else { + self.current_protocol = "BOSH"; + self.bosh(); + } + }; + // Setting up connection delay + if((this.retries == 0) && !fallback){ + setTimeout(_connect.bind(this), params.connect_delay); + } else { + _connect(); + } + }, + /** + * Closes connection to server + */ + disconnect: function(){ + this.closing=true; + window.clearTimeout(this.timeout_id); + this.rebind_delete(); + if(this.connection){ + this.connection.deleteTimedHandler(this.bosh_rebind_id); + this.connection.disconnect(); + } + }, + + /** + * BOSH connection code. + * Will attempt to re-attach is param is set and attached value stored on the client. + */ + bosh: function(){ + try{ + this.connection = new Strophe.Connection(this.params.bosh_url); + /* Reattach needs to be fixed on BOSH */ + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.rebind){ //there's a cookie + prev_connection = cookie.split(" "); + if(prev_connection[0] == "BOSH"){ //Only BOSH cookies accepted + this.connection.attach(prev_connection[1], + prev_connection[2], + prev_connection[3], + this.conn_callback.bind(this)) + } else { + this._transport_connect(); + } + } else { + this._transport_connect(); + } + } catch (e){ + this.connect(); + } + + }, + + /** + * WebSocket connection code + * Will attempt to use fast rebind is param is set and rebind data is availble on client. + * @private + */ + websocket: function(){ + try{ + this.connection = new Strophe.WebSocket(this.params.ws_url); + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.rebind){ + prev_connection = cookie.split(" "); + this.connection.rebind(prev_connection[1], + prev_connection[2], + this.conn_callback.bind(this)) + } else { + this._transport_connect(); + } + } catch (e){ + this.rebind_delete(); + this.connect(); + } + }, + + _transport_connect: function(){ + var jid = this.params.jid ? this.params.jid : this.params.domain; + var password = this.params.password ? this.params.password : "" + this.connection.connect(jid, + password, this.conn_callback.bind(this)); + }, + /** + * Checks if rebind can be safely called + * Only useful in case of non-anon connections + */ + _check_rebind: function(){ + if(!this.params.rebind){ + //rebind not active + return; + } + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.jid && this.params.jid !== ""){ + var prev = cookie.split(" "); + var jid = Strophe.getBareJidFromJid(this.params.jid); + var prev_jid = Strophe.getBareJidFromJid(cookie.split(" ")[1]); + if(jid != prev_jid){ + this.rebind_delete(); + } + } + }, + /** + * Main connection callback + * @param {Integer} status The StropheJS status of the connection. + * @private + */ + conn_callback: function(status){ + var that = this; + if(this.params.debug){ + this.connection.rawOutput = function(elem){ + that.console.log("out -> " + elem); + } + this.connection.rawInput = function(elem){ + that.console.log("in <- " + elem); + } + } + // Connection established + window.clearTimeout(this.timeout_id); + this.bosh_rebind_id = this.connection.addTimedHandler(2000, function(){ + if(that.current_protocol === "BOSH" && !that.closing){ + var c = ["BOSH", that.connection.jid, that.connection.sid, that.connection.rid].join(" "); + that.rebind_store(c); + } + return true; + }); + this.params.on_strophe_event(status, this.connection); + var login_required_cb = function(jid, pass){ + that.params.jid=jid; + that.params.password=pass; + that.connect(); + } + if (status === Strophe.Status.CONNECTED) { + this.params.on_connected(); + this.retries = 0; + if(this.params.rebind == true + && this.current_protocol == "WEBSOCKET"){ + this.connection.save(function(){ + var id = ["WEBSOCKET",that.connection.jid, that.connection.streamId].join(" ") + that.rebind_store(id); + }, function(){}); + } + this.subscribe(); + } + // Connection re-attached (or rebound) + else if (status === Strophe.Status.ATTACHED){ + // Forcing subscription. there is a problem with rebinding and anon subs in ejabberd + if(this.current_protocol == "WEBSOCKET"){ + this.subscribe(); + } + else{ + // For some reason, I have to wait the second HTTP POST to actually get the result. + // ejabberd says data is sent, the browser says no, it's not. + // One of them is lying. Or both. + // In the meantime, a short timeout will do the trick. + setTimeout(function(){ + nodes = that.params.nodes; + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + // In case of attach, we need to set up the handlers again. + if(that.params.num_old > 0){ + that.connection.pubsub.items(that.connection.jid, that.params.pubsub_domain, nodes[i], that.params.num_old, function(message){ + var items = message.getElementsByTagName("item") + for(var i = 0; i < items.length; i++){ + id = items[i].getAttribute("id"); + that.params.publish(id, items[i].firstChild); + } + }) + } + } + } + that.connection.addHandler(that.on_event.bind(that), + null,'message', null, null, that.params.pubsub_domain); + }, 100); + + } + + } + // Connection problem or reattach failed. Will attempt to reconnect after a random wait + else if (!this.closing + && (status === Strophe.Status.CONNFAIL + || status === Strophe.Status.DISCONNECTED)) { + this.connection.deleteTimedHandler(this.bosh_rebind_id); + this.rebind_delete(); + //login is required. Give user code a chance to fetch jid and password + if(!this.params.jid && this.params.login_required){ + this.params.on_login_required(login_required_cb) + } else { + var retry_time = Math.round(Math.random() * this.params.connect_timeout * (this.retries+1)); + this.timeout_id = setTimeout(function(){ + that.connect(); + }, retry_time); + } + } + else if (status === Strophe.Status.DISCONNECTED){ + this.connection.reset(); + this.closing = false; + this.params.on_disconnect(); + } + // WebSocket rebind failed. Removing user data and reconnecting + else if (status === Strophe.Status.REBINDFAILED){ + this.rebind_delete(); + this.connection = null; + this.connect(); + } + else if (status === Strophe.Status.AUTHFAIL){ + delete this.connection; + this.params.on_login_required(login_required_cb); + } + }, + /** + * Subscribe to PubSub nodes + * If num_old is set, fetch older nodes. + * Warning: The last_published item is delivered twice, if both send_last_item configured on node and num_old >= 1 + * @private + */ + subscribe: function(nodes){ + if(nodes === undefined){ + nodes = this.params.nodes; + } + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + if(this.params.num_old > 0){ + var that = this; + this.connection.pubsub.items(this.connection.jid, this.params.pubsub_domain, nodes[i], this.params.num_old, function(message){ + var items = message.getElementsByTagName("item") + for(var i = 0; i < items.length; i++){ + id = items[i].getAttribute("id"); + that.params.publish(id, items[i].firstChild); + } + }) + } + this.connection.pubsub.subscribe(this.connection.jid, + this.params.pubsub_domain, + nodes[i],[], + function(){}); + } + } + this.connection.addHandler(this.on_event.bind(this), + null,'message', null, null, this.params.pubsub_domain); + }, + + unsubscribe: function(nodes){ + if(nodes === undefined){ + nodes = this.params.nodes; + } + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + this.connection.pubsub.unsubscribe(this.connection.jid, + this.params.pubsub_domain, + nodes[i], + function(){}); + } + } + }, + + _extract_error_code: function(stanza) { + var error = stanza.getElementsByTagNameNS("http://jabber.org/protocol/pubsub#errors", "*"); + + if (!error.length) + error = stanza.getElementsByTagNameNS("urn:ietf:params:xml:ns:xmpp-stanzas", "*"); + + return error.length ? error[0].localName : null; + }, + + _publish: function(node, id, value, callback) { + var that = this; + + if (id == null) + id = this.connection.getUniqueId("publish"); + + this.connection.pubsub.publish(this.connection.jid, this.params.pubsub_domain, + node, [{id: id, value: [value]}], function(stanza) { + that._done_publish(stanza, callback); + }); + return id; + }, + + _done_publish: function(stanza, callback) { + var items = stanza.getElementsByTagName("item"); + var id = items.length ? items[0].getAttribute("id") : null; + + if (stanza.getAttribute("type") == "result") + callback(id, "ok"); + else + callback(id, this._extract_error_code(stanza) || "error") + }, + + _deleteNode: function(node, callback) { + var that = this; + + this.connection.pubsub.deleteNode(this.connection.jid, this.params.pubsub_domain, + node, function(stanza) { + that._done_delete(stanza, callback); + }); + }, + + _done_delete: function(stanza, callback) { + if (stanza.getAttribute("type") == "result") + callback("ok"); + else + callback(this._extract_error_code(stanza) || "error") + }, + + /** + * pubsub message handling. + * Triggered everytime there is an event coming from the server (publication or retraction of items) + * @param {DOMElement} msg the message from the server + * @private + **/ + on_event: function(msg){ + var retracts = msg.getElementsByTagName("retract"); + var length =retracts.length + for(var i = 0; i < length; i++){ + this.params.retract(retracts[i].getAttribute("id")); + } + var delay_time = undefined; + var delay = msg.getElementsByTagName("delay"); + if(delay.length){ + delay_time = delay[0].getAttribute("stamp"); + } + var items = msg.getElementsByTagName("items"); + length = items.length; + var node_name, node_items, ilength; + for(var i = 0; i < length; i++){ + node_name = items[i].getAttribute("node"); + node_items = items[i].getElementsByTagName("item"); + ilength = node_items.length; + for(var i = 0; i < ilength; i++){ + this.params.publish(node_items[i].getAttribute("id"), + node_items[i].firstChild, + node_name, + delay_time); + } + } + return true; + }, + + /** + * Cookie management code, taken from jquery.cookie.js + * @private + */ + cookie: function(key, value){ + if (typeof value != 'undefined'){ + options = this.params.cookie_opts; + if (value === null) { + value = ''; + options.expires = -1; + } + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + } + // CAUTION: Needed to parenthesize options.path and options.domain + // in the following expressions, otherwise they evaluate to undefined + // in the packed version for some reason... + var path = options.path ? '; path=' + (options.path) : ''; + var domain = options.domain ? '; domain=' + (options.domain) : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [key, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var trim = function( text ) { + if ( typeof String.trim === "function" ) { + return ( text || "" ).trim(); + } + return (text || "").replace( /^\s\s*/, "" ).replace( /\s\s*$/, "" ); + } + var cookie = trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, key.length + 1) == (key + '=')) { + cookieValue = decodeURIComponent(cookie.substring(key.length + 1)); + break; + } + } + } + return cookieValue; + } + + }, + /** + * stores connection information in sessionStorage if available or cookie + * @private + */ + rebind_store: function(value){ + var key = this._build_key() + if(window.sessionStorage){ + sessionStorage[key]=value; + } else { + this.cookie(key, value); + } + }, + /** + * deletes connection information in sessionStorage if available or cookie + * @private + */ + rebind_delete: function(){ + var key = this._build_key() + if(window.sessionStorage){ + sessionStorage.removeItem(key) + } else { + this.cookie(key, null); + } + }, + /** + * fetchs connection information in sessionStorage if available or cookie + * @private + */ + rebind_fetch: function(){ + var key = this._build_key(); + if(window.sessionStorage){ + return sessionStorage[key]; + } else { + return this.cookie(key); + } + }, + /** + * Build key for protocol. + * BOSH connections can be scoped, for a different set of nodes + * Not necessary for WS as the client subscribes on reattach. + */ + _build_key: function(){ + var key = P1PP.COOKIE+ "_" + this.current_protocol; + if(this.current_protocol === "BOSH"){ + key = key + "_" + this.scope; + } + return key; + } +}; + diff --git a/p1pp.js.min b/p1pp.js.min new file mode 100644 index 0000000..6d28e2a --- /dev/null +++ b/p1pp.js.min @@ -0,0 +1,163 @@ +var WEB_SOCKET_DEBUG=!1,WEB_SOCKET_DISABLE_AUTO_INITIALIZATION=!0,P1PP=function(a){this.timeout_id=this.params=this.connection=this.password=this.jid=null;this.retries=0;this.closing=!1;this.defaults={flash_location:"WebSocketMain.swf",domain:"p1pp.net",ws_url:"ws://p1pp.net:5280/xmpp",bosh_url:"ws://p1pp.net:5280/http-bind",connect_timeout:15E3,connect_delay:0,connect_retry:10,rebind:!0,debug:!1,num_old:0,on_strophe_event:function(){},on_login_required:null,publish:function(){},retract:function(){}, +on_disconnected:function(){},on_connected:function(){},cookie_opts:{},nodes:[]};this.params=function(a,c){for(var d in c)c.hasOwnProperty(d)&&(a[d]=c[d]);return a}(this.defaults,a);this.params.pubsub_domain||(this.params.pubsub_domain="pubsub."+this.params.domain);a=this.params.nodes;0g){g++;setTimeout(arguments.callee,10);return}a.removeChild(b);c=null;e()})()}else e()}function e(){var a=y.length;if(0l.wk))i(c,!0),d&&(e.success=!0,e.ref=f(c),d(e));else if(y[b].expressInstall&&g()){e={};e.data=y[b].expressInstall;e.width=o.getAttribute("width")||"0";e.height=o.getAttribute("height")||"0";o.getAttribute("class")&&(e.styleclass=o.getAttribute("class"));o.getAttribute("align")&&(e.align=o.getAttribute("align"));for(var t={},o=o.getElementsByTagName("param"),h=o.length,j=0;jl.wk)}function k(a,b,c,g){G=!0;J=g||null;L={success:!1,id:c};var d=s(c);if(d){"OBJECT"==d.nodeName?(D=r(d),H=null):(D=d,H=c);a.id= +j;if(typeof a.width==q||!/%$/.test(a.width)&&310>parseInt(a.width,10))a.width="310";if(typeof a.height==q||!/%$/.test(a.height)&&137>parseInt(a.height,10))a.height="137";m.title=m.title.slice(0,47)+" - Flash Player Installation";g=l.ie&&l.win?"ActiveX":"PlugIn";g="MMredirectURL="+w.location.toString().replace(/&/g,"%26")+"&MMplayerType="+g+"&MMdoctitle="+m.title;b.flashvars=typeof b.flashvars!=q?b.flashvars+("&"+g):g;l.ie&&l.win&&4!=d.readyState&&(g=m.createElement("div"),c+="SWFObjectNew",g.setAttribute("id", +c),d.parentNode.insertBefore(g,d),d.style.display="none",function(){4==d.readyState?d.parentNode.removeChild(d):setTimeout(arguments.callee,10)}());u(a,b,c)}}function n(a){if(l.ie&&l.win&&4!=a.readyState){var b=m.createElement("div");a.parentNode.insertBefore(b,a);b.parentNode.replaceChild(r(a),b);a.style.display="none";(function(){4==a.readyState?a.parentNode.removeChild(a):setTimeout(arguments.callee,10)})()}else a.parentNode.replaceChild(r(a),a)}function r(a){var b=m.createElement("div");if(l.win&& +l.ie)b.innerHTML=a.innerHTML;else if(a=a.getElementsByTagName(v)[0])if(a=a.childNodes)for(var c=a.length,g=0;gl.wk)return g;if(d)if(typeof a.id==q&&(a.id=c),l.ie&&l.win){var f="",e;for(e in a)a[e]!=Object.prototype[e]&&("data"==e.toLowerCase()?b.movie=a[e]:"styleclass"==e.toLowerCase()?f+=' class="'+a[e]+'"':"classid"!=e.toLowerCase()&&(f+=" "+ +e+'="'+a[e]+'"'));e="";for(var o in b)b[o]!=Object.prototype[o]&&(e+='');d.outerHTML='"+e+"";I[I.length]=a.id;g=s(a.id)}else{o=m.createElement(v);o.setAttribute("type",B);for(var k in a)a[k]!=Object.prototype[k]&&("styleclass"==k.toLowerCase()?o.setAttribute("class",a[k]):"classid"!=k.toLowerCase()&&o.setAttribute(k,a[k]));for(f in b)b[f]!=Object.prototype[f]&&"movie"!=f.toLowerCase()&& +(a=o,e=f,k=b[f],c=m.createElement("param"),c.setAttribute("name",e),c.setAttribute("value",k),a.appendChild(c));d.parentNode.replaceChild(o,d);g=o}return g}function t(a){var b=s(a);b&&"OBJECT"==b.nodeName&&(l.ie&&l.win?(b.style.display="none",function(){if(4==b.readyState){var c=s(a);if(c){for(var g in c)"function"==typeof c[g]&&(c[g]=null);c.parentNode.removeChild(c)}}else setTimeout(arguments.callee,10)}()):b.parentNode.removeChild(b))}function s(a){var b=null;try{b=m.getElementById(a)}catch(c){}return b} +function o(a,b,c){a.attachEvent(b,c);C[C.length]=[a,b,c]}function F(a){var b=l.pv,a=a.split(".");a[0]=parseInt(a[0],10);a[1]=parseInt(a[1],10)||0;a[2]=parseInt(a[2],10)||0;return b[0]>a[0]||b[0]==a[0]&&b[1]>a[1]||b[0]==a[0]&&b[1]==a[1]&&b[2]>=a[2]?!0:!1}function h(a,b,c,g){if(!l.ie||!l.mac){var d=m.getElementsByTagName("head")[0];if(d){c=c&&"string"==typeof c?c:"screen";g&&(K=x=null);if(!x||K!=c)g=m.createElement("style"),g.setAttribute("type","text/css"),g.setAttribute("media",c),x=d.appendChild(g), +l.ie&&l.win&&typeof m.styleSheets!=q&&0\.;]/.exec(a)&&typeof encodeURIComponent!=q?encodeURIComponent(a):a}var q="undefined",v="object",B="application/x-shockwave-flash", +j="SWFObjectExprInst",w=window,m=document,z=navigator,O=!1,E=[function(){O?d():e()}],y=[],I=[],C=[],D,H,J,L,A=!1,G=!1,x,K,M=!0,l=function(){var a=typeof m.getElementById!=q&&typeof m.getElementsByTagName!=q&&typeof m.createElement!=q,b=z.userAgent.toLowerCase(),c=z.platform.toLowerCase(),g=c?/win/.test(c):/win/.test(b),c=c?/mac/.test(c):/mac/.test(b),b=/webkit/.test(b)?parseFloat(b.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):!1,d=!+"\v1",e=[0,0,0],f=null;if(typeof z.plugins!=q&&typeof z.plugins["Shockwave Flash"]== +v){if((f=z.plugins["Shockwave Flash"].description)&&!(typeof z.mimeTypes!=q&&z.mimeTypes[B]&&!z.mimeTypes[B].enabledPlugin))O=!0,d=!1,f=f.replace(/^.*\s+(\S+\s+\S+$)/,"$1"),e[0]=parseInt(f.replace(/^(.*)\..*$/,"$1"),10),e[1]=parseInt(f.replace(/^.*\.(.*)\s.*$/,"$1"),10),e[2]=/[a-zA-Z]/.test(f)?parseInt(f.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}else if(typeof w.ActiveXObject!=q)try{var o=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");if(o&&(f=o.GetVariable("$version")))d=!0,f=f.split(" ")[1].split(","), +e=[parseInt(f[0],10),parseInt(f[1],10),parseInt(f[2],10)]}catch(k){}return{w3:a,pv:e,wk:b,ie:d,win:g,mac:c}}();(function(){l.w3&&((typeof m.readyState!=q&&"complete"==m.readyState||typeof m.readyState==q&&(m.getElementsByTagName("body")[0]||m.body))&&a(),A||(typeof m.addEventListener!=q&&m.addEventListener("DOMContentLoaded",a,!1),l.ie&&l.win&&(m.attachEvent("onreadystatechange",function(){"complete"==m.readyState&&(m.detachEvent("onreadystatechange",arguments.callee),a())}),w==top&&function(){if(!A){try{m.documentElement.doScroll("left")}catch(b){setTimeout(arguments.callee, +0);return}a()}}()),l.wk&&function(){A||(/loaded|complete/.test(m.readyState)?a():setTimeout(arguments.callee,0))}(),c(a)))})();(function(){l.ie&&l.win&&window.attachEvent("onunload",function(){for(var a=C.length,b=0;bl.wk)&&a&&c&&d&&f&&e?(i(c,!1),b(function(){d+="";f+="";var b={};if(n&&typeof n===v)for(var l in n)b[l]=n[l];b.data=a;b.width=d;b.height=f;l={};if(h&&typeof h===v)for(var r in h)l[r]=h[r];if(t&&typeof t===v)for(var m in t)l.flashvars=typeof l.flashvars!=q?l.flashvars+("&"+m+"="+t[m]):m+"="+t[m];if(F(e))r=u(b,l,c),b.id== +c&&i(c,!0),s.success=!0,s.ref=r;else{if(o&&g()){b.data=o;k(b,l,c,j);return}i(c,!0)}j&&j(s)})):j&&j(s)},switchOffAutoHideShow:function(){M=!1},ua:l,getFlashPlayerVersion:function(){return{major:l.pv[0],minor:l.pv[1],release:l.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(a,b,c){if(l.w3)return u(a,b,c)},showExpressInstall:function(a,b,c,d){l.w3&&g()&&k(a,b,c,d)},removeSWF:function(a){l.w3&&t(a)},createCSS:function(a,b,c,g){l.w3&&h(a,b,c,g)},addDomLoadEvent:b,addLoadEvent:c,getQueryParamValue:function(a){var b= +m.location.search||m.location.hash;if(b){/\?/.test(b)&&(b=b.split("?")[1]);if(null==a)return N(b);for(var b=b.split("&"),c=0;cswfobject.getFlashPlayerVersion().major?a.error("Flash Player >= 9.0.0 is required."):("file:"==location.protocol&&a.error("WARNING: web-socket-js doesn't work in file:///... URL unless you set Flash Security Settings properly. Open the page via Web server i.e. http://..."), +window.WebSocket=function(a,c,d,e,f){var g=this;g.__id=WebSocket.__nextId++;WebSocket.__instances[g.__id]=g;g.readyState=WebSocket.CONNECTING;g.bufferedAmount=0;g.__events={};c?"string"==typeof c&&(c=[c]):c=[];g.__createTask=setTimeout(function(){WebSocket.__addTask(function(){g.__createTask=null;WebSocket.__flash.create(g.__id,a,c,d||null,e||0,f||null)})},0)},WebSocket.prototype.send=function(a){if(this.readyState==WebSocket.CONNECTING)throw"INVALID_STATE_ERR: Web Socket connection has not been established"; +a=WebSocket.__flash.send(this.__id,encodeURIComponent(a));if(0>a)return!0;this.bufferedAmount+=a;return!1},WebSocket.prototype.close=function(){this.__createTask?(clearTimeout(this.__createTask),this.__createTask=null,this.readyState=WebSocket.CLOSED):this.readyState==WebSocket.CLOSED||this.readyState==WebSocket.CLOSING||(this.readyState=WebSocket.CLOSING,WebSocket.__flash.close(this.__id))},WebSocket.prototype.addEventListener=function(a,c){a in this.__events||(this.__events[a]=[]);this.__events[a].push(c)}, +WebSocket.prototype.removeEventListener=function(a,c){if(a in this.__events)for(var d=this.__events[a],e=d.length-1;0<=e;--e)if(d[e]===c){d.splice(e,1);break}},WebSocket.prototype.dispatchEvent=function(a){for(var c=this.__events[a.type]||[],d=0;d>2,c=(c&3)<<4|d>>4,g=(d&15)<<2|e>>6,k=e&63,isNaN(d)?g=k=64:isNaN(e)&&(k=64),b=b+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(c)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(g)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(k); +while(n>4,d=(d&15)<< +4|f>>2,e=(f&3)<<6|g,b+=String.fromCharCode(c),64!=f&&(b+=String.fromCharCode(d)),64!=g&&(b+=String.fromCharCode(e));while(k>16)+(b>>16)+(c>>16)<<16|c&65535},b=function(a){for(var b=[],c=0;c<8*a.length;c+=8)b[c>>5]|=(a.charCodeAt(c/8)&255)<>5]>>>c%32&255);return b},d=function(a){for(var b="",c=0;c<4*a.length;c++)b+= +"0123456789abcdef".charAt(a[c>>2]>>8*(c%4)+4&15)+"0123456789abcdef".charAt(a[c>>2]>>8*(c%4)&15);return b},e=function(a){for(var b="",c,g,d=0;d<4*a.length;d+=3){c=(a[d>>2]>>8*(d%4)&255)<<16|(a[d+1>>2]>>8*((d+1)%4)&255)<<8|a[d+2>>2]>>8*((d+2)%4)&255;for(g=0;4>g;g++)b=8*d+6*g>32*a.length?b+"":b+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c>>6*(3-g)&63)}return b},f=function(b,c,g,d,f,e){b=a(a(c,b),a(d,e));return a(b<>>32-f,g)},g=function(a,b,c,g,d,e,k){return f(b&c| +~b&g,a,b,d,e,k)},k=function(a,b,c,g,d,e,k){return f(b&g|c&~g,a,b,d,e,k)},n=function(a,b,c,g,d,e,k){return f(c^(b|~g),a,b,d,e,k)},r=function(b,c){b[c>>5]|=128<>>9<<4)+14]=c;for(var d=1732584193,e=-271733879,h=-1732584194,i=271733878,r,u,v,B,j=0;jf;f++)g[f]=d[f]^909522486,e[f]=d[f]^1549556828;d=r(g.concat(b(c)),512+8*c.length);return r(e.concat(d),640)};return{hexdigest:function(a){return d(r(b(a),8*a.length))},b64digest:function(a){return e(r(b(a),8*a.length))},hash:function(a){return c(r(b(a),8*a.length))},hmac_hexdigest:function(a,b){return d(u(a,b))},hmac_b64digest:function(a,b){return e(u(a,b))},hmac_hash:function(a, +b){return c(u(a,b))},test:function(){return"900150983cd24fb0d6963f7d28e17f72"===MD5.hexdigest("abc")}}}();Function.prototype.bind||(Function.prototype.bind=function(a){var b=this;return function(){return b.apply(a,arguments)}});Function.prototype.prependArg||(Function.prototype.prependArg=function(a){var b=this;return function(){for(var c=[a],d=0;dd?Math.ceil(d):Math.floor(d);for(0>d&&(d+=c);d/g,">")},xmlTextNode:function(a){a=f.xmlescape(a);f._xmlGenerator||(f._xmlGenerator=f._makeGenerator());return f._xmlGenerator.createTextNode(a)},getText:function(a){if(!a)return null;var b="";0===a.childNodes.length&&a.nodeType==f.ElementType.TEXT&&(b+=a.nodeValue); +for(var c=0;c/g,"\\3e").replace(/@/g,"\\40")},unescapeNode:function(a){return a.replace(/\\20/g," ").replace(/\\22/g,'"').replace(/\\26/g,"&").replace(/\\27/g,"'").replace(/\\2f/g,"/").replace(/\\3a/g,":").replace(/\\3c/g,"<").replace(/\\3e/g,">").replace(/\\40/g,"@").replace(/\\5c/g,"\\")},getNodeFromJid:function(a){return 0> +a.indexOf("@")?null:a.split("@")[0]},getDomainFromJid:function(a){a=f.getBareJidFromJid(a);if(0>a.indexOf("@"))return a;a=a.split("@");a.splice(0,1);return a.join("@")},getResourceFromJid:function(a){a=a.split("/");if(2>a.length)return null;a.splice(0,1);return a.join("/")},getBareJidFromJid:function(a){return a?a.split("/")[0]:null},log:function(){},debug:function(a){this.log(this.LogLevel.DEBUG,a)},info:function(a){this.log(this.LogLevel.INFO,a)},warn:function(a){this.log(this.LogLevel.WARN,a)}, +error:function(a){this.log(this.LogLevel.ERROR,a)},fatal:function(a){this.log(this.LogLevel.FATAL,a)},serialize:function(a){var b;if(!a)return null;"function"===typeof a.tree&&(a=a.tree());var c=a.nodeName,d,e;a.getAttribute("_realname")&&(c=a.getAttribute("_realname"));b="<"+c;for(d=0;d";for(d=0;d"}else b+="/>";return b},_requestId:0,_connectionPlugins:{},addConnectionPlugin:function(a,b){f._connectionPlugins[a]=b}};f.Builder=function(a,b){if("presence"==a||"message"==a||"iq"==a)b&&!b.xmlns?b.xmlns=f.NS.CLIENT:b||(b={xmlns:f.NS.CLIENT});this.node=this.nodeTree=f.xmlElement(a,b)};f.Builder.prototype={tree:function(){return this.nodeTree}, +toString:function(){return f.serialize(this.nodeTree)},up:function(){this.node=this.node.parentNode;return this},attrs:function(a){for(var b in a)a.hasOwnProperty(b)&&this.node.setAttribute(b,a[b]);return this},c:function(a,b){var c=f.xmlElement(a,b);this.node.appendChild(c);this.node=c;return this},cnode:function(a){this.node.appendChild(a);this.node=a;return this},t:function(a){this.node.appendChild(f.xmlTextNode(a));return this}};f.Handler=function(a,b,c,d,e,t,s){this.handler=a;this.ns=b;this.name= +c;this.type=d;this.id=e;this.options=s||{matchbare:!1};this.options.matchBare||(this.options.matchBare=!1);this.from=this.options.matchBare?t?f.getBareJidFromJid(t):null:t;this.user=!0};f.Handler.prototype={isMatch:function(a){var b,c=null,c=this.options.matchBare?f.getBareJidFromJid(a.getAttribute("from")):a.getAttribute("from");b=!1;if(this.ns){var d=this;f.forEachChild(a,null,function(a){a.getAttribute("xmlns")==d.ns&&(b=!0)});b=b||a.getAttribute("xmlns")==this.ns}else b=!0;return b&&(!this.name|| +f.isTagEqual(a,this.name))&&(!this.type||a.getAttribute("type")==this.type)&&(!this.id||a.getAttribute("id")==this.id)&&(!this.from||c==this.from)?!0:!1},run:function(a){var b=null;try{b=this.handler(a)}catch(c){throw c.sourceURL?f.fatal("error: "+this.handler+" "+c.sourceURL+":"+c.line+" - "+c.name+": "+c.message):c.fileName?("undefined"!=typeof console&&(console.trace(),console.error(this.handler," - error - ",c,c.message)),f.fatal("error: "+this.handler+" "+c.fileName+":"+c.lineNumber+" - "+c.name+ +": "+c.message)):f.fatal("error: "+this.handler),c;}return b},toString:function(){return"{Handler: "+this.handler+"("+this.name+","+this.id+","+this.ns+")}"}};f.TimedHandler=function(a,b){this.period=a;this.handler=b;this.lastCalled=(new Date).getTime();this.user=!0};f.TimedHandler.prototype={run:function(){this.lastCalled=(new Date).getTime();return this.handler()},reset:function(){this.lastCalled=(new Date).getTime()},toString:function(){return"{TimedHandler: "+this.handler+"("+this.period+")}"}}; +f.Request=function(a,b,c,d){this.id=++f._requestId;this.xmlData=a;this.data=f.serialize(a);this.func=this.origFunc=b;this.rid=c;this.date=NaN;this.sends=d||0;this.abort=!1;this.dead=null;this.age=function(){return!this.date?0:(new Date-this.date)/1E3};this.timeDead=function(){return!this.dead?0:(new Date-this.dead)/1E3};this.xhr=this._newXHR()};f.Request.prototype={getResponse:function(){var a=null;if(this.xhr.responseXML&&this.xhr.responseXML.documentElement){if(a=this.xhr.responseXML.documentElement, +"parsererror"==a.tagName)throw f.error("invalid response received"),f.error("responseText: "+this.xhr.responseText),f.error("responseXML: "+f.serialize(this.xhr.responseXML)),"parsererror";}else this.xhr.responseText&&(f.error("invalid response received"),f.error("responseText: "+this.xhr.responseText),f.error("responseXML: "+f.serialize(this.xhr.responseXML)));return a},_newXHR:function(){var a=null;window.XMLHttpRequest?(a=new XMLHttpRequest,a.overrideMimeType&&a.overrideMimeType("text/xml")):window.ActiveXObject&& +(a=new ActiveXObject("Microsoft.XMLHTTP"));a.onreadystatechange=this.func.prependArg(this);return a}};a&&a(f,b,c,d,e)})(function(a,b,c,d,e){window.Strophe=a;window.$build=b;window.$msg=c;window.$iq=d;window.$pres=e}); +Strophe.Connection=function(a){this.service=a;this.jid="";this.rid=Math.floor(4294967295*Math.random());this.streamId=this.sid=null;this.do_bind=this.do_session=!1;this.timedHandlers=[];this.handlers=[];this.removeTimeds=[];this.removeHandlers=[];this.addTimeds=[];this.addHandlers=[];this._disconnectTimeout=this._idleTimeout=null;this.connected=this.disconnecting=this.authenticated=!1;this.errors=0;this.paused=!1;this.hold=1;this.wait=60;this.window=5;this._data=[];this._requests=[];this._uniqueId= +Math.round(1E4*Math.random());this._sasl_challenge_handler=this._sasl_failure_handler=this._sasl_success_handler=null;this._idleTimeout=setTimeout(this._onIdle.bind(this),100);for(var b in Strophe._connectionPlugins)Strophe._connectionPlugins.hasOwnProperty(b)&&(a=function(){},a.prototype=Strophe._connectionPlugins[b],this[b]=new a,this[b].init(this))}; +Strophe.Connection.prototype={reset:function(){this.rid=Math.floor(4294967295*Math.random());this.streamId=this.sid=null;this.do_bind=this.do_session=!1;this.timedHandlers=[];this.handlers=[];this.removeTimeds=[];this.removeHandlers=[];this.addTimeds=[];this.addHandlers=[];this.connected=this.disconnecting=this.authenticated=!1;this.errors=0;this._requests=[];this._uniqueId=Math.round(1E4*Math.random())},pause:function(){this.paused=!0},resume:function(){this.paused=!1},getUniqueId:function(a){return"string"== +typeof a||"number"==typeof a?++this._uniqueId+":"+a:++this._uniqueId+""},connect:function(a,b,c,d,e){this.jid=a;this.pass=b;this.connect_callback=c;this.authenticated=this.connected=this.disconnecting=!1;this.errors=0;this.wait=d||this.wait;this.hold=e||this.hold;this.domain=Strophe.getDomainFromJid(this.jid);a=this._buildBody().attrs({to:this.domain,"xml:lang":"en",wait:this.wait,hold:this.hold,content:"text/xml; charset=utf-8",ver:"1.6","xmpp:version":"1.0","xmlns:xmpp":Strophe.NS.BOSH});this._changeConnectStatus(Strophe.Status.CONNECTING, +null);this._requests.push(new Strophe.Request(a.tree(),this._onRequestStateChange.bind(this).prependArg(this._connect_cb.bind(this)),a.tree().rid));this._throttledRequestHandler()},attach:function(a,b,c,d,e,f,g){this.jid=a;this.sid=b;this.rid=c;this.connect_callback=d;this.domain=Strophe.getDomainFromJid(this.jid);this.connected=this.authenticated=!0;this.wait=e||this.wait;this.hold=f||this.hold;this.window=g||this.window;this._changeConnectStatus(Strophe.Status.ATTACHED,null)},xmlInput:function(){}, +xmlOutput:function(){},rawInput:function(){},rawOutput:function(){},send:function(a){if(null!==a){if("function"===typeof a.sort)for(var b=0;bMath.floor(Strophe.TIMEOUT*this.wait),f=null!==b.dead&&b.timeDead()>Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait),c=4==b.xhr.readyState&&(1>c||500<=c);if(e||f||c)f&&Strophe.error("Request "+ +this._requests[a].id+" timed out (secondary), restarting"),b.abort=!0,b.xhr.abort(),b.xhr.onreadystatechange=function(){},this._requests[a]=new Strophe.Request(b.xmlData,b.origFunc,b.rid,b.sends),b=this._requests[a];if(0===b.xhr.readyState){Strophe.debug("request id "+b.id+"."+b.sends+" posting");b.date=new Date;try{b.xhr.open("POST",this.service,!0)}catch(g){Strophe.error("XHR open failed.");this.connected||this._changeConnectStatus(Strophe.Status.CONNFAIL,"bad-service");this.disconnect();return}a= +function(){b.xhr.send(b.data)};1c||5Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait))&&this._restartRequest(0),Strophe.debug("request id "+b.id+"."+b.sends+" got 200"),a(b),this.errors=0;else if(Strophe.error("request id "+b.id+"."+b.sends+" error "+c+" happened"),0===c||400<=c&&600>c||12E3<=c)this._hitError(c),400<=c&&500>c&&(this._changeConnectStatus(Strophe.Status.DISCONNECTING, +null),this._doDisconnect());0c||5=c-e?b.run()&&d.push(b):d.push(b);this.timedHandlers=d;this.authenticated&&0===this._requests.length&&0===this._data.length&&!this.disconnecting&&(Strophe.info("no requests during idle cycle, sending blank request"),this._data.push(null));if(2>this._requests.length&&0Math.floor(Strophe.SECONDARY_TIMEOUT*this.wait)&&this._throttledRequestHandler(),a>Math.floor(Strophe.TIMEOUT*this.wait)&&(Strophe.warn("Request "+ +this._requests[0].id+" timed out, over "+Math.floor(Strophe.TIMEOUT*this.wait)+" seconds since last activity"),this._throttledRequestHandler()));clearTimeout(this._idleTimeout);this._idleTimeout=setTimeout(this._onIdle.bind(this),100)}}; +Strophe.addConnectionPlugin("pubsub",{_connection:null,init:function(a){this._connection=a;Strophe.addNamespace("PUBSUB","http://jabber.org/protocol/pubsub");Strophe.addNamespace("PUBSUB_SUBSCRIBE_OPTIONS",Strophe.NS.PUBSUB+"#subscribe_options");Strophe.addNamespace("PUBSUB_ERRORS",Strophe.NS.PUBSUB+"#errors");Strophe.addNamespace("PUBSUB_EVENT",Strophe.NS.PUBSUB+"#event");Strophe.addNamespace("PUBSUB_OWNER",Strophe.NS.PUBSUB+"#owner");Strophe.addNamespace("PUBSUB_AUTO_CREATE",Strophe.NS.PUBSUB+"#auto-create"); +Strophe.addNamespace("PUBSUB_PUBLISH_OPTIONS",Strophe.NS.PUBSUB+"#publish-options");Strophe.addNamespace("PUBSUB_NODE_CONFIG",Strophe.NS.PUBSUB+"#node_config");Strophe.addNamespace("PUBSUB_CREATE_AND_CONFIGURE",Strophe.NS.PUBSUB+"#create-and-configure");Strophe.addNamespace("PUBSUB_SUBSCRIBE_AUTHORIZATION",Strophe.NS.PUBSUB+"#subscribe_authorization");Strophe.addNamespace("PUBSUB_GET_PENDING",Strophe.NS.PUBSUB+"#get-pending");Strophe.addNamespace("PUBSUB_MANAGE_SUBSCRIPTIONS",Strophe.NS.PUBSUB+"#manage-subscriptions"); +Strophe.addNamespace("PUBSUB_META_DATA",Strophe.NS.PUBSUB+"#meta-data")},createNode:function(a,b,c,d,e){var f=this._connection.getUniqueId("pubsubcreatenode"),a=$iq({from:a,to:b,type:"set",id:f}),b=Strophe.xmlElement("configure",[]),g=Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]),k=Strophe.xmlElement("field",[["var","FORM_TYPE"],["type","hidden"]]),n=Strophe.xmlElement("value",[]),r=Strophe.xmlTextNode(Strophe.NS.PUBSUB+"#node_config");n.appendChild(r);k.appendChild(n);g.appendChild(k);for(var u in d)g.appendChild(d[u]); +d.length&&0!=d.length&&b.appendChild(g);a.c("pubsub",{xmlns:Strophe.NS.PUBSUB}).c("create",{node:c}).up().cnode(b);this._connection.addHandler(e,null,"iq",null,f,null);this._connection.send(a.tree());return f},subscribe:function(a,b,c,d,e){var f=this._connection.getUniqueId("subscribenode"),g=Strophe.xmlElement("options",[]),k=Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]),n=Strophe.xmlElement("field",[["var","FORM_TYPE"],["type","hidden"]]),r=Strophe.xmlElement("value",[]),u=Strophe.xmlTextNode(Strophe.NS.PUBSUB_SUBSCRIBE_OPTIONS); +r.appendChild(u);n.appendChild(r);k.appendChild(n);b=$iq({from:a,to:b,type:"set",id:f});if(d&&d.length&&0!==d.length){for(n=0;n';this.rawOutput(a);this.ws.send(a)},_get_stream_id:function(a,b){elem=b.data;this.rawInput(elem);b.data.match(/id=[\'\"]([^\'\"]+)[\'\"]/)&&(this.streamId=RegExp.$1);this.ws.onmessage=a},_parseTree:function(a){try{return void 0==this._xml_parse&&(window.DOMParser?(this._xml_parser= +new DOMParser,this._xml_parse=function(a){return this._xml_parser.parseFromString(""+a+"","text/xml").documentElement.firstChild}):this._xml_parse=function(a){var b=new ActiveXObject("MSXML2.DomDocument");b.async="false";b.loadXML(""+a+"");return b.documentElement.firstChild}),this._xml_parse(a)}catch(b){Strophe.error("Error : "+b.message)}return null},_dataRecv:function(a){var b;try{b=this._parseTree(a.data)}catch(c){if("parsererror"!= +c)throw c;this.disconnect("strophe-parsererror")}if(null!==b){this.xmlInput(b);this.rawInput(a.data);for(var d;0")},_connect_cb:function(a){Strophe.info("_connect_cb was called");this.connected=!0;this.ws.onmessage=this._dataRecv.bind(this);if(a=a.data){stanza=this._parseTree(a);this.xmlInput(stanza);this.rawInput(Strophe.serialize(stanza));var b=a=!1,c=!1,d=stanza.getElementsByTagName("mechanism"),e,f;if(0=c-e?b.run()&&d.push(b):d.push(b);this.timedHandlers=d;clearTimeout(this._idleTimeout);this._idleTimeout=setTimeout(this._onIdle.bind(this),100)}}; +P1PP.prototype={connect:function(a){var b=this,c=this.params;if(!this.connection||!this.connection.connected){this._check_rebind();var d=function(){b.retries++;b.retries>c.connectretry?b.console.log(c.connectretry+" connect retry exceeded"):(b.timeout_id=setTimeout(function(){b.connect(!0)},c.connect_timeout),!a&&c.ws_url&&window.WebSocket?(b.current_protocol="WEBSOCKET",b.websocket()):(b.current_protocol="BOSH",b.bosh()))};0==this.retries&&!a?setTimeout(d.bind(this),c.connect_delay):d()}},disconnect:function(){this.closing= +!0;window.clearTimeout(this.timeout_id);this.rebind_delete();this.connection&&(this.connection.deleteTimedHandler(this.bosh_rebind_id),this.connection.disconnect())},bosh:function(){try{this.connection=new Strophe.Connection(this.params.bosh_url);var a=this.rebind_fetch();a&&this.params.rebind?(prev_connection=a.split(" "),"BOSH"==prev_connection[0]?this.connection.attach(prev_connection[1],prev_connection[2],prev_connection[3],this.conn_callback.bind(this)):this._transport_connect()):this._transport_connect()}catch(b){this.connect()}}, +websocket:function(){try{this.connection=new Strophe.WebSocket(this.params.ws_url);var a=this.rebind_fetch();a&&this.params.rebind?(prev_connection=a.split(" "),this.connection.rebind(prev_connection[1],prev_connection[2],this.conn_callback.bind(this))):this._transport_connect()}catch(b){this.rebind_delete(),this.connect()}},_transport_connect:function(){this.connection.connect(this.params.jid?this.params.jid:this.params.domain,this.params.password?this.params.password:"",this.conn_callback.bind(this))}, +_check_rebind:function(){if(this.params.rebind){var a=this.rebind_fetch();if(a&&this.params.jid&&""!==this.params.jid){a.split(" ");var b=Strophe.getBareJidFromJid(this.params.jid),a=Strophe.getBareJidFromJid(a.split(" ")[1]);b!=a&&this.rebind_delete()}}},conn_callback:function(a){var b=this;this.params.debug&&(this.connection.rawOutput=function(a){b.console.log("out -> "+a)},this.connection.rawInput=function(a){b.console.log("in <- "+a)});window.clearTimeout(this.timeout_id);this.bosh_rebind_id= +this.connection.addTimedHandler(2E3,function(){if("BOSH"===b.current_protocol&&!b.closing){var a=["BOSH",b.connection.jid,b.connection.sid,b.connection.rid].join(" ");b.rebind_store(a)}return!0});this.params.on_strophe_event(a,this.connection);var c=function(a,c){b.params.jid=a;b.params.password=c;b.connect()};if(a===Strophe.Status.CONNECTED)this.params.on_connected(),this.retries=0,!0==this.params.rebind&&"WEBSOCKET"==this.current_protocol&&this.connection.save(function(){var a=["WEBSOCKET",b.connection.jid, +b.connection.streamId].join(" ");b.rebind_store(a)},function(){}),this.subscribe();else if(a===Strophe.Status.ATTACHED)"WEBSOCKET"==this.current_protocol?this.subscribe():setTimeout(function(){nodes=b.params.nodes;for(var a in nodes)"string"==typeof nodes[a]&&0 + is released under the MIT License +*/ +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/rfc6455 + +(function() { + + if (window.WEB_SOCKET_FORCE_FLASH) { + // Keeps going. + } else if (window.WebSocket) { + return; + } else if (window.MozWebSocket) { + // Firefox. + window.WebSocket = MozWebSocket; + return; + } + + var logger; + if (window.WEB_SOCKET_LOGGER) { + logger = WEB_SOCKET_LOGGER; + } else if (window.console && window.console.log && window.console.error) { + // In some environment, console is defined but console.log or console.error is missing. + logger = window.console; + } else { + logger = {log: function(){ }, error: function(){ }}; + } + + // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. + if (swfobject.getFlashPlayerVersion().major < 9) { + logger.error("Flash Player >= 9.0.0 is required."); + return; + } + if (location.protocol == "file:") { + logger.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * Our own implementation of WebSocket class using Flash. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + self.__createTask = setTimeout(function() { + WebSocket.__addTask(function() { + self.__createTask = null; + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.__createTask) { + clearTimeout(this.__createTask); + this.__createTask = null; + this.readyState = WebSocket.CLOSED; + return; + } + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler.apply(this, [event]); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + jsEvent = this.__createSimpleEvent("close"); + jsEvent.wasClean = flashEvent.wasClean ? true : false; + jsEvent.code = flashEvent.code; + jsEvent.reason = flashEvent.reason; + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__initialized = false; + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + + if (WebSocket.__initialized) return; + WebSocket.__initialized = true; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && + !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && + WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { + var swfHost = RegExp.$1; + if (location.host != swfHost) { + logger.error( + "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + + "('" + location.host + "' != '" + swfHost + "'). " + + "See also 'How to host HTML file and SWF file in different domains' section " + + "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + + "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); + } + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "9.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + logger.error("[WebSocket] swfobject.embedSWF failed"); + } + } + ); + + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + logger.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + logger.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + logger.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + // NOTE: + // This fires immediately if web_socket.js is dynamically loaded after + // the document is loaded. + swfobject.addDomLoadEvent(function() { + WebSocket.__initialize(); + }); + } + +})(); diff --git a/src/p1pp.js b/src/p1pp.js new file mode 100644 index 0000000..cd42f5b --- /dev/null +++ b/src/p1pp.js @@ -0,0 +1,479 @@ +/** + * P1PP library + * Author: Eric Cestari + * + * This library will open an XMPP socket using the best transport available. + * Once connection has been established, it will suscribe to PubSub nodes on the given service + * For each publication event, it will call the user provided `publish(id, item, delayed)` function. + * + */ + +/** + * P1PP. The main class + * + **/ +P1PP.prototype = { + /** + * @param {Object} params the configuration parameters. See Readme for usage + * @param {Boolean} fallback Used internally. if connection fails and timeout code is triggered it will set this to true. + */ + connect: function(fallback){ + var self = this, + params = this.params; + if(!!this.connection && this.connection.connected){ + return; + } + + this._check_rebind(); + var _connect = function(){ + self.retries ++; + if (self.retries > params.connectretry){ + self.console.log(params.connectretry + " connect retry exceeded"); + return; + } + self.timeout_id = setTimeout(function(){ + self.connect(true); + }, params.connect_timeout); + if(!fallback && params.ws_url && window.WebSocket){ + self.current_protocol = "WEBSOCKET"; + self.websocket(); + } else { + self.current_protocol = "BOSH"; + self.bosh(); + } + }; + // Setting up connection delay + if((this.retries == 0) && !fallback){ + setTimeout(_connect.bind(this), params.connect_delay); + } else { + _connect(); + } + }, + /** + * Closes connection to server + */ + disconnect: function(){ + this.closing=true; + window.clearTimeout(this.timeout_id); + this.rebind_delete(); + if(this.connection){ + this.connection.deleteTimedHandler(this.bosh_rebind_id); + this.connection.disconnect(); + } + }, + + /** + * BOSH connection code. + * Will attempt to re-attach is param is set and attached value stored on the client. + */ + bosh: function(){ + try{ + this.connection = new Strophe.Connection(this.params.bosh_url); + /* Reattach needs to be fixed on BOSH */ + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.rebind){ //there's a cookie + prev_connection = cookie.split(" "); + if(prev_connection[0] == "BOSH"){ //Only BOSH cookies accepted + this.connection.attach(prev_connection[1], + prev_connection[2], + prev_connection[3], + this.conn_callback.bind(this)) + } else { + this._transport_connect(); + } + } else { + this._transport_connect(); + } + } catch (e){ + this.connect(); + } + + }, + + /** + * WebSocket connection code + * Will attempt to use fast rebind is param is set and rebind data is availble on client. + * @private + */ + websocket: function(){ + try{ + this.connection = new Strophe.WebSocket(this.params.ws_url); + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.rebind){ + prev_connection = cookie.split(" "); + this.connection.rebind(prev_connection[1], + prev_connection[2], + this.conn_callback.bind(this)) + } else { + this._transport_connect(); + } + } catch (e){ + this.rebind_delete(); + this.connect(); + } + }, + + _transport_connect: function(){ + var jid = this.params.jid ? this.params.jid : this.params.domain; + var password = this.params.password ? this.params.password : "" + this.connection.connect(jid, + password, this.conn_callback.bind(this)); + }, + /** + * Checks if rebind can be safely called + * Only useful in case of non-anon connections + */ + _check_rebind: function(){ + if(!this.params.rebind){ + //rebind not active + return; + } + var cookie = this.rebind_fetch(); + if(!!cookie && this.params.jid && this.params.jid !== ""){ + var prev = cookie.split(" "); + var jid = Strophe.getBareJidFromJid(this.params.jid); + var prev_jid = Strophe.getBareJidFromJid(cookie.split(" ")[1]); + if(jid != prev_jid){ + this.rebind_delete(); + } + } + }, + /** + * Main connection callback + * @param {Integer} status The StropheJS status of the connection. + * @private + */ + conn_callback: function(status){ + var that = this; + if(this.params.debug){ + this.connection.rawOutput = function(elem){ + that.console.log("out -> " + elem); + } + this.connection.rawInput = function(elem){ + that.console.log("in <- " + elem); + } + } + // Connection established + window.clearTimeout(this.timeout_id); + this.bosh_rebind_id = this.connection.addTimedHandler(2000, function(){ + if(that.current_protocol === "BOSH" && !that.closing){ + var c = ["BOSH", that.connection.jid, that.connection.sid, that.connection.rid].join(" "); + that.rebind_store(c); + } + return true; + }); + this.params.on_strophe_event(status, this.connection); + var login_required_cb = function(jid, pass){ + that.params.jid=jid; + that.params.password=pass; + that.connect(); + } + if (status === Strophe.Status.CONNECTED) { + this.params.on_connected(); + this.retries = 0; + if(this.params.rebind == true + && this.current_protocol == "WEBSOCKET"){ + this.connection.save(function(){ + var id = ["WEBSOCKET",that.connection.jid, that.connection.streamId].join(" ") + that.rebind_store(id); + }, function(){}); + } + this.subscribe(); + } + // Connection re-attached (or rebound) + else if (status === Strophe.Status.ATTACHED){ + // Forcing subscription. there is a problem with rebinding and anon subs in ejabberd + if(this.current_protocol == "WEBSOCKET"){ + this.subscribe(); + } + else{ + // For some reason, I have to wait the second HTTP POST to actually get the result. + // ejabberd says data is sent, the browser says no, it's not. + // One of them is lying. Or both. + // In the meantime, a short timeout will do the trick. + setTimeout(function(){ + nodes = that.params.nodes; + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + // In case of attach, we need to set up the handlers again. + if(that.params.num_old > 0){ + that.connection.pubsub.items(that.connection.jid, that.params.pubsub_domain, nodes[i], that.params.num_old, function(message){ + var items = message.getElementsByTagName("item") + for(var i = 0; i < items.length; i++){ + id = items[i].getAttribute("id"); + that.params.publish(id, items[i].firstChild); + } + }) + } + } + } + that.connection.addHandler(that.on_event.bind(that), + null,'message', null, null, that.params.pubsub_domain); + }, 100); + + } + + } + // Connection problem or reattach failed. Will attempt to reconnect after a random wait + else if (!this.closing + && (status === Strophe.Status.CONNFAIL + || status === Strophe.Status.DISCONNECTED)) { + this.connection.deleteTimedHandler(this.bosh_rebind_id); + this.rebind_delete(); + //login is required. Give user code a chance to fetch jid and password + if(!this.params.jid && this.params.login_required){ + this.params.on_login_required(login_required_cb) + } else { + var retry_time = Math.round(Math.random() * this.params.connect_timeout * (this.retries+1)); + this.timeout_id = setTimeout(function(){ + that.connect(); + }, retry_time); + } + } + else if (status === Strophe.Status.DISCONNECTED){ + this.connection.reset(); + this.closing = false; + this.params.on_disconnect(); + } + // WebSocket rebind failed. Removing user data and reconnecting + else if (status === Strophe.Status.REBINDFAILED){ + this.rebind_delete(); + this.connection = null; + this.connect(); + } + else if (status === Strophe.Status.AUTHFAIL){ + delete this.connection; + this.params.on_login_required(login_required_cb); + } + }, + /** + * Subscribe to PubSub nodes + * If num_old is set, fetch older nodes. + * Warning: The last_published item is delivered twice, if both send_last_item configured on node and num_old >= 1 + * @private + */ + subscribe: function(nodes){ + if(nodes === undefined){ + nodes = this.params.nodes; + } + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + if(this.params.num_old > 0){ + var that = this; + this.connection.pubsub.items(this.connection.jid, this.params.pubsub_domain, nodes[i], this.params.num_old, function(message){ + var items = message.getElementsByTagName("item") + for(var i = 0; i < items.length; i++){ + id = items[i].getAttribute("id"); + that.params.publish(id, items[i].firstChild); + } + }) + } + this.connection.pubsub.subscribe(this.connection.jid, + this.params.pubsub_domain, + nodes[i],[], + function(){}); + } + } + this.connection.addHandler(this.on_event.bind(this), + null,'message', null, null, this.params.pubsub_domain); + }, + + unsubscribe: function(nodes){ + if(nodes === undefined){ + nodes = this.params.nodes; + } + for(var i in nodes){ + if(typeof nodes[i] == "string"){ // IE6 fix + this.connection.pubsub.unsubscribe(this.connection.jid, + this.params.pubsub_domain, + nodes[i], + function(){}); + } + } + }, + + _extract_error_code: function(stanza) { + var error = stanza.getElementsByTagNameNS("http://jabber.org/protocol/pubsub#errors", "*"); + + if (!error.length) + error = stanza.getElementsByTagNameNS("urn:ietf:params:xml:ns:xmpp-stanzas", "*"); + + return error.length ? error[0].localName : null; + }, + + _publish: function(node, id, value, callback) { + var that = this; + + if (id == null) + id = this.connection.getUniqueId("publish"); + + this.connection.pubsub.publish(this.connection.jid, this.params.pubsub_domain, + node, [{id: id, value: [value]}], function(stanza) { + that._done_publish(stanza, callback); + }); + return id; + }, + + _done_publish: function(stanza, callback) { + var items = stanza.getElementsByTagName("item"); + var id = items.length ? items[0].getAttribute("id") : null; + + if (stanza.getAttribute("type") == "result") + callback(id, "ok"); + else + callback(id, this._extract_error_code(stanza) || "error") + }, + + _deleteNode: function(node, callback) { + var that = this; + + this.connection.pubsub.deleteNode(this.connection.jid, this.params.pubsub_domain, + node, function(stanza) { + that._done_delete(stanza, callback); + }); + }, + + _done_delete: function(stanza, callback) { + if (stanza.getAttribute("type") == "result") + callback("ok"); + else + callback(this._extract_error_code(stanza) || "error") + }, + + /** + * pubsub message handling. + * Triggered everytime there is an event coming from the server (publication or retraction of items) + * @param {DOMElement} msg the message from the server + * @private + **/ + on_event: function(msg){ + var retracts = msg.getElementsByTagName("retract"); + var length =retracts.length + for(var i = 0; i < length; i++){ + this.params.retract(retracts[i].getAttribute("id")); + } + var delay_time = undefined; + var delay = msg.getElementsByTagName("delay"); + if(delay.length){ + delay_time = delay[0].getAttribute("stamp"); + } + var items = msg.getElementsByTagName("items"); + length = items.length; + var node_name, node_items, ilength; + for(var i = 0; i < length; i++){ + node_name = items[i].getAttribute("node"); + node_items = items[i].getElementsByTagName("item"); + ilength = node_items.length; + for(var i = 0; i < ilength; i++){ + this.params.publish(node_items[i].getAttribute("id"), + node_items[i].firstChild, + node_name, + delay_time); + } + } + return true; + }, + + /** + * Cookie management code, taken from jquery.cookie.js + * @private + */ + cookie: function(key, value){ + if (typeof value != 'undefined'){ + options = this.params.cookie_opts; + if (value === null) { + value = ''; + options.expires = -1; + } + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + } + // CAUTION: Needed to parenthesize options.path and options.domain + // in the following expressions, otherwise they evaluate to undefined + // in the packed version for some reason... + var path = options.path ? '; path=' + (options.path) : ''; + var domain = options.domain ? '; domain=' + (options.domain) : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [key, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var trim = function( text ) { + if ( typeof String.trim === "function" ) { + return ( text || "" ).trim(); + } + return (text || "").replace( /^\s\s*/, "" ).replace( /\s\s*$/, "" ); + } + var cookie = trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, key.length + 1) == (key + '=')) { + cookieValue = decodeURIComponent(cookie.substring(key.length + 1)); + break; + } + } + } + return cookieValue; + } + + }, + /** + * stores connection information in sessionStorage if available or cookie + * @private + */ + rebind_store: function(value){ + var key = this._build_key() + if(window.sessionStorage){ + sessionStorage[key]=value; + } else { + this.cookie(key, value); + } + }, + /** + * deletes connection information in sessionStorage if available or cookie + * @private + */ + rebind_delete: function(){ + var key = this._build_key() + if(window.sessionStorage){ + sessionStorage.removeItem(key) + } else { + this.cookie(key, null); + } + }, + /** + * fetchs connection information in sessionStorage if available or cookie + * @private + */ + rebind_fetch: function(){ + var key = this._build_key(); + if(window.sessionStorage){ + return sessionStorage[key]; + } else { + return this.cookie(key); + } + }, + /** + * Build key for protocol. + * BOSH connections can be scoped, for a different set of nodes + * Not necessary for WS as the client subscribes on reattach. + */ + _build_key: function(){ + var key = P1PP.COOKIE+ "_" + this.current_protocol; + if(this.current_protocol === "BOSH"){ + key = key + "_" + this.scope; + } + return key; + } +}; + diff --git a/src/p1pp_defs.js b/src/p1pp_defs.js new file mode 100644 index 0000000..2b69cbf --- /dev/null +++ b/src/p1pp_defs.js @@ -0,0 +1,213 @@ +var WEB_SOCKET_DEBUG = false; +var WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + +var P1PP = function(params){ + this.jid= null; //for non-anonymous connections + this.password= null; // + this.connection= null; // the strophe connection + this.params= null; // merge between default params and user provided params + this.timeout_id= null; // points to the timeout function triggering the BOSH connection + //if WS connection failed + this.retries=0; // How many retries already ? + this.closing=false; + this.defaults= { + flash_location: "WebSocketMain.swf", + domain: "p1pp.net", + ws_url: "ws://p1pp.net:5280/xmpp", + bosh_url: "ws://p1pp.net:5280/http-bind", + connect_timeout: 15000, //How long should we wait before trying BOSH ? + connect_delay: 0, //Connection will not be done before this number of ms + connect_retry: 10, //Connection max attempts + rebind: true, // attempt to reuse previous connection + debug: false, // Dump traffic in console + num_old: 0, // How many old items should be fetch ? + on_strophe_event: function(){}, // Connect callback. if additional XMPP exchanges are to be done with the server. + on_login_required: null, + publish: function(){}, // User provided call back.Called everytime an event is published. + retract: function(){}, // User provided call back. Called when an item is retracted. + on_disconnected: function(){}, // User Provided callback + on_connected: function(){}, + cookie_opts: {}, + nodes: [] + }; + var merge = function (o,ob) {var i = 0;for (var z in ob) {if (ob.hasOwnProperty(z)) {o[z] = ob[z];}}return o;} + this.params = merge(this.defaults, params); + if(!this.params.pubsub_domain){ + this.params.pubsub_domain = "pubsub."+this.params.domain; + } + + var nodes = this.params.nodes; + if(nodes.length > 0){ + this.scope = MD5.hexdigest(nodes.join("-")); + } + + if (!window.console || !window.console.log || !window.console.error) { + this.console = {log: function(){ }, error: function(){ }}; + } else { + if(this.params.debug){ + this.console = window.console; + } + } + if(this.params.debug){ + window.WEB_SOCKET_DEBUG = true; + } + window.WEB_SOCKET_SWF_LOCATION=this.params.flash_location; + //initializing flash websockets (if necessary) + if(window.WebSocket && window.WebSocket.__initialize){ + window.WebSocket.__initialize(); + } + return this; +} +/** + * Connects to server and subscribes to select channels. + * @param {Object} params JSON object with configuration options (see below for attributes) + * @returns {Object} the P1PP instance used for the connection + * + *

Parameters and their default value

+ * jid: "" if set, will connect with this JID and password instead anonymous + * password: "" see above + * ws_url: "ws://gitlive.com:5280/xmpp", websocket URL + * bosh_url: "http://gitlive.com:5280/http-bind", BOSH URL + * domain: "gitlive.com", Domain to logon to + * rebind: true, should use rebind if possible + * nodes: [], list of nodes to subscribe to + * num_old: 0, maximum number of old items to fetch + * flash_location: "WebSocketMain.swf", Location of the WebSocket flash file. Can be an URL + * connect_delay: 0 The client will attempt connection after this milliseconds + * connect_timeout: 3000 How long should we wait before fallback to BOSH , + * connect_retry: 10, How many times should we try connecting + * pubsub_domain: "pubsub.gitlive.com", pubsub service url. defaults to pubsub.domain + * debug: false, Will dump traffic in console if true.. + * publish: function(){}, publish callback + * retract: function(){} retract callback + * on_strophe_event: function(){} Access to StropheJS API events + * cookie_opts: {path: false, domain: false, expire:false, secure: false} cookies options if cookies are used + * + */ +P1PP.connect = function(params){ + if(!this.push_client){ + this.push_client = new P1PP(params); + this.push_client.connect(); + } else if(this.push_client.connection.connected == false){ + this.push_client.connect(); + } + return this.push_client; + } + +/** + * Disconnect client + */ +P1PP.disconnect = function(){ + var that = this; + this.push_client.disconnect() + } +/** + * Subscribe to a channel or channels + * @param channels a string or an array of string each being a node to subscribe to + */ +P1PP.addChannel = function(channels){ + if(this.push_client){ + if(typeof channels === "string"){ + channels = [channels]; + } + var nodes = this.push_client.params.nodes; + this.merge(nodes, channels) + if(nodes.length > 0){ + this.push_client.scope = MD5.hexdigest(nodes.join("-")); + } + this.push_client.subscribe(channels) + } +} +/** + * Unsubscribe from a channel or channels + * @param channels a string or an array of string each being a node to unsubscribe from + */ +P1PP.removeChannel = function(channels){ + if(this.push_client){ + if(typeof channels === "string"){ + channels = [channels]; + } + var nodes = P1PP.diff(this.push_client.params.nodes, channel); + if(nodes.length > 0){ + this.push_client.scope = MD5.hexdigest(nodes.join("-")); + } + this.push_client.unsubscribe(channels) + } +} + +/** + * Publishes data under given node + * + * @param {String} node - Name of node which should be used for + * publishing + * @param {String} id - Id of published data, if null random name is + * generated + * @param {DOMElement} data - Data to store + * @param {Function} callback - Callback called with (id, status_code) + * after receiving response from server, status_code "ok" is used for + * successfull operation + * @returns null if connection is not yet established, Id of published data otherwise + */ +P1PP.publish = function(node, id, value, callback) { + if (this.push_client) + return this.push_client._publish(node, id, value, callback); + return null; +}, + +/** + * Publishes data under given node + * + * @param {String} node - Name of node which should be used for + * publishing + * @param {String} id - Id of published data, if null random name is + * generated + * @param {DOMElement} data - Data to store + * @param {Function} callback - Callback called with (id, status_code) + * after receiving response from server, status_code "ok" is used for + * successfull operation + * @returns Id of published data + */ +P1PP.deleteNode = function(node, callback) { + if (this.push_client) + return this.push_client._deleteNode(node, callback); + return null; +}, + +P1PP.COOKIE = "session"; // Cookie or sessionstorage key used to store attach or fast rebind data. +P1PP.couldRebind = function(){ + var protocols = ["WEBSOCKET", "BOSH"]; + for(p in protocols){ + var key = P1PP.COOKIE+ "_" + protocols[p]; + if(window.sessionStorage){ + return !!sessionStorage[key]; + } else { + return !!this.cookie(key); + } + } + } + +/** + * merges two arrays + * Taken from jQuery 1.5 + */ +P1PP.merge = function( first, second ) { + var i = first.length, + j = 0; + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + first.length = i; + return first; + } +/** + * Array diff + */ +P1PP.diff = function(first, second){ + return first.filter(function(i) {return !(second.indexOf(i) > -1);}); + } diff --git a/src/strophe/strophe.bosh.js b/src/strophe/strophe.bosh.js new file mode 100644 index 0000000..80a1949 --- /dev/null +++ b/src/strophe/strophe.bosh.js @@ -0,0 +1,1824 @@ +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The BOSH service URL. + * + * Returns: + * A new Strophe.Connection object. + */ +Strophe.Connection = function (service) +{ + /* The path to the httpbind service. */ + this.service = service; + /* The connected JID. */ + this.jid = ""; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this.paused = false; + + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); + } + } +}; + +Strophe.Connection.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function () + { + this.rid = Math.floor(Math.random() * 4294967295); + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this._requests = []; + this._uniqueId = Math.round(Math.random()*10000); + }, + + /** Function: pause + * Pause the request manager. + * + * This will prevent Strophe from sending any more requests to the + * server. This is very useful for temporarily pausing while a lot + * of send() calls are happening quickly. This causes Strophe to + * send the data in a single request, saving many request trips. + */ + pause: function () + { + this.paused = true; + }, + + /** Function: resume + * Resume the request manager. + * + * This resumes after pause() has been called. + */ + resume: function () + { + this.paused = false; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + */ + connect: function (jid, pass, callback, wait, hold) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + + // build the body tag + var body = this._buildBody().attrs({ + to: this.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._connect_cb.bind(this)), + body.tree().rid)); + this._throttledRequestHandler(); + }, + + /** Function: attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + attach: function (jid, sid, rid, callback, wait, hold, wind) + { + this.jid = jid; + this.sid = sid; + this.rid = rid; + this.connect_callback = callback; + + this.domain = Strophe.getDomainFromJid(this.jid); + + this.authenticated = true; + this.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + xmlInput: function (elem) + { + return; + }, + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + xmlOutput: function (elem) + { + return; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function (elem) + { + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + this._queueData(elem[i]); + } + } else if (typeof(elem.tree) === "function") { + this._queueData(elem.tree()); + } else { + this._queueData(elem); + } + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: flush + * Immediately send any pending outgoing data. + * + * Normally send() queues outgoing data until the next idle period + * (100ms), which optimizes network use in the common cases when + * several send()s are called in succession. flush() can be used to + * immediately send all pending data. + */ + flush: function () + { + // cancel the pending idle period and run the idle function + // immediately + clearTimeout(this._idleTimeout); + this._onIdle(); + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); + + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) { + callback(stanza); + } + } else if (iqtype == 'error') { + if (errback) { + errback(stanza); + } + } else { + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; + } + }, null, 'iq', null, id); + + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + + this.send(elem); + + return id; + }, + + /** PrivateFunction: _queueData + * Queue outgoing data for later sending. Also ensures that the data + * is a DOMElement. + */ + _queueData: function (element) { + if (element === null || + !element.tagName || + !element.childNodes) { + throw { + name: "StropheError", + message: "Cannot queue non-DOMElement." + }; + } + + this._data.push(element); + }, + + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); + } + } + } + } + + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); + } + } + }, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function (req) + { + Strophe.debug("removing request"); + + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); + } + } + + // IE6 fails on setting to null, so set to empty function + req.xhr.onreadystatechange = function () {}; + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _restartRequest + * _Private_ function to restart a request that is presumed dead. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _restartRequest: function (i) + { + var req = this._requests[i]; + if (req.dead === null) { + req.dead = new Date(); + } + + this._processRequest(i); + }, + + /** PrivateFunction: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function (i) + { + var req = this._requests[i]; + var reqStatus = -1; + + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); + } + req.abort = true; + req.xhr.abort(); + // setting to null fails on IE6, so set to empty function + req.xhr.onreadystatechange = function () {}; + this._requests[i] = new Strophe.Request(req.xmlData, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; + } + + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + req.date = new Date(); + try { + req.xhr.open("POST", this.service, true); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this.connected) { + this._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); + } + this.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.xhr.send(req.data); + }; + + // Implement progressive backoff for reconnects -- + // First retry (send == 1) should also be instantaneous + if (req.sends > 1) { + // Using a cube of the retry number creats a nicely + // expanding retry window + var backoff = Math.pow(req.sends, 3) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } + + req.sends++; + + this.xmlOutput(req.xmlData); + this.rawOutput(req.data); + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function () + { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window - 1) { + this._processRequest(1); + } + }, + + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function (func, req) + { + Strophe.debug("request id " + req.id + + "." + req.sends + " state changed to " + + req.xhr.readyState); + + if (req.abort) { + req.abort = false; + return; + } + + // request complete + var reqStatus; + if (req.xhr.readyState == 4) { + reqStatus = 0; + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. works + // around a browser bug + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; + } + + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } + + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); + + if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { + // remove from internal queue + this._removeRequest(req); + Strophe.debug("request id " + + req.id + + " should now be removed"); + } + + // request succeeded + if (reqStatus == 200) { + // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + if (reqIs1 || + (reqIs0 && this._requests.length > 0 && + this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { + this._restartRequest(0); + } + // call handler + Strophe.debug("request id " + + req.id + "." + + req.sends + " got 200"); + func(req); + this.errors = 0; + } else { + Strophe.error("request id " + + req.id + "." + + req.sends + " error " + reqStatus + + " happened"); + if (reqStatus === 0 || + (reqStatus >= 400 && reqStatus < 600) || + reqStatus >= 12000) { + this._hitError(reqStatus); + if (reqStatus >= 400 && reqStatus < 500) { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._doDisconnect(); + } + } + } + + if (!((reqStatus > 0 && reqStatus < 10000) || + req.sends > 5)) { + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function (reqStatus) + { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + + ", number of errors: " + this.errors); + if (this.errors > 4) { + this._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + this.rid = Math.floor(Math.random() * 4294967295); + + // tell the parent we disconnected + if (this.connected) { + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (req) + { + try { + var elem = req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this.disconnect("strophe-parsererror"); + } + if (elem === null) { return; } + + this.xmlInput(elem); + this.rawInput(Strophe.serialize(elem)); + + // remove handlers scheduled for deletion + var i, hand; + while (this.removeHandlers.length > 0) { + hand = this.removeHandlers.pop(); + i = this.handlers.indexOf(hand); + if (i >= 0) { + this.handlers.splice(i, 1); + } + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting && this._requests.length === 0) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + return; + } + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = elem.getAttribute("condition"); + conflict = elem.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this.disconnect(); + return; + } + + // send each incoming stanza through the handler chain + var that = this; + Strophe.forEachChild(elem, null, function (child) { + var i, newList; + // process handlers + newList = that.handlers; + that.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { + that.handlers.push(hand); + } + } else { + that.handlers.push(hand); + } + } + }); + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (this.authenticated) { + body.c('presence', { + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().getAttribute("rid")); + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (req) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + var bodyWrap = req.getResponse(); + if (!bodyWrap) { return; } + + this.xmlInput(bodyWrap); + this.rawInput(Strophe.serialize(bodyWrap)); + + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + return; + } + + // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + if (!this.stream_id) { + this.stream_id = bodyWrap.getAttribute("authid"); + } + var wind = bodyWrap.getAttribute('requests'); + if (wind) { this.window = parseInt(wind, 10); } + var hold = bodyWrap.getAttribute('hold'); + if (hold) { this.hold = parseInt(hold, 10); } + var wait = bodyWrap.getAttribute('wait'); + if (wait) { this.wait = parseInt(wait, 10); } + + + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } else { + // we didn't get stream:features yet, so we need wait for it + // by sending a blank poll request + var body = this._buildBody(); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._connect_cb.bind(this)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + return; + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.getBareJidFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = Base64.encode(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); + + this.send($iq({ + type: "get", + to: this.domain, + id: "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = Base64.decode(Strophe.getText(elem)); + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + this.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + + this._quote(Strophe.getNodeFromJid(this.jid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + this._quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + _quote: function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }, + + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .up() + .c('password').t(this.pass); + + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + + // we must send an xmpp:restart now + this._sendRestart(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, + null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + if (resource) { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .c('resource', {}).t(resource).tree()); + } else { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .tree()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } else { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; + } + + // actually disconnect + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function () + { + var i, thand, since, newList; + + // remove timed handlers that have been scheduled for deletion + while (this.removeTimeds.length > 0) { + thand = this.removeTimeds.pop(); + i = this.timedHandlers.indexOf(thand); + if (i >= 0) { + this.timedHandlers.splice(i, 1); + } + } + + // add timed handlers scheduled for addition + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } + + // call ready timed handlers + var now = new Date().getTime(); + newList = []; + for (i = 0; i < this.timedHandlers.length; i++) { + thand = this.timedHandlers[i]; + if (this.authenticated || !thand.user) { + since = thand.lastCalled + thand.period; + if (since - now <= 0) { + if (thand.run()) { + newList.push(thand); + } + } else { + newList.push(thand); + } + } + } + this.timedHandlers = newList; + + var body, time_elapsed; + + // if no requests are in progress, poll + if (this.authenticated && this._requests.length === 0 && + this._data.length === 0 && !this.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + this._data.push(null); + } + + if (this._requests.length < 2 && this._data.length > 0 && + !this.paused) { + body = this._buildBody(); + for (i = 0; i < this._data.length; i++) { + if (this._data[i] !== null) { + if (this._data[i] === "restart") { + body.attrs({ + to: this.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(this._data[i]).up(); + } + } + } + delete this._data; + this._data = []; + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().rid)); // TODO Fix that. + this._processRequest(this._requests.length - 1); + } + + if (this._requests.length > 0) { + time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } + } + + if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { + Strophe.warn("Request " + + this._requests[0].id + + " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + + " seconds since last activity"); + this._throttledRequestHandler(); + } + } + + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +}; \ No newline at end of file diff --git a/src/strophe/strophe.js b/src/strophe/strophe.js new file mode 100644 index 0000000..14e9ddb --- /dev/null +++ b/src/strophe/strophe.js @@ -0,0 +1,1710 @@ +// This code was written by Tyler Akins and has been placed in the +// public domain. It would be nice if you left this header intact. +// Base64 code from Tyler Akins -- http://rumkin.com + +var Base64 = (function () { + var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + var obj = { + /** + * Encodes a string in base64 + * @param {String} input The string to encode in base64. + */ + encode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + do { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + keyStr.charAt(enc4); + } while (i < input.length); + + return output; + }, + + /** + * Decodes a base64 string. + * @param {String} input The string to decode. + */ + decode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + + return output; + } + }; + + return obj; +})(); +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +var MD5 = (function () { + /* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + var safe_add = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + + /* + * Bitwise rotate a 32-bit number to the left. + */ + var bit_rol = function (num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + }; + + /* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ + var str2binl = function (str) { + var bin = []; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + { + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + } + return bin; + }; + + /* + * Convert an array of little-endian words to a string + */ + var binl2str = function (bin) { + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + { + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + } + return str; + }; + + /* + * Convert an array of little-endian words to a hex string. + */ + var binl2hex = function (binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; + }; + + /* + * Convert an array of little-endian words to a base-64 string + */ + var binl2b64 = function (binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for(var i = 0; i < binarray.length * 4; i += 3) + { + triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + } + } + return str; + }; + + /* + * These functions implement the four basic operations the algorithm uses. + */ + var md5_cmn = function (q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b); + }; + + var md5_ff = function (a, b, c, d, x, s, t) { + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); + }; + + var md5_gg = function (a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); + }; + + var md5_hh = function (a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); + }; + + var md5_ii = function (a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); + }; + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ + var core_md5 = function (x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + var olda, oldb, oldc, oldd; + for (var i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return [a, b, c, d]; + }; + + + /* + * Calculate the HMAC-MD5, of a key and some data + */ + var core_hmac_md5 = function (key, data) { + var bkey = str2binl(key); + if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); } + + var ipad = new Array(16), opad = new Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); + }; + + var obj = { + /* + * These are the functions you'll usually want to call. + * They take string arguments and return either hex or base-64 encoded + * strings. + */ + hexdigest: function (s) { + return binl2hex(core_md5(str2binl(s), s.length * chrsz)); + }, + + b64digest: function (s) { + return binl2b64(core_md5(str2binl(s), s.length * chrsz)); + }, + + hash: function (s) { + return binl2str(core_md5(str2binl(s), s.length * chrsz)); + }, + + hmac_hexdigest: function (key, data) { + return binl2hex(core_hmac_md5(key, data)); + }, + + hmac_b64digest: function (key, data) { + return binl2b64(core_hmac_md5(key, data)); + }, + + hmac_hash: function (key, data) { + return binl2str(core_hmac_md5(key, data)); + }, + + /* + * Perform a simple self-test to see if the VM is working + */ + test: function () { + return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72"; + } + }; + + return obj; +})(); + +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* jslint configuration: */ +/*global document, window, setTimeout, clearTimeout, console, + XMLHttpRequest, ActiveXObject, + Base64, MD5, + Strophe, $build, $msg, $iq, $pres */ + +/** File: strophe.js + * A JavaScript library for XMPP BOSH. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * has no facilities for persistent TCP connections, this library uses + * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate + * a persistent, stateful, two-way connection to an XMPP server. More + * information on BOSH can be found in XEP 124. + */ + +/** PrivateFunction: Function.prototype.bind + * Bind a function to an instance. + * + * This Function object extension method creates a bound method similar + * to those in Python. This means that the 'this' object will point + * to the instance you want. See + * Bound Functions and Function Imports in JavaScript + * for a complete explanation. + * + * This extension already exists in some browsers (namely, Firefox 3), but + * we provide it to support those that don't. + * + * Parameters: + * (Object) obj - The object that will become 'this' in the bound function. + * + * Returns: + * The bound function. + */ +if (!Function.prototype.bind) { + Function.prototype.bind = function (obj) + { + var func = this; + return function () { return func.apply(obj, arguments); }; + }; +} + +/** PrivateFunction: Function.prototype.prependArg + * Prepend an argument to a function. + * + * This Function object extension method returns a Function that will + * invoke the original function with an argument prepended. This is useful + * when some object has a callback that needs to get that same object as + * an argument. The following fragment illustrates a simple case of this + * > var obj = new Foo(this.someMethod); + * + * Foo's constructor can now use func.prependArg(this) to ensure the + * passed in callback function gets the instance of Foo as an argument. + * Doing this without prependArg would mean not setting the callback + * from the constructor. + * + * This is used inside Strophe for passing the Strophe.Request object to + * the onreadystatechange handler of XMLHttpRequests. + * + * Parameters: + * arg - The argument to pass as the first parameter to the function. + * + * Returns: + * A new Function which calls the original with the prepended argument. + */ +if (!Function.prototype.prependArg) { + Function.prototype.prependArg = function (arg) + { + var func = this; + + return function () { + var newargs = [arg]; + for (var i = 0; i < arguments.length; i++) { + newargs.push(arguments[i]); + } + return func.apply(this, newargs); + }; + }; +} + +/** PrivateFunction: Array.prototype.indexOf + * Return the index of an object in an array. + * + * This function is not supplied by some JavaScript implementations, so + * we provide it if it is missing. This code is from: + * http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf + * + * Parameters: + * (Object) elt - The object to look for. + * (Integer) from - The index from which to start looking. (optional). + * + * Returns: + * The index of elt in the array or -1 if not found. + */ +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt /*, from*/) + { + var len = this.length; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && this[from] === elt) { + return from; + } + } + + return -1; + }; +} + +/* All of the Strophe globals are defined in this special function below so + * that references to the globals become closures. This will ensure that + * on page reload, these references will still be available to callbacks + * that are still executing. + */ + +(function (callback) { +var Strophe; + +/** Function: $build + * Create a Strophe.Builder. + * This is an alias for 'new Strophe.Builder(name, attrs)'. + * + * Parameters: + * (String) name - The root element name. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $build(name, attrs) { return new Strophe.Builder(name, attrs); } +/** Function: $msg + * Create a Strophe.Builder with a element as the root. + * + * Parmaeters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $msg(attrs) { return new Strophe.Builder("message", attrs); } +/** Function: $iq + * Create a Strophe.Builder with an element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $iq(attrs) { return new Strophe.Builder("iq", attrs); } +/** Function: $pres + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $pres(attrs) { return new Strophe.Builder("presence", attrs); } + +/** Class: Strophe + * An object container for all Strophe library functions. + * + * This class is just a container for all the objects and constants + * used in the library. It is not meant to be instantiated, but to + * provide a namespace for library objects, constants, and functions. + */ +Strophe = { + /** Constant: VERSION + * The version of the Strophe library. Unreleased builds will have + * a version of head-HASH where HASH is a partial revision. + */ + VERSION: "2a276a4", + + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. + * + * NS.HTTPBIND - HTTP BIND namespace from XEP 124. + * NS.BOSH - BOSH namespace from XEP 206. + * NS.CLIENT - Main XMPP client namespace. + * NS.AUTH - Legacy authentication namespace. + * NS.ROSTER - Roster operations namespace. + * NS.PROFILE - Profile namespace. + * NS.DISCO_INFO - Service discovery info namespace from XEP 30. + * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. + * NS.MUC - Multi-User Chat namespace from XEP 45. + * NS.SASL - XMPP SASL namespace from RFC 3920. + * NS.STREAM - XMPP Streams namespace from RFC 3920. + * NS.BIND - XMPP Binding namespace from RFC 3920. + * NS.SESSION - XMPP Session namespace from RFC 3920. + */ + NS: { + HTTPBIND: "http://jabber.org/protocol/httpbind", + BOSH: "urn:xmpp:xbosh", + CLIENT: "jabber:client", + AUTH: "jabber:iq:auth", + ROSTER: "jabber:iq:roster", + PROFILE: "jabber:iq:profile", + DISCO_INFO: "http://jabber.org/protocol/disco#info", + DISCO_ITEMS: "http://jabber.org/protocol/disco#items", + MUC: "http://jabber.org/protocol/muc", + SASL: "urn:ietf:params:xml:ns:xmpp-sasl", + STREAM: "http://etherx.jabber.org/streams", + BIND: "urn:ietf:params:xml:ns:xmpp-bind", + SESSION: "urn:ietf:params:xml:ns:xmpp-session", + VERSION: "jabber:iq:version", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas" + }, + + /** Function: addNamespace + * This function is used to extend the current namespaces in + * Strophe.NS. It takes a key and a value with the key being the + * name of the new namespace, with its actual value. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + * + * Parameters: + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function (name, value) + { + Strophe.NS[name] = value; + }, + + /** Constants: Connection Status Constants + * Connection status constants for use by the connection handler + * callback. + * + * Status.ERROR - An error has occurred + * Status.CONNECTING - The connection is currently being made + * Status.CONNFAIL - The connection attempt failed + * Status.AUTHENTICATING - The connection is authenticating + * Status.AUTHFAIL - The authentication attempt failed + * Status.CONNECTED - The connection has succeeded + * Status.DISCONNECTED - The connection has been terminated + * Status.DISCONNECTING - The connection is currently being terminated + * Status.ATTACHED - The connection has been attached + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7, + ATTACHED: 8 + }, + + /** Constants: Log Level Constants + * Logging level indicators. + * + * LogLevel.DEBUG - Debug output + * LogLevel.INFO - Informational output + * LogLevel.WARN - Warnings + * LogLevel.ERROR - Errors + * LogLevel.FATAL - Fatal errors + */ + LogLevel: { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + FATAL: 4 + }, + + /** PrivateConstants: DOM Element Type Constants + * DOM element types. + * + * ElementType.NORMAL - Normal element. + * ElementType.TEXT - Text data element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3 + }, + + /** PrivateConstants: Timeout Values + * Timeout values for error states. These values are in seconds. + * These should not be changed unless you know exactly what you are + * doing. + * + * TIMEOUT - Timeout multiplier. A waiting request will be considered + * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. + * This defaults to 1.1, and with default wait, 66 seconds. + * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where + * Strophe can detect early failure, it will consider the request + * failed if it doesn't return after + * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. + * This defaults to 0.1, and with default wait, 6 seconds. + */ + TIMEOUT: 1.1, + SECONDARY_TIMEOUT: 0.1, + + /** Function: forEachChild + * Map a function over some or all child elements of a given element. + * + * This is a small convenience function for mapping a function over + * some or all of the children of an element. If elemName is null, all + * children will be passed to the function, otherwise only children + * whose tag names match elemName will be passed. + * + * Parameters: + * (XMLElement) elem - The element to operate on. + * (String) elemName - The child element tag name filter. + * (Function) func - The function to apply to each child. This + * function should take a single argument, a DOM element. + */ + forEachChild: function (elem, elemName, func) + { + var i, childNode; + + for (i = 0; i < elem.childNodes.length; i++) { + childNode = elem.childNodes[i]; + if (childNode.nodeType == Strophe.ElementType.NORMAL && + (!elemName || this.isTagEqual(childNode, elemName))) { + func(childNode); + } + } + }, + + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case insensitive. + * + * Parameters: + * (XMLElement) el - A DOM element. + * (String) name - The element name. + * + * Returns: + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function (el, name) + { + return el.tagName.toLowerCase() == name.toLowerCase(); + }, + + /** PrivateVariable: _xmlGenerator + * _Private_ variable that caches a DOM document to + * generate elements. + */ + _xmlGenerator: null, + + /** PrivateFunction: _makeGenerator + * _Private_ function that creates a dummy XML DOM document to serve as + * an element and text node generator. + */ + _makeGenerator: function () { + var doc; + + if (window.ActiveXObject) { + doc = new ActiveXObject("Microsoft.XMLDOM"); + doc.appendChild(doc.createElement('strophe')); + } else { + doc = document.implementation + .createDocument('jabber:client', 'strophe', null); + } + + return doc; + }, + + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Note that these are not HTML DOM elements, which + * aren't appropriate for XMPP stanzas. + * + * Parameters: + * (String) name - The name for the element. + * (Array|Object) attrs - An optional array or object containing + * key/value pairs to use as element attributes. The object should + * be in the format {'key': 'value'} or {key: 'value'}. The array + * should have the format [['key1', 'value1'], ['key2', 'value2']]. + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function (name) + { + if (!name) { return null; } + + var node = null; + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + node = Strophe._xmlGenerator.createElement(name); + + // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + var a, i, k; + for (a = 1; a < arguments.length; a++) { + if (!arguments[a]) { continue; } + if (typeof(arguments[a]) == "string" || + typeof(arguments[a]) == "number") { + node.appendChild(Strophe.xmlTextNode(arguments[a])); + } else if (typeof(arguments[a]) == "object" && + typeof(arguments[a].sort) == "function") { + for (i = 0; i < arguments[a].length; i++) { + if (typeof(arguments[a][i]) == "object" && + typeof(arguments[a][i].sort) == "function") { + node.setAttribute(arguments[a][i][0], + arguments[a][i][1]); + } + } + } else if (typeof(arguments[a]) == "object") { + for (k in arguments[a]) { + if (arguments[a].hasOwnProperty(k)) { + node.setAttribute(k, arguments[a][k]); + } + } + } + } + + return node; + }, + + /* Function: xmlescape + * Excapes invalid xml characters. + * + * Parameters: + * (String) text - text to escape. + * + * Returns: + * Escaped text. + */ + xmlescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(//g, ">"); + return text; + }, + + /** Function: xmlTextNode + * Creates an XML DOM text node. + * + * Provides a cross implementation version of document.createTextNode. + * + * Parameters: + * (String) text - The content of the text node. + * + * Returns: + * A new XML DOM text node. + */ + xmlTextNode: function (text) + { + //ensure text is escaped + text = Strophe.xmlescape(text); + + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + return Strophe._xmlGenerator.createTextNode(text); + }, + + /** Function: getText + * Get the concatenation of all text children of an element. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A String with the concatenated text of all text element children. + */ + getText: function (elem) + { + if (!elem) { return null; } + + var str = ""; + if (elem.childNodes.length === 0 && elem.nodeType == + Strophe.ElementType.TEXT) { + str += elem.nodeValue; + } + + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } + } + + return str; + }, + + /** Function: copyElement + * Copy an XML DOM element. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + copyElement: function (elem) + { + var i, el; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + el = Strophe.xmlElement(elem.tagName); + + for (i = 0; i < elem.attributes.length; i++) { + el.setAttribute(elem.attributes[i].nodeName.toLowerCase(), + elem.attributes[i].value); + } + + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[i])); + } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: escapeNode + * Escape the node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An escaped node (or local part). + */ + escapeNode: function (node) + { + return node.replace(/^\s+|\s+$/g, '') + .replace(/\\/g, "\\5c") + .replace(/ /g, "\\20") + .replace(/\"/g, "\\22") + .replace(/\&/g, "\\26") + .replace(/\'/g, "\\27") + .replace(/\//g, "\\2f") + .replace(/:/g, "\\3a") + .replace(//g, "\\3e") + .replace(/@/g, "\\40"); + }, + + /** Function: unescapeNode + * Unescape a node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An unescaped node (or local part). + */ + unescapeNode: function (node) + { + return node.replace(/\\20/g, " ") + .replace(/\\22/g, '"') + .replace(/\\26/g, "&") + .replace(/\\27/g, "'") + .replace(/\\2f/g, "/") + .replace(/\\3a/g, ":") + .replace(/\\3c/g, "<") + .replace(/\\3e/g, ">") + .replace(/\\40/g, "@") + .replace(/\\5c/g, "\\"); + }, + + /** Function: getNodeFromJid + * Get the node portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the node. + */ + getNodeFromJid: function (jid) + { + if (jid.indexOf("@") < 0) { return null; } + return jid.split("@")[0]; + }, + + /** Function: getDomainFromJid + * Get the domain portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the domain. + */ + getDomainFromJid: function (jid) + { + var bare = Strophe.getBareJidFromJid(jid); + if (bare.indexOf("@") < 0) { + return bare; + } else { + var parts = bare.split("@"); + parts.splice(0, 1); + return parts.join('@'); + } + }, + + /** Function: getResourceFromJid + * Get the resource portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the resource. + */ + getResourceFromJid: function (jid) + { + var s = jid.split("/"); + if (s.length < 2) { return null; } + s.splice(0, 1); + return s.join('/'); + }, + + /** Function: getBareJidFromJid + * Get the bare JID from a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the bare JID. + */ + getBareJidFromJid: function (jid) + { + return jid ? jid.split("/")[0] : null; + }, + + /** Function: log + * User overrideable logging function. + * + * This function is called whenever the Strophe library calls any + * of the logging functions. The default implementation of this + * function does nothing. If client code wishes to handle the logging + * messages, it should override this with + * > Strophe.log = function (level, msg) { + * > (user code here) + * > }; + * + * Please note that data sent and received over the wire is logged + * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput(). + * + * The different levels and their meanings are + * + * DEBUG - Messages useful for debugging purposes. + * INFO - Informational messages. This is mostly information like + * 'disconnect was called' or 'SASL auth succeeded'. + * WARN - Warnings about potential problems. This is mostly used + * to report transient connection errors like request timeouts. + * ERROR - Some error occurred. + * FATAL - A non-recoverable fatal error occurred. + * + * Parameters: + * (Integer) level - The log level of the log message. This will + * be one of the values in Strophe.LogLevel. + * (String) msg - The log message. + */ + log: function (level, msg) + { + return; + }, + + /** Function: debug + * Log a message at the Strophe.LogLevel.DEBUG level. + * + * Parameters: + * (String) msg - The log message. + */ + debug: function(msg) + { + this.log(this.LogLevel.DEBUG, msg); + }, + + /** Function: info + * Log a message at the Strophe.LogLevel.INFO level. + * + * Parameters: + * (String) msg - The log message. + */ + info: function (msg) + { + this.log(this.LogLevel.INFO, msg); + }, + + /** Function: warn + * Log a message at the Strophe.LogLevel.WARN level. + * + * Parameters: + * (String) msg - The log message. + */ + warn: function (msg) + { + this.log(this.LogLevel.WARN, msg); + }, + + /** Function: error + * Log a message at the Strophe.LogLevel.ERROR level. + * + * Parameters: + * (String) msg - The log message. + */ + error: function (msg) + { + this.log(this.LogLevel.ERROR, msg); + }, + + /** Function: fatal + * Log a message at the Strophe.LogLevel.FATAL level. + * + * Parameters: + * (String) msg - The log message. + */ + fatal: function (msg) + { + this.log(this.LogLevel.FATAL, msg); + }, + + /** Function: serialize + * Render a DOM element and all descendants to a String. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The serialized element tree as a String. + */ + serialize: function (elem) + { + var result; + + if (!elem) { return null; } + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + + var nodeName = elem.nodeName; + var i, child; + + if (elem.getAttribute("_realname")) { + nodeName = elem.getAttribute("_realname"); + } + + result = "<" + nodeName; + for (i = 0; i < elem.attributes.length; i++) { + if(elem.attributes[i].nodeName != "_realname") { + result += " " + elem.attributes[i].nodeName.toLowerCase() + + "='" + elem.attributes[i].value + .replace("&", "&") + .replace("'", "'") + .replace("<", "<") + "'"; + } + } + + if (elem.childNodes.length > 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeType == Strophe.ElementType.NORMAL) { + // normal element, so recurse + result += Strophe.serialize(child); + } else if (child.nodeType == Strophe.ElementType.TEXT) { + // text element + result += child.nodeValue; + } + } + result += ""; + } else { + result += "/>"; + } + + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0, + + /** PrivateVariable: Strophe.connectionPlugins + * _Private_ variable Used to store plugin names that need + * initialization on Strophe.Connection construction. + */ + _connectionPlugins: {}, + + /** Function: addConnectionPlugin + * Extends the Strophe.Connection object with the given plugin. + * + * Paramaters: + * (String) name - The name of the extension. + * (Object) ptype - The plugin's prototype. + */ + addConnectionPlugin: function (name, ptype) + { + Strophe._connectionPlugins[name] = ptype; + } +}; + +/** Class: Strophe.Builder + * XML DOM builder. + * + * This object provides an interface similar to JQuery but for building + * DOM element easily and rapidly. All the functions except for toString() + * and tree() return the object, so calls can be chained. Here's an + * example using the $iq() builder helper. + * > $iq({to: 'you', from: 'me', type: 'get', id: '1'}) + * > .c('query', {xmlns: 'strophe:example'}) + * > .c('example') + * > .toString() + * The above generates this XML fragment + * > + * > + * > + * > + * > + * The corresponding DOM manipulations to get a similar fragment would be + * a lot more tedious and probably involve several helper variables. + * + * Since adding children makes new operations operate on the child, up() + * is provided to traverse up the tree. To add two children, do + * > builder.c('child1', ...).up().c('child2', ...) + * The next operation on the Builder will be relative to the second child. + */ + +/** Constructor: Strophe.Builder + * Create a Strophe.Builder object. + * + * The attributes should be passed in object notation. For example + * > var b = new Builder('message', {to: 'you', from: 'me'}); + * or + * > var b = new Builder('messsage', {'xml:lang': 'en'}); + * + * Parameters: + * (String) name - The name of the root element. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder. + */ +Strophe.Builder = function (name, attrs) +{ + // Set correct namespace for jabber:client elements + if (name == "presence" || name == "message" || name == "iq") { + if (attrs && !attrs.xmlns) { + attrs.xmlns = Strophe.NS.CLIENT; + } else if (!attrs) { + attrs = {xmlns: Strophe.NS.CLIENT}; + } + } + + // Holds the tree being built. + this.nodeTree = Strophe.xmlElement(name, attrs); + + // Points to the current operation node. + this.node = this.nodeTree; +}; + +Strophe.Builder.prototype = { + /** Function: tree + * Return the DOM tree. + * + * This function returns the current DOM tree as an element object. This + * is suitable for passing to functions like Strophe.Connection.send(). + * + * Returns: + * The DOM tree as a element object. + */ + tree: function () + { + return this.nodeTree; + }, + + /** Function: toString + * Serialize the DOM tree to a String. + * + * This function returns a string serialization of the current DOM + * tree. It is often used internally to pass data to a + * Strophe.Request object. + * + * Returns: + * The serialized DOM tree in a String. + */ + toString: function () + { + return Strophe.serialize(this.nodeTree); + }, + + /** Function: up + * Make the current parent element the new current element. + * + * This function is often used after c() to traverse back up the tree. + * For example, to add two children to the same element + * > builder.c('child1', {}).up().c('child2', {}); + * + * Returns: + * The Stophe.Builder object. + */ + up: function () + { + this.node = this.node.parentNode; + return this; + }, + + /** Function: attrs + * Add or modify attributes of the current element. + * + * The attributes should be passed in object notation. This function + * does not move the current element pointer. + * + * Parameters: + * (Object) moreattrs - The attributes to add/modify in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + attrs: function (moreattrs) + { + for (var k in moreattrs) { + if (moreattrs.hasOwnProperty(k)) { + this.node.setAttribute(k, moreattrs[k]); + } + } + return this; + }, + + /** Function: c + * Add a child to the current element and make it the new current + * element. + * + * This function moves the current element pointer to the child. If you + * need to add another child, it is necessary to use up() to go back + * to the parent in the tree. + * + * Parameters: + * (String) name - The name of the child. + * (Object) attrs - The attributes of the child in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + c: function (name, attrs) + { + var child = Strophe.xmlElement(name, attrs); + this.node.appendChild(child); + this.node = child; + return this; + }, + + /** Function: cnode + * Add a child to the current element and make it the new current + * element. + * + * This function is the same as c() except that instead of using a + * name and an attributes object to create the child it uses an + * existing DOM element object. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The Strophe.Builder object. + */ + cnode: function (elem) + { + this.node.appendChild(elem); + this.node = elem; + return this; + }, + + /** Function: t + * Add a child text element. + * + * This *does not* make the child the new current element since there + * are no children of text elements. + * + * Parameters: + * (String) text - The text data to append to the current element. + * + * Returns: + * The Strophe.Builder object. + */ + t: function (text) + { + var child = Strophe.xmlTextNode(text); + this.node.appendChild(child); + return this; + } +}; + + +/** PrivateClass: Strophe.Handler + * _Private_ helper class for managing stanza handlers. + * + * A Strophe.Handler encapsulates a user provided callback function to be + * executed when matching stanzas are received by the connection. + * Handlers can be either one-off or persistant depending on their + * return value. Returning true will cause a Handler to remain active, and + * returning false will remove the Handler. + * + * Users will not use Strophe.Handler objects directly, but instead they + * will use Strophe.Connection.addHandler() and + * Strophe.Connection.deleteHandler(). + */ + +/** PrivateConstructor: Strophe.Handler + * Create and initialize a new Strophe.Handler. + * + * Parameters: + * (Function) handler - A function to be executed when the handler is run. + * (String) ns - The namespace to match. + * (String) name - The element name to match. + * (String) type - The element type to match. + * (String) id - The element id attribute to match. + * (String) from - The element from attribute to match. + * (Object) options - Handler options + * + * Returns: + * A new Strophe.Handler object. + */ +Strophe.Handler = function (handler, ns, name, type, id, from, options) +{ + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.options = options || {matchbare: false}; + + // default matchBare to false if undefined + if (!this.options.matchBare) { + this.options.matchBare = false; + } + + if (this.options.matchBare) { + this.from = from ? Strophe.getBareJidFromJid(from) : null; + } else { + this.from = from; + } + + // whether the handler is a user handler or a system handler + this.user = true; +}; + +Strophe.Handler.prototype = { + /** PrivateFunction: isMatch + * Tests if a stanza matches the Strophe.Handler. + * + * Parameters: + * (XMLElement) elem - The XML element to test. + * + * Returns: + * true if the stanza matches and false otherwise. + */ + isMatch: function (elem) + { + var nsMatch; + var from = null; + + if (this.options.matchBare) { + from = Strophe.getBareJidFromJid(elem.getAttribute('from')); + } else { + from = elem.getAttribute('from'); + } + + nsMatch = false; + if (!this.ns) { + nsMatch = true; + } else { + var that = this; + Strophe.forEachChild(elem, null, function (elem) { + if (elem.getAttribute("xmlns") == that.ns) { + nsMatch = true; + } + }); + + nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; + } + + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || elem.getAttribute("type") == this.type) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || from == this.from)) { + return true; + } + + return false; + }, + + /** PrivateFunction: run + * Run the callback on a matching stanza. + * + * Parameters: + * (XMLElement) elem - The DOM element that triggered the + * Strophe.Handler. + * + * Returns: + * A boolean indicating if the handler should remain active. + */ + run: function (elem) + { + var result = null; + try { + result = this.handler(elem); + } catch (e) { + if (e.sourceURL) { + Strophe.fatal("error: " + this.handler + + " " + e.sourceURL + ":" + + e.line + " - " + e.name + ": " + e.message); + } else if (e.fileName) { + if (typeof(console) != "undefined") { + console.trace(); + console.error(this.handler, " - error - ", e, e.message); + } + Strophe.fatal("error: " + this.handler + " " + + e.fileName + ":" + e.lineNumber + " - " + + e.name + ": " + e.message); + } else { + Strophe.fatal("error: " + this.handler); + } + + throw e; + } + + return result; + }, + + /** PrivateFunction: toString + * Get a String representation of the Strophe.Handler object. + * + * Returns: + * A String. + */ + toString: function () + { + return "{Handler: " + this.handler + "(" + this.name + "," + + this.id + "," + this.ns + ")}"; + } +}; + +/** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. + * + * A Strophe.TimedHandler encapsulates a user provided callback that + * should be called after a certain period of time or at regular + * intervals. The return value of the callback determines whether the + * Strophe.TimedHandler will continue to fire. + * + * Users will not use Strophe.TimedHandler objects directly, but instead + * they will use Strophe.Connection.addTimedHandler() and + * Strophe.Connection.deleteTimedHandler(). + */ + +/** PrivateConstructor: Strophe.TimedHandler + * Create and initialize a new Strophe.TimedHandler object. + * + * Parameters: + * (Integer) period - The number of milliseconds to wait before the + * handler is called. + * (Function) handler - The callback to run when the handler fires. This + * function should take no arguments. + * + * Returns: + * A new Strophe.TimedHandler object. + */ +Strophe.TimedHandler = function (period, handler) +{ + this.period = period; + this.handler = handler; + + this.lastCalled = new Date().getTime(); + this.user = true; +}; + +Strophe.TimedHandler.prototype = { + /** PrivateFunction: run + * Run the callback for the Strophe.TimedHandler. + * + * Returns: + * true if the Strophe.TimedHandler should be called again, and false + * otherwise. + */ + run: function () + { + this.lastCalled = new Date().getTime(); + return this.handler(); + }, + + /** PrivateFunction: reset + * Reset the last called time for the Strophe.TimedHandler. + */ + reset: function () + { + this.lastCalled = new Date().getTime(); + }, + + /** PrivateFunction: toString + * Get a string representation of the Strophe.TimedHandler object. + * + * Returns: + * The string representation. + */ + toString: function () + { + return "{TimedHandler: " + this.handler + "(" + this.period +")}"; + } +}; + +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (XMLElement) elem - The XML data to be sent in the request. + * (Function) func - The function that will be called when the + * XMLHttpRequest readyState changes. + * (Integer) rid - The BOSH rid attribute associated with this request. + * (Integer) sends - The number of times this same request has been + * sent. + */ +Strophe.Request = function (elem, func, rid, sends) +{ + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); + // save original function in case we need to make a new request + // from this one. + this.origFunc = func; + this.func = func; + this.rid = rid; + this.date = NaN; + this.sends = sends || 0; + this.abort = false; + this.dead = null; + this.age = function () { + if (!this.date) { return 0; } + var now = new Date(); + return (now - this.date) / 1000; + }; + this.timeDead = function () { + if (!this.dead) { return 0; } + var now = new Date(); + return (now - this.dead) / 1000; + }; + this.xhr = this._newXHR(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function () + { + var node = null; + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + if (node.tagName == "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + throw "parsererror"; + } + } else if (this.xhr.responseText) { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function () + { + var xhr = null; + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } + + xhr.onreadystatechange = this.func.prependArg(this); + + return xhr; + } +}; + + + +if (callback) { + callback(Strophe, $build, $msg, $iq, $pres); +} + +})(function () { + window.Strophe = arguments[0]; + window.$build = arguments[1]; + window.$msg = arguments[2]; + window.$iq = arguments[3]; + window.$pres = arguments[4]; +}); diff --git a/src/strophe/strophe.pubsub.js b/src/strophe/strophe.pubsub.js new file mode 100644 index 0000000..5855716 --- /dev/null +++ b/src/strophe/strophe.pubsub.js @@ -0,0 +1,289 @@ +/* + Copyright 2008, Stanziq Inc. +*/ + +Strophe.addConnectionPlugin('pubsub', { +/* + Extend connection object to have plugin name 'pubsub'. +*/ + _connection: null, + + //The plugin must have the init function. + init: function(conn) { + + this._connection = conn; + + /* + Function used to setup plugin. + */ + + /* extend name space + * NS.PUBSUB - XMPP Publish Subscribe namespace + * from XEP 60. + * + * NS.PUBSUB_SUBSCRIBE_OPTIONS - XMPP pubsub + * options namespace from XEP 60. + */ + Strophe.addNamespace('PUBSUB',"http://jabber.org/protocol/pubsub"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_OPTIONS', + Strophe.NS.PUBSUB+"#subscribe_options"); + Strophe.addNamespace('PUBSUB_ERRORS',Strophe.NS.PUBSUB+"#errors"); + Strophe.addNamespace('PUBSUB_EVENT',Strophe.NS.PUBSUB+"#event"); + Strophe.addNamespace('PUBSUB_OWNER',Strophe.NS.PUBSUB+"#owner"); + Strophe.addNamespace('PUBSUB_AUTO_CREATE', + Strophe.NS.PUBSUB+"#auto-create"); + Strophe.addNamespace('PUBSUB_PUBLISH_OPTIONS', + Strophe.NS.PUBSUB+"#publish-options"); + Strophe.addNamespace('PUBSUB_NODE_CONFIG', + Strophe.NS.PUBSUB+"#node_config"); + Strophe.addNamespace('PUBSUB_CREATE_AND_CONFIGURE', + Strophe.NS.PUBSUB+"#create-and-configure"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_AUTHORIZATION', + Strophe.NS.PUBSUB+"#subscribe_authorization"); + Strophe.addNamespace('PUBSUB_GET_PENDING', + Strophe.NS.PUBSUB+"#get-pending"); + Strophe.addNamespace('PUBSUB_MANAGE_SUBSCRIPTIONS', + Strophe.NS.PUBSUB+"#manage-subscriptions"); + Strophe.addNamespace('PUBSUB_META_DATA', + Strophe.NS.PUBSUB+"#meta-data"); + + }, + /***Function + + Create a pubsub node on the given service with the given node + name. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Dictionary) options - The configuration options for the node. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + createNode: function(jid,service,node,options, call_back) { + + var iqid = this._connection.getUniqueId("pubsubcreatenode"); + + var iq = $iq({from:jid, to:service, type:'set', id:iqid}); + + var c_options = Strophe.xmlElement("configure",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB+"#node_config"); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + for (var i in options) + { + var val = options[i]; + x.appendChild(val); + } + + if(options.length && options.length != 0) + { + c_options.appendChild(x); + } + + iq.c('pubsub', + {xmlns:Strophe.NS.PUBSUB}).c('create', + {node:node}).up().cnode(c_options); + + this._connection.addHandler(call_back, + null, + 'iq', + null, + iqid, + null); + this._connection.send(iq.tree()); + return iqid; + }, + /***Function + Subscribe to a node in order to receive event items. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) options - The configuration options for the node. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + subscribe: function(jid,service,node,options, call_back) { + var subid = this._connection.getUniqueId("subscribenode"); + //create subscription options + var sub_options = Strophe.xmlElement("options",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB_SUBSCRIBE_OPTIONS); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + + if(options && options.length && options.length !== 0) + { + for (var i = 0; i < options.length; i++) + { + var val = options[i]; + x.appendChild(val); + } + sub_options.appendChild(x); + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}).up().cnode(sub_options); + } + else + { + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}); + } + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + return subid; + + }, + /***Function + Unsubscribe from a node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Function) call_back - Used to determine if node + creation was sucessful. + + */ + unsubscribe: function(jid,service,node, call_back) { + + var subid = this._connection.getUniqueId("unsubscribenode"); + + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('unsubscribe', + {node:node,jid:jid}); + + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + + + return subid; + + }, + /***Function + + Publish and item to the given pubsub node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) items - The list of items to be published. + (Function) call_back - Used to determine if node + creation was sucessful. + */ + publish: function(jid, service, node, items, call_back) { + var pubid = this._connection.getUniqueId("publishnode"); + + + var publish_elem = Strophe.xmlElement("publish", + [["node", + node]/*, + ["jid", + jid]*/]); + for (var i in items) + { + var item = Strophe.xmlElement("item",[["id", items[i].id]]); + item.appendChild(items[i].value[0]); + publish_elem.appendChild(item); + } + + var pub = $iq({from:jid, to:service, type:'set', id:pubid}) + pub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).cnode(publish_elem); + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + pubid, + null); + this._connection.send(pub.tree()); + + + return pubid; + }, + + deleteNode: function(jid,service,node,call_back) { + + var subid = this._connection.getUniqueId("deletenode"); + + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB_OWNER }).c('delete', + {node:node}); + + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + + + return subid; + + }, + /*Function: items + Used to retrieve the persistent items from the pubsub node. + + */ + items: function(jid,service,node, maxitems, ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('pubsub', + { xmlns:Strophe.NS.PUBSUB }).c('items',{node:node, 'max_items': maxitems}); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + }, + item: function(jid,service,node, itemid, ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('pubsub', + { xmlns:Strophe.NS.PUBSUB }).c('items',{node:node}).c('item', {id: itemid}); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + } +}); diff --git a/src/strophe/strophe.roster.js b/src/strophe/strophe.roster.js new file mode 100644 index 0000000..dc01405 --- /dev/null +++ b/src/strophe/strophe.roster.js @@ -0,0 +1,186 @@ +// Contact object +function Contact() { + this.name = ""; + this.resources = {}; + this.subscription = "none"; + this.ask = ""; + this.groups = []; +} + +Contact.prototype = { + // compute whether user is online from their + // list of resources + online: function () { + var result = false; + for (var k in this.resources) { + result = true; + break; + } + return result; + } +}; + +// example roster plugin +Strophe.addConnectionPlugin('roster', { + init: function (connection) { + this.connection = connection; + this.contacts = {}; + + Strophe.addNamespace('ROSTER', 'jabber:iq:roster'); + }, + + // called when connection status is changed + statusChanged: function (status) { + if (status === Strophe.Status.CONNECTED) { + this.contacts = {}; + + // set up handlers for updates + this.connection.addHandler(this.rosterChanged.bind(this), + Strophe.NS.ROSTER, "iq", "set"); + this.connection.addHandler(this.presenceChanged.bind(this), + null, "presence"); + + // build and send initial roster query + var roster_iq = $iq({type: "get"}) + .c('query', {xmlns: Strophe.NS.ROSTER}); + + var that = this; + this.connection.sendIQ(roster_iq, function (iq) { + $(iq).find("item").each(function () { + // build a new contact and add it to the roster + var contact = new Contact(); + contact.name = $(this).attr('name') || ""; + contact.subscription = $(this).attr('subscription') || + "none"; + contact.ask = $(this).attr('ask') || ""; + $(this).find("group").each(function () { + contact.groups.push($(this).text()); + }); + that.contacts[$(this).attr('jid')] = contact; + }); + + // let user code know something happened + $(document).trigger('roster_changed', that); + }); + } else if (status === Strophe.Status.DISCONNECTED) { + // set all users offline + for (var contact in this.contacts) { + this.contacts[contact].resources = {}; + } + + // notify user code + $(document).trigger('roster_changed', this); + } + }, + + // called when roster udpates are received + rosterChanged: function (iq) { + var item = $(iq).find('item'); + var jid = item.attr('jid'); + var subscription = item.attr('subscription') || ""; + + if (subscription === "remove") { + // removing contact from roster + delete this.contacts[jid]; + } else if (subscription === "none") { + // adding contact to roster + var contact = new Contact(); + contact.name = item.attr('name') || ""; + item.find("group").each(function () { + contact.groups.push(this.text()); + }); + this.contacts[jid] = contact; + } else { + // modifying contact on roster + var contact = this.contacts[jid]; + contact.name = item.attr('name') || contact.name; + contact.subscription = subscription || contact.subscription; + contact.ask = item.attr('ask') || contact.ask; + contact.groups = []; + item.find("group").each(function () { + contact.groups.push(this.text()); + }); + } + + // acknowledge receipt + this.connection.send($iq({type: "result", id: $(iq).attr('id')})); + + // notify user code of roster changes + $(document).trigger("roster_changed", this); + + return true; + }, + + // called when presence stanzas are received + presenceChanged: function (presence) { + var from = $(presence).attr("from"); + var jid = Strophe.getBareJidFromJid(from); + var resource = Strophe.getResourceFromJid(from); + var ptype = $(presence).attr("type") || "available"; + + if (!this.contacts[jid] || ptype === "error") { + // ignore presence updates from things not on the roster + // as well as error presence + return true; + } + + if (ptype === "unavailable") { + // remove resource, contact went offline + delete this.contacts[jid].resources[resource]; + } else { + // contact came online or changed status + this.contacts[jid].resources[resource] = { + show: $(presence).find("show").text() || "online", + status: $(presence).find("status").text() + }; + } + + // notify user code of roster changes + $(document).trigger("roster_changed", this); + + return true; + }, + + // add a contact to the roster + addContact: function (jid, name, groups) { + var iq = $iq({type: "set"}) + .c("query", {xmlns: Strophe.NS.ROSTER}) + .c("item", {name: name || "", jid: jid}); + if (groups && groups.length > 0) { + $.each(groups, function () { + iq.c("group").t(this).up(); + }); + } + this.connection.sendIQ(iq); + }, + + // delete a contact from the roster + deleteContact: function (jid) { + var iq = $iq({type: "set"}) + .c("query", {xmlns: Strophe.NS.ROSTER}) + .c("item", {jid: jid, subscription: "remove"}); + this.connection.sendIQ(iq); + }, + + + // modify a roster contact + modifyContact: function (jid, name, groups) { + this.addContact(jid, name, groups); + }, + + // subscribe to a new contact's presence + subscribe: function (jid, name, groups) { + this.addContact(jid, name, groups); + + var presence = $pres({to: jid, "type": "subscribe"}); + this.connection.send(presence); + }, + + // unsubscribe from a contact's presence + unsubscribe: function (jid) { + var presence = $pres({to: jid, "type": "unsubscribe"}); + this.connection.send(presence); + + this.deleteContact(jid); + } +}); \ No newline at end of file diff --git a/src/strophe/strophe.websocket.js b/src/strophe/strophe.websocket.js new file mode 100644 index 0000000..68bb4d2 --- /dev/null +++ b/src/strophe/strophe.websocket.js @@ -0,0 +1,1435 @@ + +/** Class: Strophe.WebSocket + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The WebSocket service URL. + * + * Returns: + * A new Strophe.WebSocket object. + */ +Strophe.Status.REBINDFAILED = 9; +Strophe.WebSocket = function (service) +{ + /* The websocket url. */ + this.service = service; + this.ws = null; + this.connect_timeout=300; + + /* The connected JID. */ + this.jid = ""; + /* The current stream ID. */ + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + this._keep_alive_timer = 20000 + this.errors = 0; + + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + this._rebind_success_handler = null; + this._rebind_failure_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); + } + } +}; + +Strophe.WebSocket.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function () + { + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + }, + + /** Function: pause + * UNUSED with websockets + */ + pause: function () + { + return; + }, + + /** Function: resume + * UNUSED with websockets + */ + resume: function () + { + return; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback The connect callback function. + */ + connect: function (jid, pass, callback) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + if(window.WebSocket || window.MozWebSocket){ + try{ + if (window.MozWebSocket) + this.ws = new MozWebSocket(this.service); + else + this.ws = new WebSocket(this.service); + this.ws.onopen = this._send_initial_stream.bind(this); + this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this)); + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._connect_cb.bind(this)); + this.ws.onerror = function(e){ Strophe.error("error : " + e)}; + this.ws.onclose = this._ws_on_close.bind(this); + } catch(e){ + //console.log("exception "+e); + } + + } else{ + throw "no websocket support" + } + + + }, + + /** Function: attach + * UNUSED, use rebind + */ + attach: function(){ return }, + + rebind: function (jid, sid, callback) + { + this.jid = jid; + this.connect_callback = callback; + this.domain = Strophe.getDomainFromJid(this.jid); + this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this)); + if (window.MozWebSocket) + this.ws = new MozWebSocket(this.service); + else + this.ws = new WebSocket(this.service); + this.ws.onopen = this._send_initial_stream.bind(this); + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._rebind_cb.bind(this) + .prependArg(jid).prependArg(sid)); + this.ws.onclose = this._ws_on_close.bind(this); + }, + + save: function(success, failure){ + var push = $iq({type: "set"}).c("push", {xmlns: "p1:push"}) + .c("keepalive", {max: "30"}) + .up() + .c("session", {duration:"1"}); + this.sendIQ(push, success,failure ); + }, + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + xmlInput: function (elem) + { + return; + }, + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + xmlOutput: function (elem) + { + return; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function (elem) + { + var toSend = ""; + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + toSend += Strophe.serialize(elem[i]); + this.xmlOutput(elem); + } + } else if (typeof(elem.tree) === "function") { + toSend = Strophe.serialize(elem.tree()); + this.xmlOutput(elem.tree()); + } else { + toSend = Strophe.serialize(elem); + this.xmlOutput(elem); + } + + this.rawOutput(toSend); + this.ws.send(toSend); + }, + + /** Function: flush + * UNUSED + */ + flush: function () + { + return + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); + + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) {callback(stanza);} + } else if (iqtype == 'error') { + if (errback) { errback(stanza);} + } else { + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; + } + }, null, 'iq', null, id); + + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + + this.send(elem); + + return id; + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); + } + } + } + } + + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); + } + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + + // tell the parent we disconnected + if (this.connected) { + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + if(this.ws.readyState != this.ws.CLOSED) + { + this.ws.close(); + } + }, + + _ws_on_close: function(ev){ + Strophe.info("websocket closed"); + this._doDisconnect(); + }, + + _keep_alive_handler: function(){ + this.ws.send("\n"); + return true; + }, + + _send_initial_stream: function(){ + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + var stream = '' + this.rawOutput(stream); + this.ws.send(stream); + }, + + _get_stream_id: function(onmessage,event){ + elem = event.data + this.rawInput(elem); + if (event.data.match(/id=[\'\"]([^\'\"]+)[\'\"]/)) + this.streamId = RegExp.$1; + this.ws.onmessage = onmessage; + }, + + _parseTree: function(elem){ + try { + if(this._xml_parse == undefined){ + if (window.DOMParser){ + this._xml_parser=new DOMParser(); + this._xml_parse= function(text){ + // Because FF wants valid XML, with correct namespaces ! + return this._xml_parser.parseFromString("" + text + + "", "text/xml") + .documentElement.firstChild; + } + } + else{ // Internet Explorer + this._xml_parse= function(text){ + var _xml_parser=new ActiveXObject("MSXML2.DomDocument"); + _xml_parser.async="false"; + _xml_parser.loadXML("" + text+ ""); + return _xml_parser.documentElement.firstChild; + } + } + } + return this._xml_parse(elem); + } catch (e) {Strophe.error("Error : " + e.message) } + return null; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (event) + { + var elem; + try { + elem = this._parseTree(event.data); + } catch (e) { + if (e != "parsererror") { throw e; } + this.disconnect("strophe-parsererror"); + } + if (elem === null) { return; } + this.xmlInput(elem); + this.rawInput(event.data); + + // remove handlers scheduled for deletion + var i, hand; + while (this.removeHandlers.length > 0) { + hand = this.removeHandlers.pop(); + i = this.handlers.indexOf(hand); + if (i >= 0) { + this.handlers.splice(i, 1); + } + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + return; + } + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = elem.getAttribute("condition"); + conflict = elem.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this.disconnect(); + return; + } + + // send each incoming stanza through the handler chain + var i, newList; + // process handlers + newList = this.handlers; + this.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + if (hand.isMatch(elem) && + (this.authenticated || !hand.user)) { + if (hand.run(elem)) { + this.handlers.push(hand); + } + } else { + this.handlers.push(hand); + } + } + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var stanza = {} + if (this.authenticated) { + stanza = $pres({ + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + this.send(stanza); + this.ws.send(""); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (event) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + this.ws.onmessage=this._dataRecv.bind(this); + var strStanza = event.data; + if (!strStanza) { return; } + stanza = this._parseTree(strStanza) + this.xmlInput(stanza); + this.rawInput(Strophe.serialize(stanza)); + + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = stanza.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.getBareJidFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = Base64.encode(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); + + this.send($iq({ + type: "get", + to: this.domain, + id: "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + + _rebind_cb: function (jid, sid, event){ + this.connected = true; + this.ws.onmessage=this._dataRecv.bind(this); + var strStanza = event.data; + if (!strStanza) { return; } + stanza = this._parseTree(strStanza) + this.xmlInput(stanza); + this.rawInput(Strophe.serialize(stanza)); + var rebinds = stanza.getElementsByTagName("rebind"); + if (rebinds.length > 0){ + this.send($build('rebind',{ + xmlns:"p1:rebind" + }).c("jid", {}).t(jid) + .up() + .c("sid", {}).t(sid).tree()); + this._rebind_success_handler = this._addSysHandler( + this._rebind_success_cb.bind(this), null, + "rebind", null, null); + this._rebind_failure_handler = this._addSysHandler( + this._rebind_failure_cb.bind(this), null, + "failure", null, null); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-rebind-not-supported'); + } + + + }, + + _rebind_success_cb: function(elem){ + Strophe.info("Rebinding succeeded."); + // remove old handlers + this.authenticated=true; + this.connected=true; + this.deleteHandler(this._rebind_failure_handler); + this._rebind_failure_handler = null; + this._changeConnectStatus(Strophe.Status.ATTACHED, null); + return false; + }, + + _rebind_failure_cb: function(elem){ + Strophe.info("Rebinding failed."); + // delete unneeded handlers + if (this._rebind_success_handler) { + this.deleteHandler(this._rebind_success_handler); + this._rebind_success_handler = null; + } + this._changeConnectStatus(Strophe.Status.REBINDFAILED, null); + return false; + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = Base64.decode(Strophe.getText(elem)); + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + this.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + + this._quote(Strophe.getNodeFromJid(this.jid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + this._quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + _quote: function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }, + + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .up() + .c('password').t(this.pass); + + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + // We need the new stream_id + this.ws.onmessage = this._get_stream_id.bind(this) + .prependArg(this._dataRecv.bind(this)); + // we must send an xmpp:restart now + this._send_initial_stream(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, + null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + if (resource) { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .c('resource', {}).t(resource).tree()); + } else { + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .tree()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } else { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + // actually disconnect + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function () + { + var i, thand, since, newList; + + // remove timed handlers that have been scheduled for deletion + while (this.removeTimeds.length > 0) { + thand = this.removeTimeds.pop(); + i = this.timedHandlers.indexOf(thand); + if (i >= 0) { + this.timedHandlers.splice(i, 1); + } + } + + // add timed handlers scheduled for addition + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } + + // call ready timed handlers + var now = new Date().getTime(); + newList = []; + for (i = 0; i < this.timedHandlers.length; i++) { + thand = this.timedHandlers[i]; + if (this.authenticated || !thand.user) { + since = thand.lastCalled + thand.period; + if (since - now <= 0) { + if (thand.run()) { + newList.push(thand); + } + } else { + newList.push(thand); + } + } + } + this.timedHandlers = newList; + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +};