File Coverage

blib/lib/Data/v.pm
Criterion Covered Total %
statement 18 18 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 24 24 100.0


line stmt bran cond sub pod time code
1             package Data::v;
2              
3 2     2   136304 use warnings;
  2         7  
  2         82  
4 2     2   12 use strict;
  2         4  
  2         60  
5              
6 2     2   11 use Carp;
  2         4  
  2         164  
7 2     2   13 use Scalar::Util 'blessed';
  2         5  
  2         113  
8 2     2   2168 use List::MoreUtils 'any';
  2         2578  
  2         179  
9              
10             our $VERSION = '0.03';
11              
12 2     2   11 use base 'Data::Header::Fields';
  2         5  
  2         10727  
13              
14             our %v_types = (
15             'VCARD' => 'Data::v::Card',
16             );
17              
18             sub new {
19             my $class = shift;
20             return $class->SUPER::new(
21             tight_folding => 1,
22             key_cmp => \&default_key_cmp,
23             parent => $class->_default_parent,
24             @_,
25             );
26             }
27              
28             sub default_key_cmp {
29             my $a = shift;
30             my $b = shift;
31            
32             $a = lc $a;
33             $b = lc $b;
34            
35             return 0
36             if $b =~ qr{^$a (?: ; | $)}xms;
37            
38             return $a cmp $b;
39             }
40              
41             sub decode {
42             my $self = shift;
43             my $any = shift;
44            
45             my $return_self = ref $self;
46             $self = $self->new()
47             if not ref $self;
48              
49             my $dhf = $self->SUPER::decode($any);
50             my $lines = $dhf->_lines;
51            
52             my @v_entries;
53             my $v_type;
54             while (my $line = shift @{$lines}) {
55             if ($line->key eq 'BEGIN') {
56             $v_type = $line->value->as_string;
57             croak 'unknown v-type "'.$v_type.'"'
58             if not $v_types{$v_type};
59            
60             my $v_data = $v_types{$v_type}->new();
61             my $v_entry = ($v_types{$v_type}.'::Entry')->new(
62             'key' => $v_type,
63             'value' => $v_data,
64             'parent' => $self,
65             );
66             $v_data->parent($v_entry);
67             push @v_entries, $v_entry;
68             next;
69             }
70             elsif ($line->key eq 'END') {
71             croak 'BEGIN and END mismatch "'.$v_type.'" ne "'.$line->value->as_string.'"'
72             if $v_type ne $line->value->as_string;
73            
74             $v_type = undef;
75             next;
76             }
77            
78             push @{$v_entries[-1]->value->_lines}, $line;
79             }
80            
81             foreach my $v_entry (@v_entries) {
82             $v_entry->value->rebless_lines;
83             }
84            
85             if (not $return_self) {
86             return \@v_entries;
87             }
88              
89             $dhf->_lines(\@v_entries);
90             return $dhf;
91             }
92              
93             sub _read_lines {
94             my $self = shift;
95             my $any = shift;
96            
97             my $fh = IO::Any->read($any);
98            
99             # put folded lines to an array http://tools.ietf.org/html/rfc2822#section-2.2.3
100             my @lines;
101             my $quoted_printable = 0;
102             while (my $line = <$fh>) {
103             # folded line
104             if (($line =~ m/^\s/xms) or ($quoted_printable and ($lines[-1] =~ m/ = \r? \Z /xms))) {
105             # ignore if the first line starts with white space
106             next if not @lines;
107            
108             $lines[-1] .= $line;
109             next;
110             }
111            
112             # detect quoted-printable encoding which continues on a next lines when the line ends with "="
113             my ($key, $value) = split(/:/, $line, 2);
114             my @key_parts = split(/;/, $key);
115             shift @key_parts;
116             $quoted_printable = (any { $_ eq 'encoding=quoted-printable' } map { lc $_; } @key_parts);
117            
118             push @lines, $line;
119             }
120            
121             close $fh;
122              
123             return @lines;
124             }
125              
126             sub _default_parent {
127             return 'Data::Header::Fields';
128             }
129              
130             sub parent {
131             my $self = shift;
132             $self->{'parent'} = shift
133             if @_;
134            
135             return (ref $self ? $self->{'parent'} : $self->_default_parent);
136             }
137              
138             1;
139              
140             package Data::v::Card;
141              
142             use base 'Data::v';
143              
144             use List::MoreUtils 'any';
145             use Carp 'croak';
146              
147             sub version { return $_[0]->get_value('version') || '2.1'; }
148              
149             sub rebless_lines {
150             my $self = shift;
151            
152             foreach my $line (@{$self->_lines}) {
153             $line = Data::v::Card::Line->new(
154             line => $line,
155             parent => $self,
156             );
157             }
158             }
159              
160             sub _default_parent {
161             return 'Data::v::Card::Entry';
162             }
163              
164             sub line_ending {
165             my $self = shift;
166             return $self->parent->parent->line_ending(@_);
167             }
168              
169             sub get_fields {
170             my $self = shift;
171             my $field_name = shift or croak 'field_name argument is mandatory';
172             my $param_name = shift;
173             my $param_value = shift;
174            
175             my @fields = $self->SUPER::get_fields($field_name);
176             if (defined $param_name) {
177             @fields =
178             grep { any { lc $_->value eq $param_value } $_->get_key_params($param_name) }
179             @fields
180             ;
181             }
182            
183             return @fields;
184             }
185              
186             1;
187              
188              
189             package Data::v::Card::Line;
190              
191             use base 'Data::Header::Fields::Line';
192              
193             use Carp 'croak';
194             use MIME::QuotedPrint 'encode_qp', 'decode_qp';
195             use MIME::Base64 'encode_base64', 'decode_base64';
196             use Encode ();
197             use List::MoreUtils 'none';
198              
199             use overload
200             '""' => \&as_string,
201             'cmp' => \&Data::Header::Fields::Line::cmp,
202             ;
203              
204             sub new {
205             my $class = shift;
206             my $self = $class->SUPER::new(
207             @_,
208             );
209             $self->_decode_key_params;
210             return $self;
211             }
212              
213             sub version { return $_[0]->parent->version; }
214              
215             sub params {
216             my $self = shift;
217            
218             if (@_) {
219             $self->{params} = shift;
220             }
221             $self->{params} = []
222             if (not $self->{params});
223            
224             return $self->{params};
225             }
226              
227             sub _decode_key_params {
228             my $self = shift;
229            
230             my $key = $self->key;
231            
232             if ($key =~ m/^([^;]+);(.+)$/xms) {
233             my $orig_key_name = $1;
234             my @raw_key_params = split /;/, $2;
235             my @key_params;
236            
237             my $key_name = lc $orig_key_name;
238              
239             foreach my $key_param (@raw_key_params) {
240             croak 'unknown key param "'.$key_param.'"'
241             if $key_param !~ m/^ (?: ([^=]+) = )? (.+) $/xms;
242             my $param_name = $1 || 'TYPE';
243             my $param_str = $2;
244            
245             push
246             @key_params,
247             map { Data::v::Param->new('name' => $param_name, 'value' => $_, 'parent' => $self) }
248             (split(/,/, $param_str))
249             ;
250             }
251            
252             $self->key($orig_key_name);
253             $self->params(\@key_params);
254            
255             my $enc_type = lc ($self->get_key_param_value('encoding') || '');
256             if ($enc_type) {
257             if ($enc_type eq 'quoted-printable') {
258             $self->{value} = Data::Header::Fields::Value->new(
259             decode_qp($self->{value})
260             );
261             }
262             elsif ($enc_type eq 'base64') {
263             $self->{value} = Data::Header::Fields::Value->new(
264             decode_base64($self->{value})
265             );
266             }
267             else {
268             croak 'unknown encoding "'.$enc_type.'"';
269             }
270             }
271              
272             my $charset = lc ($self->get_key_param_value('charset') || '');
273             $charset ||= 'utf8'
274             if (none { $_ eq $key_name } qw(photo logo sound key));
275              
276             if ($charset) {
277             $self->{'value'} = Data::Header::Fields::Value->new(
278             eval { Encode::decode($charset, $self->{'value'}) }
279             );
280             }
281             }
282            
283             if ((lc $self->key eq 'n') and (not $self->key->isa('Data::v::Card::Value::Name'))) {
284             $self->{'value'} = Data::v::Card::Value::Name->new( # not calling value() because the set doesn't affect the content of the value
285             'value' => $self->value,
286             'parent' => $self,
287             );
288             }
289             elsif ((lc $self->key eq 'adr') and (not $self->key->isa('Data::v::Card::Value::Adr'))) {
290             $self->{'value'} = Data::v::Card::Value::Adr->new( # not calling value() because the set doesn't affect the content of the value
291             'value' => $self->value,
292             'parent' => $self,
293             );
294             }
295            
296             return;
297             }
298              
299             sub get_key_params {
300             my $self = shift;
301             my $param_name = shift or croak 'param param_name is mandatory';
302             my $params = $self->params;
303            
304             $param_name = lc $param_name;
305             return grep { lc $_->{'name'} eq $param_name } @{$params};
306             }
307              
308             sub get_key_param {
309             my $self = shift;
310             my $param_name = shift or croak 'param param_name is mandatory';
311            
312             my @params = $self->get_key_params($param_name);
313             croak 'more then one key param with name "'.$param_name.'"'
314             if @params > 1;
315            
316             return $params[0];
317             }
318              
319             sub get_key_param_value {
320             my $self = shift;
321             my $param_name = shift or croak 'param param_name is mandatory';
322            
323             my $param = $self->get_key_param($param_name);
324             return undef if not $param;
325             return $param->{'value'};
326             }
327              
328             sub update_key_params {
329             my $self = shift;
330             my $param_name = shift or croak 'param param_name is mandatory';
331             my $param_value = shift;
332            
333             # updating via array set
334             if (ref $param_value) {
335             my @new_params = @{$param_value};
336            
337             # update existing
338             foreach my $param (@{$self->params}) {
339             $param->value(shift @new_params) # will returns undefs if depleeted
340             if ($param->name eq $param_name);
341             }
342            
343             # add any additional new
344             foreach my $add_value (@new_params) {
345             push @{$self->{params}}, Data::v::Param->new(
346             'parent' => $self,
347             'name' => $param_name,
348             'value' => $add_value,
349             );
350             }
351            
352             # remove any additional old
353             $self->{params} = [
354             grep { defined $_->{'value'} }
355             @{$self->{params}}
356             ];
357             return $self;
358             }
359              
360             my @params = (
361             map {
362             ($_->{'name'} eq $param_name ? $_->{value} = $param_value : ());
363             $_;
364             } @{$self->params}
365             );
366            
367             return $self;
368             }
369              
370             sub set_key_param {
371             my $self = shift;
372             my $param_name = shift or croak 'param param_name is mandatory';
373             my $param_value = shift;
374              
375             my @params = $self->get_key_params($param_name);
376             if ((@params > 0) or (ref $param_value)) {
377             $self->update_key_params($param_name, $param_value);
378             }
379             elsif (@params == 0) {
380             push @{$self->params}, Data::v::Param->new('name' => $param_name, 'value' => $param_value, 'parent' => $self);
381             }
382             else {
383             croak 'more then one param field with name "'.$param_name.'"';
384             }
385            
386             return $self;
387             }
388              
389             sub rm_key_param {
390             my $self = shift;
391             my $param_name = shift or croak 'param param_name is mandatory';
392              
393             my @params = (
394             grep {
395             $_->name ne $param_name
396             } @{$self->params}
397             );
398             $self->params(\@params);
399            
400             return $self;
401             }
402              
403             sub _encode_key_params {
404             my $self = shift;
405              
406             my $params = $self->params;
407             return if scalar @{$params} == 0;
408             my $key = $self->key;
409              
410             my $charset = lc ($self->get_key_param_value('charset') || 'utf8');
411             $self->{value} = eval { Encode::encode($charset, $self->{value}) };
412            
413             my $enc_type = lc ($self->get_key_param_value('encoding') || '');
414             if ($enc_type) {
415             if ($enc_type eq 'quoted-printable') {
416             $self->{value} = encode_qp($self->{value}, "");
417             }
418             elsif ($enc_type eq 'base64') {
419             $self->{value} = encode_base64($self->{value}, "");
420             }
421             else {
422             croak 'unknown encoding "'.$enc_type.'"';
423             }
424             }
425            
426             if ($self->version eq '2.1') {
427             $key .= ';'.(
428             join(
429             ';',
430             (
431             map { $_->as_string }
432             grep { defined $_->value }
433             @{$params}
434             ),
435             )
436             );
437             }
438             elsif ($self->version ge '3.0') {
439             my @types = map { $_->as_string } $self->get_key_params('type');
440             $key .= ';'.(
441             join(
442             ';',
443             (
444             map {
445             (lc $_->name eq 'type')
446             ? ( @types ? ('TYPE='.join(',',splice(@types,0,scalar @types))) : () )
447             : $_->as_string
448             }
449             grep { defined $_->value }
450             @{$params}
451             ),
452             )
453             );
454             }
455             else {
456             croak 'unsupported VCARD version '.$self->version;
457             }
458            
459             $self->params(undef);
460             $self->key($key);
461            
462             return;
463             }
464              
465             sub as_string {
466             my $self = shift;
467            
468             if (exists $self->{'original_line'}) {
469             my $original_line = $self->{'original_line'};
470            
471             # make sure the line has line_ending, even the original one could be created without using ->new()
472             $original_line .= $self->parent->line_ending
473             if $original_line !~ m/ \n \Z /xms;
474            
475             return $original_line;
476             }
477              
478             $self->_encode_key_params;
479              
480             my ($key, $value) = ($self->key, $self->value);
481             #$value = String::Escape::printable($value);
482             # FIXME ^^^ should be moved to _encode_key_params
483              
484             my $line = join(':', $key, $value);
485            
486             $line .= $self->parent->line_ending
487             if $line !~ m/\n$/xms;
488              
489             $self->_decode_key_params;
490            
491             return $line;
492             }
493              
494             1;
495              
496             package Data::v::Card::Entry;
497              
498             use base 'Data::Header::Fields::Line';
499              
500             use overload
501             '""' => \&as_string,
502             'cmp' => \&Data::Header::Fields::Line::cmp,
503             ;
504              
505             sub as_string {
506             my $self = shift;
507              
508             return
509             'BEGIN:VCARD'.$self->parent->line_ending
510             .$self->value->as_string()
511             .'END:VCARD'.$self->parent->line_ending
512             ;
513             }
514              
515             1;
516              
517             package Data::v::Card::Value::Name;
518              
519             use base 'Data::Header::Fields::Value';
520              
521             our @NAME_PART_TYPES = qw{family_name given_name additional_names honorific_prefixes honorific_suffixes};
522              
523             use overload
524             '""' => \&Data::Header::Fields::Value::as_string,
525             'cmp' => \&Data::Header::Fields::Value::cmp,
526             ;
527              
528             sub new {
529             my $class = shift;
530             my $self = $class->SUPER::new(@_);
531            
532             defined $self->{'value'}
533             ? $self->_parse_value()
534             : $self->_update_value();
535            
536             return $self;
537             }
538              
539             sub _update_value {
540             my $self = shift;
541            
542             my @name_parts = map { $self->$_() } @NAME_PART_TYPES;
543             # remove the undef fields from the end of the N
544             while ((@name_parts) and (not defined $name_parts[-1])) {
545             pop @name_parts;
546             }
547             @name_parts = map { defined $_ ? $_ : '' } @name_parts;
548            
549             $self->value(join(';', @name_parts));
550            
551             return $self;
552             }
553              
554             sub _parse_value {
555             my $self = shift;
556            
557             my $name_str = $self->{'value'};
558             my @name_parts = split(/;/, $name_str);
559            
560             foreach my $name_part_type (@NAME_PART_TYPES) {
561             my $name_part_value = shift @name_parts;
562             $self->{$name_part_type} = $name_part_value;
563             }
564            
565             return $self;
566             }
567              
568             sub family_name {
569             my $self = shift;
570            
571             if (@_) {
572             $self->{'family_name'} = shift;
573             $self->_update_value();
574             }
575             return $self->{'family_name'};
576             }
577             sub given_name {
578             my $self = shift;
579             if (@_) {
580             $self->{'given_name'} = shift;
581             $self->_update_value();
582             }
583             return $self->{'given_name'};
584             }
585             sub additional_names {
586             my $self = shift;
587             if (@_) {
588             $self->{'additional_names'} = shift;
589             $self->_update_value();
590             }
591             return $self->{'additional_names'};
592             }
593             sub honorific_prefixes {
594             my $self = shift;
595             if (@_) {
596             $self->{'honorific_prefixes'} = shift;
597             $self->_update_value();
598             }
599             return $self->{'honorific_prefixes'};
600             }
601             sub honorific_suffixes {
602             my $self = shift;
603             if (@_) {
604             $self->{'honorific_suffixes'} = shift;
605             $self->_update_value();
606             }
607             return $self->{'honorific_suffixes'};
608             }
609              
610             1;
611              
612             package Data::v::Card::Value::Adr;
613              
614             use base 'Data::Header::Fields::Value';
615              
616             our @ADR_PART_TYPES = qw{po_box ext_address street city state postal_code country};
617              
618             use overload
619             '""' => \&Data::Header::Fields::Value::as_string,
620             'cmp' => \&Data::Header::Fields::Value::cmp,
621             ;
622              
623             sub new {
624             my $class = shift;
625             my $self = $class->SUPER::new(@_);
626            
627             defined $self->{'value'}
628             ? $self->_parse_value()
629             : $self->_update_value();
630            
631             return $self;
632             }
633              
634             sub _update_value {
635             my $self = shift;
636              
637             my @adr_parts = map { $self->$_() } @ADR_PART_TYPES;
638             # remove the undef fields from the end of the N
639             while ((@adr_parts) and (not defined $adr_parts[-1])) {
640             pop @adr_parts;
641             }
642             @adr_parts = map { defined $_ ? $_ : '' } @adr_parts;
643            
644             $self->value(join(';', @adr_parts));
645            
646             return $self;
647             }
648              
649             sub _parse_value {
650             my $self = shift;
651            
652             my $adr_str = $self->{'value'};
653             $adr_str =~ s/ \r? \n \Z//xms;
654             my @adr_parts = split(/;/, $adr_str);
655            
656             foreach my $adr_part_type (@ADR_PART_TYPES) {
657             my $adr_part_value = shift @adr_parts;
658             $self->{$adr_part_type} = $adr_part_value;
659             }
660            
661             return $self;
662             }
663              
664             sub po_box {
665             my $self = shift;
666              
667             if (@_) {
668             $self->{'po_box'} = shift @_;
669             $self->_update_value();
670             }
671              
672             return $self->{'po_box'};
673             }
674             sub ext_address {
675             my $self = shift;
676              
677             if (@_) {
678             $self->{'ext_address'} = shift @_;
679             $self->_update_value();
680             }
681              
682             return $self->{'ext_address'};
683             }
684             sub street {
685             my $self = shift;
686              
687             if (@_) {
688             $self->{'street'} = shift @_;
689             $self->_update_value();
690             }
691              
692             return $self->{'street'};
693             }
694             sub city {
695             my $self = shift;
696              
697             if (@_) {
698             $self->{'city'} = shift @_;
699             $self->_update_value();
700             }
701              
702             return $self->{'city'};
703             }
704             sub state {
705             my $self = shift;
706              
707             if (@_) {
708             $self->{'state'} = shift @_;
709             $self->_update_value();
710             }
711              
712             return $self->{'state'};
713             }
714             sub postal_code {
715             my $self = shift;
716              
717             if (@_) {
718             $self->{'postal_code'} = shift @_;
719             $self->_update_value();
720             }
721              
722             return $self->{'postal_code'};
723             }
724             sub country {
725             my $self = shift;
726              
727             if (@_) {
728             $self->{'country'} = shift @_;
729             $self->_update_value();
730             }
731              
732             return $self->{'country'};
733             }
734              
735             1;
736              
737             package Data::v::Param;
738              
739             use overload
740             '""' => \&as_string,
741             'cmp' => \&Data::Header::Fields::Value::cmp,
742             ;
743              
744             sub new {
745             my $class = shift;
746             return bless {
747             @_
748             }, $class;
749             }
750              
751             sub name {
752             my $self = shift;
753              
754             $self->{'name'} = shift @_
755             if (@_);
756              
757             return $self->{'name'};
758             }
759              
760             sub value {
761             my $self = shift;
762              
763             $self->{'value'} = shift @_
764             if (@_);
765              
766             return $self->{'value'};
767             }
768              
769             sub as_string {
770             my $self = shift;
771            
772             return
773             (lc $self->name eq 'type')
774             ? $self->value
775             : $self->name.'='.$self->value
776             ;
777             }
778              
779             1;
780              
781             package Data::v::Calendar;
782              
783             use base 'Data::v';
784              
785             1;
786              
787              
788             __END__