diaspora_federation 0.1.9 → 0.2.0

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 (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