diaspora_federation 0.1.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +34 -0
  3. data/lib/diaspora_federation/discovery/discovery.rb +2 -2
  4. data/lib/diaspora_federation/discovery/h_card.rb +2 -10
  5. data/lib/diaspora_federation/discovery/host_meta.rb +1 -1
  6. data/lib/diaspora_federation/discovery/web_finger.rb +47 -87
  7. data/lib/diaspora_federation/discovery/xrd_document.rb +20 -7
  8. data/lib/diaspora_federation/entities/account_migration.rb +74 -0
  9. data/lib/diaspora_federation/entities/comment.rb +0 -4
  10. data/lib/diaspora_federation/entities/event_participation.rb +0 -8
  11. data/lib/diaspora_federation/entities/like.rb +6 -10
  12. data/lib/diaspora_federation/entities/message.rb +14 -49
  13. data/lib/diaspora_federation/entities/participation.rb +23 -13
  14. data/lib/diaspora_federation/entities/poll_participation.rb +0 -4
  15. data/lib/diaspora_federation/entities/profile.rb +5 -0
  16. data/lib/diaspora_federation/entities/related_entity.rb +17 -0
  17. data/lib/diaspora_federation/entities/relayable.rb +82 -113
  18. data/lib/diaspora_federation/entities/relayable_retraction.rb +4 -43
  19. data/lib/diaspora_federation/entities/request.rb +4 -12
  20. data/lib/diaspora_federation/entities/reshare.rb +11 -7
  21. data/lib/diaspora_federation/entities/retraction.rb +4 -5
  22. data/lib/diaspora_federation/entities/signable.rb +54 -0
  23. data/lib/diaspora_federation/entities/signed_retraction.rb +4 -44
  24. data/lib/diaspora_federation/entities.rb +2 -0
  25. data/lib/diaspora_federation/entity.rb +74 -96
  26. data/lib/diaspora_federation/federation/fetcher.rb +1 -1
  27. data/lib/diaspora_federation/federation/receiver.rb +1 -1
  28. data/lib/diaspora_federation/federation/sender/hydra_wrapper.rb +37 -12
  29. data/lib/diaspora_federation/federation/sender.rb +2 -2
  30. data/lib/diaspora_federation/parsers/base_parser.rb +61 -0
  31. data/lib/diaspora_federation/parsers/json_parser.rb +60 -0
  32. data/lib/diaspora_federation/parsers/relayable_json_parser.rb +25 -0
  33. data/lib/diaspora_federation/parsers/relayable_xml_parser.rb +22 -0
  34. data/lib/diaspora_federation/parsers/xml_parser.rb +84 -0
  35. data/lib/diaspora_federation/parsers.rb +13 -0
  36. data/lib/diaspora_federation/properties_dsl.rb +1 -1
  37. data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +1 -1
  38. data/lib/diaspora_federation/salmon/encrypted_slap.rb +2 -99
  39. data/lib/diaspora_federation/salmon/magic_envelope.rb +3 -19
  40. data/lib/diaspora_federation/salmon/slap.rb +1 -42
  41. data/lib/diaspora_federation/salmon/xml_payload.rb +0 -19
  42. data/lib/diaspora_federation/schemas/federation_entities.json +379 -0
  43. data/lib/diaspora_federation/validators/account_deletion_validator.rb +1 -1
  44. data/lib/diaspora_federation/validators/account_migration_validator.rb +12 -0
  45. data/lib/diaspora_federation/validators/contact_validator.rb +2 -2
  46. data/lib/diaspora_federation/validators/conversation_validator.rb +1 -1
  47. data/lib/diaspora_federation/validators/event_validator.rb +1 -1
  48. data/lib/diaspora_federation/validators/h_card_validator.rb +2 -5
  49. data/lib/diaspora_federation/validators/message_validator.rb +1 -1
  50. data/lib/diaspora_federation/validators/participation_validator.rb +1 -1
  51. data/lib/diaspora_federation/validators/person_validator.rb +2 -2
  52. data/lib/diaspora_federation/validators/photo_validator.rb +1 -1
  53. data/lib/diaspora_federation/validators/profile_validator.rb +1 -1
  54. data/lib/diaspora_federation/validators/related_entity_validator.rb +1 -1
  55. data/lib/diaspora_federation/validators/relayable_validator.rb +1 -1
  56. data/lib/diaspora_federation/validators/reshare_validator.rb +2 -2
  57. data/lib/diaspora_federation/validators/retraction_validator.rb +1 -1
  58. data/lib/diaspora_federation/validators/rules/boolean.rb +2 -2
  59. data/lib/diaspora_federation/validators/status_message_validator.rb +1 -1
  60. data/lib/diaspora_federation/validators/web_finger_validator.rb +5 -6
  61. data/lib/diaspora_federation/validators.rb +1 -5
  62. data/lib/diaspora_federation/version.rb +1 -1
  63. data/lib/diaspora_federation.rb +6 -2
  64. metadata +14 -11
  65. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +0 -15
  66. data/lib/diaspora_federation/validators/request_validator.rb +0 -12
  67. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +0 -15
  68. data/lib/tasks/build.rake +0 -14
  69. data/lib/tasks/diaspora_federation_tasks.rake +0 -4
  70. data/lib/tasks/rails4.rake +0 -15
  71. data/lib/tasks/tests.rake +0 -18
