brakeman 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/FEATURES +16 -0
  2. data/README.md +112 -0
  3. data/WARNING_TYPES +69 -0
  4. data/bin/brakeman +266 -0
  5. data/lib/checks.rb +67 -0
  6. data/lib/checks/base_check.rb +338 -0
  7. data/lib/checks/check_cross_site_scripting.rb +216 -0
  8. data/lib/checks/check_default_routes.rb +29 -0
  9. data/lib/checks/check_evaluation.rb +29 -0
  10. data/lib/checks/check_execute.rb +110 -0
  11. data/lib/checks/check_file_access.rb +46 -0
  12. data/lib/checks/check_forgery_setting.rb +25 -0
  13. data/lib/checks/check_mass_assignment.rb +72 -0
  14. data/lib/checks/check_model_attributes.rb +36 -0
  15. data/lib/checks/check_redirect.rb +98 -0
  16. data/lib/checks/check_render.rb +65 -0
  17. data/lib/checks/check_send_file.rb +15 -0
  18. data/lib/checks/check_session_settings.rb +36 -0
  19. data/lib/checks/check_sql.rb +124 -0
  20. data/lib/checks/check_validation_regex.rb +60 -0
  21. data/lib/format/style.css +105 -0
  22. data/lib/processor.rb +83 -0
  23. data/lib/processors/alias_processor.rb +384 -0
  24. data/lib/processors/base_processor.rb +235 -0
  25. data/lib/processors/config_processor.rb +146 -0
  26. data/lib/processors/controller_alias_processor.rb +222 -0
  27. data/lib/processors/controller_processor.rb +175 -0
  28. data/lib/processors/erb_template_processor.rb +84 -0
  29. data/lib/processors/erubis_template_processor.rb +62 -0
  30. data/lib/processors/haml_template_processor.rb +115 -0
  31. data/lib/processors/lib/find_call.rb +176 -0
  32. data/lib/processors/lib/find_model_call.rb +39 -0
  33. data/lib/processors/lib/processor_helper.rb +36 -0
  34. data/lib/processors/lib/render_helper.rb +118 -0
  35. data/lib/processors/library_processor.rb +117 -0
  36. data/lib/processors/model_processor.rb +125 -0
  37. data/lib/processors/output_processor.rb +204 -0
  38. data/lib/processors/params_processor.rb +77 -0
  39. data/lib/processors/route_processor.rb +338 -0
  40. data/lib/processors/template_alias_processor.rb +86 -0
  41. data/lib/processors/template_processor.rb +55 -0
  42. data/lib/report.rb +628 -0
  43. data/lib/scanner.rb +232 -0
  44. data/lib/tracker.rb +144 -0
  45. data/lib/util.rb +141 -0
  46. data/lib/warning.rb +97 -0
  47. metadata +191 -0
