File Coverage

blib/lib/DJabberd/RosterStorage/SQLite.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package DJabberd::RosterStorage::SQLite;
2             # abstract base class
3 1     1   748 use strict;
  1         2  
  1         32  
4 1     1   5 use warnings;
  1         2  
  1         34  
5 1     1   15 use base 'DJabberd::RosterStorage';
  1         2  
  1         484  
6              
7 1     1   3477 use DBI;
  1         18946  
  1         82  
8 1     1   560 use DJabberd::Log;
  0            
  0            
9             our $logger = DJabberd::Log->get_logger();
10              
11             use vars qw($_respect_subscription $VERSION);
12             $VERSION = '1.00';
13              
14             sub set_config_database {
15             my ($self, $dbfile) = @_;
16             $self->{dbfile} = $dbfile;
17             $logger->info("Loaded SQLite RosterStorage using file '$dbfile'");
18             }
19              
20             sub finalize {
21             my $self = shift;
22             die "No 'Database' configured'" unless $self->{dbfile};
23              
24             my $dbh = DBI->connect_cached("dbi:SQLite:dbname=$self->{dbfile}","","", { RaiseError => 1, PrintError => 0, AutoCommit => 1 });
25             $self->{dbh} = $dbh;
26             $self->check_install_schema;
27             return $self;
28             }
29              
30             sub check_install_schema {
31             my $self = shift;
32             my $dbh = $self->{dbh};
33              
34             eval {
35             $dbh->do(qq{
36             CREATE TABLE jidmap (
37             jidid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
38             jid VARCHAR(255) NOT NULL,
39             UNIQUE (jid)
40             )});
41             $dbh->do(qq{
42             CREATE TABLE roster (
43             userid INTEGER REFERENCES jidmap NOT NULL,
44             contactid INTEGER REFERENCES jidmap NOT NULL,
45             name VARCHAR(255),
46             subscription INTEGER NOT NULL REFERENCES substates DEFAULT 0,
47             PRIMARY KEY (userid, contactid)
48             )});
49             $dbh->do(qq{
50             CREATE TABLE rostergroup (
51             groupid INTEGER PRIMARY KEY NOT NULL,
52             userid INTEGER REFERENCES jidmap NOT NULL,
53             name VARCHAR(255),
54             UNIQUE (userid, name)
55             )});
56             $dbh->do(qq{
57             CREATE TABLE groupitem (
58             groupid INTEGER REFERENCES jidmap NOT NULL,
59             contactid INTEGER REFERENCES jidmap NOT NULL,
60             PRIMARY KEY (groupid, contactid)
61             )});
62              
63             };
64             if ($@ && $@ !~ /table \w+ already exists/) {
65             $logger->logdie("SQL error $@");
66             die "SQL error: $@\n";
67             }
68              
69             $logger->info("Created all roster tables");
70              
71             }
72              
73             sub blocking { 1 }
74              
75             sub get_roster {
76             my ($self, $cb, $jid) = @_;
77              
78              
79             $logger->debug("Getting roster for '$jid'");
80              
81             my $dbh = $self->{dbh};
82              
83             my $roster = DJabberd::Roster->new;
84              
85             my $sql = qq{
86             SELECT r.contactid, r.name, r.subscription, jmc.jid
87             FROM roster r, jidmap jm, jidmap jmc
88             WHERE r.userid=jm.jidid and jm.jid=? and jmc.jidid=r.contactid
89             };
90              
91             # contacts is { contactid -> $row_hashref }
92             my $contacts = eval {
93             $dbh->selectall_hashref($sql, "contactid", undef, $jid->as_bare_string);
94             };
95             $logger->logdie("Failed to load roster: $@") if $@;
96              
97             foreach my $contact (values %$contacts) {
98             my $item =
99             DJabberd::RosterItem->new(
100             jid => $contact->{jid},
101             name => $contact->{name},
102             subscription => DJabberd::Subscription->from_bitmask($contact->{subscription}),
103             );
104              
105             # convert all the values in the hashref into RosterItems
106             $contacts->{$contact->{contactid}} = $item;
107             $roster->add($item);
108             }
109              
110             # get all the groups, and add them to the roster items
111             eval {
112             $sql = qq{
113             SELECT rg.name, gi.contactid
114             FROM rostergroup rg, jidmap j, groupitem gi
115             WHERE gi.groupid=rg.groupid AND rg.userid=j.jidid AND j.jid=?
116             };
117             my $sth = $dbh->prepare($sql);
118             $sth->execute($jid->as_bare_string);
119             while (my ($group_name, $contactid) = $sth->fetchrow_array) {
120             my $ri = $contacts->{$contactid} or next;
121             $ri->add_group($group_name);
122             }
123             };
124             $logger->logdie("Failed to load roster groups: $@") if $@;
125             $logger->debug(" ... got groups, calling set_roster..");
126              
127             $cb->set_roster($roster);
128              
129             }
130              
131             # to be called outside of a transaction, in auto-commit mode
132             sub _jidid_alloc {
133             my ($self, $jid) = @_;
134             my $dbh = $self->{dbh};
135             my $jids = $jid->as_bare_string;
136             my $id = eval {
137             $dbh->selectrow_array("SELECT jidid FROM jidmap WHERE jid=?",
138             undef, $jids);
139             };
140             $logger->logdie("Failed to select from jidmap: $@") if $@;
141             return $id if $id;
142              
143             eval {
144             $dbh->do("INSERT INTO jidmap (jidid, jid) VALUES (NULL, ?)",
145             undef, $jids);
146             };
147             $logger->logdie("_jidid_alloc failed: $@") if $@;
148              
149             $id = $dbh->last_insert_id(undef, undef, "jidmap", "jidid")
150             or $logger->logdie("Failed to allocate a number in _jidid_alloc");
151              
152             return $id;
153             }
154              
155             # to be called outside of a transaction, in auto-commit mode
156             sub _groupid_alloc {
157             my ($self, $userid, $name) = @_;
158             my $dbh = $self->{dbh};
159             my $id = eval {
160             $dbh->selectrow_array("SELECT groupid FROM rostergroup WHERE userid=? AND name=?",
161             undef, $userid, $name);
162             };
163             $logger->logdie("Failed to select from groupid: $@") if $@;
164             return $id if $id;
165              
166             eval {
167             $dbh->do("INSERT INTO rostergroup (groupid, userid, name) VALUES (NULL, ?, ?)",
168             undef, $userid, $name);
169             };
170             $logger->logdie("_groupid_alloc failed: $@") if $@;
171              
172             $id = $dbh->last_insert_id(undef, undef, "rostergroup", "groupid")
173             or $logger->logdie("Failed to allocate a number in _groupid_alloc");
174              
175             return $id;
176             }
177              
178             sub set_roster_item {
179             my ($self, $cb, $jid, $ritem) = @_;
180             local $_respect_subscription = 1;
181             $logger->debug("Set roster item");
182             $self->addupdate_roster_item($cb, $jid, $ritem);
183             }
184              
185             sub addupdate_roster_item {
186             my ($self, $cb, $jid, $ritem) = @_;
187             my $dbh = $self->{dbh};
188              
189             my $userid = $self->_jidid_alloc($jid);
190             my $contactid = $self->_jidid_alloc($ritem->jid);
191              
192             unless ($userid && $contactid) {
193             $cb->error("no userid and contactid");
194             return;
195             }
196              
197             $dbh->begin_work or
198             $logger->logdie("Failed to begin work");
199              
200             my $fail = sub {
201             my $reason = shift;
202             die "Failing to addupdate: $reason";
203             $dbh->rollback;
204             $cb->error($reason);
205             return;
206             };
207              
208             my $exist_row = $dbh->selectrow_hashref("SELECT * FROM roster WHERE userid=? AND contactid=?",
209             undef, $userid, $contactid);
210              
211              
212             my %in_group; # groupname -> 1
213              
214             if ($exist_row) {
215             my @groups = $self->_groups_of_contactid($userid, $contactid);
216             my %to_del; # groupname -> groupid
217             foreach my $g (@groups) {
218             $in_group{$g->[1]} = 1;
219             $to_del {$g->[1]} = $g->[0];
220             }
221             foreach my $gname ($ritem->groups) {
222             delete $to_del{$gname};
223             }
224             if (my $in = join(",", values %to_del)) {
225             $dbh->do("DELETE FROM groupitem WHERE groupid IN ($in) AND contactid=?",
226             undef, $contactid);
227             }
228              
229             # by default, don't change subscription, unless we're being called
230             # via set_roster_item.
231             my $sub_value = "subscription";
232             if ($_respect_subscription) {
233             $sub_value = $ritem->subscription->as_bitmask;
234             $logger->debug(" sub_value = $sub_value");
235             } else {
236             # but let's set our subscription in $ritem (since it comes to
237             # us as 'none') because we have to pass it back with the real
238             # value.
239             $ritem->set_subscription(DJabberd::Subscription->from_bitmask($exist_row->{subscription}));
240             }
241              
242             my $sql = "UPDATE roster SET name=?, subscription=$sub_value WHERE userid=? AND contactid=?";
243             my @args = ($ritem->name, $userid, $contactid);
244             $dbh->do($sql, undef, @args);
245             } else {
246             $dbh->do("INSERT INTO roster (userid, contactid, name, subscription) ".
247             "VALUES (?,?,?,?)", undef,
248             $userid, $contactid, $ritem->name, $ritem->subscription->as_bitmask)
249             }
250              
251             # add to groups
252             foreach my $gname ($ritem->groups) {
253             next if $in_group{$gname}; # already in this group, skip
254             my $gid = $self->_groupid_alloc($userid, $gname);
255             $dbh->do("INSERT OR IGNORE INTO groupitem (groupid, contactid) VALUES (?,?)",
256             undef, $gid, $contactid);
257             }
258              
259             $dbh->commit
260             or return $fail->();
261              
262             $cb->done($ritem);
263             }
264              
265             # returns ([groupid, groupname], ...)
266             sub _groups_of_contactid {
267             my ($self, $userid, $contactid) = @_;
268             my @ret;
269             my $sql = qq{
270             SELECT rg.groupid, rg.name
271             FROM rostergroup rg, groupitem gi
272             WHERE rg.userid=? AND gi.groupid=rg.groupid AND gi.contactid=?
273             };
274             my $sth = $self->{dbh}->prepare($sql);
275             $sth->execute($userid, $contactid);
276             while (my ($gid, $name) = $sth->fetchrow_array) {
277             push @ret, [$gid, $name];
278             }
279             return @ret;
280             }
281              
282             sub delete_roster_item {
283             my ($self, $cb, $jid, $ritem) = @_;
284             $logger->debug("delete roster item!");
285              
286             my $dbh = $self->{dbh};
287              
288             my $userid = $self->_jidid_alloc($jid);
289             my $contactid = $self->_jidid_alloc($ritem->jid);
290              
291             unless ($userid && $contactid) {
292             $cb->error("no userid/contactid in delete");
293             return;
294             }
295              
296             $dbh->begin_work;
297              
298             my $fail = sub {
299             $dbh->rollback;
300             $cb->error;
301             return;
302             };
303              
304             my @groups = $self->_groups_of_contactid($userid, $contactid);
305              
306             if (my $in = join(",", map { $_->[0] } @groups)) {
307             $dbh->do("DELETE FROM groupitem WHERE groupid IN ($in) AND contactid=?",
308             undef, $contactid);
309             }
310              
311             $dbh->do("DELETE FROM roster WHERE userid=? AND contactid=?",
312             undef, $userid, $contactid)
313             or return $fail->();
314              
315             $dbh->commit or $fail->();
316              
317             $cb->done;
318             }
319              
320             sub load_roster_item {
321             my ($self, $jid, $contact_jid, $cb) = @_;
322              
323             my $dbh = $self->{dbh};
324              
325             my $userid = $self->_jidid_alloc($jid);
326             my $contactid = $self->_jidid_alloc($contact_jid);
327             unless ($userid && $contactid) {
328             $cb->error("no userid/contactid in load");
329             return;
330             }
331              
332             my $row = $dbh->selectrow_hashref("SELECT name, subscription FROM roster ".
333             "WHERE userid=? AND contactid=?",
334             undef, $userid, $contactid);
335             unless ($row) {
336             $cb->set(undef);
337             return;
338             }
339              
340             my $item =
341             DJabberd::RosterItem->new(
342             jid => $contact_jid,,
343             name => $row->{name},
344             subscription => DJabberd::Subscription->from_bitmask($row->{subscription}),
345             );
346             foreach my $ga ($self->_groups_of_contactid($userid, $contactid)) {
347             $item->add_group($ga->[1]);
348             }
349              
350             $cb->set($item);
351             return;
352             }
353              
354             sub wipe_roster {
355             my ($self, $cb, $jid) = @_;
356              
357             my $dbh = $self->{dbh};
358              
359             my $userid = $self->_jidid_alloc($jid);
360             unless ($userid) {
361             $cb->error("no userid/contactid in delete");
362             return;
363             }
364              
365             $dbh->begin_work;
366              
367             my $fail = sub {
368             $dbh->rollback;
369             $cb->error;
370             return;
371             };
372              
373             $dbh->do("DELETE FROM roster WHERE userid=?", undef, $userid)
374             or return $fail->();
375             $dbh->do("DELETE FROM rostergroup WHERE userid=?", undef, $userid)
376             or return $fail->();
377             # FIXME: clean up other tables too.
378              
379             $dbh->commit or $fail->();
380             $cb->done;
381             }
382              
383             1;
384              
385             __END__