diaspora_federation 0.0.8 → 0.0.9
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.
- 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
|