├── .gitignore
├── .idea
├── libraries
│ └── Dart_SDK.xml
├── modules.xml
└── workspace.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
├── app
│ └── src
│ │ └── main
│ │ └── java
│ │ └── io
│ │ └── flutter
│ │ └── plugins
│ │ └── GeneratedPluginRegistrant.java
└── local.properties
├── lib
├── src
│ ├── bosh.dart
│ ├── core.dart
│ ├── enums.dart
│ ├── md5.dart
│ ├── plugins
│ │ ├── administration.dart
│ │ ├── bookmark.dart
│ │ ├── caps.dart
│ │ ├── chat-notifications.dart
│ │ ├── disco.dart
│ │ ├── last-activity.dart
│ │ ├── muc.dart
│ │ ├── pep.dart
│ │ ├── plugins.dart
│ │ ├── privacy.dart
│ │ ├── private-storage.dart
│ │ ├── pubsub.dart
│ │ ├── register.dart
│ │ ├── roster.dart
│ │ └── vcard-temp.dart
│ ├── sessionstorage.dart
│ ├── sha1.dart
│ ├── utils.dart
│ └── websocket.dart
└── strophe.dart
├── pubspec.lock
├── pubspec.yaml
├── strophe.iml
└── test
└── strophe_test.dart
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 | ios/.generated/
9 | ios/Flutter/Generated.xcconfig
10 | ios/Runner/GeneratedPluginRegistrant.*
11 |
--------------------------------------------------------------------------------
/.idea/libraries/Dart_SDK.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.0.1] - TODO: Add release date.
2 |
3 | * TODO: Describe initial release.
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | TODO: Add your license here.
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Strophe-dart is a pure Dart library for speaking XMPP via BOSH (XEP 124 and XEP 206) and WebSockets (RFC 7395).
2 |
3 | Its primary purpose is to enable mobile-based(flutter),desktop and web-based, real-time XMPP applications.
4 |
5 | The book Professional XMPP Programming with JavaScript and jQuery covers Strophe.Js in detail in the context of web applications.
6 |
7 | Caveats
8 |
9 | Bosh and all authenticate mechanisms don't work for the moment .
10 |
11 | License
12 | It is licensed under the MIT license, except for the files sha1.js, base64.js and md5.js, which are licensed as public domain and BSD (see these files for details).
13 |
14 | Author & History
15 | Strophe.js was originally created by Jack Moffitt. It was originally developed for Chesspark, an online chess community based on XMPP technology. It has been cared for and improved over the years and is currently maintained by many people in the community.
--------------------------------------------------------------------------------
/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java:
--------------------------------------------------------------------------------
1 | package io.flutter.plugins;
2 |
3 | import io.flutter.plugin.common.PluginRegistry;
4 |
5 | /**
6 | * Generated file. Do not edit.
7 | */
8 | public final class GeneratedPluginRegistrant {
9 | public static void registerWith(PluginRegistry registry) {
10 | if (alreadyRegisteredWith(registry)) {
11 | return;
12 | }
13 | }
14 |
15 | private static boolean alreadyRegisteredWith(PluginRegistry registry) {
16 | final String key = GeneratedPluginRegistrant.class.getCanonicalName();
17 | if (registry.hasPlugin(key)) {
18 | return true;
19 | }
20 | registry.registrarFor(key);
21 | return false;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/android/local.properties:
--------------------------------------------------------------------------------
1 | sdk.dir=/home/andre/MyInstalled/android-sdk-linux
2 | flutter.sdk=/home/andre/MyInstalled/flutter
3 | flutter.versionName=0.0.5
--------------------------------------------------------------------------------
/lib/src/bosh.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:math';
4 | import 'dart:io' show HttpClient;
5 | import 'package:strophe/src/core.dart';
6 | import 'package:strophe/src/enums.dart';
7 | import 'package:strophe/src/sessionstorage.dart';
8 | import 'package:xml/xml.dart' as xml;
9 |
10 | class StropheBosh extends ServiceType {
11 | StropheConnection _conn;
12 |
13 | int rid;
14 |
15 | String sid;
16 |
17 | int hold;
18 |
19 | int wait;
20 |
21 | int window;
22 |
23 | int errors;
24 |
25 | int inactivity;
26 |
27 | Map lastResponseHeaders;
28 |
29 | List _requests;
30 |
31 | bool disconnecting;
32 |
33 | StropheBosh(StropheConnection connection) {
34 | this._conn = connection;
35 | /* request id for body tags */
36 | this.rid = new Random().nextInt(4294967295);
37 | /* The current session ID. */
38 | this.sid = null;
39 |
40 | // default BOSH values
41 | this.hold = 1;
42 | this.wait = 60;
43 | this.window = 5;
44 | this.errors = 0;
45 | this.inactivity = null;
46 |
47 | this.lastResponseHeaders = null;
48 |
49 | this._requests = [];
50 | }
51 | @override
52 | StropheConnection get conn => null;
53 | /** Variable: strip
54 | *
55 | * BOSH-Connections will have all stanzas wrapped in a tag when
56 | * passed to or .
57 | * To strip this tag, User code can set to "body":
58 | *
59 | * > Strophe.Bosh.prototype.strip = "body";
60 | *
61 | * This will enable stripping of the body tag in both
62 | * and .
63 | */
64 | String strip;
65 |
66 | /** PrivateFunction: _buildBody
67 | * _Private_ helper function to generate the wrapper for BOSH.
68 | *
69 | * Returns:
70 | * A Strophe.Builder with a element.
71 | */
72 | StanzaBuilder _buildBody() {
73 | StanzaBuilder bodyWrap = Strophe.$build(
74 | 'body', {'rid': this.rid++, 'xmlns': Strophe.NS['HTTPBIND']});
75 | if (this.sid != null) {
76 | bodyWrap = bodyWrap.attrs({sid: this.sid});
77 | }
78 | if (this._conn.options['keepalive'] &&
79 | this._conn.sessionCachingSupported()) {
80 | this._cacheSession();
81 | }
82 | return bodyWrap;
83 | }
84 |
85 | /** PrivateFunction: _reset
86 | * Reset the connection.
87 | *
88 | * This function is called by the reset function of the Strophe Connection
89 | */
90 | reset() {
91 | this._reset();
92 | }
93 |
94 | _reset() {
95 | this.rid = new Random().nextInt(4294967295);
96 | this.sid = null;
97 | this.errors = 0;
98 | if (this._conn.sessionCachingSupported()) {
99 | SessionStorage.removeItem('strophe-bosh-session');
100 | }
101 |
102 | this._conn.nextValidRid(this.rid);
103 | }
104 |
105 | /** PrivateFunction: _connect
106 | * _Private_ function that initializes the BOSH connection.
107 | *
108 | * Creates and sends the Request that initializes the BOSH connection.
109 | */
110 | connect([int wait, int hold, String route]) {
111 | _connect(wait, hold, route);
112 | }
113 |
114 | _connect([int wait, int hold, String route]) {
115 | this.wait = wait ?? this.wait;
116 | this.hold = hold ?? this.hold;
117 | this.errors = 0;
118 |
119 | // build the body tag
120 | var body = this._buildBody().attrs({
121 | 'to': this._conn.domain,
122 | "xml:lang": "en",
123 | 'wait': this.wait,
124 | 'hold': this.hold,
125 | 'content': "text/xml; charset=utf-8",
126 | 'ver': "1.6",
127 | "xmpp:version": "1.0",
128 | "xmlns:xmpp": Strophe.NS['BOSH']
129 | });
130 |
131 | if (route != null && route.isNotEmpty) {
132 | body.attrs({route: route});
133 | }
134 | StropheRequest req =
135 | new StropheRequest(body.tree(), null, body.tree().getAttribute("rid"));
136 | req.func = this._onRequestStateChange(this._conn.connectCb, req);
137 | req.origFunc = req.func;
138 |
139 | this._requests.add(req);
140 | this._throttledRequestHandler();
141 | }
142 |
143 | /** PrivateFunction: _attach
144 | * Attach to an already created and authenticated BOSH session.
145 | *
146 | * This function is provided to allow Strophe to attach to BOSH
147 | * sessions which have been created externally, perhaps by a Web
148 | * application. This is often used to support auto-login type features
149 | * without putting user credentials into the page.
150 | *
151 | * Parameters:
152 | * (String) jid - The full JID that is bound by the session.
153 | * (String) sid - The SID of the BOSH session.
154 | * (String) rid - The current RID of the BOSH session. This RID
155 | * will be used by the next request.
156 | * (Function) callback The connect callback function.
157 | * (Integer) wait - The optional HTTPBIND wait value. This is the
158 | * time the server will wait before returning an empty result for
159 | * a request. The default setting of 60 seconds is recommended.
160 | * Other settings will require tweaks to the Strophe.TIMEOUT value.
161 | * (Integer) hold - The optional HTTPBIND hold value. This is the
162 | * number of connections the server will hold at one time. This
163 | * should almost always be set to 1 (the default).
164 | * (Integer) wind - The optional HTTBIND window value. This is the
165 | * allowed range of request ids that are valid. The default is 5.
166 | */
167 | void attach(String jid, String sid, int rid, Function callback, int wait,
168 | int hold, int wind) {
169 | this._attach(jid, sid, rid, callback, wait, hold, wind);
170 | }
171 |
172 | _attach(String jid, String sid, int rid, Function callback, int wait,
173 | int hold, int wind) {
174 | this._conn.jid = jid;
175 | this.sid = sid;
176 | this.rid = rid;
177 |
178 | this._conn.connectCallback = callback;
179 |
180 | this._conn.domain = Strophe.getDomainFromJid(this._conn.jid);
181 |
182 | this._conn.authenticated = true;
183 | this._conn.connected = true;
184 |
185 | this.wait = wait ?? this.wait;
186 | this.hold = hold ?? this.hold;
187 | this.window = wind ?? this.window;
188 |
189 | this._conn.changeConnectStatus(Strophe.Status['ATTACHED'], null);
190 | }
191 |
192 | /** PrivateFunction: _restore
193 | * Attempt to restore a cached BOSH session
194 | *
195 | * Parameters:
196 | * (String) jid - The full JID that is bound by the session.
197 | * This parameter is optional but recommended, specifically in cases
198 | * where prebinded BOSH sessions are used where it's important to know
199 | * that the right session is being restored.
200 | * (Function) callback The connect callback function.
201 | * (Integer) wait - The optional HTTPBIND wait value. This is the
202 | * time the server will wait before returning an empty result for
203 | * a request. The default setting of 60 seconds is recommended.
204 | * Other settings will require tweaks to the Strophe.TIMEOUT value.
205 | * (Integer) hold - The optional HTTPBIND hold value. This is the
206 | * number of connections the server will hold at one time. This
207 | * should almost always be set to 1 (the default).
208 | * (Integer) wind - The optional HTTBIND window value. This is the
209 | * allowed range of request ids that are valid. The default is 5.
210 | */
211 | void restore(String jid, Function callback, int wait, int hold, int wind) {
212 | this._restore(jid, callback, wait, hold, wind);
213 | }
214 |
215 | _restore(String jid, Function callback, int wait, int hold, int wind) {
216 | var session =
217 | JsonCodec().decode(SessionStorage.getItem('strophe-bosh-session'));
218 | if (session != null &&
219 | session.rid &&
220 | session.sid &&
221 | session.jid &&
222 | (jid == null ||
223 | Strophe.getBareJidFromJid(session.jid) ==
224 | Strophe.getBareJidFromJid(jid) ||
225 | // If authcid is null, then it's an anonymous login, so
226 | // we compare only the domains:
227 | ((Strophe.getNodeFromJid(jid) == null) &&
228 | (Strophe.getDomainFromJid(session.jid) == jid)))) {
229 | this._conn.restored = true;
230 | this._attach(
231 | session.jid, session.sid, session.rid, callback, wait, hold, wind);
232 | } else {
233 | throw {
234 | 'name': "StropheSessionError",
235 | 'message': "_restore: no restoreable session."
236 | };
237 | }
238 | }
239 |
240 | /** PrivateFunction: _cacheSession
241 | * _Private_ handler for the beforeunload event.
242 | *
243 | * This handler is used to process the Bosh-part of the initial request.
244 | * Parameters:
245 | * (Request) bodyWrap - The received stanza.
246 | */
247 | void _cacheSession() {
248 | if (this._conn.authenticated) {
249 | if (this._conn.jid != null &&
250 | this._conn.jid.isNotEmpty &&
251 | this.rid != null &&
252 | this.rid != 0 &&
253 | this.sid != null &&
254 | this.sid.isNotEmpty) {
255 | SessionStorage.setItem(
256 | 'strophe-bosh-session',
257 | JsonCodec().encode(
258 | {'jid': this._conn.jid, 'rid': this.rid, 'sid': this.sid}));
259 | }
260 | } else {
261 | SessionStorage.removeItem('strophe-bosh-session');
262 | }
263 | }
264 |
265 | /** PrivateFunction: _connect_cb
266 | * _Private_ handler for initial connection request.
267 | *
268 | * This handler is used to process the Bosh-part of the initial request.
269 | * Parameters:
270 | * (Request) bodyWrap - The received stanza.
271 | */
272 | connectCb(xml.XmlElement bodyWrap) {
273 | this._connectCb(bodyWrap);
274 | }
275 |
276 | _connectCb(xml.XmlElement bodyWrap) {
277 | String typ = bodyWrap.getAttribute("type");
278 | String cond;
279 | List conflict;
280 | if (typ != null && typ == "terminate") {
281 | // an error occurred
282 | cond = bodyWrap.getAttribute("condition");
283 | Strophe.error("BOSH-Connection failed: " + cond);
284 | conflict = bodyWrap.findAllElements("conflict");
285 | if (cond != null) {
286 | if (cond == "remote-stream-error" && conflict.length > 0) {
287 | cond = "conflict";
288 | }
289 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], cond);
290 | } else {
291 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], "unknown");
292 | }
293 | this._conn.doDisconnect(cond);
294 | return Strophe.Status['CONNFAIL'];
295 | }
296 |
297 | // check to make sure we don't overwrite these if _connect_cb is
298 | // called multiple times in the case of missing stream:features
299 | if (this.sid == null || this.sid.isEmpty) {
300 | this.sid = bodyWrap.getAttribute("sid");
301 | }
302 | String wind = bodyWrap.getAttribute('requests');
303 | if (wind != null) {
304 | this.window = int.parse(wind);
305 | }
306 | String hold = bodyWrap.getAttribute('hold');
307 | if (hold != null) {
308 | this.hold = int.parse(hold);
309 | }
310 | String wait = bodyWrap.getAttribute('wait');
311 | if (wait != null) {
312 | this.wait = int.parse(wait);
313 | }
314 | String inactivity = bodyWrap.getAttribute('inactivity');
315 | if (inactivity != null) {
316 | this.inactivity = int.parse(inactivity);
317 | }
318 | }
319 |
320 | /** PrivateFunction: _disconnect
321 | * _Private_ part of Connection.disconnect for Bosh
322 | *
323 | * Parameters:
324 | * (Request) pres - This stanza will be sent before disconnecting.
325 | */
326 | disconnect(xml.XmlElement pres) {
327 | this._disconnect(pres);
328 | }
329 |
330 | _disconnect(xml.XmlElement pres) {
331 | this._sendTerminate(pres);
332 | }
333 |
334 | /** PrivateFunction: _doDisconnect
335 | * _Private_ function to disconnect.
336 | *
337 | * Resets the SID and RID.
338 | */
339 | doDisconnect() {
340 | this._doDisconnect();
341 | }
342 |
343 | _doDisconnect() {
344 | this.sid = null;
345 | this.rid = new Random().nextInt(4294967295);
346 | if (this._conn.sessionCachingSupported()) {
347 | SessionStorage.removeItem('strophe-bosh-session');
348 | }
349 |
350 | this._conn.nextValidRid(this.rid);
351 | }
352 |
353 | /** PrivateFunction: _emptyQueue
354 | * _Private_ function to check if the Request queue is empty.
355 | *
356 | * Returns:
357 | * True, if there are no Requests queued, False otherwise.
358 | */
359 | bool emptyQueue() {
360 | return this._emptyQueue();
361 | }
362 |
363 | bool _emptyQueue() {
364 | return this._requests.length == 0;
365 | }
366 |
367 | /** PrivateFunction: _callProtocolErrorHandlers
368 | * _Private_ function to call error handlers registered for HTTP errors.
369 | *
370 | * Parameters:
371 | * (Request) req - The request that is changing readyState.
372 | */
373 | _callProtocolErrorHandlers(StropheRequest req) {
374 | int reqStatus = this._getRequestStatus(req);
375 | Function err_callback = this._conn.protocolErrorHandlers['HTTP'][reqStatus];
376 | if (err_callback != null) {
377 | err_callback.call(this, reqStatus);
378 | }
379 | }
380 |
381 | /** PrivateFunction: _hitError
382 | * _Private_ function to handle the error count.
383 | *
384 | * Requests are resent automatically until their error count reaches
385 | * 5. Each time an error is encountered, this function is called to
386 | * increment the count and disconnect if the count is too high.
387 | *
388 | * Parameters:
389 | * (Integer) reqStatus - The request status.
390 | */
391 | void _hitError(int reqStatus) {
392 | this.errors++;
393 | Strophe.warn("request errored, status: " +
394 | reqStatus.toString() +
395 | ", number of errors: " +
396 | this.errors.toString());
397 | if (this.errors > 4) {
398 | this._conn.onDisconnectTimeout();
399 | }
400 | }
401 |
402 | /** PrivateFunction: _onDisconnectTimeout
403 | * _Private_ timeout handler for handling non-graceful disconnection.
404 | *
405 | * Cancels all remaining Requests and clears the queue.
406 | */
407 | onDisconnectTimeout() {
408 | this._onDisconnectTimeout();
409 | }
410 |
411 | _onDisconnectTimeout() {
412 | this._abortAllRequests();
413 | }
414 |
415 | /** PrivateFunction: _abortAllRequests
416 | * _Private_ helper function that makes sure all pending requests are aborted.
417 | */
418 | abortAllRequests() {
419 | this._abortAllRequests();
420 | }
421 |
422 | _abortAllRequests() {
423 | StropheRequest req;
424 | while (this._requests.length > 0) {
425 | req = this._requests.removeLast();
426 | req.abort = true;
427 | req.xhr.close();
428 | }
429 | }
430 |
431 | /** PrivateFunction: _onIdle
432 | * _Private_ handler called by Strophe.Connection._onIdle
433 | *
434 | * Sends all queued Requests or polls with empty Request if there are none.
435 | */
436 | onIdle() {
437 | this._onIdle();
438 | }
439 |
440 | _onIdle() {
441 | var data = this._conn.data;
442 | // if no requests are in progress, poll
443 | if (this._conn.authenticated &&
444 | this._requests.length == 0 &&
445 | data.length == 0 &&
446 | !this._conn.disconnecting) {
447 | Strophe.info("no requests during idle cycle, sending " + "blank request");
448 | data.add(null);
449 | }
450 |
451 | if (this._conn.paused) {
452 | return;
453 | }
454 |
455 | if (this._requests.length < 2 && data.length > 0) {
456 | StanzaBuilder body = this._buildBody();
457 | for (int i = 0; i < data.length; i++) {
458 | if (data[i] != null) {
459 | if (data[i] == "restart") {
460 | body.attrs({
461 | 'to': this._conn.domain,
462 | "xml:lang": "en",
463 | "xmpp:restart": "true",
464 | "xmlns:xmpp": Strophe.NS['BOSH']
465 | });
466 | } else {
467 | body.cnode(data[i]).up();
468 | }
469 | }
470 | }
471 | this._conn.data = [];
472 | StropheRequest req = new StropheRequest(
473 | body.tree(), null, body.tree().getAttribute("rid"));
474 | req.func = this._onRequestStateChange(this._conn.dataRecv, req);
475 | req.origFunc = req.func;
476 | this._requests.add(req);
477 | this._throttledRequestHandler();
478 | }
479 |
480 | if (this._requests.length > 0) {
481 | var time_elapsed = this._requests[0].age();
482 | if (this._requests[0].dead != null) {
483 | if (this._requests[0].timeDead() >
484 | (Strophe.SECONDARY_TIMEOUT * this.wait).floor()) {
485 | this._throttledRequestHandler();
486 | }
487 | }
488 |
489 | if (time_elapsed > (Strophe.TIMEOUT * this.wait).floor()) {
490 | Strophe.warn("Request " +
491 | this._requests[0].id.toString() +
492 | " timed out, over " +
493 | (Strophe.TIMEOUT * this.wait).floor().toString() +
494 | " seconds since last activity");
495 | this._throttledRequestHandler();
496 | }
497 | }
498 | }
499 |
500 | /** PrivateFunction: _getRequestStatus
501 | *
502 | * Returns the HTTP status code from a Request
503 | *
504 | * Parameters:
505 | * (Request) req - The Request instance.
506 | * (Integer) def - The default value that should be returned if no
507 | * status value was found.
508 | */
509 | int _getRequestStatus(StropheRequest req, [num def]) {
510 | int reqStatus;
511 | if (req.response != null) {
512 | try {
513 | reqStatus = req.response.statusCode;
514 | } catch (e) {
515 | Strophe.error("Caught an error while retrieving a request's status, " +
516 | "reqStatus: " +
517 | reqStatus.toString());
518 | }
519 | }
520 | if (reqStatus == null) {
521 | reqStatus = def ?? 0;
522 | }
523 | return reqStatus;
524 | }
525 |
526 | /** PrivateFunction: _onRequestStateChange
527 | * _Private_ handler for Request state changes.
528 | *
529 | * This function is called when the XMLHttpRequest readyState changes.
530 | * It contains a lot of error handling logic for the many ways that
531 | * requests can fail, and calls the request callback when requests
532 | * succeed.
533 | *
534 | * Parameters:
535 | * (Function) func - The handler for the request.
536 | * (Request) req - The request that is changing readyState.
537 | */
538 | _onRequestStateChange(Function func, StropheRequest req) {
539 | Strophe.debug("request id " +
540 | req.id.toString() +
541 | "." +
542 | req.sends.toString() +
543 | " state changed to " +
544 | (req.response != null ? req.response.statusCode : "0"));
545 | if (req.abort) {
546 | req.abort = false;
547 | return;
548 | }
549 | if (req.response != null &&
550 | req.response.statusCode != 200 &&
551 | req.response.statusCode != 304) {
552 | // The request is not yet complete
553 | return;
554 | }
555 | int reqStatus = this._getRequestStatus(req);
556 | this.lastResponseHeaders = req.response.headers;
557 | if (this.disconnecting && reqStatus >= 400) {
558 | this._hitError(reqStatus);
559 | this._callProtocolErrorHandlers(req);
560 | return;
561 | }
562 |
563 | bool valid_request = reqStatus > 0 && reqStatus < 500;
564 | bool too_many_retries = req.sends > this._conn.maxRetries;
565 | if (valid_request || too_many_retries) {
566 | // remove from internal queue
567 | this._removeRequest(req);
568 | Strophe.debug(
569 | "request id " + req.id.toString() + " should now be removed");
570 | }
571 |
572 | if (reqStatus == 200) {
573 | // request succeeded
574 | bool reqIs0 = (this._requests[0] == req);
575 | bool reqIs1 = (this._requests[1] == req);
576 | // if request 1 finished, or request 0 finished and request
577 | // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to
578 | // restart the other - both will be in the first spot, as the
579 | // completed request has been removed from the queue already
580 | if (reqIs1 ||
581 | (reqIs0 &&
582 | this._requests.length > 0 &&
583 | this._requests[0].age() >
584 | (Strophe.SECONDARY_TIMEOUT * this.wait).floor())) {
585 | this._restartRequest(0);
586 | }
587 | this._conn.nextValidRid(int.parse(req.rid) + 1);
588 | Strophe.debug("request id " +
589 | req.id.toString() +
590 | "." +
591 | req.sends.toString() +
592 | " got 200");
593 | func(req); // call handler
594 | this.errors = 0;
595 | } else if (reqStatus == 0 ||
596 | (reqStatus >= 400 && reqStatus < 600) ||
597 | reqStatus >= 12000) {
598 | // request failed
599 | Strophe.error("request id " +
600 | req.id.toString() +
601 | "." +
602 | req.sends.toString() +
603 | " error " +
604 | reqStatus.toString() +
605 | " happened");
606 | this._hitError(reqStatus);
607 | this._callProtocolErrorHandlers(req);
608 | if (reqStatus >= 400 && reqStatus < 500) {
609 | this._conn.changeConnectStatus(Strophe.Status['DISCONNECTING'], null);
610 | this._conn.doDisconnect();
611 | }
612 | } else {
613 | Strophe.error("request id " +
614 | req.id.toString() +
615 | "." +
616 | req.sends.toString() +
617 | " error " +
618 | reqStatus.toString() +
619 | " happened");
620 | }
621 |
622 | if (!valid_request && !too_many_retries) {
623 | this._throttledRequestHandler();
624 | } else if (too_many_retries && !this._conn.connected) {
625 | this._conn.changeConnectStatus(Strophe.Status['CONNFAIL'], "giving-up");
626 | }
627 | }
628 |
629 | /** PrivateFunction: _processRequest
630 | * _Private_ function to process a request in the queue.
631 | *
632 | * This function takes requests off the queue and sends them and
633 | * restarts dead requests.
634 | *
635 | * Parameters:
636 | * (Integer) i - The index of the request in the queue.
637 | */
638 | _processRequest(int i) {
639 | StropheRequest req = this._requests[i];
640 | int reqStatus = this._getRequestStatus(req, -1);
641 |
642 | // make sure we limit the number of retries
643 | if (req.sends > this._conn.maxRetries) {
644 | this._conn.onDisconnectTimeout();
645 | return;
646 | }
647 |
648 | var time_elapsed = req.age();
649 | var primaryTimeout = (time_elapsed is num &&
650 | time_elapsed > (Strophe.TIMEOUT * this.wait).floor());
651 | var secondaryTimeout = (req.dead != null &&
652 | req.timeDead() > (Strophe.SECONDARY_TIMEOUT * this.wait).floor());
653 | var requestCompletedWithServerError =
654 | (req.response != null && (reqStatus < 1 || reqStatus >= 500));
655 | if (primaryTimeout || secondaryTimeout || requestCompletedWithServerError) {
656 | if (secondaryTimeout) {
657 | Strophe.error("Request " +
658 | this._requests[i].id.toString() +
659 | " timed out (secondary), restarting");
660 | }
661 | req.abort = true;
662 | req.xhr.close();
663 | this._requests[i] =
664 | new StropheRequest(req.xmlData, req.origFunc, req.rid, req.sends);
665 | req = this._requests[i];
666 | }
667 |
668 | if (req.response == null) {
669 | Strophe.debug("request id " +
670 | req.id.toString() +
671 | "." +
672 | req.sends.toString() +
673 | " posting");
674 |
675 | // Fires the XHR request -- may be invoked immediately
676 | // or on a gradually expanding retry window for reconnects
677 |
678 | // Implement progressive backoff for reconnects --
679 | // First retry (send == 1) should also be instantaneous
680 | if (req.sends > 1) {
681 | // Using a cube of the retry number creates a nicely
682 | // expanding retry window
683 | num backoff =
684 | min((Strophe.TIMEOUT * this.wait).floor(), pow(req.sends, 3)) *
685 | 1000;
686 | new Timer(new Duration(milliseconds: backoff), () {
687 | // XXX: setTimeout should be called only with function expressions (23974bc1)
688 | this._sendFunc(req);
689 | });
690 | } else {
691 | this._sendFunc(req);
692 | }
693 |
694 | req.sends++;
695 |
696 | //if (this._conn.xmlOutput != Strophe.Connection.xmlOutput) {
697 | if (req.xmlData.name == this.strip && req.xmlData.children.length > 0) {
698 | this._conn.xmlOutput(req.xmlData.firstChild);
699 | } else {
700 | this._conn.xmlOutput(req.xmlData);
701 | }
702 | //}
703 | //if (this._conn.rawOutput != Strophe.Connection.rawOutput) {
704 | this._conn.rawOutput(req.data);
705 | //}
706 | } else {
707 | Strophe.debug("_processRequest: " +
708 | (i == 0 ? "first" : "second") +
709 | " request has readyState of " +
710 | (req.response != null ? req.response.reasonPhrase : "0"));
711 | }
712 | }
713 |
714 | _sendFunc(StropheRequest req) {
715 | String contentType;
716 | Map map;
717 | var request;
718 | HttpClient httpClient = HttpClient();
719 | try {
720 | contentType =
721 | this._conn.options['contentType'] ?? "text/xml; charset=utf-8";
722 | request = httpClient.getUrl(Uri.parse(this._conn.service));
723 | request.persistentConnection = this._conn.options['sync'] ? false : true;
724 | map = {"Content-Type": contentType};
725 | if (this._conn.options['withCredentials']) {
726 | map['withCredentials'] = true;
727 | }
728 | } catch (e2) {
729 | Strophe.error("XHR open failed: " + e2.toString());
730 | if (!this._conn.connected) {
731 | this
732 | ._conn
733 | .changeConnectStatus(Strophe.Status['CONNFAIL'], "bad-service");
734 | }
735 | this._conn.disconnect();
736 | return;
737 | }
738 | req.date = new DateTime.now().millisecondsSinceEpoch;
739 | if (this._conn.options['customHeaders']) {
740 | var headers = this._conn.options['customHeaders'];
741 | for (var header in headers) {
742 | map[header] = headers[header];
743 | }
744 | }
745 |
746 | request.bodyFields = map;
747 | /* req.xhr.send(request).then((http.StreamedResponse response) {
748 | req.response = response as http.Response;
749 | }).catchError(() {}); */
750 | }
751 |
752 | /** PrivateFunction: _removeRequest
753 | * _Private_ function to remove a request from the queue.
754 | *
755 | * Parameters:
756 | * (Request) req - The request to remove.
757 | */
758 | void _removeRequest(StropheRequest req) {
759 | Strophe.debug("removing request");
760 | for (int i = this._requests.length - 1; i >= 0; i--) {
761 | if (req == this._requests[i]) {
762 | this._requests.removeAt(i);
763 | }
764 | }
765 | this._throttledRequestHandler();
766 | }
767 |
768 | /** PrivateFunction: _restartRequest
769 | * _Private_ function to restart a request that is presumed dead.
770 | *
771 | * Parameters:
772 | * (Integer) i - The index of the request in the queue.
773 | */
774 | _restartRequest(i) {
775 | var req = this._requests[i];
776 | if (req.dead == null) {
777 | req.dead = new DateTime.now().millisecondsSinceEpoch;
778 | }
779 |
780 | this._processRequest(i);
781 | }
782 |
783 | /** PrivateFunction: _reqToData
784 | * _Private_ function to get a stanza out of a request.
785 | *
786 | * Tries to extract a stanza out of a Request Object.
787 | * When this fails the current connection will be disconnected.
788 | *
789 | * Parameters:
790 | * (Object) req - The Request.
791 | *
792 | * Returns:
793 | * The stanza that was passed.
794 | */
795 | xml.XmlElement reqToData(dynamic req) {
796 | req = req as StropheRequest;
797 | return this._reqToData(req);
798 | }
799 |
800 | xml.XmlElement _reqToData(StropheRequest req) {
801 | try {
802 | return req.getResponse();
803 | } catch (e) {
804 | if (e != "parsererror") {
805 | throw e;
806 | }
807 | this._conn.disconnect("strophe-parsererror");
808 | return null;
809 | }
810 | }
811 |
812 | /** PrivateFunction: _sendTerminate
813 | * _Private_ function to send initial disconnect sequence.
814 | *
815 | * This is the first step in a graceful disconnect. It sends
816 | * the BOSH server a terminate body and includes an unavailable
817 | * presence if authentication has completed.
818 | */
819 | _sendTerminate(pres) {
820 | Strophe.info("_sendTerminate was called");
821 | StanzaBuilder body = this._buildBody().attrs({'type': "terminate"});
822 | if (pres) {
823 | body.cnode(pres.tree());
824 | }
825 | StropheRequest req =
826 | new StropheRequest(body.tree(), null, body.tree().getAttribute("rid"));
827 | req.func = this._onRequestStateChange(this._conn.dataRecv, req);
828 | req.origFunc = req.func;
829 | this._requests.add(req);
830 | this._throttledRequestHandler();
831 | }
832 |
833 | /** PrivateFunction: _send
834 | * _Private_ part of the Connection.send function for BOSH
835 | *
836 | * Just triggers the RequestHandler to send the messages that are in the queue
837 | */
838 | send() {
839 | this._send();
840 | }
841 |
842 | _send() {
843 | if (this._conn.idleTimeout != null) this._conn.idleTimeout.cancel();
844 | this._throttledRequestHandler();
845 |
846 | // XXX: setTimeout should be called only with function expressions (23974bc1)
847 | this._conn.idleTimeout = new Timer(new Duration(milliseconds: 100), () {
848 | this._onIdle();
849 | });
850 | }
851 |
852 | /** PrivateFunction: _sendRestart
853 | *
854 | * Send an xmpp:restart stanza.
855 | */
856 | sendRestart() {
857 | this._sendRestart();
858 | }
859 |
860 | _sendRestart() {
861 | this._throttledRequestHandler();
862 | if (this._conn.idleTimeout != null) this._conn.idleTimeout.cancel();
863 | }
864 |
865 | /** PrivateFunction: _throttledRequestHandler
866 | * _Private_ function to throttle requests to the connection window.
867 | *
868 | * This function makes sure we don't send requests so fast that the
869 | * request ids overflow the connection window in the case that one
870 | * request died.
871 | */
872 | _throttledRequestHandler() {
873 | if (this._requests == null) {
874 | Strophe.debug(
875 | "_throttledRequestHandler called with " + "undefined requests");
876 | } else {
877 | Strophe.debug("_throttledRequestHandler called with " +
878 | this._requests.length.toString() +
879 | " requests");
880 | }
881 |
882 | if (this._requests == null || this._requests.length == 0) {
883 | return;
884 | }
885 |
886 | if (this._requests.length > 0) {
887 | this._processRequest(0);
888 | }
889 | if (this._requests.length > 1 &&
890 | (int.parse(this._requests[0].rid) - int.parse(this._requests[1].rid))
891 | .abs() <
892 | this.window) {
893 | this._processRequest(1);
894 | }
895 | }
896 | }
897 | /** PrivateClass: Request
898 | * _Private_ helper class that provides a cross implementation abstraction
899 | * for a BOSH related XMLHttpRequest.
900 | *
901 | * The Request class is used internally to encapsulate BOSH request
902 | * information. It is not meant to be used from user's code.
903 | */
904 |
905 | /** PrivateConstructor: Request
906 | * Create and initialize a new Request object.
907 | *
908 | * Parameters:
909 | * (XMLElement) elem - The XML data to be sent in the request.
910 | * (Function) func - The function that will be called when the
911 | * XMLHttpRequest readyState changes.
912 | * (Integer) rid - The BOSH rid attribute associated with this request.
913 | * (Integer) sends - The number of times this same request has been sent.
914 | */
915 | class StropheRequest {
916 | int id;
917 |
918 | xml.XmlElement xmlData;
919 |
920 | String data;
921 |
922 | Function origFunc;
923 |
924 | Function func;
925 |
926 | num date;
927 |
928 | String rid;
929 |
930 | int sends;
931 |
932 | bool abort;
933 |
934 | int dead;
935 |
936 | //http.Client
937 | var xhr;
938 | //http.Response
939 | var response;
940 |
941 | StropheRequest(xml.XmlElement elem, Function func, String rid, [int sends]) {
942 | this.id = ++Strophe.requestId;
943 | this.xmlData = elem;
944 | this.data = Strophe.serialize(elem);
945 | // save original function in case we need to make a new request
946 | // from this one.
947 | this.origFunc = func;
948 | this.func = func;
949 | this.rid = rid;
950 | this.date = null;
951 | this.sends = sends ?? 0;
952 | this.abort = false;
953 | this.dead = null;
954 | this.age();
955 | this.xhr = this._newXHR();
956 | }
957 | num timeDead() {
958 | if (this.dead == null) {
959 | return 0;
960 | }
961 | int now = new DateTime.now().millisecondsSinceEpoch;
962 | return (now - this.dead) / 1000;
963 | }
964 |
965 | num age() {
966 | if (this.date == null || this.date == 0) {
967 | return 0;
968 | }
969 | int now = new DateTime.now().millisecondsSinceEpoch;
970 | return (now - this.date) / 1000;
971 | }
972 |
973 | /** PrivateFunction: getResponse
974 | * Get a response from the underlying XMLHttpRequest.
975 | *
976 | * This function attempts to get a response from the request and checks
977 | * for errors.
978 | *
979 | * Throws:
980 | * "parsererror" - A parser error occured.
981 | * "badformat" - The entity has sent XML that cannot be processed.
982 | *
983 | * Returns:
984 | * The DOM element tree of the response.
985 | */
986 | xml.XmlElement getResponse() {
987 | String body = response.body;
988 | xml.XmlElement node = null;
989 | try {
990 | node = xml.parse(body).rootElement;
991 | Strophe.error("responseXML: " + Strophe.serialize(node));
992 | if (node == null) {
993 | throw {'message': 'Parsing produced null node'};
994 | }
995 | } catch (e) {
996 | // if (node.name == "parsererror") {
997 | Strophe.error("invalid response received" + e.toString());
998 | Strophe.error("responseText: " + body);
999 | throw "parsererror";
1000 | //}
1001 | }
1002 | return node;
1003 | }
1004 |
1005 | /** PrivateFunction: _newXHR
1006 | * _Private_ helper function to create XMLHttpRequests.
1007 | *
1008 | * This function creates XMLHttpRequests across all implementations.
1009 | *
1010 | * Returns:
1011 | * A new XMLHttpRequest.
1012 | */
1013 | _newXHR() {
1014 | //return new http.Client();
1015 | }
1016 | }
1017 |
--------------------------------------------------------------------------------
/lib/src/core.dart:
--------------------------------------------------------------------------------
1 | import 'package:strophe/src/bosh.dart';
2 | import 'package:strophe/src/plugins/plugins.dart';
3 | import 'package:strophe/src/websocket.dart';
4 | import 'package:xml/xml.dart' as xml;
5 | import 'package:strophe/src/enums.dart';
6 |
7 | class Strophe {
8 | static const String VERSION = '0.0.1';
9 | /** PrivateConstants: Timeout Values
10 | * Timeout values for error states. These values are in seconds.
11 | * These should not be changed unless you know exactly what you are
12 | * doing.
13 | *
14 | * TIMEOUT - Timeout multiplier. A waiting request will be considered
15 | * failed after Math.floor(TIMEOUT * wait) seconds have elapsed.
16 | * This defaults to 1.1, and with default wait, 66 seconds.
17 | * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where
18 | * Strophe can detect early failure, it will consider the request
19 | * failed if it doesn't return after
20 | * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed.
21 | * This defaults to 0.1, and with default wait, 6 seconds.
22 | */
23 | static const num TIMEOUT = 1.1;
24 | static const num SECONDARY_TIMEOUT = 0.1;
25 | static Map Status = ConnexionStatus;
26 | static Map NS = NAMESPACE;
27 | static const Map ErrorCondition = ERRORSCONDITIONS;
28 | static const Map LogLevel = LOGLEVEL;
29 | static const Map ElementType = ELEMENTTYPE;
30 | static Map XHTML = {
31 | "tags": [
32 | 'a',
33 | 'blockquote',
34 | 'br',
35 | 'cite',
36 | 'em',
37 | 'img',
38 | 'li',
39 | 'ol',
40 | 'p',
41 | 'span',
42 | 'strong',
43 | 'ul',
44 | 'body'
45 | ],
46 | 'attributes': {
47 | 'a': ['href'],
48 | 'blockquote': ['style'],
49 | 'br': [],
50 | 'cite': ['style'],
51 | 'em': [],
52 | 'img': ['src', 'alt', 'style', 'height', 'width'],
53 | 'li': ['style'],
54 | 'ol': ['style'],
55 | 'p': ['style'],
56 | 'span': ['style'],
57 | 'strong': [],
58 | 'ul': ['style'],
59 | 'body': []
60 | },
61 | 'css': [
62 | 'background-color',
63 | 'color',
64 | 'font-family',
65 | 'font-size',
66 | 'font-style',
67 | 'font-weight',
68 | 'margin-left',
69 | 'margin-right',
70 | 'text-align',
71 | 'text-decoration'
72 | ],
73 | /** Function: XHTML.validTag
74 | *
75 | * Utility method to determine whether a tag is allowed
76 | * in the XHTML_IM namespace.
77 | *
78 | * XHTML tag names are case sensitive and must be lower case.
79 | */
80 | 'validTag': (String tag) {
81 | for (int i = 0; i < Strophe.XHTML['tags'].length; i++) {
82 | if (tag == Strophe.XHTML['tags'][i]) {
83 | return true;
84 | }
85 | }
86 | return false;
87 | },
88 | /** Function: XHTML.validAttribute
89 | *
90 | * Utility method to determine whether an attribute is allowed
91 | * as recommended per XEP-0071
92 | *
93 | * XHTML attribute names are case sensitive and must be lower case.
94 | */
95 | 'validAttribute': (String tag, String attribute) {
96 | if (Strophe.XHTML['attributes'][tag] != null &&
97 | Strophe.XHTML['attributes'][tag].length > 0) {
98 | for (int i = 0; i < Strophe.XHTML['attributes'][tag].length; i++) {
99 | if (attribute == Strophe.XHTML['attributes'][tag][i]) {
100 | return true;
101 | }
102 | }
103 | }
104 | return false;
105 | },
106 | 'validCSS': (style) {
107 | for (int i = 0; i < Strophe.XHTML['css'].length; i++) {
108 | if (style == Strophe.XHTML['css'][i]) {
109 | return true;
110 | }
111 | }
112 | return false;
113 | }
114 | };
115 |
116 | /** Function: addNamespace
117 | * This function is used to extend the current namespaces in
118 | * Strophe.NS. It takes a key and a value with the key being the
119 | * name of the new namespace, with its actual value.
120 | * For example:
121 | * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub");
122 | *
123 | * Parameters:
124 | * (String) name - The name under which the namespace will be
125 | * referenced under Strophe.NS
126 | * (String) value - The actual namespace.
127 | */
128 | static void addNamespace(String name, String value) {
129 | Strophe.NS[name] = value;
130 | }
131 |
132 | /** Function: forEachChild
133 | * Map a function over some or all child elements of a given element.
134 | *
135 | * This is a small convenience function for mapping a function over
136 | * some or all of the children of an element. If elemName is null, all
137 | * children will be passed to the function, otherwise only children
138 | * whose tag names match elemName will be passed.
139 | *
140 | * Parameters:
141 | * (XMLElement) elem - The element to operate on.
142 | * (String) elemName - The child element tag name filter.
143 | * (Function) func - The function to apply to each child. This
144 | * function should take a single argument, a DOM element.
145 | */
146 | static void forEachChild(elem, String elemName, Function func) {
147 | if (elem == null) return;
148 | var childNode;
149 | for (int i = 0; i < elem.children.length; i++) {
150 | try {
151 | if (elem.children.elementAt(i) is xml.XmlElement)
152 | childNode = elem.children.elementAt(i);
153 | else if (elem.children.elementAt(i) is xml.XmlDocument)
154 | childNode = elem.rootElement.children.elementAt(i);
155 | } catch (e) {
156 | childNode = null;
157 | }
158 | if (childNode == null) continue;
159 | if (childNode.nodeType == xml.XmlNodeType.ELEMENT &&
160 | (elemName == null || isTagEqual(childNode, elemName))) {
161 | func(childNode);
162 | }
163 | }
164 | }
165 |
166 | /** Function: isTagEqual
167 | * Compare an element's tag name with a string.
168 | *
169 | * This function is case sensitive.
170 | *
171 | * Parameters:
172 | * (XMLElement) el - A DOM element.
173 | * (String) name - The element name.
174 | *
175 | * Returns:
176 | * true if the element's tag name matches _el_, and false
177 | * otherwise.
178 | */
179 | static bool isTagEqual(xml.XmlElement el, String name) {
180 | return el.name.qualified == name;
181 | }
182 |
183 | /** PrivateVariable: _xmlGenerator
184 | * _Private_ variable that caches a DOM document to
185 | * generate elements.
186 | */
187 | static xml.XmlBuilder _xmlGenerator;
188 |
189 | /** PrivateFunction: _makeGenerator
190 | * _Private_ function that creates a dummy XML DOM document to serve as
191 | * an element and text node generator.
192 | */
193 | static xml.XmlBuilder _makeGenerator() {
194 | xml.XmlBuilder builder = new xml.XmlBuilder();
195 | //builder.element('strophe', namespace: 'jabber:client');
196 | return builder;
197 | }
198 |
199 | /** Function: xmlGenerator
200 | * Get the DOM document to generate elements.
201 | *
202 | * Returns:
203 | * The currently used DOM document.
204 | */
205 | static xml.XmlBuilder xmlGenerator() {
206 | //if (Strophe._xmlGenerator == null) {
207 | Strophe._xmlGenerator = Strophe._makeGenerator();
208 | //}
209 | return Strophe._xmlGenerator;
210 | }
211 |
212 | /** Function: xmlElement
213 | * Create an XML DOM element.
214 | *
215 | * This function creates an XML DOM element correctly across all
216 | * implementations. Note that these are not HTML DOM elements, which
217 | * aren't appropriate for XMPP stanzas.
218 | *
219 | * Parameters:
220 | * (String) name - The name for the element.
221 | * (Array|Object) attrs - An optional array or object containing
222 | * key/value pairs to use as element attributes. The object should
223 | * be in the format {'key': 'value'} or {key: 'value'}. The array
224 | * should have the format [['key1', 'value1'], ['key2', 'value2']].
225 | * (String) text - The text child data for the element.
226 | *
227 | * Returns:
228 | * A new XML DOM element.
229 | */
230 | static xml.XmlNode xmlElement(String name, {dynamic attrs, String text}) {
231 | if (name == null || name.isEmpty || name.trim().length == 0) {
232 | return null;
233 | }
234 | if (attrs != null &&
235 | (attrs is! List>) &&
236 | (attrs is! Map)) {
237 | return null;
238 | }
239 | Map attributes = {};
240 | if (attrs != null) {
241 | if (attrs is List>) {
242 | for (int i = 0; i < attrs.length; i++) {
243 | List attr = attrs[i];
244 | if (attr.length == 2 && attr[1] != null && attr.isNotEmpty) {
245 | attributes[attr[0]] = attr[1].toString();
246 | }
247 | }
248 | } else if (attrs is Map) {
249 | List keys = attrs.keys.toList();
250 | for (int i = 0, len = keys.length; i < len; i++) {
251 | String key = keys[i];
252 | if (key != null && key.isNotEmpty && attrs[key] != null) {
253 | attributes[key] = attrs[key].toString();
254 | }
255 | }
256 | }
257 | }
258 | xml.XmlBuilder builder = Strophe.xmlGenerator();
259 | builder.element(name, attributes: attributes, nest: text);
260 | return builder.build();
261 | }
262 | /* Function: xmlescape
263 | * Excapes invalid xml characters.
264 | *
265 | * Parameters:
266 | * (String) text - text to escape.
267 | *
268 | * Returns:
269 | * Escaped text.
270 | */
271 |
272 | static String xmlescape(String text) {
273 | text = text.replaceAll(new RegExp(r'&'), "&");
274 | text = text.replaceAll(new RegExp(r'<'), "<");
275 | text = text.replaceAll(new RegExp(r'>'), ">");
276 | text = text.replaceAll(new RegExp(r"'"), "'");
277 | text = text.replaceAll(new RegExp(r'"'), """);
278 | return text;
279 | }
280 |
281 | /* Function: xmlunescape
282 | * Unexcapes invalid xml characters.
283 | *
284 | * Parameters:
285 | * (String) text - text to unescape.
286 | *
287 | * Returns:
288 | * Unescaped text.
289 | */
290 | static String xmlunescape(String text) {
291 | text = text.replaceAll(new RegExp(r'&'), "&");
292 | text = text.replaceAll(new RegExp(r'<'), "<");
293 | text = text.replaceAll(new RegExp(r'>'), ">");
294 | text = text.replaceAll(new RegExp(r"'"), "'");
295 | text = text.replaceAll(new RegExp(r'"'), '"');
296 | return text;
297 | }
298 |
299 | /** Function: xmlTextNode
300 | * Creates an XML DOM text node.
301 | *
302 | *
303 | * Parameters:
304 | * (String) text - The content of the text node.
305 | *
306 | * Returns:
307 | * A new XML DOM text node.
308 | */
309 | static xml.XmlNode xmlTextNode(String text) {
310 | xml.XmlBuilder builder = Strophe.xmlGenerator();
311 | builder.element('strophe', nest: text);
312 | return builder.build();
313 | }
314 |
315 | /** Function: xmlHtmlNode
316 | * Creates an XML DOM html node.
317 | *
318 | * Parameters:
319 | * (String) html - The content of the html node.
320 | *
321 | * Returns:
322 | * A new XML DOM text node.
323 | */
324 | static xml.XmlNode xmlHtmlNode(String html) {
325 | xml.XmlNode parsed;
326 | try {
327 | parsed = xml.parse(html);
328 | } catch (e) {
329 | parsed = null;
330 | }
331 | return parsed;
332 | }
333 |
334 | /** Function: getText
335 | * Get the concatenation of all text children of an element.
336 | *
337 | * Parameters:
338 | * (XMLElement) elem - A DOM element.
339 | *
340 | * Returns:
341 | * A String with the concatenated text of all text element children.
342 | */
343 | static String getText(xml.XmlNode elem) {
344 | if (elem == null) {
345 | return null;
346 | }
347 | String str = "";
348 | if (elem.children.length == 0 && elem.nodeType == xml.XmlNodeType.TEXT) {
349 | str += elem.toString();
350 | }
351 |
352 | for (int i = 0; i < elem.children.length; i++) {
353 | if (elem.children[i].nodeType == xml.XmlNodeType.TEXT) {
354 | str += elem.children[i].toString();
355 | }
356 | }
357 | return Strophe.xmlescape(str);
358 | }
359 |
360 | /** Function: copyElement
361 | * Copy an XML DOM element.
362 | *
363 | * This function copies a DOM element and all its descendants and returns
364 | * the new copy.
365 | *
366 | * Parameters:
367 | * (XMLElement) elem - A DOM element.
368 | *
369 | * Returns:
370 | * A new, copied DOM element tree.
371 | */
372 | static xml.XmlNode copyElement(xml.XmlNode elem) {
373 | var el = elem;
374 | if (elem.nodeType == xml.XmlNodeType.ELEMENT) {
375 | el = elem.copy();
376 | } else if (elem.nodeType == xml.XmlNodeType.TEXT) {
377 | el = elem;
378 | } else if (elem.nodeType == xml.XmlNodeType.DOCUMENT) {
379 | el = elem.document.rootElement;
380 | }
381 | return el;
382 | }
383 |
384 | /** Function: createHtml
385 | * Copy an HTML DOM element into an XML DOM.
386 | *
387 | * This function copies a DOM element and all its descendants and returns
388 | * the new copy.
389 | *
390 | * Parameters:
391 | * (HTMLElement) elem - A DOM element.
392 | *
393 | * Returns:
394 | * A new, copied DOM element tree.
395 | */
396 | static xml.XmlNode createHtml(xml.XmlNode elem) {
397 | xml.XmlNode el;
398 | String tag;
399 | if (elem.nodeType == xml.XmlNodeType.ELEMENT) {
400 | // XHTML tags must be lower case.
401 | //tag = elem.
402 | if (Strophe.XHTML['validTag'](tag)) {
403 | try {
404 | el = Strophe.copyElement(elem);
405 | } catch (e) {
406 | // invalid elements
407 | el = Strophe.xmlTextNode('');
408 | }
409 | } else {
410 | el = Strophe.copyElement(elem);
411 | }
412 | } else if (elem.nodeType == xml.XmlNodeType.DOCUMENT_FRAGMENT) {
413 | el = Strophe.copyElement(elem);
414 | } else if (elem.nodeType == xml.XmlNodeType.TEXT) {
415 | el = Strophe.xmlTextNode(elem.toString());
416 | }
417 | return el;
418 | }
419 |
420 | /** Function: escapeNode
421 | * Escape the node part (also called local part) of a JID.
422 | *
423 | * Parameters:
424 | * (String) node - A node (or local part).
425 | *
426 | * Returns:
427 | * An escaped node (or local part).
428 | */
429 | static String escapeNode(String node) {
430 | return node
431 | .replaceAll(new RegExp(r"^\s+|\s+$"), '')
432 | .replaceAll(new RegExp(r"\\"), "\\5c")
433 | .replaceAll(new RegExp(r" "), "\\20")
434 | .replaceAll(new RegExp(r'"'), "\\22")
435 | .replaceAll(new RegExp(r'&'), "\\26")
436 | .replaceAll(new RegExp(r"'"), "\\27")
437 | .replaceAll(new RegExp(r'/'), "\\2f")
438 | .replaceAll(new RegExp(r':'), "\\3a")
439 | .replaceAll(new RegExp(r'<'), "\\3c")
440 | .replaceAll(new RegExp(r'>'), "\\3e")
441 | .replaceAll(new RegExp(r'@'), "\\40");
442 | }
443 |
444 | /** Function: unescapeNode
445 | * Unescape a node part (also called local part) of a JID.
446 | *
447 | * Parameters:
448 | * (String) node - A node (or local part).
449 | *
450 | * Returns:
451 | * An unescaped node (or local part).
452 | */
453 | static String unescapeNode(String node) {
454 | return node
455 | .replaceAll(new RegExp(r"\\5c"), "\\")
456 | .replaceAll(new RegExp(r"\\20"), " ")
457 | .replaceAll(new RegExp(r'\\22'), '"')
458 | .replaceAll(new RegExp(r'\\26'), "&")
459 | .replaceAll(new RegExp(r"\\27"), "'")
460 | .replaceAll(new RegExp(r'\\2f'), "/")
461 | .replaceAll(new RegExp(r'\\3a'), ":")
462 | .replaceAll(new RegExp(r'\\3c'), "<")
463 | .replaceAll(new RegExp(r'\\3e'), ">")
464 | .replaceAll(new RegExp(r'\\40'), "@");
465 | }
466 |
467 | /** Function: getNodeFromJid
468 | * Get the node portion of a JID String.
469 | *
470 | * Parameters:
471 | * (String) jid - A JID.
472 | *
473 | * Returns:
474 | * A String containing the node.
475 | */
476 | static String getNodeFromJid(String jid) {
477 | if (jid.indexOf("@") < 0) {
478 | return null;
479 | }
480 | return jid.split("@")[0];
481 | }
482 |
483 | /** Function: getDomainFromJid
484 | * Get the domain portion of a JID String.
485 | *
486 | * Parameters:
487 | * (String) jid - A JID.
488 | *
489 | * Returns:
490 | * A String containing the domain.
491 | */
492 | static String getDomainFromJid(String jid) {
493 | String bare = Strophe.getBareJidFromJid(jid);
494 | if (bare.indexOf("@") < 0) {
495 | return bare;
496 | } else {
497 | List parts = bare.split("@");
498 | parts.removeAt(0);
499 | return parts.join('@');
500 | }
501 | }
502 |
503 | /** Function: getResourceFromJid
504 | * Get the resource portion of a JID String.
505 | *
506 | * Parameters:
507 | * (String) jid - A JID.
508 | *
509 | * Returns:
510 | * A String containing the resource.
511 | */
512 | static String getResourceFromJid(String jid) {
513 | List s = jid.split("/");
514 | if (s.length < 2) {
515 | return null;
516 | }
517 | s.removeAt(0);
518 | return s.join('/');
519 | }
520 |
521 | /** Function: getBareJidFromJid
522 | * Get the bare JID from a JID String.
523 | *
524 | * Parameters:
525 | * (String) jid - A JID.
526 | *
527 | * Returns:
528 | * A String containing the bare JID.
529 | */
530 | static String getBareJidFromJid(String jid) {
531 | return jid != null && jid.isNotEmpty ? jid.split("/")[0] : null;
532 | }
533 |
534 | /** PrivateFunction: _handleError
535 | * _Private_ function that properly logs an error to the console
536 | */
537 | static handleError(Error e) {
538 | if (e.stackTrace != null) {
539 | Strophe.fatal(e.stackTrace.toString());
540 | }
541 | if (e.toString() != null) {
542 | Strophe.fatal("error: " +
543 | e.hashCode.toString() +
544 | " - " +
545 | e.runtimeType.toString() +
546 | ": " +
547 | e.toString());
548 | }
549 | }
550 |
551 | /** Function: log
552 | * User overrideable logging function.
553 | *
554 | * This function is called whenever the Strophe library calls any
555 | * of the logging functions. The default implementation of this
556 | * function logs only fatal errors. If client code wishes to handle the logging
557 | * messages, it should override this with
558 | * > Strophe.log = function (level, msg) {
559 | * > (user code here)
560 | * > };
561 | *
562 | * Please note that data sent and received over the wire is logged
563 | * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
564 | *
565 | * The different levels and their meanings are
566 | *
567 | * DEBUG - Messages useful for debugging purposes.
568 | * INFO - Informational messages. This is mostly information like
569 | * 'disconnect was called' or 'SASL auth succeeded'.
570 | * WARN - Warnings about potential problems. This is mostly used
571 | * to report transient connection errors like request timeouts.
572 | * ERROR - Some error occurred.
573 | * FATAL - A non-recoverable fatal error occurred.
574 | *
575 | * Parameters:
576 | * (Integer) level - The log level of the log message. This will
577 | * be one of the values in Strophe.LogLevel.
578 | * (String) msg - The log message.
579 | */
580 | static log(int level, String msg) {
581 | if (level != Strophe.LogLevel['FATAL']) {
582 | print(msg);
583 | }
584 | }
585 |
586 | /** Function: debug
587 | * Log a message at the Strophe.LogLevel.DEBUG level.
588 | *
589 | * Parameters:
590 | * (String) msg - The log message.
591 | */
592 | static debug(String msg) {
593 | Strophe.log(Strophe.LogLevel['DEBUG'], msg);
594 | }
595 |
596 | /** Function: info
597 | * Log a message at the Strophe.LogLevel.INFO level.
598 | *
599 | * Parameters:
600 | * (String) msg - The log message.
601 | */
602 | static info(String msg) {
603 | Strophe.log(Strophe.LogLevel['INFO'], msg);
604 | }
605 |
606 | /** Function: warn
607 | * Log a message at the Strophe.LogLevel.WARN level.
608 | *
609 | * Parameters:
610 | * (String) msg - The log message.
611 | */
612 | static warn(String msg) {
613 | Strophe.log(Strophe.LogLevel['WARN'], msg);
614 | }
615 |
616 | /** Function: error
617 | * Log a message at the Strophe.LogLevel.ERROR level.
618 | *
619 | * Parameters:
620 | * (String) msg - The log message.
621 | */
622 | static error(String msg) {
623 | Strophe.log(Strophe.LogLevel['ERROR'], msg);
624 | }
625 |
626 | /** Function: fatal
627 | * Log a message at the Strophe.LogLevel.FATAL level.
628 | *
629 | * Parameters:
630 | * (String) msg - The log message.
631 | */
632 | static fatal(String msg) {
633 | Strophe.log(Strophe.LogLevel['FATAL'], msg);
634 | }
635 |
636 | /** Function: serialize
637 | * Render a DOM element and all descendants to a String.
638 | *
639 | * Parameters:
640 | * (XMLElement) elem - A DOM element.
641 | *
642 | * Returns:
643 | * The serialized element tree as a String.
644 | */
645 | static String serialize(xml.XmlNode elem) {
646 | if (elem == null) {
647 | return null;
648 | }
649 |
650 | return elem.toXmlString();
651 | }
652 |
653 | /** PrivateVariable: _requestId
654 | * _Private_ variable that keeps track of the request ids for
655 | * connections.
656 | */
657 | static int requestId = 0;
658 |
659 | /** PrivateVariable: Strophe.connectionPlugins
660 | * _Private_ variable Used to store plugin names that need
661 | * initialization on Strophe.Connection construction.
662 | */
663 | static Map _connectionPlugins = {};
664 | static Map get connectionPlugins {
665 | return _connectionPlugins;
666 | }
667 |
668 | /** Function: addConnectionPlugin
669 | * Extends the Strophe.Connection object with the given plugin.
670 | *
671 | * Parameters:
672 | * (String) name - The name of the extension.
673 | * (Object) ptype - The plugin's prototype.
674 | */
675 | static void addConnectionPlugin(String name, ptype) {
676 | Strophe._connectionPlugins[name] = ptype;
677 | }
678 | /** Class: Strophe.Builder
679 | * XML DOM builder.
680 | *
681 | * This object provides an interface similar to JQuery but for building
682 | * DOM elements easily and rapidly. All the functions except for toString()
683 | * and tree() return the object, so calls can be chained. Here's an
684 | * example using the $iq() builder helper.
685 | * > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
686 | * > .c('query', {xmlns: 'strophe:example'})
687 | * > .c('example')
688 | * > .toString()
689 | *
690 | * The above generates this XML fragment
691 | * >
692 | * >
693 | * >
694 | * >
695 | * >
696 | * The corresponding DOM manipulations to get a similar fragment would be
697 | * a lot more tedious and probably involve several helper variables.
698 | *
699 | * Since adding children makes new operations operate on the child, up()
700 | * is provided to traverse up the tree. To add two children, do
701 | * > builder.c('child1', ...).up().c('child2', ...)
702 | * The next operation on the Builder will be relative to the second child.
703 | */
704 |
705 | /** Constructor: Strophe.Builder
706 | * Create a Strophe.Builder object.
707 | *
708 | * The attributes should be passed in object notation. For example
709 | * > var b = new Builder('message', {to: 'you', from: 'me'});
710 | * or
711 | * > var b = new Builder('messsage', {'xml:lang': 'en'});
712 | *
713 | * Parameters:
714 | * (String) name - The name of the root element.
715 | * (Object) attrs - The attributes for the root element in object notation.
716 | *
717 | * Returns:
718 | * A new Strophe.Builder.
719 | */
720 | static StanzaBuilder Builder(String name, {Map attrs}) {
721 | return new StanzaBuilder(name, attrs);
722 | }
723 | /** PrivateClass: Strophe.Handler
724 | * _Private_ helper class for managing stanza handlers.
725 | *
726 | * A Strophe.Handler encapsulates a user provided callback function to be
727 | * executed when matching stanzas are received by the connection.
728 | * Handlers can be either one-off or persistant depending on their
729 | * return value. Returning true will cause a Handler to remain active, and
730 | * returning false will remove the Handler.
731 | *
732 | * Users will not use Strophe.Handler objects directly, but instead they
733 | * will use Strophe.Connection.addHandler() and
734 | * Strophe.Connection.deleteHandler().
735 | */
736 |
737 | /** PrivateConstructor: Strophe.Handler
738 | * Create and initialize a new Strophe.Handler.
739 | *
740 | * Parameters:
741 | * (Function) handler - A function to be executed when the handler is run.
742 | * (String) ns - The namespace to match.
743 | * (String) name - The element name to match.
744 | * (String) type - The element type to match.
745 | * (String) id - The element id attribute to match.
746 | * (String) from - The element from attribute to match.
747 | * (Object) options - Handler options
748 | *
749 | * Returns:
750 | * A new Strophe.Handler object.
751 | */
752 | static StanzaHandler Handler(Function handler, String ns, String name,
753 | [type, String id, String from, Map options]) {
754 | if (options != null) {
755 | options.putIfAbsent('matchBareFromJid', () => false);
756 | options.putIfAbsent('ignoreNamespaceFragment', () => false);
757 | } else
758 | options = {'matchBareFromJid': false, 'ignoreNamespaceFragment': false};
759 | StanzaHandler stanzaHandler =
760 | new StanzaHandler(handler, ns, name, type, id, options);
761 | // BBB: Maintain backward compatibility with old `matchBare` option
762 | if (stanzaHandler.options['matchBare'] != null) {
763 | Strophe.warn(
764 | 'The "matchBare" option is deprecated, use "matchBareFromJid" instead.');
765 | stanzaHandler.options['matchBareFromJid'] =
766 | stanzaHandler.options['matchBare'];
767 | stanzaHandler.options.remove('matchBare');
768 | }
769 |
770 | if (stanzaHandler.options['matchBareFromJid'] != null &&
771 | stanzaHandler.options['matchBareFromJid'] == true) {
772 | stanzaHandler.from = (from != null && from.isNotEmpty)
773 | ? Strophe.getBareJidFromJid(from)
774 | : null;
775 | } else {
776 | stanzaHandler.from = from;
777 | }
778 | // whether the handler is a user handler or a system handler
779 | stanzaHandler.user = true;
780 | return stanzaHandler;
781 | }
782 | /** PrivateClass: Strophe.TimedHandler
783 | * _Private_ helper class for managing timed handlers.
784 | *
785 | * A Strophe.TimedHandler encapsulates a user provided callback that
786 | * should be called after a certain period of time or at regular
787 | * intervals. The return value of the callback determines whether the
788 | * Strophe.TimedHandler will continue to fire.
789 | *
790 | * Users will not use Strophe.TimedHandler objects directly, but instead
791 | * they will use Strophe.Connection.addTimedHandler() and
792 | * Strophe.Connection.deleteTimedHandler().
793 | */
794 |
795 | /** PrivateConstructor: Strophe.TimedHandler
796 | * Create and initialize a new Strophe.TimedHandler object.
797 | *
798 | * Parameters:
799 | * (Integer) period - The number of milliseconds to wait before the
800 | * handler is called.
801 | * (Function) handler - The callback to run when the handler fires. This
802 | * function should take no arguments.
803 | *
804 | * Returns:
805 | * A new Strophe.TimedHandler object.
806 | */
807 | static StanzaTimedHandler TimedHandler(int period, Function handler) {
808 | StanzaTimedHandler stanzaTimedHandler =
809 | new StanzaTimedHandler(period, handler);
810 | return stanzaTimedHandler;
811 | }
812 |
813 | static StanzaBuilder $build(String name, Map attrs) {
814 | return Strophe.Builder(name, attrs: attrs);
815 | }
816 |
817 | static StanzaBuilder $msg([Map attrs]) {
818 | return Strophe.Builder("message", attrs: attrs);
819 | }
820 |
821 | static StanzaBuilder $iq([Map attrs]) {
822 | return Strophe.Builder("iq", attrs: attrs);
823 | }
824 |
825 | static StanzaBuilder $pres([Map attrs]) {
826 | return Strophe.Builder("presence", attrs: attrs);
827 | }
828 | /** Class: Strophe.Connection
829 | * XMPP Connection manager.
830 | *
831 | * This class is the main part of Strophe. It manages a BOSH or websocket
832 | * connection to an XMPP server and dispatches events to the user callbacks
833 | * as data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, SASL SCRAM-SHA1
834 | * and legacy authentication.
835 | *
836 | * After creating a Strophe.Connection object, the user will typically
837 | * call connect() with a user supplied callback to handle connection level
838 | * events like authentication failure, disconnection, or connection
839 | * complete.
840 | *
841 | * The user will also have several event handlers defined by using
842 | * addHandler() and addTimedHandler(). These will allow the user code to
843 | * respond to interesting stanzas or do something periodically with the
844 | * connection. These handlers will be active once authentication is
845 | * finished.
846 | *
847 | * To send data to the connection, use send().
848 | */
849 |
850 | /** Constructor: Strophe.Connection
851 | * Create and initialize a Strophe.Connection object.
852 | *
853 | * The transport-protocol for this connection will be chosen automatically
854 | * based on the given service parameter. URLs starting with "ws://" or
855 | * "wss://" will use WebSockets, URLs starting with "http://", "https://"
856 | * or without a protocol will use BOSH.
857 | *
858 | * To make Strophe connect to the current host you can leave out the protocol
859 | * and host part and just pass the path, e.g.
860 | *
861 | * > var conn = new Strophe.Connection("/http-bind/");
862 | *
863 | * Options common to both Websocket and BOSH:
864 | * ------------------------------------------
865 | *
866 | * cookies:
867 | *
868 | * The *cookies* option allows you to pass in cookies to be added to the
869 | * document. These cookies will then be included in the BOSH XMLHttpRequest
870 | * or in the websocket connection.
871 | *
872 | * The passed in value must be a map of cookie names and string values.
873 | *
874 | * > { "myCookie": {
875 | * > "value": "1234",
876 | * > "domain": ".example.org",
877 | * > "path": "/",
878 | * > "expires": expirationDate
879 | * > }
880 | * > }
881 | *
882 | * Note that cookies can't be set in this way for other domains (i.e. cross-domain).
883 | * Those cookies need to be set under those domains, for example they can be
884 | * set server-side by making a XHR call to that domain to ask it to set any
885 | * necessary cookies.
886 | *
887 | * mechanisms:
888 | *
889 | * The *mechanisms* option allows you to specify the SASL mechanisms that this
890 | * instance of Strophe.Connection (and therefore your XMPP client) will
891 | * support.
892 | *
893 | * The value must be an array of objects with Strophe.SASLMechanism
894 | * prototypes.
895 | *
896 | * If nothing is specified, then the following mechanisms (and their
897 | * priorities) are registered:
898 | *
899 | * SCRAM-SHA1 - 70
900 | * DIGEST-MD5 - 60
901 | * PLAIN - 50
902 | * OAUTH-BEARER - 40
903 | * OAUTH-2 - 30
904 | * ANONYMOUS - 20
905 | * EXTERNAL - 10
906 | *
907 | * WebSocket options:
908 | * ------------------
909 | *
910 | * If you want to connect to the current host with a WebSocket connection you
911 | * can tell Strophe to use WebSockets through a "protocol" attribute in the
912 | * optional options parameter. Valid values are "ws" for WebSocket and "wss"
913 | * for Secure WebSocket.
914 | * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call
915 | *
916 | * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"});
917 | *
918 | * Note that relative URLs _NOT_ starting with a "/" will also include the path
919 | * of the current site.
920 | *
921 | * Also because downgrading security is not permitted by browsers, when using
922 | * relative URLs both BOSH and WebSocket connections will use their secure
923 | * variants if the current connection to the site is also secure (https).
924 | *
925 | * BOSH options:
926 | * -------------
927 | *
928 | * By adding "sync" to the options, you can control if requests will
929 | * be made synchronously or not. The default behaviour is asynchronous.
930 | * If you want to make requests synchronous, make "sync" evaluate to true.
931 | * > var conn = new Strophe.Connection("/http-bind/", {sync: true});
932 | *
933 | * You can also toggle this on an already established connection.
934 | * > conn.options.sync = true;
935 | *
936 | * The *customHeaders* option can be used to provide custom HTTP headers to be
937 | * included in the XMLHttpRequests made.
938 | *
939 | * The *keepalive* option can be used to instruct Strophe to maintain the
940 | * current BOSH session across interruptions such as webpage reloads.
941 | *
942 | * It will do this by caching the sessions tokens in sessionStorage, and when
943 | * "restore" is called it will check whether there are cached tokens with
944 | * which it can resume an existing session.
945 | *
946 | * The *withCredentials* option should receive a Boolean value and is used to
947 | * indicate wether cookies should be included in ajax requests (by default
948 | * they're not).
949 | * Set this value to true if you are connecting to a BOSH service
950 | * and for some reason need to send cookies to it.
951 | * In order for this to work cross-domain, the server must also enable
952 | * credentials by setting the Access-Control-Allow-Credentials response header
953 | * to "true". For most usecases however this setting should be false (which
954 | * is the default).
955 | * Additionally, when using Access-Control-Allow-Credentials, the
956 | * Access-Control-Allow-Origin header can't be set to the wildcard "*", but
957 | * instead must be restricted to actual domains.
958 | *
959 | * The *contentType* option can be set to change the default Content-Type
960 | * of "text/xml; charset=utf-8", which can be useful to reduce the amount of
961 | * CORS preflight requests that are sent to the server.
962 | *
963 | * Parameters:
964 | * (String) service - The BOSH or WebSocket service URL.
965 | * (Object) options - A hash of configuration options
966 | *
967 | * Returns:
968 | * A new Strophe.Connection object.
969 | */
970 | static StropheConnection Connection(String service, [Map options]) {
971 | StropheConnection stropheConnection =
972 | new StropheConnection(service, options);
973 | return stropheConnection;
974 | }
975 |
976 | static StropheWebSocket Websocket(StropheConnection connection) {
977 | return new StropheWebSocket(connection);
978 | }
979 |
980 | static StropheBosh Bosh(StropheConnection connection) {
981 | return new StropheBosh(connection);
982 | }
983 |
984 | static StropheSASLAnonymous SASLAnonymous = new StropheSASLAnonymous();
985 | static StropheSASLPlain SASLPlain = new StropheSASLPlain();
986 | static StropheSASLMD5 SASLMD5 = new StropheSASLMD5();
987 | static StropheSASLSHA1 SASLSHA1 = new StropheSASLSHA1();
988 | static StropheSASLOAuthBearer SASLOAuthBearer = new StropheSASLOAuthBearer();
989 | static StropheSASLExternal SASLExternal = new StropheSASLExternal();
990 | static StropheSASLXOAuth2 SASLXOAuth2 = new StropheSASLXOAuth2();
991 | }
992 |
--------------------------------------------------------------------------------
/lib/src/md5.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:crypto/crypto.dart';
3 |
4 | class MD5 {
5 | /*
6 | * Convert an array of little-endian words to a string
7 | */
8 | static String binl2str(List bytes) {
9 | return String.fromCharCodes(bytes);
10 | }
11 |
12 | static String binl2hex(List binarray) {
13 | return md5.convert(binarray).toString();
14 | }
15 |
16 | static List coreMd5(String s, int len) {
17 | var bytes = utf8.encode(s); // data being hashed
18 | Digest digest = md5.convert(bytes);
19 | return digest.bytes;
20 | }
21 |
22 | static String hexdigest(String s) {
23 | return binl2hex(coreMd5(s, s.length * 8));
24 | }
25 |
26 | static String hash(String s) {
27 | return binl2str(coreMd5(s, s.length * 8));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/plugins/administration.dart:
--------------------------------------------------------------------------------
1 | import 'package:strophe/src/core.dart';
2 | import 'package:strophe/src/enums.dart';
3 | import 'package:strophe/src/plugins/plugins.dart';
4 |
5 | class AdministrationPlugin extends PluginClass {
6 | @override
7 | init(StropheConnection conn) {
8 | this.connection = conn;
9 | if (Strophe.NS['COMMANDS'] == null)
10 | Strophe.addNamespace('COMMANDS', 'http://jabber.org/protocol/commands');
11 | Strophe.addNamespace('REGISTERED_USERS_NUM',
12 | 'http://jabber.org/protocol/admin#get-registered-users-num');
13 | Strophe.addNamespace('ONLINE_USERS_NUM',
14 | 'http://jabber.org/protocol/admin#get-online-users-num');
15 | }
16 |
17 | getRegisteredUsersNum(Function success, [Function error]) {
18 | String id = this.connection.getUniqueId('get-registered-users-num');
19 | this.connection.sendIQ(
20 | Strophe.$iq({
21 | 'type': 'set',
22 | 'id': id,
23 | 'xml:lang': 'en',
24 | 'to': this.connection.domain
25 | }).c('command', {
26 | 'xmlns': Strophe.NS['COMMANDS'],
27 | 'action': 'execute',
28 | 'node': Strophe.NS['REGISTERED_USERS_NUM']
29 | }).tree(),
30 | success,
31 | error);
32 | return id;
33 | }
34 |
35 | getOnlineUsersNum(Function success, [Function error]) {
36 | String id = this.connection.getUniqueId('get-registered-users-num');
37 | this.connection.sendIQ(
38 | Strophe.$iq({
39 | 'type': 'set',
40 | 'id': id,
41 | 'xml:lang': 'en',
42 | 'to': this.connection.domain
43 | }).c('command', {
44 | 'xmlns': Strophe.NS['COMMANDS'],
45 | 'action': 'execute',
46 | 'node': Strophe.NS['ONLINE_USERS_NUM']
47 | }).tree(),
48 | success,
49 | error);
50 | return id;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/src/plugins/bookmark.dart:
--------------------------------------------------------------------------------
1 | import 'package:strophe/src/core.dart';
2 | import 'package:strophe/src/enums.dart';
3 | import 'package:strophe/src/plugins/plugins.dart';
4 | import 'package:xml/xml.dart' as xml;
5 |
6 | class BookMarkPlugin extends PluginClass {
7 | init(StropheConnection connection) {
8 | this.connection = connection;
9 | Strophe.addNamespace('PRIVATE', 'jabber:iq:private');
10 | Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks');
11 | Strophe.addNamespace('PRIVACY', 'jabber:iq:privacy');
12 | Strophe.addNamespace('DELAY', 'jabber:x:delay');
13 | Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
14 | }
15 |
16 | ///
17 | /// Create private bookmark node.
18 | ///
19 | /// @param {function} [success] - Callback after success
20 | /// @param {function} [error] - Callback after error
21 | ///
22 | bool createBookmarksNode([Function success, Function error]) {
23 | // We do this instead of using publish-options because this is not
24 | // mandatory to implement according to XEP-0060
25 | this.connection.sendIQ(
26 | Strophe.$iq({'type': 'set'})
27 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']})
28 | .c('create', {'node': Strophe.NS['BOOKMARKS']})
29 | .up()
30 | .c('configure')
31 | .c('x', {'xmlns': 'jabber:x:data', 'type': 'submit'})
32 | .c('field', {'var': 'FORM_TYPE', 'type': 'hidden'})
33 | .c('value')
34 | .t('http://jabber.org/protocol/pubsub#node_config')
35 | .up()
36 | .up()
37 | .c('field', {'var': 'pubsub#persist_items'})
38 | .c('value')
39 | .t('1')
40 | .up()
41 | .up()
42 | .c('field', {'var': 'pubsub#access_model'})
43 | .c('value')
44 | .t('whitelist')
45 | .tree(),
46 | success,
47 | error);
48 |
49 | return true;
50 | }
51 |
52 | /**
53 | * Add bookmark to storage or update it.
54 | *
55 | * The specified room is bookmarked into the remote bookmark storage. If the room is
56 | * already bookmarked, then it is updated with the specified arguments.
57 | *
58 | * @param {string} roomJid - The JabberID of the chat roomJid
59 | * @param {string} [alias] - A friendly name for the bookmark
60 | * @param {string} [nick] - The users's preferred roomnick for the chatroom
61 | * @param {boolean} [autojoin=false] - Whether the client should automatically join
62 | * the conference room on login.
63 | * @param {function} [success] - Callback after success
64 | * @param {function} [error] - Callback after error
65 | */
66 | add(String roomJid, String alias,
67 | [String nick, bool autojoin = true, Function success, Function error]) {
68 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'})
69 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', {
70 | 'node': Strophe.NS['BOOKMARKS']
71 | }).c('item', {'id': 'current'}).c(
72 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']});
73 |
74 | Function _bookmarkGroupChat = (bool bookmarkit) {
75 | if (bookmarkit) {
76 | Map conferenceAttr = {
77 | 'jid': roomJid,
78 | 'autojoin': autojoin || false
79 | };
80 |
81 | if (alias != null && alias.isNotEmpty) {
82 | conferenceAttr['name'] = alias;
83 | }
84 |
85 | stanza.c('conference', conferenceAttr);
86 | if (nick != null && nick.isNotEmpty) {
87 | stanza.c('nick').t(nick);
88 | }
89 | }
90 |
91 | this.connection.sendIQ(stanza.tree(), success, error);
92 | };
93 |
94 | this.get((xml.XmlElement s) {
95 | List confs = s.findAllElements('conference').toList();
96 | bool bookmarked = false;
97 | for (int i = 0; i < confs.length; i++) {
98 | Map conferenceAttr = {
99 | 'jid': confs[i].getAttribute('jid'),
100 | 'autojoin': confs[i].getAttribute('autojoin') ?? false
101 | };
102 | String roomName = confs[i].getAttribute('name');
103 | List nickname =
104 | confs[i].findAllElements('nick').toList();
105 |
106 | if (conferenceAttr['jid'] == roomJid) {
107 | // the room is already bookmarked, then update it
108 | bookmarked = true;
109 |
110 | conferenceAttr['autojoin'] = autojoin || false;
111 |
112 | if (alias != null && alias.isNotEmpty) {
113 | conferenceAttr['name'] = alias;
114 | }
115 | stanza.c('conference', conferenceAttr);
116 |
117 | if (nick != null && nick.isNotEmpty) {
118 | stanza.c('nick').t(nick).up();
119 | }
120 | } else {
121 | if (roomName != null && roomName.isNotEmpty) {
122 | conferenceAttr['name'] = roomName;
123 | }
124 | stanza.c('conference', conferenceAttr);
125 |
126 | if (nickname.length == 1) {
127 | stanza.c('nick').t(nickname[0].text).up();
128 | }
129 | }
130 |
131 | stanza.up();
132 | }
133 |
134 | _bookmarkGroupChat(!bookmarked);
135 | }, (xml.XmlElement s) {
136 | if (s.findAllElements('item-not-found').length > 0) {
137 | _bookmarkGroupChat(true);
138 | } else {
139 | error(s);
140 | }
141 | });
142 | }
143 |
144 | /**
145 | * Retrieve all stored bookmarks.
146 | *
147 | * @param {function} [success] - Callback after success
148 | * @param {function} [error] - Callback after error
149 | */
150 | get([Function success, Function error]) {
151 | this.connection.sendIQ(
152 | Strophe.$iq({'type': 'get'})
153 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c(
154 | 'items', {'node': Strophe.NS['BOOKMARKS']}).tree(),
155 | success,
156 | error);
157 | }
158 |
159 | /**
160 | * Delete the bookmark with the given roomJid in the bookmark storage.
161 | *
162 | * The whole remote bookmark storage is just updated by removing the
163 | * bookmark corresponding to the specified room.
164 | *
165 | * @param {string} roomJid - The JabberID of the chat roomJid you want to remove
166 | * @param {function} [success] - Callback after success
167 | * @param {function} [error] - Callback after error
168 | */
169 | delete(String roomJid, [Function success, Function error]) {
170 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'})
171 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', {
172 | 'node': Strophe.NS['BOOKMARKS']
173 | }).c('item', {'id': 'current'}).c(
174 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']});
175 |
176 | this.get((xml.XmlElement s) {
177 | List confs = s.findAllElements('conference').toList();
178 | for (int i = 0; i < confs.length; i++) {
179 | Map conferenceAttr = {
180 | 'jid': confs[i].getAttribute('jid'),
181 | 'autojoin': confs[i].getAttribute('autojoin')
182 | };
183 | if (conferenceAttr['jid'] == roomJid) {
184 | continue;
185 | }
186 | String roomName = confs[i].getAttribute('name');
187 | if (roomName != null && roomName.isNotEmpty) {
188 | conferenceAttr['name'] = roomName;
189 | }
190 | stanza.c('conference', conferenceAttr);
191 | List nickname =
192 | confs[i].findAllElements('nick').toList();
193 | if (nickname.length == 1) {
194 | stanza.c('nick').t(nickname[0].text).up();
195 | }
196 | stanza.up();
197 | }
198 | this.connection.sendIQ(stanza.tree(), success, error);
199 | }, (s) {
200 | error(s);
201 | });
202 | }
203 |
204 | /**
205 | * Update the bookmark with the given roomJid in the bookmark storage.
206 | *
207 | * The whole remote bookmark storage is just updated by updating the
208 | * bookmark corresponding to the specified room.
209 | *
210 | * @param {string} roomJid - The JabberID of the chat roomJid you want to remove
211 | * @param {function} [success] - Callback after success
212 | * @param {function} [error] - Callback after error
213 | */
214 | update(String roomJid, String alias,
215 | [String nick, bool autojoin = true, Function success, Function error]) {
216 | StanzaBuilder stanza = Strophe.$iq({'type': 'set'})
217 | .c('pubsub', {'xmlns': Strophe.NS['PUBSUB']}).c('publish', {
218 | 'node': Strophe.NS['BOOKMARKS']
219 | }).c('item', {'id': 'current'}).c(
220 | 'storage', {'xmlns': Strophe.NS['BOOKMARKS']});
221 |
222 | this.get((xml.XmlElement s) {
223 | List confs = s.findAllElements('conference').toList();
224 | for (int i = 0; i < confs.length; i++) {
225 | Map conferenceAttr = {
226 | 'jid': confs[i].getAttribute('jid'),
227 | 'autojoin': confs[i].getAttribute('autojoin'),
228 | 'name': confs[i].getAttribute('name')
229 | };
230 | if (conferenceAttr['jid'] == roomJid) {
231 | conferenceAttr['autojoin'] = autojoin ?? conferenceAttr['autojoin'];
232 | String roomName = confs[i].getAttribute('name');
233 | if (alias != null && alias.isNotEmpty) roomName = alias;
234 | conferenceAttr['name'] = roomName ?? '';
235 | }
236 | stanza.c('conference', conferenceAttr);
237 | List nickname =
238 | confs[i].findAllElements('nick').toList();
239 | if (nick != null &&
240 | nick.isNotEmpty &&
241 | conferenceAttr['jid'] == roomJid) {
242 | stanza.c('nick').t(nick).up();
243 | } else if (nickname.length == 1) {
244 | stanza.c('nick').t(nickname[0].text).up();
245 | }
246 | stanza.up();
247 | }
248 | this.connection.sendIQ(stanza.tree(), success, error);
249 | }, (s) {
250 | error(s);
251 | });
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/lib/src/plugins/caps.dart:
--------------------------------------------------------------------------------
1 | import 'package:strophe/src/core.dart';
2 | import 'package:strophe/src/enums.dart';
3 | import 'package:strophe/src/plugins/plugins.dart';
4 | import 'package:strophe/src/sha1.dart';
5 |
6 | class CapsPlugin extends PluginClass {
7 | String _hash = 'sha-1';
8 | String _node = 'http://strophe.im/strophejs/';
9 | init(StropheConnection c) {
10 | this.connection = c;
11 | Strophe.addNamespace('CAPS', "http://jabber.org/protocol/caps");
12 | if (this.connection.disco == null) {
13 | throw {'error': "disco plugin required!"};
14 | }
15 | this.connection.disco.addFeature(Strophe.NS['CAPS']);
16 | this.connection.disco.addFeature(Strophe.NS['DISCO_INFO']);
17 | if (this.connection.disco.identities.length == 0) {
18 | return this.connection.disco.addIdentity("client", "pc", "strophejs", "");
19 | }
20 | }
21 |
22 | addFeature(String feature) {
23 | return this.connection.disco.addFeature(feature);
24 | }
25 |
26 | removeFeature(String feature) {
27 | return this.connection.disco.removeFeature(feature);
28 | }
29 |
30 | sendPres() {
31 | StanzaBuilder caps = createCapsNode();
32 | return this.connection.send(Strophe.$pres().cnode(caps.tree()));
33 | }
34 |
35 | StanzaBuilder createCapsNode() {
36 | String node;
37 | if (this.connection.disco.identities.length > 0) {
38 | node = this.connection.disco.identities[0]['name'] ?? "";
39 | } else {
40 | node = this._node;
41 | }
42 | return Strophe.$build("c", {
43 | 'xmlns': Strophe.NS['CAPS'],
44 | 'hash': this._hash,
45 | 'node': node,
46 | 'ver': generateVerificationString()
47 | });
48 | }
49 |
50 | propertySort(List