aq1018-rforce 0.5.1

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 (48) hide show
  1. data/.gitignore +3 -0
  2. data/History.txt +38 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +49 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/aq1018-rforce.gemspec +101 -0
  8. data/bugs/issue-09d998dc3f9d6e8928598fd5d9c6627102dedd65.yaml +18 -0
  9. data/bugs/issue-1b668248120eb4fd055f0ea6b75ff807f6955936.yaml +22 -0
  10. data/bugs/issue-3ef7a7c5c7e38fb76e1f53e1b67f45559e21b104.yaml +18 -0
  11. data/bugs/issue-a7a87cb98d29943a49d1608adb68475838ad2e65.yaml +22 -0
  12. data/bugs/issue-c92d34e10038ba6b9acfd7e3dfb2b154e463baba.yaml +29 -0
  13. data/bugs/project.yaml +16 -0
  14. data/lib/rforce.rb +88 -0
  15. data/lib/rforce/binding.rb +261 -0
  16. data/lib/rforce/method_keys.rb +14 -0
  17. data/lib/rforce/soap_pullable.rb +93 -0
  18. data/lib/rforce/soap_response.rb +7 -0
  19. data/lib/rforce/soap_response_expat.rb +35 -0
  20. data/lib/rforce/soap_response_hpricot.rb +75 -0
  21. data/lib/rforce/soap_response_nokogiri.rb +64 -0
  22. data/lib/rforce/soap_response_rexml.rb +34 -0
  23. data/lib/rforce/version.rb +3 -0
  24. data/spec/rforce_spec.rb +105 -0
  25. data/spec/soap-response.xml +5928 -0
  26. data/spec/spec.opts +1 -0
  27. data/spec/spec_helper.rb +66 -0
  28. data/tasks/timing.rake +21 -0
  29. data/test/oauth_tests.rb +42 -0
  30. data/wsdl/README.html +23 -0
  31. data/wsdl/README.txt +22 -0
  32. data/wsdl/ca.pem +36 -0
  33. data/wsdl/default.rb +1627 -0
  34. data/wsdl/defaultDriver.rb +179 -0
  35. data/wsdl/orderedhash.rb +254 -0
  36. data/wsdl/partner.wsdl.xml +3104 -0
  37. data/wsdl/partner.wsdl.xml.old +1858 -0
  38. data/wsdl/sample/create.rb +37 -0
  39. data/wsdl/sample/describeGlobal.rb +28 -0
  40. data/wsdl/sample/describeSObjects.rb +34 -0
  41. data/wsdl/sample/getServerTimestamp.rb +27 -0
  42. data/wsdl/sample/query.rb +31 -0
  43. data/wsdl/sample/queryMore.rb +32 -0
  44. data/wsdl/sample/retrieve.rb +32 -0
  45. data/wsdl/sforceDriver.rb +43 -0
  46. data/wsdl/soap/property +1 -0
  47. data/wsdl/wsdl.rb +121 -0
  48. metadata +172 -0