@@ -12,6 +12,7 @@ require "diaspora_federation/entities/related_entity"
12
12
 
13
13
  # abstract types
14
14
  require "diaspora_federation/entities/post"
15
+ require "diaspora_federation/entities/signable"
15
16
  require "diaspora_federation/entities/relayable"
16
17
 
17
18
  # types
@@ -19,6 +20,7 @@ require "diaspora_federation/entities/profile"
19
20
  require "diaspora_federation/entities/person"
20
21
  require "diaspora_federation/entities/contact"
21
22
  require "diaspora_federation/entities/account_deletion"
23
+ require "diaspora_federation/entities/account_migration"
22
24
 
23
25
  require "diaspora_federation/entities/participation"
24
26
  require "diaspora_federation/entities/like"
@@ -90,7 +90,6 @@ module DiasporaFederation
90
90
  # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}s
91
91
  #
92
92
  # @see Nokogiri::XML::Node.to_xml
93
- # @see XmlPayload#pack
94
93
  #
95
94
  # @return [Nokogiri::XML::Element] root element containing properties as child elements
96
95
  def to_xml
@@ -109,10 +108,21 @@ module DiasporaFederation
109
108
  # @param [Nokogiri::XML::Element] root_node xml nodes
110
109
  # @return [Entity] instance
111
110
  def self.from_xml(root_node)
112
- raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element)
113
- raise InvalidRootNode, "'#{root_node.name}' can't be parsed by #{name}" unless root_node.name == entity_name
111
+ from_hash(*xml_parser_class.new(self).parse(root_node))
112
+ end
113
+
114
+ private_class_method def self.xml_parser_class
115
+ DiasporaFederation::Parsers::XmlParser
116
+ end
114
117
 
115
- populate_entity(root_node)
118
+ # Creates an instance of self by parsing a hash in the format of JSON serialized object (which usually means
119
+ # data from a parsed JSON input).
120
+ def self.from_json(json_hash)
121
+ from_hash(*json_parser_class.new(self).parse(json_hash))
122
+ end
123
+
124
+ private_class_method def self.json_parser_class
125
+ DiasporaFederation::Parsers::JsonParser
116
126
  end
117
127
 
118
128
  # Makes an underscored, lowercase form of the class name
@@ -121,7 +131,7 @@ module DiasporaFederation
121
131
  #
122
132
  # @return [String] entity name
123
133
  def self.entity_name
124
- name.rpartition("::").last.tap do |word|
134
+ class_name.tap do |word|
125
135
  word.gsub!(/(.)([A-Z])/, '\1_\2')
126
136
  word.downcase!
127
137
  end
@@ -144,16 +154,52 @@ module DiasporaFederation
144
154
  Entities.const_get(class_name)
145
155
  end
146
156
 
157
+ # @return [String] class name as string
158
+ def self.class_name
159
+ name.rpartition("::").last
160
+ end
161
+
147
162
  # @return [String] string representation of this object
148
163
  def to_s
