line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
#------------------------------------------------------------------------------ |
2
|
|
|
|
|
|
|
# File: VCard.pm |
3
|
|
|
|
|
|
|
# |
4
|
|
|
|
|
|
|
# Description: Read vCard and iCalendar meta information |
5
|
|
|
|
|
|
|
# |
6
|
|
|
|
|
|
|
# Revisions: 2015/04/05 - P. Harvey Created |
7
|
|
|
|
|
|
|
# 2015/05/02 - PH Added iCalendar support |
8
|
|
|
|
|
|
|
# |
9
|
|
|
|
|
|
|
# References: 1) http://en.m.wikipedia.org/wiki/VCard |
10
|
|
|
|
|
|
|
# 2) http://tools.ietf.org/html/rfc6350 |
11
|
|
|
|
|
|
|
# 3) http://tools.ietf.org/html/rfc5545 |
12
|
|
|
|
|
|
|
#------------------------------------------------------------------------------ |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
package Image::ExifTool::VCard; |
15
|
|
|
|
|
|
|
|
16
|
1
|
|
|
1
|
|
3610
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
27
|
|
17
|
1
|
|
|
1
|
|
5
|
use vars qw($VERSION); |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
33
|
|
18
|
1
|
|
|
1
|
|
5
|
use Image::ExifTool qw(:DataAccess :Utils); |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
1953
|
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
$VERSION = '1.06'; |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
my %unescapeVCard = ( '\\'=>'\\', ','=>',', 'n'=>"\n", 'N'=>"\n" ); |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# lookup for iCalendar components (used to generate family 1 group names if top level) |
25
|
|
|
|
|
|
|
my %isComponent = ( Event=>1, Todo=>1, Journal=>1, Freebusy=>1, Timezone=>1, Alarm=>1 ); |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
my %timeInfo = ( |
28
|
|
|
|
|
|
|
# convert common date/time formats to EXIF style |
29
|
|
|
|
|
|
|
ValueConv => q{ |
30
|
|
|
|
|
|
|
$val =~ s/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)/$1:$2:$3 $4:$5:$6$7/g; |
31
|
|
|
|
|
|
|
$val =~ s/(\d{4})(\d{2})(\d{2})/$1:$2:$3/g; |
32
|
|
|
|
|
|
|
$val =~ s/(\d{4})-(\d{2})-(\d{2})/$1:$2:$3/g; |
33
|
|
|
|
|
|
|
return $val; |
34
|
|
|
|
|
|
|
}, |
35
|
|
|
|
|
|
|
PrintConv => '$self->ConvertDateTime($val)', |
36
|
|
|
|
|
|
|
); |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
# vCard tags (ref 1/2/PH) |
39
|
|
|
|
|
|
|
# Note: The case of all tag ID's is normalized to lowercase with uppercase first letter |
40
|
|
|
|
|
|
|
%Image::ExifTool::VCard::Main = ( |
41
|
|
|
|
|
|
|
GROUPS => { 2 => 'Document' }, |
42
|
|
|
|
|
|
|
VARS => { NO_LOOKUP => 1 }, # omit tags from lookup |
43
|
|
|
|
|
|
|
NOTES => q{ |
44
|
|
|
|
|
|
|
This table lists common vCard tags, but ExifTool will also extract any other |
45
|
|
|
|
|
|
|
vCard tags found. Tag names may have "Pref" added to indicate the preferred |
46
|
|
|
|
|
|
|
instance of a vCard property, and other "TYPE" parameters may also added to |
47
|
|
|
|
|
|
|
the tag name. VCF files may contain multiple vCard entries which are |
48
|
|
|
|
|
|
|
distinguished by the ExifTool family 3 group name (document number). See |
49
|
|
|
|
|
|
|
L for the vCard 4.0 specification. |
50
|
|
|
|
|
|
|
}, |
51
|
|
|
|
|
|
|
Version => { Name => 'VCardVersion', Description => 'VCard Version' }, |
52
|
|
|
|
|
|
|
Fn => { Name => 'FormattedName', Groups => { 2 => 'Author' } }, |
53
|
|
|
|
|
|
|
N => { Name => 'Name', Groups => { 2 => 'Author' } }, |
54
|
|
|
|
|
|
|
Bday => { Name => 'Birthday', Groups => { 2 => 'Time' }, %timeInfo }, |
55
|
|
|
|
|
|
|
Tz => { Name => 'TimeZone', Groups => { 2 => 'Time' } }, |
56
|
|
|
|
|
|
|
Adr => { Name => 'Address', Groups => { 2 => 'Location' } }, |
57
|
|
|
|
|
|
|
Geo => { |
58
|
|
|
|
|
|
|
Name => 'Geolocation', |
59
|
|
|
|
|
|
|
Groups => { 2 => 'Location' }, |
60
|
|
|
|
|
|
|
# when used as a parameter, VCard 4.0 adds a "geo:" prefix that we need to remove |
61
|
|
|
|
|
|
|
ValueConv => '$val =~ s/^geo://; $val', |
62
|
|
|
|
|
|
|
}, |
63
|
|
|
|
|
|
|
Anniversary => { }, |
64
|
|
|
|
|
|
|
Email => { }, |
65
|
|
|
|
|
|
|
Gender => { }, |
66
|
|
|
|
|
|
|
Impp => 'IMPP', |
67
|
|
|
|
|
|
|
Lang => 'Language', |
68
|
|
|
|
|
|
|
Logo => { }, |
69
|
|
|
|
|
|
|
Nickname => { }, |
70
|
|
|
|
|
|
|
Note => { }, |
71
|
|
|
|
|
|
|
Org => 'Organization', |
72
|
|
|
|
|
|
|
Photo => { Groups => { 2 => 'Preview' } }, |
73
|
|
|
|
|
|
|
Prodid => 'Software', |
74
|
|
|
|
|
|
|
Rev => 'Revision', |
75
|
|
|
|
|
|
|
Sound => { }, |
76
|
|
|
|
|
|
|
Tel => 'Telephone', |
77
|
|
|
|
|
|
|
Title => 'JobTitle', |
78
|
|
|
|
|
|
|
Uid => 'UID', |
79
|
|
|
|
|
|
|
Url => 'URL', |
80
|
|
|
|
|
|
|
'X-ablabel' => { Name => 'ABLabel', PrintConv => '$val =~ s/^_\$!<(.*)>!\$_$/$1/; $val' }, |
81
|
|
|
|
|
|
|
'X-abdate' => { Name => 'ABDate', Groups => { 2 => 'Time' }, %timeInfo }, |
82
|
|
|
|
|
|
|
'X-aim' => 'AIM', |
83
|
|
|
|
|
|
|
'X-icq' => 'ICQ', |
84
|
|
|
|
|
|
|
'X-abuid' => 'AB_UID', |
85
|
|
|
|
|
|
|
'X-abrelatednames' => 'ABRelatedNames', |
86
|
|
|
|
|
|
|
'X-socialprofile' => 'SocialProfile', |
87
|
|
|
|
|
|
|
); |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
%Image::ExifTool::VCard::VCalendar = ( |
90
|
|
|
|
|
|
|
GROUPS => { 1 => 'VCalendar', 2 => 'Document' }, |
91
|
|
|
|
|
|
|
VARS => { |
92
|
|
|
|
|
|
|
NO_LOOKUP => 1, # omit tags from lookup |
93
|
|
|
|
|
|
|
LONG_TAGS => 6, # some X-microsoft tags have unavoidably long ID's |
94
|
|
|
|
|
|
|
}, |
95
|
|
|
|
|
|
|
NOTES => q{ |
96
|
|
|
|
|
|
|
The VCard module is also used to process iCalendar ICS files since they use |
97
|
|
|
|
|
|
|
a format similar to vCard. The following table lists standard iCalendar |
98
|
|
|
|
|
|
|
tags, but any existing tags will be extracted. Top-level iCalendar |
99
|
|
|
|
|
|
|
components (eg. Event, Todo, Timezone, etc.) are used for the family 1 group |
100
|
|
|
|
|
|
|
names, and embedded components (eg. Alarm) are added as a prefix to the tag |
101
|
|
|
|
|
|
|
name. See L for the official iCalendar |
102
|
|
|
|
|
|
|
2.0 specification. |
103
|
|
|
|
|
|
|
}, |
104
|
|
|
|
|
|
|
Version => { Name => 'VCalendarVersion', Description => 'VCalendar Version' }, |
105
|
|
|
|
|
|
|
Calscale => 'CalendarScale', |
106
|
|
|
|
|
|
|
Method => { }, |
107
|
|
|
|
|
|
|
Prodid => 'Software', |
108
|
|
|
|
|
|
|
Attach => 'Attachment', |
109
|
|
|
|
|
|
|
Categories => { }, |
110
|
|
|
|
|
|
|
Class => 'Classification', |
111
|
|
|
|
|
|
|
Comment => { }, |
112
|
|
|
|
|
|
|
Description => { }, |
113
|
|
|
|
|
|
|
Geo => { |
114
|
|
|
|
|
|
|
Name => 'Geolocation', |
115
|
|
|
|
|
|
|
Groups => { 2 => 'Location' }, |
116
|
|
|
|
|
|
|
ValueConv => '$val =~ s/^geo://; $val', |
117
|
|
|
|
|
|
|
}, |
118
|
|
|
|
|
|
|
Location => { Name => 'Location', Groups => { 2 => 'Location' } }, |
119
|
|
|
|
|
|
|
'Percent-complete' => 'PercentComplete', |
120
|
|
|
|
|
|
|
Priority => { }, |
121
|
|
|
|
|
|
|
Resources => { }, |
122
|
|
|
|
|
|
|
Status => { }, |
123
|
|
|
|
|
|
|
Summary => { }, |
124
|
|
|
|
|
|
|
Completed => { Name => 'DateTimeCompleted', Groups => { 2 => 'Time' }, %timeInfo }, |
125
|
|
|
|
|
|
|
Dtend => { Name => 'DateTimeEnd', Groups => { 2 => 'Time' }, %timeInfo }, |
126
|
|
|
|
|
|
|
Due => { Name => 'DateTimeDue', Groups => { 2 => 'Time' }, %timeInfo }, |
127
|
|
|
|
|
|
|
Dtstart => { Name => 'DateTimeStart', Groups => { 2 => 'Time' }, %timeInfo }, |
128
|
|
|
|
|
|
|
Duration => { }, |
129
|
|
|
|
|
|
|
Freebusy => 'FreeBusyTime', |
130
|
|
|
|
|
|
|
Transp => 'TimeTransparency', |
131
|
|
|
|
|
|
|
Tzid => { Name => 'TimezoneID', Groups => { 2 => 'Time' } }, |
132
|
|
|
|
|
|
|
Tzname => { Name => 'TimezoneName', Groups => { 2 => 'Time' } }, |
133
|
|
|
|
|
|
|
Tzoffsetfrom=> { Name => 'TimezoneOffsetFrom', Groups => { 2 => 'Time' } }, |
134
|
|
|
|
|
|
|
Tzoffsetto => { Name => 'TimezoneOffsetTo', Groups => { 2 => 'Time' } }, |
135
|
|
|
|
|
|
|
Tzurl => { Name => 'TimeZoneURL', Groups => { 2 => 'Time' } }, |
136
|
|
|
|
|
|
|
Attendee => { }, |
137
|
|
|
|
|
|
|
Contact => { }, |
138
|
|
|
|
|
|
|
Organizer => { }, |
139
|
|
|
|
|
|
|
'Recurrence-id' => 'RecurrenceID', |
140
|
|
|
|
|
|
|
'Related-to' => 'RelatedTo', |
141
|
|
|
|
|
|
|
Url => 'URL', |
142
|
|
|
|
|
|
|
Uid => 'UID', |
143
|
|
|
|
|
|
|
Exdate => { Name => 'ExceptionDateTimes', Groups => { 2 => 'Time' }, %timeInfo }, |
144
|
|
|
|
|
|
|
Rdate => { Name => 'RecurrenceDateTimes', Groups => { 2 => 'Time' }, %timeInfo }, |
145
|
|
|
|
|
|
|
Rrule => { Name => 'RecurrenceRule', Groups => { 2 => 'Time' } }, |
146
|
|
|
|
|
|
|
Action => { }, |
147
|
|
|
|
|
|
|
Repeat => { }, |
148
|
|
|
|
|
|
|
Trigger => { }, |
149
|
|
|
|
|
|
|
Created => { Name => 'DateCreated', Groups => { 2 => 'Time' }, %timeInfo }, |
150
|
|
|
|
|
|
|
Dtstamp => { Name => 'DateTimeStamp', Groups => { 2 => 'Time' }, %timeInfo }, |
151
|
|
|
|
|
|
|
'Last-modified' => { Name => 'ModifyDate', Groups => { 2 => 'Time' }, %timeInfo }, |
152
|
|
|
|
|
|
|
Sequence => 'SequenceNumber', |
153
|
|
|
|
|
|
|
'Request-status' => 'RequestStatus', |
154
|
|
|
|
|
|
|
Acknowledged=> { Name => 'Acknowledged', Groups => { 2 => 'Time' }, %timeInfo }, |
155
|
|
|
|
|
|
|
# |
156
|
|
|
|
|
|
|
# Observed X-tags (not a comprehensive list): |
157
|
|
|
|
|
|
|
# |
158
|
|
|
|
|
|
|
'X-apple-calendar-color'=> 'CalendarColor', |
159
|
|
|
|
|
|
|
'X-apple-default-alarm' => 'DefaultAlarm', |
160
|
|
|
|
|
|
|
'X-apple-local-default-alarm' => 'LocalDefaultAlarm', |
161
|
|
|
|
|
|
|
'X-microsoft-cdo-appt-sequence' => 'AppointmentSequence', |
162
|
|
|
|
|
|
|
'X-microsoft-cdo-ownerapptid' => 'OwnerAppointmentID', |
163
|
|
|
|
|
|
|
'X-microsoft-cdo-busystatus' => 'BusyStatus', |
164
|
|
|
|
|
|
|
'X-microsoft-cdo-intendedstatus' => 'IntendedBusyStatus', |
165
|
|
|
|
|
|
|
'X-microsoft-cdo-alldayevent' => 'AllDayEvent', |
166
|
|
|
|
|
|
|
'X-microsoft-cdo-importance' => { |
167
|
|
|
|
|
|
|
Name => 'Importance', |
168
|
|
|
|
|
|
|
PrintConv => { |
169
|
|
|
|
|
|
|
0 => 'Low', |
170
|
|
|
|
|
|
|
1 => 'Normal', |
171
|
|
|
|
|
|
|
2 => 'High', |
172
|
|
|
|
|
|
|
}, |
173
|
|
|
|
|
|
|
}, |
174
|
|
|
|
|
|
|
'X-microsoft-cdo-insttype' => { |
175
|
|
|
|
|
|
|
Name => 'InstanceType', |
176
|
|
|
|
|
|
|
PrintConv => { |
177
|
|
|
|
|
|
|
0 => 'Non-recurring Appointment', |
178
|
|
|
|
|
|
|
1 => 'Recurring Appointment', |
179
|
|
|
|
|
|
|
2 => 'Single Instance of Recurring Appointment', |
180
|
|
|
|
|
|
|
3 => 'Exception to Recurring Appointment', |
181
|
|
|
|
|
|
|
}, |
182
|
|
|
|
|
|
|
}, |
183
|
|
|
|
|
|
|
'X-microsoft-donotforwardmeeting' => 'DoNotForwardMeeting', |
184
|
|
|
|
|
|
|
'X-microsoft-disallow-counter' => 'DisallowCounterProposal', |
185
|
|
|
|
|
|
|
'X-microsoft-locations' => { Name => 'MeetingLocations', Groups => { 2 => 'Location' } }, |
186
|
|
|
|
|
|
|
'X-wr-caldesc' => 'CalendarDescription', |
187
|
|
|
|
|
|
|
'X-wr-calname' => 'CalendarName', |
188
|
|
|
|
|
|
|
'X-wr-relcalid' => 'CalendarID', |
189
|
|
|
|
|
|
|
'X-wr-timezone' => { Name => 'TimeZone2', Groups => { 2 => 'Time' } }, |
190
|
|
|
|
|
|
|
'X-wr-alarmuid' => 'AlarmUID', |
191
|
|
|
|
|
|
|
); |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
#------------------------------------------------------------------------------ |
194
|
|
|
|
|
|
|
# Get vCard tag, creating if necessary |
195
|
|
|
|
|
|
|
# Inputs: 0) ExifTool ref, 1) tag table ref, 2) tag ID, 3) tag Name, |
196
|
|
|
|
|
|
|
# 4) source tagInfo ref, 5) lang code |
197
|
|
|
|
|
|
|
# Returns: tagInfo ref |
198
|
|
|
|
|
|
|
sub GetVCardTag($$$$;$$) |
199
|
|
|
|
|
|
|
{ |
200
|
120
|
|
|
120
|
0
|
271
|
my ($et, $tagTablePtr, $tag, $name, $srcInfo, $langCode) = @_; |
201
|
120
|
|
|
|
|
181
|
my $tagInfo = $$tagTablePtr{$tag}; |
202
|
120
|
100
|
|
|
|
193
|
unless ($tagInfo) { |
203
|
52
|
100
|
|
|
|
75
|
if ($srcInfo) { |
204
|
50
|
|
|
|
|
203
|
$tagInfo = { %$srcInfo }; |
205
|
|
|
|
|
|
|
} else { |
206
|
2
|
|
|
|
|
5
|
$tagInfo = { }; |
207
|
2
|
|
|
|
|
14
|
$et->VPrint(0, $$et{INDENT}, "[adding $tag]\n"); |
208
|
|
|
|
|
|
|
} |
209
|
52
|
|
|
|
|
93
|
$$tagInfo{Name} = $name; |
210
|
52
|
|
|
|
|
61
|
delete $$tagInfo{Description}; # create new description |
211
|
52
|
|
|
|
|
103
|
AddTagToTable($tagTablePtr, $tag, $tagInfo); |
212
|
|
|
|
|
|
|
} |
213
|
|
|
|
|
|
|
# handle alternate languages (the "language" parameter) |
214
|
120
|
100
|
|
|
|
185
|
$tagInfo = Image::ExifTool::GetLangInfo($tagInfo, $langCode) if $langCode; |
215
|
120
|
|
|
|
|
187
|
return $tagInfo; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
#------------------------------------------------------------------------------ |
219
|
|
|
|
|
|
|
# Decode vCard text |
220
|
|
|
|
|
|
|
# Inputs: 0) ExifTool ref, 1) vCard text, 2) encoding |
221
|
|
|
|
|
|
|
# Returns: decoded text (or array ref for a list of values) |
222
|
|
|
|
|
|
|
sub DecodeVCardText($$;$) |
223
|
|
|
|
|
|
|
{ |
224
|
120
|
|
|
120
|
0
|
271
|
my ($et, $val, $enc) = @_; |
225
|
120
|
100
|
|
|
|
208
|
$enc = defined($enc) ? lc $enc : ''; |
226
|
120
|
100
|
100
|
|
|
346
|
if ($enc eq 'b' or $enc eq 'base64') { |
227
|
2
|
|
|
|
|
1044
|
require Image::ExifTool::XMP; |
228
|
2
|
|
|
|
|
69
|
$val = Image::ExifTool::XMP::DecodeBase64($val); |
229
|
|
|
|
|
|
|
} else { |
230
|
118
|
100
|
|
|
|
171
|
if ($enc eq 'quoted-printable') { |
231
|
|
|
|
|
|
|
# convert "=HH" hex codes to characters |
232
|
1
|
|
|
|
|
14
|
$val =~ s/=([0-9a-f]{2})/chr(hex($1))/ige; |
|
4
|
|
|
|
|
14
|
|
233
|
|
|
|
|
|
|
} |
234
|
118
|
|
|
|
|
274
|
$val = $et->Decode($val, 'UTF8'); # convert from UTF-8 |
235
|
|
|
|
|
|
|
# convert unescaped commas to nulls to separate list items |
236
|
118
|
100
|
|
|
|
397
|
$val =~ s/(\\.)|(,)/$1 || "\0"/sge; |
|
6
|
|
|
|
|
26
|
|
237
|
|
|
|
|
|
|
# unescape necessary characters in value |
238
|
118
|
50
|
|
|
|
164
|
$val =~ s/\\(.)/$unescapeVCard{$1}||$1/sge; |
|
5
|
|
|
|
|
17
|
|
239
|
|
|
|
|
|
|
# split into list if necessary |
240
|
118
|
|
|
|
|
260
|
my @vals = split /\0/, $val; |
241
|
118
|
100
|
|
|
|
240
|
$val = \@vals if @vals > 1; |
242
|
|
|
|
|
|
|
} |
243
|
120
|
|
|
|
|
195
|
return $val; |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
#------------------------------------------------------------------------------ |
247
|
|
|
|
|
|
|
# Read information in a vCard file |
248
|
|
|
|
|
|
|
# Inputs: 0) ExifTool ref, 1) dirInfo ref |
249
|
|
|
|
|
|
|
# Returns: 1 on success, 0 if this wasn't a valid vCard file |
250
|
|
|
|
|
|
|
sub ProcessVCard($$) |
251
|
|
|
|
|
|
|
{ |
252
|
2
|
|
|
2
|
0
|
3
|
local $_; |
253
|
2
|
|
|
|
|
7
|
my ($et, $dirInfo) = @_; |
254
|
2
|
|
|
|
|
4
|
my $raf = $$dirInfo{RAF}; |
255
|
2
|
|
|
|
|
6
|
my ($buff, $val, $ok, $component, %compNum, @count); |
256
|
|
|
|
|
|
|
|
257
|
2
|
50
|
33
|
|
|
5
|
return 0 unless $raf->Read($buff, 24) and $raf->Seek(0,0) and $buff=~/^BEGIN:(VCARD|VCALENDAR)\r\n/i; |
|
|
|
33
|
|
|
|
|
258
|
2
|
100
|
|
|
|
15
|
my ($type, $lbl, $tbl, $ext) = uc($1) eq 'VCARD' ? qw(VCard vCard Main VCF) : qw(ICS iCalendar VCalendar ICS); |
259
|
2
|
|
|
|
|
14
|
$et->SetFileType($type, undef, $ext); |
260
|
2
|
50
|
33
|
|
|
9
|
return 1 if $$et{OPTIONS}{FastScan} and $$et{OPTIONS}{FastScan} == 3; |
261
|
2
|
|
|
|
|
11
|
local $/ = "\r\n"; |
262
|
2
|
|
|
|
|
44
|
my $tagTablePtr = GetTagTable("Image::ExifTool::VCard::$tbl"); |
263
|
2
|
|
|
|
|
12
|
my $more = $raf->ReadLine($buff); # read first line |
264
|
2
|
50
|
|
|
|
9
|
chomp $buff if $more; |
265
|
2
|
|
|
|
|
4
|
while ($more) { |
266
|
|
|
|
|
|
|
# retrieve previous line from $buff |
267
|
139
|
50
|
|
|
|
264
|
$val = $buff if defined $buff; |
268
|
|
|
|
|
|
|
# read ahead to next line to see if is a continuation |
269
|
139
|
|
|
|
|
311
|
$more = $raf->ReadLine($buff); |
270
|
139
|
100
|
|
|
|
247
|
if ($more) { |
271
|
137
|
|
|
|
|
168
|
chomp $buff; |
272
|
|
|
|
|
|
|
# add continuation line if necessary |
273
|
137
|
50
|
|
|
|
337
|
$buff =~ s/^[ \t]// and $val .= $buff, undef($buff), next; |
274
|
|
|
|
|
|
|
} |
275
|
139
|
100
|
|
|
|
403
|
if ($val =~ /^(BEGIN|END):(V?)(\w+)$/i) { |
|
|
100
|
|
|
|
|
|
276
|
28
|
100
|
|
|
|
113
|
my ($begin, $v, $what) = ((lc($1) eq 'begin' ? 1 : 0), $2, ucfirst lc $3); |
277
|
28
|
100
|
100
|
|
|
99
|
if ($what eq 'Card' or $what eq 'Calendar') { |
278
|
6
|
100
|
|
|
|
11
|
if ($begin) { |
279
|
3
|
|
|
|
|
8
|
@count = ( { } ); # reset group counters |
280
|
|
|
|
|
|
|
} else { |
281
|
3
|
|
|
|
|
7
|
$ok = 1; # ok if we read at least on full VCARD or VCALENDAR |
282
|
|
|
|
|
|
|
} |
283
|
6
|
|
|
|
|
13
|
next; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
# absorb top-level component into family 1 group name |
286
|
22
|
100
|
|
|
|
44
|
if ($isComponent{$what}) { |
287
|
14
|
100
|
66
|
|
|
35
|
if ($begin) { |
|
|
100
|
|
|
|
|
|
288
|
7
|
100
|
|
|
|
12
|
unless ($component) { |
289
|
|
|
|
|
|
|
# begin a new top-level component |
290
|
4
|
|
|
|
|
10
|
@count = ( { } ); |
291
|
4
|
|
|
|
|
8
|
$component = $what; |
292
|
4
|
|
100
|
|
|
14
|
$compNum{$component} = ($compNum{$component} || 0) + 1; |
293
|
4
|
|
|
|
|
9
|
next; |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
} elsif ($component and $component eq $what) { |
296
|
|
|
|
|
|
|
# this top-level component has ended |
297
|
4
|
|
|
|
|
7
|
undef $component; |
298
|
4
|
|
|
|
|
8
|
next; |
299
|
|
|
|
|
|
|
} |
300
|
|
|
|
|
|
|
} |
301
|
|
|
|
|
|
|
# keep count of each component at this level |
302
|
14
|
100
|
|
|
|
42
|
if ($begin) { |
|
|
50
|
|
|
|
|
|
303
|
7
|
100
|
100
|
|
|
20
|
$count[-1]{$what} = ($count[-1]{$what} || 0) + 1 if $v; |
304
|
7
|
|
|
|
|
18
|
push @count, { obj => $what }; |
305
|
|
|
|
|
|
|
} elsif (@count > 1) { |
306
|
7
|
|
|
|
|
10
|
pop @count; |
307
|
|
|
|
|
|
|
} |
308
|
14
|
|
|
|
|
32
|
next; |
309
|
|
|
|
|
|
|
} elsif ($ok) { |
310
|
1
|
|
|
|
|
2
|
$ok = 0; |
311
|
1
|
|
|
|
|
7
|
$$et{DOC_NUM} = ++$$et{DOC_COUNT}; # read next card as a new document |
312
|
|
|
|
|
|
|
} |
313
|
111
|
50
|
|
|
|
410
|
unless ($val =~ s/^([-A-Za-z0-9.]+)//) { |
314
|
0
|
|
|
|
|
0
|
$et->WarnOnce("Unrecognized line in $lbl file"); |
315
|
0
|
|
|
|
|
0
|
next; |
316
|
|
|
|
|
|
|
} |
317
|
111
|
|
|
|
|
247
|
my $tag = $1; |
318
|
|
|
|
|
|
|
# set group if it exists |
319
|
111
|
100
|
|
|
|
231
|
if ($tag =~ s/^([-A-Za-z0-9]+)\.//) { |
|
|
100
|
|
|
|
|
|
320
|
8
|
|
|
|
|
21
|
$$et{SET_GROUP1} = ucfirst lc $1; |
321
|
|
|
|
|
|
|
} elsif ($component) { |
322
|
64
|
|
|
|
|
153
|
$$et{SET_GROUP1} = $component . $compNum{$component}; |
323
|
|
|
|
|
|
|
} else { |
324
|
39
|
|
|
|
|
53
|
delete $$et{SET_GROUP1}; |
325
|
|
|
|
|
|
|
} |
326
|
111
|
|
|
|
|
150
|
my ($name, %param, $p); |
327
|
|
|
|
|
|
|
# vCard tag ID's are case-insensitive, so normalize to lowercase with |
328
|
|
|
|
|
|
|
# an uppercase first letter for use as a tag name |
329
|
111
|
100
|
|
|
|
233
|
$name = ucfirst $tag if $tag =~ /[a-z]/; # preserve mixed case in name if it exists |
330
|
111
|
|
|
|
|
174
|
$tag = ucfirst lc $tag; |
331
|
|
|
|
|
|
|
# get source tagInfo reference |
332
|
111
|
|
|
|
|
230
|
my $srcInfo = $et->GetTagInfo($tagTablePtr, $tag); |
333
|
111
|
100
|
|
|
|
212
|
if ($srcInfo) { |
334
|
110
|
|
|
|
|
170
|
$name = $$srcInfo{Name}; # use our name |
335
|
|
|
|
|
|
|
} else { |
336
|
1
|
50
|
|
|
|
3
|
$name or $name = $tag; |
337
|
|
|
|
|
|
|
# remove leading "X-" from name if it exists |
338
|
1
|
50
|
|
|
|
4
|
$name =~ s/^X-// and $name = ucfirst $name; |
339
|
|
|
|
|
|
|
} |
340
|
|
|
|
|
|
|
# add object name(s) to tag if necessary |
341
|
111
|
100
|
|
|
|
193
|
if (@count > 1) { |
342
|
40
|
|
|
|
|
42
|
my $i; |
343
|
40
|
|
|
|
|
84
|
for ($i=$#count-1; $i>=0; --$i) { |
344
|
40
|
|
|
|
|
66
|
my $pre = $count[$i-1]{obj}; # use containing object name as tag prefix |
345
|
40
|
|
|
|
|
52
|
my $c = $count[$i]{$pre}; # add index for object number |
346
|
40
|
100
|
|
|
|
71
|
$c = '' unless defined $c; |
347
|
40
|
|
|
|
|
96
|
$tag = $pre . $c . $tag; |
348
|
40
|
|
|
|
|
88
|
$name = $pre . $c . $name; |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
} |
351
|
|
|
|
|
|
|
# parse parameters |
352
|
111
|
|
|
|
|
298
|
while ($val =~ s/^;([-A-Za-z0-9]*)(=?)//) { |
353
|
56
|
|
|
|
|
110
|
$p = ucfirst lc $1; |
354
|
|
|
|
|
|
|
# convert old vCard 2.x parameters to the new "TYPE=" format |
355
|
56
|
100
|
|
|
|
107
|
$2 or $val = $1 . $val, $p = 'Type'; |
356
|
|
|
|
|
|
|
# read parameter value |
357
|
56
|
|
|
|
|
65
|
for (;;) { |
358
|
112
|
100
|
100
|
|
|
434
|
last unless $val =~ s/^"([^"]*)",?// or $val =~ s/^([^";:,]+,?)//; |
359
|
56
|
100
|
|
|
|
131
|
my $v = $p eq 'Type' ? ucfirst lc $1 : $1; |
360
|
56
|
100
|
|
|
|
134
|
$param{$p} = defined($param{$p}) ? $param{$p} . $v : $v; |
361
|
|
|
|
|
|
|
} |
362
|
56
|
50
|
|
|
|
87
|
if (defined $param{$p}) { |
363
|
56
|
50
|
|
|
|
142
|
$param{$p} =~ s/\\(.)/$unescapeVCard{$1}||$1/sge; |
|
1
|
|
|
|
|
7
|
|
364
|
|
|
|
|
|
|
} else { |
365
|
0
|
|
|
|
|
0
|
$param{$p} = ''; |
366
|
|
|
|
|
|
|
} |
367
|
|
|
|
|
|
|
} |
368
|
111
|
50
|
|
|
|
327
|
$val =~ s/^:// or $et->WarnOnce("Invalid line in $lbl file"), next; |
369
|
|
|
|
|
|
|
# add 'Type' parameter to id and name if it exists |
370
|
111
|
100
|
|
|
|
219
|
$param{Type} and $tag .= $param{Type}, $name .= $param{Type}; |
371
|
|
|
|
|
|
|
# convert base64-encoded data |
372
|
111
|
100
|
|
|
|
198
|
if ($val =~ s{^data:(\w+)/(\w+);base64,}{}) { |
373
|
1
|
|
|
|
|
5
|
my $xtra = ucfirst(lc $1) . ucfirst(lc $2); |
374
|
1
|
|
|
|
|
4
|
$tag .= $xtra; |
375
|
1
|
|
|
|
|
2
|
$name .= $xtra; |
376
|
1
|
|
|
|
|
2
|
$param{Encoding} = 'base64'; |
377
|
|
|
|
|
|
|
} |
378
|
111
|
|
|
|
|
266
|
$val = DecodeVCardText($et, $val, $param{Encoding}); |
379
|
111
|
|
|
|
|
282
|
my $tagInfo = GetVCardTag($et, $tagTablePtr, $tag, $name, $srcInfo, $param{Language}); |
380
|
111
|
|
|
|
|
320
|
$et->HandleTag($tagTablePtr, $tag, $val, TagInfo => $tagInfo); |
381
|
|
|
|
|
|
|
# handle some other parameters that we care about (ignore the rest for now) |
382
|
111
|
|
|
|
|
161
|
foreach $p (qw(Geo Label Tzid)) { |
383
|
333
|
100
|
|
|
|
658
|
next unless defined $param{$p}; |
384
|
|
|
|
|
|
|
# use tag attributes from our table if it exists |
385
|
9
|
|
|
|
|
28
|
my $srcTag2 = $et->GetTagInfo($tagTablePtr, $p); |
386
|
9
|
100
|
|
|
|
21
|
my $pn = $srcTag2 ? $$srcTag2{Name} : $p; |
387
|
9
|
|
|
|
|
24
|
$val = DecodeVCardText($et, $param{$p}); |
388
|
|
|
|
|
|
|
# add parameter to tag ID and name |
389
|
9
|
|
|
|
|
26
|
my ($tg, $nm) = ($tag . $p, $name . $pn); |
390
|
9
|
|
|
|
|
22
|
$tagInfo = GetVCardTag($et, $tagTablePtr, $tg, $nm, $srcTag2, $param{Language}); |
391
|
9
|
|
|
|
|
28
|
$et->HandleTag($tagTablePtr, $tg, $val, TagInfo => $tagInfo); |
392
|
|
|
|
|
|
|
} |
393
|
|
|
|
|
|
|
} |
394
|
2
|
|
|
|
|
5
|
delete $$et{SET_GROUP1}; |
395
|
2
|
|
|
|
|
3
|
delete $$et{DOC_NUM}; |
396
|
2
|
50
|
|
|
|
6
|
$ok or $et->Warn("Missing $lbl end"); |
397
|
2
|
|
|
|
|
15
|
return 1; |
398
|
|
|
|
|
|
|
} |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
1; # end |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
__END__ |