File Coverage

blib/lib/Net/Google/Calendar/Server.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Net::Google::Calendar::Server;
2              
3 1     1   946 use strict;
  1         2  
  1         39  
4 1     1   404 use Net::Google::Calendar;
  0            
  0            
5             use Data::ICal::DateTime;
6             use XML::Atom::Feed;
7             use XML::Atom::Util qw( set_ns first nodelist iso2dt);
8              
9             use vars qw($VERSION);
10              
11             $VERSION="0.1";
12              
13             =head1 NAME
14              
15             Net::Google::Calendar::Server - pretend to be like Google's Calendar
16              
17             =head1 SYNOPSIS
18              
19              
20              
21             # in reality this will be something like the Apache handler
22             my $handler = Net::Google::Calendar::Server::Handler::Foo->new;
23              
24             # $be_class might be ICalendar and $au_class might be Dummy
25             my $cal = eval {
26             Net::Google::Calendar::Server->new( backend_class => $be_class, backend_opts => \%backend_opts,
27             auth_class => $au_class, auth_opts => \%auth_opts )
28             };
29              
30             return $self->error($@) if $@;
31             return $cal->handle_request($handle);
32            
33             =head1 DESCRIPTION
34              
35             This is an implementation of the Calendar portion of Google's GData API.
36              
37             http://code.google.com/apis/gdata/protocol.html
38              
39             It's very incomplete but it's been lurking round in my Subversion repo for
40             too long (nearly a year now) so it deserves to be free. Free! FREE! Emancipia!
41              
42             A server requires a handler to call it (something like CGI or an Apache handler
43             or a standalone server).
44              
45             A server also requires a backend class (something that will retrieve and store
46             entries) and an auth class (something that will authentic the user) which can
47             be the same as the backend class.
48              
49             =head1 METHODS
50              
51             =cut
52              
53              
54             =head2 new [ opts ]
55              
56             Create a new server. Requires at least the options
57              
58             =over 4
59              
60             =item backend_class
61              
62             A class that will retrieve and store entries.
63              
64             Must be a subclass of a C so for
65             example to use C pass
66             in 'ICalendar'.
67              
68             =item backend_opts
69              
70             Options for the backend class.
71              
72             =item auth_class
73              
74             A class that will authenticate a user. Can be the same as the backend class i.e
75             if you passed in 'ICalendar' for C and C then
76             the C object instantiated
77             for the backend will be used for the auth class.
78              
79             =item auth_opts
80              
81             Options for the authentication class.
82              
83             =back
84              
85             =cut
86              
87             sub new {
88             my $class = shift;
89             my %opts = @_;
90              
91             my $dp_class = $opts{'backend_class'};
92             my $au_class = $opts{'auth_class'};
93              
94             # require the backend class
95             my $backend = $class->_require('Backend', $dp_class);
96             die $@ if $@;
97              
98             # fetch the variables for the Backend and the Auth classes
99             my %backend_opts = %{$opts{'backend_opts'}};
100             my %auth_opts = %{$opts{'auth_opts'}};
101              
102             # If auth is the same as backend then do some shennanigans
103             # this will allow a backend to also act as an Auth class if necessary
104             # useful for something like a DB backend or authenticating against Gmail or Exchange
105             my $auth;
106             if ($au_class eq $dp_class) {
107             $backend = $auth = $backend->new(%backend_opts, %auth_opts);
108             } else {
109             $auth = $class->_require('Auth', $au_class);
110             die $@ if $@;
111             $auth = $auth->new(%auth_opts);
112             }
113             $opts{backend} = $backend;
114             $opts{auth} = $auth;
115              
116             return bless \%opts, $class;
117             }
118              
119             sub _require {
120             my $self = shift;
121             my $base = shift;
122             my $class = shift;
123              
124             $class = "Net::Google::Calendar::Server::${base}::${class}";
125             eval "CORE::require $class";
126             return $class;
127             }
128              
129             =head2 auth
130              
131             The authentication object.
132              
133             =cut
134              
135             sub auth {
136             return $_[0]->{auth};
137             }
138              
139             =head backend
140              
141             The backend object.
142              
143             =cut
144              
145             sub backend {
146             return $_[0]->{backend};
147             }
148              
149              
150             =head2 fetch [ opts ]
151              
152             Get an event or events.
153              
154             Returns an Atom feed.
155              
156             =cut
157              
158             sub fetch {
159             my $self = shift;
160             my %opts = @_;
161              
162             # convert to DT first
163             foreach my $key (keys %opts) {
164             next if UNIVERSAL::isa($opts{$key},'DateTime');
165             if ($key =~ m!-m(in|ax)$!) {
166             $opts{$key} = iso2dt($opts{$key});
167             }
168             }
169             my @events = $self->{backend}->fetch(%opts);
170             my $feed = XML::Atom::Feed->new;
171             for (@events) {
172             $feed->add_entry($self->_to_atom($_));
173             }
174             return $feed;
175              
176             }
177              
178             =head2 create
179              
180             Takes an Atom entry, creates an entry, returns the updated Atom entry.
181              
182             =cut
183              
184             sub create {
185             my $self = shift;
186             return $self->_do('create', @_);
187             }
188              
189             =head2 update
190              
191             Takes an Atom entry, updates the entry, returns the updated Atom entry.
192              
193             =cut
194              
195             sub update {
196             my $self = shift;
197             return $self->_do('update', @_);0
198             }
199              
200              
201             =head2 deletes
202              
203             Takes an Atom entry, deletes an event, returns undef on failure.
204              
205             =cut
206              
207             sub delete {
208             my $self = shift;
209             return $self->_do('delete', @_);
210             }
211              
212             sub _do {
213             my $self = shift;
214             my $meth = shift;
215             my $entry = shift;
216              
217             my $item = $self->_from_atom($entry);
218             $item = eval { $self->{backend}->$meth($item) };
219             return undef if $@;
220             return $self->_to_atom($item);
221              
222             }
223              
224             # TODO all the other fields
225              
226             # take an atom entry and turn it into something useful
227             sub _from_atom {
228             my $self = shift;
229             my $entry = shift;
230             my $e = Net::Google::Calendar::Entry->new(\$entry);
231             my $event = Data::ICal::Entry::Event->new;
232              
233             my ($start, $end) = $e->when;
234              
235             $event->uid($e->id) if $e->id;
236             $event->summary($e->title);
237             $event->description($e->content);
238             $event->start($start);
239             $event->end($end);
240             $event->recurrence($e->recurrence);
241              
242             return $event;
243             }
244              
245              
246             # create an atom entry
247             sub _to_atom {
248             my $self = shift;
249             my $entry = shift;
250             my $e = Net::Google::Calendar::Entry->new;
251            
252             my $start = $entry->start;
253             my $end = $entry->when;
254              
255             $e->id($entry->uid) if $entry->uid;
256             $e->title($entry->summary);
257             $e->content($entry->description);
258             $e->when($start, $end);
259             $e->recurrence($entry->recurrence);
260            
261             return $e;
262             }
263              
264             =head2 handle_request
265              
266             Requires a subclass of C.
267              
268             =cut
269              
270             sub handle_request {
271             my $self = shift;
272             my $handler = shift;
273              
274             my $auth = $self->auth;
275             my $r_method = $handler->request_method();
276             my $x_method = $handler->header_in('X-HTTP-Method-Override');
277              
278             my $path = $handler->path;
279              
280             # NOTES on login
281             # 1. We should be able to handle a magic cookie URL - note that this automatically returns a full list of events
282             # 2. Stuff is POSTed to accounts/ClientLogin, not GETed (DONE)
283             # 3. We need to be able to handle different feed types full/basic/private
284             # 4. Once we've AUTHed we should redirect to the private feed
285             # 5. Should this be moved into ::Server somehow?
286              
287             if ($r_method eq 'GET' && $path =~ m!feeds/([^/]+)/private-([^/]+)/full!) {
288             my $email = $1;
289             my $cookie = $2;
290             my $session_id = $auth->magic_cookie_auth($email, $cookie);
291             return $handler->error('Invalid magic cookie auth', 'FORBIDDEN') unless $session_id;
292             # generate session id (from auth)
293             # redirect to feed with session id
294             }
295              
296              
297              
298             # is this the login url?
299             # accounts/ClientLogin
300             if ($r_method eq 'POST' && $path eq 'accounts/ClientLogin') {
301             # first off get the email and password
302             my $email = $handler->header_in('email');
303             my $pass = $handler->header_in('password');
304             # get the auth key and hand it back
305             my $key = $auth->validate($email, $pass);
306             unless (defined $key) {
307             return $handler->error("Login failed", 'DECLINED');
308             }
309             return $handler->send_reponse( type => 'text/plain', body => 'Auth=$key', code => 'OK' );
310             }
311              
312             # if they're up to here then they must have an Auth key
313             my $key = $handler->header_in('Authorization');
314              
315             # TODO session id
316              
317             unless (defined $key && $key =~ s!GoogleLogin auth=!!) {
318             return $handler->error("You must pass an Authorization key", 'AUTH_REQUIRED');
319             }
320             unless ($auth->auth($key)) {
321             return $handler->error("Login Failed", 'FORBIDDEN');
322             }
323             my $r_content;
324             # Fetch entries
325             # method=GET
326             if ($r_method eq 'GET') {
327              
328             # get params
329             my %opts = $handler->get_args();
330             # get categories
331             my @categories = split '/', $path;
332             shift @categories if $categories[0] eq '-';
333             $opts{'categories'} = [ @categories ];
334             $r_content = eval { $self->fetch(%opts) };
335              
336             # method=Everything else
337             } else {
338              
339             my %map = ( POST => 'create', PUT => 'update', DELETE => 'delete' );
340             $r_method = $x_method if defined $x_method && $r_method eq 'POST';
341             return $handler->error("No such method: $r_method") unless defined $map{$r_method};
342              
343             my $method = $map{$r_method};
344             my $content = $handler->header_in('Content');
345             return $handler->error("No content") unless defined $content;
346              
347             my $backend = $self->backend();
348             $r_content = eval { $backend->$method($content) };
349              
350             }
351              
352             return $handler->error($@) if $@;
353             return $handler->error("Got not content back from $r_method") unless defined $r_content;
354              
355             return $handler->send_response( type => 'application/atom+xml', body => $r_content->as_xml, code => 'OK' );
356              
357              
358             }
359              
360             =head1 SEE ALSO
361              
362             L
363              
364             L
365              
366             The Lucene implementation of the GData server.
367             http://wiki.apache.org/lucene-java/GdataServer
368              
369             =head1 SUBVERSION
370              
371             https://svn.unixbeard.net/simon/Net-Google-Calendar-Server/
372              
373             =head1 AUTHOR
374              
375             Simon Wistow
376              
377             =head1 COPYRIGHT
378              
379             Copyright 2006, 2007 - Simon Wistow
380              
381             =cut
382              
383             1;