149
- "#{self.class.name.rpartition('::').last}#{":#{guid}" if respond_to?(:guid)}"
164
+ "#{self.class.class_name}#{":#{guid}" if respond_to?(:guid)}"
165
+ end
166
+
167
+ # Renders entity to a hash representation of the entity JSON format
168
+ # @return [Hash] Returns a hash that is equal by structure to the entity in JSON format
169
+ def to_json
170
+ {
171
+ entity_type: self.class.entity_name,
172
+ entity_data: json_data
173
+ }
174
+ end
175
+
176
+ # Creates an instance of self, filling it with data from a provided hash of properties.
177
+ #
178
+ # The hash format is described as following:<br>
179
+ # 1) Properties of the hash are representation of the entity's class properties<br>
180
+ # 2) Keys of the hash must be of Symbol type<br>
181
+ # 3) Possible values of the hash properties depend on the types of the entity's class properties<br>
182
+ # 4) Basic properties, such as booleans, strings, integers and timestamps are represented by values of respective
183
+ # formats<br>
184
+ # 5) Nested hashes and arrays of hashes are allowed to represent nested entities. Nested hashes follow the same
185
+ # format as the parent hash.<br>
186
+ # 6) Besides, the nested entities can be passed in the hash as already instantiated objects of the respective type.
187
+ #
188
+ # @param [Hash] properties_hash A hash of the expected format
189
+ # @return [Entity] an instance
190
+ def self.from_hash(properties_hash)
191
+ new(properties_hash)
150
192
  end
151
193
 
152
194
  private
153
195
 
154
196
  def validate_missing_props(entity_data)
155
197
  missing_props = self.class.missing_props(entity_data)
156
- raise ValidationError, "missing required properties: #{missing_props.join(', ')}" unless missing_props.empty?
198
+ return if missing_props.empty?
199
+
200
+ obj_str = "#{self.class.class_name}#{":#{entity_data[:guid]}" if entity_data.has_key?(:guid)}" \
201
+ "#{" from #{entity_data[:author]}" if entity_data.has_key?(:author)}"
202
+ raise ValidationError, "#{obj_str}: Missing required properties: #{missing_props.join(', ')}"
157
203
  end
158
204
 
159
205
  def setable?(name, val)
@@ -168,7 +214,7 @@ module DiasporaFederation
168
214
  end
169
215
 
170
216
  def setable_string?(type, val)
171
- %i(string integer boolean).include?(type) && val.respond_to?(:to_s)
217
+ %i[string integer boolean].include?(type) && val.respond_to?(:to_s)
172
218
  end
173
219
 
174
220
  def setable_nested?(type, val)
@@ -209,7 +255,7 @@ module DiasporaFederation
209
255
  errors = validator.errors.map do |prop, rule|
210
256
  "property: #{prop}, value: #{public_send(prop).inspect}, rule: #{rule[:rule]}, with params: #{rule[:params]}"
211
257
  end
212
- "Failed validation for properties: #{errors.join(' | ')}"
258
+ "Failed validation for #{self}#{" from #{author}" if respond_to?(:author)} for properties: #{errors.join(' | ')}"
213
259
  end
214
260
 
215
261
  # @return [Hash] hash with all properties
@@ -225,7 +271,7 @@ module DiasporaFederation
225
271
 
226
272
  def normalize_property(name, value)
227
273
  case self.class.class_props[name]
228
- when :string, :integer, :boolean
274
+ when :string
229
275
  value.to_s
230
276
  when :timestamp
231
277
  value.nil? ? "" : value.utc.iso8601
@@ -245,8 +291,8 @@ module DiasporaFederation
245
291
  end
246
292
 
247
293
  def add_property_to_xml(doc, root_element, name, value)
248
- if value.is_a? String
249
- root_element << simple_node(doc, name, value)
294
+ if [String, TrueClass, FalseClass, Integer].any? {|c| value.is_a? c }
295
+ root_element << simple_node(doc, name, value.to_s)
250
296
  else
251
297
  # call #to_xml for each item and append to root
252
298
  [*value].compact.each do |item|
@@ -258,102 +304,34 @@ module DiasporaFederation
258
304
 