@@ -0,0 +1,55 @@
1
+ require 'processors/base_processor'
2
+
3
+ #Base Processor for templates/views
4
+ class TemplateProcessor < BaseProcessor
5
+
6
+ #Initializes template information.
7
+ def initialize tracker, template_name, called_from = nil, file_name = nil
8
+ super(tracker)
9
+ @current_template = { :name => template_name,
10
+ :caller => called_from,
11
+ :partial => template_name.to_s[0,1] == "_",
12
+ :outputs => [],
13
+ :src => nil, #set in Processor
14
+ :type => nil, #set in Processor
15
+ :file => file_name }
16
+ if called_from
17
+ template_name = (template_name.to_s + "." + called_from.to_s).to_sym
18
+ end
19
+
20
+ tracker.templates[template_name] = @current_template
21
+
22
+ @inside_concat = false
23
+ self.warn_on_default = false
24
+ end
25
+
26
+ #Process the template Sexp.
27
+ def process exp
28
+ begin
29
+ super
30
+ rescue Exception => e
31
+ except = e.exception("Error when processing #{@current_template[:name]}: #{e.message}")
32
+ except.set_backtrace(e.backtrace)
33
+ raise except
34
+ end
35
+ end
36
+
37
+ #Ignore initial variable assignment
38
+ def process_lasgn exp
39
+ if exp[1] == :_erbout and exp[2].node_type == :str #ignore
40
+ ignore
41
+ elsif exp[1] == :_buf and exp[2].node_type == :str
42
+ ignore
43
+ else
44
+ exp[2] = process exp[2]
45
+ exp
46
+ end
47
+ end
48
+
49
+ #Adds output to the list of outputs.
50
+ def process_output exp
51
+ process exp[1]
52
+ @current_template[:outputs] << exp
53
+ exp
54
+ end
55
+ end
@@ -0,0 +1,628 @@
1
+ require 'rubygems'
2
+ require 'cgi'
3
+ require 'set'
4
+ require 'ruport'
5
+ require 'processors/output_processor'
6
+ require 'util'
7
+
8
+ #Generates a report based on the Tracker and the results of
9
+ #Tracker#run_checks. Be sure to +run_checks+ before generating
10
+ #a report.
11
+ class Report
12
+ include Util
13
+
14
+ attr_reader :tracker, :checks
15
+
16
+ TEXT_CONFIDENCE = [ "High", "Medium", "Weak" ]
17
+ HTML_CONFIDENCE = [ "<span class='high-confidence'>High</span>",
18
+ "<span class='med-confidence'>Medium</span>",
19
+ "<span class='weak-confidence'>Weak</span>" ]
20
+
21
+ def initialize tracker
22
+ @tracker = tracker
23
+ @checks = tracker.checks
24
+ @element_id = 0 #Used for HTML ids
25
+ @warnings_summary = nil
26
+ end
27
+
28
+ #Generate summary table of what was parsed
29
+ def generate_overview
30
+ templates = Set.new(@tracker.templates.map {|k,v| v[:name].to_s[/[^.]+/]}).length
31
+ warnings = checks.warnings.length +
32
+ checks.controller_warnings.length +
33
+ checks.model_warnings.length +
34
+ checks.template_warnings.length
35
+
36
+ #Add number of high confidence warnings in summary.
37
+ #Skipping for CSV because it makes the cell text instead of
38
+ #a number.
39
+ unless OPTIONS[:output_format] == :to_csv
40
+ summary = warnings_summary
41
+
42
+ if OPTIONS[:output_format] == :to_html
43
+ warnings = "#{warnings} <span class='high-confidence'>(#{summary[:high_confidence]})</span>"
44
+ else
45
+ warnings = "#{warnings} (#{summary[:high_confidence]})"
46
+ end
47
+ end
48
+
49
+ table = Ruport::Data::Table(["Scanned/Reported", "Total"])
50
+ table << { "Scanned/Reported" => "Controllers", "Total" => tracker.controllers.length }
51
+ #One less because of the 'fake' one used for unknown models
52
+ table << { "Scanned/Reported" => "Models", "Total" => tracker.models.length - 1 }
53
+ table << { "Scanned/Reported" => "Templates", "Total" => templates }
54
+ table << { "Scanned/Reported" => "Errors", "Total" => tracker.errors.length }
55
+ table << { "Scanned/Reported" => "Security Warnings", "Total" => warnings}
56
+ end
57
+
58
+ #Generate table of how many warnings of each warning type were reported
59
+ def generate_warning_overview
60
+ table = Ruport::Data::Table(["Warning Type", "Total"])
61
+ types = warnings_summary.keys
62
+ types.delete :high_confidence
63
+ types.sort.each do |warning_type|
64
+ table << { "Warning Type" => warning_type, "Total" => warnings_summary[warning_type] }
65
+ end
66
+ table
67
+ end
68
+
69
+ #Generate table of errors or return nil if no errors
70
+ def generate_errors
71
+ unless tracker.errors.empty?
72
+ table = Ruport::Data::Table(["Error", "Location"])
73
+ tracker.errors.each do |w|
74
+ p w if OPTIONS[:debug]
75
+ table << { "Error" => w[:error], "Location" => w[:backtrace][0] }
76
+ end
77
+
78
+ table
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ #Generate table of general security warnings
85
+ def generate_warnings
86
+ table = Ruport::Data::Table(["Confidence", "Class", "Method", "Warning Type", "Message"])
87
+ checks.warnings.each do |warning|
88
+ next if warning.confidence > OPTIONS[:min_confidence]
89
+ w = warning.to_row
90
+
91
+ if OPTIONS[:output_format] == :to_html
92
+ w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
93
+ w["Message"] = with_context warning, w["Message"]
94
+ else
95
+ w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
96
+ end
97
+
98
+ table << w
99
+ end
100
+
101
+ table.sort_rows_by! "Class"
102
+ table.sort_rows_by! "Warning Type"
103
+ table.sort_rows_by! "Confidence"
104
+
105
+ if table.empty?
106
+ table = Ruport::Data::Table("General Warnings")
107
+ table << { "General Warnings" => "[NONE]" }
108
+ end
109
+
110
+ table
111
+ end
112
+
113
+ #Generate table of template warnings or return nil if no warnings
114
+ def generate_template_warnings
115
+ unless checks.template_warnings.empty?
116
+ table = Ruport::Data::Table(["Confidence", "Template", "Warning Type", "Message"])
117
+ checks.template_warnings.each do |warning|
118
+ next if warning.confidence > OPTIONS[:min_confidence]
119
+ w = warning.to_row :template
120
+
121
+ if OPTIONS[:output_format] == :to_html
122
+ w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
123
+ w["Message"] = with_context warning, w["Message"]
124
+ else
125
+ w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
126
+ end
127
+
128
+ table << w
129
+ end
130
+
131
+ if table.empty?
132
+ nil
133
+ else
134
+ table.sort_rows_by! "Template"
135
+ table.sort_rows_by! "Warning Type"
136
+ table.sort_rows_by! "Confidence"
137
+ table.to_group "View Warnings"
138
+ end
139
+ else
140
+ nil
141
+ end
142
+ end
143
+
144
+ #Generate table of model warnings or return nil if no warnings
145
+ def generate_model_warnings
146
+ unless checks.model_warnings.empty?
147
+ table = Ruport::Data::Table(["Confidence", "Model", "Warning Type", "Message"])
148
+ checks.model_warnings.each do |warning|
149
+ next if warning.confidence > OPTIONS[:min_confidence]
150
+ w = warning.to_row :model
151
+
152
+ if OPTIONS[:output_format] == :to_html
153
+ w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
154
+ w["Message"] = with_context warning, w["Message"]
155
+ else
156
+ w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
157
+ end
158
+
159
+ table << w
160
+ end
161
+
162
+ if table.empty?
163
+ nil
164
+ else
165
+ table.sort_rows_by! "Model"
166
+ table.sort_rows_by! "Warning Type"
167
+ table.sort_rows_by! "Confidence"
168
+ table.to_group "Model Warnings"
169
+ end
170
+ else
171
+ nil
172
+ end
173
+ end
174
+
175
+ #Generate table of controller warnings or nil if no warnings
176
+ def generate_controller_warnings
177
+ unless checks.controller_warnings.empty?
178
+ table = Ruport::Data::Table(["Confidence", "Controller", "Warning Type", "Message"])
179
+ checks.controller_warnings.each do |warning|
180
+ next if warning.confidence > OPTIONS[:min_confidence]
181
+ w = warning.to_row :controller
182
+
183
+ if OPTIONS[:output_format] == :to_html
184
+ w["Confidence"] = HTML_CONFIDENCE[w["Confidence"]]
185
+ w["Message"] = with_context warning, w["Message"]
186
+ else
187
+ w["Confidence"] = TEXT_CONFIDENCE[w["Confidence"]]
188
+ end
189
+
190
+ table << w
191
+ end
192
+
193
+ if table.empty?
194
+ nil
195
+ else
196
+ table.sort_rows_by! "Controller"
197
+ table.sort_rows_by! "Warning Type"
198
+ table.sort_rows_by! "Confidence"
199
+ table.to_group "Controller Warnings"
200
+ end
201
+ else
202
+ nil
203
+ end
204
+ end
205
+
206
+ #Generate table of controllers and routes found for those controllers
207
+ def generate_controllers
208
+ table = Ruport::Data::Table(["Name", "Parent", "Includes", "Routes"])
209
+ tracker.controllers.keys.map{|k| k.to_s}.sort.each do |name|
210
+ name = name.to_sym
211
+ c = tracker.controllers[name]
212
+
213
+ if tracker.routes[:allow_all_actions] or tracker.routes[name] == :allow_all_actions
214
+ routes = c[:public].keys.map{|e| e.to_s}.sort.join(", ")
215
+ elsif tracker.routes[name].nil?
216
+ #No routes defined for this controller.
217
+ #This can happen when it is only a parent class
218
+ #for other controllers, for example.
219
+ routes = "[None]"
220
+
221
+ else
222
+ routes = (Set.new(c[:public].keys) & tracker.routes[name.to_sym]).
223
+ to_a.
224
+ map {|e| e.to_s}.
225
+ sort.
226
+ join(", ")
227
+ end
228
+
229
+ if routes == ""
230
+ routes = "[None]"
231
+ end
232
+
233
+ table << { "Name" => name.to_s,
234
+ "Parent" => c[:parent].to_s,
235
+ "Includes" => c[:includes].join(", "),
236
+ "Routes" => routes
237
+ }
238
+ end
239
+ table.sort_rows_by "Name"
240
+ end
241
+
242
+ #Generate listings of templates and their output
243
+ def generate_templates
244
+ out_processor = OutputProcessor.new
245
+ table = Ruport::Data::Table(["Name", "Output"])
246
+ tracker.templates.each do |name, template|
247
+ unless template[:outputs].empty?
248
+ template[:outputs].each do |out|
249
+ out = out_processor.format out
250
+ out = CGI.escapeHTML(out) if OPTIONS[:output_format] == :to_html
251
+ table << { "Name" => name,
252
+ "Output" => out.gsub("\n", ";").gsub(/\s+/, " ") }
253
+ end
254
+ end
255
+ end
256
+ Ruport::Data::Grouping(table, :by => "Name")
257
+ end
258
+
259
+ #Generate HTML output
260
+ def to_html
261
+ out = html_header <<
262
+ "<h2 id='summary'>Summary</h2>" <<
263
+ generate_overview.to_html << "<br/>" <<
264
+ generate_warning_overview.to_html
265
+
266
+ if OPTIONS[:report_routes] or OPTIONS[:debug]
267
+ out << "<h2>Controllers</h2>" <<
268
+ generate_controllers.to_html
269
+ end
270
+
271
+ if OPTIONS[:debug]
272
+ out << "<h2>Templates</h2>" <<
273
+ generate_templates.to_html
274
+ end
275
+
276
+ res = generate_errors
277
+ out << "<h2>Errors</h2>" << res.to_html if res
278
+
279
+ res = generate_warnings
280
+ out << "<h2>Security Warnings</h2>" << res.to_html if res
281
+
282
+ res = generate_controller_warnings
283
+ out << res.to_html if res
284
+
285
+ res = generate_model_warnings
286
+ out << res.to_html if res
287
+
288
+ res = generate_template_warnings
289
+ out << res.to_html if res
290
+
291
+ out << "</body></html>"
292
+ end
293
+
294
+ #Output text version of the report
295
+ def to_s
296
+ out = text_header <<
297
+ "\n+SUMMARY+\n" <<
298
+ generate_overview.to_s << "\n" <<
299
+ generate_warning_overview.to_s << "\n"
300
+
301
+ if OPTIONS[:report_routes] or OPTIONS[:debug]
302
+ out << "+CONTROLLERS+\n" <<
303
+ generate_controllers.to_s << "\n"
304
+ end
305
+
306
+ if OPTIONS[:debug]
307
+ out << "+TEMPLATES+\n\n" <<
308
+ generate_templates.to_s << "\n"
309
+ end
310
+
311
+ res = generate_errors
312
+ out << "+ERRORS+\n" << res.to_s << "\n" if res
313
+
314
+ res = generate_warnings
315
+ out << "+SECURITY WARNINGS+\n" << res.to_s << "\n" if res
316
+
317
+ res = generate_controller_warnings
318
+ out << res.to_s << "\n" if res
319
+
320
+ res = generate_model_warnings
321
+ out << res.to_s << "\n" if res
322
+
323
+ res = generate_template_warnings
324
+ out << res.to_s << "\n" if res
325
+
326
+ out
327
+ end
328
+
329
+ #Generate CSV output
330
+ def to_csv
331
+ out = csv_header <<
332
+ "\nSUMMARY\n" <<
333
+ generate_overview.to_csv << "\n" <<
334
+ generate_warning_overview.to_csv << "\n"
335
+
336
+ if OPTIONS[:report_routes] or OPTIONS[:debug]
337
+ out << "CONTROLLERS\n" <<
338
+ generate_controllers.to_csv << "\n"
339
+ end
340
+
341
+ if OPTIONS[:debug]
342
+ out << "TEMPLATES\n\n" <<
343
+ generate_templates.to_csv << "\n"
344
+ end
345
+
346
+ res = generate_errors
347
+ out << "ERRORS\n" << res.to_csv << "\n" if res
348
+
349
+ res = generate_warnings
350
+ out << "SECURITY WARNINGS\n" << res.to_csv << "\n" if res
351
+
352
+ res = generate_controller_warnings
353
+ out << res.to_csv << "\n" if res
354
+
355
+ res = generate_model_warnings
356
+ out << res.to_csv << "\n" if res
357
+
358
+ res = generate_template_warnings
359
+ out << res.to_csv << "\n" if res
360
+
361
+ out
362
+ end
363
+
364
+ #Not yet implemented
365
+ def to_pdf
366
+ raise "PDF output is not yet supported."
367
+ end
368
+
369
+ #Return header for HTML output. Uses CSS from OPTIONS[:html_style]
370
+ def html_header
371
+ if File.exist? OPTIONS[:html_style]
372
+ css = File.read OPTIONS[:html_style]
373
+ else
374
+ raise "Cannot find CSS stylesheet for HTML: #{OPTIONS[:html_style]}"
375
+ end
376
+
377
+ <<-HTML
378
+ <!DOCTYPE HTML SYSTEM>
379
+ <html>
380
+ <head>
381
+ <title>Brakeman Report</title>
382
+ <script type="text/javascript">
383
+ function toggle(context){
384
+ if (document.getElementById(context).style.display != "block")
385
+ document.getElementById(context).style.display = "block";
386
+ else
387
+ document.getElementById(context).style.display = "none";
388
+ }
389
+ </script>
390
+ <style type="text/css">
391
+ #{css}
392
+ </style>
393
+ </head>
394
+ <body>
395
+ <h1>Brakeman Report</h1>
396
+ <table>
397
+ <tr>
398
+ <th>Application Path</th>
399
+ <th>Report Generation Time</th>
400
+ <th>Checks Performed</th>
401
+ </tr>
402
+ <tr>
403
+ <td>#{File.expand_path OPTIONS[:app_path]}</td>
404
+ <td>#{Time.now}</td>
405
+ <td>#{checks.checks_run.sort.join(", ")}</td>
406
+ </tr>
407
+ </table>
408
+ HTML
409
+ end
410
+
411
+ #Generate header for text output
412
+ def text_header
413
+ "\n+BRAKEMAN REPORT+\n\nApplication path: #{File.expand_path OPTIONS[:app_path]}\nGenerated at #{Time.now}\nChecks run: #{checks.checks_run.sort.join(", ")}\n"
414
+ end
415
+
416
+ #Generate header for CSV output
417
+ def csv_header
418
+ header = Ruport::Data::Table(["Application Path", "Report Generation Time", "Checks Performed"])
419
+ header << [File.expand_path(OPTIONS[:app_path]), Time.now.to_s, checks.checks_run.sort.join(", ")]
420
+ "BRAKEMAN REPORT\n\n" << header.to_csv
421
+ end
422
+
423
+ #Return summary of warnings in hash and store in @warnings_summary
424
+ def warnings_summary
425
+ return @warnings_summary if @warnings_summary
426
+
427
+ summary = Hash.new(0)
428
+ high_confidence_warnings = 0
429
+
430
+ [checks.warnings,
431
+ checks.controller_warnings,
432
+ checks.model_warnings,
433
+ checks.template_warnings].each do |warnings|
434
+
435
+ warnings.each do |warning|
436
+ summary[warning.warning_type.to_s] += 1
437
+
438
+ if warning.confidence == 0
439
+ high_confidence_warnings += 1
440
+ end
441
+ end
442
+ end
443
+
444
+ summary[:high_confidence] = high_confidence_warnings
445
+ @warnings_summary = summary
446
+ end
447
+
448
+ #Return file name related to given warning. Uses +warning.file+ if it exists
449
+ def file_for warning
450
+ if warning.file
451
+ File.expand_path warning.file, OPTIONS[:app_path]
452
+ else
453
+ case warning.warning_set
454
+ when :controller
455
+ file_by_name warning.controller, :controller
456
+ when :template
457
+ file_by_name warning.template[:name], :template
458
+ when :model
459
+ file_by_name warning.model, :model
460
+ when :warning
461
+ file_by_name warning.class
462
+ else
463
+ nil
464
+ end
465
+ end
466
+ end
467
+
468
+ #Attempt to determine path to context file based on the reported name
469
+ #in the warning.
470
+ #
471
+ #For example,
472
+ #
473
+ # file_by_name FileController #=> "/rails/root/app/controllers/file_controller.rb
474
+ def file_by_name name, type = nil
475
+ return nil unless name
476
+ string_name = name.to_s
477
+ name = name.to_sym
478
+
479
+ unless type
480
+ if string_name =~ /Controller$/
481
+ type = :controller
482
+ elsif camelize(string_name) == string_name
483
+ type = :model
484
+ else
485
+ type = :template
486
+ end
487
+ end
488
+
489
+ path = OPTIONS[:app_path]
490
+
491
+ case type
492
+ when :controller
493
+ if tracker.controllers[name] and tracker.controllers[name][:file]
494
+ path = tracker.controllers[name][:file]
495
+ else
496
+ path += "/app/controllers/#{underscore(string_name)}.rb"
497
+ end
498
+ when :model
499
+ if tracker.models[name] and tracker.models[name][:file]
500
+ path = tracker.models[name][:file]
501
+ else
502
+ path += "/app/controllers/#{underscore(string_name)}.rb"
503
+ end
504
+ when :template
505
+ if tracker.templates[name] and tracker.templates[name][:file]
506
+ path = tracker.templates[name][:file]
507
+ elsif string_name.include? " "
508
+ name = string_name.split[0].to_sym
509
+ path = file_for name, :template
510
+ else
511
+ path = nil
512
+ end
513
+ end
514
+
515
+ path
516
+ end
517
+
518
+ #Return array of lines surrounding the warning location from the original
519
+ #file.
520
+ def context_for warning
521
+ file = file_for warning
522
+ context = []
523
+ return context unless warning.line and file and File.exist? file
524
+
525
+ current_line = 0
526
+ start_line = warning.line - 5
527
+ end_line = warning.line + 5
528
+
529
+ start_line = 1 if start_line < 0
530
+
531
+ File.open file do |f|
532
+ f.each_line do |line|
533
+ current_line += 1
534
+
535
+ next if line.strip == ""
536
+
537
+ if current_line > end_line
538
+ break
539
+ end
540
+
541
+ if current_line >= start_line
542
+ context << [current_line, line]
543
+ end
544
+ end
545
+ end
546
+
547
+ context
548
+ end
549
+
550
+ #Generate HTML for warnings, including context show/hidden via Javascript
551
+ def with_context warning, message
552
+ context = context_for warning
553
+ full_message = nil
554
+
555
+ if OPTIONS[:message_limit] and
556
+ OPTIONS[:message_limit] > 0 and
557
+ message.length > OPTIONS[:message_limit]
558
+
559
+ full_message = message
560
+ message = message[0..OPTIONS[:message_limit]] << "..."
561
+ end
562
+
563
+ if context.empty?
564
+ return CGI.escapeHTML(message)
565
+ end
566
+
567
+ @element_id += 1
568
+ code_id = "context#@element_id"
569
+ message_id = "message#@element_id"
570
+ full_message_id = "full_message#@element_id"
571
+ alt = false
572
+ output = "<div class='warning_message' onClick=\"toggle('#{code_id}');toggle('#{message_id}');toggle('#{full_message_id}')\" >" <<
573
+ if full_message
574
+ "<span id='#{message_id}' style='display:block' >#{CGI.escapeHTML(message)}</span>" <<
575
+ "<span id='#{full_message_id}' style='display:none'>#{CGI.escapeHTML(full_message)}</span>"
576
+ else
577
+ CGI.escapeHTML(message)
578
+ end <<
579
+ "<table id='#{code_id}' class='context' style='display:none'>"
580
+
581
+ unless context.empty?
582
+ if warning.line - 1 == 1 or warning.line + 1 == 1
583
+ error = " near_error"
584
+ elsif 1 == warning.line
585
+ error = " error"
586
+ else
587
+ error = ""
588
+ end
589
+
590
+ output << <<-HTML
591
+ <tr class='context first#{error}'>
592
+ <td class='context_line'>
593
+ <pre class='context'>#{context.first[0]}</pre>
594
+ </td>
595
+ <td class='context'>
596
+ <pre class='context'>#{CGI.escapeHTML context.first[1].chomp}</pre>
597
+ </td>
598
+ </tr>
599
+ HTML
600
+
601
+ if context.length > 1
602
+ output << context[1..-1].map do |code|
603
+ alt = !alt
604
+ if code[0] == warning.line - 1 or code[0] == warning.line + 1
605
+ error = " near_error"
606
+ elsif code[0] == warning.line
607
+ error = " error"
608
+ else
609
+ error = ""
610
+ end
611
+
612
+ <<-HTML
613
+ <tr class='context#{alt ? ' alt' : ''}#{error}'>
614
+ <td class='context_line'>
615
+ <pre class='context'>#{code[0]}</pre>
616
+ </td>
617
+ <td class='context'>
618
+ <pre class='context'>#{CGI.escapeHTML code[1].chomp}</pre>
619
+ </td>
620
+ </tr>
621
+ HTML
622
+ end.join
623
+ end
624
+ end
625
+
626
+ output << "</table></div>"
627
+ end
628
+ end