1package DJabberd::IQ;
2use strict;
3use base qw(DJabberd::Stanza);
4use DJabberd::Util qw(exml);
5use DJabberd::Roster;
6use Digest::SHA1;
7
8use DJabberd::Log;
9our $logger = DJabberd::Log->get_logger();
10
11sub on_recv_from_client {
12    my ($self, $conn) = @_;
13
14    my $to = $self->to_jid;
15    if (! $to || $conn->vhost->uses_jid($to)) {
16        $self->process($conn);
17        return;
18    }
19
20    $self->deliver;
21}
22
23my $iq_handler = {
24    'get-{jabber:iq:roster}query' => \&process_iq_getroster,
25    'set-{jabber:iq:roster}query' => \&process_iq_setroster,
26    'get-{jabber:iq:auth}query' => \&process_iq_getauth,
27    'set-{jabber:iq:auth}query' => \&process_iq_setauth,
28    'set-{urn:ietf:params:xml:ns:xmpp-session}session' => \&process_iq_session,
29    'set-{urn:ietf:params:xml:ns:xmpp-bind}bind' => \&process_iq_bind,
30    'get-{http://jabber.org/protocol/disco#info}query'  => \&process_iq_disco_info_query,
31    'get-{http://jabber.org/protocol/disco#items}query' => \&process_iq_disco_items_query,
32    'get-{jabber:iq:register}query' => \&process_iq_getregister,
33    'set-{jabber:iq:register}query' => \&process_iq_setregister,
34    'set-{djabberd:test}query' => \&process_iq_set_djabberd_test,
35};
36
37# DO NOT OVERRIDE THIS
38sub process {
39    my DJabberd::IQ $self = shift;
40    my $conn = shift;
41
42    # FIXME: handle 'result'/'error' IQs from when we send IQs
43    # out, like in roster pushes
44
45    # Trillian Jabber 3.1 is stupid and sends a lot of IQs (but non-important ones)
46    # without ids.  If we respond to them (also without ids, or with id='', rather),
47    # then Trillian crashes.  So let's just ignore them.
48    return unless defined($self->id) && length($self->id);
49
50    $conn->vhost->run_hook_chain(phase    => "c2s-iq",
51                                 args     => [ $self ],
52                                 fallback => sub {
53                                     my $sig = $self->signature;
54                                     my $meth = $iq_handler->{$sig};
55                                     unless ($meth) {
56                                         $self->send_error(
57                                            qq{<error type='cancel'>}.
58                                            qq{<feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
59                                            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}.
60                                            qq{This feature is not implemented yet in DJabberd.}.
61                                            qq{</text>}.
62                                            qq{</error>}
63                                         );
64                                         $logger->warn("Unknown IQ packet: $sig");
65                                         return;
66                                     }
67
68                                     $DJabberd::Stats::counter{"InIQ:$sig"}++;
69                                     $meth->($conn, $self);
70                                 });
71}
72
73sub signature {
74    my $iq = shift;
75    my $fc = $iq->first_element;
76    # FIXME: should signature ever get called on a bogus IQ packet?
77    return $iq->type . "-" . ($fc ? $fc->element : "(BOGUS)");
78}
79
80sub send_result {
81    my DJabberd::IQ $self = shift;
82    $self->send_reply("result");
83}
84
85sub send_error {
86    my DJabberd::IQ $self = shift;
87    my $raw = shift || '';
88    $self->send_reply("error", $self->innards_as_xml . "\n" . $raw);
89}
90
91# caller must send well-formed XML (but we do the wrapping element)
92sub send_result_raw {
93    my DJabberd::IQ $self = shift;
94    my $raw = shift;
95    return $self->send_reply("result", $raw);
96}
97
98sub send_reply {
99    my DJabberd::IQ $self = shift;
100    my ($type, $raw) = @_;
101
102    my $conn = $self->{connection}
103        or return;
104
105    $raw ||= "";
106    my $id = $self->id;
107    my $bj = $conn->bound_jid;
108    my $from_jid = $self->to;
109    my $to = $bj ? (" to='" . $bj->as_string_exml . "'") : "";
110    my $from = $from_jid ? (" from='" . $from_jid . "'") : "";
111    my $xml = qq{<iq$to$from type='$type' id='$id'>$raw</iq>};
112    $conn->xmllog->info($xml);
113    $conn->write(\$xml);
114}
115
116sub process_iq_disco_info_query {
117    my ($conn, $iq) = @_;
118
119    # Trillian, again, is fucking stupid and crashes on just
120    # about anything its homemade XML parser doesn't like.
121    # so ignore it when it asks for this, just never giving
122    # it a reply.
123    if ($conn->vhost->quirksmode && $iq->id =~ /^trill_/) {
124        return;
125    }
126
127    # TODO: these can be sent back to another server I believe -- sky
128
129    # TODO: Here we need to figure out what identities we have and
130    # capabilities we have
131    my $xml;
132    $xml  = qq{<query xmlns='http://jabber.org/protocol/disco#info'>};
133    $xml .= qq{<identity category='server' type='im' name='djabberd'/>};
134
135    foreach my $cap ('http://jabber.org/protocol/disco#info',
136                     $conn->vhost->features)
137    {
138        $xml .= "<feature var='$cap'/>";
139    }
140    $xml .= qq{</query>};
141
142    $iq->send_reply('result', $xml);
143}
144
145sub process_iq_disco_items_query {
146    my ($conn, $iq) = @_;
147
148    my $vhost = $conn->vhost;
149
150    my $items = $vhost ? $vhost->child_services : {};
151
152    my $xml = qq{<query xmlns='http://jabber.org/protocol/disco#items'>}.
153        join('', map({ "<item jid='".exml($_)."' name='".exml($items->{$_})."' />" } keys %$items)).
154        qq{</query>};
155
156    $iq->send_reply('result', $xml);
157}
158
159sub process_iq_getroster {
160    my ($conn, $iq) = @_;
161
162    my $send_roster = sub {
163        my $roster = shift;
164        $logger->info("Sending roster to conn $conn->{id}");
165        $iq->send_result_raw($roster->as_xml);
166
167        # JIDs who want to subscribe to us, since we were offline
168        foreach my $jid (map  { $_->jid }
169                         grep { $_->subscription->pending_in }
170                         $roster->items) {
171            my $subpkt = DJabberd::Presence->make_subscribe(to   => $conn->bound_jid,
172                                                            from => $jid);
173            # already in roster as pendin, we've already processed it,
174            # so just deliver it (or queue it) so user can reply with
175            # subscribed/unsubscribed:
176            $conn->note_pend_in_subscription($subpkt);
177        }
178    };
179
180    # need to be authenticated to request a roster.
181    my $bj = $conn->bound_jid;
182    unless ($bj) {
183        $iq->send_error(
184            qq{<error type='auth'>}.
185            qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
186            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}.
187            qq{You need to be authenticated before requesting a roster.}.
188            qq{</text>}.
189            qq{</error>}
190        );
191        return;
192    }
193
194    # {=getting-roster-on-login}
195    $conn->set_requested_roster(1);
196
197    $conn->vhost->get_roster($bj,
198                             on_success => $send_roster,
199                             on_fail => sub {
200                                 $send_roster->(DJabberd::Roster->new);
201                             });
202
203    return 1;
204}
205
206sub process_iq_setroster {
207    my ($conn, $iq) = @_;
208
209    my $item = $iq->query->first_element;
210    unless ($item && $item->element eq "{jabber:iq:roster}item") {
211        $iq->send_error( # TODO make this error proper
212            qq{<error type='error-type'>}.
213            qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
214            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='langcode'>}.
215            qq{You need to be authenticated before requesting a roster.}.
216            qq{</text>}.
217            qq{</error>}
218        );
219        return;
220    }
221
222    # {=xmpp-ip-7.6-must-ignore-subscription-values}
223    my $subattr  = $item->attr('{}subscription') || "";
224    my $removing = $subattr eq "remove" ? 1 : 0;
225
226    my $jid = $item->attr("{}jid")
227        or return $iq->send_error( # TODO Yeah, this one too
228            qq{<error type='error-type'>}.
229            qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
230            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='langcode'>}.
231            qq{You need to be authenticated before requesting a roster.}.
232            qq{</text>}.
233            qq{</error>}
234        );
235
236    my $name = $item->attr("{}name");
237
238    # find list of group names to add/update.  can ignore
239    # if we're just removing.
240    my @groups;  # scalars of names
241    unless ($removing) {
242        foreach my $ele ($item->children_elements) {
243            next unless $ele->element eq "{jabber:iq:roster}group";
244            push @groups, $ele->first_child;
245        }
246    }
247
248    my $ritem = DJabberd::RosterItem->new(jid     => $jid,
249                                          name    => $name,
250                                          remove  => $removing,
251                                          groups  => \@groups,
252                                          );
253
254    # TODO if ($removing), send unsubscribe/unsubscribed presence
255    # stanzas.  See RFC3921 8.6
256
257    # {=add-item-to-roster}
258    my $phase = $removing ? "RosterRemoveItem" : "RosterAddUpdateItem";
259    $conn->vhost->run_hook_chain(phase   => $phase,
260                                 args    => [ $conn->bound_jid, $ritem ],
261                                 methods => {
262                                     done => sub {
263                                         my ($self, $ritem_final) = @_;
264
265                                         # the RosterRemoveItem isn't required to return the final item
266                                         $ritem_final = $ritem if $removing;
267
268                                         $iq->send_result;
269                                         $conn->vhost->roster_push($conn->bound_jid, $ritem_final);
270
271                                         # TODO: section 8.6: must send a
272                                         # bunch of presence
273                                         # unsubscribe/unsubscribed messages
274                                     },
275                                     error => sub { # TODO What sort of error stat is being hit here?
276                                         $iq->send_error;
277                                     },
278                                 },
279                                 fallback => sub {
280                                     if ($removing) {
281                                         # NOTE: we used to send an error here, but clients get
282                                         # out of sync and we need to let them think a delete
283                                         # happened even if it didn't.
284                                         $iq->send_result;
285                                     } else { # TODO ACK, This one as well
286                                         $iq->send_error;
287                                     }
288                                 });
289
290    return 1;
291}
292
293sub process_iq_getregister {
294    my ($conn, $iq) = @_;
295
296    # If the entity is not already registered and the host supports
297    # In-Band Registration, the host MUST inform the entity of the
298    # required registration fields. If the host does not support
299    # In-Band Registration, it MUST return a <service-unavailable/>
300    # error. If the host is redirecting registration requests to some
301    # other medium (e.g., a website), it MAY return an <instructions/>
302    # element only, as shown in the Redirection section of this
303    # document.
304    my $vhost = $conn->vhost;
305    unless ($vhost->allow_inband_registration) {
306        # MUST return a <service-unavailable/>
307        $iq->send_error(
308            qq{<error type='cancel' code='503'>}.
309            qq{<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
310            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}.
311            qq{In-Band registration is not supported by this server's configuration.}.
312            qq{</text>}.
313            qq{</error>}
314        );
315        return;
316    }
317
318    # if authenticated, give them existing login info:
319    if (my $jid = $conn->bound_jid) {
320
321        my $password = 0 ? "<password></password>" : "";  # TODO
322        my $username = $jid->node;
323        $iq->send_result_raw(qq{<query xmlns='jabber:iq:register'>
324                                    <registered/>
325                                    <username>$username</username>
326                                    $password
327                                    </query>});
328        return;
329    }
330
331    # not authenticated, ask for their required fields
332    # NOTE: we send_result_raw here, which just writes, so they don't
333    # need to be an available resource (since they're not even authed
334    # yet) for this to work.  that's like most things in IQ anyway.
335    $iq->send_result_raw(qq{<query xmlns='jabber:iq:register'>
336                                <instructions>
337                                Choose a username and password for use with this service.
338                                </instructions>
339                                <username/>
340                                <password/>
341                                </query>});
342}
343
344sub process_iq_setregister {
345    my ($conn, $iq) = @_;
346
347    my $vhost = $conn->vhost;
348    unless ($vhost->allow_inband_registration) {
349        # MUST return a <service-unavailable/>
350        $iq->send_error(
351            qq{<error type='cancel'>}.
352            qq{<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}.
353            qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}.
354            qq{In-Band registration is not supported by this server\'s configuration.}.
355            qq{</text>}.
356            qq{</error>}
357        );
358        return;
359    }
360
361    my $bjid = $conn->bound_jid;
362
363    # remove (cancel) support
364    my $item = $iq->query->first_element;
365    if ($item && $item->element eq "{jabber:iq:register}remove") {
366        if ($bjid) {
367            my $rosterwipe = sub {
368                $vhost->run_hook_chain(phase => "RosterWipe",
369                                       args => [ $bjid ],
370                                       methods => {
371                                           done => sub {
372                                               $iq->send_result;
373                                               $conn->stream_error("not-authorized");
374                                           },
375                                       });
376            };
377
378            $vhost->run_hook_chain(phase => "UnregisterJID",
379                                   args => [ username => $bjid->node, conn => $conn ],
380                                   methods => {
381                                       deleted => sub {
382                                           $rosterwipe->();
383                                       },
384                                       notfound => sub {
385                                           warn "notfound.\n";
386                                           return $iq->send_error;
387                                       },
388                                       error => sub {
389                                           return $iq->send_error;
390                                       },
391                                   });
392
393            $iq->send_result;
394        } else {
395            $iq->send_error; # TODO: <forbidden/>
396        }
397        return;
398    }
399
400    my $query = $iq->query
401        or die;
402    my @children = $query->children;
403    my $get = sub {
404        my $lname = shift;
405        foreach my $c (@children) {
406            next unless ref $c && $c->element eq "{jabber:iq:register}$lname";
407            my $text = $c->first_child;
408            return undef if ref $text;
409            return $text;
410        }
411        return undef;
412    };
413
414    my $username = $get->("username");
415    my $password = $get->("password");
416    return $iq->send_error unless $username =~ /^\w+$/;
417    return $iq->send_error if $bjid && $bjid->node ne $username;
418
419    # create the account
420    $vhost->run_hook_chain(phase => "RegisterJID",
421                           args => [ username => $username, conn => $conn, password => $password ],
422                           methods => {
423                               saved => sub {
424                                   return $iq->send_result;
425                               },
426                               conflict => sub {
427                                   my $epass = exml($password);
428                                   return $iq->send_error(qq{
429                                       <query xmlns='jabber:iq:register'>
430                                           <username>$username</username>
431                                           <password>$epass</password>
432                                           </query>
433                                           <error code='409' type='cancel'>
434                                           <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
435                                           </error>
436                                       });
437                               },
438                               error => sub {
439                                   return $iq->send_error;
440                               },
441                           });
442
443}
444
445
446sub process_iq_getauth {
447    my ($conn, $iq) = @_;
448    # <iq type='get' id='gaimf46fbc1e'><query xmlns='jabber:iq:auth'><username>brad</username></query></iq>
449
450    # force SSL by not letting them login
451    if ($conn->vhost->requires_ssl && ! $conn->ssl) {
452        $conn->stream_error("policy-violation", "Local policy requires use of SSL before authentication.");
453        return;
454    }
455
456    my $username = "";
457    my $child = $iq->query->first_element;
458    if ($child && $child->element eq "{jabber:iq:auth}username") {
459        $username = $child->first_child;
460        die "Element in username field?" if ref $username;
461    }
462
463    # FIXME:  use nodeprep or whatever, not \w+
464    $username = '' unless $username =~ /^\w+$/;
465
466    my $type = ($conn->vhost->are_hooks("GetPassword") ||
467                $conn->vhost->are_hooks("CheckDigest")) ? "<digest/>" : "<password/>";
468
469    $iq->send_result_raw("<query xmlns='jabber:iq:auth'><username>$username</username>$type<resource/></query>");
470    return 1;
471}
472
473sub process_iq_setauth {
474    my ($conn, $iq) = @_;
475    # <iq type='set' id='gaimbb822399'><query xmlns='jabber:iq:auth'><username>brad</username><resource>work</resource><digest>ab2459dc7506d56247e2dc684f6e3b0a5951a808</digest></query></iq>
476    my $id = $iq->id;
477
478    my $query = $iq->query
479        or die;
480    my @children = $query->children;
481
482    my $get = sub {
483        my $lname = shift;
484        foreach my $c (@children) {
485            next unless ref $c && $c->element eq "{jabber:iq:auth}$lname";
486            my $text = $c->first_child;
487            return undef if ref $text;
488            return $text;
489        }
490        return undef;
491    };
492
493    my $username = $get->("username");
494    my $resource = $get->("resource");
495    my $password = $get->("password");
496    my $digest   = $get->("digest");
497
498    # "Both the username and the resource are REQUIRED for client
499    # authentication" Section 3.1 of XEP 0078
500    return unless $username && $username =~ /^\w+$/;
501    return unless $resource;
502
503    my $vhost = $conn->vhost;
504
505    my $reject = sub {
506        $DJabberd::Stats::counter{'auth_failure'}++;
507        $iq->send_reply("error", qq{<error code='401' type='auth'><not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>});
508        return 1;
509    };
510
511
512    my $accept = sub {
513        my $cb = shift;
514        my $authjid = shift;
515
516        # create default JID
517        unless (defined $authjid) {
518            my $sname = $vhost->name;
519            $authjid = "$username\@$sname";
520        }
521
522        # register
523        my $jid = DJabberd::JID->new("$authjid");
524
525        unless ($jid) {
526            $reject->();
527            return;
528        }
529
530        my $regcb = DJabberd::Callback->new({
531            registered => sub {
532                (undef, my $fulljid) = @_;
533                $conn->set_bound_jid($fulljid);
534                $DJabberd::Stats::counter{'auth_success'}++;
535                $iq->send_result;
536            },
537            error => sub {
538                $iq->send_error;
539            },
540            _post_fire => sub {
541                $conn = undef;
542                $iq   = undef;
543            },
544        });
545
546        $vhost->register_jid($jid, $resource, $conn, $regcb);
547    };
548
549
550    # XXX FIXME
551    # If the client ignores your wishes get a digest or password
552    # We should throw an error indicating so
553    # Currently we will just return authentication denied -- artur
554
555    if ($vhost->are_hooks("GetPassword")) {
556        $vhost->run_hook_chain(phase => "GetPassword",
557                              args  => [ username => $username, conn => $conn ],
558                              methods => {
559                                  set => sub {
560                                      my (undef, $good_password) = @_;
561                                      if ($password && $password eq $good_password) {
562                                          $accept->();
563                                      } elsif ($digest) {
564                                          my $good_dig = lc(Digest::SHA1::sha1_hex($conn->{stream_id} . $good_password));
565                                          if ($good_dig eq $digest) {
566                                              $accept->();
567                                          } else {
568                                              $reject->();
569                                          }
570                                      } else {
571                                          $reject->();
572                                      }
573                                  },
574                              },
575                              fallback => $reject);
576    } elsif ($vhost->are_hooks("CheckDigest")) {
577        $vhost->run_hook_chain(phase => "CheckDigest",
578                              args => [ username => $username, conn => $conn, digest => $digest, resource => $resource ],
579                              methods => {
580                                  accept => $accept,
581                                  reject => $reject,
582                              });
583    } else {
584        $vhost->run_hook_chain(phase => "CheckCleartext",
585                              args => [ username => $username, conn => $conn, password => $password ],
586                              methods => {
587                                  accept => $accept,
588                                  reject => $reject,
589                              });
590    }
591
592    return 1;  # signal that we've handled it
593}
594
595## sessions have been deprecated, see appendix E of:
596## http://xmpp.org/internet-drafts/draft-saintandre-rfc3921bis-07.html
597## BUT, we have to advertise session support since, libpurple REQUIRES it
598## (sigh)
599sub process_iq_session {
600    my ($conn, $iq) = @_;
601
602    my $from = $iq->from;
603    my $id   = $iq->id;
604
605    my $xml = qq{<iq from='$from' type='result' id='$id'/>};
606    $conn->xmllog->info($xml);
607    $conn->write(\$xml);
608}
609
610sub process_iq_bind {
611    my ($conn, $iq) = @_;
612
613    # <iq type='set' id='purple88621b5d'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>yann</resource></bind></iq>
614    my $id = $iq->id;
615
616    my $query = $iq->bind
617        or die;
618
619    my $bindns = 'urn:ietf:params:xml:ns:xmpp-bind';
620    my @children = $query->children;
621
622    my $get = sub {
623        my $lname = shift;
624        foreach my $c (@children) {
625            next unless ref $c && $c->element eq "{$bindns}$lname";
626            my $text = $c->first_child;
627            return undef if ref $text;
628            return $text;
629        }
630        return undef;
631    };
632
633    my $resource = $get->("resource") || DJabberd::JID->rand_resource;
634
635    my $vhost = $conn->vhost;
636
637    my $reject = sub {
638        my $xml = <<EOX;
639<iq id='$id' type='error'>
640    <error type='modify'>
641        <bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
642    </error>
643</iq>
644EOX
645        $conn->xmllog->info($xml);
646        $conn->write(\$xml);
647        return 1;
648    };
649
650    ## rfc3920 §8.4.2.2
651    my $cancel = sub {
652        my $reason = shift || "no reason";
653        my $xml = <<EOX;
654<iq id='$id' type='error'>
655     <error type='cancel'>
656       <not-allowed
657           xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
658     </error>
659   </iq>
660EOX
661        $conn->log->error("Reject bind request: $reason");
662        $conn->xmllog->info($xml);
663        $conn->write(\$xml);
664        return 1;
665    };
666
667    my $sasl = $conn->sasl
668        or return $cancel->("no sasl");
669
670    my $authjid = $conn->sasl->authenticated_jid
671        or return $cancel->("no authenticated_jid");
672
673    # register
674    my $jid = DJabberd::JID->new($authjid);
675
676    unless ($jid) {
677        $reject->();
678        return;
679    }
680
681    my $regcb = DJabberd::Callback->new({
682        registered => sub {
683            (undef, my $fulljid) = @_;
684            $conn->set_bound_jid($fulljid);
685            $DJabberd::Stats::counter{'auth_success'}++;
686            my $xml = <<EOX;
687<iq id='$id' type='result'>
688    <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
689        <jid>$fulljid</jid>
690    </bind>
691</iq>
692EOX
693            $conn->xmllog->info($xml);
694            $conn->write(\$xml);
695        },
696        error => sub {
697            $reject->();
698        },
699        _post_fire => sub {
700            $conn = undef;
701            $iq   = undef;
702        },
703    });
704
705    $vhost->register_jid($jid, $resource, $conn, $regcb);
706    return 1;
707}
708
709sub process_iq_set_djabberd_test {
710    my ($conn, $iq) = @_;
711    # <iq type='set' id='foo'><query xmlns='djabberd:test'>some command</query></iq>
712    my $id = $iq->id;
713
714    unless ($ENV{DJABBERD_TEST_COMMANDS}) {
715        $iq->send_error;
716        return;
717    }
718
719    my $query = $iq->query
720        or die;
721    my $command = $query->first_child;
722
723    if ($command eq "write error") {
724        $conn->set_writer_func(sub {
725            my ($bref, $to_write, $offset) = @_;
726            $conn->close;
727            return 0;
728        });
729        $iq->send_result_raw("<wont_get_to_you_anyway/>");
730        return;
731    }
732
733    $iq->send_result_raw("<unknown-command/>");
734}
735
736sub id {
737    return $_[0]->attr("{}id");
738}
739
740sub type {
741    return $_[0]->attr("{}type");
742}
743
744sub from {
745    return $_[0]->attr("{}from");
746}
747
748sub query {
749    my $self = shift;
750    my $child = $self->first_element
751        or return;
752    my $ele = $child->element
753        or return;
754    return undef unless $child->element =~ /\}query$/;
755    return $child;
756}
757
758sub bind {
759    my $self = shift;
760    my $child = $self->first_element
761        or return;
762    my $ele = $child->element
763        or return;
764    return unless $child->element =~ /\}bind$/;
765    return $child;
766}
767
768sub deliver_when_unavailable {
769    my $self = shift;
770    return $self->type eq "result" ||
771        $self->type eq "error";
772}
773
774sub make_response {
775    my ($self) = @_;
776
777    my $response = $self->SUPER::make_response();
778    $response->attrs->{"{}type"} = "result";
779    return $response;
780}
781
7821;
783