259
305
  # Create simple node, fill it with text and append to root
260
306
  def simple_node(doc, name, value)
261
- xml_name = self.class.xml_names[name]
262
- Nokogiri::XML::Element.new(xml_name ? xml_name.to_s : name, doc).tap do |node|
307
+ Nokogiri::XML::Element.new(name.to_s, doc).tap do |node|
263
308
  node.content = value.gsub(INVALID_XML_REGEX, "\uFFFD") unless value.empty?
264
309
  end
265
310
  end
266
311
 
267
- # @param [Nokogiri::XML::Element] root_node xml nodes
268
- # @return [Entity] instance
269
- private_class_method def self.populate_entity(root_node)
270
- new(entity_data(root_node))
271
- end
272
-
273
- # @param [Nokogiri::XML::Element] root_node xml nodes
274
- # @return [Hash] entity data
275
- private_class_method def self.entity_data(root_node)
276
- class_props.map {|name, type|
277
- value = parse_element_from_node(name, type, root_node)
278
- [name, value] unless value.nil?
279
- }.compact.to_h
280
- end
281
-
282
- # @param [String] name property name to parse
283
- # @param [Class, Symbol] type target type to parse
284
- # @param [Nokogiri::XML::Element] root_node XML node to parse
285
- # @return [Object] parsed data
286
- private_class_method def self.parse_element_from_node(name, type, root_node)
287
- if type.instance_of?(Symbol)
288
- parse_string_from_node(name, type, root_node)
289
- elsif type.instance_of?(Array)
290
- parse_array_from_node(type.first, root_node)
291
- elsif type.ancestors.include?(Entity)
292
- parse_entity_from_node(type, root_node)
293
- end
294
- end
312
+ # Generates a hash with entity properties which is put to the "entity_data"
313
+ # field of a JSON serialized object.
314
+ # @return [Hash] object properties in JSON format
315
+ def json_data
316
+ enriched_properties.map {|key, value|
317
+ type = self.class.class_props[key]
295
318
 
296
- # Create simple entry in data hash
297
- #
298
- # @param [String] name xml tag to parse
299
- # @param [Class, Symbol] type target type to parse
300
- # @param [Nokogiri::XML::Element] root_node XML root_node to parse
301
- # @return [String] data
302
- private_class_method def self.parse_string_from_node(name, type, root_node)
303
- node = root_node.xpath(name.to_s)
304
- node = root_node.xpath(xml_names[name].to_s) if node.empty?
305
- parse_string(type, node.first.text) if node.any?
306
- end
307
-
308
- # @param [Symbol] type target type to parse
309
- # @param [String] text data as string
310
- # @return [String, Boolean, Integer, Time] data
311
- private_class_method def self.parse_string(type, text)
312
- case type
313
- when :timestamp
314
- begin
315
- Time.parse(text).utc
316
- rescue
317
- nil
319
+ if !value.nil? && type.instance_of?(Class) && value.respond_to?(:to_json)
320
+ entity_data = value.to_json
321
+ [key, entity_data] unless entity_data.nil?
322
+ elsif type.instance_of?(Array)
323
+ entity_data = value.nil? ? nil : value.map(&:to_json)
324
+ [key, entity_data] unless entity_data.nil?
325
+ else
326
+ [key, value]
318
327
  end
319
- when :integer
320
- text.to_i if text =~ /\A\d+\z/
321
- when :boolean
322
- return true if text =~ /\A(true|t|yes|y|1)\z/i
323
- false if text =~ /\A(false|f|no|n|0)\z/i
324
- else
325
- text
326
- end
327
- end
328
-
329
- # Create an entry in the data hash for the nested entity
330
- #
331
- # @param [Class] type target type to parse
332
- # @param [Nokogiri::XML::Element] root_node XML node to parse
333
- # @return [Entity] parsed child entity
334
- private_class_method def self.parse_entity_from_node(type, root_node)
335
- node = root_node.xpath(type.entity_name)
336
- type.from_xml(node.first) if node.any? && node.first.children.any?
337
- end
338
-
339
- # Collect all nested children of that type and create an array in the data hash
340
- #
341
- # @param [Class] type target type to parse
342
- # @param [Nokogiri::XML::Element] root_node XML node to parse
343
- # @return [Array<Entity>] array with parsed child entities
344
- private_class_method def self.parse_array_from_node(type, root_node)
345
- node = root_node.xpath(type.entity_name)
346
- node.select {|child| child.children.any? }.map {|child| type.from_xml(child) } unless node.empty?
328
+ }.compact.to_h
347
329
  end
