aq1018-rforce 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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