virginity 0.3.31

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/virginity.rb +6 -0
  3. data/lib/virginity/api_extensions.rb +87 -0
  4. data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
  5. data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
  6. data/lib/virginity/bnf.rb +84 -0
  7. data/lib/virginity/dir_info.rb +93 -0
  8. data/lib/virginity/dir_info/content_line.rb +146 -0
  9. data/lib/virginity/dir_info/line_folding.rb +60 -0
  10. data/lib/virginity/dir_info/param.rb +208 -0
  11. data/lib/virginity/dir_info/query.rb +144 -0
  12. data/lib/virginity/encoding_decoding.rb +177 -0
  13. data/lib/virginity/encodings.rb +36 -0
  14. data/lib/virginity/fixes.rb +230 -0
  15. data/lib/virginity/vcard.rb +244 -0
  16. data/lib/virginity/vcard/base_field.rb +126 -0
  17. data/lib/virginity/vcard/categories.rb +57 -0
  18. data/lib/virginity/vcard/cleaning.rb +364 -0
  19. data/lib/virginity/vcard/field.rb +22 -0
  20. data/lib/virginity/vcard/field/params.rb +93 -0
  21. data/lib/virginity/vcard/field_values.rb +10 -0
  22. data/lib/virginity/vcard/field_values/binary.rb +22 -0
  23. data/lib/virginity/vcard/field_values/boolean.rb +14 -0
  24. data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
  25. data/lib/virginity/vcard/field_values/date.rb +16 -0
  26. data/lib/virginity/vcard/field_values/integer.rb +15 -0
  27. data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
  28. data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
  29. data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
  30. data/lib/virginity/vcard/field_values/text.rb +23 -0
  31. data/lib/virginity/vcard/field_values/uri.rb +15 -0
  32. data/lib/virginity/vcard/fields.rb +284 -0
  33. data/lib/virginity/vcard/fields_osx.rb +95 -0
  34. data/lib/virginity/vcard/fields_soocial.rb +45 -0
  35. data/lib/virginity/vcard/name_handler.rb +151 -0
  36. data/lib/virginity/vcard/patching.rb +262 -0
  37. data/lib/virginity/vcard21.rb +2 -0
  38. data/lib/virginity/vcard21/base.rb +30 -0
  39. data/lib/virginity/vcard21/parser.rb +359 -0
  40. data/lib/virginity/vcard21/reader.rb +103 -0
  41. data/lib/virginity/vcard21/writer.rb +139 -0
  42. metadata +111 -0