348
330
 
349
331
  # Raised, if entity is not valid
350
332
  class ValidationError < RuntimeError
351
333
  end
352
334
 
353
- # Raised, if the root node doesn't match the class name
354
- class InvalidRootNode < RuntimeError
355
- end
356
-
357
335
  # Raised, if the entity name in the XML is invalid
358
336
  class InvalidEntityName < RuntimeError
359
337
  end
@@ -13,7 +13,7 @@ module DiasporaFederation
13
13
  response = HttpClient.get(url)
14
14
  raise "Failed to fetch #{url}: #{response.status}" unless response.success?
15
15
 
16
- magic_env_xml = Nokogiri::XML::Document.parse(response.body).root
16
+ magic_env_xml = Nokogiri::XML(response.body).root
17
17
  magic_env = Salmon::MagicEnvelope.unenvelop(magic_env_xml)
18
18
  Receiver::Public.new(magic_env).receive
19
19
  rescue => e
@@ -11,7 +11,7 @@ module DiasporaFederation
11
11
  magic_env = if legacy
12
12
  Salmon::Slap.from_xml(data)
13
13
  else
14
- magic_env_xml = Nokogiri::XML::Document.parse(data).root
14
+ magic_env_xml = Nokogiri::XML(data).root
15
15
  Salmon::MagicEnvelope.unenvelop(magic_env_xml)
16
16
  end
17
17
  Public.new(magic_env).receive
@@ -1,3 +1,5 @@
1
+ require "typhoeus"
2
+
1
3
  module DiasporaFederation
2
4
  module Federation
3
5
  module Sender
@@ -17,12 +19,21 @@ module DiasporaFederation
17
19
  method: :post,
18
20
  verbose: DiasporaFederation.http_verbose,
19
21
  cainfo: DiasporaFederation.certificate_authorities,
20
- forbid_reuse: true,
21
- headers: {
22
- "Expect" => "",
23
- "Transfer-Encoding" => "",
24
- "User-Agent" => DiasporaFederation.http_user_agent
25
- }
22
+ forbid_reuse: true
23
+ }
24
+ end
25
+
26
+ def self.xml_headers
27
+ @xml_headers ||= {
28
+ "Content-Type" => "application/magic-envelope+xml",
29
+ "User-Agent" => DiasporaFederation.http_user_agent
30
+ }
31
+ end
32
+
33
+ def self.json_headers
34
+ @json_headers ||= {
35
+ "Content-Type" => "application/json",
36
+ "User-Agent" => DiasporaFederation.http_user_agent
26
37
  }
27
38
  end
28
39
 
@@ -36,13 +47,18 @@ module DiasporaFederation
36
47
  @urls_to_retry = []
37
48
  end
38
49
 
39
- # Prepares and inserts job into the hydra queue
50
+ # Prepares and inserts a public MagicEnvelope job into the hydra queue
40
51
  # @param [String] url the receive-url for the xml
41
- # @param [String] xml xml salmon message
42
- def insert_job(url, xml)
43
- request = Typhoeus::Request.new(url, HydraWrapper.hydra_opts.merge(body: {xml: xml}))
44
- prepare_request(request)
45
- hydra.queue(request)
52
+ # @param [String] xml MagicEnvelope xml
53
+ def insert_magic_env_request(url, xml)
54
+ insert_job(url, HydraWrapper.hydra_opts.merge(body: xml, headers: HydraWrapper.xml_headers))
55
+ end
56
+
57
+ # Prepares and inserts a private encrypted MagicEnvelope job into the hydra queue
58
+ # @param [String] url the receive-url for the message
59
+ # @param [String] json encrypted MagicEnvelope json
60
+ def insert_enc_magic_env_request(url, json)
61
+ insert_job(url, HydraWrapper.hydra_opts.merge(body: json, headers: HydraWrapper.json_headers))
46
62
  end
