vpim 0.16 → 0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,594 @@
1
+ =begin
2
+ $Id: field.rb,v 1.11 2005/01/07 03:32:16 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/rfc2425'
12
+ require 'vpim/vpim'
13
+ require 'date'
14
+
15
+ module Vpim
16
+
17
+ class DirectoryInfo
18
+
19
+ # A field in a directory info object.
20
+ #
21
+ # TODO
22
+ # - Field should know which params have case-insensitive values,
23
+ # configurably, so it can downcase them
24
+ # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
25
+ # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
26
+ # and del deletes any that are present
27
+ # - I really, really, need a case-insensitive string...
28
+ # - should allow nil as a field value, its not the same as '', if there is
29
+ # more than one pvalue, the empty string will show up. This isn't strictly
30
+ # disallowed, but its odd. Should also strip empty strings on decoding, if
31
+ # I don't already.
32
+ class Field
33
+ private_class_method :new
34
+
35
+ def Field.create_array(fields)
36
+ case fields
37
+ when Hash
38
+ fields.map do |name,value|
39
+ DirectoryInfo::Field.create( name, value )
40
+ end
41
+ else
42
+ fields.to_ary
43
+ end
44
+ end
45
+
46
+ # Encode a field.
47
+ def Field.encode0(group, name, params={}, value='') # :nodoc:
48
+ line = ""
49
+
50
+ # A reminder of the line format:
51
+ # [<group>.]<name>;<pname>=<pvalue>,<pvalue>:<value>
52
+
53
+ if group
54
+ line << group << '.'
55
+ end
56
+
57
+ line << name
58
+
59
+ params.each do |pname, pvalues|
60
+
61
+ unless pvalues.respond_to? :to_ary
62
+ pvalues = [ pvalues ]
63
+ end
64
+
65
+ line << ';' << pname << '='
66
+
67
+ sep = "" # set to ',' after one pvalue has been appended
68
+
69
+ pvalues.each do |pvalue|
70
+ # check if we need to do any encoding
71
+ if pname.downcase == 'encoding' && pvalue == :b64
72
+ pvalue = 'b' # the RFC definition of the base64 param value
73
+ value = [ value.to_str ].pack('m').gsub("\n", '')
74
+ end
75
+
76
+ line << sep << pvalue
77
+ sep =",";
78
+ end
79
+ end
80
+
81
+ line << ':'
82
+
83
+ line << Field.value_str(value)
84
+
85
+ line
86
+ end
87
+
88
+ def Field.value_str(value) # :nodoc:
89
+ line = ''
90
+ case value
91
+ when Date
92
+ line << Vpim.encode_date(value)
93
+
94
+ when Time #, DateTime
95
+ line << Vpim.encode_date_time(value)
96
+
97
+ when Array
98
+ line << value.map { |v| Field.value_str(v) }.join(';')
99
+
100
+ when Symbol
101
+ line << value
102
+
103
+ else
104
+ line << value.to_str
105
+ end
106
+ line
107
+ end
108
+
109
+ # Decode a field.
110
+ def Field.decode0(atline) # :nodoc:
111
+ unless atline =~ %r{#{Bnf::LINE}}i
112
+ raise Vpim::InvalidEncodingError, atline
113
+ end
114
+
115
+ atgroup = $1
116
+ atname = $2
117
+ paramslist = $3
118
+ atvalue = $~[-1]
119
+
120
+ # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so
121
+ # strip it. I'm not absolutely sure this is allowed... it certainly
122
+ # breaks round-trip encoding.
123
+ atvalue.strip!
124
+
125
+ if atgroup.length > 0
126
+ atgroup.chomp!('.')
127
+ else
128
+ atgroup = nil
129
+ end
130
+
131
+ atparams = {}
132
+
133
+ # Collect the params, if any.
134
+ if paramslist.size > 1
135
+
136
+ # v3.0 and v2.1 params
137
+ paramslist.scan( %r{#{Bnf::PARAM}}i ) do
138
+
139
+ # param names are case-insensitive, and multi-valued
140
+ name = $1.downcase
141
+ params = $3
142
+
143
+ # v2.1 params have no '=' sign, figure out what kind of param it
144
+ # is (either its a known encoding, or we treat it as a 'type'
145
+ # param).
146
+
147
+ if $2 == ""
148
+ params = $1
149
+ case $1
150
+ when /quoted-printable/i
151
+ name = 'encoding'
152
+
153
+ when /base64/i
154
+ name = 'encoding'
155
+
156
+ else
157
+ name = 'type'
158
+ end
159
+ end
160
+
161
+ # TODO - In ruby1.8 I can give an initial value to the atparams
162
+ # hash values instead of this.
163
+ unless atparams.key? name
164
+ atparams[name] = []
165
+ end
166
+
167
+ params.scan( %r{#{Bnf::PVALUE}} ) do
168
+ atparams[name] << ($1 || $2) # Used to do this, want to stop! .downcase
169
+ end
170
+ end
171
+ end
172
+
173
+ [ atgroup, atname, atparams, atvalue ]
174
+ end
175
+
176
+ def initialize(line) # :nodoc:
177
+ @line = line.to_str
178
+ @group, @name, @params, @value = Field.decode0(@line)
179
+
180
+ @params.each do |pname,pvalues|
181
+ pvalues.freeze
182
+ end
183
+ self
184
+ end
185
+
186
+ # Create a field by decoding +line+, a String which must already be
187
+ # unfolded. Decoded fields are frozen, but see #copy().
188
+ def Field.decode(line)
189
+ new(line).freeze
190
+ end
191
+
192
+ # Create a field with name +name+ (a String), value +value+ (see below),
193
+ # and optional parameters, +params+. +params+ is a hash of the parameter
194
+ # name (a String) to either a single string or symbol, or an array of
195
+ # strings and symbols (parameters can be multi-valued).
196
+ #
197
+ # If 'encoding' => :b64 is specified as a parameter, the value will be
198
+ # base-64 encoded. If it's already base-64 encoded, then use String
199
+ # values ('encoding' => 'b'), and no further encoding will be done by
200
+ # this routine.
201
+ #
202
+ # Currently handled value types are:
203
+ # - Time, encoded as a date-time value
204
+ # - Date, encoded as a date value
205
+ # - String, encoded directly
206
+ # - Array of String, concatentated with ';' between them.
207
+ #
208
+ # TODO - need a way to encode String values as TEXT, at least optionally,
209
+ # so as to escape special chars, etc.
210
+ def Field.create(name, value="", params={})
211
+ line = Field.encode0(nil, name, params, value)
212
+
213
+ begin
214
+ new(line)
215
+ rescue Vpim::InvalidEncodingError => e
216
+ raise ArgumentError, e.to_s
217
+ end
218
+ end
219
+
220
+ # Create a copy of Field. If the original Field was frozen, this one
221
+ # won't be.
222
+ def copy
223
+ Marshal.load(Marshal.dump(self))
224
+ end
225
+
226
+ # The String encoding of the Field. The String will be wrapped to a
227
+ # maximum line width of +width+, where +0+ means no wrapping, and nil is
228
+ # to accept the default wrapping (75, recommended by RFC2425).
229
+ #
230
+ # Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it
231
+ # imports vCards (it treats them as raw new-line characters), nor wraps
232
+ # long lines on export. This is mostly a cosmetic problem, but wrapping
233
+ # can be disabled by setting width to +0+, if desired.
234
+ def encode(width=nil)
235
+ width = 75 unless width
236
+ l = @line
237
+ # Wrap to width, unless width is zero.
238
+ if width > 0
239
+ l = l.gsub(/.{#{width},#{width}}/) { |m| m + "\n " }
240
+ end
241
+ # Make sure it's terminated with no more than a single NL.
242
+ l.gsub(/\s*\z/, '') + "\n"
243
+ end
244
+
245
+ alias to_s encode
246
+
247
+ # The name.
248
+ def name
249
+ @name
250
+ end
251
+
252
+ # The group, if present, or nil if not present.
253
+ def group
254
+ @group
255
+ end
256
+
257
+ # An Array of all the param names.
258
+ def pnames
259
+ @params.keys
260
+ end
261
+
262
+ # FIXME - remove my own uses of #params
263
+ alias params pnames # :nodoc:
264
+
265
+ # The Array of all values of the param +name+, nil if there is no such
266
+ # param, [] if the param has no values. If the Field isn't frozen, the
267
+ # Array is mutable.
268
+ def pvalues(name)
269
+ @params[name.downcase]
270
+ end
271
+
272
+ # FIXME - remove my own uses of #param
273
+ alias param pvalues # :nodoc:
274
+
275
+ alias [] param
276
+
277
+ # Yield once for each param, +name+ is the parameter name, +value+ is an
278
+ # array of the parameter values.
279
+ def each_param(&block) #:yield: name, value
280
+ if @params
281
+ @params.each(&block)
282
+ end
283
+ end
284
+
285
+ # The decoded value.
286
+ #
287
+ # The encoding specified by the #encoding, if any, is stripped.
288
+ #
289
+ # Note: Both the RFC 2425 encoding param ("b", meaning base-64) and the
290
+ # vCard 2.1 encoding params ("base64", "quoted-printable", "8bit", and
291
+ # "7bit") are supported.
292
+ def value
293
+ case encoding
294
+ when nil, '8bit', '7bit' then @value
295
+
296
+ # Hack - if the base64 lines started with 2 SPC chars, which is invalid,
297
+ # there will be extra spaces in @value. Since no SPC chars show up in
298
+ # b64 encodings, they can be safely stripped out before unpacking.
299
+ when 'b', 'base64' then @value.gsub(' ', '').unpack('m*').first
300
+
301
+ when 'quoted-printable' then @value.unpack('M*').first
302
+
303
+ else raise Vpim::InvalidEncodingError, "unrecognized encoding (#{encoding})"
304
+ end
305
+ end
306
+
307
+ # Is the #name of this Field +name+? Names are case insensitive.
308
+ def name?(name)
309
+ name.to_s.downcase == @name.downcase
310
+ end
311
+
312
+ # Is the #group of this field +group+? Group names are case insensitive.
313
+ # A +group+ of nil matches if the field has no group.
314
+ def group?(group)
315
+ g1 = @group ? @group.downcase : nil
316
+ g2 = group ? group.downcase : nil
317
+ g1 == g2
318
+ end
319
+
320
+ # Is the value of this field of type +kind+? RFC2425 allows the type of
321
+ # a fields value to be encoded in the VALUE parameter. Don't rely on its
322
+ # presence, they aren't required, and usually aren't bothered with. In
323
+ # cases where the kind of value might vary (an iCalendar DTSTART can be
324
+ # either a date or a date-time, for example), you are more likely to see
325
+ # the kind of value specified explicitly.
326
+ #
327
+ # The value types defined by RFC 2425 are:
328
+ # - uri:
329
+ # - text:
330
+ # - date: a list of 1 or more dates
331
+ # - time: a list of 1 or more times
332
+ # - date-time: a list of 1 or more date-times
333
+ # - integer:
334
+ # - boolean:
335
+ # - float:
336
+ def kind?(kind)
337
+ kind.downcase == self.kind
338
+ end
339
+
340
+ # Is one of the values of the TYPE parameter of this field +type+? The
341
+ # type parameter values are case insensitive. False if there is no TYPE
342
+ # parameter.
343
+ #
344
+ # TYPE parameters are used for general categories, such as
345
+ # distinguishing between an email address used at home or at work.
346
+ def type?(type)
347
+ types = param('type')
348
+
349
+ if types
350
+ types = types.include?(type.to_str.downcase)
351
+ end
352
+ end
353
+
354
+ # Is this field marked as preferred? A vCard field is preferred if
355
+ # #type?('pref'). This method is not necessarily meaningful for
356
+ # non-vCard profiles.
357
+ def pref?
358
+ type? 'pref'
359
+ end
360
+
361
+ # Set whether a field is marked as preferred. See #pref?
362
+ def pref=(ispref)
363
+ if ispref
364
+ pvalue_iadd('type', 'pref')
365
+ else
366
+ pvalue_idel('type', 'pref')
367
+ end
368
+ end
369
+
370
+ # Is the value of this field +value+? The check is case insensitive.
371
+ def value?(value)
372
+ @value && @value.downcase == value.downcase
373
+ end
374
+
375
+ # The value of the ENCODING parameter, if present, or nil if not
376
+ # present.
377
+ def encoding
378
+ e = param("encoding")
379
+
380
+ if e
381
+ if e.length > 1
382
+ raise Vpim::InvalidEncodingError, "multi-valued param 'encoding' (#{e})"
383
+ end
384
+ e = e.first
385
+ end
386
+ e
387
+ end
388
+
389
+ # The type of the value, as specified by the VALUE parameter, nil if
390
+ # unspecified.
391
+ def kind
392
+ v = param('value')
393
+ if v
394
+ if v.size > 1
395
+ raise InvalidEncodingError, "multi-valued param 'value' (#{values})"
396
+ end
397
+ v = v.first
398
+ end
399
+ v
400
+ end
401
+
402
+ # The value as an array of Time objects (all times and dates in
403
+ # RFC2425 are lists, even where it might not make sense, such as a
404
+ # birthday). The time will be UTC if marked as so (with a timezone of
405
+ # "Z"), and in localtime otherwise.
406
+ #
407
+ # TODO: support timezone offsets
408
+ #
409
+ # TODO - if year is before 1970, this won't work... but some people
410
+ # are generating calendars saying Canada Day started in 1753!
411
+ # That's just wrong! So, what to do? I add a message
412
+ # saying what the year is that breaks, so they at least know that
413
+ # its ridiculous! I think I need my own DateTime variant.
414
+ def to_time
415
+ begin
416
+ Vpim.decode_date_time_list(value).collect do |d|
417
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
418
+ begin
419
+ if(d.pop == "Z")
420
+ Time.gm(*d)
421
+ else
422
+ Time.local(*d)
423
+ end
424
+ rescue ArgumentError => e
425
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
426
+ end
427
+ end
428
+ rescue Vpim::InvalidEncodingError
429
+ Vpim.decode_date_list(value).collect do |d|
430
+ # We get [ year, month, day ]
431
+ begin
432
+ Time.gm(*d)
433
+ rescue ArgumentError => e
434
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
435
+ end
436
+ end
437
+ end
438
+ end
439
+
440
+ # The value as an array of Date objects (all times and dates in
441
+ # RFC2425 are lists, even where it might not make sense, such as a
442
+ # birthday).
443
+ #
444
+ # The field value may be a list of either DATE or DATE-TIME values,
445
+ # decoding is tried first as a DATE-TIME, then as a DATE, if neither
446
+ # works an InvalidEncodingError will be raised.
447
+ def to_date
448
+ begin
449
+ Vpim.decode_date_time_list(value).collect do |d|
450
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
451
+ Date.new(d[0], d[1], d[2])
452
+ end
453
+ rescue Vpim::InvalidEncodingError
454
+ Vpim.decode_date_list(value).collect do |d|
455
+ # We get [ year, month, day ]
456
+ Date.new(*d)
457
+ end
458
+ end
459
+ end
460
+
461
+ # The value as text. Text can have escaped newlines, commas, and escape
462
+ # characters, this method will strip them, if present.
463
+ #
464
+ # In theory, #value could also do this, but it would need to know that
465
+ # the value is of type 'text', and often for text values the 'type'
466
+ # parameter is not present, so knowledge of the expected type of the
467
+ # field is required from the decoder.
468
+ def to_text
469
+ Vpim.decode_text(value)
470
+ end
471
+
472
+ # The undecoded value, see +value+.
473
+ def value_raw
474
+ @value
475
+ end
476
+
477
+ def inspect # :nodoc:
478
+ "#{self.class}: #{@line.inspect}"
479
+ end
480
+
481
+ # TODO def pretty_print() ...
482
+
483
+ # Set the group of this field to +group+.
484
+ def group=(group)
485
+ mutate(group, @name, @params, @value)
486
+ group
487
+ end
488
+
489
+ # Set the value of this field to +value+. Valid values are as in
490
+ # Field.create().
491
+ def value=(value)
492
+ mutate(@group, @name, @params, value)
493
+ value
494
+ end
495
+
496
+ # Convert +value+ to text, then assign.
497
+ #
498
+ # TODO - unimplemented
499
+ def text=(text)
500
+ end
501
+
502
+ # Set a the param +pname+'s value to +pvalue+, replacing any value it
503
+ # currently has. See Field.create() for a description of +pvalue+.
504
+ #
505
+ # Example:
506
+ # if field['type']
507
+ # field['type'] << 'home'
508
+ # else
509
+ # field['type'] = [ 'home'
510
+ # end
511
+ #
512
+ # TODO - this could be an alias to #pvalue_set
513
+ def []=(pname,pvalue)
514
+ unless pvalue.respond_to?(:to_ary)
515
+ pvalue = [ pvalue ]
516
+ end
517
+
518
+ h = @params.dup
519
+
520
+ h[pname.downcase] = pvalue
521
+
522
+ mutate(@group, @name, h, @value)
523
+ pvalue
524
+ end
525
+
526
+ # Add +pvalue+ to the param +pname+'s value. The values are treated as a
527
+ # set so duplicate values won't occur, and String values are case
528
+ # insensitive. See Field.create() for a description of +pvalue+.
529
+ def pvalue_iadd(pname, pvalue)
530
+ pname = pname.downcase
531
+
532
+ # Get a uniq set, where strings are compared case-insensitively.
533
+ values = [ pvalue, @params[pname] ].flatten.compact
534
+ values = values.collect do |v|
535
+ if v.respond_to? :to_str
536
+ v = v.to_str.downcase
537
+ end
538
+ v
539
+ end
540
+ values.uniq!
541
+
542
+ h = @params.dup
543
+
544
+ h[pname] = values
545
+
546
+ mutate(@group, @name, h, @value)
547
+ values
548
+ end
549
+
550
+ # Delete +pvalue+ from the param +pname+'s value. The values are treated
551
+ # as a set so duplicate values won't occur, and String values are case
552
+ # insensitive. +pvalue+ must be a single String or Symbol.
553
+ def pvalue_idel(pname, pvalue)
554
+ pname = pname.downcase
555
+ if pvalue.respond_to? :to_str
556
+ pvalue = pvalue.to_str.downcase
557
+ end
558
+
559
+ # Get a uniq set, where strings are compared case-insensitively.
560
+ values = [ nil, @params[pname] ].flatten.compact
561
+ values = values.collect do |v|
562
+ if v.respond_to? :to_str
563
+ v = v.to_str.downcase
564
+ end
565
+ v
566
+ end
567
+ values.uniq!
568
+ values.delete pvalue
569
+
570
+ h = @params.dup
571
+
572
+ h[pname] = values
573
+
574
+ mutate(@group, @name, h, @value)
575
+ values
576
+ end
577
+
578
+ def mutate(g, n, p, v) #:nodoc:
579
+ line = Field.encode0(g, n, p, v)
580
+
581
+ begin
582
+ @group, @name, @params, @value = Field.decode0(line)
583
+ @line = line
584
+ rescue Vpim::InvalidEncodingError => e
585
+ raise ArgumentError, e.to_s
586
+ end
587
+ self
588
+ end
589
+
590
+ private :mutate
591
+ end
592
+ end
593
+ end
594
+