virginity 0.3.31

Sign up to get free protection for your applications and to get access to all the features.
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