47
63
 
48
64
  # Sends all queued messages
@@ -54,6 +70,15 @@ module DiasporaFederation
54
70
 
55
71
  private
56
72
 
73
+ # Prepares and inserts job into the hydra queue
74
+ # @param [String] url the receive-url for the message
75
+ # @param [Hash] options request options
76
+ def insert_job(url, options)
77
+ request = Typhoeus::Request.new(url, options)
78
+ prepare_request(request)
79
+ hydra.queue(request)
80
+ end
81
+
57
82
  # @return [Typhoeus::Hydra] hydra
58
83
  def hydra
59
84
  @hydra ||= Typhoeus::Hydra.new(max_concurrency: DiasporaFederation.http_concurrency)
@@ -11,7 +11,7 @@ module DiasporaFederation
11
11
  # @return [Array<String>] url to retry
12
12
  def self.public(sender_id, obj_str, urls, xml)
13
13
  hydra = HydraWrapper.new(sender_id, obj_str)
14
- urls.each {|url| hydra.insert_job(url, xml) }
14
+ urls.each {|url| hydra.insert_magic_env_request(url, xml) }
15
15
  hydra.send
16
16
  end
17
17
 
@@ -23,7 +23,7 @@ module DiasporaFederation
23
23
  # @return [Hash] targets to retry
24
24
  def self.private(sender_id, obj_str, targets)
25
25
  hydra = HydraWrapper.new(sender_id, obj_str)
26
- targets.each {|url, xml| hydra.insert_job(url, xml) }
26
+ targets.each {|url, json| hydra.insert_enc_magic_env_request(url, json) }
27
27
  hydra.send.map {|url| [url, targets[url]] }.to_h
28
28
  end
29
29
  end
