提交 cd1663fb 编写于 作者: J Justin

Initial release

上级
Can detect:
-Possibly unescaped model attributes or parameters in views (Cross Site Scripting)
-Bad string interpolation in calls to Model.find, Model.last, Model.first, etc., as well as chained calls (SQL Injection)
-String interpolation in find_by_sql (SQL Injection)
-String interpolation or params in calls to system, exec, and syscall and `` (Command Injection)
-Unrestricted mass assignments
-Global restriction of mass assignment
-Missing call to protect_from_forgery in ApplicationController (CSRF protection)
-Default routes, per-controller and globally
-Redirects based on params (probably too broad currently)
-Validation regexes not using \A and \z
-Calls to render with dynamic paths
General capabilities:
-Search for method calls based on target class and/or method name
-Determine 'output' of templates using ERB, Erubis, or HAML. Can handle automatic escaping
The MIT License
Copyright (c) 2010, YELLOWPAGES.COM, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# Brakeman
Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
It targets Rails versions > 2.0 and < 3.0.
# Installation
gem build brakeman.gemspec
gem install brakeman*.gem
# Usage
brakeman path/to/rails/app/root
# Options
To specify an output file for the results:
brakeman -o output_file path/to/rails/app/root
The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, and `csv`.
To suppress informational warnings and just output the report:
brakeman -q path/to/rails/app/root
To see all kinds of debugging information:
brakeman -d path/to/rails/app/root
Specific checks can be skipped, if desired. The name needs to be the correct case. For example, to skip looking for default routes (`DefaultRoutes`):
brakeman -x DefaultRoutes path/to/rails/app/root
Multiple checks should be separated by a comma:
brakeman -x DefaultRoutes,Redirect path/to/rails/app/root
To do the opposite and only run a certain set of tests:
brakeman -t Find,ValidationRegex path/to/rails/app/root
To indicate certain methods are "safe":
brakeman -s benign_method,totally_safe path/to/rails/app/root
By default, brakeman will assume that unknown methods involving untrusted data are dangerous. For example, this would a warning:
<%= some_method(:option => params[:input]) %>
To only raise warnings only when untrusted data is being directly used:
brakeman -r path/to/rails/app/root
# Warning information
See WARNING_TYPES for more information on the warnings reported by this tool.
# Warning context
The HTML output format provides an excerpt from the original application source where a warning was triggered. Due to the processing done while looking for vulnerabilities, the source may not resemble the reported warning and reported line numbers may be slightly off. However, the context still provides a quick look into the code which raised the warning.
# Confidence levels
Brakeman assigns a confidence level to each warning. This provides a rough estimate of how certain the tool is that a given warning is actually a problem. Naturally, these ratings should not be taken as absolute truth.
There are three levels of confidence:
+ High - Either this is a simple warning (boolean value) or user input is very likely being used in unsafe ways.
+ Medium - This generally indicates an unsafe use of a variable, but the variable may or may not be user input.
+ Weak - Typically means user input was indirectly used in a potentially unsafe manner.
To only get warnings above a given confidence level:
brakeman -w3 /path/to/rails/app/root
The `-w` switch takes a number from 1 to 3, with 1 being low (all warnings) and 3 being high (only high confidence warnings).
# Configuration files
Brakeman options can stored and read from YAML files. To simplify the process of writing a configuration file, the `-C` option will output the currently set options.
Options passed in on the commandline have priority over configuration files.
The default config locations are `./config.yaml`, `~/.brakeman/`, and `/etc/brakeman/config.yaml`
The `-c` option can be used to specify a configuration file to use.
# License
The MIT License
Copyright (c) 2010, YELLOWPAGES.COM, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This file describes the various warning types reported by this tool.
# Cross Site Scripting
Cross site scripting warnings are raised when a parameter or model attribute is output through a view without being escaped.
See http://guides.rubyonrails.org/security.html#cross-site-scripting-xss for details.
# SQL Injection
String interpolation or concatenation has been detected in an SQL query. Use parameterized queries instead.
See http://guides.rubyonrails.org/security.html#sql-injection for details.
# Command Injection
Request parameters or string interpolation has been detected in a `system` call. This can lead to someone executing arbitrary commands. Use the safe form of `system` instead, which will pass in arguments safely.
See http://guides.rubyonrails.org/security.html#command-line-injection for details.
# Mass Assignment
Mass assignment is a method for initializing models. If the attributes which are set is not restricted, someone may set the attributes to any value they wish.
Mass assignment can be disabled globally.
Please see http://railspikes.com/2008/9/22/is-your-rails-application-safe-from-mass-assignment for more details.
# Attribute Restriction
This warning comes up if a model does not limit what attributes can be set through mass assignment.
In particular, this check looks for `attr_accessible` inside model definitions. If it is not found, this warning will be issued.
Note that disabling mass assignment globally will suppress these warnings.
# Cross-Site Request Forgery
No call to `protect_from_forgery` was found in `ApplicationController`. This method prevents CSRF.
See http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf for details.
# Redirect
Redirects which rely on user-supplied values can be used to "spoof" websites or hide malicious links in otherwise harmless-looking URLs. They can also allow access to restricted areas of a site if the destination is not validated.
This warning is shown when request parameters are used inside a call to `redirect_to`.
See http://www.owasp.org/index.php/Top_10_2010-A10 for more information.
# Default Routes
The general default routes warning means there is a call to `map.connect ":controller/:action/:id"` in config/routes.rb. This allows any public method on any controller to be called as an action.
If this warning is reported for a particular controller, it means there is a route to that controller containing `:action`.
Default routes can be dangerous if methods are made public which are not intended to be used as URLs or actions.
# Format Validation
Calls to `validates_format_of ..., :with => //` which do not use `\A` and `\z` as anchors will cause this warning. Using `^` and `$` is not sufficient, as `$` will only match up to a new line. This allows an attacker to put whatever malicious input they would like after a new line character.
See http://guides.rubyonrails.org/security.html#regular-expressions for details.
# Dynamic Render Path
When a call to `render` uses a dynamically generated path, template name, file name, or action, there is the possibility that a user can access templates that should be restricted. The issue may be worse if those templates execute code or modify the database.
This warning is shown whenever the path to be rendered is not a static string or symbol.
#!/usr/bin/env ruby
require "optparse"
require 'set'
require 'yaml'
Version = "0.0.1"
trap("INT") do
$stderr.puts "\nInterrupted - exiting."
exit!
end
#Load scanner
begin
require 'scanner'
rescue LoadError
#Try to find lib directory locally
$: << "#{File.expand_path(File.dirname(__FILE__))}/../lib"
begin
require 'scanner'
rescue LoadError
abort "Cannot find lib/ directory."
end
end
#Parse command line options
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: brakeman [options] rails/root/path"
opts.on "-p", "--path PATH", "Specify path to Rails application" do |path|
options[:app_path] = File.expand_path path
end
opts.on "-q", "--quiet", "Suppress informational messages" do
options[:quiet] = true
$VERBOSE = nil
end
opts.separator ""
opts.separator "Scanning options:"
opts.on "--ignore-model-output", "Consider model attributes XSS-safe" do
options[:ignore_model_output] = true
end
opts.on "-r", "--report-direct", "Only report direct use of untrusted data" do |option|
options[:check_arguments] = !option
end
opts.on "-s", "--safe-methods meth1,meth2,etc", Array, "Consider the specified methods safe" do |methods|
options[:safe_methods] ||= Set.new
options[:safe_methods].merge methods.map {|e| e.to_sym }
end
opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks|
checks.each_with_index do |s, index|
if s[0,5] != "Check"
checks[index] = "Check" << s
end
end
options[:run_checks] ||= Set.new
options[:run_checks].merge checks
end
opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip|
skip.each do |s|
if s[0,5] != "Check"
s = "Check" << s
end
options[:skip_checks] ||= Set.new
options[:skip_checks] << s
end
end
opts.separator ""
opts.separator "Output options:"
opts.on "-d", "--debug", "Lots of output" do
options[:debug] = true
end
opts.on "-f",
"--format TYPE",
[:pdf, :text, :html, :csv],
"Specify output format. Default is text" do |type|
type = "s" if type == :text
options[:output_format] = ("to_" << type.to_s).to_sym
end
opts.on "-l", "--[no]-combine-locations", "Combine warning locations (Default)" do |combine|
options[:combine_locations] = combine
end
opts.on "-m", "--routes", "Report controller information" do
options[:report_routes] = true
end
opts.on "--message-limit LENGTH", "Limit message length in HTML report" do |limit|
options[:message_limit] = limit.to_i
end
opts.on "-o", "--output FILE", "Specify file for output. Defaults to stdout" do |file|
options[:output_file] = file
end
opts.on "-w",
"--confidence-level LEVEL",
["1", "2", "3"],
"Set minimal confidence level (1 - 3). Default: 1" do |level|
options[:min_confidence] = 3 - level.to_i
end
opts.separator ""
opts.separator "Configuration files:"
opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file|
options[:config_file] = File.expand_path(file)
end
opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file|
if file
options[:create_config] = file
else
options[:create_config] = true
end
end
opts.separator ""
opts.on_tail "-h", "--help", "Display this message" do
puts opts
exit
end
end.parse!(ARGV)
#Load configuation file
[File.expand_path(options[:config_file].to_s),
File.expand_path("./config.yaml"),
File.expand_path("~/.brakeman/config.yaml"),
File.expand_path("/etc/brakeman/config.yaml"),
"#{File.expand_path(File.dirname(__FILE__))}/../lib/config.yaml"].each do |f|
if File.exist? f and not File.directory? f
warn "[Notice] Using configuration in #{f}" unless options[:quiet]
OPTIONS = YAML.load_file f
OPTIONS.merge! options
OPTIONS.each do |k,v|
if v.is_a? Array
OPTIONS[k] = Set.new v
end
end
break
end
end
OPTIONS = options unless defined? OPTIONS
#Set defaults just in case
{ :skip_checks => Set.new,
:check_arguments => true,
:safe_methods => Set.new,
:min_confidence => 2,
:combine_locations => true,
:collapse_mass_assignment => true,
:ignore_redirect_to_model => true,
:ignore_model_output => false,
:message_limit => 100,
:html_style => "#{File.expand_path(File.dirname(__FILE__))}/../lib/format/style.css"
}.each do |k,v|
OPTIONS[k] = v if OPTIONS[k].nil?
end
#Set output format
if OPTIONS[:output_format]
case OPTIONS[:output_format]
when :html, :to_html
OPTIONS[:output_format] = :to_html
when :csv, :to_csv
OPTIONS[:output_format] = :to_csv
when :pdf, :to_pdf
OPTIONS[:output_format] = :to_pdf
else
OPTIONS[:output_format] = :to_s
end
else
case OPTIONS[:output_file]
when /\.html$/i
OPTIONS[:output_format] = :to_html
when /\.csv$/i
OPTIONS[:output_format] = :to_csv
when /\.pdf$/i
OPTIONS[:output_format] = :to_pdf
else
OPTIONS[:output_format] = :to_s
end
end
#Output configuration if requested
if OPTIONS[:create_config]
if OPTIONS[:create_config].is_a? String
file = OPTIONS[:create_config]
else
file = nil
end
OPTIONS.delete :create_config
OPTIONS.each do |k,v|
if v.is_a? Set
OPTIONS[k] = v.to_a
end
end
if file
File.open file, "w" do |f|
YAML.dump OPTIONS, f
end
puts "Output configuration to #{file}"
else
puts YAML.dump(OPTIONS)
end
exit
end
#Check application path
unless OPTIONS[:app_path]
if ARGV[-1].nil?
OPTIONS[:app_path] = File.expand_path "."
else
OPTIONS[:app_path] = File.expand_path ARGV[-1]
end
end
app_path = OPTIONS[:app_path]
abort("Please supply the path to a Rails application.") unless app_path and File.exist? app_path + "/app"
warn "[Notice] Using Ruby #{RUBY_VERSION}. Please make sure this matches the one used to run your Rails application."
#Start scanning
scanner = Scanner.new app_path
warn "Processing application in #{app_path}"
tracker = scanner.process
warn "Running checks..."
tracker.run_checks
warn "Generating report..."
if OPTIONS[:output_file]
File.open OPTIONS[:output_file], "w" do |f|
f.puts tracker.report.send(OPTIONS[:output_format])
end
else
puts tracker.report.send(OPTIONS[:output_format])
end
Gem::Specification.new do |s|
s.name = %q{brakeman}
s.version = "0.0.1"
s.authors = ["Justin Collins"]
s.email = "justin@presidentbeef.com"
s.summary = "Security vulnerability scanner for Ruby on Rails."
s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis."
s.homepage = "http://github.com/presidentbeef/brakeman"
s.files = ["bin/brakeman", "WARNING_TYPES", "FEATURES", "README.md"] + Dir["lib/**/*.rb"] + Dir["lib/format/*.css"]
s.executables = ["brakeman"]
s.add_dependency "activesupport", "~>2.2.2"
s.add_dependency "ruby2ruby", "~>1.2.4"
s.add_dependency "ruport", "~>1.6.3"
s.add_dependency "erubis", "~>2.6.5"
s.add_dependency "haml", "~>3.0.12"
end
#Collects up results from running different checks.
#
#Checks can be added with +Check.add(check_class)+
#
#All .rb files in checks/ will be loaded.
class Checks
@checks = []
attr_reader :warnings, :controller_warnings, :model_warnings, :template_warnings, :checks_run
#Add a check. This will call +_klass_.new+ when running tests
def self.add klass
@checks << klass
end
#No need to use this directly.
def initialize
@warnings = []
@template_warnings = []
@model_warnings = []
@controller_warnings = []
@checks_run = []
end
#Add Warning to list of warnings to report.
#Warnings are split into four different arrays
#for template, controller, model, and generic warnings.
def add_warning warning
case warning.warning_set
when :template
@template_warnings << warning
when :warning
@warnings << warning
when :controller
@controller_warnings << warning
when :model
@model_warnings << warning
else
raise "Unknown warning: #{warning.warning_set}"
end
end
#Run all the checks on the given Tracker.
#Returns a new instance of Checks with the results.
def self.run_checks tracker
checks = self.new
@checks.each do |c|
#Run or don't run check based on options
unless OPTIONS[:skip_checks].include? c.to_s or
(OPTIONS[:run_checks] and not OPTIONS[:run_checks].include? c.to_s)
warn " - #{c}"
c.new(checks, tracker).run_check
#Maintain list of which checks were run
#mainly for reporting purposes
checks.checks_run << c.to_s[5..-1]
end
end
checks
end
end
#Load all files in checks/ directory
Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/checks/*.rb").sort.each do |f|
require f.match(/(checks\/.*)\.rb$/)[0]
end
require 'rubygems'
require 'sexp_processor'
require 'processors/output_processor'
require 'warning'
require 'util'
#Basis of vulnerability checks.
class BaseCheck < SexpProcessor
include ProcessorHelper
include Util
attr_reader :checks, :tracker
CONFIDENCE = { :high => 0, :med => 1, :low => 2 }
#Initialize Check with Checks.
def initialize checks, tracker
super()
@results = [] #only to check for duplicates
@checks = checks
@tracker = tracker
@string_interp = false
@current_template = @current_module = @current_class = @current_method = nil
self.strict = false
self.auto_shift_type = false
self.require_empty = false
self.default_method = :process_default
self.warn_on_default = false
end
#Add result to result list, which is used to check for duplicates
def add_result result, location = nil
location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
location = location[:name] if location.is_a? Hash
location = location.to_sym
@results << [result.line, location, result]
end
#Default Sexp processing. Iterates over each value in the Sexp
#and processes them if they are also Sexps.
def process_default exp
type = exp.shift
exp.each_with_index do |e, i|
if sexp? e
process e
else
e
end
end
exp.unshift type
end
#Process calls and check if they include user input
def process_call exp
process exp[1] if sexp? exp[1]
process exp[3]
if ALL_PARAMETERS.include? exp[1] or ALL_PARAMETERS.include? exp or params? exp[1]
@has_user_input = :params
elsif exp[1] == COOKIES or exp == COOKIES or cookies? exp[1]
@has_user_input = :cookies
elsif sexp? exp[1] and model_name? exp[1][1]
@has_user_input = :model
end
exp
end
#Note that params are included in current expression
def process_params exp
@has_user_input = :params
exp
end
#Note that cookies are included in current expression
def process_cookies exp
@has_user_input = :cookies
exp
end
private
#Report a warning
def warn options
@checks.add_warning Warning.new(options.merge({ :check => self.class.to_s }))
end
#Run _exp_ through OutputProcessor to get a nice String.
def format_output exp
OutputProcessor.new.format(exp).gsub(/\r|\n/, "")
end
#Checks if the model inherits from parent,
def parent? tracker, model, parent
if model == nil
false
elsif model[:parent] == parent
true
elsif model[:parent]
parent? tracker, tracker.models[model[:parent]], parent
else
false
end
end
#Checks if mass assignment is disabled globally in an initializer.
def mass_assign_disabled? tracker
matches = tracker.check_initializers(:"ActiveRecord::Base", :send)
if matches.empty?
false
else
matches.each do |result|
if result[3][3] == Sexp.new(:arg_list, Sexp.new(:lit, :attr_accessible), Sexp.new(:nil))
return true
end
end
end
end
#This is to avoid reporting duplicates. Checks if the result has been
#reported already from the same line number.
def duplicate? result, location = nil
line = result.line
location ||= (@current_template && @current_template[:name]) || @current_class || @current_module || @current_set || result[1]
location = location[:name] if location.is_a? Hash
location = location.to_sym
@results.each do |r|
if r[0] == line and r[1] == location
if OPTIONS[:combine_locations]
return true
elsif r[2] == result
return true
end
end
end
false
end
#Ignores ignores
def process_ignore exp
exp
end
#Does not actually process string interpolation, but notes that it occurred.
def process_string_interp exp
@string_interp = true
exp
end
#Checks if an expression contains string interpolation.
def include_interp? exp
@string_interp = false
process exp
@string_interp
end
#Checks if _exp_ includes parameters or cookies, but this only works
#with the base process_default.
def include_user_input? exp
@has_user_input = false
process exp
@has_user_input
end
#This is used to check for user input being used directly.
#
#Returns false if none is found, otherwise it returns an array
#where the first element is the type of user input
#(either :params or :cookies) and the second element is the matching
#expression
def has_immediate_user_input? exp
if params? exp
return :params, exp
elsif cookies? exp
return :cookies, exp
elsif call? exp
if sexp? exp[1]
if ALL_PARAMETERS.include? exp[1] or params? exp[1]
return :params, exp
elsif exp[1] == COOKIES
return :cookies, exp
else
false
end
else
false
end
elsif sexp? exp
case exp.node_type
when :string_interp
exp.each do |e|
if sexp? e
type, match = has_immediate_user_input?(e)
if type
return type, match
end
end
end
false
when :string_eval
if sexp? exp[1]
if exp[1].node_type == :rlist
exp[1].each do |e|
if sexp? e
type, match = has_immediate_user_input?(e)
if type
return type, match
end
end
end
false
else
has_immediate_user_input? exp[1]
end
end
when :format
has_immediate_user_input? exp[1]
when :if
(sexp? exp[2] and has_immediate_user_input? exp[2]) or
(sexp? exp[3] and has_immediate_user_input? exp[3])
else
false
end
end
end
#Checks for a model attribute at the top level of the
#expression.
def has_immediate_model? exp, out = nil
out = exp if out.nil?
if sexp? exp and exp.node_type == :output
exp = exp[1]
end
if call? exp
target = exp[1]
method = exp[2]
if call? target and not method.to_s[-1,1] == "?"
has_immediate_model? target, out
elsif model_name? target
exp
else
false
end
elsif sexp? exp
case exp.node_type
when :string_interp
exp.each do |e|
if sexp? e and match = has_immediate_model?(e, out)
return match
end
end
false
when :string_eval
if sexp? exp[1]
if exp[1].node_type == :rlist
exp[1].each do |e|
if sexp? e and match = has_immediate_model?(e, out)
return match
end
end
false
else
has_immediate_model? exp[1], out
end
end
when :format
has_immediate_model? exp[1], out
when :if
((sexp? exp[2] and has_immediate_model? exp[2], out) or
(sexp? exp[3] and has_immediate_model? exp[3], out))
else
false
end
end
end
#Checks if +exp+ is a model name.
#
#Prior to using this method, either @tracker must be set to
#the current tracker, or else @models should contain an array of the model
#names, which is available via tracker.models.keys
def model_name? exp
@models ||= @tracker.models.keys
if exp.is_a? Symbol
@models.include? exp
elsif sexp? exp
klass = nil
begin
klass = class_name exp
rescue StandardError
end
klass and @models.include? klass
else
false
end
end
#Finds entire method call chain where +target+ is a target in the chain
def find_chain exp, target
return unless sexp? exp
case exp.node_type
when :output, :format
find_chain exp[1], target
when :call
if exp == target or include_target? exp, target
return exp
end
else
exp.each do |e|
if sexp? e
res = find_chain e, target
return res if res
end
end
nil
end
end
#Returns true if +target+ is in +exp+
def include_target? exp, target
return false unless call? exp
exp.each do |e|
return true if e == target or include_target? e, target
end
false
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
require 'processors/lib/processor_helper'
require 'util'
require 'set'
#This check looks for unescaped output in templates which contains
#parameters or model attributes.
#
#For example:
#
# <%= User.find(:id).name %>
# <%= params[:id] %>
class CheckCrossSiteScripting < BaseCheck
Checks.add self
#Ignore these methods and their arguments.
#It is assumed they will take care of escaping their output.
IGNORE_METHODS = Set.new([:h, :escapeHTML, :link_to, :text_field_tag, :hidden_field_tag,
:image_tag, :select, :submit_tag, :hidden_field, :url_encode,
:radio_button, :will_paginate, :button_to, :url_for, :mail_to,
:fields_for, :label, :text_area, :text_field, :hidden_field, :check_box,
:field_field])
IGNORE_MODEL_METHODS = Set.new([:average, :count, :maximum, :minimum, :sum])
MODEL_METHODS = Set.new([:all, :find, :first, :last, :new])
IGNORE_LIKE = /^link_to_|_path|_tag|_url$/
HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers)
URI = Sexp.new(:const, :URI)
CGI = Sexp.new(:const, :CGI)
FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new, Sexp.new(:arglist))
#Run check
def run_check
IGNORE_METHODS.merge OPTIONS[:safe_methods]
@models = tracker.models.keys
@inspect_arguments = OPTIONS[:check_arguments]
tracker.each_template do |name, template|
@current_template = template
template[:outputs].each do |out|
type, match = has_immediate_user_input?(out[1])
if type
unless duplicate? out
add_result out
case type
when :params
warn :template => @current_template,
:warning_type => "Cross Site Scripting",
:message => "Unescaped parameter value",
:line => match.line,
:code => match,
:confidence => CONFIDENCE[:high]
when :cookies
warn :template => @current_template,
:warning_type => "Cross Site Scripting",
:message => "Unescaped cookie value",
:line => match.line,
:code => match,
:confidence => CONFIDENCE[:high]
end
end
elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out[1])
method = match[2]
unless duplicate? out or IGNORE_MODEL_METHODS.include? method
add_result out
if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:med]
end
code = find_chain out, match
warn :template => @current_template,
:warning_type => "Cross Site Scripting",
:message => "Unescaped model attribute",
:line => code.line,
:code => code,
:confidence => confidence
end
else
@matched = false
@mark = false
process out
end
end
end
end
#Process an output Sexp
def process_output exp
process exp[1]
end
#Check a call for user input
#
#
#Since we want to report an entire call and not just part of one, use @mark
#to mark when a call is started. Any dangerous values inside will then
#report the entire call chain.
def process_call exp
if @mark
actually_process_call exp
else
@mark = true
actually_process_call exp
message = nil
if @matched == :model and not OPTIONS[:ignore_model_output]
message = "Unescaped model attribute"
elsif @matched == :params
message = "Unescaped parameter value"
end
if message and not duplicate? exp
add_result exp
warn :template => @current_template,
:warning_type => "Cross Site Scripting",
:message => message,
:line => exp.line,
:code => exp,
:confidence => CONFIDENCE[:low]
end
@mark = @matched = false
end
exp
end
def actually_process_call exp
return if @matched
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
args = exp[3]
#Ignore safe items
if (target.nil? and (IGNORE_METHODS.include? method or method.to_s =~ IGNORE_LIKE)) or
(@matched == :model and IGNORE_MODEL_METHODS.include? method) or
(target == HAML_HELPERS and method == :html_escape) or
((target == URI or target == CGI) and method == :escape) or
(target == FORM_BUILDER and IGNORE_METHODS.include? method) or
(method.to_s[-1,1] == "?")
exp[0] = :ignore
@matched = false
elsif sexp? exp[1] and model_name? exp[1][1]
@matched = :model
elsif @inspect_arguments and (ALL_PARAMETERS.include?(exp) or params? exp)
@matched = :params
else
process args if @inspect_arguments
end
end
#Note that params have been found
def process_params exp
@matched = :params
exp
end
#Note that cookies have been found
def process_cookies exp
@matched = :cookies
exp
end
#Ignore calls to render
def process_render exp
exp
end
#Process as default
def process_string_interp exp
process_default exp
end
#Process as default
def process_format exp
process_default exp
end
#Ignore output HTML escaped via HAML
def process_format_escaped exp
exp
end
#Ignore condition in if Sexp
def process_if exp
exp[2..-1].each do |e|
process e if sexp? e
end
exp
end
end
require 'checks/base_check'
#Checks if default routes are allowed in routes.rb
class CheckDefaultRoutes < BaseCheck
Checks.add self
#Checks for :allow_all_actions globally and for individual routes
#if it is not enabled globally.
def run_check
if tracker.routes[:allow_all_actions]
#Default routes are enabled globally
warn :warning_type => "Default Routes",
:message => "All public methods in controllers are available as actions in routes.rb",
:line => tracker.routes[:allow_all_actions].line,
:confidence => CONFIDENCE[:high],
:file => "#{OPTIONS[:app_path]}/config/routes.rb"
else #Report each controller separately
tracker.routes.each do |name, actions|
if actions == :allow_all_actions
warn :controller => name,
:warning_type => "Default Routes",
:message => "Any public method in #{name} can be used as an action.",
:confidence => CONFIDENCE[:med],
:file => "#{OPTIONS[:app_path]}/config/routes.rb"
end
end
end
end
end
require 'checks/base_check'
#This check looks for calls to +eval+, +instance_eval+, etc. which include
#user input.
class CheckEvaluation < BaseCheck
Checks.add self
#Process calls
def run_check
calls = tracker.find_call nil, [:eval, :instance_eval, :class_eval, :module_eval]
@templates = tracker.templates
calls.each do |call|
process_result call
end
end
#Warns if result includes user input
def process_result result
if include_user_input? result[-1]
warn :result => result,
:warning_type => "Dangerous Eval",
:message => "User input in eval",
:code => result[-1],
:confidence => CONFIDENCE[:high]
end
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
#Checks for string interpolation and parameters in calls to
#Kernel#system, Kernel#exec, Kernel#syscall, and inside backticks.
#
#Examples of command injection vulnerabilities:
#
# system("rf -rf #{params[:file]}")
# exec(params[:command])
# `unlink #{params[:something}`
class CheckExecute < BaseCheck
Checks.add self
#Check models, controllers, and views for command injection.
def run_check
check_for_backticks tracker
calls = tracker.find_call [:IO, :Open3, :Kernel, []], [:exec, :popen, :popen3, :syscall, :system]
calls.each do |result|
process result
end
end
#Processes results from FindCall.
def process_result exp
call = exp[-1]
args = process call[3]
case call[2]
when :system, :exec
failure = include_user_input?(args[1]) || include_interp?(args[1])
else
failure = include_user_input?(args) || include_interp?(args)
end
if failure and not duplicate? call, exp[1]
add_result call, exp[1]
if @string_interp
confidence = CONFIDENCE[:med]
else
confidence = CONFIDENCE[:high]
end
warn :result => exp,
:warning_type => "Command Injection",
:message => "Possible command injection",
:line => call.line,
:code => call,
:confidence => confidence
end
exp
end
#Looks for calls using backticks such as
#
# `rm -rf #{params[:file]}`
def check_for_backticks tracker
tracker.each_method do |exp, set_name, method_name|
@current_set = set_name
@current_method = method_name
process exp
end
@current_set = nil
tracker.each_template do |name, template|
@current_template = template
process template[:src]
end
@current_template = nil
end
#Processes backticks.
def process_dxstr exp
return exp if duplicate? exp
add_result exp
if include_user_input? exp
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:med]
end
warning = { :warning_type => "Command Injection",
:message => "Possible command injection",
:line => exp.line,
:code => exp,
:confidence => confidence }
if @current_template
warning[:template] = @current_template
else
warning[:class] = @current_set
warning[:method] = @current_method
end
warn warning
exp
end
end
require 'checks/base_check'
require 'processors/lib/processor_helper'
#Checks for user input in methods which open or manipulate files
class CheckFileAccess < BaseCheck
Checks.add self
def run_check
methods = tracker.find_call [[:Dir, :File, :IO, :Kernel, :"Net::FTP", :"Net::HTTP", :PStore, :Pathname, :Shell, :YAML], []], [:[], :chdir, :chroot, :delete, :entries, :foreach, :glob, :install, :lchmod, :lchown, :link, :load, :load_file, :makedirs, :move, :new, :open, :read, :read_lines, :rename, :rmdir, :safe_unlink, :symlink, :syscopy, :sysopen, :truncate, :unlink]
methods.concat tracker.find_call(:FileUtils, nil)
methods.each do |call|
process_result call
end
end
def process_result result
call = result[-1]
file_name = call[3][1]
if check = include_user_input?(file_name)
unless duplicate? call, result[1]
add_result call, result[1]
if check == :params
message = "Parameter"
elsif check == :cookies
message = "Cookie"
else
message = "User input"
end
message << " value used in file name"
warn :result => result,
:warning_type => "File Access",
:message => message,
:confidence => CONFIDENCE[:high],
:line => call.line,
:code => call
end
end
end
end
require 'checks/base_check'
#Checks that +protect_from_forgery+ is set in the ApplicationController
class CheckForgerySetting < BaseCheck
Checks.add self
def run_check
app_controller = tracker.controllers[:ApplicationController]
if tracker.config[:rails][:action_controller] and
tracker.config[:rails][:action_controller][:allow_forgery_protection] == Sexp.new(:false)
warn :controller => :ApplicationController,
:warning_type => "Cross Site Request Forgery",
:message => "Forgery protection is disabled",
:confidence => CONFIDENCE[:high]
elsif app_controller and not app_controller[:options][:protect_from_forgery]
warn :controller => :ApplicationController,
:warning_type => "Cross-Site Request Forgery",
:message => "'protect_from_forgery' should be called in ApplicationController",
:confidence => CONFIDENCE[:high]
end
end
end
require 'checks/base_check'
#Checks for mass assignments to models.
#
#See http://guides.rubyonrails.org/security.html#mass-assignment for details
class CheckMassAssignment < BaseCheck
Checks.add self
def run_check
return if mass_assign_disabled? tracker
models = []
tracker.models.each do |name, m|
if parent?(tracker, m, :"ActiveRecord::Base") and m[:attr_accessible].nil?
models << name
end
end
return if models.empty?
@results = Set.new
calls = tracker.find_call models, [:new,
:attributes=,
:update_attribute,
:update_attributes,
:update_attributes!]
calls.each do |result|
process result
end
end
#All results should be Model.new(...) or Model.attributes=() calls
def process_result res
call = res[-1]
check = check_call call
if check and not @results.include? call
@results << call
if include_user_input? call[3]
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:med]
end
warn :result => res,
:warning_type => "Mass Assignment",
:message => "Unprotected mass assignment",
:line => call.line,
:code => call,
:confidence => confidence
end
res
end
#Want to ignore calls to Model.new that have no arguments
def check_call call
args = process call[3]
if args.length <= 1 #empty new()
false
elsif hash? args[1]
#Still should probably check contents of hash
false
else
true
end
end
end
require 'checks/base_check'
#Check if mass assignment is used with models
#which inherit from ActiveRecord::Base.
#
#If OPTIONS[:collapse_mass_assignment] is +true+ (default), all models which do
#not use attr_accessible will be reported in a single warning
class CheckModelAttributes < BaseCheck
Checks.add self
def run_check
return if mass_assign_disabled? tracker
names = []
tracker.models.each do |name, model|
if model[:attr_accessible].nil? and parent? tracker, model, :"ActiveRecord::Base"
if OPTIONS[:collapse_mass_assignment]
names << name.to_s
else
warn :model => name,
:warning_type => "Attribute Restriction",
:message => "Mass assignment is not restricted using attr_accessible",
:confidence => CONFIDENCE[:high]
end
end
end
if OPTIONS[:collapse_mass_assignment] and not names.empty?
warn :model => names.sort.join(", "),
:warning_type => "Attribute Restriction",
:message => "Mass assignment is not restricted using attr_accessible",
:confidence => CONFIDENCE[:high]
end
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
#Reports any calls to +redirect_to+ which include parameters in the arguments.
#
#For example:
#
# redirect_to params.merge(:action => :elsewhere)
class CheckRedirect < BaseCheck
Checks.add self
def run_check
@tracker.find_call(nil, :redirect_to).each do |c|
process c
end
end
def process_result exp
call = exp[-1]
method = call[2]
if method == :redirect_to and not only_path?(call) and res = include_user_input?(call)
if res == :immediate
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:low]
end
warn :result => exp,
:warning_type => "Redirect",
:message => "Possible unprotected redirect",
:line => call.line,
:code => call,
:confidence => confidence
end
exp
end
#Custom check for user input. First looks to see if the user input
#is being output directly. This is necessary because of OPTIONS[:check_arguments]
#which can be used to enable/disable reporting output of method calls which use
#user input as arguments.
def include_user_input? call
if OPTIONS[:ignore_redirect_to_model] and call? call[3][1] and
call[3][1][2] == :new and call[3][1][1]
begin
target = class_name call[3][1][1]
if @tracker.models.include? target
return false
end
rescue
end
end
call[3].each do |arg|
if call? arg
if ALL_PARAMETERS.include? arg or arg[2] == COOKIES
return :immediate
elsif arg[2] == :url_for and include_user_input? arg
return :immediate
#Ignore helpers like some_model_url?
elsif arg[2].to_s =~ /_(url|path)$/
return false
end
elsif params? arg or cookies? arg
return :immediate
end
end
if OPTIONS[:check_arguments]
super
else
false
end
end
#Checks +redirect_to+ arguments for +only_path => true+ which essentially
#nullifies the danger posed by redirecting with user input
def only_path? call
call[3].each do |arg|
if hash? arg
hash_iterate(arg) do |k,v|
if symbol? k and k[1] == :only_path and v.is_a? Sexp and v[0] == :true
return true
end
end
elsif call? arg and arg[2] == :url_for
return only_path?(arg)
end
end
false
end
end
require 'checks/base_check'
#Check calls to +render()+ for dangerous values
class CheckRender < BaseCheck
Checks.add self
def run_check
tracker.each_method do |src, class_name, method_name|
@current_class = class_name
@current_method = method_name
process src
end
tracker.each_template do |name, template|
@current_template = template
process template[:src]
end
end
def process_render exp
case exp[1]
when :partial, :template, :action, :file
check_for_dynamic_path exp
when :inline
when :js
when :json
when :text
when :update
when :xml
end
exp
end
#Check if path to action or file is determined dynamically
def check_for_dynamic_path exp
view = exp[2]
if sexp? view and view.node_type != :str and view.node_type != :lit and not duplicate? exp
add_result exp
if include_user_input? view
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:low]
end
warning = { :warning_type => "Dynamic Render Path",
:message => "Render path is dynamic",
:line => exp.line,
:code => exp,
:confidence => confidence }
if @current_template
warning[:template] = @current_template
else
warning[:class] = @current_class
warning[:method] = @current_method
end
warn warning
end
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
#This check looks for calls to +send()+ which use user input
#for or in the method name.
#
#For example:
#
# some_object.send(params[:input], "hello")
#
#This is dangerous for (at least) two reasons. First, the user is controlling
#what method is being called, which cannot be good. Secondly, the method
#name will be converted to a Symbol, which is never garbage collected. This
#could possibly lead to DoS attacks by using up memory.
class CheckSend < BaseCheck
Checks.add self
#Check calls to +send+ and +__send__+
def run_check
calls = tracker.find_call nil, [:send, :__send__]
calls.each do |result|
process result
end
end
#Check instances of +send+ which have user input in the method name
def process_result exp
call = exp[-1]
method_name = process call[3][1]
message = nil
confidence = nil
type, = has_immediate_user_input? method_name
if type
confidence = CONFIDENCE[:high]
elsif match = has_immediate_model?(method_name)
type = :model
confidence = CONFIDENCE[:high]
elsif type = include_user_input?(method_name)
confidence = CONFIDENCE[:med]
end
if type == :model
message = "Database value used to generate method name"
elsif type
message = "User input used to generate method name"
end
if message and confidence and not duplicate? call
add_result call
warn :result => exp,
:warning_type => "Object#send",
:message => message,
:line => call.line,
:code => call,
:confidence => confidence
end
exp
end
end
require 'checks/check_file_access'
require 'processors/lib/processor_helper'
#Checks for user input in send_file()
class CheckSendFile < CheckFileAccess
Checks.add self
def run_check
methods = tracker.find_call nil, :send_file
methods.each do |call|
process_result call
end
end
end
require 'checks/base_check'
class CheckSessionSettings < BaseCheck
Checks.add self
def run_check
settings = tracker.config[:rails] and
tracker.config[:rails][:action_controller] and
tracker.config[:rails][:action_controller][:session]
if settings and hash? settings
hash_iterate settings do |key, value|
if symbol? key
if key[1] == :session_http_only and
sexp? value and
value.node_type == :false
warn :warning_type => "Session Setting",
:message => "Session cookies should be set to HTTP only",
:confidence => CONFIDENCE[:high]
elsif key[1] == :secret and
string? value and
value[1].length < 30
warn :warning_type => "Session Setting",
:message => "Session secret should be at least 30 characters long",
:confidence => CONFIDENCE[:high]
end
end
end
end
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
#This check tests for find calls which do not use Rails' auto SQL escaping
#
#For example:
# Project.find(:all, :conditions => "name = '" + params[:name] + "'")
#
# Project.find(:all, :conditions => "name = '#{params[:name]}'")
#
# User.find_by_sql("SELECT * FROM projects WHERE name = '#{params[:name]}'")
class CheckSQL < BaseCheck
Checks.add self
def run_check
@rails_version = tracker.config[:rails_version]
calls = tracker.find_model_find tracker.models.keys
calls.concat tracker.find_call([], /^(find.*|last|first|all|count|sum|average|minumum|maximum)$/)
calls.each do |c|
process c
end
end
#Process result from FindCall.
def process_result exp
call = exp[-1]
args = process call[3]
if call[2] == :find_by_sql
failed = check_arguments args[1]
elsif call[2].to_s =~ /^find/
failed = (args.length > 2 and check_arguments args[-1])
else
failed = (args.length > 1 and check_arguments args[-1])
end
if failed and not duplicate? call, exp[1]
add_result call, exp[1]
if include_user_input? args[-1]
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:med]
end
warn :result => exp,
:warning_type => "SQL Injection",
:message => "Possible SQL injection",
:confidence => confidence
end
if check_for_limit_or_offset_vulnerability args[-1]
if include_user_input? args[-1]
confidence = CONFIDENCE[:high]
else
confidence = CONFIDENCE[:low]
end
warn :result => exp,
:warning_type => "SQL Injection",
:message => "Upgrade to Rails >= 2.1.2 to escape :limit and :offset. Possible SQL injection",
:confidence => confidence
end
exp
end
private
#Check arguments for any string interpolation
def check_arguments arg
if sexp? arg
case arg.node_type
when :hash
hash_iterate(arg) do |key, value|
if check_arguments value
return true
end
end
when :array
return check_arguments(arg[1])
when :string_interp
return true
when :call
return check_call(arg)
end
end
false
end
#Check call for user input and string building
def check_call exp
target = exp[1]
method = exp[2]
args = exp[3]
if sexp? target and
(method == :+ or method == :<< or method == :concat) and
(string? target or include_user_input? exp)
true
elsif call? target
check_call target
else
false
end
end
#Prior to Rails 2.1.1, the :offset and :limit parameters were not
#escaping input properly.
#
#http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/
def check_for_limit_or_offset_vulnerability options
return false if @rails_version.nil? or @rails_version >= "2.1.1" or not hash? options
hash_iterate(options) do |key, value|
if symbol? key
return (key[1] == :limit or key[1] == :offset)
end
end
false
end
end
require 'checks/base_check'
require 'processors/lib/find_call'
#If an application turns user input into Symbols, there is a possiblity
#of DoS by using up all the memory.
class CheckSymbolCreation < BaseCheck
Checks.add self
def run_check
calls = tracker.find_call nil, :to_sym
calls.each do |result|
process_call_to_sym result
end
end
#Check calls to .to_sym which have user input as a target.
def process_call_to_sym exp
call = exp[-1]
confidence = nil
type, = has_immediate_user_input? call[1]
if type
confidence = CONFIDENCE[:high]
elsif match = has_immediate_model?(call[1])
type = :model
confidence = CONFIDENCE[:high]
elsif type = include_user_input?(call[1])
confidence = CONFIDENCE[:med]
end
if type and not duplicate? exp
add_result exp
if res == :model
message = "Symbol created from database value"
else
message = "Symbol created from user input"
end
warn :result => exp,
:warning_type => "Symbol Creation",
:message => message,
:line => call.line,
:code => call,
:confidence => CONFIDENCE[:med]
end
exp
end
end
require 'checks/base_check'
#Reports any calls to +validates_format_of+ which do not use +\A+ and +\z+
#as anchors in the given regular expression.
#
#For example:
#
# #Allows anything after new line
# validates_format_of :user_name, :with => /^\w+$/
class CheckValidationRegex < BaseCheck
Checks.add self
WITH = Sexp.new(:lit, :with)
def run_check
tracker.models.each do |name, model|
@current_model = name
format_validations = model[:options][:validates_format_of]
if format_validations
format_validations.each do |v|
process_validator v
end
end
end
end
#Check validates_format_of
def process_validator validator
hash_iterate(validator[-1]) do |key, value|
if key == WITH
check_regex value, validator
end
end
end
#Issue warning if the regular expression does not use
#+\A+ and +\z+
def check_regex value, validator
return unless regexp? value
regex = value[1].inspect
if regex =~ /[^\A].*[^\z]\/(m|i|x|n|e|u|s|o)*\z/
warn :model => @current_model,
:warning_type => "Format Validation",
:message => "Insufficient validation for '#{get_name validator}' using #{value[1].inspect}. Use \\A and \\z as anchors",
:line => value.line,
:confidence => CONFIDENCE[:high]
end
end
#Get the name of the attribute being validated.
def get_name validator
name = validator[1]
if sexp? name
name[1]
else
name
end
end
end
/* CSS style used for HTML reports */
body {
font-family: sans-serif;
color: #161616;
}
p {
font-weight: bold;
font-size: 11pt;
color: #2D0200;
}
th {
background-color: #980905;
border-bottom: 5px solid #530200;
color: white;
font-size: 11pt;
padding: 1px 8px 1px 8px;
}
td {
border-bottom: 2px solid white;
font-family: monospace;
padding: 5px 8px 1px 8px;
}
table {
background-color: #FCF4D4;
border-collapse: collapse;
}
h1 {
color: #2D0200;
font-size: 14pt;
}
h2 {
color: #2D0200;
font-size: 12pt;
}
span.high-confidence {
font-weight:bold;
color: red;
}
span.med-confidence {
}
span.weak-confidence {
color:gray;
}
div.warning_message {
cursor: pointer;
}
div.warning_message:hover {
background-color: white;
}
table.context {
margin-top: 5px;
margin-bottom: 5px;
border-left: 1px solid #90e960;
color: #212121;
}
tr.context {
background-color: white;
}
tr.first {
border-top: 1px solid #7ecc54;
padding-top: 2px;
}
tr.error {
background-color: #f4c1c1 !important
}
tr.near_error {
background-color: #f4d4d4 !important
}
tr.alt {
background-color: #e8f4d4;
}
td.context {
padding: 2px 10px 0px 6px;
border-bottom: none;
}
td.context_line {
padding: 2px 8px 0px 7px;
border-right: 1px solid #b3bda4;
border-bottom: none;
color: #6e7465;
}
pre.context {
margin-bottom: 1px;
}
#Load all files in processors/
Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/processors/*.rb").each { |f| require f.match(/processors.*/)[0] }
require 'tracker'
require 'set'
#Makes calls to the appropriate processor.
#
#The ControllerProcessor, TemplateProcessor, and ModelProcessor will
#update the Tracker with information about what is parsed.
class Processor
def initialize
@tracker = Tracker.new self
end
def tracked_events
@tracker
end
#Process configuration file source
def process_config src
ConfigProcessor.new(@tracker).process_config src
end
#Process route file source
def process_routes src
RoutesProcessor.new(@tracker).process_routes src
end
#Process controller source. +file_name+ is used for reporting
def process_controller src, file_name
ControllerProcessor.new(@tracker).process_controller src, file_name
end
#Process variable aliasing in controller source and save it in the
#tracker.
def process_controller_alias src
ControllerAliasProcessor.new(@tracker).process src
end
#Process a model source
def process_model src, file_name
result = ModelProcessor.new(@tracker).process_model src, file_name
AliasProcessor.new.process result
end
#Process either an ERB or HAML template
def process_template name, src, type, called_from = nil, file_name = nil
case type
when :erb
result = ErbTemplateProcessor.new(@tracker, name, called_from, file_name).process src
when :haml
result = HamlTemplateProcessor.new(@tracker, name, called_from, file_name).process src
else
abort "Unknown template type: #{type} (#{name})"
end
#Each template which is rendered is stored separately
#with a new name.
if called_from
name = (name.to_s + "." + called_from.to_s).to_sym
end
@tracker.templates[name][:src] = result
@tracker.templates[name][:type] = type
end
#Process any calls to render() within a template
def process_template_alias template
TemplateAliasProcessor.new(@tracker, template).process_safely template[:src]
end
#Process source for initializing files
def process_initializer name, src
res = BaseProcessor.new(@tracker).process src
res = AliasProcessor.new.process res
@tracker.initializers[name] = res
end
#Process source for a library file
def process_lib src, file_name
LibraryProcessor.new(@tracker).process_library src, file_name
end
end
require 'rubygems'
require 'sexp_processor'
require 'util'
require 'processors/lib/processor_helper'
#Returns an s-expression with aliases replaced with their value.
#This does not preserve semantics (due to side effects, etc.), but it makes
#processing easier when searching for various things.
class AliasProcessor < SexpProcessor
include ProcessorHelper
include Util
attr_reader :result
#Returns a new AliasProcessor with an empty environment.
#
#The recommended usage is:
#
# AliasProcessor.new.process_safely src
def initialize
super()
self.strict = false
self.auto_shift_type = false
self.require_empty = false
self.default_method = :process_default
self.warn_on_default = false
@env = SexpProcessor::Environment.new
set_env_defaults
end
#This method processes the given Sexp, but copies it first so
#the original argument will not be modified.
#
#_set_env_ should be an instance of SexpProcessor::Environment. If provided,
#it will be used as the starting environment.
#
#This method returns a new Sexp with variables replaced with their values,
#where possible.
def process_safely src, set_env = nil
@env = Marshal.load(Marshal.dump(set_env)) if set_env
@result = src.deep_clone
process @result
#Process again to propogate replaced variables and process more.
#For example,
# x = [1,2]
# y = [3,4]
# z = x + y
#
#After first pass:
#
# z = [1,2] + [3,4]
#
#After second pass:
#
# z = [1,2,3,4]
if set_env
@env = set_env
else
@env = SexpProcessor::Environment.new
end
process @result
@result
end
#Process a Sexp. If the Sexp has a value associated with it in the
#environment, that value will be returned.
def process_default exp
begin
type = exp.shift
exp.each_with_index do |e, i|
if sexp? e and not e.empty?
exp[i] = process e
else
e
end
end
rescue Exception => err
@tracker.error err if @tracker
ensure
#The type must be put back on, or else later processing
#will trip up on it
exp.unshift type
end
#Generic replace
if replacement = env[exp]
set_line replacement.deep_clone, exp.line
else
exp
end
end
#Process a method call.
def process_call exp
target_var = exp[1]
exp = process_default exp
#In case it is replaced with something else
return exp unless call? exp
target = exp[1]
method = exp[2]
args = exp[3]
#See if it is possible to simplify some basic cases
#of addition/concatenation.
case method
when :+
if array? target and array? args[1]
joined = join_arrays target, args[1]
joined.line(exp.line)
exp = joined
elsif string? target and string? args[1]
joined = join_strings target, args[1]
joined.line(exp.line)
exp = joined
end
when :[]
if array? target
temp_exp = process_array_access target, args[1..-1]
exp = temp_exp if temp_exp
elsif hash? target
temp_exp = process_hash_access target, args[1..-1]
exp = temp_exp if temp_exp
end
when :merge!, :update
if hash? target and hash? args[1]
target = process_hash_merge! target, args[1]
env[target_var] = target
return target
end
when :merge
if hash? target and hash? args[1]
return process_hash_merge(target, args[1])
end
end
exp
end
#Process a new scope.
def process_scope exp
env.scope do
process exp[1]
end
exp
end
#Start new scope for block.
def process_block exp
env.scope do
process_default exp
end
end
#Process a method definition.
def process_methdef exp
env.scope do
set_env_defaults
process exp[3]
end
exp
end
#Process a method definition on self.
def process_selfdef exp
env.scope do
set_env_defaults
process exp[4]
end
exp
end
alias process_defn process_methdef
alias process_defs process_selfdef
#Local assignment
# x = 1
def process_lasgn exp
exp[2] = process exp[2] if sexp? exp[2]
local = Sexp.new(:lvar, exp[1]).line(exp.line || -2)
env[local] = exp[2]
exp
end
#Instance variable assignment
# @x = 1
def process_iasgn exp
exp[2] = process exp[2]
ivar = Sexp.new(:ivar, exp[1]).line(exp.line)
env[ivar] = exp[2]
exp
end
#Global assignment
# $x = 1
def process_gasgn exp
match = Sexp.new(:gvar, exp[1])
value = exp[2] = process(exp[2])
env[match] = value
exp
end
#Class variable assignment
# @@x = 1
def process_cvdecl exp
match = Sexp.new(:cvar, exp[1])
value = exp[2] = process(exp[2])
env[match] = value
exp
end
#'Attribute' assignment
# x.y = 1
#or
# x[:y] = 1
def process_attrasgn exp
tar_variable = exp[1]
target = exp[1] = process(exp[1])
method = exp[2]
if method == :[]=
index = exp[3][1] = process(exp[3][1])
value = exp[3][2] = process(exp[3][2])
match = Sexp.new(:call, target, :[], Sexp.new(:arglist, index))
env[match] = value
if hash? target
env[tar_variable] = hash_insert target.deep_clone, index, value
end
elsif method.to_s[-1,1] == "="
value = exp[3][1] = process(exp[3][1])
#This is what we'll replace with the value
match = Sexp.new(:call, target, method.to_s[0..-2].to_sym, Sexp.new(:arglist))
env[match] = value
else
raise "Unrecognized assignment: #{exp}"
end
exp
end
#Merge values into hash when processing
#
# h.merge! :something => "value"
def process_hash_merge! hash, args
hash = hash.deep_clone
hash_iterate args do |key, replacement|
hash_insert hash, key, replacement
match = Sexp.new(:call, hash, :[], Sexp.new(:arglist, key))
env[match] = replacement
end
hash
end
#Return a new hash Sexp with the given values merged into it.
#
#+args+ should be a hash Sexp as well.
def process_hash_merge hash, args
hash = hash.deep_clone
hash_iterate args do |key, replacement|
hash_insert hash, key, replacement
end
hash
end
#Assignments like this
# x[:y] ||= 1
def process_op_asgn1 exp
return process_default(exp) if exp[3] != :"||"
target = exp[1] = process(exp[1])
index = exp[2][1] = process(exp[2][1])
value = exp[4] = process(exp[4])
match = Sexp.new(:call, target, :[], Sexp.new(:arglist, index))
unless env[match]
env[match] = value
end
exp
end
#Assignments like this
# x.y ||= 1
def process_op_asgn2 exp
return process_default(exp) if exp[3] != :"||"
target = exp[1] = process(exp[1])
value = exp[4] = process(exp[4])
method = exp[2]
match = Sexp.new(:call, target, method.to_s[0..-2].to_sym, Sexp.new(:arglist))
unless env[match]
env[match] = value
end
exp
end
#Constant assignments like
# BIG_CONSTANT = 234810983
def process_cdecl exp
if sexp? exp[2]
exp[2] = process exp[2]
end
if exp[1].is_a? Symbol
match = Sexp.new(:const, exp[1])
else
match = exp[1]
end
env[match] = exp[2]
exp
end
#Process single integer access to an array.
#
#Returns the value inside the array, if possible.
def process_array_access target, args
if args.length == 1 and integer? args[0]
index = args[0][1]
target[index + 1]
else
nil
end
end
#Process hash access by returning the value associated
#with the given arguments.
def process_hash_access target, args
if args.length == 1
index = args[0]
hash_iterate(target) do |key, value|
if key == index
return value
end
end
end
nil
end
#Join two array literals into one.
def join_arrays array1, array2
result = Sexp.new(:array)
result.concat array1[1..-1]
result.concat array2[1..-1]
end
#Join two string literals into one.
def join_strings string1, string2
result = Sexp.new(:str)
result[1] = string1[1] + string2[1]
result
end
#Returns a new SexpProcessor::Environment containing only instance variables.
#This is useful, for example, when processing views.
def only_ivars
res = SexpProcessor::Environment.new
env.all.each do |k, v|
res[k] = v if k.node_type == :ivar
end
res
end
#Set line nunber for +exp+ and every Sexp it contains. Used when replacing
#expressions, so warnings indicate the correct line.
def set_line exp, line_number
if sexp? exp
exp.line(line_number)
exp.each do |e|
set_line e, line_number
end
end
exp
end
end
require 'rubygems'
require 'sexp_processor'
require 'processors/lib/processor_helper'
require 'util'
#Base processor for most processors.
class BaseProcessor < SexpProcessor
include ProcessorHelper
include Util
attr_reader :ignore
#Return a new Processor.
def initialize tracker
super()
self.strict = false
self.auto_shift_type = false
self.require_empty = false
self.default_method = :process_default
self.warn_on_default = false
@last = nil
@tracker = tracker
@ignore = Sexp.new :ignore
@current_template = @current_module = @current_class = @current_method = nil
end
#Process a new scope. Removes expressions that are set to nil.
def process_scope exp
exp.shift
exp.map! do |e|
res = process e
if res.empty?
res = nil
else
res
end
end.compact
exp.unshift :scope
end
#Default processing.
def process_default exp
type = exp.shift
exp.each_with_index do |e, i|
if sexp? e and not e.empty?
exp[i] = process e
else
e
end
end
ensure
exp.unshift type
end
#Process an if statement.
def process_if exp
exp[1] = process exp[1]
exp[2] = process exp[2] if exp[2]
exp[3] = process exp[3] if exp[3]
exp
end
#Processes calls with blocks. Changes Sexp node type to :call_with_block
#
#s(:iter, CALL, {:lasgn|:masgn}, BLOCK)
def process_iter exp
call = process exp[1]
#deal with assignments somehow
if exp[3]
block = process exp[3]
block = nil if block.empty?
else
block = nil
end
call = Sexp.new(:call_with_block, call, exp[2], block).compact
call.line(exp.line)
call
end
#String with interpolation. Changes Sexp node type to :string_interp
def process_dstr exp
exp.shift
exp.map! do |e|
if e.is_a? String
e
elsif e[1].is_a? String
e[1]
else
res = process e
if res.empty?
nil
else
res
end
end
end.compact!
exp.unshift :string_interp
end
#Processes a block. Changes Sexp node type to :rlist
def process_block exp
exp.shift
exp.map! do |e|
process e
end
exp.unshift :rlist
end
#Processes the inside of an interpolated String.
#Changes Sexp node type to :string_eval
def process_evstr exp
exp[0] = :string_eval
exp[1] = process exp[1]
exp
end
#Processes an or keyword
def process_or exp
exp[1] = process exp[1]
exp[2] = process exp[2]
exp
end
#Processes an and keyword
def process_and exp
exp[1] = process exp[1]
exp[2] = process exp[2]
exp
end
#Processes a hash
def process_hash exp
exp.shift
exp.map! do |e|
if sexp? e
process e
else
e
end
end
exp.unshift :hash
end
#Processes the values in an argument list
def process_arglist exp
exp.shift
exp.map! do |e|
process e
end
exp.unshift :arglist
end
#Processes a local assignment
def process_lasgn exp
exp[2] = process exp[2]
exp
end
#Processes an instance variable assignment
def process_iasgn exp
exp[2] = process exp[2]
exp
end
#Processes an attribute assignment, which can be either x.y = 1 or x[:y] = 1
def process_attrasgn exp
exp[1] = process exp[1]
exp[3] = process exp[3]
exp
end
#Ignore ignore Sexps
def process_ignore exp
exp
end
#Generates :render node from call to render.
def make_render exp
render_type, value, rest = find_render_type exp[3]
rest = process rest
result = Sexp.new(:render, render_type, value, rest)
result.line(exp.line)
result
end
#Determines the type of a call to render.
#
#Possible types are:
#:action, :default :file, :inline, :js, :json, :nothing, :partial,
#:template, :text, :update, :xml
def find_render_type args
rest = Sexp.new(:hash)
type = nil
value = nil
if args.length == 2 and args[-1] == Sexp.new(:lit, :update)
return :update, nil, args[0..-2]
end
#Look for render :action, ... or render "action", ...
if string? args[1] or symbol? args[1]
type = :action
value = args[1]
elsif args[1].is_a? Symbol or args[1].is_a? String
type = :action
value = Sexp.new(:lit, args[1].to_sym)
elsif not hash? args[1]
type = :action
value = args[1]
end
if hash? args[-1]
hash_iterate(args[-1]) do |key, val|
case key[1]
when :action, :file, :inline, :js, :json, :nothing, :partial, :text, :update, :xml
type = key[1]
value = val
else
rest << key << val
end
end
end
type ||= :default
value ||= :default
args[-1] = rest
return type, value, rest
end
end
require 'processors/base_processor'
require 'processors/alias_processor'
#Replace block variable in
#
# Rails::Initializer.run |config|
#
#with this value so we can keep track of it.
RAILS_CONFIG = Sexp.new(:const, :"!BRAKEMAN_RAILS_CONFIG")
#Processes configuration. Results are put in tracker.config.
#
#Configuration of Rails via Rails::Initializer are stored in tracker.config[:rails].
#For example:
#
# Rails::Initializer.run |config|
# config.action_controller.session_store = :cookie_store
# end
#
#will be stored in
#
# tracker.config[:rails][:action_controller][:session_store]
#
#Values for tracker.config[:rails] will still be Sexps.
class ConfigProcessor < BaseProcessor
def initialize *args
super
@tracker.config[:rails] ||= {}
end
#Use this method to process configuration file
def process_config src
res = ConfigAliasProcessor.new.process_safely(src)
process res
end
#Check if config is set to use Erubis
def process_call exp
target = exp[1]
target = process target if sexp? target
if exp[2] == :gem and exp[3][1][1] == "erubis"
warn "[Notice] Using Erubis for ERB templates"
@tracker.config[:erubis] = true
end
exp
end
#Look for configuration settings
def process_attrasgn exp
if exp[1] == RAILS_CONFIG
#Get rid of '=' at end
attribute = exp[2].to_s[0..-2].to_sym
if exp[3].length > 2
#Multiple arguments?...not sure if this will ever happen
@tracker.config[:rails][exp[2]] = exp[3][1..-1]
else
@tracker.config[:rails][exp[2]] = exp[3][1]
end
elsif include_rails_config? exp
options = get_rails_config exp
level = @tracker.config[:rails]
options[0..-2].each do |o|
level[o] ||= {}
level = level[o]
end
level[options.last] = exp[3][1]
end
exp
end
#Check for Rails version
def process_cdecl exp
#Set Rails version required
if exp[1] == :RAILS_GEM_VERSION
@tracker.config[:rails_version] = exp[2][1]
end
exp
end
#Check if an expression includes a call to set Rails config
def include_rails_config? exp
target = exp[1]
if call? target
if target[1] == RAILS_CONFIG
true
else
include_rails_config? target
end
elsif target == RAILS_CONFIG
true
else
false
end
end
#Returns an array of symbols for each 'level' in the config
#
# config.action_controller.session_store = :cookie
#
#becomes
#
# [:action_controller, :session_store]
def get_rails_config exp
if sexp? exp and exp.node_type == :attrasgn
attribute = exp[2].to_s[0..-2].to_sym
get_rails_config(exp[1]) << attribute
elsif call? exp
if exp[1] == RAILS_CONFIG
[exp[2]]
else
get_rails_config(exp[1]) << exp[2]
end
else
raise "WHAT"
end
end
end
#This is necessary to replace block variable so we can track config settings
class ConfigAliasProcessor < AliasProcessor
RAILS_INIT = Sexp.new(:colon2, Sexp.new(:const, :Rails), :Initializer)
#Look for a call to
#
# Rails::Initializer.run do |config|
# ...
# end
#
#and replace config with RAILS_CONFIG
def process_iter exp
target = exp[1][1]
method = exp[1][2]
if sexp? target and target == RAILS_INIT and method == :run
exp[2][2] = RAILS_CONFIG
end
process_default exp
end
end
require 'processors/alias_processor'
require 'processors/lib/render_helper'
#Processes aliasing in controllers, but includes following
#renders in routes and putting variables into templates
class ControllerAliasProcessor < AliasProcessor
include RenderHelper
def initialize tracker
super()
@tracker = tracker
@rendered = false
@current_class = @current_module = @current_method = nil
end
#Processes a class which is probably a controller.
def process_class exp
@current_class = class_name(exp[1])
if @current_module
@current_class = (@current_module + "::" + @current_class.to_s).to_sym
end
process_default exp
end
#Processes a method definition, which may include
#processing any rendered templates.
def process_methdef exp
set_env_defaults
is_route = route? exp[1]
other_method = @current_method
@current_method = exp[1]
@rendered = false if is_route
env.scope do
if is_route
before_filter_list(@current_method, @current_class).each do |f|
process_before_filter f
end
end
process exp[3]
if is_route and not @rendered
process_default_render exp
end
end
@current_method = other_method
exp
end
#Look for calls to head()
def process_call exp
exp = super
if exp[2] == :head
@rendered = true
end
exp
end
#Check for +respond_to+
def process_call_with_block exp
process_default exp
if exp[1][2] == :respond_to
@rendered = true
end
exp
end
#Processes a call to a before filter.
#Basically, adds any instance variable assignments to the environment.
#TODO: method arguments?
def process_before_filter name
method = find_method name, @current_class
if method.nil?
warn "[Notice] Could not find filter #{name}" if OPTIONS[:debug]
return
end
processor = AliasProcessor.new
processor.process_safely(method[3])
processor.only_ivars.all.each do |variable, value|
env[variable] = value
end
end
#Processes the default template for the current action
def process_default_render exp
process_template template_name, nil
end
#Process template and add the current class and method name as called_from info
def process_template name, args
super name, args, "#@current_class##@current_method"
end
#Turns a method name into a template name
def template_name name = nil
name ||= @current_method
name = name.to_s
if name.include? "/"
name
else
controller = @current_class.to_s.gsub("Controller", "")
controller.gsub!("::", "/")
underscore(controller + "/" + name.to_s)
end
end
#Returns true if the given method name is also a route
def route? method
return true if @tracker.routes[:allow_all_actions]
routes = @tracker.routes[@current_class]
routes and (routes == :allow_all_actions or routes.include? method)
end
#Get list of filters, including those that are inherited
def before_filter_list method, klass
controller = @tracker.controllers[klass]
filters = []
while controller
filters = get_before_filters(method, controller) + filters
controller = @tracker.controllers[controller[:parent]]
end
filters
end
#Returns an array of filter names
def get_before_filters method, controller
filters = []
return filters unless controller[:options]
filter_list = controller[:options][:before_filters]
return filters unless filter_list
filter_list.each do |filter|
f = before_filter_to_hash filter
if f[:all] or
(f[:only] == method) or
(f[:only].is_a? Array and f[:only].include? method) or
(f[:except] == method) or
(f[:except].is_a? Array and not f[:except].include? method)
filters.concat f[:methods]
end
end
filters
end
#Returns a before filter as a hash table
def before_filter_to_hash args
filter = {}
#Process args for the uncommon but possible situation
#in which some variables are used in the filter.
args.each do |a|
if sexp? a
a = process_default a
end
end
filter[:methods] = [args[0][1]]
args[1..-1].each do |a|
filter[:methods] << a[1] unless a.node_type == :hash
end
if args[-1].node_type == :hash
option = args[-1][1][1]
value = args[-1][2]
case value.node_type
when :array
filter[option] = value[1..-1].map {|v| v[1] }
when :lit, :str
filter[option] = value[1]
else
warn "[Notice] Unknown before_filter value: #{option} => #{value}" if OPTIONS[:debug]
end
else
filter[:all] = true
end
filter
end
#Finds a method in the given class or a parent class
def find_method method_name, klass
return nil if sexp? method_name
method_name = method_name.to_sym
controller = @tracker.controllers[klass]
controller ||= @tracker.libs[klass]
if klass and controller
method = controller[:public][method_name]
method ||= controller[:private][method_name]
method ||= controller[:protected][method_name]
if method.nil?
controller[:includes].each do |included|
method = find_method method_name, included
return method if method
end
find_method method_name, controller[:parent]
else
method
end
else
nil
end
end
end
require 'ruby_parser'
require 'processors/base_processor'
#Processes controller. Results are put in tracker.controllers
class ControllerProcessor < BaseProcessor
FORMAT_HTML = Sexp.new(:call, Sexp.new(:lvar, :format), :html, Sexp.new(:arglist))
def initialize tracker
super
@controller = nil
@current_method = nil
@current_module = nil
@visibility = :public
@file_name = nil
end
#Use this method to process a Controller
def process_controller src, file_name = nil
@file_name = file_name
process src
end
#s(:class, NAME, PARENT, s(:scope ...))
def process_class exp
if @controller
warn "[Notice] Skipping inner class: #{class_name exp[1]}" if OPTIONS[:debug]
return ignore
end
name = class_name(exp[1])
if @current_module
name = (@current_module + "::" + name.to_s).to_sym
end
@controller = { :name => name,
:parent => class_name(exp[2]),
:includes => [],
:public => {},
:private => {},
:protected => {},
:options => {},
:src => exp,
:file => @file_name }
@tracker.controllers[@controller[:name]] = @controller
exp[3] = process exp[3]
@controller = nil
exp
end
#Look for specific calls inside the controller
def process_call exp
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
args = exp[3]
#Methods called inside class definition
#like attr_* and other settings
if @current_method.nil? and target.nil?
if args.length == 1 #actually, empty
case method
when :private, :protected, :public
@visibility = method
when :protect_from_forgery
@controller[:options][:protect_from_forgery] = true
else
#??
end
else
case method
when :include
@controller[:includes] << class_name(args[1]) if @controller
when :before_filter
@controller[:options][:before_filters] ||= []
@controller[:options][:before_filters] << args[1..-1]
end
end
ignore
elsif target == nil and method == :render
make_render exp
elsif exp == FORMAT_HTML and context[1] != :iter
#This is an empty call to
# format.html
#Which renders the default template if no arguments
#Need to make more generic, though.
call = Sexp.new :render, :default, @current_method
call.line(exp.line)
call
else
call = Sexp.new :call, target, method, process(args)
call.line(exp.line)
call
end
end
#Process method definition and store in Tracker
def process_defn exp
name = exp[1]
@current_method = name
res = Sexp.new :methdef, name, process(exp[2]), process(exp[3][1])
res.line(exp.line)
@current_method = nil
@controller[@visibility][name] = res unless @controller.nil?
res
end
#Process self.method definition and store in Tracker
def process_defs exp
name = exp[2]
if exp[1].node_type == :self
target = @controller[:name]
else
target = class_name exp[1]
end
@current_method = name
res = Sexp.new :selfdef, target, name, process(exp[3]), process(exp[4][1])
res.line(exp.line)
@current_method = nil
@controller[@visibility][name] = res unless @controller.nil?
res
end
#Look for before_filters and add fake ones if necessary
def process_iter exp
if exp[1][2] == :before_filter
add_fake_filter exp
else
super
end
end
#This is to handle before_filter do |controller| ... end
#
#We build a new method and process that the same way as usual
#methods and filters.
def add_fake_filter exp
filter_name = ("fake_filter" + rand.to_s[/\d+$/]).to_sym
args = exp[1][3]
args.insert(1, Sexp.new(:lit, filter_name))
before_filter_call = Sexp.new(:call, nil, :before_filter, args)
if exp[2]
block_variable = exp[2][1]
else
block_variable = :temp
end
if sexp? exp[3] and exp[3].node_type == :block
block_inner = exp[3][1..-1]
else
block_inner = [exp[3]]
end
#Build Sexp for filter method
body = Sexp.new(:scope,
Sexp.new(:block,
Sexp.new(:lasgn, block_variable,
Sexp.new(:call, Sexp.new(:const, @controller[:name]), :new, Sexp.new(:arglist)))).concat(block_inner))
filter_method = Sexp.new(:defn, filter_name, Sexp.new(:args), body).line(exp.line)
vis = @visibility
@visibility = :private
process_defn filter_method
@visibility = vis
process before_filter_call
exp
end
end
require 'processors/template_processor'
#Processes ERB templates
#(those ending in .html.erb or .rthml).
class ErbTemplateProcessor < TemplateProcessor
#s(:call, TARGET, :method, s(:arglist))
def process_call exp
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
#_erbout is the default output variable for erb
if target and target[1] == :_erbout
if method == :concat
@inside_concat = true
args = exp[3] = process(exp[3])
@inside_concat = false
if args.length > 2
raise Exception.new("Did not expect more than a single argument to _erbout.concat")
end
args = args[1]
if args.node_type == :call and args[2] == :to_s #erb always calls to_s on output
args = args[1]
end
if args.node_type == :str #ignore plain strings
ignore
else
s = Sexp.new :output, args
s.line(exp.line)
@current_template[:outputs] << s
s
end
elsif method == :force_encoding
ignore
else
abort "Unrecognized action on _erbout: #{method}"
end
elsif target == nil and method == :render
exp[3] = process(exp[3])
make_render exp
else
args = exp[3] = process(exp[3])
call = Sexp.new :call, target, method, args
call.line(exp.line)
call
end
end
#Process block, removing irrelevant expressions
def process_block exp
exp.shift
if @inside_concat
@inside_concat = false
exp[0..-2].each do |e|
process e
end
@inside_concat = true
process exp[-1]
else
exp.map! do |e|
res = process e
if res.empty? or res == ignore
nil
elsif sexp? res and res.node_type == :lvar and res[1] == :_erbout
nil
else
res
end
end
block = Sexp.new(:rlist).concat(exp).compact
block.line(exp.line)
block
end
end
end
require 'processors/template_processor'
#Processes ERB templates using Erubis instead of erb.
class ErubisTemplateProcessor < TemplateProcessor
#s(:call, TARGET, :method, s(:arglist))
def process_call exp
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
#_buf is the default output variable for Erubis
if target and target[1] == :_buf
if method == :<<
args = exp[3][1] = process(exp[3][1])
if args.node_type == :call and args[2] == :to_s #just calling to_s on inner code
args = args[1]
end
if args.node_type == :str #ignore plain strings
ignore
else
s = Sexp.new :output, args
s.line(exp.line)
@current_template[:outputs] << s
s
end
elsif method == :to_s
ignore
else
abort "Unrecognized action on _buf: #{method}"
end
elsif target == nil and method == :render
exp[3] = process exp[3]
make_render exp
else
args = exp[3] = process(exp[3])
call = Sexp.new :call, target, method, args
call.line(exp.line)
call
end
end
#Process blocks, ignoring :ignore exps
def process_block exp
exp.shift
exp.map! do |e|
res = process e
if res.empty? or res == ignore
nil
else
res
end
end
block = Sexp.new(:rlist).concat(exp).compact
block.line(exp.line)
block
end
end
require 'processors/template_processor'
#Processes HAML templates.
class HamlTemplateProcessor < TemplateProcessor
HAML_FORMAT_METHOD = /format_script_(true|false)_(true|false)_(true|false)_(true|false)_(true|false)_(true|false)_(true|false)/
#Processes call, looking for template output
def process_call exp
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
if (sexp? target and target[2] == :_hamlout) or target == :_hamlout
res = case method
when :adjust_tabs, :rstrip!
ignore
when :options
Sexp.new :call, :_hamlout, :options, exp[3]
when :buffer
Sexp.new :call, :_hamlout, :buffer, exp[3]
when :open_tag
Sexp.new(:tag, process(exp[3]))
else
arg = exp[3][1]
if arg
@inside_concat = true
out = exp[3][1] = process(arg)
@inside_concat = false
else
raise Exception.new("Empty _hamlout.#{method}()?")
end
if string? out
ignore
else
case method.to_s
when "push_text"
s = Sexp.new(:output, out)
@current_template[:outputs] << s
s
when HAML_FORMAT_METHOD
if $4 == "true"
Sexp.new :format_escaped, out
else
Sexp.new :format, out
end
else
raise Exception.new("Unrecognized action on _hamlout: #{method}")
end
end
end
res.line(exp.line)
res
#_hamlout.buffer <<
#This seems to be used rarely, but directly appends args to output buffer
elsif sexp? target and method == :<< and is_buffer_target? target
@inside_concat = true
out = exp[3][1] = process(exp[3][1])
@inside_concat = false
if out.node_type == :str #ignore plain strings
ignore
else
s = Sexp.new(:output, out)
@current_template[:outputs] << s
s.line(exp.line)
s
end
elsif target == nil and method == :render
#Process call to render()
exp[3] = process exp[3]
make_render exp
else
args = process exp[3]
call = Sexp.new :call, target, method, args
call.line(exp.line)
call
end
end
#If inside an output stream, only return the final expression
def process_block exp
exp.shift
if @inside_concat
@inside_concat = false
exp[0..-2].each do |e|
process e
end
@inside_concat = true
process exp[-1]
else
exp.map! do |e|
res = process e
if res.empty?
nil
else
res
end
end
Sexp.new(:rlist).concat(exp).compact
end
end
#Checks if the buffer is the target in a method call Sexp.
def is_buffer_target? exp
exp.node_type == :call and exp[1] == :_hamlout and exp[2] == :buffer
end
end
require 'processors/base_processor'
#Finds method calls matching the given target(s).
#
#Targets/methods can be:
#
# - nil: matches anything, including nothing
# - Empty array: matches nothing
# - Symbol: matches single target/method exactly
# - Array of symbols: matches against any of the symbols
# - Regular expression: matches the expression
# - Array of regular expressions: matches any of the expressions
#
#If a target is also the name of a class, methods called on instances
#of that class will also be matched, in a very limited way.
#(Any methods called on Klass.new, basically. More useful when used
#in conjunction with AliasProcessor.)
#
#Examples:
#
# #To find any uses of this class:
# FindCall.new :FindCall, nil
#
# #Find system calls without a target
# FindCall.new [], [:system, :exec, :syscall]
#
# #Find all calls to length(), no matter the target
# FindCall.new nil, :length
#
# #Find all calls to sub, sub!, gsub, or gsub!
# FindCall.new nil, /^g?sub!?$/
class FindCall < BaseProcessor
def initialize targets, methods
super(nil)
@calls = []
@find_targets = targets
@find_methods = methods
@current_class = nil
@current_method = nil
end
#Returns a list of results.
#
#A result looks like:
#
# s(:result, :ClassName, :method_name, s(:call, ...))
#
#or
#
# s(:result, :template_name, s(:call, ...))
def matches
@calls
end
#Process the given source. Provide either class and method being searched
#or the template. These names are used when reporting results.
#
#Use FindCall#matches to retrieve results.
def process_source exp, klass = nil, method = nil, template = nil
@current_class = klass
@current_method = method
@current_template = template
process exp
end
#Process body of method
def process_methdef exp
process exp[3]
end
#Process body of method
def process_selfdef exp
process exp[4]
end
#Process body of block
def process_rlist exp
exp[1..-1].each do |e|
process e
end
exp
end
#Look for matching calls and add them to results
def process_call exp
target = get_target exp[1]
method = exp[2]
process exp[3]
if match(@find_targets, target) and match(@find_methods, method)
if @current_template
@calls << Sexp.new(:result, @current_template, exp).line(exp.line)
else
@calls << Sexp.new(:result, @current_class, @current_method, exp).line(exp.line)
end
end
exp
end
#Process an assignment like a call
def process_attrasgn exp
process_call exp
end
private
#Gets the target of a call as a Symbol
#if possible
def get_target exp
if sexp? exp
case exp.node_type
when :ivar, :lvar, :const
exp[1]
when :true, :false
exp[0]
when :lit
exp[1]
when :colon2
class_name exp
else
exp
end
else
exp
end
end
#Checks if the search terms match the given item
def match search_terms, item
case search_terms
when Symbol
if search_terms == item
true
elsif sexp? item
is_instance_of? item, search_terms
else
false
end
when Enumerable
if search_terms.empty?
item == nil
else
search_terms.each do|term|
if match(term, item)
return true
end
end
false
end
when Regexp
search_terms.match item.to_s
when nil
true
else
raise "Cannot match #{search_terms} and #{item}"
end
end
#Checks if +item+ is an instance of +klass+ by looking for Klass.new
def is_instance_of? item, klass
if call? item
if sexp? item[1]
item[2] == :new and item[1].node_type == :const and item[1][1] == klass
else
item[2] == :new and item[1] == klass
end
else
false
end
end
end
require 'processors/lib/find_call'
#This processor specifically looks for calls like
# User.active.human.find(:all, :conditions => ...)
class FindModelCall < FindCall
#Passes +targets+ to FindCall
def initialize targets
super(targets, /^(find.*|first|last|all)$/)
end
#Matches entire method chain as a target. This differs from
#FindCall#get_target, which only matches the first expression in the chain.
def get_target exp
if sexp? exp
case exp.node_type
when :ivar, :lvar, :const
exp[1]
when :true, :false
exp[0]
when :lit
exp[1]
when :colon2
class_name exp
when :call
t = get_target(exp[1])
if t and match(@find_targets, t)
t
else
process exp
end
else
process exp
end
else
exp
end
end
end
#Contains a couple shared methods for Processors.
module ProcessorHelper
#Sets the current module.
def process_module exp
@current_module = class_name(exp[1]).to_s
process exp[2]
@current_module = nil
exp
end
#Returns a class name as a Symbol.
def class_name exp
case exp
when Sexp
case exp.node_type
when :const
exp[1]
when :colon2
"#{class_name(exp[1])}::#{exp[2]}".to_sym
when :colon3
"::#{exp[1]}".to_sym
when :call
process exp
else
raise "Error: Cannot get class name from #{exp}"
end
when Symbol
exp
when nil
nil
else
raise "Error: Cannot get class name from #{exp}"
end
end
end
require 'digest/sha1'
#Processes a call to render() in a controller or template
module RenderHelper
#Process s(:render, TYPE, OPTIONS)
def process_render exp
process_default exp
@rendered = true
case exp[1]
when :action
process_action exp[2][1], exp[3]
when :default
process_template template_name, exp[3]
when :partial
process_partial exp[2], exp[3]
when :nothing
end
exp
end
#Determines file name for partial and then processes it
def process_partial name, args
if name == "" or !(string? name or symbol? name)
return
end
names = name[1].to_s.split("/")
names[-1] = "_" + names[-1]
process_template template_name(names.join("/")), args
end
#Processes a given action
def process_action name, args
process_template template_name(name), args
end
#Processes a template, adding any instance variables
#to its environment.
def process_template name, args, called_from = nil
#Get scanned source for this template
name = name.to_s.gsub(/^\//, "")
template = @tracker.templates[name.to_sym]
unless template
warn "[Notice] No such template: #{name}" if OPTIONS[:debug]
return
end
template_env = only_ivars
#Hash the environment and the source of the template to avoid
#pointlessly processing templates, which can become prohibitively
#expensive in terms of time and memory.
digest = Digest::SHA1.new.update(template_env.instance_variable_get(:@env).to_a.sort.to_s << template[:src].to_s).to_s.to_sym
if @tracker.template_cache.include? digest
#Already processed this template with identical environment
return
else
@tracker.template_cache << digest
options = get_options args
if hash? options[:locals]
hash_iterate options[:locals] do |key, value|
template_env[Sexp.new(:call, nil, key[1], Sexp.new(:arglist))] = value
end
end
if options[:collection]
#The collection name is the name of the partial without the leading
#underscore.
variable = template[:name].to_s.match(/[^\/_][^\/]+$/)[0].to_sym
#Unless the :as => :variable_name option is used
if options[:as]
if string? options[:as] or symbol? options[:as]
variable = options[:as][1].to_sym
end
end
template_env[Sexp.new(:call, nil, variable, Sexp.new(:arglist))] = Sexp.new(:call, Sexp.new(:const, Tracker::UNKNOWN_MODEL), :new, Sexp.new(:arglist))
end
#Run source through AliasProcessor with instance variables from the
#current environment.
#TODO: Add in :locals => { ... } to environment
src = TemplateAliasProcessor.new(@tracker, template).process_safely(template[:src], template_env)
#Run alias-processed src through the template processor to pull out
#information and outputs.
#This information will be stored in tracker.templates, but with a name
#specifying this particular route. The original source should remain
#pristine (so it can be processed within other environments).
@tracker.processor.process_template name, src, template[:type], called_from
end
end
#Override to process name, such as adding the controller name.
def template_name name
raise "RenderHelper#template_name should be overridden."
end
#Turn options Sexp into hash
def get_options args
options = {}
return options unless hash? args
hash_iterate args do |key, value|
if symbol? key
options[key[1]] = value
end
end
options
end
end
require 'processors/base_processor'
require 'processors/alias_processor'
#Process generic library and stores it in Tracker.libs
class LibraryProcessor < BaseProcessor
def initialize tracker
super
@file_name = nil
@alias_processor = AliasProcessor.new
end
def process_library src, file_name = nil
@file_name = file_name
process src
end
def process_class exp
name = class_name(exp[1])
if @current_class
outer_class = @current_class
name = (outer_class[:name].to_s + "::" + name.to_s).to_sym
end
if @current_module
name = (@current_module[:name].to_s + "::" + name.to_s).to_sym
end
if @tracker.libs[name]
@current_class = @tracker.libs[name]
else
@current_class = { :name => name,
:parent => class_name(exp[2]),
:includes => [],
:public => {},
:private => {},
:protected => {},
:src => exp,
:file => @file_name }
@tracker.libs[name] = @current_class
end
exp[3] = process exp[3]
if outer_class
@current_class = outer_class
else
@current_class = nil
end
exp
end
def process_module exp
name = class_name(exp[1])
if @current_module
outer_class = @current_module
name = (outer_class[:name].to_s + "::" + name.to_s).to_sym
end
if @current_class
name = (@current_class[:name].to_s + "::" + name.to_s).to_sym
end
if @tracker.libs[name]
@current_module = @tracker.libs[name]
else
@current_module = { :name => name,
:includes => [],
:public => {},
:private => {},
:protected => {},
:src => exp }
@tracker.libs[name] = @current_module
end
exp[2] = process exp[2]
if outer_class
@current_module = outer_class
else
@current_module = nil
end
exp
end
def process_defn exp
exp[0] = :methdef
exp[3] = @alias_processor.process_safely process(exp[3]), SexpProcessor::Environment.new
if @current_class
@current_class[:public][exp[1]] = exp[3]
elsif @current_module
@current_module[:public][exp[1]] = exp[3]
end
exp
end
def process_defs exp
exp[0] = :selfdef
exp[4] = @alias_processor.process_safely process(exp[4]), SexpProcessor::Environment.new
if @current_class
@current_class[:public][exp[2]] = exp[4]
elsif @current_module
@current_module[:public][exp[3]] = exp[4]
end
exp
end
end
require 'processors/base_processor'
#Processes models. Puts results in tracker.models
class ModelProcessor < BaseProcessor
def initialize tracker
super
@model = nil
@current_method = nil
@visibility = :public
@file_name = nil
end
#Process model source
def process_model src, file_name = nil
@file_name = file_name
process src
end
#s(:class, NAME, PARENT, s(:scope ...))
def process_class exp
if @model
warn "[Notice] Skipping inner class: #{class_name exp[1]}" if OPTIONS[:debug]
ignore
else
@model = { :name => class_name(exp[1]),
:parent => class_name(exp[2]),
:includes => [],
:public => {},
:private => {},
:protected => {},
:options => {},
:file => @file_name }
@tracker.models[@model[:name]] = @model
res = process exp[3]
@model = nil
res
end
end
#Handle calls outside of methods,
#such as include, attr_accessible, private, etc.
def process_call exp
return exp unless @model
target = exp[1]
if sexp? target
target = process target
end
method = exp[2]
args = exp[3]
#Methods called inside class definition
#like attr_* and other settings
if @current_method.nil? and target.nil?
if args.length == 1 #actually, empty
case method
when :private, :protected, :public
@visibility = method
else
#??
end
else
case method
when :include
@model[:includes] << class_name(args[1]) if @model
when :attr_accessible
@model[:attr_accessible] ||= []
args = args[1..-1].map do |e|
e[1]
end
@model[:attr_accessible].concat args
else
if @model
@model[:options][method] ||= []
@model[:options][method] << process(args)
end
end
end
ignore
else
call = Sexp.new :call, target, method, process(args)
call.line(exp.line)
call
end
end
#Add method definition to tracker
def process_defn exp
return exp unless @model
name = exp[1]
@current_method = name
res = Sexp.new :methdef, name, process(exp[2]), process(exp[3][1])
res.line(exp.line)
@current_method = nil
if @model
list = @model[@visibility]
list[name] = res
end
res
end
#Add method definition to tracker
def process_defs exp
return exp unless @model
name = exp[2]
if exp[1].node_type == :self
target = @model[:name]
else
target = class_name exp[1]
end
@current_method = name
res = Sexp.new :selfdef, target, name, process(exp[3]), process(exp[4][1])
res.line(exp.line)
@current_method = nil
if @model
@model[@visibility][name] = res unless @model.nil?
end
res
end
end
require 'rubygems'
require 'ruby2ruby'
require 'util'
#Produces formatted output strings from Sexps.
#Recommended usage is
#
# OutputProcessor.new.format(Sexp.new(:str, "hello"))
class OutputProcessor < Ruby2Ruby
include Util
#Copies +exp+ and then formats it.
def format exp
process exp.deep_clone
end
alias process_safely format
def process exp
begin
super exp if sexp? exp and not exp.empty?
rescue Exception => e
warn "While formatting #{exp}: #{e}\n#{e.backtrace.join("\n")}" if OPTIONS[:debug]
end
end
def process_call exp
if exp[0].is_a? Symbol
target = exp[0]
method = exp[1]
args = process exp[2]
out = nil
if method == :[]
if target
out = "#{target}[#{args}]"
else
raise Exception.new("Not sure what to do with access and no target: #{exp}")
end
else
if target
out = "#{target}.#{method}(#{args})"
else
out = "#{method}(#{args})"
end
end
exp.clear
out
else
super exp
end
end
def process_lvar exp
out = "(local #{exp[0]})"
exp.clear
out
end
def process_ignore exp
exp.clear
"[ignored]"
end
def process_params exp
exp.clear
"params"
end
def process_session exp
exp.clear
"session"
end
def process_cookies exp
exp.clear
"cookies"
end
def process_string_interp exp
out = '"'
exp.each do |e|
if e.is_a? String
out << e
else
res = process e
out << res unless res == ""
end
end
out << '"'
exp.clear
out
end
def process_string_eval exp
out = "\#{#{process(exp[0])}}"
exp.clear
out
end
def process_dxstr exp
out = "`"
out << exp.map! do |e|
if e.is_a? String
e
elsif string? e
e[1]
else
process e
end
end.join
exp.clear
out << "`"
end
def process_rlist exp
out = exp.map do |e|
res = process e
if res == ""
nil
else
res
end
end.compact.join("\n")
exp.clear
out
end
def process_call_with_block exp
call = process exp[0]
block = process exp[1] if exp[1]
out = "#{call} do\n #{block}\n end"
exp.clear
out
end
def process_output exp
out = if exp[0].node_type == :str
""
else
res = process exp[0]
if res == ""
""
else
"[Output] #{res}"
end
end
exp.clear
out
end
def process_format exp
out = if exp[0].node_type == :str or exp[0].node_type == :ignore
""
else
res = process exp[0]
if res == ""
""
else
"[Format] #{res}"
end
end
exp.clear
out
end
def process_format_escaped exp
out = if exp[0].node_type == :str or exp[0].node_type == :ignore
""
else
res = process exp[0]
if res == ""
""
else
"[Escaped] #{res}"
end
end
exp.clear
out
end
def process_const exp
if exp[0] == Tracker::UNKNOWN_MODEL
exp.clear
"(Unresolved Model)"
else
super exp
end
end
def process_render exp
exp[1] = process exp[1] if sexp? exp[1]
exp[2] = process exp[2] if sexp? exp[2]
out = "render(#{exp[0]} => #{exp[1]}, #{exp[2]})"
exp.clear
out
end
end
require 'rubygems'
require 'sexp_processor'
require 'set'
#Looks for request parameters. Not used currently.
class ParamsProcessor < SexpProcessor
attr_reader :result
def initialize
super()
self.strict = false
self.auto_shift_type = false
self.require_empty = false
self.default_method = :process_default
self.warn_on_default = false
@result = []
@matched = false
@mark = false
@watch_nodes = Set.new([:call, :iasgn, :lasgn, :gasgn, :cvasgn, :return, :attrasgn])
@params = Sexp.new(:call, nil, :params, Sexp.new(:arglist))
end
def process_default exp
if @watch_nodes.include?(exp.node_type) and not @mark
@mark = true
@matched = false
process_these exp[1..-1]
if @matched
@result << exp
@matched = false
end
@mark = false
else
process_these exp[1..-1]
end
exp
end
def process_these exp
exp.each do |e|
if sexp? e and not e.empty?
process e
end
end
end
def process_call exp
if @mark
actually_process_call exp
else
@mark = true
actually_process_call exp
if @matched
@result << exp
end
@mark = @matched = false
end
exp
end
def actually_process_call exp
process exp[1]
process exp[3]
if exp[1] == @params or exp == @params
@matched = true
end
end
#Don't really care about condition
def process_if exp
process_these exp[2..-1]
exp
end
end
require 'processors/base_processor'
require 'processors/alias_processor'
require 'util'
require 'set'
#Processes the Sexp from routes.rb. Stores results in tracker.routes.
#
#Note that it is only interested in determining what methods on which
#controllers are used as routes, not the generated URLs for routes.
class RoutesProcessor < BaseProcessor
attr_reader :map, :nested, :current_controller
def initialize tracker
super
@map = Sexp.new(:lvar, :map)
@nested = nil #used for identifying nested targets
@prefix = [] #Controller name prefix (a module name, usually)
@current_controller = nil
@with_options = nil #For use inside map.with_options
end
#Call this with parsed route file information.
#
#This method first calls RouteAliasProcessor#process_safely on the +exp+,
#so it does not modify the +exp+.
def process_routes exp
process RouteAliasProcessor.new.process_safely(exp)
end
#Looking for mapping of routes
def process_call exp
target = exp[1]
if target == map or target == nested
process_map exp
else
process_default exp
end
exp
end
#Process a map.something call
#based on the method used
def process_map exp
args = exp[3][1..-1]
case exp[2]
when :resource
process_resource args
when :resources
process_resources args
when :connect, :root
process_connect args
else
process_named_route args
end
exp
end
#Look for map calls that take a block.
#Otherwise, just do the default processing.
def process_iter exp
if exp[1][1] == map or exp[1][1] == nested
method = exp[1][2]
case method
when :namespace
process_namespace exp
when :resources, :resource
process_resources exp[1][3][1..-1]
process_default exp[3]
when :with_options
process_with_options exp
end
exp
else
super
end
end
#Process
# map.resources :x, :controller => :y, :member => ...
#etc.
def process_resources exp
controller = check_for_controller_name exp
if controller
self.current_controller = controller
process_resource_options exp[-1]
else
exp.each do |argument|
if sexp? argument and argument.node_type == :lit
self.current_controller = exp[0][1]
add_resources_routes
process_resource_options exp[-1]
end
end
end
end
#Add default routes
def add_resources_routes
@tracker.routes[@current_controller].merge [:index, :new, :create, :show, :edit, :update, :destroy]
end
#Process all the options that might be in the hash passed to
#map.resource, et al.
def process_resource_options exp
if exp.nil? and @with_options
exp = @with_options
elsif @with_options
exp = exp.concat @with_options[1..-1]
end
return unless exp.node_type == :hash
hash_iterate(exp) do |option, value|
case option[1]
when :controller, :requirements, :singular, :path_prefix, :as,
:path_names, :shallow, :name_prefix
#should be able to skip
when :collection, :member, :new
process_collection value
when :has_one
save_controller = current_controller
process_resource value[1..-1]
self.current_controller = save_controller
when :has_many
save_controller = current_controller
process_resources value[1..-1]
self.current_controller = save_controller
when :only
process_option_only value
when :except
process_option_except value
else
raise "Unhandled resource option: #{option}"
end
end
end
#Process route option :only => ...
def process_option_only exp
routes = @tracker.routes[@current_controller]
[:index, :new, :create, :show, :edit, :update, :destroy].each do |r|
routes.delete r
end
if exp.node_type == :array
exp[1..-1].each do |e|
routes << e[1]
end
end
end
#Process route option :except => ...
def process_option_except exp
return unless exp.node_type == :array
routes = @tracker.routes[@current_controller]
exp[1..-1].each do |e|
routes.delete e[1]
end
end
# map.resource :x, ..
def process_resource exp
controller = check_for_controller_name exp
if controller
self.current_controller = controller
process_resource_options exp[-1]
else
exp.each do |argument|
if argument.node_type == :lit
self.current_controller = pluralize(exp[0][1].to_s)
add_resource_routes
process_resource_options exp[-1]
end
end
end
end
#Add default routes minus :index
def add_resource_routes
@tracker.routes[@current_controller].merge [:new, :create, :show, :edit, :update, :destroy]
end
#Process
# map.connect '/something', :controller => 'blah', :action => 'whatever'
def process_connect exp
controller = check_for_controller_name exp
self.current_controller = controller if controller
#Check for default route
if string? exp[0]
if exp[0][1] == ":controller/:action/:id"
@tracker.routes[:allow_all_actions] = exp[0]
elsif exp[0][1].include? ":action"
@tracker.routes[@current_controller] = :allow_all_actions
return
end
end
#This -seems- redundant, but people might connect actions
#to a controller which already allows them all
return if @tracker.routes[@current_controller] == :allow_all_actions
exp[-1].each_with_index do |e,i|
if symbol? e and e[1] == :action
@tracker.routes[@current_controller] << exp[-1][i + 1][1].to_sym
return
end
end
end
# map.with_options :controller => 'something' do |something|
# something.resources :blah
# end
def process_with_options exp
@with_options = exp[1][3][-1]
@nested = Sexp.new(:lvar, exp[2][1])
self.current_controller = check_for_controller_name exp[1][3]
#process block
process exp[3]
@with_options = nil
@nested = nil
end
# map.namespace :something do |something|
# something.resources :blah
# end
def process_namespace exp
call = exp[1]
formal_args = exp[2]
block = exp[3]
@prefix << camelize(call[3][1][1])
@nested = Sexp.new(:lvar, formal_args[1])
process block
@prefix.pop
end
# map.something_abnormal '/blah', :controller => 'something', :action => 'wohoo'
def process_named_route exp
process_connect exp
end
#Process collection option
# :collection => { :some_action => :http_actions }
def process_collection exp
return unless exp.node_type == :hash
routes = @tracker.routes[@current_controller]
hash_iterate(exp) do |action, type|
routes << action[1]
end
end
#Manage Controller prefixes
#@prefix is an Array, but this method returns a string
#suitable for prefixing onto a controller name.
def prefix
if @prefix.length > 0
@prefix.join("::") << "::"
else
''
end
end
#Sets the controller name to a proper class name.
#For example
# self.current_controller = :session
# @controller == :SessionController #true
#
#Also prepends the prefix if there is one set.
def current_controller= name
@current_controller = (prefix + camelize(name) + "Controller").to_sym
@tracker.routes[@current_controller] ||= Set.new
end
private
#Checks an argument list for a hash that has a key :controller.
#If it does, returns the value.
#
#Otherwise, returns nil.
def check_for_controller_name args
args.each do |a|
if hash? a
hash_iterate(a) do |k, v|
if k[1] == :controller
return v[1]
end
end
end
end
nil
end
end
#This is for a really specific case where a hash is used as arguments
#to one of the map methods.
class RouteAliasProcessor < AliasProcessor
#This replaces
# { :some => :hash }.keys
#with
# [:some]
def process_call exp
process_default exp
if hash? exp[1] and exp[2] == :keys
keys = get_keys exp[1]
exp.clear
keys.each_with_index do |e,i|
exp[i] = e
end
end
exp
end
#Returns an array Sexp containing the keys from the hash
def get_keys hash
keys = Sexp.new(:array)
hash_iterate(hash) do |key, value|
keys << key
end
keys
end
end
require 'set'
require 'processors/alias_processor'
require 'processors/lib/render_helper'
#Processes aliasing in templates.
#Handles calls to +render+.
class TemplateAliasProcessor < AliasProcessor
include RenderHelper
FORM_METHODS = Set.new([:form_for, :remote_form_for, :form_remote_for])
def initialize tracker, template
super()
@tracker = tracker
@template = template
end
#Process template
def process_template name, args
super name, args, "Template:#{@template[:name]}"
end
#Determine template name
def template_name name
unless name.to_s.include? "/"
name = "#{@template[:name].to_s.match(/^(.*\/).*$/)[1]}#{name}"
end
name
end
#Looks for form methods and iterating over collections of Models
def process_call_with_block exp
process_default exp
call = exp[1]
target = call[1]
method = call[2]
args = exp[2]
block = exp[3]
#Check for e.g. Model.find.each do ... end
if method == :each and args and block and model = get_model_target(target)
if sexp? args and args.node_type == :lasgn
if model == target[1]
env[Sexp.new(:lvar, args[1])] = Sexp.new(:call, model, :new, Sexp.new(:arglist))
else
env[Sexp.new(:lvar, args[1])] = Sexp.new(:call, Sexp.new(:const, Tracker::UNKNOWN_MODEL), :new, Sexp.new(:arglist))
end
process block if sexp? block
end
elsif FORM_METHODS.include? method
if sexp? args and args.node_type == :lasgn
env[Sexp.new(:lvar, args[1])] = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new, Sexp.new(:arglist))
process block if sexp? block
end
end
exp
end
alias process_iter process_call_with_block
#Checks if +exp+ is a call to Model.all or Model.find*
def get_model_target exp
if call? exp
target = exp[1]
if exp[2] == :all or exp[2].to_s[0,4] == "find"
models = Set.new @tracker.models.keys
begin
name = class_name target
return target if models.include?(name)
rescue StandardError
end
end
return get_model_target(target)
end
false
end
end
require 'processors/base_processor'
#Base Processor for templates/views
class TemplateProcessor < BaseProcessor
#Initializes template information.
def initialize tracker, template_name, called_from = nil, file_name = nil
super(tracker)
@current_template = { :name => template_name,
:caller => called_from,
:partial => template_name.to_s[0,1] == "_",
:outputs => [],
:src => nil, #set in Processor
:type => nil, #set in Processor
:file => file_name }
if called_from
template_name = (template_name.to_s + "." + called_from.to_s).to_sym
end
tracker.templates[template_name] = @current_template
@inside_concat = false
self.warn_on_default = false
end
#Process the template Sexp.
def process exp
begin
super
rescue Exception => e
except = e.exception("Error when processing #{@current_template[:name]}: #{e.message}")
except.set_backtrace(e.backtrace)
raise except
end
end
#Ignore initial variable assignment
def process_lasgn exp
if exp[1] == :_erbout and exp[2].node_type == :str #ignore
ignore
elsif exp[1] == :_buf and exp[2].node_type == :str
ignore
else
exp[2] = process exp[2]
exp
end
end
#Adds output to the list of outputs.
def process_output exp
process exp[1]
@current_template[:outputs] << exp
exp
end
end
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册