arachni 0.2.4 → 0.3
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/CHANGELOG.md +33 -0
- data/README.md +2 -4
- data/Rakefile +15 -4
- data/bin/arachni +0 -0
- data/bin/arachni_web +0 -0
- data/bin/arachni_web_autostart +0 -0
- data/bin/arachni_xmlrpc +0 -0
- data/bin/arachni_xmlrpcd +0 -0
- data/bin/arachni_xmlrpcd_monitor +0 -0
- data/lib/arachni.rb +1 -1
- data/lib/framework.rb +36 -6
- data/lib/http.rb +12 -5
- data/lib/module/auditor.rb +482 -59
- data/lib/module/base.rb +17 -0
- data/lib/module/manager.rb +26 -2
- data/lib/module/trainer.rb +1 -12
- data/lib/module/utilities.rb +12 -0
- data/lib/parser/auditable.rb +8 -3
- data/lib/parser/elements.rb +11 -0
- data/lib/parser/page.rb +3 -1
- data/lib/parser/parser.rb +130 -18
- data/lib/rpc/xml/server/dispatcher.rb +21 -0
- data/lib/spider.rb +141 -82
- data/lib/ui/cli/cli.rb +2 -3
- data/lib/ui/web/addon_manager.rb +273 -0
- data/lib/ui/web/addons/autodeploy.rb +172 -0
- data/lib/ui/web/addons/autodeploy/lib/manager.rb +291 -0
- data/lib/ui/web/addons/autodeploy/views/index.erb +124 -0
- data/lib/ui/web/addons/sample.rb +78 -0
- data/lib/ui/web/addons/sample/views/index.erb +4 -0
- data/lib/ui/web/addons/scheduler.rb +139 -0
- data/lib/ui/web/addons/scheduler/views/index.erb +131 -0
- data/lib/ui/web/addons/scheduler/views/options.erb +93 -0
- data/lib/ui/web/dispatcher_manager.rb +80 -13
- data/lib/ui/web/instance_manager.rb +87 -0
- data/lib/ui/web/scheduler.rb +166 -0
- data/lib/ui/web/server.rb +142 -202
- data/lib/ui/web/server/public/js/jquery-ui-timepicker.js +985 -0
- data/lib/ui/web/server/public/plugins/sample/style.css +0 -0
- data/lib/ui/web/server/public/style.css +42 -0
- data/lib/ui/web/server/views/addon.erb +15 -0
- data/lib/ui/web/server/views/addons.erb +46 -0
- data/lib/ui/web/server/views/dispatchers.erb +1 -1
- data/lib/ui/web/server/views/instance.erb +9 -11
- data/lib/ui/web/server/views/layout.erb +14 -1
- data/lib/ui/web/server/views/welcome.erb +7 -6
- data/lib/ui/web/utilities.rb +134 -0
- data/modules/audit/code_injection_timing.rb +6 -2
- data/modules/audit/code_injection_timing/payloads.txt +2 -2
- data/modules/audit/os_cmd_injection_timing.rb +7 -3
- data/modules/audit/os_cmd_injection_timing/payloads.txt +1 -1
- data/modules/audit/sqli_blind_rdiff.rb +18 -233
- data/modules/audit/sqli_blind_rdiff/payloads.txt +5 -0
- data/modules/audit/sqli_blind_timing.rb +9 -2
- data/path_extractors/anchors.rb +1 -1
- data/path_extractors/forms.rb +1 -1
- data/path_extractors/frames.rb +1 -1
- data/path_extractors/generic.rb +1 -1
- data/path_extractors/links.rb +1 -1
- data/path_extractors/meta_refresh.rb +1 -1
- data/path_extractors/scripts.rb +1 -1
- data/path_extractors/sitemap.rb +1 -1
- data/plugins/proxy/server.rb +3 -2
- data/plugins/waf_detector.rb +0 -3
- metadata +37 -34
- data/lib/anemone/cookie_store.rb +0 -35
- data/lib/anemone/core.rb +0 -371
- data/lib/anemone/exceptions.rb +0 -5
- data/lib/anemone/http.rb +0 -144
- data/lib/anemone/page.rb +0 -338
- data/lib/anemone/page_store.rb +0 -160
- data/lib/anemone/storage.rb +0 -34
- data/lib/anemone/storage/base.rb +0 -75
- data/lib/anemone/storage/exceptions.rb +0 -15
- data/lib/anemone/storage/mongodb.rb +0 -89
- data/lib/anemone/storage/pstore.rb +0 -50
- data/lib/anemone/storage/redis.rb +0 -90
- data/lib/anemone/storage/tokyo_cabinet.rb +0 -57
- data/lib/anemone/tentacle.rb +0 -40
data/lib/module/base.rb
CHANGED
@@ -115,6 +115,19 @@ class Base
|
|
115
115
|
def clean_up( )
|
116
116
|
end
|
117
117
|
|
118
|
+
#
|
119
|
+
# ABSTRACT - OPTIONAL
|
120
|
+
#
|
121
|
+
# Prevents auditting elements that have been previously
|
122
|
+
# logged by any of the modules returned by this method.
|
123
|
+
#
|
124
|
+
# @return [Array] module names
|
125
|
+
#
|
126
|
+
def redundant
|
127
|
+
# [ 'sqli', 'sqli_blind_rdiff' ]
|
128
|
+
[]
|
129
|
+
end
|
130
|
+
|
118
131
|
#
|
119
132
|
# ABSTRACT - REQUIRED
|
120
133
|
#
|
@@ -169,6 +182,10 @@ class Base
|
|
169
182
|
Arachni::Module::Manager.register_results( results )
|
170
183
|
end
|
171
184
|
|
185
|
+
def set_framework( framework )
|
186
|
+
@framework = framework
|
187
|
+
end
|
188
|
+
|
172
189
|
end
|
173
190
|
end
|
174
191
|
end
|
data/lib/module/manager.rb
CHANGED
@@ -36,7 +36,7 @@ module Module
|
|
36
36
|
# @author: Tasos "Zapotek" Laskos
|
37
37
|
# <tasos.laskos@gmail.com>
|
38
38
|
# <zapotek@segfault.gr>
|
39
|
-
# @version: 0.1
|
39
|
+
# @version: 0.1.1
|
40
40
|
#
|
41
41
|
class Manager < Arachni::ComponentManager
|
42
42
|
|
@@ -48,7 +48,8 @@ class Manager < Arachni::ComponentManager
|
|
48
48
|
def initialize( opts )
|
49
49
|
super( opts.dir['modules'], Arachni::Modules )
|
50
50
|
@opts = opts
|
51
|
-
@@results
|
51
|
+
@@results = []
|
52
|
+
@@issue_set = Set.new
|
52
53
|
end
|
53
54
|
|
54
55
|
#
|
@@ -60,8 +61,31 @@ class Manager < Arachni::ComponentManager
|
|
60
61
|
#
|
61
62
|
def self.register_results( results )
|
62
63
|
@@results |= results
|
64
|
+
results.each { |issue| @@issue_set << self.issue_set_id_from_issue( issue ) }
|
63
65
|
end
|
64
66
|
|
67
|
+
def self.issue_set_id_from_issue( issue )
|
68
|
+
issue_url = URI( issue.url )
|
69
|
+
issue_url_str = issue_url.scheme + "://" + issue_url.host + issue_url.path
|
70
|
+
return "#{issue.mod_name}:#{issue.elem}:#{issue.var}:#{issue_url_str}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.issue_set_id_from_elem( mod_name, elem )
|
74
|
+
elem_url = URI( elem.action )
|
75
|
+
elem_url_str = elem_url.scheme + "://" + elem_url.host + elem_url.path
|
76
|
+
|
77
|
+
return "#{mod_name}:#{elem.type}:#{elem.altered}:#{elem_url_str}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.issue_set
|
81
|
+
@@issue_set
|
82
|
+
end
|
83
|
+
|
84
|
+
def issue_set
|
85
|
+
@@issue_set
|
86
|
+
end
|
87
|
+
|
88
|
+
|
65
89
|
#
|
66
90
|
# Class method
|
67
91
|
#
|
data/lib/module/trainer.rb
CHANGED
@@ -27,6 +27,7 @@ class Trainer
|
|
27
27
|
|
28
28
|
include Output
|
29
29
|
include ElementDB
|
30
|
+
include Utilities
|
30
31
|
|
31
32
|
attr_writer :page
|
32
33
|
attr_accessor :http
|
@@ -70,18 +71,6 @@ class Trainer
|
|
70
71
|
|
71
72
|
end
|
72
73
|
|
73
|
-
#
|
74
|
-
# Decodes URLs to reverse multiple encodes and removes NULL characters
|
75
|
-
#
|
76
|
-
def url_sanitize( url )
|
77
|
-
|
78
|
-
while( url =~ /%/ )
|
79
|
-
url = ( URI.decode( url ).to_s.unpack( 'A*' )[0] )
|
80
|
-
end
|
81
|
-
|
82
|
-
return URI.encode( url )
|
83
|
-
end
|
84
|
-
|
85
74
|
def follow?( url )
|
86
75
|
@parser.url = @page.url
|
87
76
|
|
data/lib/module/utilities.rb
CHANGED
@@ -24,6 +24,18 @@ module Module
|
|
24
24
|
#
|
25
25
|
module Utilities
|
26
26
|
|
27
|
+
#
|
28
|
+
# Decodes URLs to reverse multiple encodes and removes NULL characters
|
29
|
+
#
|
30
|
+
def url_sanitize( url )
|
31
|
+
|
32
|
+
while( url =~ /%/ )
|
33
|
+
url = ( URI.decode( url ).to_s.unpack( 'A*' )[0] )
|
34
|
+
end
|
35
|
+
|
36
|
+
return URI.encode( url )
|
37
|
+
end
|
38
|
+
|
27
39
|
#
|
28
40
|
# Gets path from URL
|
29
41
|
#
|
data/lib/parser/auditable.rb
CHANGED
@@ -49,6 +49,11 @@ class Auditable
|
|
49
49
|
@auditor = auditor
|
50
50
|
end
|
51
51
|
|
52
|
+
def get_auditor
|
53
|
+
@auditor
|
54
|
+
end
|
55
|
+
|
56
|
+
|
52
57
|
#
|
53
58
|
# Delegate output related methods to the auditor
|
54
59
|
#
|
@@ -142,7 +147,7 @@ class Auditable
|
|
142
147
|
return if skip?( elem )
|
143
148
|
|
144
149
|
# inform the user about what we're auditing
|
145
|
-
print_status( get_status_str( opts[:altered] ) )
|
150
|
+
print_status( get_status_str( opts[:altered] ) ) if !opts[:silent]
|
146
151
|
|
147
152
|
# submit the element with the injection values
|
148
153
|
req = elem.submit( opts )
|
@@ -179,7 +184,7 @@ class Auditable
|
|
179
184
|
var_combo = []
|
180
185
|
if( !hash || hash.size == 0 ) then return [] end
|
181
186
|
|
182
|
-
if( self.is_a?( Arachni::Parser::Element::Form ) )
|
187
|
+
if( self.is_a?( Arachni::Parser::Element::Form ) && !opts[:skip_orig] )
|
183
188
|
|
184
189
|
if !audited?( audit_id( Arachni::Parser::Element::Form::FORM_VALUES_ORIGINAL ) )
|
185
190
|
# this is the original hash, in case the default values
|
@@ -295,7 +300,7 @@ class Auditable
|
|
295
300
|
print_error( 'Failed to get responses, backing out... ' )
|
296
301
|
next
|
297
302
|
else
|
298
|
-
print_status( 'Analyzing response #' + res.request.id.to_s + '...' )
|
303
|
+
print_status( 'Analyzing response #' + res.request.id.to_s + '...' ) if elem.opts && !elem.opts[:silent]
|
299
304
|
end
|
300
305
|
|
301
306
|
# call the block, if there's one
|
data/lib/parser/elements.rb
CHANGED
@@ -48,6 +48,8 @@ class Base < Arachni::Element::Auditable
|
|
48
48
|
|
49
49
|
attr_accessor :auditable
|
50
50
|
|
51
|
+
attr_accessor :orig
|
52
|
+
|
51
53
|
#
|
52
54
|
# Relatively 'raw' hash holding the element's attributes, values, etc.
|
53
55
|
#
|
@@ -113,6 +115,8 @@ class Link < Base
|
|
113
115
|
@method = 'get'
|
114
116
|
|
115
117
|
@auditable = @raw['vars']
|
118
|
+
@orig = @auditable.deep_clone
|
119
|
+
@orig.freeze
|
116
120
|
end
|
117
121
|
|
118
122
|
def http_request( url, opts )
|
@@ -155,6 +159,8 @@ class Form < Base
|
|
155
159
|
@method = @raw['attrs']['method']
|
156
160
|
|
157
161
|
@auditable = simple['auditable'] || {}
|
162
|
+
@orig = @auditable.deep_clone
|
163
|
+
@orig.freeze
|
158
164
|
end
|
159
165
|
|
160
166
|
def http_request( url, opts )
|
@@ -248,6 +254,9 @@ class Cookie < Base
|
|
248
254
|
|cookie|
|
249
255
|
Options.instance.exclude_cookies.include?( cookie )
|
250
256
|
}
|
257
|
+
|
258
|
+
@orig = @auditable.deep_clone
|
259
|
+
@orig.freeze
|
251
260
|
end
|
252
261
|
|
253
262
|
def http_request( url, opts )
|
@@ -274,6 +283,8 @@ class Header < Base
|
|
274
283
|
@method = 'header'
|
275
284
|
|
276
285
|
@auditable = @raw
|
286
|
+
@orig = @auditable.deep_clone
|
287
|
+
@orig.freeze
|
277
288
|
end
|
278
289
|
|
279
290
|
def http_request( url, opts )
|
data/lib/parser/page.rb
CHANGED
@@ -19,7 +19,7 @@ class Parser
|
|
19
19
|
# @author: Tasos "Zapotek" Laskos
|
20
20
|
# <tasos.laskos@gmail.com>
|
21
21
|
# <zapotek@segfault.gr>
|
22
|
-
# @version: 0.2
|
22
|
+
# @version: 0.2.1
|
23
23
|
#
|
24
24
|
class Page
|
25
25
|
|
@@ -60,6 +60,8 @@ class Page
|
|
60
60
|
#
|
61
61
|
attr_accessor :response_headers
|
62
62
|
|
63
|
+
attr_accessor :paths
|
64
|
+
|
63
65
|
#
|
64
66
|
# @see Parser#links
|
65
67
|
#
|
data/lib/parser/parser.rb
CHANGED
@@ -10,9 +10,11 @@
|
|
10
10
|
module Arachni
|
11
11
|
|
12
12
|
opts = Arachni::Options.instance
|
13
|
+
require 'webrick'
|
13
14
|
require opts.dir['lib'] + 'parser/elements'
|
14
15
|
require opts.dir['lib'] + 'parser/page'
|
15
16
|
require opts.dir['lib'] + 'module/utilities'
|
17
|
+
require opts.dir['lib'] + 'component_manager'
|
16
18
|
|
17
19
|
#
|
18
20
|
# Analyzer class
|
@@ -41,12 +43,41 @@ require opts.dir['lib'] + 'module/utilities'
|
|
41
43
|
# @author: Tasos "Zapotek" Laskos
|
42
44
|
# <tasos.laskos@gmail.com>
|
43
45
|
# <zapotek@segfault.gr>
|
44
|
-
# @version: 0.2
|
46
|
+
# @version: 0.2.1
|
45
47
|
#
|
46
48
|
class Parser
|
47
|
-
|
49
|
+
include Arachni::UI::Output
|
48
50
|
include Arachni::Module::Utilities
|
49
51
|
|
52
|
+
module Extractors
|
53
|
+
#
|
54
|
+
# Base Spider parser class for modules.
|
55
|
+
#
|
56
|
+
# The aim of such modules is to extract paths from a webpage for the Spider to follow.
|
57
|
+
#
|
58
|
+
#
|
59
|
+
# @author: Tasos "Zapotek" Laskos
|
60
|
+
# <tasos.laskos@gmail.com>
|
61
|
+
# <zapotek@segfault.gr>
|
62
|
+
# @version: 0.1
|
63
|
+
# @abstract
|
64
|
+
#
|
65
|
+
class Paths
|
66
|
+
|
67
|
+
#
|
68
|
+
# This method must be implemented by all modules and must return an array
|
69
|
+
# of paths as plain strings
|
70
|
+
#
|
71
|
+
# @param [Nokogiri] Nokogiri document
|
72
|
+
#
|
73
|
+
# @return [Array<String>] paths
|
74
|
+
#
|
75
|
+
def run( doc )
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
50
81
|
#
|
51
82
|
# @return [String] the url of the page
|
52
83
|
#
|
@@ -68,7 +99,7 @@ class Parser
|
|
68
99
|
def initialize( opts, res )
|
69
100
|
@opts = opts
|
70
101
|
|
71
|
-
@url = res.effective_url
|
102
|
+
@url = url_sanitize( res.effective_url )
|
72
103
|
@html = res.body
|
73
104
|
@response_headers = res.headers_hash
|
74
105
|
end
|
@@ -89,6 +120,7 @@ class Parser
|
|
89
120
|
:html => @html,
|
90
121
|
:headers => [],
|
91
122
|
:response_headers => @response_headers,
|
123
|
+
:paths => [],
|
92
124
|
:forms => [],
|
93
125
|
:links => [],
|
94
126
|
:cookies => [],
|
@@ -108,14 +140,25 @@ class Parser
|
|
108
140
|
|
109
141
|
jar = preped.merge( jar )
|
110
142
|
|
143
|
+
c_links = links
|
144
|
+
|
145
|
+
if !( vars = link_vars( @url ) ).empty?
|
146
|
+
url = to_absolute( @url )
|
147
|
+
c_links << Arachni::Parser::Element::Link.new( url, {
|
148
|
+
'href' => url,
|
149
|
+
'vars' => vars
|
150
|
+
} )
|
151
|
+
end
|
152
|
+
|
111
153
|
return Page.new( {
|
112
154
|
:url => @url,
|
113
155
|
:query_vars => link_vars( @url ),
|
114
156
|
:html => @html,
|
115
157
|
:headers => headers(),
|
116
158
|
:response_headers => @response_headers,
|
159
|
+
:paths => paths(),
|
117
160
|
:forms => @opts.audit_forms ? forms() : [],
|
118
|
-
:links => @opts.audit_links ?
|
161
|
+
:links => @opts.audit_links ? c_links : [],
|
119
162
|
:cookies => merge_with_cookiestore( merge_with_cookiejar( cookies_arr ) ),
|
120
163
|
:cookiejar => jar
|
121
164
|
} )
|
@@ -258,7 +301,7 @@ class Parser
|
|
258
301
|
if( !elements[i]['attrs'] || !elements[i]['attrs']['action'] )
|
259
302
|
action = @url.to_s
|
260
303
|
else
|
261
|
-
action = elements[i]['attrs']['action']
|
304
|
+
action = url_sanitize( elements[i]['attrs']['action'] )
|
262
305
|
end
|
263
306
|
action = URI.escape( action ).to_s
|
264
307
|
|
@@ -322,8 +365,17 @@ class Parser
|
|
322
365
|
if( !include?( link['href'] ) ) then next end
|
323
366
|
if !in_domain?( URI.parse( link['href'] ) ) then next end
|
324
367
|
|
325
|
-
link['vars'] =
|
368
|
+
link['vars'] = {}
|
369
|
+
link_vars( link['href'] ).each_pair {
|
370
|
+
|key, val|
|
371
|
+
begin
|
372
|
+
link['vars'][key] = url_sanitize( val )
|
373
|
+
rescue
|
374
|
+
link['vars'][key] = val
|
375
|
+
end
|
376
|
+
}
|
326
377
|
|
378
|
+
link['href'] = url_sanitize( link['href'] )
|
327
379
|
|
328
380
|
link_arr << Element::Link.new( @url, link )
|
329
381
|
|
@@ -356,11 +408,12 @@ class Parser
|
|
356
408
|
rescue
|
357
409
|
end
|
358
410
|
|
411
|
+
|
359
412
|
# don't ask me why....
|
360
413
|
if @response_headers.to_s.substring?( 'set-cookie' )
|
361
414
|
begin
|
362
|
-
cookies << WEBrick::Cookie.parse_set_cookies( @response_headers['Set-Cookie'].to_s )
|
363
|
-
cookies << WEBrick::Cookie.parse_set_cookies( @response_headers['set-cookie'].to_s )
|
415
|
+
cookies << ::WEBrick::Cookie.parse_set_cookies( @response_headers['Set-Cookie'].to_s )
|
416
|
+
cookies << ::WEBrick::Cookie.parse_set_cookies( @response_headers['set-cookie'].to_s )
|
364
417
|
rescue
|
365
418
|
return cookies_arr
|
366
419
|
end
|
@@ -390,6 +443,32 @@ class Parser
|
|
390
443
|
return cookies_arr
|
391
444
|
end
|
392
445
|
|
446
|
+
def dir( url )
|
447
|
+
URI( File.dirname( URI( url.to_s ).path ) + '/' )
|
448
|
+
end
|
449
|
+
|
450
|
+
#
|
451
|
+
# Array of distinct links to follow
|
452
|
+
#
|
453
|
+
# @return [Array<URI>]
|
454
|
+
#
|
455
|
+
def paths
|
456
|
+
return @paths unless @paths.nil?
|
457
|
+
@paths = []
|
458
|
+
return @paths if !doc
|
459
|
+
|
460
|
+
run_extractors( ).each {
|
461
|
+
|path|
|
462
|
+
next if path.nil? or path.empty?
|
463
|
+
abs = to_absolute( path ) rescue next
|
464
|
+
|
465
|
+
@paths << abs if in_domain?( abs )
|
466
|
+
}
|
467
|
+
|
468
|
+
@paths.uniq!
|
469
|
+
return @paths
|
470
|
+
end
|
471
|
+
|
393
472
|
#
|
394
473
|
# Extracts variables and their values from a link
|
395
474
|
#
|
@@ -434,26 +513,36 @@ class Parser
|
|
434
513
|
end
|
435
514
|
rescue Exception => e
|
436
515
|
return nil if link.nil?
|
437
|
-
# return link
|
438
516
|
end
|
439
517
|
|
440
518
|
# remove anchor
|
441
|
-
link = URI.encode( link.to_s.gsub( /#[a-zA-Z0-9_-]*$/,
|
519
|
+
link = URI.encode( link.to_s.gsub( /#[a-zA-Z0-9_-]*$/,'' ) )
|
442
520
|
|
443
|
-
|
444
|
-
|
445
|
-
|
521
|
+
if url = base
|
522
|
+
base_url = URI( url )
|
523
|
+
else
|
524
|
+
base_url = URI( @url )
|
525
|
+
end
|
446
526
|
|
447
|
-
|
527
|
+
relative = URI( link )
|
528
|
+
absolute = base_url.merge( relative )
|
448
529
|
|
449
|
-
|
450
|
-
rescue Exception => e
|
451
|
-
return
|
452
|
-
end
|
530
|
+
absolute.path = '/' if absolute.path && absolute.path.empty?
|
453
531
|
|
454
532
|
return absolute.to_s
|
455
533
|
end
|
456
534
|
|
535
|
+
|
536
|
+
def base
|
537
|
+
begin
|
538
|
+
tmp = doc.search( '//base[@href]' )
|
539
|
+
return tmp[0]['href'].dup
|
540
|
+
rescue
|
541
|
+
return
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
|
457
546
|
#
|
458
547
|
# Returns +true+ if *uri* is in the same domain as the page, returns
|
459
548
|
# +false+ otherwise
|
@@ -508,6 +597,29 @@ class Parser
|
|
508
597
|
|
509
598
|
private
|
510
599
|
|
600
|
+
#
|
601
|
+
# Runs all Spider (path extraction) modules and returns an array of paths
|
602
|
+
#
|
603
|
+
# @return [Array] paths
|
604
|
+
#
|
605
|
+
def run_extractors
|
606
|
+
lib = @opts.dir['root'] + 'path_extractors/'
|
607
|
+
|
608
|
+
|
609
|
+
begin
|
610
|
+
@@manager ||= ::Arachni::ComponentManager.new( lib, Extractors )
|
611
|
+
|
612
|
+
return @@manager.available.map {
|
613
|
+
|name|
|
614
|
+
@@manager[name].new.run( doc )
|
615
|
+
}.flatten.uniq
|
616
|
+
|
617
|
+
rescue ::Exception => e
|
618
|
+
print_error( e.to_s )
|
619
|
+
print_debug_backtrace( e )
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
511
623
|
#
|
512
624
|
# Merges an array of form inputs with an array of form selects
|
513
625
|
#
|