@@ -0,0 +1,61 @@
1
+ module DiasporaFederation
2
+ module Parsers
3
+ # +BaseParser+ is an abstract class which is used for defining parsers for different
4
+ # deserialization methods.
5
+ class BaseParser
6
+ # @param [Class] entity_type type of DiasporaFederation::Entity that we want to parse with that parser instance
7
+ def initialize(entity_type)
8
+ @entity_type = entity_type
9
+ end
10
+
11
+ # This method is used to parse input with a serialized object data. It returns
12
+ # a comprehensive data which must be enough to construct a DiasporaFederation::Entity instance.
13
+ #
14
+ # Since parser method output is normally passed to a .from_hash method of an entity
15
+ # as arguments using * operator, the parse method must return an array of a size matching the number
16
+ # of arguments of .from_hash method of the entity type we link with
17
+ # @abstract
18
+ def parse(*)
19
+ raise NotImplementedError.new("you must override this method when creating your own parser")
20
+ end
21
+
22
+ private
23
+
24
+ # @param [Symbol] type target type to parse
25
+ # @param [String] text data as string
26
+ # @return [String, Boolean, Integer, Time] data
27
+ def parse_string(type, text)
28
+ case type
29
+ when :timestamp
30
+ begin
31
+ Time.parse(text).utc
32
+ rescue
33
+ nil
34
+ end
35
+ when :integer
36
+ text.to_i if text =~ /\A\d+\z/
37
+ when :boolean
38
+ return true if text =~ /\A(true|t|yes|y|1)\z/i
39
+ false if text =~ /\A(false|f|no|n|0)\z/i
40
+ else
41
+ text
42
+ end
43
+ end
44
+
45
+ def assert_parsability_of(entity_class)
46
+ return if entity_class == entity_type.entity_name
47
+ raise InvalidRootNode, "'#{entity_class}' can't be parsed by #{entity_type.name}"
48
+ end
49
+
50
+ attr_reader :entity_type
51
+
52
+ def class_properties
53
+ entity_type.class_props
54
+ end
55
+
56
+ # Raised, if the root node doesn't match the class name
57
+ class InvalidRootNode < RuntimeError
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,60 @@
1
+ module DiasporaFederation
2
+ module Parsers
3
+ # This is a parser of JSON serialized object. JSON object format is defined by
4
+ # JSON schema which is available at https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json.
5
+ # TODO: We must publish the schema at a real URL
6
+ class JsonParser < BaseParser
7
+ # @see BaseParser#parse
8
+ # @param [Hash] json_hash A hash acquired by running JSON.parse with JSON serialized entity
9
+ # @return [Array[1]] comprehensive data for an entity instantiation
10
+ def parse(json_hash)
11
+ from_json_sanity_validation(json_hash)
12
+ parse_entity_data(json_hash["entity_data"])
13
+ end
14
+
15
+ private
16
+
17
+ def parse_entity_data(entity_data)
18
+ hash = entity_data.map {|key, value|
19
+ property = entity_type.find_property_for_xml_name(key)
20
+ if property
21
+ type = entity_type.class_props[property]
22
+ [property, parse_element_from_value(type, entity_data[key])]
23
+ else
24
+ [key, value]
25
+ end
26
+ }.to_h
27
+
28
+ [hash]
29
+ end
30
+
31
+ def parse_element_from_value(type, value)
32
+ return if value.nil?
33
+ if %i[integer boolean timestamp].include?(type) && !value.is_a?(String)
34
+ value
35
+ elsif type.instance_of?(Symbol)
36
+ parse_string(type, value)
37
+ elsif type.instance_of?(Array)
38
+ raise DeserializationError, "Expected array for #{type}" unless value.respond_to?(:map)
39
+ value.map {|element|
40
+ type.first.from_json(element)
41
+ }
42
+ elsif type.ancestors.include?(Entity)
43
+ type.from_json(value)
44
+ end
45
+ end
46
+
47
+ def from_json_sanity_validation(json_hash)
48
+ missing = %w[entity_type entity_data].map {|prop|
49
+ prop if json_hash[prop].nil?
50
+ }.compact.join(", ")
51
+ raise DeserializationError, "Required properties are missing in JSON object: #{missing}" unless missing.empty?
52
+ assert_parsability_of(json_hash["entity_type"])
53
+ end
54
+
55
+ # Raised when the format of the input JSON data doesn't match the parser's expectations
56
+ class DeserializationError < RuntimeError
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module DiasporaFederation
2
+ module Parsers
3
+ # This is a parser of JSON serialized object, that is normally used for parsing data of relayables.
4
+ # Assumed format differs from the usual entity by additional "property_order" property which is used to
5
+ # compute signatures deterministically.
6
+ # Input JSON for this parser is expected to match "/definitions/relayable" subschema of the JSON schema at
7
+ # https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json.
8
+ class RelayableJsonParser < JsonParser
9
+ # @see JsonParser#parse
10
+ # @see BaseParser#parse
11
+ # @return [Array[2]] comprehensive data for an entity instantiation
12
+ def parse(json_hash)
13
+ super.push(json_hash["property_order"])
14
+ end
15
+
16
+ private
17
+
18
+ def from_json_sanity_validation(json_hash)
19
+ super
20
+ return unless json_hash["property_order"].nil?
21
+ raise DeserializationError, "Required property is missing in JSON object: property_order"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module DiasporaFederation
2
+ module Parsers
3
+ # This is a parser of XML serialized object that is normally used for parsing data of relayables.
4
+ # Explanations about the XML data format can be found
5
+ # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}.
6
+ # Specific features of relayables are described
7
+ # {https://diaspora.github.io/diaspora_federation/federation/relayable.html here}.
8
+ #
9
+ # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization
10
+ # documentation
11
+ # @see https://diaspora.github.io/diaspora_federation/federation/relayable.html Relayable documentation
12
+ class RelayableXmlParser < XmlParser
13
+ # @see XmlParser#parse
14
+ # @see BaseParser#parse
15
+ # @return [Array[2]] comprehensive data for an entity instantiation
16
+ def parse(*args)
17
+ hash = super[0]
18
+ [hash, hash.keys]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,84 @@
1
+ module DiasporaFederation
2
+ module Parsers
3
+ # This is a parser of XML serialized object.
4
+ # Explanations about the XML data format can be found
5
+ # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}.
6
+ # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization
7
+ # documentation
8
+ class XmlParser < BaseParser
9
+ # @see BaseParser#parse
10
+ # @param [Nokogiri::XML::Element] root_node root XML node of the XML representation of the entity
11
+ # @return [Array[1]] comprehensive data for an entity instantiation
12
+ def parse(root_node)
13
+ from_xml_sanity_validation(root_node)
14
+
15
+ hash = root_node.element_children.map {|child|
16
+ xml_name = child.name
17
+ property = entity_type.find_property_for_xml_name(xml_name)
18
+ if property
19
+ type = class_properties[property]
20
+ value = parse_element_from_node(xml_name, type, root_node)
21
+ [property, value]
22
+ else
23
+ [xml_name, child.text]
24
+ end
25
+ }.to_h
26
+
27
+ [hash]
28
+ end
29
+
30
+ private
31
+
32
+ # @param [String] name property name to parse
33
+ # @param [Class, Symbol] type target type to parse
34
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
35
+ # @return [Object] parsed data
36
+ def parse_element_from_node(name, type, root_node)
37
+ if type.instance_of?(Symbol)
38
+ parse_string_from_node(name, type, root_node)
39
+ elsif type.instance_of?(Array)
40
+ parse_array_from_node(type.first, root_node)
41
+ elsif type.ancestors.include?(Entity)
42
+ parse_entity_from_node(type, root_node)
43
+ end
44
+ end
45
+
46
+ # Create simple entry in data hash
47
+ #
48
+ # @param [String] name xml tag to parse
49
+ # @param [Class, Symbol] type target type to parse
50
+ # @param [Nokogiri::XML::Element] root_node XML root_node to parse
51
+ # @return [String] data
52
+ def parse_string_from_node(name, type, root_node)
53
+ node = root_node.xpath(name.to_s)
54
+ node = root_node.xpath(xml_names[name].to_s) if node.empty?
55
+ parse_string(type, node.first.text) if node.any?
56
+ end
57
+
58
+ # Create an entry in the data hash for the nested entity
59
+ #
60
+ # @param [Class] type target type to parse
61
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
62
+ # @return [Entity] parsed child entity
63
+ def parse_entity_from_node(type, root_node)
64
+ node = root_node.xpath(type.entity_name)
65
+ type.from_xml(node.first) if node.any? && node.first.children.any?
66
+ end
67
+
68
+ # Collect all nested children of that type and create an array in the data hash
69
+ #
70
+ # @param [Class] type target type to parse
71
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
72
+ # @return [Array<Entity>] array with parsed child entities
73
+ def parse_array_from_node(type, root_node)
74
+ node = root_node.xpath(type.entity_name)
75
+ node.select {|child| child.children.any? }.map {|child| type.from_xml(child) } unless node.empty?
76
+ end
77
+
78
+ def from_xml_sanity_validation(root_node)
79
+ raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element)
80
+ assert_parsability_of(root_node.name)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ module DiasporaFederation
2
+ # This namespace contains parsers which are used to deserialize federation entities
3
+ # objects from supported formats (XML, JSON) to objects of DiasporaFederation::Entity
4
+ # classes
5
+ module Parsers
6
+ end
7
+ end
8
+
9
+ require "diaspora_federation/parsers/base_parser"
10
+ require "diaspora_federation/parsers/json_parser"
11
+ require "diaspora_federation/parsers/xml_parser"
12
+ require "diaspora_federation/parsers/relayable_json_parser"
13
+ require "diaspora_federation/parsers/relayable_xml_parser"
@@ -127,7 +127,7 @@ module DiasporaFederation
127
127
  end
128
128
 
129
129
  def property_type_valid?(type)
130
- %i(string integer boolean timestamp).include?(type)
130
+ %i[string integer boolean timestamp].include?(type)
131
131
  end
132
132
 
133
133
  # Checks if the type extends {Entity}
@@ -54,7 +54,7 @@ module DiasporaFederation
54
54
  key = encoded_key.map {|k, v| [k, Base64.decode64(v)] }.to_h
55
55
 
56
56
  xml = AES.decrypt(encrypted_json["encrypted_magic_envelope"], key["key"], key["iv"])
57
- Nokogiri::XML::Document.parse(xml).root
57
+ Nokogiri::XML(xml).root
58
58
  end
59
59
  end
60
60
  end