ginjo-rfm 1.4.2

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.
data/lib/rfm/server.rb ADDED
@@ -0,0 +1,395 @@
1
+ require 'net/https'
2
+ require 'cgi'
3
+ module Rfm
4
+ # This class represents a single FileMaker server. It is initialized with basic
5
+ # connection information, including the hostname, port number, and default database
6
+ # account name and password.
7
+ #
8
+ # Note: The host and port number refer to the FileMaker Web Publishing Engine, which
9
+ # must be installed and configured in order to use RFM. It may not actually be running
10
+ # on the same server computer as FileMaker Server itself. See your FileMaker Server
11
+ # or FileMaker Server Advanced documentation for information about configuring a Web
12
+ # Publishing Engine.
13
+ #
14
+ # =Accessing Databases
15
+ #
16
+ # Typically, you access a Database object from the Server like this:
17
+ #
18
+ # myDatabase = myServer["Customers"]
19
+ #
20
+ # This code gets the Database object representing the Customers object.
21
+ #
22
+ # Note: RFM does not talk to the server when you retrieve a database object in this way. Instead, it
23
+ # simply assumes you know what you're talking about. If the database you specify does not exist, you
24
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object you get
25
+ # from this database. This makes debugging a little less convenient, but it would introduce too much
26
+ # overhead to hit the server at this point.
27
+ #
28
+ # The Server object has a +db+ attribute that provides alternate access to Database objects. It acts
29
+ # like a hash of Database objects, one for each accessible database on the server. So, for example, you
30
+ # can do this if you want to print out a list of all databses on the server:
31
+ #
32
+ # myServer.db.each {|database|
33
+ # puts database.name
34
+ # }
35
+ #
36
+ # The Server::db attribute is actually a DbFactory object, although it subclasses hash, so it should work
37
+ # in all the ways you expect. Note, though, that it is completely empty until the first time you attempt
38
+ # to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of databases, and
39
+ # constructs a Database object for each one. In other words, it incurrs no overhead until you use it.
40
+ #
41
+ # =Attributes
42
+ #
43
+ # In addition to the +db+ attribute, Server has a few other useful attributes:
44
+ #
45
+ # * *host_name* is the host name this server points to
46
+ # * *port* is the port number this server communicates on
47
+ # * *state* is a hash of all server options used to initialize this server
48
+
49
+
50
+
51
+ # The Database object represents a single FileMaker Pro database. When you retrieve a Database
52
+ # object from a server, its account name and password are set to the account name and password you
53
+ # used when initializing the Server object. You can override this of course:
54
+ #
55
+ # myDatabase = myServer["Customers"]
56
+ # myDatabase.account_name = "foo"
57
+ # myDatabase.password = "bar"
58
+ #
59
+ # =Accessing Layouts
60
+ #
61
+ # All interaction with FileMaker happens through a Layout object. You can get a Layout object
62
+ # from the Database object like this:
63
+ #
64
+ # myLayout = myDatabase["Details"]
65
+ #
66
+ # This code gets the Layout object representing the layout called Details in the database.
67
+ #
68
+ # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
69
+ # simply assumes you know what you're talking about. If the layout you specify does not exist, you
70
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
71
+ # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
72
+ # overhead to hit the server at this point.
73
+ #
74
+ # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
75
+ # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
76
+ # can do this if you want to print out a list of all layouts:
77
+ #
78
+ # myDatabase.layout.each {|layout|
79
+ # puts layout.name
80
+ # }
81
+ #
82
+ # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
83
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
84
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
85
+ # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
86
+ #
87
+ # =Accessing Scripts
88
+ #
89
+ # If for some reason you need to enumerate the scripts in a database, you can do so:
90
+ #
91
+ # myDatabase.script.each {|script|
92
+ # puts script.name
93
+ # }
94
+ #
95
+ # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
96
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
97
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
98
+ # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
99
+ #
100
+ # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
101
+ #
102
+ # =Attributes
103
+ #
104
+ # In addition to the +layout+ attribute, Server has a few other useful attributes:
105
+ #
106
+ # * *server* is the Server object this database comes from
107
+ # * *name* is the name of this database
108
+ # * *state* is a hash of all server options used to initialize this server
109
+ class Server
110
+ #
111
+ # To create a Server object, you typically need at least a host name:
112
+ #
113
+ # myServer = Rfm::Server.new({:host => 'my.host.com'})
114
+ #
115
+ # Several other options are supported:
116
+ #
117
+ # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
118
+ #
119
+ # * *port* the port number the WPE is listening no (defaults to 80 unless *ssl* +true+ which sets it to 443)
120
+ #
121
+ # * *account_name* the default account name to log in to databases with (you can also supply a
122
+ # account name on a per-database basis if necessary)
123
+ #
124
+ # * *password* the default password to log in to databases with (you can also supplly a password
125
+ # on a per-databases basis if necessary)
126
+ #
127
+ # * *log_actions* when +true+, RFM logs all action URLs that are sent to FileMaker server to stderr
128
+ # (defaults to +false+)
129
+ #
130
+ # * *log_responses* when +true+, RFM logs all raw XML responses (including headers) from FileMaker to
131
+ # stderr (defaults to +false+)
132
+ #
133
+ # * *warn_on_redirect* normally, RFM prints a warning to stderr if the Web Publishing Engine redirects
134
+ # (this can usually be fixed by using a different host name, which speeds things up); if you *don't*
135
+ # want this warning printed, set +warn_on_redirect+ to +true+
136
+ #
137
+ # * *raise_on_401* although RFM raises error when FileMaker returns error responses, it typically
138
+ # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
139
+ # prefer a raised error when a find produces no errors, set this option to +true+
140
+ #
141
+ #SSL Options (SSL AND CERTIFICATE VERIFICATION ARE ON BY DEFAULT):
142
+ #
143
+ # * *ssl* +false+ if you want to turn SSL (HTTPS) off when connecting to connect to FileMaker (default is +true+)
144
+ #
145
+ # If you are using SSL and want to verify the certificate use the following options:
146
+ #
147
+ # * *root_cert* +false+ if you do not want to verify your SSL session (default is +true+).
148
+ # You will want to turn this off if you are using a self signed certificate and do not have a certificate authority cert file.
149
+ # If you choose this option you will need to provide a cert *root_cert_name* and *root_cert_path* (if not in root directory).
150
+ #
151
+ # * *root_cert_name* name of pem file for certificate verification (Root cert from certificate authority who issued certificate.
152
+ # If self signed certificate do not use this option!!). You can download the entire bundle of CA Root Certificates
153
+ # from http://curl.haxx.se/ca/cacert.pem. Place the pem file in config directory.
154
+ #
155
+ # * *root_cert_path* path to cert file. (defaults to '/' if no path given)
156
+ #
157
+ #Configuration Examples:
158
+ #
159
+ # Example to turn off SSL:
160
+ #
161
+ # myServer = Rfm::Server.new({
162
+ # :host => 'localhost',
163
+ # :account_name => 'sample',
164
+ # :password => '12345',
165
+ # :ssl => false
166
+ # })
167
+ #
168
+ # Example using SSL without *root_cert*:
169
+ #
170
+ # myServer = Rfm::Server.new({
171
+ # :host => 'localhost',
172
+ # :account_name => 'sample',
173
+ # :password => '12345',
174
+ # :root_cert => false
175
+ # })
176
+ #
177
+ # Example using SSL with *root_cert* at file root:
178
+ #
179
+ # myServer = Rfm::Server.new({
180
+ # :host => 'localhost',
181
+ # :account_name => 'sample',
182
+ # :password => '12345',
183
+ # :root_cert_name => 'example.pem'
184
+ # })
185
+ #
186
+ # Example using SSL with *root_cert* specifying *root_cert_path*:
187
+ #
188
+ # myServer = Rfm::Server.new({
189
+ # :host => 'localhost',
190
+ # :account_name => 'sample',
191
+ # :password => '12345',
192
+ # :root_cert_name => 'example.pem'
193
+ # :root_cert_path => '/usr/cert_file/'
194
+ # })
195
+
196
+ def initialize(options)
197
+ @state = {
198
+ :host => 'localhost',
199
+ :port => 80,
200
+ :ssl => true,
201
+ :root_cert => true,
202
+ :root_cert_name => '',
203
+ :root_cert_path => '/',
204
+ :account_name => '',
205
+ :password => '',
206
+ :log_actions => false,
207
+ :log_responses => false,
208
+ :warn_on_redirect => true,
209
+ :raise_on_401 => false,
210
+ :timeout => 60
211
+ }.merge(options)
212
+
213
+ @state.freeze
214
+
215
+ @host_name = @state[:host]
216
+ @scheme = @state[:ssl] ? "https" : "http"
217
+ @port = @state[:ssl] && options[:port].nil? ? 443 : @state[:port]
218
+
219
+ @db = Rfm::Factory::DbFactory.new(self)
220
+ end
221
+
222
+ # Access the database object representing a database on the server. For example:
223
+ #
224
+ # myServer['Customers']
225
+ #
226
+ # would return a Database object representing the _Customers_
227
+ # database on the server.
228
+ #
229
+ # Note: RFM never talks to the server until you perform an action. The database object
230
+ # returned is created on the fly and assumed to refer to a valid database, but you will
231
+ # get no error at this point if the database you access doesn't exist. Instead, you'll
232
+ # receive an error when you actually try to perform some action on a layout from this
233
+ # database.
234
+ def [](dbname)
235
+ self.db[dbname]
236
+ end
237
+
238
+ attr_reader :db, :host_name, :port, :scheme, :state
239
+
240
+ # Performs a raw FileMaker action. You will generally not call this method directly, but it
241
+ # is exposed in case you need to do something "under the hood."
242
+ #
243
+ # The +action+ parameter is any valid FileMaker web url action. For example, +-find+, +-finadny+ etc.
244
+ #
245
+ # The +args+ parameter is a hash of arguments to be included in the action url. It will be serialized
246
+ # and url-encoded appropriately.
247
+ #
248
+ # The +options+ parameter is a hash of RFM-specific options, which correspond to the more esoteric
249
+ # FileMaker URL parameters. They are exposed separately because they can also be passed into
250
+ # various methods on the Layout object, which is a much more typical way of sending an action to
251
+ # FileMaker.
252
+ #
253
+ # This method returns the Net::HTTP response object representing the response from FileMaker.
254
+ #
255
+ # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
256
+ # "Customers" database whose first name is "Bill" you might do this:
257
+ #
258
+ # response = myServer.connect(
259
+ # '-find',
260
+ # {
261
+ # "-db" => "Customers",
262
+ # "-lay" => "Details",
263
+ # "First Name" => "Bill"
264
+ # },
265
+ # { :max_records => 20 }
266
+ # )
267
+ def connect(account_name, password, action, args, options = {})
268
+ post = args.merge(expand_options(options)).merge({action => ''})
269
+ http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
270
+ end
271
+
272
+ def load_layout(layout)
273
+ post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
274
+ resp = http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
275
+ remove_namespace(resp.body)
276
+ end
277
+
278
+ # Removes namespace from fmpxmllayout, so xpath will work
279
+ def remove_namespace(xml)
280
+ xml.gsub('xmlns="http://www.filemaker.com/fmpxmllayout"', '')
281
+ end
282
+
283
+ private
284
+
285
+ def http_fetch(host_name, port, path, account_name, password, post_data, limit=10)
286
+ raise Rfm::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.") if limit == 0
287
+
288
+ if @state[:log_actions] == true
289
+ qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
290
+ warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
291
+ end
292
+
293
+ request = Net::HTTP::Post.new(path)
294
+ request.basic_auth(account_name, password)
295
+ request.set_form_data(post_data)
296
+
297
+ response = Net::HTTP.new(host_name, port)
298
+ #ADDED LONG TIMEOUT TIMOTHY TING 05/12/2011
299
+ response.open_timeout = response.read_timeout = @state[:timeout]
300
+ if @state[:ssl]
301
+ response.use_ssl = true
302
+ if @state[:root_cert]
303
+ response.verify_mode = OpenSSL::SSL::VERIFY_PEER
304
+ response.ca_file = File.join(@state[:root_cert_path], @state[:root_cert_name])
305
+ else
306
+ response.verify_mode = OpenSSL::SSL::VERIFY_NONE
307
+ end
308
+ end
309
+
310
+ response = response.start { |http| http.request(request) }
311
+ if @state[:log_responses] == true
312
+ response.to_hash.each { |key, value| warn "#{key}: #{value}" }
313
+ warn response.body
314
+ end
315
+
316
+ case response
317
+ when Net::HTTPSuccess
318
+ response
319
+ when Net::HTTPRedirection
320
+ if @state[:warn_on_redirect]
321
+ warn "The web server redirected to " + response['location'] +
322
+ ". You should revise your connection hostname or fix your server configuration if possible to improve performance."
323
+ end
324
+ newloc = URI.parse(response['location'])
325
+ http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
326
+ when Net::HTTPUnauthorized
327
+ msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
328
+ raise Rfm::AuthenticationError.new(msg)
329
+ when Net::HTTPNotFound
330
+ msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
331
+ raise Rfm::CommunicationError.new(msg)
332
+ else
333
+ msg = "Unexpected response from server: #{response.code} (#{response.class.to_s}). Unable to communicate with the Web Publishing Engine."
334
+ raise Rfm::CommunicationError.new(msg)
335
+ end
336
+ end
337
+
338
+ def expand_options(options)
339
+ result = {}
340
+ options.each do |key,value|
341
+ case key
342
+ when :max_records
343
+ result['-max'] = value
344
+ when :skip_records
345
+ result['-skip'] = value
346
+ when :sort_field
347
+ if value.kind_of? Array
348
+ raise Rfm::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
349
+ value.each_index { |i| result["-sortfield.#{i+1}"] = value[i] }
350
+ else
351
+ result["-sortfield.1"] = value
352
+ end
353
+ when :sort_order
354
+ if value.kind_of? Array
355
+ raise Rfm::ParameterError.new(":sort_order can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
356
+ value.each_index { |i| result["-sortorder.#{i+1}"] = value[i] }
357
+ else
358
+ result["-sortorder.1"] = value
359
+ end
360
+ when :post_script
361
+ if value.class == Array
362
+ result['-script'] = value[0]
363
+ result['-script.param'] = value[1]
364
+ else
365
+ result['-script'] = value
366
+ end
367
+ when :pre_find_script
368
+ if value.class == Array
369
+ result['-script.prefind'] = value[0]
370
+ result['-script.prefind.param'] = value[1]
371
+ else
372
+ result['-script.presort'] = value
373
+ end
374
+ when :pre_sort_script
375
+ if value.class == Array
376
+ result['-script.presort'] = value[0]
377
+ result['-script.presort.param'] = value[1]
378
+ else
379
+ result['-script.presort'] = value
380
+ end
381
+ when :response_layout
382
+ result['-lay.response'] = value
383
+ when :logical_operator
384
+ result['-lop'] = value
385
+ when :modification_id
386
+ result['-modid'] = value
387
+ else
388
+ raise Rfm::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
389
+ end
390
+ end
391
+ return result
392
+ end
393
+
394
+ end
395
+ end
@@ -0,0 +1,10 @@
1
+ module Rfm
2
+ class CaseInsensitiveHash < Hash
3
+ def []=(key, value)
4
+ super(key.to_s.downcase, value)
5
+ end
6
+ def [](key)
7
+ super(key.to_s.downcase)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,85 @@
1
+ # The classes in this module are used internally by RFM and are not intended for outside
2
+ # use.
3
+ #
4
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
5
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
6
+ # License:: See MIT-LICENSE for details
7
+
8
+
9
+ module Rfm
10
+ module Factory # :nodoc: all
11
+ class DbFactory < Rfm::CaseInsensitiveHash
12
+
13
+ def initialize(server)
14
+ @server = server
15
+ @loaded = false
16
+ end
17
+
18
+ def [](dbname)
19
+ super or (self[dbname] = Rfm::Database.new(dbname, @server))
20
+ end
21
+
22
+ def all
23
+ if !@loaded
24
+ Rfm::Resultset.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-dbnames', {}).body, nil).each {|record|
25
+ name = record['DATABASE_NAME']
26
+ self[name] = Rfm::Database.new(name, @server) if self[name] == nil
27
+ }
28
+ @loaded = true
29
+ end
30
+ self.keys
31
+ end
32
+
33
+ end
34
+
35
+ class LayoutFactory < Rfm::CaseInsensitiveHash
36
+
37
+ def initialize(server, database)
38
+ @server = server
39
+ @database = database
40
+ @loaded = false
41
+ end
42
+
43
+ def [](layout_name)
44
+ super or (self[layout_name] = Rfm::Layout.new(layout_name, @database))
45
+ end
46
+
47
+ def all
48
+ if !@loaded
49
+ Rfm::Resultset.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-layoutnames', {"-db" => @database.name}).body, nil).each {|record|
50
+ name = record['LAYOUT_NAME']
51
+ self[name] = Rfm::Layout.new(name, @database) if self[name] == nil
52
+ }
53
+ @loaded = true
54
+ end
55
+ self.keys
56
+ end
57
+
58
+ end
59
+
60
+ class ScriptFactory < Rfm::CaseInsensitiveHash
61
+
62
+ def initialize(server, database)
63
+ @server = server
64
+ @database = database
65
+ @loaded = false
66
+ end
67
+
68
+ def [](script_name)
69
+ super or (self[script_name] = Rfm::Metadata::Script.new(script_name, @database))
70
+ end
71
+
72
+ def all
73
+ if !@loaded
74
+ Rfm::Resultset.new(@server, @server.connect(@server.state[:account_name], @server.state[:password], '-scriptnames', {"-db" => @database.name}).body, nil).each {|record|
75
+ name = record['SCRIPT_NAME']
76
+ self[name] = Rfm::Metadata::Script.new(name, @database) if self[name] == nil
77
+ }
78
+ @loaded = true
79
+ end
80
+ self.keys
81
+ end
82
+
83
+ end
84
+ end
85
+ end