@@ -0,0 +1,261 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+ require 'zlib'
4
+ require 'stringio'
5
+ require 'rexml/document'
6
+ require 'builder'
7
+ require 'oauth' # the 0.3.6 version must be checked out
8
+
9
+
10
+ module RForce
11
+ # Implements the connection to the SalesForce server.
12
+ class Binding
13
+ include RForce
14
+
15
+ DEFAULT_BATCH_SIZE = 2000
16
+ attr_accessor :batch_size, :url, :assignment_rule_id,
17
+ :use_default_rule, :update_mru, :client_id,
18
+ :trigger_user_email,
19
+ :trigger_other_email, :trigger_auto_response_email
20
+
21
+ # Fill in the guts of this typical SOAP envelope
22
+ # with the session ID and the body of the SOAP request.
23
+ Envelope = <<-HERE
24
+ <?xml version="1.0" encoding="utf-8" ?>
25
+ <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
26
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
27
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
28
+ xmlns:partner="urn:partner.soap.sforce.com">
29
+ xmlns:spartner="urn:sobject.partner.soap.sforce.com">
30
+ <soap:Header>
31
+ <partner:SessionHeader soap:mustUnderstand='1'>
32
+ <partner:sessionId>%s</partner:sessionId>
33
+ </partner:SessionHeader>
34
+ <partner:QueryOptions soap:mustUnderstand='1'>
35
+ <partner:batchSize>%d</partner:batchSize>
36
+ </partner:QueryOptions>
37
+ %s
38
+ </soap:Header>
39
+ <soap:Body>
40
+ %s
41
+ </soap:Body>
42
+ </soap:Envelope>
43
+ HERE
44
+
45
+ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
46
+ AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
47
+ MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
48
+ ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
49
+
50
+ # Connect to the server securely. If you pass an oauth hash, it
51
+ # must contain the keys :consumer_key, :consumer_secret,
52
+ # :access_token, :access_secret, and :login_url.
53
+ def initialize(url, sid = nil, oauth = nil)
54
+ @session_id = sid
55
+ @oauth = oauth
56
+ @batch_size = DEFAULT_BATCH_SIZE
57
+
58
+ init_server(url)
59
+ end
60
+
61
+
62
+ def show_debug
63
+ ENV['SHOWSOAP'] == 'true'
64
+ end
65
+
66
+
67
+ def init_server(url)
68
+ @url = URI.parse(url)
69
+ @server = Net::HTTP.new(@url.host, @url.port)
70
+ @server.use_ssl = @url.scheme == 'https'
71
+ @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+
73
+ # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
74
+ @server.set_debug_output $stderr if show_debug
75
+ end
76
+
77
+
78
+ def consumer
79
+ OAuth::Consumer.new(@oauth[:consumer_key],
80
+ @oauth[:consumer_secret], {
81
+ :site => "https://login.salesforce.com",
82
+ :signature_method => 'HMAC-SHA1', # this is default, but just for clarity
83
+ :scheme => :body,
84
+ :request_token_path => "/_nc_external/system/security/oauth/RequestTokenHandler",
85
+ :access_token_path => "/_nc_external/system/security/oauth/AccessTokenHandler",
86
+ :authorize_path => '/setup/secur/RemoteAccessAuthorizationPage.apexp',
87
+ })
88
+ end
89
+
90
+
91
+
92
+ # Log in to the server with OAuth, remembering
93
+ # the session ID returned to us by SalesForce.
94
+ def login_with_oauth
95
+ # post is method of Oauth::AccessToken, @server is instance of it
96
+ access_token = OAuth::AccessToken.new consumer, @oauth[:access_token], @oauth[:access_secret]
97
+ result = access_token.post @oauth[:login_url]
98
+
99
+ case result
100
+ when Net::HTTPSuccess
101
+ doc = REXML::Document.new result.body
102
+ @session_id = doc.elements['*/sessionId'].text
103
+ server_url = doc.elements['*/serverUrl'].text
104
+ init_server(server_url)
105
+
106
+ return {:sessionId => @session_id, :serverUrl => server_url}
107
+ when Net::HTTPUnauthorized
108
+ raise "Invalid OAuth tokens=#{@oauth.inspect}"
109
+ else
110
+ raise "Unexpected error: #{result.inspect}"
111
+ end
112
+ end
113
+
114
+
115
+
116
+
117
+ # Log in to the server with a user name and password, remembering
118
+ # the session ID returned to us by SalesForce.
119
+ def login(user, password)
120
+ @user = user
121
+ @password = password
122
+
123
+ response = call_remote(:login, [:username, user, :password, password])
124
+
125
+ raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
126
+
127
+ result = response[:loginResponse][:result]
128
+ @session_id = result[:sessionId]
129
+
130
+ init_server(result[:serverUrl])
131
+
132
+ response
133
+ end
134
+
135
+
136
+
137
+ # Call a method on the remote server. Arguments can be
138
+ # a hash or (if order is important) an array of alternating
139
+ # keys and values.
140
+ def call_remote(method, args)
141
+
142
+ urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", @url.path]
143
+
144
+ # Create XML text from the arguments.
145
+ expanded = ''
146
+ @builder = Builder::XmlMarkup.new(:target => expanded)
147
+ expand(@builder, {method => args}, urn)
148
+
149
+ extra_headers = ""
150
+ extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
151
+ extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
152
+ extra_headers << MruHeader if update_mru
153
+ extra_headers << (ClientIdHeader % client_id) if client_id
154
+
155
+ if trigger_user_email or trigger_other_email or trigger_auto_response_email
156
+ extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
157
+
158
+ extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
159
+ extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
160
+ extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
161
+
162
+ extra_headers << '</partner:EmailHeader>'
163
+ end
164
+
165
+ # Fill in the blanks of the SOAP envelope with our
166
+ # session ID and the expanded XML of our request.
167
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
168
+
169
+ # reset the batch size for the next request
170
+ @batch_size = DEFAULT_BATCH_SIZE
171
+
172
+ # gzip request
173
+ request = encode(request)
174
+
175
+ headers = {
176
+ 'Connection' => 'Keep-Alive',
177
+ 'Content-Type' => 'text/xml',
178
+ 'SOAPAction' => '""',
179
+ 'User-Agent' => 'activesalesforce rforce/0.4.1 LH'
180
+ }
181
+
182
+ unless show_debug
183
+ headers['Accept-Encoding'] = 'gzip'
184
+ headers['Content-Encoding'] = 'gzip'
185
+ end
186
+
187
+ # Send the request to the server and read the response.
188
+ response = @server.post2(soap_url, request.lstrip, headers)
189
+
190
+ # decode if we have encoding
191
+ content = decode(response)
192
+
193
+ # Check to see if INVALID_SESSION_ID was raised and try to relogin in
194
+ if method != :login && @session_id && content =~ /sf:INVALID_SESSION_ID/
195
+ if @oauth
196
+ login_with_oauth
197
+ else
198
+ login(@user, @password)
199
+ end
200
+
201
+ # repackage and rencode request with the new session id
202
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
203
+ request = encode(request)
204
+
205
+ # Send the request to the server and read the response.
206
+ response = @server.post2(soap_url, request.lstrip, headers)
207
+
208
+ content = decode(response)
209
+ end
210
+
211
+ SoapResponse.new(content).parse
212
+ end
213
+
214
+
215
+ # decode gzip
216
+ def decode(response)
217
+ encoding = response['Content-Encoding']
218
+
219
+ # return body if no encoding
220
+ if !encoding then return response.body end
221
+
222
+ # decode gzip
223
+ case encoding.strip
224
+ when 'gzip' then
225
+ begin
226
+ gzr = Zlib::GzipReader.new(StringIO.new(response.body))
227
+ decoded = gzr.read
228
+ ensure
229
+ gzr.close
230
+ end
231
+ decoded
232
+ else
233
+ response.body
234
+ end
235
+ end
236
+
237
+
238
+ # encode gzip
239
+ def encode(request)
240
+ return request if show_debug
241
+ begin
242
+ ostream = StringIO.new
243
+ gzw = Zlib::GzipWriter.new(ostream)
244
+ gzw.write(request)
245
+ ostream.string
246
+ ensure
247
+ gzw.close
248
+ end
249
+ end
250
+
251
+
252
+ # Turns method calls on this object into remote SOAP calls.
253
+ def method_missing(method, *args)
254
+ unless args.size == 1 && [Hash, Array].include?(args[0].class)
255
+ raise 'Expected 1 Hash or Array argument'
256
+ end
257
+
258
+ call_remote method, args[0]
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,14 @@
1
+ module RForce
2
+ # Allows indexing hashes like method calls: hash.key
3
+ # to supplement the traditional way of indexing: hash[key]
4
+ module MethodKeys
5
+ def method_missing(method, *args)
6
+ raise NoMethodError unless respond_to?('[]')
7
+ self[method]
8
+ end
9
+ end
10
+
11
+ class MethodHash < Hash
12
+ include MethodKeys
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+ require 'rforce/method_keys'
2
+
3
+ module RForce
4
+ module SoapPullable
5
+ SOAP_ENVELOPE = 'soapenv:Envelope'
6
+
7
+ # Split off the local name portion of an XML tag.
8
+ def local(tag)
9
+ first, second = tag.split ':'
10
+ return first if second.nil?
11
+ @namespaces.include?(first) ? second : tag
12
+ end
13
+
14
+ def tag_start(name, attrs)
15
+ # For shorter hash keys, we can strip any namespaces of the SOAP
16
+ # envelope tag from the tags inside it.
17
+ if name == SOAP_ENVELOPE
18
+ @namespaces = attrs.keys.grep(/xmlns:/).map {|k| k.split(':').last}
19
+ return
20
+ end
21
+
22
+ @stack.push MethodHash.new
23
+ end
24
+
25
+ def text(data)
26
+ adding = data.strip.empty? ? nil : data
27
+
28
+ if adding
29
+ @current_value = (@current_value || '') + adding
30
+ end
31
+ end
32
+
33
+ def tag_end(name)
34
+ return if @done || name == SOAP_ENVELOPE
35
+
36
+ tag_name = local name
37
+ working_hash = @stack.pop
38
+
39
+ # We are either done or working on a certain depth in the current
40
+ # stack.
41
+ if @stack.empty?
42
+ @parsed = working_hash
43
+ @done = true
44
+ return
45
+ end
46
+
47
+ index = @stack.size - 1
48
+
49
+ # working_hash and @current_value have a mutually exclusive relationship.
50
+ # If the current element doesn't have a value then it means that there
51
+ # is a nested data structure. In this case then working_hash is populated
52
+ # and @current_value is nil. Conversely, if @current_value has a value
53
+ # then we do not have a nested data structure and working_hash will
54
+ # be empty.
55
+ raise 'Parser is confused' unless working_hash.empty? || @current_value.nil?
56
+
57
+ use_value = working_hash.empty? ? @current_value : working_hash
58
+ tag_sym = tag_name.to_sym
59
+ element = @stack[index][tag_sym]
60
+
61
+ if @stack[index].keys.include? tag_sym
62
+ # This is here to handle the Id value being included twice and thus
63
+ # producing an array. We skip the second instance so the array is
64
+ # not created.
65
+ #
66
+ # We also need to clear out the current value, so that the next
67
+ # tag doesn't erroneously pick up the value of the Id.
68
+ if tag_name == 'Id'
69
+ @current_value = nil
70
+ return
71
+ end
72
+
73
+ # We are here because the name of our current element is one that
74
+ # already exists in the hash. If this is the first encounter with
75
+ # the duplicate tag_name then we convert the existing value to an
76
+ # array otherwise we push the value we are working with and add it
77
+ # to the existing array.
78
+ if element.is_a?( Array )
79
+ element << use_value
80
+ else
81
+ @stack[index][tag_sym] = [element, use_value]
82
+ end
83
+ else
84
+ # We are here because the name of our current element has not been
85
+ # assigned yet.
86
+ @stack[index][tag_sym] = use_value
87
+ end
88
+
89
+ # We are done with the current tag so reset the data for the next one
90
+ @current_value = nil
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,7 @@
1
+ require 'rforce/soap_response_nokogiri'
2
+
3
+
4
+ module RForce
5
+ # always use Nokogiri because the other ones seem to be broken
6
+ SoapResponse = RForce::SoapResponseNokogiri
7
+ end
@@ -0,0 +1,35 @@
1
+ require 'xml/parser'
2
+ require 'rforce/soap_pullable'
3
+
4
+ module RForce
5
+ class SoapResponseExpat
6
+ include SoapPullable
7
+
8
+ def initialize(content)
9
+ @content = content
10
+ end
11
+
12
+ def parse
13
+ @current_value = nil
14
+ @stack = []
15
+ @parsed = MethodHash.new
16
+ @done = false
17
+ @namespaces = []
18
+
19
+ XML::Parser.new.parse(@content) do |type, name, data|
20
+ case type
21
+ when XML::Parser::START_ELEM then
22
+ tag_start name, data
23
+ when XML::Parser::CDATA then
24
+ text data
25
+ when XML::Parser::END_ELEM then
26
+ tag_end name
27
+ end
28
+
29
+ break if @done
30
+ end
31
+
32
+ @parsed
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,75 @@
1
+ require 'hpricot'
2
+ require 'cgi'
3
+
4
+
5
+ module RForce
6
+ class SoapResponseHpricot
7
+ # Parses an XML string into structured data.
8
+ def initialize(content)
9
+ @content = content
10
+ end
11
+
12
+ # Digests an XML DOM node into nested Ruby types.
13
+ def parse
14
+ document = Hpricot.XML(@content)
15
+ node = document % 'soapenv:Body'
16
+ self.class.node_to_ruby node
17
+ end
18
+
19
+ private
20
+
21
+ def self.unescapeXML(string)
22
+ CGI.unescapeHTML(string).gsub("&apos;", "'")
23
+ end
24
+
25
+ def self.node_to_ruby(node)
26
+ # Convert text nodes into simple strings.
27
+ children = (node.children || []).reject do |c|
28
+ c.is_a?(Hpricot::Text) && c.to_s.strip.empty?
29
+ end
30
+
31
+ if node.is_a?(Hpricot::Text)
32
+ return SoapResponseHpricot.unescapeXML(node.inspect[1..-2])
33
+ end
34
+
35
+ if children.first.is_a?(Hpricot::Text)
36
+ return SoapResponseHpricot.unescapeXML(children.first.inspect[1..-2])
37
+ end
38
+
39
+ # Convert nodes with children into MethodHashes.
40
+ elements = MethodHash.new
41
+
42
+ # Add all the element's children to the hash.
43
+ children.each do |e|
44
+ next if e.is_a?(Hpricot::Text) && e.to_s.strip.empty?
45
+ name = e.name
46
+
47
+ if name.include? ':'
48
+ name = name.split(':').last
49
+ end
50
+
51
+ name = name.to_sym
52
+
53
+ case elements[name]
54
+ # The most common case: unique child element tags.
55
+ when NilClass then elements[name] = node_to_ruby(e)
56
+
57
+ # Non-unique child elements become arrays:
58
+
59
+ # We've already created the array: just
60
+ # add the element.
61
+ when Array then elements[name] << node_to_ruby(e)
62
+
63
+ # We haven't created the array yet: do so,
64
+ # then put the existing element in, followed
65
+ # by the new one.
66
+ else
67
+ elements[name] = [elements[name]]
68
+ elements[name] << node_to_ruby(e)
69
+ end
70
+ end
71
+
72
+ return elements.empty? ? nil : elements
73
+ end
74
+ end
75
+ end