rforcedotcom 20.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,2 @@
1
+ == 20 2011-02-04
2
+ * Forked from Rforce-0.4.1 gem to allow easier maintenance of user-agent
data/README.txt ADDED
@@ -0,0 +1,77 @@
1
+ = rforce
2
+
3
+ * todo: update to github url and point to rubyforce.org
4
+
5
+ == DESCRIPTION:
6
+
7
+ RForcedotcom is a simple, usable binding to the SalesForce API.
8
+ Code is forked from RForce - all credit due to the originators!
9
+
10
+ This gem should be patched for each release of the Force.com APIs. Specifically
11
+ the bindings.rb file should be updates to specify the correct API version
12
+ in the user-agent header.
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ Rather than enforcing adherence to the sforce.com schema, RForce assumes you are familiar with the API. Ruby method names become SOAP method names. Nested Ruby hashes become nested XML elements.
17
+
18
+ == SYNOPSIS:
19
+
20
+ binding = RForce::Binding.new \
21
+ 'https://www.salesforce.com/services/Soap/u/20.0'
22
+
23
+ binding.login \
24
+ 'email', 'password_with_token'
25
+
26
+ answer = binding.search \
27
+ :searchString =>
28
+ 'find {McFakerson Co} in name fields returning account(id)'
29
+
30
+ account = answer.searchResponse.result.searchRecords.record
31
+ account = account.first if account.is_a? Array
32
+
33
+ account_id = account.Id
34
+ account_id = account_id.first if account_id.is_a? Array
35
+
36
+ opportunity = [
37
+ :type, 'Opportunity',
38
+ :accountId, account_id,
39
+ :amount, '10.00',
40
+ :name, 'Fakey McFakerson',
41
+ :closeDate, '2008-07-04',
42
+ :stageName, 'Closed Won'
43
+ ]
44
+
45
+ binding.create :sObject => opportunity
46
+
47
+ == REQUIREMENTS:
48
+
49
+ * Builder gem
50
+ * A SalesForce Enterprise or Developer account
51
+
52
+ == INSTALL:
53
+
54
+ * sudo gem install rforce
55
+
56
+ == LICENSE:
57
+
58
+ Copyright (c) 2005-2010 Ian Dees and contributors
59
+
60
+ Permission is hereby granted, free of charge, to any person obtaining
61
+ a copy of this software and associated documentation files (the
62
+ 'Software'), to deal in the Software without restriction, including
63
+ without limitation the rights to use, copy, modify, merge, publish,
64
+ distribute, sublicense, and/or sell copies of the Software, and to
65
+ permit persons to whom the Software is furnished to do so, subject to
66
+ the following conditions:
67
+
68
+ The above copyright notice and this permission notice shall be
69
+ included in all copies or substantial portions of the Software.
70
+
71
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
72
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
73
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
74
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
75
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
76
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
77
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ $:.unshift './lib'
4
+
5
+ require 'rubygems'
6
+ require 'hoe'
7
+ require 'rforce/version'
8
+
9
+ Hoe.new('rforce', RForce::VERSION) do |p|
10
+ p.developer 'Ian Dees', 'undees@gmail.com'
11
+ p.extra_deps = [['builder', '>= 2.0.0'], ['facets', '>= 2.4']]
12
+ p.extra_dev_deps = [['rspec', '>= 1.3']]
13
+ p.remote_rdoc_dir = ''
14
+ p.rspec_options = ['-rubygems', '--options', 'spec/spec.opts']
15
+ end
16
+
17
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
18
+
19
+ # vim: syntax=Ruby
data/lib/rforce.rb ADDED
@@ -0,0 +1,87 @@
1
+ =begin
2
+ Copyright (c) 2005-2010 Ian Dees and contributors
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ =end
22
+
23
+ # RForce is a simple Ruby binding to the SalesForce CRM system.
24
+ # Rather than enforcing adherence to the sforce.com schema,
25
+ # RForce assumes you are familiar with the API. Ruby method names
26
+ # become SOAP method names. Nested Ruby hashes become nested
27
+ # XML elements.
28
+ #
29
+ # Example:
30
+ #
31
+ # binding = RForce::Binding.new 'https://www.salesforce.com/services/Soap/u/10.0'
32
+ # binding.login 'username', 'password'
33
+ # answer = binding.search(
34
+ # :searchString =>
35
+ # 'find {Some Account Name} in name fields returning account(id)')
36
+ # account_id = answer.searchResponse.result.searchRecords.record.Id
37
+ #
38
+ # opportunity = {
39
+ # :accountId => account_id,
40
+ # :amount => "10.00",
41
+ # :name => "New sale",
42
+ # :closeDate => "2005-09-01",
43
+ # :stageName => "Closed Won"
44
+ # }
45
+ #
46
+ # binding.create 'sObject {"xsi:type" => "Opportunity"}' => opportunity
47
+ #
48
+
49
+ require 'rforce/binding'
50
+ require 'rforce/soap_response'
51
+
52
+ module RForce
53
+ # Expand Ruby data structures into XML.
54
+ def expand(builder, args, xmlns = nil)
55
+ # Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
56
+ if (args.class == Array)
57
+ args.each_index{|i| args[i, 2] = [args[i, 2]]}
58
+ end
59
+
60
+ args.each do |key, value|
61
+ attributes = xmlns ? {:xmlns => xmlns} : {}
62
+
63
+ # If the XML tag requires attributes,
64
+ # the tag name will contain a space
65
+ # followed by a string representation
66
+ # of a hash of attributes.
67
+ #
68
+ # e.g. 'sObject {"xsi:type" => "Opportunity"}'
69
+ # becomes <sObject xsi:type="Opportunity>...</sObject>
70
+ if key.is_a? String
71
+ key, modifier = key.split(' ', 2)
72
+
73
+ attributes.merge!(eval(modifier)) if modifier
74
+ end
75
+
76
+ # Create an XML element and fill it with this
77
+ # value's sub-items.
78
+ case value
79
+ when Hash, Array then
80
+ builder.tag!(key, attributes) do expand builder, value; end
81
+
82
+ when String then
83
+ builder.tag!(key, attributes) { builder.text! value }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,212 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+ require 'zlib'
4
+ require 'stringio'
5
+ require 'builder'
6
+
7
+
8
+ module RForce
9
+ # Implements the connection to the SalesForce server.
10
+ class Binding
11
+ include RForce
12
+
13
+ DEFAULT_BATCH_SIZE = 2000
14
+ attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
15
+ :trigger_other_email, :trigger_auto_response_email
16
+
17
+ # Fill in the guts of this typical SOAP envelope
18
+ # with the session ID and the body of the SOAP request.
19
+ Envelope = <<-HERE
20
+ <?xml version="1.0" encoding="utf-8" ?>
21
+ <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
22
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
23
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
24
+ xmlns:partner="urn:partner.soap.sforce.com">
25
+ xmlns:spartner="urn:sobject.partner.soap.sforce.com">
26
+ <soap:Header>
27
+ <partner:SessionHeader soap:mustUnderstand='1'>
28
+ <partner:sessionId>%s</partner:sessionId>
29
+ </partner:SessionHeader>
30
+ <partner:QueryOptions soap:mustUnderstand='1'>
31
+ <partner:batchSize>%d</partner:batchSize>
32
+ </partner:QueryOptions>
33
+ %s
34
+ </soap:Header>
35
+ <soap:Body>
36
+ %s
37
+ </soap:Body>
38
+ </soap:Envelope>
39
+ HERE
40
+
41
+ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
42
+ AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
43
+ MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
44
+ ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
45
+
46
+ # Connect to the server securely.
47
+ def initialize(url, sid = nil)
48
+ init_server(url)
49
+
50
+ @session_id = sid
51
+ @batch_size = DEFAULT_BATCH_SIZE
52
+ end
53
+
54
+
55
+ def show_debug
56
+ ENV['SHOWSOAP'] == 'true'
57
+ end
58
+
59
+
60
+ def init_server(url)
61
+ @url = URI.parse(url)
62
+ @server = Net::HTTP.new(@url.host, @url.port)
63
+ @server.use_ssl = @url.scheme == 'https'
64
+ @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
65
+
66
+ # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
67
+ @server.set_debug_output $stderr if show_debug
68
+ end
69
+
70
+
71
+ # Log in to the server and remember the session ID
72
+ # returned to us by SalesForce.
73
+ def login(user, password)
74
+ @user = user
75
+ @password = password
76
+
77
+ response = call_remote(:login, [:username, user, :password, password])
78
+
79
+ raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
80
+
81
+ result = response[:loginResponse][:result]
82
+ @session_id = result[:sessionId]
83
+
84
+ init_server(result[:serverUrl])
85
+
86
+ response
87
+ end
88
+
89
+
90
+ # Call a method on the remote server. Arguments can be
91
+ # a hash or (if order is important) an array of alternating
92
+ # keys and values.
93
+ def call_remote(method, args)
94
+
95
+ urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", @url.path]
96
+
97
+ # Create XML text from the arguments.
98
+ expanded = ''
99
+ @builder = Builder::XmlMarkup.new(:target => expanded)
100
+ expand(@builder, {method => args}, urn)
101
+
102
+ extra_headers = ""
103
+ extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
104
+ extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
105
+ extra_headers << MruHeader if update_mru
106
+ extra_headers << (ClientIdHeader % client_id) if client_id
107
+
108
+ if trigger_user_email or trigger_other_email or trigger_auto_response_email
109
+ extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
110
+
111
+ extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
112
+ extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
113
+ extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
114
+
115
+ extra_headers << '</partner:EmailHeader>'
116
+ end
117
+
118
+ # Fill in the blanks of the SOAP envelope with our
119
+ # session ID and the expanded XML of our request.
120
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
121
+
122
+ # reset the batch size for the next request
123
+ @batch_size = DEFAULT_BATCH_SIZE
124
+
125
+ # gzip request
126
+ request = encode(request)
127
+
128
+ headers = {
129
+ 'Connection' => 'Keep-Alive',
130
+ 'Content-Type' => 'text/xml',
131
+ 'SOAPAction' => '""',
132
+ 'User-Agent' => 'salesforce-toolkit-ruby/20.0'
133
+ }
134
+
135
+ unless show_debug
136
+ headers['Accept-Encoding'] = 'gzip'
137
+ headers['Content-Encoding'] = 'gzip'
138
+ end
139
+
140
+ # Send the request to the server and read the response.
141
+ response = @server.post2(soap_url, request.lstrip, headers)
142
+
143
+ # decode if we have encoding
144
+ content = decode(response)
145
+
146
+ # Check to see if INVALID_SESSION_ID was raised and try to relogin in
147
+ if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
148
+ login(@user, @password)
149
+
150
+ # repackage and rencode request with the new session id
151
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
152
+ request = encode(request)
153
+
154
+ # Send the request to the server and read the response.
155
+ response = @server.post2(soap_url, request.lstrip, headers)
156
+
157
+ content = decode(response)
158
+ end
159
+
160
+ SoapResponse.new(content).parse
161
+ end
162
+
163
+
164
+ # decode gzip
165
+ def decode(response)
166
+ encoding = response['Content-Encoding']
167
+
168
+ # return body if no encoding
169
+ if !encoding then return response.body end
170
+
171
+ # decode gzip
172
+ case encoding.strip
173
+ when 'gzip' then
174
+ begin
175
+ gzr = Zlib::GzipReader.new(StringIO.new(response.body))
176
+ decoded = gzr.read
177
+ ensure
178
+ gzr.close
179
+ end
180
+ decoded
181
+ else
182
+ response.body
183
+ end
184
+ end
185
+
186
+
187
+ # encode gzip
188
+ def encode(request)
189
+ return request if show_debug
190
+
191
+ begin
192
+ ostream = StringIO.new
193
+ gzw = Zlib::GzipWriter.new(ostream)
194
+ gzw.write(request)
195
+ ostream.string
196
+ ensure
197
+ gzw.close
198
+ end
199
+ end
200
+
201
+
202
+ # Turns method calls on this object into remote SOAP calls.
203
+ def method_missing(method, *args)
204
+ unless args.size == 1 && [Hash, Array].include?(args[0].class)
205
+ raise 'Expected 1 Hash or Array argument'
206
+ end
207
+
208
+ call_remote method, args[0]
209
+ end
210
+ end
211
+ end
212
+
@@ -0,0 +1,93 @@
1
+ require 'facets/openhash'
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 OpenHash.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