diaspora_federation 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/lib/diaspora_federation.rb +66 -1
- data/lib/diaspora_federation/discovery/h_card.rb +2 -3
- data/lib/diaspora_federation/discovery/web_finger.rb +3 -6
- data/lib/diaspora_federation/entities.rb +18 -0
- data/lib/diaspora_federation/entities/account_deletion.rb +14 -0
- data/lib/diaspora_federation/entities/comment.rb +26 -0
- data/lib/diaspora_federation/entities/conversation.rb +38 -0
- data/lib/diaspora_federation/entities/like.rb +35 -0
- data/lib/diaspora_federation/entities/location.rb +23 -0
- data/lib/diaspora_federation/entities/message.rb +38 -0
- data/lib/diaspora_federation/entities/participation.rb +28 -0
- data/lib/diaspora_federation/entities/person.rb +6 -3
- data/lib/diaspora_federation/entities/photo.rb +59 -0
- data/lib/diaspora_federation/entities/poll.rb +24 -0
- data/lib/diaspora_federation/entities/poll_answer.rb +19 -0
- data/lib/diaspora_federation/entities/poll_participation.rb +28 -0
- data/lib/diaspora_federation/entities/profile.rb +10 -8
- data/lib/diaspora_federation/entities/relayable.rb +101 -0
- data/lib/diaspora_federation/entities/relayable_retraction.rb +95 -0
- data/lib/diaspora_federation/entities/request.rb +21 -0
- data/lib/diaspora_federation/entities/reshare.rb +49 -0
- data/lib/diaspora_federation/entities/retraction.rb +24 -0
- data/lib/diaspora_federation/entities/signed_retraction.rb +66 -0
- data/lib/diaspora_federation/entities/status_message.rb +55 -0
- data/lib/diaspora_federation/entity.rb +5 -6
- data/lib/diaspora_federation/fetcher.rb +1 -2
- data/lib/diaspora_federation/properties_dsl.rb +18 -8
- data/lib/diaspora_federation/salmon.rb +17 -0
- data/lib/diaspora_federation/salmon/aes.rb +58 -0
- data/lib/diaspora_federation/salmon/encrypted_slap.rb +187 -0
- data/lib/diaspora_federation/salmon/exceptions.rb +50 -0
- data/lib/diaspora_federation/salmon/magic_envelope.rb +191 -0
- data/lib/diaspora_federation/salmon/slap.rb +128 -0
- data/lib/diaspora_federation/salmon/xml_payload.rb +158 -0
- data/lib/diaspora_federation/signing.rb +56 -0
- data/lib/diaspora_federation/validators.rb +20 -0
- data/lib/diaspora_federation/validators/account_deletion_validator.rb +10 -0
- data/lib/diaspora_federation/validators/comment_validator.rb +17 -0
- data/lib/diaspora_federation/validators/conversation_validator.rb +14 -0
- data/lib/diaspora_federation/validators/like_validator.rb +14 -0
- data/lib/diaspora_federation/validators/location_validator.rb +11 -0
- data/lib/diaspora_federation/validators/message_validator.rb +16 -0
- data/lib/diaspora_federation/validators/participation_validator.rb +16 -0
- data/lib/diaspora_federation/validators/photo_validator.rb +24 -0
- data/lib/diaspora_federation/validators/poll_answer_validator.rb +11 -0
- data/lib/diaspora_federation/validators/poll_participation_validator.rb +16 -0
- data/lib/diaspora_federation/validators/poll_validator.rb +11 -0
- data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +15 -0
- data/lib/diaspora_federation/validators/relayable_validator.rb +14 -0
- data/lib/diaspora_federation/validators/request_validator.rb +11 -0
- data/lib/diaspora_federation/validators/reshare_validator.rb +18 -0
- data/lib/diaspora_federation/validators/retraction_validator.rb +14 -0
- data/lib/diaspora_federation/validators/rules/diaspora_id_count.rb +37 -0
- data/lib/diaspora_federation/validators/signed_retraction_validator.rb +15 -0
- data/lib/diaspora_federation/validators/status_message_validator.rb +14 -0
- data/lib/diaspora_federation/version.rb +1 -1
- metadata +49 -4
@@ -0,0 +1,191 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Salmon
|
3
|
+
# Represents a Magic Envelope for Diaspora* federation messages.
|
4
|
+
#
|
5
|
+
# When generating a Magic Envelope, an instance of this class is created and
|
6
|
+
# the contents are specified on initialization. Optionally, the payload can be
|
7
|
+
# encrypted ({MagicEnvelope#encrypt!}), before the XML is returned
|
8
|
+
# ({MagicEnvelope#envelop}).
|
9
|
+
#
|
10
|
+
# The generated XML appears like so:
|
11
|
+
#
|
12
|
+
# <me:env>
|
13
|
+
# <me:data type="application/xml">{data}</me:data>
|
14
|
+
# <me:encoding>base64url</me:encoding>
|
15
|
+
# <me:alg>RSA-SHA256</me:alg>
|
16
|
+
# <me:sig>{signature}</me:sig>
|
17
|
+
# </me:env>
|
18
|
+
#
|
19
|
+
# When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop}
|
20
|
+
# is used.
|
21
|
+
#
|
22
|
+
# @see http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html
|
23
|
+
class MagicEnvelope
|
24
|
+
# returns the payload (only used for testing purposes)
|
25
|
+
attr_reader :payload
|
26
|
+
|
27
|
+
# encoding used for the payload data
|
28
|
+
ENCODING = "base64url"
|
29
|
+
|
30
|
+
# algorithm used for signing the payload data
|
31
|
+
ALGORITHM = "RSA-SHA256"
|
32
|
+
|
33
|
+
# mime type describing the payload data
|
34
|
+
DATA_TYPE = "application/xml"
|
35
|
+
|
36
|
+
# digest instance used for signing
|
37
|
+
DIGEST = OpenSSL::Digest::SHA256.new
|
38
|
+
|
39
|
+
# XML namespace url
|
40
|
+
XMLNS = "http://salmon-protocol.org/ns/magic-env"
|
41
|
+
|
42
|
+
# Creates a new instance of MagicEnvelope.
|
43
|
+
#
|
44
|
+
# @param [OpenSSL::PKey::RSA] rsa_pkey private key used for signing
|
45
|
+
# @param [Entity] payload Entity instance
|
46
|
+
# @raise [ArgumentError] if either argument is not of the right type
|
47
|
+
def initialize(rsa_pkey, payload)
|
48
|
+
raise ArgumentError unless rsa_pkey.instance_of?(OpenSSL::PKey::RSA) &&
|
49
|
+
payload.is_a?(Entity)
|
50
|
+
|
51
|
+
@rsa_pkey = rsa_pkey
|
52
|
+
@payload = XmlPayload.pack(payload).to_xml.strip
|
53
|
+
end
|
54
|
+
|
55
|
+
# Builds the XML structure for the magic envelope, inserts the {ENCODING}
|
56
|
+
# encoded data and signs the envelope using {DIGEST}.
|
57
|
+
#
|
58
|
+
# @param [Nokogiri::XML::Builder] xml Salmon XML builder
|
59
|
+
def envelop(xml)
|
60
|
+
xml["me"].env {
|
61
|
+
xml["me"].data(Base64.urlsafe_encode64(@payload), type: DATA_TYPE)
|
62
|
+
xml["me"].encoding(ENCODING)
|
63
|
+
xml["me"].alg(ALGORITHM)
|
64
|
+
xml["me"].sig(Base64.urlsafe_encode64(signature))
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Encrypts the payload with a new, random AES cipher and returns the cipher
|
69
|
+
# params that were used.
|
70
|
+
#
|
71
|
+
# This must happen after the MagicEnvelope instance was created and before
|
72
|
+
# {MagicEnvelope#envelop} is called.
|
73
|
+
#
|
74
|
+
# @see AES#generate_key_and_iv
|
75
|
+
# @see AES#encrypt
|
76
|
+
#
|
77
|
+
# @return [Hash] AES key and iv. E.g.: { key: "...", iv: "..." }
|
78
|
+
def encrypt!
|
79
|
+
AES.generate_key_and_iv.tap do |key|
|
80
|
+
@payload = AES.encrypt(@payload, key[:key], key[:iv])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Extracts the entity encoded in the magic envelope data, if the signature
|
85
|
+
# is valid. If +cipher_params+ is given, also attempts to decrypt the payload first.
|
86
|
+
#
|
87
|
+
# Does some sanity checking to avoid bad surprises...
|
88
|
+
#
|
89
|
+
# @see XmlPayload#unpack
|
90
|
+
# @see AES#decrypt
|
91
|
+
#
|
92
|
+
# @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope
|
93
|
+
# @param [OpenSSL::PKey::RSA] rsa_pubkey public key to verify the signature
|
94
|
+
# @param [Hash] cipher_params hash containing the key and iv for
|
95
|
+
# AES-decrypting previously encrypted data. E.g.: { iv: "...", key: "..." }
|
96
|
+
#
|
97
|
+
# @return [Entity] reconstructed entity instance
|
98
|
+
#
|
99
|
+
# @raise [ArgumentError] if any of the arguments is of invalid type
|
100
|
+
# @raise [InvalidEnvelope] if the envelope XML structure is malformed
|
101
|
+
# @raise [InvalidSignature] if the signature can't be verified
|
102
|
+
# @raise [InvalidEncoding] if the data is wrongly encoded
|
103
|
+
# @raise [InvalidAlgorithm] if the algorithm used doesn't match
|
104
|
+
def self.unenvelop(magic_env, rsa_pubkey, cipher_params=nil)
|
105
|
+
raise ArgumentError unless rsa_pubkey.instance_of?(OpenSSL::PKey::RSA) &&
|
106
|
+
magic_env.instance_of?(Nokogiri::XML::Element)
|
107
|
+
|
108
|
+
raise InvalidEnvelope unless envelope_valid?(magic_env)
|
109
|
+
raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey)
|
110
|
+
|
111
|
+
raise InvalidEncoding unless encoding_valid?(magic_env)
|
112
|
+
raise InvalidAlgorithm unless algorithm_valid?(magic_env)
|
113
|
+
|
114
|
+
data = read_and_decrypt_data(magic_env, cipher_params)
|
115
|
+
|
116
|
+
XmlPayload.unpack(Nokogiri::XML::Document.parse(data).root)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# create the signature for all fields according to specification
|
122
|
+
#
|
123
|
+
# @return [String] the signature
|
124
|
+
def signature
|
125
|
+
subject = self.class.sig_subject([@payload,
|
126
|
+
DATA_TYPE,
|
127
|
+
ENCODING,
|
128
|
+
ALGORITHM])
|
129
|
+
@rsa_pkey.sign(DIGEST, subject)
|
130
|
+
end
|
131
|
+
|
132
|
+
# @param [Nokogiri::XML::Element] env magic envelope XML
|
133
|
+
def self.envelope_valid?(env)
|
134
|
+
(env.instance_of?(Nokogiri::XML::Element) &&
|
135
|
+
env.name == "env" &&
|
136
|
+
!env.at_xpath("me:data").content.empty? &&
|
137
|
+
!env.at_xpath("me:encoding").content.empty? &&
|
138
|
+
!env.at_xpath("me:alg").content.empty? &&
|
139
|
+
!env.at_xpath("me:sig").content.empty?)
|
140
|
+
end
|
141
|
+
private_class_method :envelope_valid?
|
142
|
+
|
143
|
+
# @param [Nokogiri::XML::Element] env magic envelope XML
|
144
|
+
# @param [OpenSSL::PKey::RSA] pkey public key
|
145
|
+
# @return [Boolean]
|
146
|
+
def self.signature_valid?(env, pkey)
|
147
|
+
subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content),
|
148
|
+
env.at_xpath("me:data")["type"],
|
149
|
+
env.at_xpath("me:encoding").content,
|
150
|
+
env.at_xpath("me:alg").content])
|
151
|
+
|
152
|
+
sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content)
|
153
|
+
pkey.verify(DIGEST, sig, subject)
|
154
|
+
end
|
155
|
+
private_class_method :signature_valid?
|
156
|
+
|
157
|
+
# constructs the signature subject.
|
158
|
+
# the given array should consist of the data, data_type (mimetype), encoding
|
159
|
+
# and the algorithm
|
160
|
+
# @param [Array<String>] data_arr
|
161
|
+
# @return [String] signature subject
|
162
|
+
def self.sig_subject(data_arr)
|
163
|
+
data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".")
|
164
|
+
end
|
165
|
+
|
166
|
+
# @param [Nokogiri::XML::Element] magic_env magic envelope XML
|
167
|
+
# @return [Boolean]
|
168
|
+
def self.encoding_valid?(magic_env)
|
169
|
+
magic_env.at_xpath("me:encoding").content == ENCODING
|
170
|
+
end
|
171
|
+
private_class_method :encoding_valid?
|
172
|
+
|
173
|
+
# @param [Nokogiri::XML::Element] magic_env magic envelope XML
|
174
|
+
# @return [Boolean]
|
175
|
+
def self.algorithm_valid?(magic_env)
|
176
|
+
magic_env.at_xpath("me:alg").content == ALGORITHM
|
177
|
+
end
|
178
|
+
private_class_method :algorithm_valid?
|
179
|
+
|
180
|
+
# @param [Nokogiri::XML::Element] magic_env magic envelope XML
|
181
|
+
# @param [Hash] cipher_params hash containing the key and iv
|
182
|
+
# @return [String] data
|
183
|
+
def self.read_and_decrypt_data(magic_env, cipher_params)
|
184
|
+
data = Base64.urlsafe_decode64(magic_env.at_xpath("me:data").content)
|
185
|
+
data = AES.decrypt(data, cipher_params[:key], cipher_params[:iv]) unless cipher_params.nil?
|
186
|
+
data
|
187
|
+
end
|
188
|
+
private_class_method :read_and_decrypt_data
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Salmon
|
3
|
+
# +Slap+ provides class methods to create unencrypted Slap XML from payload
|
4
|
+
# data and parse incoming XML into a Slap instance.
|
5
|
+
#
|
6
|
+
# A Diaspora*-flavored magic-enveloped XML message looks like the following:
|
7
|
+
#
|
8
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
9
|
+
# <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
|
10
|
+
# <header>
|
11
|
+
# <author_id>{author}</author_id>
|
12
|
+
# </header>
|
13
|
+
# {magic_envelope}
|
14
|
+
# </diaspora>
|
15
|
+
#
|
16
|
+
# @example Generating a Salmon Slap
|
17
|
+
# author_id = "author@pod.example.tld"
|
18
|
+
# author_privkey = however_you_retrieve_the_authors_private_key(author_id)
|
19
|
+
# entity = YourEntity.new(attr: "val")
|
20
|
+
#
|
21
|
+
# slap_xml = Slap.generate_xml(author_id, author_privkey, entity)
|
22
|
+
#
|
23
|
+
# @example Parsing a Salmon Slap
|
24
|
+
# slap = Slap.from_xml(slap_xml)
|
25
|
+
# author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
|
26
|
+
#
|
27
|
+
# entity = slap.entity(author_pubkey)
|
28
|
+
class Slap
|
29
|
+
# the author of the slap
|
30
|
+
# @overload author_id
|
31
|
+
# @return [String] the author diaspora id
|
32
|
+
# @overload author_id=
|
33
|
+
# @param [String] the author diaspora id
|
34
|
+
attr_accessor :author_id
|
35
|
+
|
36
|
+
# the key and iv if it is an encrypted slap
|
37
|
+
# @param [Hash] value hash containing the key and iv
|
38
|
+
attr_writer :cipher_params
|
39
|
+
|
40
|
+
# Namespaces
|
41
|
+
NS = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}
|
42
|
+
|
43
|
+
# Returns new instance of the Entity that is contained within the XML of
|
44
|
+
# this Slap.
|
45
|
+
#
|
46
|
+
# The first time this is called, a public key has to be specified to verify
|
47
|
+
# the Magic Envelope signature. On repeated calls, the key may be omitted.
|
48
|
+
#
|
49
|
+
# @see MagicEnvelope.unenvelop
|
50
|
+
#
|
51
|
+
# @param [OpenSSL::PKey::RSA] pubkey public key for validating the signature
|
52
|
+
# @return [Entity] entity instance from the XML
|
53
|
+
# @raise [ArgumentError] if the public key is of the wrong type
|
54
|
+
def entity(pubkey=nil)
|
55
|
+
return @entity unless @entity.nil?
|
56
|
+
|
57
|
+
raise ArgumentError unless pubkey.instance_of?(OpenSSL::PKey::RSA)
|
58
|
+
@entity = MagicEnvelope.unenvelop(@magic_envelope, pubkey, @cipher_params)
|
59
|
+
@entity
|
60
|
+
end
|
61
|
+
|
62
|
+
# Parses an unencrypted Salmon XML string and returns a new instance of
|
63
|
+
# {Slap} populated with the XML data.
|
64
|
+
#
|
65
|
+
# @param [String] slap_xml Salmon XML
|
66
|
+
# @return [Slap] new Slap instance
|
67
|
+
# @raise [ArgumentError] if the argument is not a String
|
68
|
+
# @raise [MissingAuthor] if the +author_id+ element is missing from the XML
|
69
|
+
# @raise [MissingMagicEnvelope] if the +me:env+ element is missing from the XML
|
70
|
+
def self.from_xml(slap_xml)
|
71
|
+
raise ArgumentError unless slap_xml.instance_of?(String)
|
72
|
+
doc = Nokogiri::XML::Document.parse(slap_xml)
|
73
|
+
|
74
|
+
Slap.new.tap do |slap|
|
75
|
+
author_elem = doc.at_xpath("d:diaspora/d:header/d:author_id", Slap::NS)
|
76
|
+
raise MissingAuthor if author_elem.nil? || author_elem.content.empty?
|
77
|
+
slap.author_id = author_elem.content
|
78
|
+
|
79
|
+
slap.add_magic_env_from_doc(doc)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates an unencrypted Salmon Slap and returns the XML string.
|
84
|
+
#
|
85
|
+
# @param [String] author_id Diaspora* handle of the author
|
86
|
+
# @param [OpenSSL::PKey::RSA] pkey sender private_key for signing the magic envelope
|
87
|
+
# @param [Entity] entity payload
|
88
|
+
# @return [String] Salmon XML string
|
89
|
+
# @raise [ArgumentError] if any of the arguments is not the correct type
|
90
|
+
def self.generate_xml(author_id, pkey, entity)
|
91
|
+
raise ArgumentError unless author_id.instance_of?(String) &&
|
92
|
+
pkey.instance_of?(OpenSSL::PKey::RSA) &&
|
93
|
+
entity.is_a?(Entity)
|
94
|
+
|
95
|
+
build_xml do |xml|
|
96
|
+
xml.header {
|
97
|
+
xml.author_id(author_id)
|
98
|
+
}
|
99
|
+
|
100
|
+
MagicEnvelope.new(pkey, entity).envelop(xml)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Builds the xml for the Salmon Slap.
|
105
|
+
#
|
106
|
+
# @yield [xml] Invokes the block with the
|
107
|
+
# {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder}
|
108
|
+
# @return [String] Slap XML
|
109
|
+
def self.build_xml
|
110
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
111
|
+
xml.diaspora("xmlns" => Salmon::XMLNS, "xmlns:me" => MagicEnvelope::XMLNS) {
|
112
|
+
yield xml
|
113
|
+
}
|
114
|
+
end
|
115
|
+
builder.to_xml
|
116
|
+
end
|
117
|
+
|
118
|
+
# Parses the magic envelop from the document.
|
119
|
+
#
|
120
|
+
# @param [Nokogiri::XML::Document] doc Salmon XML Document
|
121
|
+
def add_magic_env_from_doc(doc)
|
122
|
+
@magic_envelope = doc.at_xpath("d:diaspora/me:env", Slap::NS).tap do |env|
|
123
|
+
raise MissingMagicEnvelope if env.nil?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Salmon
|
3
|
+
# +XmlPayload+ provides methods to wrap a XML-serialized {Entity} inside a
|
4
|
+
# common XML structure that will become the payload for federation messages.
|
5
|
+
#
|
6
|
+
# The wrapper looks like so:
|
7
|
+
# <XML>
|
8
|
+
# <post>
|
9
|
+
# {data}
|
10
|
+
# </post>
|
11
|
+
# </XML>
|
12
|
+
#
|
13
|
+
# (The +post+ element is there for historic reasons...)
|
14
|
+
module XmlPayload
|
15
|
+
# Encapsulates an Entity inside the wrapping xml structure
|
16
|
+
# and returns the XML Object.
|
17
|
+
#
|
18
|
+
# @param [Entity] entity subject
|
19
|
+
# @return [Nokogiri::XML::Element] XML root node
|
20
|
+
# @raise [ArgumentError] if the argument is not an Entity subclass
|
21
|
+
def self.pack(entity)
|
22
|
+
raise ArgumentError, "only instances of DiasporaFederation::Entity allowed" unless entity.is_a?(Entity)
|
23
|
+
|
24
|
+
entity_xml = entity.to_xml
|
25
|
+
doc = entity_xml.document
|
26
|
+
wrap = Nokogiri::XML::Element.new("XML", doc)
|
27
|
+
wrap_post = Nokogiri::XML::Element.new("post", doc)
|
28
|
+
entity_xml.parent = wrap_post
|
29
|
+
wrap << wrap_post
|
30
|
+
|
31
|
+
wrap
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extracts the Entity XML from the wrapping XML structure, parses the entity
|
35
|
+
# XML and returns a new instance of the Entity that was packed inside the
|
36
|
+
# given payload.
|
37
|
+
#
|
38
|
+
# @param [Nokogiri::XML::Element] xml payload XML root node
|
39
|
+
# @return [Entity] re-constructed Entity instance
|
40
|
+
# @raise [ArgumentError] if the argument is not an
|
41
|
+
# {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}
|
42
|
+
# @raise [InvalidStructure] if the XML doesn't look like the wrapper XML
|
43
|
+
# @raise [UnknownEntity] if the class for the entity contained inside the
|
44
|
+
# XML can't be found
|
45
|
+
def self.unpack(xml)
|
46
|
+
raise ArgumentError, "only Nokogiri::XML::Element allowed" unless xml.instance_of?(Nokogiri::XML::Element)
|
47
|
+
raise Salmon::InvalidStructure unless wrap_valid?(xml)
|
48
|
+
|
49
|
+
data = xml.at_xpath("post/*[1]")
|
50
|
+
klass_name = entity_class_name(data.name)
|
51
|
+
raise Salmon::UnknownEntity, "'#{klass_name}' not found" unless Entities.const_defined?(klass_name)
|
52
|
+
|
53
|
+
klass = Entities.const_get(klass_name)
|
54
|
+
populate_entity(klass, data)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# @param [Nokogiri::XML::Element] element
|
60
|
+
def self.wrap_valid?(element)
|
61
|
+
(element.name == "XML" && !element.at_xpath("post").nil? &&
|
62
|
+
!element.at_xpath("post").children.empty?)
|
63
|
+
end
|
64
|
+
private_class_method :wrap_valid?
|
65
|
+
|
66
|
+
# Transform the given String from the lowercase underscored version to a
|
67
|
+
# camelized variant, used later for getting the Class constant.
|
68
|
+
#
|
69
|
+
# @param [String] term "snake_case" class name
|
70
|
+
# @return [String] "CamelCase" class name
|
71
|
+
def self.entity_class_name(term)
|
72
|
+
term.to_s.tap do |string|
|
73
|
+
raise Salmon::InvalidEntityName, "'#{string}' is invalid" unless string =~ /^[a-z]*(_[a-z]*)*$/
|
74
|
+
string.sub!(/^[a-z]/, &:upcase)
|
75
|
+
string.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
private_class_method :entity_class_name
|
79
|
+
|
80
|
+
# Construct a new instance of the given Entity and populate the properties
|
81
|
+
# with the attributes found in the XML.
|
82
|
+
# Works recursively on nested Entities and Arrays thereof.
|
83
|
+
#
|
84
|
+
# @param [Class] klass entity class
|
85
|
+
# @param [Nokogiri::XML::Element] root_node xml nodes
|
86
|
+
# @return [Entity] instance
|
87
|
+
def self.populate_entity(klass, root_node)
|
88
|
+
# Use all known properties to build the Entity. All other elements are respected
|
89
|
+
# and attached to resulted hash as string. It is intended to build a hash
|
90
|
+
# invariable of an Entity definition, in order to support receiving objects
|
91
|
+
# from the future versions of Diaspora, where new elements may have been added.
|
92
|
+
data = Hash[root_node.element_children.map { |child|
|
93
|
+
xml_name = child.name
|
94
|
+
property = klass.class_props.find {|prop| prop[:xml_name].to_s == xml_name }
|
95
|
+
if property
|
96
|
+
parse_element_from_node(property[:name], property[:type], xml_name, root_node)
|
97
|
+
else
|
98
|
+
[xml_name, child.text]
|
99
|
+
end
|
100
|
+
}]
|
101
|
+
|
102
|
+
Entities::Relayable.verify_signatures(data) if klass.included_modules.include?(Entities::Relayable)
|
103
|
+
|
104
|
+
klass.new(data)
|
105
|
+
end
|
106
|
+
private_class_method :populate_entity
|
107
|
+
|
108
|
+
# @param [Symbol] name property name
|
109
|
+
# @param [Class] type target type to parse
|
110
|
+
# @param [String] xml_name xml tag to parse
|
111
|
+
# @param [Nokogiri::XML::Element] node XML node to parse
|
112
|
+
# @return [Array<Symbol, Object>] parsed data
|
113
|
+
def self.parse_element_from_node(name, type, xml_name, node)
|
114
|
+
if type == String
|
115
|
+
[name, parse_string_from_node(xml_name, node)]
|
116
|
+
elsif type.instance_of?(Array)
|
117
|
+
[name, parse_array_from_node(type, node)]
|
118
|
+
elsif type.ancestors.include?(Entity)
|
119
|
+
[name, parse_entity_from_node(type, node)]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
private_class_method :parse_element_from_node
|
123
|
+
|
124
|
+
# create simple entry in data hash
|
125
|
+
#
|
126
|
+
# @param [String] name xml tag to parse
|
127
|
+
# @param [Nokogiri::XML::Element] root_node XML root_node to parse
|
128
|
+
# @return [String] data
|
129
|
+
def self.parse_string_from_node(name, root_node)
|
130
|
+
node = root_node.xpath(name.to_s)
|
131
|
+
node.first.text if node.any?
|
132
|
+
end
|
133
|
+
private_class_method :parse_string_from_node
|
134
|
+
|
135
|
+
# create an entry in the data hash for the nested entity
|
136
|
+
#
|
137
|
+
# @param [Class] type target type to parse
|
138
|
+
# @param [Nokogiri::XML::Element] root_node XML node to parse
|
139
|
+
# @return [Entity] parsed child entity
|
140
|
+
def self.parse_entity_from_node(type, root_node)
|
141
|
+
node = root_node.xpath(type.entity_name)
|
142
|
+
populate_entity(type, node.first) if node.any?
|
143
|
+
end
|
144
|
+
private_class_method :parse_entity_from_node
|
145
|
+
|
146
|
+
# collect all nested children of that type and create an array in the data hash
|
147
|
+
#
|
148
|
+
# @param [Array<Class>] type target type to parse
|
149
|
+
# @param [Nokogiri::XML::Element] root_node XML node to parse
|
150
|
+
# @return [Array<Entity>] array with parsed child entities
|
151
|
+
def self.parse_array_from_node(type, root_node)
|
152
|
+
node = root_node.xpath(type.first.entity_name)
|
153
|
+
node.map {|child| populate_entity(type.first, child) }
|
154
|
+
end
|
155
|
+
private_class_method :parse_array_from_node
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|