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,22 @@
1
+ require 'virginity/vcard/base_field'
2
+
3
+ module Virginity
4
+
5
+ # Basic field, if we don't know anything about it, we assume it can at least handle text encoding
6
+ class Field < BaseField
7
+ include FieldValues::Text
8
+
9
+ field_register.default = self
10
+ end
11
+
12
+
13
+ # monkey patch ContentLine to make a #to_field method
14
+ class ContentLine
15
+ # convert to a vcard-field (see Field)
16
+ def to_field
17
+ Field.parse(self)
18
+ end
19
+ end
20
+
21
+
22
+ end
@@ -0,0 +1,93 @@
1
+ module Virginity
2
+
3
+ module Params
4
+ module Type
5
+
6
+
7
+ class TypeArray < SerializingArray
8
+ def initialize(field)
9
+ @field = field
10
+ super(@field.params("TYPE").map { |p| p.value }.uniq)
11
+ end
12
+
13
+ # def reload!
14
+ # @array = Array.new(@field.params("TYPE").map { |p| p.value }.uniq)
15
+ # self
16
+ # end
17
+
18
+ def rewrite!
19
+ @field.params("TYPE").each {|t| @field.params.delete t }
20
+ @array.each do |type|
21
+ @field.params << Param.new('TYPE', type)
22
+ end
23
+ end
24
+
25
+ # Locations are a subset of all the TYPE-params
26
+ LOCATIONS = { "CELL" => "Mobile", "HOME" => "Home", "OTHER" => "Other", "WORK" => "Work" }
27
+ def locations
28
+ @array.select { |t| LOCATIONS.keys.include?(t)}
29
+ end
30
+
31
+ def locations=(locs)
32
+ locations.each {|l| delete(l) }
33
+ locs.each { |l| self << l }
34
+ end
35
+ end
36
+
37
+
38
+ def types
39
+ TypeArray.new(self)
40
+ end
41
+
42
+ def types=(array)
43
+ types.replace(array)
44
+ end
45
+
46
+ def type=(str)
47
+ self.types = str.split(/ /)
48
+ end
49
+
50
+ def add_type(thing)
51
+ t = types
52
+ t << thing unless t.include? thing
53
+ end
54
+
55
+ def remove_type(thing)
56
+ types.delete(thing)
57
+ end
58
+
59
+ # =============================
60
+ # Preferred
61
+ def preferred?
62
+ types.include? 'PREF'
63
+ end
64
+
65
+ def preferred=(val)
66
+ if val
67
+ add_type 'PREF'
68
+ else
69
+ remove_type 'PREF'
70
+ end
71
+ end
72
+
73
+ # =============================
74
+ # Location handling:
75
+ def locations
76
+ types.locations
77
+ end
78
+
79
+ def locations=(array)
80
+ types.locations = array
81
+ end
82
+
83
+ def location
84
+ locations.join(" ")
85
+ end
86
+
87
+ def location=(str)
88
+ self.locations = str.split(/ /)
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ require 'virginity/vcard/field_values/binary.rb'
2
+ require 'virginity/vcard/field_values/boolean.rb'
3
+ require 'virginity/vcard/field_values/case_insensitive_value.rb'
4
+ require 'virginity/vcard/field_values/date.rb'
5
+ require 'virginity/vcard/field_values/integer.rb'
6
+ require 'virginity/vcard/field_values/separated_text.rb'
7
+ require 'virginity/vcard/field_values/structured_text.rb'
8
+ require 'virginity/vcard/field_values/optional_structured_text.rb'
9
+ require 'virginity/vcard/field_values/text.rb'
10
+ require 'virginity/vcard/field_values/uri.rb'
@@ -0,0 +1,22 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Binary
5
+ def binary
6
+ Base64.decode64(@value)
7
+ end
8
+
9
+ def binary=(s)
10
+ @params.delete_if { |p| p.key == "ENCODING" }
11
+ @params << Param.new("ENCODING", "b")
12
+ b64 = Base64.encode64(s)
13
+ b64.delete!("\n") # can return nil... bah, but probably faster than #delete without an exclamation mark
14
+ @value = b64
15
+ end
16
+
17
+ def sha1
18
+ Digest::SHA1.hexdigest(@value)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Boolean
5
+ def boolean
6
+ (@value.downcase == "true")
7
+ end
8
+
9
+ def boolean=(b)
10
+ @value = b ? "true" : "false"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module CaseInsensitiveValue
5
+ def ==(other)
6
+ group == other.group &&
7
+ has_name?(other.name) &&
8
+ params == other.params &&
9
+ (raw_value.casecmp(other.raw_value) == 0)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+
3
+ module Virginity
4
+ module FieldValues
5
+
6
+ module DateValue
7
+ def date
8
+ Date.parse(text)
9
+ end
10
+
11
+ def date=(d)
12
+ self.text = d.to_s
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Integer
5
+ def integer
6
+ @value.to_i
7
+ end
8
+
9
+ def integer=(i)
10
+ @value = @integer.to_s
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ class OptionalStructuredText < StructuredText
5
+ def self.define(components)
6
+ m = super
7
+
8
+ m.module_eval <<-RUBY, __FILE__, __LINE__+1
9
+ def reencode!(options = {})
10
+ v = values
11
+
12
+ v.pop while v.last.empty?
13
+
14
+ @value = EncodingDecoding::encode_structured_text(v)
15
+ end
16
+ RUBY
17
+
18
+ components.each_with_index do |component, idx|
19
+ m.module_eval <<-RUBY, __FILE__, __LINE__+1
20
+ def #{component}=(new_value)
21
+ structure = values
22
+ structure[#{idx}] = new_value.to_s
23
+
24
+ structure.pop while structure.size > 0 && (structure.last.nil? || structure.last.empty?)
25
+
26
+ @value = EncodingDecoding::encode_structured_text(structure)
27
+ end
28
+ RUBY
29
+ end
30
+
31
+ m
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ require 'reactive_array'
2
+ require 'digest/sha1'
3
+
4
+ module Virginity
5
+ module FieldValues
6
+
7
+ module SeparatedText
8
+
9
+
10
+ class TextList < SerializingArray
11
+ def initialize(field)
12
+ @field = field # a reference to the original Field
13
+ super(EncodingDecoding::decode_text_list(@field.raw_value))
14
+ save_sha1!
15
+ end
16
+
17
+ def sha1
18
+ Digest::SHA1.hexdigest(@field.raw_value)
19
+ end
20
+
21
+ def save_sha1!
22
+ @sha1 = sha1
23
+ end
24
+
25
+ def needs_refresh?
26
+ @sha1 != sha1
27
+ end
28
+
29
+ def rewrite!
30
+ @array.delete_if {|v| v.empty? }
31
+ @field.raw_value = EncodingDecoding::encode_text_list(@array)
32
+ save_sha1!
33
+ end
34
+ end
35
+
36
+
37
+ def values
38
+ if (@textlist.needs_refresh? rescue true)
39
+ @textlist = TextList.new(self)
40
+ else
41
+ @textlist
42
+ end
43
+ end
44
+
45
+ def values=(a)
46
+ values.replace(a)
47
+ end
48
+
49
+ def reencode!
50
+ values.rewrite!
51
+ end
52
+
53
+ def subset_of?(other)
54
+ values.all? { |v| other.values.include? v }
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,71 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ class StructuredText
5
+ def self.define(components)
6
+ m = Module.new
7
+ m.const_set("COMPONENTS", components)
8
+ m.module_eval <<-RUBY
9
+ def components
10
+ COMPONENTS
11
+ end
12
+
13
+ def values
14
+ EncodingDecoding::decode_structured_text(@value, COMPONENTS.size)
15
+ end
16
+
17
+ def empty?
18
+ values.all? {|v| v.empty? }
19
+ end
20
+
21
+ def reencode!(options={})
22
+ v = values
23
+ unless options[:variable_number_of_fields]
24
+ v.pop while v.size > components.size
25
+ v.push(nil) while v.size < components.size
26
+ else
27
+ v.pop while v.last.empty?
28
+ end
29
+ @value = EncodingDecoding::encode_structured_text(v)
30
+ end
31
+
32
+ def [](component)
33
+ raise "which component? \#\{component\}?? I only know \#\{components.inspect\}" unless components.include? component.to_sym
34
+ send("\#\{component\}".to_sym)
35
+ end
36
+
37
+ def []=(component, new_value)
38
+ raise "which component? \#\{component\}?? I only know \#\{components.inspect\}" unless components.include? component.to_sym
39
+ send("\#\{component\}=", new_value)
40
+ end
41
+ RUBY
42
+
43
+ components.each_with_index do |component, idx|
44
+ m.module_eval <<-RUBY
45
+ def #{component}
46
+ values[#{idx}]
47
+ end
48
+
49
+ def #{component}=(new_value)
50
+ structure = values
51
+ structure[#{idx}] = new_value.to_s
52
+ @value = EncodingDecoding::encode_structured_text(structure)
53
+ end
54
+
55
+ def subset_of?(other_field)
56
+ components.all? do |component|
57
+ send(component).empty? or send(component) == other_field.send(component)
58
+ end
59
+ end
60
+
61
+ def superset_of?(other_field)
62
+ other_field.subset_of?(self)
63
+ end
64
+ RUBY
65
+ end
66
+ m
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Text
5
+ def text
6
+ EncodingDecoding::decode_text(@value)
7
+ end
8
+
9
+ def text=(s)
10
+ @params.delete_if { |p| p.key == "ENCODING" }
11
+ @value = EncodingDecoding::encode_text(s)
12
+ end
13
+
14
+ def reencode!
15
+ self.text = text
16
+ end
17
+
18
+ def value_to_xml
19
+ xml_element("text", text.strip)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ # needs module Text
5
+ module Uri
6
+ def uri
7
+ URI::parse(text)
8
+ end
9
+
10
+ def uri=(new_uri)
11
+ self.text = new_uri.to_s
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,284 @@
1
+ require "virginity/vcard/field"
2
+ require "virginity/vcard/field/params"
3
+ require "base64"
4
+
5
+ module Virginity
6
+
7
+ # BEGIN or END
8
+ #
9
+ # We don't do much with them
10
+ class BeginEnd < BaseField
11
+ include FieldValues::CaseInsensitiveValue
12
+ # value MUST be "VCARD"
13
+ register_for :BEGIN, :END
14
+ end
15
+
16
+
17
+ class Profile < BaseField
18
+ register_for :PROFILE
19
+ end
20
+
21
+
22
+ # Text fields, see FieldValues::Text for safe encoding/decoding methods
23
+ class TextField < BaseField
24
+ include FieldValues::Text
25
+ register_for :CLASS, :FN, :LABEL, :MAILER, :NOTE, :PRODID, :ROLE, 'SORT-STRING', :TITLE, :UID, :VERSION, :'X-PHONETIC-LAST-NAME', :'X-PHONETIC-FIRST-NAME'
26
+ end
27
+
28
+
29
+ # A BDAY in a vCard can be a free form text-value or a date. This class provides methods for both cases.
30
+ class Birthday < BaseField
31
+ include FieldValues::Text
32
+ include FieldValues::DateValue
33
+ register_for :BDAY
34
+ end
35
+
36
+
37
+ class Anniversary < BaseField
38
+ include FieldValues::Text
39
+ include FieldValues::DateValue
40
+ register_for "X-ANNIVERSARY"
41
+ end
42
+
43
+
44
+ # Instant messaging fields are defined in rfc 2427. They have a scheme and an address. If an IMPP is not 'clean' one can still get/set the value by using the methods from FieldValues::Text and FieldValues::Uri
45
+ class Impp < BaseField
46
+ include FieldValues::Text
47
+ include FieldValues::Uri
48
+ include Params::Type
49
+ register_for :IMPP
50
+ # include LocationHandling
51
+ # include PurposeHandling
52
+ # include PreferenceHandling
53
+ # PURPOSE_SETS = { "personal" => "Personal", "business" => "Business" }
54
+ # HUMAN_DESCRIPTION = "instant messaging"
55
+
56
+ # def initialize(content_line=ContentLine.new("IMPP:"))
57
+ # super
58
+ # @cline.name = "IMPP"
59
+ # end
60
+
61
+ def scheme
62
+ # every piece of text before the first colon, if there is a colon present
63
+ text.match(/^(.*?):/) # Note the use of "*?" for non greedy matching of the colon!
64
+ $1 || ""
65
+ end
66
+
67
+ def address
68
+ # everything after the first colon if that colon is present, otherwise the whole text
69
+ text.match(/^.*?:(.*)$|^(.*)$/)
70
+ $1 || $2
71
+ end
72
+
73
+ def scheme=(s)
74
+ self.text = "#{s}:#{address}"
75
+ end
76
+
77
+ def address=(s)
78
+ self.text = "#{self.scheme}:#{s}"
79
+ end
80
+
81
+ def raw_value
82
+ scheme.empty? && address.empty? ? "" : @value
83
+ end
84
+ end
85
+
86
+
87
+ # OS X AddressBook does not use the IMPP fields. Instead Apple chose to use their own proprietary format. This format is handles by CustomImField
88
+ class CustomImField < BaseField
89
+ include FieldValues::Text
90
+ PROTOCOL_TRANSLATION_TABLE = {
91
+ :aim => "X-AIM",
92
+ :msn => "X-MSN",
93
+ :ymsgr => "X-YAHOO",
94
+ :skype => "X-SKYPE",
95
+ :qq => "X-QQ",
96
+ :gtalk => "X-GOOGLE TALK",
97
+ :icq => "X-ICQ",
98
+ :xmpp => "X-JABBER",
99
+ }
100
+ PROTOCOL_TRANSLATION_TABLE.values.each { |protocol| register_for protocol }
101
+ PROTOCOL_TRANSLATION_INVERSE_TABLE = Hash[PROTOCOL_TRANSLATION_TABLE.map { |k, v| [v, k] }]
102
+
103
+ # convert a standard IMPP field to a Custom IM field. Only schemes defined in PROTOCOL_TRANSLATION_TABLE are supported.
104
+ def self.from_impp(impp)
105
+ nm = PROTOCOL_TRANSLATION_TABLE[impp.scheme.to_sym]
106
+ raise "unknown scheme #{impp.scheme} for #{impp.text.inspect}" if nm.nil?
107
+ x = Field.parse("#{nm}:")
108
+ x.params = Param::deep_copy(impp.params)
109
+ x.text = impp.address
110
+ x
111
+ end
112
+
113
+ def protocol
114
+ PROTOCOL_TRANSLATION_INVERSE_TABLE[name]
115
+ end
116
+
117
+ # convert to a standard IMPP field
118
+ def to_impp
119
+ impp = Field.parse("IMPP:")
120
+ impp.params = Param::deep_copy(@params)
121
+ impp.text = "#{protocol}:#{text}"
122
+ impp
123
+ end
124
+ end
125
+
126
+
127
+ # handle ORG fields.
128
+ #
129
+ # An Org has an orgname, a unit1 and a unit2. We stick to this simple
130
+ # definition for now since it is widely used. The vCard specs seem to
131
+ # specify an unlimited amount of units
132
+ class Org < BaseField
133
+ register_for :ORG
134
+ include FieldValues::StructuredText.define([:orgname, :unit1, :unit2])
135
+
136
+ def shortened
137
+ [orgname, unit1, unit2].join(" ").strip
138
+ end
139
+
140
+ # def ==(other)
141
+ # super ||
142
+ # has_name?(other.name) &&
143
+ # values == other.try(:values) && self.class === other && self.group == other.group
144
+ # end
145
+ end
146
+
147
+
148
+ # SeparatedField handles an array of text values
149
+ class SeparatedField < BaseField
150
+ include FieldValues::SeparatedText
151
+ register_for :CATEGORIES, :NICKNAME
152
+
153
+ def unpacked
154
+ values.map do |text|
155
+ self.class.new(name, EncodingDecoding::encode_text_list([text]), params, group)
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ # telephone number
162
+ #
163
+ # provides the easy getter/setter #number and generators for random numbers
164
+ class Tel < BaseField
165
+ include FieldValues::Text
166
+ include Params::Type
167
+ # include Params::Type::Preference
168
+ register_for :TEL
169
+ COMPARISON_REGEX = /[^\+\*\#\/\,\w]/u # anything that is NOT a plus, a star, a hash, a slash, a comma, or 0-9/a-z/A-Z is insignificant
170
+
171
+ alias_method :number, :text
172
+ alias_method :number=, :text=
173
+
174
+ def self.random_number(length = 10)
175
+ # (1..length).map { rand(10).to_s }.join
176
+ # only 555-0100 through 555-0199 are now specifically reserved for fictional use
177
+ random_part = 100 + (rand(99) + 1)
178
+ "555-0#{random_part}"
179
+ end
180
+
181
+ def self.at_random
182
+ new("TEL", random_number)
183
+ end
184
+
185
+ def significant_chars
186
+ # it's a phone number (but people could store alphanumeric stuff here) so I opt to remove all dashes and spaces
187
+ # I leave the plus since we don't know with what prefix to replace it with, see: http://en.wikipedia.org/wiki/List_of_international_call_prefixes
188
+ number.gsub(COMPARISON_REGEX, "")
189
+ end
190
+ alias_method :normalized_number, :significant_chars
191
+
192
+ end
193
+
194
+
195
+ class Email < BaseField
196
+ include FieldValues::Text
197
+ include Params::Type
198
+ register_for :EMAIL
199
+ alias_method :address, :text
200
+ alias_method :address=, :text=
201
+ end
202
+
203
+
204
+ class Adr < BaseField
205
+ include FieldValues::StructuredText.define([:pobox, :extended, :street, :locality, :region, :postal_code, :country])
206
+ include Params::Type
207
+ register_for :ADR
208
+ NAME = "ADR"
209
+ end
210
+
211
+
212
+ class Url < BaseField
213
+ include FieldValues::Text
214
+ include FieldValues::Uri
215
+ # URLs officialy don't have TYPE params,
216
+ # but everyone and their dog thinks they do.
217
+ # So we decided to support them too.
218
+ # Conclusion: no-one reads the RFC
219
+ include Params::Type
220
+ register_for :URL
221
+ end
222
+
223
+
224
+ class Name < BaseField
225
+ PARTS = %w(family given additional prefix suffix)
226
+ include FieldValues::StructuredText.define(PARTS)
227
+ register_for :N
228
+ end
229
+
230
+
231
+ class Photo < BaseField
232
+ include FieldValues::Text
233
+ include FieldValues::Binary
234
+ include FieldValues::Uri
235
+ register_for :LOGO, :PHOTO
236
+
237
+ def is_binary?
238
+ @params.detect {|p| p.key == "ENCODING" and p.value =~ /B/i }
239
+ end
240
+
241
+ def is_url?
242
+ !is_binary?
243
+ end
244
+ end
245
+
246
+
247
+ class Sound < BaseField
248
+ include FieldValues::Binary
249
+ include FieldValues::Text
250
+ include FieldValues::Uri
251
+ register_for :SOUND
252
+ end
253
+
254
+
255
+ class Key < BaseField
256
+ include FieldValues::Binary
257
+ include FieldValues::Text
258
+ register_for :KEY
259
+ end
260
+
261
+ class Gender < BaseField
262
+ register_for :GENDER
263
+ PARTS = %w(sex identity)
264
+ include FieldValues::OptionalStructuredText.define(PARTS)
265
+
266
+ {male: 'M', female: 'F', other: 'O', none: 'N', unknown: 'U'}.each do |name, letter|
267
+ class_eval <<-RUBY
268
+ def #{name}?
269
+ sex == "#{letter}"
270
+ end
271
+
272
+ def #{name}=(v)
273
+ raise "Only true is acceptable" unless v
274
+ self.sex = "#{letter}"
275
+ end
276
+ RUBY
277
+ end
278
+
279
+ def neither?
280
+ sex !~ /^[MFNOU]$/
281
+ end
282
+ end
283
+
284
+ end