File Coverage

blib/lib/Net/Google/Calendar.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Net::Google::Calendar;
2             {
3             $Net::Google::Calendar::VERSION = '1.05';
4             }
5              
6 5     5   3668 use strict;
  5         8  
  5         310  
7 5     5   5570 use LWP::UserAgent;
  5         292384  
  5         170  
8 5     5   11900 use HTTP::Cookies;
  5         54473  
  5         177  
9 5     5   42 use HTTP::Request;
  5         9  
  5         117  
10 5     5   27 use HTTP::Headers;
  5         9  
  5         156  
11 5     5   4365 use HTTP::Request::Common;
  5         18838  
  5         407  
12 5     5   7344 use XML::Atom::Feed;
  0            
  0            
13             use XML::Atom::Entry;
14             use Data::Dumper;
15             use Net::Google::AuthSub;
16             use Net::Google::Calendar::Entry;
17             use Net::Google::Calendar::Person;
18             use Net::Google::Calendar::Calendar;
19             use URI;
20             use URI::Escape;
21             use Carp qw(confess);
22              
23             use vars qw($VERSION $APP_NAME $REDIRECT_MAX);
24              
25             $APP_NAME = $Net::Google::OAuth::APP_NAME = __PACKAGE__."-${VERSION}";
26              
27             $REDIRECT_MAX = 10; #Maximum number of redirects to allow
28              
29             # ABSTRACT: Interface to Google calendars
30              
31             =head1 NAME
32              
33             Net::Google::Calendar - programmatic access to Google's Calendar API
34              
35              
36             =head1 SYNOPSIS
37              
38             # this will only get you a read only feed
39             my $cal = Net::Google::Calendar->new( url => $private_url );
40              
41             or
42              
43             # this will get you a read-write feed.
44             my $cal = Net::Google::Calendar->new;
45             $cal->login($username, $password);
46              
47             or
48              
49             # this will also get you a read-write feed
50             my $cal = Net::Google::Calendar->new;
51             $cal->auth($username, $auth_token);
52              
53             or
54             # this will again get you a read-write feed
55             my $cal = Net::Google::Calendar->new;
56             $cal->oauth(Net::Google::OAuth);
57              
58             or you can pass in a url to specify a particular calendar
59              
60             my $cal = Net::Google::Calendar->new( url => $non_default_url );
61             $cal->login($username, $password);
62             # or $cal->auth($username, $auth_token) obviously
63              
64              
65             then
66              
67             for ($cal->get_events()) {
68             print $_->title."\n";
69             print $_->content->body."\n*****\n\n";
70             }
71              
72             my $c;
73             for ($cal->get_calendars) {
74             print $_->title."\n";
75             print $_->id."\n\n";
76             $c = $_ if ($_->title eq 'My Non Default Calendar');
77             }
78             $cal->set_calendar($c);
79             print $cal->id." has ".scalar($cal->get_events)." events\n";
80              
81              
82             # everything below here requires a read-write feed
83             my $entry = Net::Google::Calendar::Entry->new();
84             $entry->title($title);
85             $entry->content("My content");
86             $entry->location('London, England');
87             $entry->transparency('transparent');
88             $entry->status('confirmed');
89             $entry->when(DateTime->now, DateTime->now() + DateTime::Duration->new( hours => 6 ) );
90              
91              
92             my $author = Net::Google::Calendar::Person->new();
93             $author->name('Foo Bar');
94             $author->email('foo@bar.com');
95             $entry->author($author);
96              
97             By default new or updated entries are modified in place with
98             any new information provided by Google.
99              
100             $cal->add_entry($entry);
101              
102             $entry->content('Updated');
103             $cal->update_entry($entry);
104              
105             $cal->delete_entry($entry);
106              
107             However if you don't want the entry updated in place pass
108             C in to the C method.
109              
110             my $cal = Net::Google::Calendar->new( no_event_modification => 1 );
111             $cal->login($user, $pass);
112            
113             my $tmp = $cal->add_entry($entry);
114             die "Couldn't add event: $@\n" unless defined $tmp;
115              
116             print "Events=".scalar($cal->get_events())."\n";
117              
118             $tmp->content('Updated');
119              
120             $tmp = $cal->update_entry($tmp) || die "Couldn't update ".$tmp->id.": $@\n";
121              
122             $cal->delete_entry($tmp) || die "Couldn't delete ".$tmp->id.": $@\n";
123              
124              
125              
126             =head1 DESCRIPTION
127              
128             Interact with Google's new calendar using the GData API.
129              
130              
131             =head1 AUTHENTICATION AND READ-WRITE CALENDARS
132              
133             There are effectively four ways to get events from a Google calendar.
134              
135             You can get any public events by querying
136              
137             http://www.google.com/calendar/feeds//public/full
138              
139             Then there are the three ways to get private entries. The first of these
140             involves a magic cookie in the url like this:
141              
142             http://www.google.com/calendar/feeds//private-/full
143              
144             Google has information on how to find this url here
145              
146             http://code.google.com/apis/calendar/developers_guide_protocol.html#find_feed_url
147              
148             To use either the private or public feeds do
149              
150             my $cal = Net::Google::Calendar->new( url => $url);
151              
152             Both these feeds will be read only however. This means that you won't be able to
153             add, update or delete entries.
154              
155             You can also get all the private entries in a read-write feed by either logging in
156             or using C.
157              
158             Logging in is the easiest. Simply do
159              
160             my $cal = Net::Google::Calendar->new;
161             $cal->login($username, $password);
162              
163             Where C<$username> and C<$password> are the same as if you were logging into the
164             Google Calendar site.
165              
166             Alternatively if you don't want to use username and password (if, for example you were
167             providing Calendar reading as a service on your website and didn't want to have to ask
168             your users for their Google login details) you can use C.
169              
170             http://code.google.com/apis/accounts/AuthForWebApps.html
171              
172             Once you have an AuthSub token (or you user has supplied you with one)
173             then you can login using
174              
175             my $cal = Net::Google::Calendar->new;
176             $cal->auth($username, $token);
177              
178             =head1 METHODS
179              
180             =cut
181              
182             =head2 new
183              
184             Create a new instance. C is a hash which must contain your private Google url
185             as the key C unless you plan to log in or authenticate.
186              
187             See
188              
189             http://code.google.com/apis/gdata/calendar.html#find_feed_url
190              
191             for how to get that.
192              
193             If you pass the option C as a psotive value then
194             add_entry and update_entry will not modify the entry in place.
195              
196             =cut
197              
198             sub new {
199             my ($class, %opts) = @_;
200             $opts{_ua} = LWP::UserAgent->new( max_redirect => 0 );
201             $opts{_ua}->env_proxy;
202             $opts{_auth} = Net::Google::AuthSub->new( service => 'cl' );
203             $opts{_cookie_jar} = HTTP::Cookies->new;
204             $opts{no_event_modification} ||= 0;
205             my $self = bless \%opts, $class;
206             $self->_find_calendar_id if $opts{url};
207             return $self;
208             }
209              
210              
211             =head2 login [opt[s]]
212              
213             Login to google.
214              
215             Can optionally take a hash of options which will override the
216             default login params.
217              
218             =over 4
219              
220             =item service
221              
222             Name of the Google service for which authorization is requested.
223              
224             Defaults to 'cl' for calendar.
225              
226             =item source
227              
228             Short string identifying your application, for logging purposes.
229              
230             Defaults to 'Net::Google::Calendar-'
231              
232             =item accountType
233              
234             Type of account to be authenticated.
235              
236             Defaults to 'HOSTED_OR_GOOGLE'.
237              
238             =back
239              
240             See http://code.google.com/apis/accounts/AuthForInstalledApps.html#ClientLogin for more details.
241              
242             =cut
243              
244             sub login {
245             my $self = shift;
246             my $user = shift;
247             my $pass = shift;
248             my $r = $self->{_auth}->login($user, $pass);
249             my $error;
250             if (!defined $r) {
251             $error = $@;
252             } elsif (!$r->is_success) {
253             $error = $r->error;
254             }
255             die "Couldn't log in - $error" if defined $error;
256              
257             $self->{user} = $user;
258             $self->_generate_url();
259             return 1;
260             }
261              
262              
263             =head2 auth
264              
265             Use the AuthSub method for calendar access.
266             See http://code.google.com/apis/accounts/AuthForWebApps.html
267             for details.
268              
269              
270             =cut
271              
272             sub auth {
273             my $self = shift;
274             my $user = shift;
275             my $token = shift;
276             $self->{_auth}->auth($user, $token);
277             $self->{user} = $user;
278             $self->_generate_url();
279             return 1;
280             }
281              
282             =head2 oauth Net::Google::OAuth
283              
284             Use OAuth for calendar access
285              
286             =cut
287              
288             sub oauth {
289             my $self = shift;
290             $self->{_auth} = shift;
291             }
292              
293             sub _generate_url {
294             my $self= shift;
295             $self->{url} ||= $self->_get_protocol()."://google.com/calendar/feeds/$self->{user}/private/full";
296             $self->{url} =~ s!/private-[^/]+!/private!;
297             $self->_find_calendar_id;
298              
299             }
300              
301             =head2 auth_object [Net::Google::AuthSub]
302              
303             Get or set the current C object.
304              
305             =cut
306             sub auth_object {
307             my $self = shift;
308             $self->{_auth} = shift if @_;
309             return $self->{_auth};
310             }
311              
312             sub _find_calendar_id {
313             my $self = shift;
314             ($self->{calendar_id}) = ($self->{url} =~ m!/feeds/([^/]+)/!);
315             }
316              
317             =head2 ssl bool
318              
319             Use ssl or not. Auth tokens (AuthSub and OAuth) have a scope that includes http:// or https://. Make sure you use ssl(1) if your scope is https://www.google.com/calendar/feeds/.
320              
321             =cut
322              
323             sub ssl {
324             my $self = shift;
325             $self->{_use_ssl} = shift;
326             }
327              
328             sub _get_protocol {
329             my $self = shift;
330             if ($self->{_use_ssl}) {
331             return 'https';
332             }
333             return 'http';
334             }
335              
336             =head2 get_events [ %opts ]
337              
338             Return a list of Net::Google::Calendar::Entry objects;
339              
340             You can pass in a hash of options which map to the Google Data API's generic
341             searching mechanisms plus the specific calendar ones.
342              
343             See
344              
345             http://code.google.com/apis/gdata/protocol.html#query-requests
346              
347             for more details.
348              
349              
350             =over 4
351              
352             =item q
353              
354             Full-text query string
355              
356             When creating a query, list search terms separated by spaces, in the
357             form q=term1 term2 term3. (As with all of the query parameter values,
358             the spaces must be URL encoded.) The GData service returns all entries
359             that match all of the search terms (like using AND between terms). Like
360             Google's web search, a GData service searches on complete words (and
361             related words with the same stem), not substrings.
362              
363             To search for an exact phrase, enclose the phrase in quotation marks:
364              
365             q => '"exact phrase'
366              
367             To exclude entries that match a given term, use the form
368              
369             q => '-term'
370              
371             The search is case-insensitive.
372              
373             Example: to search for all entries that contain the exact phrase
374             'Elizabeth Bennet' and the word 'Darcy' but don't contain the word
375             'Austen', use the following query:
376              
377             q => '"Elizabeth Bennet" Darcy -Austen'
378              
379              
380             =item category
381              
382             Category filter
383              
384             To search in just one category do
385              
386             category => 'Fritz'
387              
388             You can query on multiple categories by listing multiple category parameters. For example
389              
390             category => [ 'Fritz', 'Laurie' ]
391              
392             returns entries that match both categories.
393              
394              
395             To do an OR between terms, use a pipe character (|). For example
396              
397              
398             category => 'Fritz|Laurie'
399              
400             returns entries that match either category.
401              
402             To exclude entries that match a given category, use the form
403              
404             category => '-categoryname'
405              
406             You can, of course, mix and match
407              
408             [ 'Jo', 'Fritz|Laurie', '-Simon' ]
409              
410             means in category
411              
412             (Jo AND ( Fritz OR Laurie ) AND (NOT Simon))
413              
414              
415             =item author
416              
417             Entry author
418              
419             The service returns entries where the author name and/or email address
420             match your query string.
421              
422             =item updated-min
423              
424             =item updated-max
425              
426             Bounds on the entry publication date.
427              
428             Use DateTime objects or the RFC 3339 timestamp format. For example:
429             2005-08-09T10:57:00-08:00.
430              
431             The lower bound is inclusive, whereas the upper bound is exclusive.
432              
433             =item start-min
434              
435             =item start-max
436              
437             Respectively, the earliest event start time to match (If not specified,
438             default is 1970-01-01) and the latest event start time to match (If
439             not specified, default is 2031-01-01).
440              
441             Use DateTime objects or the RFC 3339 timestamp format. For example:
442             2005-08-09T10:57:00-08:00.
443              
444             The lower bound is inclusive, whereas the upper bound is exclusive.
445              
446             =item start-index
447              
448             1-based index of the first result to be retrieved
449              
450             Note that this isn't a general cursoring mechanism. If you first send a
451             query with
452              
453             start-index => 1,
454             max-results => 10
455              
456             and then send another query with
457              
458             start-index => 11,
459             max-results => 10
460              
461             the service cannot guarantee that the results are equivalent to
462            
463             start-index => 1
464             max-results => 20
465              
466             because insertions and deletions could have taken place in between the
467             two queries.
468              
469             =item max-results
470              
471             Maximum number of results to be retrieved.
472              
473             For any service that has a default max-results value (to limit default
474             feed size), you can specify a very large number if you want to receive
475             the entire feed.
476              
477             =item entryID
478              
479             ID of a specific entry to be retrieved.
480              
481             If you specify an entry ID, you can't specify any other parameters.
482              
483             =back
484              
485             =cut
486              
487             sub get_events {
488             my ($self, %opts) = @_;
489              
490              
491             # check for DateTime objects and convert them to RFC 3339
492             for (keys %opts) {
493             next unless UNIVERSAL::isa($opts{$_}, 'DateTime');
494             # maybe we should chuck an error if it's a Ref and *not* a DateTime
495             #next unless $opts{$_}->isa('DateTime');
496             $opts{$_} = $opts{$_}->iso8601 . 'Z';
497             }
498              
499             my $url = URI->new($self->{url});
500              
501             # special handling for single entryID lookup
502             if (exists $opts{entryID}) {
503             if (scalar(keys %opts)>1) {
504             $@ = "You can't specify entryID and anything else";
505             return undef;
506             }
507             my $path = $url->path;
508             $url->path("$path/".$opts{entryID});
509             return $self->_get_entry("$url", "Net::Google::Calendar::Entry");
510             }
511              
512             if (exists $opts{category} && 'ARRAY' eq ref($opts{category})) {
513             my $path = $url->path."/".join("/", ( '-', @{delete $opts{category}}));
514             $url->path("$path");
515             }
516              
517             $url->query_form(%opts);
518             $self->_get("$url", "Net::Google::Calendar::Entry");
519             }
520              
521              
522             =head2 add_entry
523              
524             Create a new entry.
525              
526             Returns the new entry with extra data provided by Google but will
527             also modify the entry in place unless the C
528             option is passed to C.
529              
530             Returns undef on failure.
531              
532             =cut
533              
534             sub add_entry {
535             my ($self, $entry) = @_;
536              
537             # TODO for neatness' sake we could make calendar_id = 'default' when calendar_id = user
538             my $url = $self->_get_protocol()."://www.google.com/calendar/feeds/$self->{calendar_id}/private/full";
539             push @_, ($url, 'POST');
540             goto $self->can('_do');
541             }
542              
543              
544             =head2 delete_entry
545              
546             Delete a given entry.
547              
548             Returns undef on failure or the old entry on success.
549              
550             =cut
551              
552             sub delete_entry {
553             my ($self, $entry) = @_;
554             my $force = (scalar(@_)>2)? pop @_ : 0;
555             my $url = $entry->edit_url($force) || return undef;
556             push @_, ($url, 'DELETE');
557             goto $self->can('_do');
558             }
559              
560             =head2 update_entry
561              
562             Update a given entry.
563              
564             Returns the updated entry with extra data provided by Google but will
565             also modify the entry in place unless the C
566             option is passed to C.
567              
568             Returns undef on failure.
569              
570             =cut
571              
572             sub update_entry {
573             my ($self, $entry) = @_;
574             my $url = $entry->edit_url || return undef;
575             push @_, ($url, 'PUT');
576             goto $self->can('_do');
577             }
578              
579             =head2 get_calendars
580              
581             Get a list of all of a user's Calendars as C objects.
582              
583             If C is true then only get the ones a user owns.
584            
585             =cut
586              
587             sub get_calendars {
588             my $self = shift;
589             my $owned = shift || 0;
590             my $which = ($owned)? "owncalendars" : "allcalendars";
591             my $url = $self->_get_protocol()."://www.google.com/calendar/feeds/default/$which/full";
592             return $self->_get("$url", "Net::Google::Calendar::Calendar");
593             }
594              
595              
596             sub _get {
597             my ($self, $url, $class, %opts) = @_;
598             my $feed = $self->get_feed(URI->new("$url"), %opts);
599             return map { bless $_, $class; $_->_initialize(); $_ } $feed->entries;
600             }
601              
602             =head2 get_feed [feed] [opt[s]]
603              
604             If C is a C object then feed is fetch remotely.
605             Otherwise it is assumed to be XML data and is parsed.
606              
607             Returns an C object.
608              
609             =cut
610              
611             sub get_feed {
612             my ($self, $feed, %opts) = @_;
613             if (ref($feed)){
614             return $feed if $feed->isa('XML::Atom::Feed');
615             if ($feed->isa('URI')) {
616             my %params = ($self->{_auth}->auth_params('GET', $feed), %opts);
617             my $r = $self->{_ua}->get("$feed", %params);
618              
619             my $redirect_tries = 0;
620             while ($r->code == 302 || $r->code == 301) {
621             my $location = $r->header('location');
622             %params = ($self->{_auth}->auth_params('GET', $location), %opts);
623             $r = $self->{_ua}->get($location, %params);
624             $redirect_tries++;
625             die "Too many redirects ($redirect_tries)"
626             if $redirect_tries > $REDIRECT_MAX;
627             }
628              
629             die $r->status_line unless $r->is_success;
630             $feed = $r->content;
631             }
632             }
633             return XML::Atom::Feed->new(\$feed);
634             }
635              
636             =head2 update_feed
637              
638             Take an C object with a C link and post it.
639              
640             =cut
641              
642             sub update_feed {
643             my ($self, $feed) = @_;
644             #my $uri = Net::Google::Calendar::Base::_generic_url($feed, 'http://schemas.google.com/g/2005#post') || die("Couldn't get url");
645             my $uri = Net::Google::Calendar::Base::_generic_url($feed, 'edit') || die("Couldn't get url");
646             push @_, ($uri, 'POST');
647             goto $self->can('_do');
648             }
649              
650             # TODO collapse this with _get somehow
651             sub _get_entry {
652             my ($self, $url, $class) = @_;
653             my %params = ($self->{_auth}->auth_params);
654             my $r = $self->{_ua}->get("$url", %params);
655            
656             if (!$r->is_success) {
657             if ($r->code == 404) {
658             $@ = "EntryID not found";
659             } else {
660             $@ = $r->status_line;
661             }
662             return;
663             }
664             my $atom = $r->content;
665              
666             my $entry = XML::Atom::Entry->new(\$atom);
667             $entry = bless $entry, $class;
668             $entry->_initialize();
669             return $entry;
670             }
671              
672             =head2 set_calendar
673              
674             Set the current calendar to use.
675              
676             =cut
677              
678             sub set_calendar {
679             my $self = shift;
680             my $cal = shift;
681              
682             ($self->{calendar_id}) = (uri_unescape($cal->id) =~ m!([^/]+)$!);
683             $self->{url} = $self->_get_protocol()."://www.google.com/calendar/feeds/$self->{calendar_id}/private/full";
684             }
685              
686              
687             =head2 add_calendar
688              
689             Create a new calendar
690              
691             Returns the new calendar with extra data provided by Google but will
692             also modify the entry in place unless the C
693             option is passed to C.
694              
695             Returns undef on failure.
696              
697             =cut
698              
699             sub add_calendar {
700             my ($self, $entry) = @_;
701             my $url = $self->_get_protocol()."://www.google.com/calendar/feeds/$self->{calendar_id}/owncalendars/full";
702             push @_, ($url, 'POST');
703             goto $self->can('_do');
704             }
705              
706             =head2 update_calendar
707              
708             Update a calendar.
709              
710             Returns the updated calendar with extra data provided by Google but will
711             also modify the entry in place unless the C
712             option is passed to C.
713              
714             Returns undef on failure.
715              
716             =cut
717              
718             sub update_calendar {
719             my $self = shift;
720             $self->update_entry(@_);
721             }
722              
723              
724             =head2 delete_calendar [force]
725              
726             Delete a given calendar.
727              
728             Returns undef on failure or the old entry on success.
729              
730             Note that, at the moment, only C objects returned
731             by C with the C parameter set to C
732             can be deleted (unlike editing - I don't know if this is a Google
733             bug or not).
734              
735             However, you can pass in an optional true C parameter to this
736             method that will allow C objects returned by C
737             where no positive C paramemter was passed to be deleted. It uses
738             an egregious hack though and might suddenly stop working if Google change
739             things or I suddenly decide to remove it.
740              
741             =cut
742              
743              
744             sub delete_calendar {
745             my $self = shift;
746             $self->delete_entry(@_);
747             }
748              
749             sub _do {
750             my ($self, $entry, $url, $method) = @_;
751              
752             unless (defined $self->{_auth}) {
753             $@ = "You must log in to do a $method\n";
754             return undef;
755             }
756             my $class = ref($entry);
757             my $xml = eval { $entry->as_xml };
758             confess($@) if $@;
759             _utf8_off($xml);
760             my %params = $self->{_auth}->auth_params;
761             $params{Content_Type} = 'application/atom+xml; charset=UTF-8';
762             $params{Content} = $xml;
763             $params{'X-HTTP-Method-Override'} = $method unless "POST" eq $method;
764              
765             if (defined $self->{_session_id} && !$self->{_force_no_session_id}) {
766             my $tmp = URI->new($url);
767             $tmp->query_form({ gsessionid => $self->{_session_id} });
768             $url = "$tmp";
769             }
770              
771            
772              
773             REQUEST: while (1) {
774             my $rq = POST "$url", %params;
775             $self->{_cookie_jar}->add_cookie_header($rq);
776             #my $h = HTTP::Headers->new(%params);
777             #my $rq = HTTP::Request->new($method => $url, $h);
778             my $r = $self->{_ua}->request( $rq );
779             $self->{_cookie_jar}->extract_cookies($r);
780             my $redirect_tries = 0;
781             while (302 == $r->code || 301 == $r->code) {
782             $url = $r->header('location');
783             my %args = URI->new($url)->query_form;
784             $self->{_session_id} = $args{gsessionid};
785             $redirect_tries++;
786             die "Too many redirects ($redirect_tries)"
787             if $redirect_tries > $REDIRECT_MAX;
788             next REQUEST;
789             }
790             #print $rq->as_string unless $params{'X-HTTP-Method-Override'} ;
791              
792             if (!$r->is_success) {
793             $@ = $r->status_line." - ".$r->content." - $url";
794             return undef;
795             }
796             my $c = $r->content;
797             if (defined $c && length($c)) {
798             my $tmp = $class->new(Stream => \$c);
799             $_[1] = $tmp unless $self->{no_event_modification};
800             return $tmp;
801             } else {
802             # in the case of DELETE should we return 1 instead?
803             return $entry;
804             }
805             }
806              
807              
808             }
809              
810             sub _utf8_off {
811             if ($] >= 5.008) {
812             require Encode;
813             return Encode::_utf8_off($_[0]);
814             }
815             }
816              
817             =head1 WARNING
818              
819             This is ALPHA level software.
820              
821             Don't use it. Ever. Or something.
822              
823             =head1 TODO
824              
825             Abstract this out to Net::Google::Data
826              
827             =head1 LATEST VERSION
828              
829             The latest version can always be obtained from my
830             Subversion repository.
831              
832             http://svn.unixbeard.net/simon/Net-Google-Calendar
833              
834             =head1 AUTHOR
835              
836             Simon Wistow
837              
838             =head1 COPYRIGHT
839              
840             Copyright Simon Wistow, 2006
841              
842             Distributed under the same terms as Perl itself.
843              
844             =head1 SEE ALSO
845              
846             http://code.google.com/apis/gdata/calendar.html
847              
848             =cut
849             1;