@@ -0,0 +1,126 @@
1
+ require "virginity/dir_info/content_line"
2
+ require "virginity/vcard/cleaning"
3
+ require "virginity/vcard21/writer"
4
+ require "virginity/vcard/field_values"
5
+
6
+ module Virginity
7
+
8
+ # As a Vcard is a special type of DirectoryInformation, so is the Field a special type of ContentLine.
9
+ # A Field in Virginity is more content aware than a ContentLine. It knows what sort of content to expect for BDAY or ADR (respectively date or text, and a structured text value) and it provides the according methods that handle the differences in encoding.
10
+ class BaseField < ContentLine
11
+ include Vcard21::Writer
12
+ include FieldCleaning
13
+
14
+ def self.merger(left, right)
15
+ # ContentLine.merger returns a ContentLine, let's convert it to a field again.
16
+ Field.parse(super)
17
+ end
18
+
19
+ # is this field a preferred field?
20
+ TYPE = "TYPE"
21
+ PREF = /^pref$/i
22
+ def pref?
23
+ params(TYPE).any? { |p| p.value =~ PREF }
24
+ end
25
+
26
+ BEGIN_REGEX = /^BEGIN$/i
27
+ END_REGEX = /^END$/i
28
+
29
+ # Fields can be sorted
30
+ def <=>(other)
31
+ # BEGIN and END are special. and Virginity does not support nested vcards
32
+ return -1 if name =~ BEGIN_REGEX or other.name =~ END_REGEX
33
+ return 1 if name =~ END_REGEX or other.name =~ BEGIN_REGEX
34
+ unless (diff = (name <=> other.name)) == 0
35
+ diff
36
+ else
37
+ unless (diff = (pref? ? 0 : 1) <=> (other.pref? ? 0 : 1)) == 0
38
+ diff
39
+ else
40
+ to_s <=> other.to_s
41
+ end
42
+ end
43
+ end
44
+
45
+ # fields ARE content_lines but most often one should not deal with the value. In virtually all cases value contains some encoded text that is only useful when decoded as text or a text list.
46
+ def value
47
+ $stderr.puts "WARNING, you probably don't want to read value, if you do, please use #raw_value. Called from: #{caller.first}"
48
+ raw_value
49
+ end
50
+
51
+ alias_method :raw_value=, :value=
52
+ def value=(new_value)
53
+ $stderr.puts "WARNING, you probably don't want to write value, if you do, please use #raw_value=. Called from: #{caller.first}"
54
+ raw_value=(new_value)
55
+ end
56
+
57
+ def ==(other)
58
+ group == other.group &&
59
+ has_name?(other.name) &&
60
+ params == other.params &&
61
+ raw_value == other.raw_value
62
+ end
63
+
64
+
65
+ # a Hash to containing name => class
66
+ @@field_register = Hash.new(self)
67
+
68
+ def self.field_register
69
+ @@field_register
70
+ end
71
+
72
+ def self.named(name)
73
+ if registered? name
74
+ self[name].new(name)
75
+ else
76
+ new(name)
77
+ end
78
+ end
79
+
80
+ # redefine ContentLine#parse to gain a few nanoseconds and initialize the correct Field using the register
81
+ def self.parse(line)
82
+ if line.is_a? ContentLine
83
+ self[line.name].new(line.name, line.raw_value, line.params, line.group)
84
+ else
85
+ group, name, params, value = line_parts(line.to_s)
86
+ self[name].new(name, value, params, group, :no_deep_copy => true)
87
+ end
88
+ end
89
+
90
+ class << self
91
+ alias_method :from_line, :parse
92
+ end
93
+
94
+ # register a new field name with the class that should be used to represent it
95
+ def self.register_field(name, field_class)
96
+ raise "#{name} is already registered" if registered?(name)
97
+ @@field_register[name.to_s.upcase] = field_class
98
+ end
99
+
100
+ # when called from a Field-descendant, this method registers that class as the one handling fields with a name in names
101
+ def self.register_for(*names)
102
+ names.each { |name| register_field(name, self) }
103
+ end
104
+
105
+ # is name registered?
106
+ def self.registered?(name)
107
+ @@field_register.keys.include?(name.to_s.upcase)
108
+ end
109
+
110
+ def self.unregister(name)
111
+ @@field_register[name.to_s.upcase] = nil
112
+ end
113
+
114
+ # TODO: figure out if we really need upcase here
115
+ def self.[](name)
116
+ @@field_register[name.to_s.upcase]
117
+ end
118
+
119
+ # # a hash mapping field-names/types to Field-classes
120
+ def self.types
121
+ @@field_register.dup
122
+ end
123
+ end
124
+
125
+
126
+ end
@@ -0,0 +1,57 @@
1
+ require 'reactive_array'
2
+ require 'virginity/vcard/fields'
3
+
4
+ module Virginity
5
+
6
+ # methods to handle ALL categories fields in a Vcard. These work around the
7
+ # difficulties of working with multiple CATEGORIES-lines in a vCard by not
8
+ # trying to preserve any ordering or grouping.
9
+ module VcardCategories
10
+
11
+ def category_values
12
+ categories.map {|cat| cat.values.to_a }.flatten.uniq.sort
13
+ end
14
+
15
+ def add_category(c)
16
+ @tags = nil
17
+ self.push SeparatedField.new("CATEGORIES", EncodingDecoding::encode_text(c))
18
+ end
19
+
20
+ def remove_category(c)
21
+ @tags = nil
22
+ categories.each { |cat| cat.values.delete(c) }
23
+ end
24
+
25
+ def in_category?(c)
26
+ categories.any? { |cat| cat.values.include?(c) }
27
+ end
28
+
29
+
30
+ class TagArray < SerializingArray
31
+ def initialize(vcard)
32
+ @vcard = vcard
33
+ super(@vcard.category_values)
34
+ end
35
+
36
+ def rewrite!
37
+ @vcard.categories.each {|cat| @vcard.delete_field cat }
38
+ @array.each do |tag|
39
+ @vcard.add_category tag
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ def tags
46
+ @tags ||= TagArray.new(self)
47
+ end
48
+
49
+ def tags=(array_of_tags)
50
+ tags.replace(array_of_tags)
51
+ end
52
+
53
+ def tag(tag)
54
+ tags << tag
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,364 @@
1
+ module Virginity
2
+
3
+ module FieldCleaning
4
+
5
+ def clean!
6
+ clean_quoted_printable_encoding!
7
+ clean_base64!
8
+ clean_binary_data!
9
+ clean_charsets!
10
+ guess_latin!
11
+ remove_encoding_8bit!
12
+ remove_x_synthesis_ref_params!
13
+ remove_bom!
14
+ clean_types!
15
+ uniq_params!
16
+ end
17
+
18
+ # remove QUOTED-PRINTABLE-encoding
19
+ #
20
+ # According to vcard21.doc QUOTED-PRINTABLE cannot occur in structured text and separated text
21
+ # ... but from experience we know it does.
22
+ #
23
+ # Note: reencoding could fail because the characters are not encodable as text
24
+ LIST_NAMES = %w(CATEGORIES)
25
+ QUOTED_PRINTABLE = /^quoted-printable$/i
26
+ ENCODING = /^ENCODING$/i
27
+ def clean_quoted_printable_encoding!
28
+ return unless @params.any? {|p| p.key =~ ENCODING and p.value =~ QUOTED_PRINTABLE }
29
+ if @value.include?(";") # if the unencoded value contains ";" it's a list (or a structured value)
30
+ v = @value.split(";").map { |e| EncodingDecoding::decode_quoted_printable(e) }
31
+ @value = EncodingDecoding::encode_text_list(v, ";")
32
+ elsif LIST_NAMES.include?(@name) or @value.include?(",") # kludge
33
+ v = @value.split(",").map { |e| EncodingDecoding::decode_quoted_printable(e) }
34
+ @value = EncodingDecoding::encode_text_list(v, ",")
35
+ else
36
+ v = EncodingDecoding::decode_quoted_printable(@value)
37
+ @value = EncodingDecoding::encode_text(v)
38
+ end
39
+ @params.delete_if {|p| p.key =~ ENCODING and p.value =~ QUOTED_PRINTABLE }
40
+ self
41
+ end
42
+
43
+ # convert BASE64 to b
44
+ def clean_base64!
45
+ @params.each do |param|
46
+ next unless param.key =~ ENCODING and param.value =~ /^base64$/i
47
+ param.value = "b"
48
+ end
49
+ self
50
+ end
51
+
52
+ def clean_binary_data!
53
+ return unless @params.any? {|param| param.key =~ ENCODING and param.value =~ /^b$/i }
54
+ @value.gsub!(/\s/, '')
55
+ self
56
+ end
57
+
58
+ def remove_encoding_8bit! # since it's already implicitly encoded in 8 bits...
59
+ @params.delete_if {|param| param.key =~ ENCODING and param.value =~ /^8BIT$/ }
60
+ self
61
+ end
62
+
63
+ CHARSET = "CHARSET"
64
+ def clean_charsets!
65
+ return unless charset = @params.find { |param| param.key.casecmp(CHARSET) == 0 }
66
+ @value.encode!(Encoding::UTF_8, charset.value) unless charset.value == "UTF-8"
67
+ @value = @value.force_encoding(Encoding::UTF_8)
68
+ @params.delete charset
69
+ self
70
+ end
71
+
72
+ # Why do we have two boms? well duh, the string could be in either of those encodings!
73
+ BOM_UTF8 = [65279].pack('U')
74
+ BOM_BINARY = BOM_UTF8.dup.force_encoding(Encoding::BINARY)
75
+ def remove_bom!
76
+ if @value.encoding == Encoding::BINARY
77
+ @value.gsub!(BOM_BINARY, '')
78
+ else
79
+ # if it's not utf-8, it's callers fault.
80
+ @value.gsub!(BOM_UTF8, '') # remove the BOM
81
+ end
82
+ self
83
+ end
84
+
85
+ CASE_SENSITIVE_TYPES = /^(DOM|INTL|POSTAL|PARCEL|HOME|WORK|OTHER|PREF|VOICE|FAX|MSG|CELL|PAGER|BBS|MODEM|CAR|ISDN|VIDEO|AOL|APPLELINK|ATTMAIL|CIS|EWORLD|INTERNET|IBMMAIL|MCIMAIL|POWERSHARE|PRODIGY|TLX|X400|GIF|CGM|WMF|BMP|MET|PMB|DIB|PICT|TIFF|PDF|PS|JPEG|QTIME|MPEG|MPEG2|AVI|WAVE|AIFF|PCM|X509|PGP)$/i
86
+ TYPE = "TYPE"
87
+ def clean_types!
88
+ params(TYPE).each do |type|
89
+ type.value.upcase! if type.value =~ CASE_SENSITIVE_TYPES
90
+ end
91
+ self
92
+ end
93
+
94
+ X_SYNTHESIS_REF = /^X-Synthesis-Ref\d*$/i
95
+ def remove_x_synthesis_ref_params!
96
+ @params.delete_if {|p| p.key =~ X_SYNTHESIS_REF or p.value =~ X_SYNTHESIS_REF }
97
+ self
98
+ end
99
+
100
+ def uniq_params!
101
+ params.uniq!
102
+ self
103
+ end
104
+
105
+ def guess_latin!
106
+ return if @value.valid_encoding?
107
+ @value.encode!(Encoding::UTF_8, "ISO-8859-1")
108
+ end
109
+ end
110
+
111
+
112
+ module VcardCleaning
113
+
114
+ # run almost every clean method on whole vcards and on separate fields (FieldCleaning), in a correct order
115
+ def clean!
116
+ clean_version!
117
+ remove_x_abuids!
118
+ remove_x_irmc_luids!
119
+ rstrip_text_fields!
120
+ # strip_structured_fields! # mh, better not do this here. It's too much
121
+ remove_empty_fields!
122
+ fields.each { |field| field.clean! }
123
+ clean_categories!
124
+ unpack_nicknames!
125
+ clean_orgs!
126
+ max_one_name!
127
+ clean_name!
128
+ clean_dates!
129
+ clean_adrs!
130
+ convert_xabadrs_to_param!
131
+ convert_xablabels_to_param!
132
+ remove_duplicate_lines!
133
+ remove_singleton_groups!
134
+ clean_same_value_fields!
135
+ remove_subset_addresses!
136
+ reset_empty_formatted_name!
137
+ self
138
+ end
139
+ alias_method :super_clean!, :clean!
140
+
141
+ # make sure there is exactly one version-field that says "3.0"
142
+ # and do that in a smart way so the vcard is not changed if it's not nescessary
143
+ VERSION30 = "VERSION:3.0"
144
+ VERSION = "VERSION"
145
+ def clean_version!
146
+ unless (self/VERSION30).size == 1
147
+ lines_with_name(VERSION).each {|f| delete_field(f) }
148
+ self << VERSION30
149
+ end
150
+ self
151
+ end
152
+
153
+ X_ABUID = "X-ABUID"
154
+ # OS-X is not sending those anymore, but existing contact can still contain those fields
155
+ def remove_x_abuids!
156
+ lines_with_name(X_ABUID).each {|f| delete_field(f) }
157
+ self
158
+ end
159
+
160
+ # Sony-Ericsson phones send those. They don't mean anything for us. They are meant for Windows software that syncs the desktop with a phone afaik.
161
+ X_IRMC_LUID = "X-IRMC-LUID"
162
+ def remove_x_irmc_luids!
163
+ lines_with_name(X_IRMC_LUID).each {|f| delete_field(f) }
164
+ self
165
+ end
166
+
167
+ # why? to remove trailing semicolons like in: "ORG:foo;bar;"
168
+ def clean_orgs!
169
+ organizations.each { |org| org.reencode!(:variable_number_of_fields => true) }
170
+ self
171
+ end
172
+
173
+ # since we could have received a vcard with too many semicolons
174
+ NAME = 'N'
175
+ def max_one_name!
176
+ n_fields = lines_with_name(NAME)
177
+ return self unless n_fields.size > 1
178
+ # remove all N fields except the biggest
179
+ n_fields.delete(n_fields.max_by { |f| f.raw_value.size })
180
+ delete(*n_fields)
181
+ self
182
+ end
183
+
184
+ def clean_name!
185
+ lines_with_name(NAME).each { |n| n.reencode! }
186
+ self
187
+ end
188
+
189
+ # there's no use in keeping fields without a value
190
+ def remove_empty_fields!
191
+ lines.delete_if do |x|
192
+ x.raw_value.strip.empty? or (x.respond_to? :values and x.values.join.empty?)
193
+ end
194
+ self
195
+ end
196
+
197
+ # make one field containing all of values of fields with the same name
198
+ def clean_multivalue_fields!(name)
199
+ fields = lines_with_name(name)
200
+ while fields.size > 1
201
+ last = fields.pop
202
+ fields.first.values.concat( last.values.to_a )
203
+ delete_field(last)
204
+ end
205
+ self
206
+ end
207
+
208
+ # concatenate all CATEGORIES-lines, make categories unique and sorted
209
+ CATEGORIES = "CATEGORIES"
210
+ def clean_categories!
211
+ clean_multivalue_fields!(CATEGORIES) # concat
212
+ if categories = lines_with_name(CATEGORIES).first
213
+ categories.values = categories.values.map { |c| c.strip }.sort.uniq
214
+ end
215
+ self
216
+ end
217
+
218
+ # after unpacking there SHOULD be one field for each value
219
+ # the order MUST NOT change.
220
+ def unpack_field!(field)
221
+ return if field.values.size == 1
222
+ field.unpacked.each {|f| add_field(f) }
223
+ delete_field(field)
224
+ end
225
+
226
+ def unpack_list!(list_name)
227
+ lines_with_name(list_name).each { |field| unpack_field!(field) }
228
+ remove_empty_fields!
229
+ self
230
+ end
231
+
232
+ # after unpacking there should be one categories lines per category
233
+ # the order should not change, but that doesn't actually matter for categories
234
+ def unpack_categories!
235
+ unpack_list!(CATEGORIES)
236
+ end
237
+
238
+ # after unpacking there should be one nickname value per NICKNAME
239
+ # the order should not change.
240
+ NICKNAME = "NICKNAME"
241
+ def unpack_nicknames!
242
+ unpack_list!(NICKNAME)
243
+ end
244
+
245
+ # if there's only one field in a certain group that group can be reset
246
+ def remove_singleton_groups!
247
+ groups = fields.map {|f| f.group }.compact!
248
+ fields.each do |field|
249
+ next if field.group.nil?
250
+ field.group = nil if groups.select { |g| g == field.group }.size == 1
251
+ end
252
+ self
253
+ end
254
+
255
+ def convert_custom_osx_field_to_param!(fields)
256
+ fields.each do |custom|
257
+ unless custom.group.nil?
258
+ same_group = where(:group => custom.group) - [custom]
259
+ same_group.each { |field| field.params << custom.to_param }
260
+ end
261
+ delete_field(custom)
262
+ end
263
+ self
264
+ end
265
+
266
+ def convert_xabadrs_to_param!
267
+ convert_custom_osx_field_to_param! lines_with_name("X-ABADR")
268
+ end
269
+
270
+ def convert_xablabels_to_param!
271
+ convert_custom_osx_field_to_param! lines_with_name("X-ABLabel")
272
+ end
273
+
274
+ def remove_duplicate_lines!
275
+ fields.uniq!
276
+ self
277
+ end
278
+
279
+ # merge fields with the same value (collect all params in the remaining field)
280
+ def clean_same_value_fields!
281
+ fields.each do |upper|
282
+ next if Vcard::SINGLETON_FIELDS.include?(upper.name)
283
+ fields.each do |lower|
284
+ next if lower.object_id == upper.object_id # nescessary
285
+ next unless lower.name == upper.name # optimisation
286
+ next unless lower.raw_value == upper.raw_value # optimisation
287
+ begin
288
+ # puts "merging #{upper} and #{lower}"
289
+ upper.merge_with!(lower)
290
+ delete_field(lower)
291
+ # puts "merged: #{upper}"
292
+ rescue # nescessary
293
+ end
294
+ end
295
+ end
296
+ self
297
+ end
298
+
299
+ # removes fields that are a subset of another existing field
300
+ #
301
+ # In the following example, field two is a subset of field one.
302
+ # one = Field.new("ADR;TYPE=HOME:;;Bickerson Street 4;Dudley Town;;6215 AX;NeverNeverland")
303
+ # two = Field.new("ADR;TYPE=HOME:;;Bickerson Street 4;Dudley Town;;;")
304
+ # two.subset_of?(one) ==> true
305
+ def remove_subsets_of_structured_fields!(fields)
306
+ fields = fields.dup
307
+ while field = fields.pop
308
+ delete_field(field) if fields.any? {|other| field.subset_of?(other) }
309
+ end
310
+ self
311
+ end
312
+
313
+ def remove_subset_addresses!
314
+ remove_subsets_of_structured_fields!(addresses)
315
+ end
316
+
317
+ def reset_empty_formatted_name!
318
+ name.reset_formatted!
319
+ self
320
+ end
321
+
322
+ def remove_extra_photos!(p = photos)
323
+ p.shift
324
+ p.each { |line| delete_field(line) }
325
+ self
326
+ end
327
+
328
+ def remove_extra_logos!
329
+ remove_extra_photos!(logos)
330
+ end
331
+
332
+ RSTRIPPABLE_FIELDS = /^(EMAIL|FN|IMPP|NOTE|TEL|URL)$/i
333
+ # remove right hand whitespace from the values of all RSTRIPPABLE_FIELDS
334
+ def rstrip_text_fields!
335
+ fields.each do |field|
336
+ field.raw_value.rstrip! if field.name =~ RSTRIPPABLE_FIELDS
337
+ end
338
+ self
339
+ end
340
+
341
+ # remove whitespace around parts of a name
342
+ def strip_structured_fields!
343
+ (lines_with_name(NAME) + organizations + addresses).each do |field|
344
+ field.components.each { |component| field.send(component.to_s+'=', field.send(component).strip) }
345
+ end
346
+ end
347
+
348
+ def clean_dates!
349
+ (birthdays + anniversaries + dates).each do |day|
350
+ begin
351
+ day.date = day.date.to_s # normalize the date format
352
+ rescue => e
353
+ nil
354
+ end
355
+ end
356
+ end
357
+
358
+ def clean_adrs!
359
+ lines_with_name("ADR").each do |adr|
360
+ adr.reencode! if EncodingDecoding::decode_text_list(adr.raw_value, ";").size != 7
361
+ end
362
+ end
363
+ end
364
+ end