File Coverage

blib/lib/Cal/DAV.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Cal::DAV;
2              
3              
4 4     4   7682 use strict;
  4         8  
  4         131  
5 4     4   3405 use Data::ICal;
  4         158405  
  4         48  
6 4     4   5650 use HTTP::DAV;
  0            
  0            
7              
8             our $VERSION="0.6";
9              
10             =head1 NAME
11              
12             Cal::DAV - a CalDAV client
13              
14             =head1 SYNOPSIS
15              
16             my $cal = Cal::DAV->new( user => $user, pass => $pass, url => $url);
17             # the ics data will be fetched automatically if it's there
18              
19             # ... or you can parse some ics
20             $cal->parse(filename => $data);
21              
22             # cal now has all the methods of Data::ICal
23             # you can now monkey around with the object
24              
25             # saves the updated calendar
26             $cal->save;
27              
28             # deletes the calendar
29             $cal->delete;
30              
31             # lock the file on the server
32             $cal->lock;
33              
34             # unlock the file on the server
35             $cal->unlock
36              
37             # steal the lock
38             $cal->steal_lock;
39              
40             # also
41             $cal->forcefully_unlock_all
42              
43             # and
44             $cal->lockdiscovery
45              
46             # resyncs it with the server
47             $cal->get;
48              
49             # Get the underlying HTTP::DAV object
50             my $dav = $cal->dav;
51              
52             =head1 DESCRIPTION
53              
54             C is actually a very thin wrapper round C and
55             C but it may gain more functionality later and, in the mean
56             time, serves as something that
57              
58             =head1 TESTING
59              
60             In order to test you need to define three environment variables:
61             C, C and C which
62             points to a DAV collection that the user supplied has write
63             permissions for.
64              
65             It should be noted that, at the moment, I'm having problems finding
66             a CalDAV server that allows me to create files and so I can't run all
67             the tests.
68              
69             =head1 METHODS
70              
71             =cut
72              
73             =head2 new
74              
75             Must have at least C, C and C args where
76             C is the url of a remote, DAV accessible C<.ics> file.
77              
78             Can optionally take an C option. See C method below.
79              
80             =cut
81              
82             # TODO if we remove the option to do operations with other urls
83             # we could then cache the resource object
84             sub new {
85             my $class = shift;
86             my %args = @_;
87             my %opts;
88             for (qw(user pass url)) {
89             die "You must pass in a $_ param\n" unless defined $args{$_};
90             $opts{"-${_}"} = $args{$_};
91             }
92             my $dav = HTTP::DAV->new;
93             $dav->credentials(%opts);
94             return bless { _dav => $dav, url => $args{url}, _auto_commit => $args{auto_commit} }, $class;
95             }
96              
97             =head2 parse
98              
99             Make a new calendar object using same arguments as C's C or C methods.
100              
101             Does not auto save for you.
102              
103             Returns 1 on success and 0 on failure.
104              
105             =cut
106              
107             sub parse {
108             my $self = shift;
109             my %args = @_;
110             $self->{_cal} = Data::ICal->new(%args);
111             return (defined $self->{_cal}) ?
112             $self->dav->ok("Loaded data successfully") :
113             $self->dav->err('ERR_GENERIC', "Failed to load calendar: parse error $@");
114             }
115              
116             =head2 save [url]
117              
118             Save the calendar back to the server (or optionally to another path).
119              
120             Returns 1 on success and 0 on failure.
121              
122             =cut
123              
124             sub save {
125             my $self = shift;
126             my $url = shift || $self->{url};
127             my $cal = $self->{_cal}; # TODO should this be cal()
128             return 1 unless defined $cal;
129             my $res = $self->dav->new_resource( -uri => $url );
130             #unless ($self->{_fetched}) {
131             #my $ret = $res->mkcol;
132             #unless ($ret->is_success) {
133             # return $self->dav->err( 'ERR_RESP_FAIL',"mkcol in put failed ".$ret->message(), $url);
134             #}
135             #$self->{_fetched} = 1;
136             #}
137             my $data = $cal->as_string;
138             my $ret = $res->put($data);
139              
140             if ($ret->is_success) {
141             return $self->dav->ok( "put $url (" . length($data) ." bytes)",$url );
142             } else {
143             return $self->dav->err( 'ERR_RESP_FAIL',"put failed ".$ret->message(), $url);
144             }
145             }
146              
147             =head2 delete [url]
148              
149             Delete the file on the server or optionally another url.
150              
151             Returns 1 on success and 0 on failure.
152              
153             =cut
154              
155             sub delete {
156             my $self = shift;
157             my $url = shift || $self->{url};
158             my $res = $self->dav->new_resource( -uri => $url );
159             my $ret = $res->delete();
160             if ($ret->is_success) {
161             return $self->dav->ok( "deleted $url successfully", $url );
162             } else {
163             return $self->dav->err( 'ERR_RESP_FAIL',$ret->message(), $url);
164             }
165              
166             }
167              
168             =head2 get [url]
169              
170             Refetch the file from the sever to sync it -
171              
172             Alternatively fetch an alternative url.
173              
174             These will lose any local changes.
175              
176             =cut
177              
178             sub get {
179             my $self = shift;
180             my $url = shift || $self->{url};
181             my $res = $self->dav->new_resource( -uri => $url );
182             my $ret = $res->get();
183             if ($ret->is_success) {
184             $self->{_fetched} = 1;
185             #return $self->dav->ok("get $url", $url, $ret->content_length() );
186             } else {
187             return $self->dav->err('ERR_GENERIC', "get $url failed: ". $ret->message, $url);
188             }
189             my $data = $res->get_content();
190             return $self->dav->err('ERR_GENERIC', "Couldn't get data from $url", $url) unless defined $data;
191             return $self->parse(data => $data);
192             }
193              
194             =head2 lock
195              
196             Same options as C's C.
197              
198             =cut
199              
200             sub lock {
201             my $self = shift;
202             my $resp = $self->_do_on_dav('lock', @_);
203             if ( $resp->is_success() ) {
204             return $self->dav->ok( "lock $self->{url} succeeded",$self->{url} );
205             } else {
206             return $self->dav->err( 'ERR_RESP_FAIL',$resp->message, $self->{url} );
207             }
208             }
209              
210             =head2 unlock
211              
212             Same options as C's C.
213              
214             =cut
215              
216             sub unlock {
217             my $self = shift;
218             my $resp = $self->_do_on_dav('unlock', @_);
219             if ( $resp->is_success ) {
220             return $self->dav->ok( "unlock $self->{url} succeeded",$self->{url} );
221             } else {
222             # The Resource.pm::lock routine has a hack
223             # where if it doesn't know the locktoken, it will
224             # just return an empty response with message "Client Error".
225             # Make a custom message for this case.
226             my $msg = $resp->message;
227             if ( $msg=~ /Client error/i ) {
228             $msg = "No locks found. Try steal";
229             return $self->dav->err( 'ERR_GENERIC',$msg,$self->{url} );
230             } else {
231             return $self->dav->err( 'ERR_RESP_FAIL',$msg,$self->{url} );
232             }
233             }
234             }
235              
236             =head2 steal_lock
237              
238             Same options as C's C.
239              
240             =cut
241              
242             sub steal_lock {
243             my $self = shift;
244             my $resp = $self->_do_on_dav('steal_lock', @_);
245             if ( $resp->is_success() ) {
246             return $self->dav->ok( "steal succeeded",$self->{url} );
247             } else {
248             return $self->dav->err( 'ERR_RESP_FAIL',$resp->message(),$self->{url} );
249             }
250             }
251              
252             =head2 lockdiscovery
253              
254             Same options as C's C.
255              
256             =cut
257              
258             sub lockdiscovery {
259             my $self = shift;
260             my $resp = $self->_do_on_dav('lockdiscovery', @_);
261             }
262              
263             =head2 forcefully_unlock_all
264              
265             Same options as C's C.
266              
267             =cut
268              
269             sub forcefully_unlock_all {
270             my $self = shift;
271             $self->_do_on_dav('forcefully_unlock_all', @_);
272             }
273              
274              
275             sub _do_on_dav {
276             my $self = shift;
277             my $meth = shift;
278             my $url = $self->{url};
279             my $res = $self->dav->new_resource( -uri => $url );
280             $res->$meth(@_);
281             }
282              
283             =head2 dav [HTTP::DAV]
284              
285             Get the underlying C object or, alterntively, replace it with
286             a a new one.
287              
288             =cut
289              
290             sub dav {
291             my $self = shift;
292             if (@_) {
293             $self->{_dav} = shift;
294             }
295             return $self->{_dav};
296             }
297              
298             =head2 cal
299              
300             Get the underlying cal object
301              
302             =cut
303              
304             sub cal {
305             my $self = shift;
306             if (!defined $self->{_cal}) {
307             my $ret = $self->get || die "Couldn't autofetch calendar: ".$self->dav->message;
308             }
309             return $self->{_cal};
310             }
311              
312             =head2 auto_commit [boolean]
313              
314             Whether to auto save on desctruction or not.
315              
316             Defaults to 0.
317              
318             =cut
319              
320             sub auto_commit {
321             my $self = shift;
322             if (@_) {
323             $self->{_auto_commit} = shift;
324             }
325             return $self->{_auto_commit};
326              
327             }
328              
329             =head2 message
330              
331             Same as C's C function.
332              
333             =cut
334              
335             sub message {
336             my $self = shift;
337             return $self->dav->message;
338             }
339              
340             =head2 errors
341              
342             Same as C's C function.
343              
344             =cut
345              
346             sub errors {
347             my $self = shift;
348             return $self->dav->errors;
349             }
350             use Carp qw(croak confess cluck);
351              
352             our $AUTOLOAD;
353             sub AUTOLOAD {
354             my $self = shift;
355             my $method = $AUTOLOAD;
356             $method =~ s/.*://; # strip fully-qualified portion
357             # TODO should we cache this in a glob?
358             $self->cal->$method(@_)
359             }
360              
361              
362             sub DESTROY {
363             my $self = shift;
364             $self->save if $self->auto_commit;
365             }
366              
367              
368              
369             =head1 AUTHOR
370              
371             Simon Wistow
372              
373             =head1 COPYRIGHT
374              
375             Copyright 2007, Simon Wistow
376              
377             Released under the same terms as Perl itself.
378              
379             =head1 SEE ALSO
380              
381             L
382              
383             L
384              
385             http://tools.ietf.org/html/rfc4791
386              
387             =cut
388              
389             1;