File Coverage

blib/lib/Net/CardDAVTalk.pm
Criterion Covered Total %
statement 32 248 12.9
branch 0 118 0.0
condition 0 45 0.0
subroutine 11 30 36.6
pod 17 17 100.0
total 60 458 13.1


line stmt bran cond sub pod time code
1             package Net::CardDAVTalk;
2              
3 2     2   72405 use 5.006;
  2         7  
4 2     2   10 use strict;
  2         4  
  2         47  
5 2     2   9 use warnings FATAL => 'all';
  2         5  
  2         67  
6              
7              
8 2     2   545 use Net::DAVTalk;
  2         1018231  
  2         78  
9 2     2   20 use base qw(Net::DAVTalk);
  2         5  
  2         177  
10              
11 2     2   10 use Carp;
  2         5  
  2         125  
12 2     2   823 use Text::VCardFast qw(vcard2hash);
  2         4730  
  2         144  
13 2     2   26 use XML::Spice;
  2         5  
  2         18  
14 2     2   75 use URI::Escape qw(uri_unescape);
  2         4  
  2         78  
15 2     2   773 use Net::CardDAVTalk::VCard;
  2         6  
  2         88  
16 2     2   20 use Data::Dumper;
  2         20  
  2         5199  
17              
18              
19             =head1 NAME
20              
21             Net::CardDAVTalk - A library for talking to CardDAV servers
22              
23             =head1 VERSION
24              
25             Version 0.07
26              
27             =cut
28              
29             our $VERSION = '0.07';
30              
31             our $BATCHSIZE = 100;
32              
33              
34             =head1 SYNOPSIS
35              
36             This module maps from CardDAV to an old version of the FastMail API.
37             It's mostly useful as an example of how to talk CardDAV and for the
38             Cyrus IMAP test suite Cassandane.
39              
40             use Net::CardDAVTalk;
41              
42             my $foo = Net::CardDAVTalk->new();
43             ...
44              
45             =head1 SUBROUTINES/METHODS
46              
47             =head2 $class->new()
48              
49             Takes the same arguments as Net::DAVTalk and adds the single
50             namespace:
51              
52             C => 'urn:ietf:params:xml:ns:carddav'
53              
54             =cut
55              
56             sub new {
57 0     0 1   my ($Class, %Params) = @_;
58              
59 0           $Params{homesetns} = 'C';
60 0           $Params{homeset} = 'addressbook-home-set';
61 0           $Params{wellknown} = 'carddav';
62              
63 0           my $Self = $Class->SUPER::new(%Params);
64              
65 0           $Self->ns(C => 'urn:ietf:params:xml:ns:carddav');
66 0           $Self->ns(CY => 'http://cyrusimap.org/ns/');
67              
68 0           return $Self;
69             }
70              
71             # Address book methods {{{
72              
73             =head2 $self->NewAddressBook($Path, %Args)
74              
75             Creates a new addressbook collection. Requires the full
76             path (unlike Net::CalDAVTalk, which creates paths by UUID)
77             and takes a single argument, the name:
78              
79             e.g.
80              
81             $CardDAV->NewAddressBook("Default", name => "Addressbook");
82              
83             =cut
84              
85             sub NewAddressBook {
86 0     0 1   my ($Self, $Path, %Args) = @_;
87              
88 0 0         $Path || confess 'New address book path not specified';
89              
90             $Self->Request(
91             'MKCOL',
92             "$Path/",
93             x('D:mkcol', $Self->NS(),
94             x('D:set',
95             x('D:prop',
96             x('D:resourcetype',
97             x('D:collection'),
98             x('C:addressbook'),
99             ),
100 0           x('D:displayname', $Args{name}),
101             ),
102             ),
103             ),
104             );
105              
106 0           return $Path;
107             }
108              
109             =head2 $self->DeleteAddressBook($Path)
110              
111             Deletes the addressbook at the given path
112              
113             e.g.
114              
115             $CardDAV->DeleteAddressBook("Shared");
116              
117             =cut
118              
119             sub DeleteAddressBook {
120 0     0 1   my ($Self, $Path) = @_;
121              
122 0 0         $Path || confess 'Delete address book path not specified';
123              
124 0           $Self->Request(
125             'DELETE',
126             "$Path/"
127             );
128              
129 0           return 1;
130             }
131              
132             =head2 $self->UpdateAddressBook($Path, %Args)
133              
134             Like 'new', but for an existing addressbook. For now, can only change
135             the name.
136              
137             e.g.
138              
139             $CardDAV->UpdateAddressBook("Default", name => "My Happy Addressbook");
140              
141             =cut
142              
143             sub UpdateAddressBook {
144 0     0 1   my ($Self, $Path, %Args) = @_;
145              
146 0 0         $Path || confess 'Update address book path not specified';
147              
148 0           my @Params;
149              
150 0 0         if (defined $Args{name}) {
151 0           push @Params, x('D:displayname', $Args{name});
152             }
153              
154 0 0         return undef unless @Params;
155              
156 0           $Self->Request(
157             'PROPPATCH',
158             "$Path/",
159             x('D:propertyupdate', $Self->NS(),
160             x('D:set',
161             x('D:prop',
162             @Params,
163             ),
164             ),
165             ),
166             );
167              
168 0           return 1;
169             }
170              
171             =head2 $self->GetAddressBook($Path, %Args)
172              
173             Calls 'GetAddressBooks' with the args, and greps for the one with the
174             matching path.
175              
176             e.g.
177              
178             my $AB = $CardDAV->GetAddressBook("Default");
179              
180             =cut
181              
182             sub GetAddressBook {
183 0     0 1   my ($Self, $Id, %Args) = @_;
184              
185 0           my $Data = $Self->GetAddressBooks(%Args);
186              
187 0 0         die "Can't read data" unless $Data;
188 0           my ($AddressBook) = grep { $_->{path} eq $Id } @$Data;
  0            
189              
190 0           return $AddressBook;
191             }
192              
193             =head2 $self->GetAddressBooks(%Args)
194              
195             Get all the addressbooks on the server. If the arg 'Sync' is true,
196             also requests the DAV:sync-token and returns it as syncToken in the
197             addressbook hash.
198              
199             Returns an arrayref of hashrefs
200              
201             e.g.
202              
203             my $ABs = $CardDAV->GetAddressBooks(Sync => 1);
204             foreach my $AB (@$ABs) {
205             say "$AB->{path}: $AB->{name}";
206             }
207              
208             =cut
209              
210             sub GetAddressBooks {
211 0     0 1   my ($Self, %Args) = @_;
212              
213 0           my @props;
214 0 0         if ($Args{Sync}) {
215 0           push @props, x('D:sync-token');
216             }
217              
218 0           my $Response = $Self->Request(
219             'PROPFIND',
220             '',
221             x('D:propfind', $Self->NS(),
222             x('D:prop',
223             x('D:displayname'),
224             x('D:resourcetype'),
225             x('D:current-user-privilege-set'),
226             x('D:acl'),
227             @props,
228             ),
229             ),
230             Depth => 1,
231             );
232              
233 0           my @AddressBooks;
234              
235 0           my $NS_C = $Self->ns('C');
236 0           my $NS_D = $Self->ns('D');
237 0           my $NS_CY = $Self->ns('CY');
238 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
239             my $HRef = $Response->{"{$NS_D}href"}{content}
240 0   0       || next;
241 0           my $Path = $Self->_unrequest_url($HRef);
242              
243 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
244 0 0         next unless $Propstat->{"{$NS_D}prop"}{"{$NS_D}resourcetype"}{"{$NS_C}addressbook"};
245              
246             # XXX - this is really quite specific and probably wrong-namespaced...
247 0           my $Perms = $Propstat->{"{$NS_D}prop"}{"{$NS_D}current-user-privilege-set"}{"{$NS_D}privilege"};
248              
249 0           my @ShareWith;
250 0           my $ace = $Propstat->{"{$NS_D}prop"}{"{$NS_D}acl"}{"{$NS_D}ace"};
251 0 0 0       $ace = [] unless ($ace and ref($ace) eq 'ARRAY');
252 0           foreach my $Acl (@$ace) {
253 0 0         next if $Acl->{"{$NS_D}protected"}; # ignore admin ACLs
254 0   0       my $user = uri_unescape($Acl->{"{$NS_D}principal"}{"{$NS_D}href"}{content} // '');
255 0 0         next unless $user =~ m{^/dav/principals/user/([^/]+)};
256 0           my $email = $1;
257 0 0         next if $email eq 'admin';
258 0           my %ShareObject = (
259             email => $email,
260             mayAdmin => $JSON::false,
261             mayWrite => $JSON::false,
262             mayRead => $JSON::false,
263             );
264 0           foreach my $item (@{$Acl->{"{$NS_D}grant"}{"{$NS_D}privilege"}}) {
  0            
265 0 0         $ShareObject{'mayAdmin'} = $JSON::true if $item->{"{$NS_CY}admin"};
266 0 0         $ShareObject{'mayWrite'} = $JSON::true if $item->{"{$NS_D}write-content"};
267 0 0         $ShareObject{'mayRead'} = $JSON::true if $item->{"{$NS_D}read"};
268             }
269              
270 0           push @ShareWith, \%ShareObject;
271             }
272              
273             my %AddressBook = (
274             href => $HRef,
275             path => $Path,
276             name => ($Propstat->{"{$NS_D}prop"}{"{$NS_D}displayname"}{content} || ''),
277 0 0         isReadOnly => (grep { exists $_->{"{$NS_D}write-content"} } @{$Perms || []}) ? $JSON::false : $JSON::true,
  0            
278 0 0         mayRead => (grep { exists $_->{"{$NS_D}read"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0            
279 0 0         mayWrite => (grep { exists $_->{"{$NS_D}write-content"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0            
280 0 0 0       mayAdmin => (grep { exists $_->{"{$NS_CY}admin"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0 0          
  0 0          
    0          
    0          
    0          
281             shareWith => (@ShareWith ? \@ShareWith : $JSON::false),
282             );
283 0 0         if ($Args{Sync}) {
284 0   0       $AddressBook{syncToken} = $Propstat->{"{$NS_D}prop"}{"{$NS_D}sync-token"}{content} || '';
285             }
286 0           push @AddressBooks, \%AddressBook;
287             }
288             }
289              
290 0           return \@AddressBooks;
291             }
292              
293             # }}}
294              
295             # Contact methods {{{
296              
297             =head2 $Self->NewContact($AddressBookPath, $VCard)
298              
299             Create a new contact from the Net::CardDAVTalk::VCard object,
300             either using its uid field or generating a new UUID and appending
301             .vcf for the filename.
302              
303             Returns the full path to the card.
304              
305             NOTE: can also be used for a kind: group v4 style group.
306              
307             =cut
308              
309             sub NewContact {
310 0     0 1   my ($Self, $Path, $VCard) = @_;
311              
312 0 0         $Path || confess "New contact path not specified";
313 0 0         $VCard->isa("Net::CardDAVTalk::VCard") || confess "Invalid contact";
314              
315 0   0       my $Uid = $VCard->uid() // $VCard->uid($Self->genuuid());
316              
317 0           $Self->Request(
318             'PUT',
319             "$Path/$Uid.vcf",
320             $VCard->as_string(),
321             'Content-Type' => 'text/vcard',
322             'If-None-Match' => '*',
323             );
324              
325 0           return $VCard->{CPath} = "$Path/$Uid.vcf";
326             }
327              
328             =head2 $self->DeleteContact($Path)
329              
330             Delete the contact at path $Path.
331              
332             =cut
333              
334             sub DeleteContact {
335 0     0 1   my ($Self, $CPath) = @_;
336              
337 0 0         $CPath || confess "Delete contact path not specified";
338              
339 0           $Self->Request(
340             'DELETE',
341             $CPath,
342             );
343              
344 0           return $CPath;
345             }
346              
347             =head2 $Self->UpdateContact($Path, $VCard)
348              
349             Identical to NewContact, but will fail unless there is an
350             existing contact with that path. Also takes the full path
351             instead of just the addressbook path.
352              
353             NOTE: can also be used for a kind: group v4 style group.
354              
355             =cut
356              
357             sub UpdateContact {
358 0     0 1   my ($Self, $CPath, $VCard) = @_;
359              
360 0 0         $CPath || confess "Update contact path not specified";
361 0 0         $VCard->isa("Net::CardDAVTalk::VCard") || confess "Invalid contact";
362              
363 0           $Self->Request(
364             'PUT',
365             $CPath,
366             $VCard->as_string(),
367             'Content-Type' => 'text/vcard',
368             'If-Match' => '*',
369             );
370              
371 0           return $VCard->{CPath} = $CPath;
372             }
373              
374             =head2 $Self->GetContact($Path)
375              
376             Fetch a specific contact by path. Returns a
377             Net::CardDAVTalk::VCard object.
378              
379             =cut
380              
381             sub GetContact {
382 0     0 1   my ($Self, $CPath) = @_;
383              
384 0 0         $CPath || confess "Get contact path not specified";
385              
386 0           my $Response = $Self->Request(
387             'GET',
388             $CPath,
389             );
390              
391             my $Data = $Response && $Response->{content}
392 0   0       // return undef;
      0        
393              
394 0   0       my $VCard = eval { Net::CardDAVTalk::VCard->new_fromstring($Data) }
  0            
395             // return undef;
396              
397 0           $VCard->{CPath} = $CPath;
398              
399 0           return $VCard;
400             }
401              
402             =head2 $Self->GetContactAndProps($Path, $Props)
403              
404             Use a multiget to fetch the properties in the arrayref as well
405             as the card content.
406              
407             Returns the card in scalar context - the card and an array of errors
408             in list context.
409              
410             =cut
411              
412             sub GetContactAndProps {
413 0     0 1   my ($Self, $CPath, $Props) = @_;
414 0   0       $Props //= [];
415              
416 0 0         $CPath || confess "Get contact path not specified";
417              
418             my $Response = $Self->Request(
419             'REPORT',
420             $CPath,
421             x('C:addressbook-multiget', $Self->NS(),
422             x('D:prop',
423             x('D:getetag'),
424             x('D:getcontenttype'),
425             x('C:address-data'),
426 0           map { x(join ":", @$_) } @$Props,
  0            
427             ),
428             x('D:href', $CPath),
429             ),
430             Depth => '0',
431             );
432              
433 0           my ($Contact, @Errors);
434              
435 0           my $NS_C = $Self->ns('C');
436 0           my $NS_D = $Self->ns('D');
437 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
438 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
439 0   0       my $VCard = eval { $Self->_ParseReportData($Response, $Propstat, $Props) } || do {
440             push @Errors, $@ if $@;
441             next;
442             };
443              
444 0           $Contact = $VCard;
445             }
446             }
447              
448 0 0         return wantarray ? ($Contact, \@Errors) : $Contact;
449             }
450              
451             =head2 $self->GetContacts($Path, $Props, %Args)
452              
453             Get multiple cards, possibly including props, using both a propfind
454             AND a multiget.
455              
456             Returns an arrayref of contact and an arrayref of errors (or just the
457             contacts in scalar context again)
458              
459             =cut
460              
461             sub GetContacts {
462 0     0 1   my ($Self, $Path, $Props) = @_;
463              
464 0           my $data = $Self->GetContactLinks($Path);
465 0           my @AllUrls = sort keys %$data;
466              
467 0           my ($Contacts, $Errors, $HRefs) = $Self->GetContactsMulti($Path, \@AllUrls, $Props);
468              
469 0 0         return wantarray ? ($Contacts, $Errors, $HRefs) : $Contacts;
470             }
471              
472             =head2 $self->GetContactLinks($Path)
473              
474             Returns a hash of href => etag for every contact URL (type: text/(x-)?vcard)
475             inside the collection at \$Path.
476              
477             =cut
478              
479             sub GetContactLinks {
480 0     0 1   my ($Self, $Path) = @_;
481              
482 0           my $Response = $Self->Request(
483             'PROPFIND',
484             "$Path/",
485             x('D:propfind', $Self->NS(),
486             x('D:prop',
487             x('D:getcontenttype'),
488             x('D:getetag'),
489             ),
490             ),
491             Depth => '1',
492             );
493              
494 0           my %response;
495 0           my $NS_C = $Self->ns('C');
496 0           my $NS_D = $Self->ns('D');
497 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
498 0           my $href = $Response->{"{$NS_D}href"}{content};
499 0 0         next unless $href;
500 0 0         if ($Response->{"{$NS_D}prop"}{"{$NS_D}getcontenttype"}) {
501 0   0       my $type = $Response->{"{$NS_D}prop"}{"{$NS_D}getcontenttype"}{content} || '';
502 0 0         next unless $type =~ m{text/(x-)?vcard};
503             }
504 0   0       my $etag = $Response->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content} || '';
505 0           $response{$href} = $etag;
506             }
507              
508 0           return \%response;
509             }
510              
511             =head2 $self->GetContactsMulti($Path, $Urls, $Props)
512              
513             Does an addressbook-multiget on the \$Path for all the URLs in \$Urls
514             also fetching \$Props on top of the address-data and getetag.
515              
516             =cut
517              
518             sub GetContactsMulti {
519 0     0 1   my ($Self, $Path, $Urls, $Props) = @_;
520 0   0       $Props //= [];
521 0           my (@Contacts, @Errors, %Links);
522              
523 0           while (my @urls = splice(@$Urls, 0, $BATCHSIZE)) {
524             my $Response = $Self->Request(
525             'REPORT',
526             "$Path/",
527             x('C:addressbook-multiget', $Self->NS(),
528             x('D:prop',
529             x('D:getetag'),
530             x('C:address-data'),
531 0           map { x(join ":", @$_) } @$Props,
532             ),
533 0           map { x('D:href', $_) } @urls,
  0            
534             ),
535             Depth => '0',
536             );
537              
538 0           my $NS_C = $Self->ns('C');
539 0           my $NS_D = $Self->ns('D');
540 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
541 0           my $href = $Response->{"{$NS_D}href"}{content};
542 0 0         next unless $href;
543 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
544 0   0       my $etag = $Propstat->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content} || '';
545 0   0       my $VCard = eval { $Self->_ParseReportData($Response, $Propstat, $Props) } || do {
546             push @Errors, $@ if $@;
547             next;
548             };
549              
550 0           push @Contacts, $VCard;
551              
552 0           $Links{$href} = $etag;
553             }
554             }
555             }
556              
557 0 0         return wantarray ? (\@Contacts, \@Errors, \%Links) : \@Contacts;
558             }
559              
560             =head2 $self->SyncContacts($Path, $Props, %Args)
561              
562             uses the argument 'syncToken' to find newly added and removed
563             cards from the server. Returns just the added/changed contacts
564             in scalar context, or a list of array of contacts, array of
565             removed, array of errors and the new syncToken as 4 items in
566             list context.
567              
568             =cut
569              
570             sub SyncContacts {
571 0     0 1   my ($Self, $Path, $Props, %Args) = @_;
572              
573 0           my ($Added, $Removed, $Errors, $SyncToken) = $Self->SyncContactLinks($Path, %Args);
574              
575 0           my @AllUrls = sort keys %$Added;
576              
577 0           my ($Contacts, $ThisErrors, $Links) = $Self->GetContactsMulti($Path, \@AllUrls, $Props);
578 0           push @$Errors, @$ThisErrors;
579              
580 0 0         return wantarray ? ($Contacts, $Removed, $Errors, $SyncToken, $Links) : $Contacts;
581             }
582              
583             =head2 $self->SyncContactLinks($Path, %Args)
584              
585             uses the argument 'syncToken' to find newly added and removed
586             cards from the server.
587              
588             Returns a list of:
589              
590             * Hash of href to etag for added/changed cargs
591             * List of href of removed cards
592             * List of errors
593             * Scalar value of new syncToken
594              
595             =cut
596              
597             sub SyncContactLinks {
598 0     0 1   my ($Self, $Path, %Args) = @_;
599              
600 0 0         $Path || confess "Sync contacts path required";
601              
602             # WebDAV Collection Synchronization (RFC6578)
603             my $Response = $Self->Request(
604             'REPORT',
605             "$Path/",
606             x('D:sync-collection', $Self->NS(),
607 0 0         x('D:sync-token', ($Args{syncToken} ? ($Args{syncToken}) : ())),
608             x('D:sync-level', 1),
609             x('D:prop',
610             x('D:getetag'),
611             ),
612             ),
613             );
614              
615 0           my (%Added, @Removed, @Errors);
616              
617 0           my $NS_C = $Self->ns('C');
618 0           my $NS_D = $Self->ns('D');
619 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
620             my $href = $Response->{"{$NS_D}href"}{content}
621 0   0       || next;
622              
623             # For members that have been removed, the DAV:response MUST
624             # contain one DAV:status with a value set to '404 Not Found' and
625             # MUST NOT contain any DAV:propstat element
626 0 0         if (!$Response->{"{$NS_D}propstat"}) {
627 0           my $Status = $Response->{"{$NS_D}status"}{content};
628 0 0         if ($Status =~ m/ 404 /) {
629 0           push @Removed, $href;
630             } else {
631 0           warn "ODD STATUS";
632 0           push @Errors, "Odd status in non-propstat response $href: $Status";
633             }
634 0           next;
635             }
636              
637             # For members that have changed (i.e., are new or have had their
638             # mapped resource modified), the DAV:response MUST contain at
639             # least one DAV:propstat element and MUST NOT contain any
640             # DAV:status element.
641 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
642 0           my $Status = $Propstat->{"{$NS_D}status"}{content};
643              
644 0 0         if ($Status =~ m/ 200 /) {
    0          
645 0           my $etag = $Propstat->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content};
646 0           $Added{$href} = $etag;
647             }
648             elsif ($Status =~ m/ 404 /) {
649             # Missing properties return 404 status response, ignore
650              
651             }
652             else {
653 0           warn "ODD STATUS";
654 0           push @Errors, "Odd status in propstat response $href: $Status";
655             }
656             }
657             }
658              
659 0           my $SyncToken = $Response->{"{$NS_D}sync-token"}{content};
660              
661 0           return (\%Added, \@Removed, \@Errors, $SyncToken);
662             }
663              
664             =head2 $self->MoveContact($Path, $NewPath)
665              
666             Move a contact to a new path (usually in a new addressbook) - both
667             paths are card paths.
668              
669             =cut
670              
671             sub MoveContact {
672 0     0 1   my ($Self, $CPath, $NewPath) = @_;
673              
674 0 0         $CPath || confess "Move contact path not specified";
675 0 0         $NewPath || confess "Move contact destination path not specified";
676              
677 0           $Self->Request(
678             'MOVE',
679             $CPath,
680             undef,
681             'Destination' => $Self->request_url($NewPath),
682             );
683              
684 0           return $NewPath;
685             }
686              
687             # }}}
688              
689             sub _ParseReportData {
690 0     0     my ($Self, $Response, $Propstat, $Props) = @_;
691              
692 0           my $NS_C = $Self->ns('C');
693 0           my $NS_D = $Self->ns('D');
694              
695             my $HRef = $Response->{"{$NS_D}href"}{content}
696 0   0       // return;
697 0           my $CPath = $Self->_unrequest_url($HRef);
698              
699             my $Data = $Propstat->{"{$NS_D}prop"}{"{$NS_C}address-data"}{content}
700 0   0       // return;
701              
702 0           my $VCard = Net::CardDAVTalk::VCard->new_fromstring($Data);
703 0 0         return unless $VCard;
704              
705 0           $VCard->{CPath} = $CPath;
706 0           $VCard->{href} = $HRef;
707              
708 0           my %Props;
709 0           for (@$Props) {
710 0           my ($NS, $PropName) = @$_;
711 0           my $NS_P = $Self->ns($NS);
712             my $PropValue = $Propstat->{"{$NS_D}prop"}{"{$NS_P}$PropName"}{content}
713 0   0       // next;
714 0           $Props{"${NS}:${PropName}"} = $PropValue;
715             }
716              
717 0           $VCard->{meta} = \%Props;
718              
719 0           return $VCard;
720             }
721              
722             sub _unrequest_url {
723 0     0     my $Self = shift;
724 0           my $Path = shift;
725              
726 0 0         if ($Path =~ m{^/}) {
727 0           $Path =~ s#^\Q$Self->{basepath}\E/?##;
728             } else {
729 0           $Path =~ s#^\Q$Self->{url}\E/?##;
730             }
731 0           $Path =~ s#/$##;
732              
733 0           return $Path;
734             }
735              
736             =head1 AUTHOR
737              
738             Bron Gondwana, C<< <brong at cpan.org> >>
739              
740             =head1 BUGS
741              
742             Please report any bugs or feature requests to C<bug-net-carddavtalk at rt.cpan.org>, or through
743             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-CardDAVTalk>. I will be notified, and then you'll
744             automatically be notified of progress on your bug as I make changes.
745              
746              
747              
748              
749             =head1 SUPPORT
750              
751             You can find documentation for this module with the perldoc command.
752              
753             perldoc Net::CardDAVTalk
754              
755              
756             You can also look for information at:
757              
758             =over 4
759              
760             =item * RT: CPAN's request tracker (report bugs here)
761              
762             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-CardDAVTalk>
763              
764             =item * AnnoCPAN: Annotated CPAN documentation
765              
766             L<http://annocpan.org/dist/Net-CardDAVTalk>
767              
768             =item * CPAN Ratings
769              
770             L<http://cpanratings.perl.org/d/Net-CardDAVTalk>
771              
772             =item * Search CPAN
773              
774             L<http://search.cpan.org/dist/Net-CardDAVTalk/>
775              
776             =back
777              
778              
779             =head1 ACKNOWLEDGEMENTS
780              
781              
782             =head1 LICENSE AND COPYRIGHT
783              
784             Copyright 2015 FastMail Pty. Ltd.
785              
786             This program is free software; you can redistribute it and/or modify it
787             under the terms of the the Artistic License (2.0). You may obtain a
788             copy of the full license at:
789              
790             L<http://www.perlfoundation.org/artistic_license_2_0>
791              
792             Any use, modification, and distribution of the Standard or Modified
793             Versions is governed by this Artistic License. By using, modifying or
794             distributing the Package, you accept this license. Do not use, modify,
795             or distribute the Package, if you do not accept this license.
796              
797             If your Modified Version has been derived from a Modified Version made
798             by someone other than you, you are nevertheless required to ensure that
799             your Modified Version complies with the requirements of this license.
800              
801             This license does not grant you the right to use any trademark, service
802             mark, tradename, or logo of the Copyright Holder.
803              
804             This license includes the non-exclusive, worldwide, free-of-charge
805             patent license to make, have made, use, offer to sell, sell, import and
806             otherwise transfer the Package with respect to any patent claims
807             licensable by the Copyright Holder that are necessarily infringed by the
808             Package. If you institute patent litigation (including a cross-claim or
809             counterclaim) against any party alleging that the Package constitutes
810             direct or contributory patent infringement, then this Artistic License
811             to you shall terminate on the date that such litigation is filed.
812              
813             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
814             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
815             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
816             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
817             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
818             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
819             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
820             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
821              
822              
823             =cut
824              
825             1; # End of Net::CardDAVTalk