ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/Net-XMPP2/lib/Net/XMPP2/Writer.pm
Revision: 1.18
Committed: Fri Jul 6 22:22:21 2007 UTC (19 years, 3 months ago) by elmex
Branch: MAIN
Changes since 1.17: +22 -0 lines
Log Message:
implemented dataforms - phew! that was a bullet of work

File Contents

# User Rev Content
1 elmex 1.1 package Net::XMPP2::Writer;
2     use strict;
3     use XML::Writer;
4     use Authen::SASL;
5     use MIME::Base64;
6     use Net::XMPP2::Namespaces qw/xmpp_ns/;
7 elmex 1.18 use Net::XMPP2::Util qw/simxml/;
8 elmex 1.1
9     =head1 NAME
10    
11 elmex 1.17 Net::XMPP2::Writer - "XML" writer for XMPP
12 elmex 1.1
13     =head1 SYNOPSIS
14    
15     use Net::XMPP2::Writer;
16     ...
17    
18 elmex 1.3 =head1 DESCRIPTION
19    
20 elmex 1.5 This module contains some helper functions for writing XMPP "XML",
21 elmex 1.3 which is not real XML at all ;-( I use L<XML::Writer> and tune it
22 elmex 1.5 until it creates "XML" that is accepted by most servers propably
23 elmex 1.16 (all of the XMPP servers I tested should work (jabberd14, jabberd2,
24     ejabberd, googletalk).
25 elmex 1.3
26     I hope the semantics of L<XML::Writer> don't change much over the future,
27     but if they do and you run into problems, please report them!
28    
29 elmex 1.5 The whole "XML" concept of XMPP is fundamentally broken anyway. It's supposed
30 elmex 1.3 to be an subset of XML. But a subset of XML productions is not XML. Strictly
31 elmex 1.5 speaking you need a special XMPP "XML" parser and writer to be 100% conformant.
32 elmex 1.3
33 elmex 1.14 On top of that XMPP B<requires> you to parse these partial "XML" documents.
34 elmex 1.10 But a partial XML document is not well-formed, heck, it's not even a XML document!.
35     And a parser should bail out with an error. But XMPP doesn't care, it just relies on
36     implementation dependend behaviour of chunked parsing modes for SAX parsing.
37     This functionality isn't even specified by the XML recommendation in any way.
38     The recommendation even says that it's undefined what happens if you process
39     not-well-formed XML documents.
40    
41 elmex 1.9 But I try to be as "XML" XMPP conformant as possible (it should be around 99-100%).
42 elmex 1.5 But it's hard to say what XML is conformant, as the specifications of XMPP "XML" and XML
43 elmex 1.3 are contradicting. For example XMPP also says you only have to generated and accept
44     utf-8 encodings of XML, but the XML recommendation says that each parser has
45 elmex 1.14 to accept utf-8 B<and> utf-16. So, what do you do? Do you use a XML conformant parser
46 elmex 1.3 or do you write your own?
47    
48 elmex 1.5 I'm using XML::Parser::Expat because expat knows how to parse broken (aka 'partial')
49     "XML" documents, as XMPP requires. Another argument is that if you capture a XMPP
50 elmex 1.3 conversation to the end, and even if a '</stream:stream>' tag was captured, you
51     wont have a valid XML document. The problem is that you have to resent a <stream> tag
52 elmex 1.10 after TLS and SASL authentication each! Awww... I'm repeating myself.
53 elmex 1.3
54     But well... Net::XMPP2 does it's best with expat to cope with the fundamental brokeness
55 elmex 1.5 of "XML" in XMPP.
56 elmex 1.3
57 elmex 1.5 Back to the issue with "XML" generation: I've discoverd that many XMPP servers (eg.
58 elmex 1.3 jabberd14 and ejabberd) have problems with XML namespaces. Thats the reason why
59 elmex 1.9 I'm assigning the namespace prefixes manually: The servers just don't accept validly
60 elmex 1.3 namespaced XML. The draft 3921bis does even state that a client SHOULD generate a 'stream'
61     prefix for the <stream> tag.
62    
63 elmex 1.5 I advice you to explictly set the namespaces too if you generate "XML" for XMPP yourself,
64 elmex 1.3 at least until all or most of the XMPP servers have been fixed. Which might take some
65     years :-) And maybe will happen never.
66    
67 elmex 1.5 And another note: As XMPP requires all predefined entity characters to be escaped
68     in character data you need a "XML" writer that will escape everything:
69    
70     RFC 3920 - 11.1. Restrictions:
71    
72     character data or attribute values containing unescaped characters
73     that map to the predefined entities (Section 4.6 therein);
74     such characters MUST be escaped
75    
76     This means:
77     You have to escape '>' in the character data. I don't know whether XML::Writer
78 elmex 1.9 does that. And I honestly don't care much about this. XMPP is broken by design and
79     I have barely time to writer my own XML parsers and writers to suit their sick taste
80     of "XML". (Do I repeat myself?)
81 elmex 1.5
82 elmex 1.10 I would be happy if they finally say (in RFC3920): "XMPP is NOT XML. It's just
83     XML-like, and some XML utilities allow you to process this kind of XML.".
84    
85 elmex 1.1 =head1 METHODS
86    
87 elmex 1.16 =over 4
88    
89     =item B<new (%args)>
90 elmex 1.1
91     This methods takes following arguments:
92    
93     =over 4
94    
95     =item write_cb
96    
97     The callback that is called when a XML stanza was completly written
98     and is ready for transfer. The first argument of the callback
99     will be the character data to send to the socket.
100    
101 elmex 1.9 =back
102    
103 elmex 1.1 And calls C<init>.
104    
105     =cut
106    
107     sub new {
108     my $this = shift;
109     my $class = ref($this) || $this;
110     my $self = { write_cb => sub {}, @_ };
111     bless $self, $class;
112     $self->init;
113     return $self;
114     }
115    
116 elmex 1.16 =item B<init>
117 elmex 1.1
118     (Re)initializes the writer.
119    
120     =cut
121    
122     sub init {
123     my ($self) = @_;
124     $self->{write_buf} = "";
125     $self->{writer} = XML::Writer->new (OUTPUT => \$self->{write_buf}, NAMESPACES => 1);
126     }
127    
128 elmex 1.16 =item B<flush ()>
129 elmex 1.1
130     This method flushes the internal write buffer and will invoke the C<write_cb>
131     callback. (see also C<new ()> above)
132    
133     =cut
134    
135     sub flush {
136     my ($self) = @_;
137     $self->{write_cb}->(substr $self->{write_buf}, 0, (length $self->{write_buf}), '');
138     }
139    
140 elmex 1.16 =item B<send_init_stream ($domain)>
141 elmex 1.1
142     This method will generate a XMPP stream header. C<$domain> has to be the
143     domain of the server (or endpoint) we want to connect to.
144    
145     =cut
146    
147     sub send_init_stream {
148     my ($self, $language, $domain) = @_;
149    
150     my $w = $self->{writer};
151     $w->xmlDecl ('UTF-8');
152     $w->addPrefix (xmpp_ns ('stream'), 'stream');
153     $w->addPrefix (xmpp_ns ('client'), '');
154 elmex 1.9 $w->forceNSDecl (xmpp_ns ('client'));
155 elmex 1.1 $w->startTag (
156     [xmpp_ns ('stream'), 'stream'],
157     to => $domain,
158     version => '1.0',
159     [xmpp_ns ('xml'), 'lang'] => $language
160     );
161     $self->flush;
162     }
163    
164 elmex 1.16 =item B<send_end_of_stream>
165 elmex 1.1
166     Sends end of the stream.
167    
168     =cut
169    
170     sub send_end_of_stream {
171     my ($self) = @_;
172     my $w = $self->{writer};
173     $w->endTag ([xmpp_ns ('stream'), 'stream']);
174     $self->flush;
175     }
176    
177 elmex 1.16 =item B<send_sasl_auth ($mechanisms)>
178 elmex 1.1
179     This methods sends the start of a SASL authentication. C<$mechanisms> is
180     a string with space seperated mechanisms that are supported by the other
181     end.
182    
183     =cut
184    
185     sub send_sasl_auth {
186     my ($self, $mechanisms, $user, $domain, $pass) = @_;
187    
188     my $sasl = Authen::SASL->new (
189     mechanism => $mechanisms,
190     callback => {
191 elmex 1.15 # XXX: removed authname, because it ensures maximum connectivitiy
192     # along multiple server implementations - XMPP is such a crap
193     # authname => $user . '@' . $domain,
194 elmex 1.1 user => $user,
195     pass => $pass,
196     }
197     );
198    
199     $self->{sasl} = $sasl->client_new ('xmpp', $domain);
200    
201     my $w = $self->{writer};
202     $w->addPrefix (xmpp_ns ('sasl'), '');
203     $w->startTag ([xmpp_ns ('sasl'), 'auth'], mechanism => $self->{sasl}->mechanism);
204     $w->characters (MIME::Base64::encode_base64 ($self->{sasl}->client_start, ''));
205     $w->endTag;
206     $self->flush;
207     }
208    
209 elmex 1.16 =item B<send_sasl_response ($challenge)>
210 elmex 1.1
211     This method generated the SASL authentication response to a C<$challenge>.
212     You must not call this method without calling C<send_sasl_auth ()> before.
213    
214     =cut
215    
216     sub send_sasl_response {
217     my ($self, $challenge) = @_;
218     $challenge = MIME::Base64::decode_base64 ($challenge);
219     my $ret = '';
220     unless ($challenge =~ /rspauth=/) { # rspauth basically means: we are done
221     $ret = $self->{sasl}->client_step ($challenge);
222     unless ($ret) {
223     die "Error in SASL authentication in client step with challenge: '$challenge'\n";
224     }
225     }
226     my $w = $self->{writer};
227     $w->addPrefix (xmpp_ns ('sasl'), '');
228     $w->startTag ([xmpp_ns ('sasl'), 'response']);
229     $w->characters (MIME::Base64::encode_base64 ($ret, ''));
230     $w->endTag;
231     $self->flush;
232     }
233    
234 elmex 1.16 =item B<send_starttls>
235 elmex 1.2
236     Sends the starttls command to the server.
237    
238     =cut
239    
240     sub send_starttls {
241     my ($self) = @_;
242     my $w = $self->{writer};
243     $w->addPrefix (xmpp_ns ('tls'), '');
244     $w->emptyTag ([xmpp_ns ('tls'), 'starttls']);
245     $self->flush;
246     }
247    
248 elmex 1.16 =item B<send_iq ($id, $type, $create_cb, %attrs)>
249 elmex 1.1
250 elmex 1.3 This method sends an IQ stanza of type C<$type> (to be compliant
251     only use: 'get', 'set', 'result' and 'error'). C<$create_cb>
252 elmex 1.1 will be called with an XML::Writer instance as first argument.
253     C<$create_cb> should be used to fill the IQ xml stanza.
254 elmex 1.3 If C<$create_cb> is undefined an empty tag will be generated.
255 elmex 1.1
256     C<%attrs> should have further attributes for the IQ stanza tag.
257     For example 'to' or 'from'. If the C<%attrs> contain a 'lang' attribute
258     it will be put into the 'xml' namespace.
259    
260 elmex 1.3 C<$id> is the id to give this IQ stanza and is mandatory in this API.
261 elmex 1.1
262     =cut
263    
264     sub send_iq {
265     my ($self, $id, $type, $create_cb, %attrs) = @_;
266 elmex 1.18
267     $create_cb = _trans_create_cb ($create_cb);
268    
269 elmex 1.1 my $w = $self->{writer};
270     $w->addPrefix (xmpp_ns ('bind'), '');
271     my (@from) = ($self->{jid} ? (from => $self->{jid}) : ());
272     if ($attrs{lang}) {
273     push @from, ([ xmpp_ns ('xml'), 'lang' ] => delete $attrs{leng})
274     }
275 elmex 1.6 push @from, (id => $id) if defined $id;
276 elmex 1.3 if (defined $create_cb) {
277 elmex 1.6 $w->startTag ('iq', type => $type, @from, %attrs);
278 elmex 1.3 $create_cb->($w);
279     $w->endTag;
280     } else {
281 elmex 1.6 $w->emptyTag ('iq', type => $type, @from, %attrs);
282 elmex 1.3 }
283     $self->flush;
284     }
285    
286 elmex 1.16 =item B<send_presence ($id, $type, $create_cb, %attrs)>
287 elmex 1.3
288     Sends a presence stanza.
289    
290     C<$create_cb> has the same meaning as for C<send_iq>.
291     C<%attrs> will let you pass further optional arguments like 'to'.
292    
293     C<$type> is the type of the presence, which may be one of:
294    
295     unavailable, subscribe, subscribed, unsubscribe, unsubscribed, probe, error
296    
297 elmex 1.18 Or undef, in case you want to send a 'normal' presence.
298 elmex 1.3 Or something completly different if you don't like the RFC 3921 :-)
299    
300     C<%attrs> contains further attributes for the presence tag or may contain one of the
301     following exceptional keys:
302    
303     If C<%attrs> contains a 'show' key: a child xml tag with that name will be geenerated
304     with the value as the content, which should be one of 'away', 'chat', 'dnd' and 'xa'.
305    
306     If C<%attrs> contains a 'status' key: a child xml tag with that name will be generated
307     with the value as content. If the value of the 'status' key is an hash reference
308     the keys will be interpreted as language identifiers for the xml:lang attribute
309     of each status element. If one of these keys is the empty string '' no xml:lang attribute
310     will be generated for it. The values will be the character content of the status tags.
311    
312     If C<%attrs> contains a 'priority' key: a child xml tag with that name will be generated
313     with the value as content, which must be a number between -128 and +127.
314    
315     Note: If C<$create_cb> is undefined and one of the above attributes (show,
316     status or priority) were given, the generates presence tag won't be empty.
317    
318     =cut
319    
320     sub _generate_key_xml {
321     my ($w, $key, $value) = @_;
322     $w->startTag ($key);
323     $w->characters ($value);
324 elmex 1.1 $w->endTag;
325     }
326    
327 elmex 1.3 sub _generate_key_xmls {
328     my ($w, $key, $value) = @_;
329     if (ref ($value) eq 'HASH') {
330     for (keys %$value) {
331 elmex 1.11 $w->startTag ($key, ($_ ne '' ? ([xmpp_ns ('xml'), 'lang'] => $_) : ()));
332 elmex 1.3 $w->characters ($value->{$_});
333     $w->endTag;
334     }
335     } else {
336     $w->startTag ($key);
337     $w->characters ($value);
338     $w->endTag;
339     }
340 elmex 1.1 }
341    
342 elmex 1.18 sub _trans_create_cb {
343     my ($cb) = @_;
344     return unless defined $cb;
345     if (ref ($cb) eq 'HASH') {
346     my $args = $cb;
347     $cb = sub {
348     my ($w) = @_;
349     simxml ($w, %$args);
350     }
351     }
352     $cb
353     }
354    
355 elmex 1.1 sub send_presence {
356 elmex 1.4 my ($self, $id, $type, $create_cb, %attrs) = @_;
357 elmex 1.3
358 elmex 1.18 $create_cb = _trans_create_cb ($create_cb);
359    
360 elmex 1.1 my $w = $self->{writer};
361     $w->addPrefix (xmpp_ns ('client'), '');
362 elmex 1.3
363     my @add;
364 elmex 1.4 push @add, (type => $type) if defined $type;
365     push @add, (id => $id) if defined $id;
366 elmex 1.3
367 elmex 1.7 my %fattrs =
368     map { $_ => $attrs{$_} }
369 elmex 1.12 grep { my $k = $_; not grep { $k eq $_ } qw/show priority status/ }
370 elmex 1.7 keys %attrs;
371    
372 elmex 1.3 if (defined $create_cb) {
373 elmex 1.7 $w->startTag ('presence', @add, %fattrs);
374     _generate_key_xml ($w, show => $attrs{show}) if defined $attrs{show};
375     _generate_key_xml ($w, priority => $attrs{priority}) if defined $attrs{priority};
376     _generate_key_xmls ($w, status => $attrs{status}) if defined $attrs{status};
377 elmex 1.3 $create_cb->($w);
378     $w->endTag;
379     } else {
380     if (exists $attrs{show} or $attrs{priority} or $attrs{status}) {
381 elmex 1.7 $w->startTag ('presence', @add, %fattrs);
382     _generate_key_xml ($w, show => $attrs{show}) if defined $attrs{show};
383     _generate_key_xml ($w, priority => $attrs{priority}) if defined $attrs{priority};
384     _generate_key_xmls ($w, status => $attrs{status}) if defined $attrs{status};
385 elmex 1.3 $w->endTag;
386     } else {
387 elmex 1.7 $w->emptyTag ('presence', @add, %fattrs);
388 elmex 1.3 }
389     }
390    
391 elmex 1.1 $self->flush;
392     }
393    
394 elmex 1.16 =item B<send_message ($id, $to, $type, $create_cb, %attrs)>
395 elmex 1.3
396     Sends a message stanza.
397    
398     C<$to> is the destination JID of the message. C<$type> is
399 elmex 1.6 the type of the message, and if C<$type> is undefined it will default to 'chat'.
400 elmex 1.3 C<$type> must be one of the following: 'chat', 'error', 'groupchat', 'headline'
401     or 'normal'.
402    
403     C<$create_cb> has the same meaning as in C<send_iq>.
404    
405     C<%attrs> contains further attributes for the message tag or may contain one of the
406     following exceptional keys:
407    
408     If C<%attrs> contains a 'body' key: a child xml tag with that name will be generated
409     with the value as content. If the value of the 'body' key is an hash reference
410     the keys will be interpreted as language identifiers for the xml:lang attribute
411     of each body element. If one of these keys is the empty string '' no xml:lang attribute
412     will be generated for it. The values will be the character content of the body tags.
413    
414     If C<%attrs> contains a 'subject' key: a child xml tag with that name will be generated
415     with the value as content. If the value of the 'subject' key is an hash reference
416     the keys will be interpreted as language identifiers for the xml:lang attribute
417     of each subject element. If one of these keys is the empty string '' no xml:lang attribute
418     will be generated for it. The values will be the character content of the subject tags.
419    
420     If C<%attrs> contains a 'thread' key: a child xml tag with that name will be generated
421     and the value will be the character content.
422    
423     =cut
424    
425 elmex 1.1 sub send_message {
426 elmex 1.4 my ($self, $id, $to, $type, $create_cb, %attrs) = @_;
427 elmex 1.3
428 elmex 1.18 $create_cb = _trans_create_cb ($create_cb);
429    
430 elmex 1.1 my $w = $self->{writer};
431     $w->addPrefix (xmpp_ns ('client'), '');
432 elmex 1.3
433 elmex 1.4 my @add;
434     push @add, (id => $id) if defined $id;
435    
436 elmex 1.3 $type ||= 'chat';
437    
438 elmex 1.7 my %fattrs =
439     map { $_ => $attrs{$_} }
440 elmex 1.12 grep { my $k = $_; not grep { $k eq $_ } qw/subject body thread/ }
441 elmex 1.7 keys %attrs;
442    
443 elmex 1.3 if (defined $create_cb) {
444 elmex 1.7 $w->startTag ('message', @add, to => $to, type => $type, %fattrs);
445     _generate_key_xmls ($w, subject => $attrs{subject}) if defined $attrs{subject};
446     _generate_key_xmls ($w, body => $attrs{body}) if defined $attrs{body};
447     _generate_key_xml ($w, thread => $attrs{thread}) if defined $attrs{thread};
448 elmex 1.3 $create_cb->($w);
449     $w->endTag;
450     } else {
451     if (exists $attrs{subject} or $attrs{body} or $attrs{thread}) {
452 elmex 1.7 $w->startTag ('message', @add, to => $to, type => $type, %fattrs);
453     _generate_key_xmls ($w, subject => $attrs{subject}) if defined $attrs{subject};
454     _generate_key_xmls ($w, body => $attrs{body}) if defined $attrs{body};
455     _generate_key_xml ($w, thread => $attrs{thread}) if defined $attrs{thread};
456 elmex 1.3 $w->endTag;
457     } else {
458 elmex 1.7 $w->emptyTag ('message', @add, to => $to, type => $type, %fattrs);
459 elmex 1.3 }
460     }
461    
462 elmex 1.1 $self->flush;
463     }
464    
465 elmex 1.4
466 elmex 1.16 =item B<write_error_tag ($error_stanza_node, $error_type, $error)>
467 elmex 1.4
468     C<$error_type> is one of 'cancel', 'continue', 'modify', 'auth' and 'wait'.
469     C<$error> is the name of the error tag child element. If C<$error> is one of
470     the following:
471    
472     'bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone',
473     'internal-server-error', 'item-not-found', 'jid-malformed', 'not-acceptable',
474     'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable',
475     'redirect', 'registration-required', 'remote-server-not-found',
476     'remote-server-timeout', 'resource-constraint', 'service-unavailable',
477     'subscription-required', 'undefined-condition', 'unexpected-request'
478    
479     then a default can be select for C<$error_type>, and the argument can be undefined.
480    
481     Note: This method is currently a bit limited in the generation of the xml
482     for the errors, if you need more please contact me.
483    
484     =cut
485    
486     our %STANZA_ERRORS = (
487     'bad-request' => ['modify', 400],
488     'conflict' => ['cancel', 409],
489     'feature-not-implemented' => ['cancel', 501],
490     'forbidden' => ['auth', 403],
491     'gone' => ['modify', 302],
492     'internal-server-error' => ['wait', 500],
493     'item-not-found' => ['cancel', 404],
494     'jid-malformed' => ['modify', 400],
495     'not-acceptable' => ['modify', 406],
496     'not-allowed' => ['cancel', 405],
497     'not-authorized' => ['auth', 401],
498     'payment-required' => ['auth', 402],
499     'recipient-unavailable' => ['wait', 404],
500     'redirect' => ['modify', 302],
501     'registration-required' => ['auth', 407],
502     'remote-server-not-found' => ['cancel', 404],
503     'remote-server-timeout' => ['wait', 504],
504     'resource-constraint' => ['wait', 500],
505     'service-unavailable' => ['cancel', 503],
506     'subscription-required' => ['auth', 407],
507     'undefined-condition' => ['cancel', 500],
508     'unexpected-request' => ['wait', 400],
509     );
510    
511     sub write_error_tag {
512     my ($self, $errstanza, $type, $error) = @_;
513    
514     my $w = $self->{writer};
515    
516     $_->write_on ($w) for $errstanza->nodes;
517    
518     my @add;
519    
520     unless (defined $type and defined $STANZA_ERRORS{$error}) {
521     $type = $STANZA_ERRORS{$error}->[0];
522     }
523    
524 elmex 1.13 push @add, (code => $STANZA_ERRORS{$error}->[1]);
525 elmex 1.4
526     $w->addPrefix (xmpp_ns ('client'), '');
527     $w->startTag ([xmpp_ns ('client') => 'error'], type => $type, @add);
528     $w->addPrefix (xmpp_ns ('stanzas'), '');
529     $w->emptyTag ([xmpp_ns ('stanzas') => $error]);
530     $w->endTag;
531     }
532    
533 elmex 1.16 =back
534    
535 elmex 1.1 =head1 AUTHOR
536    
537 elmex 1.16 Robin Redeker, C<< <elmex at ta-sa.org> >>, JID: C<< <elmex at jabber.org> >>
538 elmex 1.1
539     =head1 COPYRIGHT & LICENSE
540    
541     Copyright 2007 Robin Redeker, all rights reserved.
542    
543     This program is free software; you can redistribute it and/or modify it
544     under the same terms as Perl itself.
545    
546     =cut
547    
548     1; # End of Net::XMPP2