brakeman 0.0.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.
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