diff options
Diffstat (limited to 'actionpack')
67 files changed, 4141 insertions, 179 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index c3df2ebc0c..57b8b5dfc9 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,24 @@ ## Rails 4.0.0 (unreleased) ## +* Added `Mime::NullType` class. This allows to use html?, xml?, json?..etc when + the `format` of `request` is unknown, without raise an exception. + + *Angelo Capilleri* + +* Integrate the Journey gem into Action Dispatch so that the global namespace + is not polluted with names that may be used as models. + + *Andrew White* + +* Extract support for email address obfuscation via `:encode`, `:replace_at`, and `replace_dot` + options from the `mail_to` helper into the `actionview-encoded_mail_to` gem. + + *Nick Reed + DHH* + +* Handle `:protocol` option in `stylesheet_link_tag` and `javascript_include_tag` + + *Vasiliy Ermolovich* + * Clear url helper methods when routes are reloaded. *Andrew White* * Fix a bug in `ActionDispatch::Request#raw_post` that caused `env['rack.input']` @@ -118,7 +137,7 @@ resources :users end - *Guillermo Iguaran* + *Guillermo Iguaran + Amparo Luna* * Fix error when using a non-hash query argument named "params" in `url_for`. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 50e3bb0d48..ba7956c3ab 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -15,7 +15,7 @@ Rake::TestTask.new(:test_action_pack) do |t| # make sure we include the tests in alphabetical order as on some systems # this will not happen automatically and the tests (as a whole) will error - t.test_files = Dir.glob('test/{abstract,controller,dispatch,template,assertions}/**/*_test.rb').sort + t.test_files = Dir.glob('test/{abstract,controller,dispatch,template,assertions,journey}/**/*_test.rb').sort t.warning = true t.verbose = true @@ -75,3 +75,9 @@ task :lines do puts "Total: Lines #{total_lines}, LOC #{total_codelines}" end + +rule '.rb' => '.y' do |t| + sh "racc -l -o #{t.name} #{t.source}" +end + +task compile: 'lib/action_dispatch/journey/parser.rb' diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 89fdd528c2..c65870cac6 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -23,7 +23,6 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1.0' s.add_dependency 'rack', '~> 1.4.1' s.add_dependency 'rack-test', '~> 0.6.1' - s.add_dependency 'journey', '~> 2.0.0' s.add_dependency 'erubis', '~> 2.7.0' s.add_development_dependency 'activemodel', version diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 36a0dcb2de..812a35735f 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -113,7 +113,7 @@ module AbstractController # helpers with the following behavior: # # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", - # and "foo_bar_helper.rb" is loaded using require_dependency. + # and "foo_bar_helper.rb" is loaded using require_dependency. # # Module:: No further processing # diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 283f6413ec..896238b7dc 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -384,6 +384,8 @@ module ActionController # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token + TOKEN_REGEX = /^Token / + AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self module ControllerMethods @@ -431,20 +433,34 @@ module ActionController # Returns an Array of [String, Hash] if a token is present. # Returns nil if no token is found. def token_and_options(request) - if request.authorization.to_s[/^Token (.*)/] - values = Hash[$1.split(',').map do |value| - value.strip! # remove any spaces between commas and values - key, value = value.split(/\=\"?/) # split key=value pairs - if value - value.chomp!('"') # chomp trailing " in value - value.gsub!(/\\\"/, '"') # unescape remaining quotes - [key, value] - end - end.compact] - [values.delete("token"), values.with_indifferent_access] + authorization_request = request.authorization.to_s + if authorization_request[TOKEN_REGEX] + params = token_params_from authorization_request + [params.shift.last, Hash[params].with_indifferent_access] end end + def token_params_from(auth) + rewrite_param_values params_array_from raw_params auth + end + + # Takes raw_params and turns it into an array of parameters + def params_array_from(raw_params) + raw_params.map { |param| param.split %r/=(.+)?/ } + end + + # This removes the `"` characters wrapping the value. + def rewrite_param_values(array_params) + array_params.each { |param| param.last.gsub! %r/^"|"$/, '' } + end + + # This method takes an authorization body and splits up the key-value + # pairs by the standardized `:`, `;`, or `\t` delimiters defined in + # `AUTHN_PAIR_DELIMITERS`. + def raw_params(auth) + auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + end + # Encodes the given token and options into an Authorization header value. # # token - String token. diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index d002babee3..938161dc69 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -63,6 +63,7 @@ module ActionDispatch autoload :Static end + autoload :Journey autoload :MiddlewareStack, 'action_dispatch/middleware/stack' autoload :Routing diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index f56f09c5b3..912da741b7 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -27,7 +27,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) + Type.lookup_by_extension(type) || NullType.new end def fetch(type) @@ -306,6 +306,17 @@ module Mime method.to_s.ends_with? '?' end end + + class NullType + def nil? + true + end + + private + def method_missing(method, *args) + false if method.to_s.ends_with? '?' + end + end end require 'action_dispatch/http/mime_types' diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb new file mode 100644 index 0000000000..ad42713482 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey.rb @@ -0,0 +1,5 @@ +require 'action_dispatch/journey/router' +require 'action_dispatch/journey/gtg/builder' +require 'action_dispatch/journey/gtg/simulator' +require 'action_dispatch/journey/nfa/builder' +require 'action_dispatch/journey/nfa/simulator' diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb new file mode 100644 index 0000000000..3bd20fdf81 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/backwards.rb @@ -0,0 +1,5 @@ +module Rack # :nodoc: + Mount = ActionDispatch::Journey::Router + Mount::RouteSet = ActionDispatch::Journey::Router + Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern +end diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb new file mode 100644 index 0000000000..4a344f71af --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -0,0 +1,144 @@ +module ActionDispatch + module Journey + # The Formatter class is used for formatting URLs. For example, parameters + # passed to +url_for+ in rails will eventually call Formatter#generate. + class Formatter # :nodoc: + attr_reader :routes + + def initialize(routes) + @routes = routes + @cache = nil + end + + def generate(type, name, options, recall = {}, parameterize = nil) + constraints = recall.merge(options) + missing_keys = [] + + match_route(name, constraints) do |route| + parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) + next if !name && route.requirements.empty? && route.parts.empty? + + missing_keys = missing_keys(route, parameterized_parts) + next unless missing_keys.empty? + params = options.dup.delete_if do |key, _| + parameterized_parts.key?(key) || route.defaults.key?(key) + end + + return [route.format(parameterized_parts), params] + end + + raise Router::RoutingError.new "missing required keys: #{missing_keys}" + end + + def clear + @cache = nil + end + + private + + def extract_parameterized_parts(route, options, recall, parameterize = nil) + constraints = recall.merge(options) + data = constraints.dup + + keys_to_keep = route.parts.reverse.drop_while { |part| + !options.key?(part) || (options[part] || recall[part]).nil? + } | route.required_parts + + (data.keys - keys_to_keep).each do |bad_key| + data.delete(bad_key) + end + + parameterized_parts = data.dup + + if parameterize + parameterized_parts.each do |k, v| + parameterized_parts[k] = parameterize.call(k, v) + end + end + + parameterized_parts.keep_if { |_, v| v } + parameterized_parts + end + + def named_routes + routes.named_routes + end + + def match_route(name, options) + if named_routes.key?(name) + yield named_routes[name] + else + routes = non_recursive(cache, options.to_a) + + hash = routes.group_by { |_, r| r.score(options) } + + hash.keys.sort.reverse_each do |score| + next if score < 0 + + hash[score].sort_by { |i, _| i }.each do |_, route| + yield route + end + end + end + end + + def non_recursive(cache, options) + routes = [] + stack = [cache] + + while stack.any? + c = stack.shift + routes.concat(c[:___routes]) if c.key?(:___routes) + + options.each do |pair| + stack << c[pair] if c.key?(pair) + end + end + + routes + end + + # Returns an array populated with missing keys if any are present. + def missing_keys(route, parts) + missing_keys = [] + tests = route.path.requirements + route.required_parts.each { |key| + if tests.key?(key) + missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] + else + missing_keys << key unless parts[key] + end + } + missing_keys + end + + def possibles(cache, options, depth = 0) + cache.fetch(:___routes) { [] } + options.find_all { |pair| + cache.key?(pair) + }.map { |pair| + possibles(cache[pair], options, depth + 1) + }.flatten(1) + end + + # Returns +true+ if no missing keys are present, otherwise +false+. + def verify_required_parts!(route, parts) + missing_keys(route, parts).empty? + end + + def build_cache + root = { ___routes: [] } + routes.each_with_index do |route, i| + leaf = route.required_defaults.inject(root) do |h, tuple| + h[tuple] ||= {} + end + (leaf[:___routes] ||= []) << [i, route] + end + root + end + + def cache + @cache ||= build_cache + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb new file mode 100644 index 0000000000..7d2791714b --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -0,0 +1,162 @@ +require 'action_dispatch/journey/gtg/transition_table' + +module ActionDispatch + module Journey # :nodoc: + module GTG # :nodoc: + class Builder # :nodoc: + DUMMY = Nodes::Dummy.new + + attr_reader :root, :ast, :endpoints + + def initialize(root) + @root = root + @ast = Nodes::Cat.new root, DUMMY + @followpos = nil + end + + def transition_table + dtrans = TransitionTable.new + marked = {} + state_id = Hash.new { |h,k| h[k] = h.length } + + start = firstpos(root) + dstates = [start] + until dstates.empty? + s = dstates.shift + next if marked[s] + marked[s] = true # mark s + + s.group_by { |state| symbol(state) }.each do |sym, ps| + u = ps.map { |l| followpos(l) }.flatten + next if u.empty? + + if u.uniq == [DUMMY] + from = state_id[s] + to = state_id[Object.new] + dtrans[from, to] = sym + + dtrans.add_accepting(to) + ps.each { |state| dtrans.add_memo(to, state.memo) } + else + dtrans[state_id[s], state_id[u]] = sym + + if u.include?(DUMMY) + to = state_id[u] + + accepting = ps.find_all { |l| followpos(l).include?(DUMMY) } + + accepting.each { |accepting_state| + dtrans.add_memo(to, accepting_state.memo) + } + + dtrans.add_accepting(state_id[u]) + end + end + + dstates << u + end + end + + dtrans + end + + def nullable?(node) + case node + when Nodes::Group + true + when Nodes::Star + true + when Nodes::Or + node.children.any? { |c| nullable?(c) } + when Nodes::Cat + nullable?(node.left) && nullable?(node.right) + when Nodes::Terminal + !node.left + when Nodes::Unary + nullable?(node.left) + else + raise ArgumentError, 'unknown nullable: %s' % node.class.name + end + end + + def firstpos(node) + case node + when Nodes::Star + firstpos(node.left) + when Nodes::Cat + if nullable?(node.left) + firstpos(node.left) | firstpos(node.right) + else + firstpos(node.left) + end + when Nodes::Or + node.children.map { |c| firstpos(c) }.flatten.uniq + when Nodes::Unary + firstpos(node.left) + when Nodes::Terminal + nullable?(node) ? [] : [node] + else + raise ArgumentError, 'unknown firstpos: %s' % node.class.name + end + end + + def lastpos(node) + case node + when Nodes::Star + firstpos(node.left) + when Nodes::Or + node.children.map { |c| lastpos(c) }.flatten.uniq + when Nodes::Cat + if nullable?(node.right) + lastpos(node.left) | lastpos(node.right) + else + lastpos(node.right) + end + when Nodes::Terminal + nullable?(node) ? [] : [node] + when Nodes::Unary + lastpos(node.left) + else + raise ArgumentError, 'unknown lastpos: %s' % node.class.name + end + end + + def followpos(node) + followpos_table[node] + end + + private + + def followpos_table + @followpos ||= build_followpos + end + + def build_followpos + table = Hash.new { |h, k| h[k] = [] } + @ast.each do |n| + case n + when Nodes::Cat + lastpos(n.left).each do |i| + table[i] += firstpos(n.right) + end + when Nodes::Star + lastpos(n).each do |i| + table[i] += firstpos(n) + end + end + end + table + end + + def symbol(edge) + case edge + when Journey::Nodes::Symbol + edge.regexp + else + edge.left + end + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb new file mode 100644 index 0000000000..58ad803841 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -0,0 +1,44 @@ +require 'strscan' + +module ActionDispatch + module Journey # :nodoc: + module GTG # :nodoc: + class MatchData # :nodoc: + attr_reader :memos + + def initialize(memos) + @memos = memos + end + end + + class Simulator # :nodoc: + attr_reader :tt + + def initialize(transition_table) + @tt = transition_table + end + + def simulate(string) + input = StringScanner.new(string) + state = [0] + while sym = input.scan(%r([/.?]|[^/.?]+)) + state = tt.move(state, sym) + end + + acceptance_states = state.find_all { |s| + tt.accepting? s + } + + return if acceptance_states.empty? + + memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + + MatchData.new(memos) + end + + alias :=~ :simulate + alias :match :simulate + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb new file mode 100644 index 0000000000..da0cddd93c --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -0,0 +1,156 @@ +require 'action_dispatch/journey/nfa/dot' + +module ActionDispatch + module Journey # :nodoc: + module GTG # :nodoc: + class TransitionTable # :nodoc: + include Journey::NFA::Dot + + attr_reader :memos + + def initialize + @regexp_states = Hash.new { |h,k| h[k] = {} } + @string_states = Hash.new { |h,k| h[k] = {} } + @accepting = {} + @memos = Hash.new { |h,k| h[k] = [] } + end + + def add_accepting(state) + @accepting[state] = true + end + + def accepting_states + @accepting.keys + end + + def accepting?(state) + @accepting[state] + end + + def add_memo(idx, memo) + @memos[idx] << memo + end + + def memo(idx) + @memos[idx] + end + + def eclosure(t) + Array(t) + end + + def move(t, a) + move_string(t, a).concat(move_regexp(t, a)) + end + + def to_json + require 'json' + + simple_regexp = Hash.new { |h,k| h[k] = {} } + + @regexp_states.each do |from, hash| + hash.each do |re, to| + simple_regexp[from][re.source] = to + end + end + + JSON.dump({ + regexp_states: simple_regexp, + string_states: @string_states, + accepting: @accepting + }) + end + + def to_svg + svg = IO.popen('dot -Tsvg', 'w+') { |f| + f.write(to_dot) + f.close_write + f.readlines + } + 3.times { svg.shift } + svg.join.sub(/width="[^"]*"/, '').sub(/height="[^"]*"/, '') + end + + def visualizer(paths, title = 'FSM') + viz_dir = File.join File.dirname(__FILE__), '..', 'visualizer' + fsm_js = File.read File.join(viz_dir, 'fsm.js') + fsm_css = File.read File.join(viz_dir, 'fsm.css') + erb = File.read File.join(viz_dir, 'index.html.erb') + states = "function tt() { return #{to_json}; }" + + fun_routes = paths.shuffle.first(3).map do |ast| + ast.map { |n| + case n + when Nodes::Symbol + case n.left + when ':id' then rand(100).to_s + when ':format' then %w{ xml json }.shuffle.first + else + 'omg' + end + when Nodes::Terminal then n.symbol + else + nil + end + }.compact.join + end + + stylesheets = [fsm_css] + svg = to_svg + javascripts = [states, fsm_js] + + # Annoying hack for 1.9 warnings + fun_routes = fun_routes + stylesheets = stylesheets + svg = svg + javascripts = javascripts + + require 'erb' + template = ERB.new erb + template.result(binding) + end + + def []=(from, to, sym) + case sym + when String + @string_states[from][sym] = to + when Regexp + @regexp_states[from][sym] = to + else + raise ArgumentError, 'unknown symbol: %s' % sym.class + end + end + + def states + ss = @string_states.keys + @string_states.values.map(&:values).flatten + rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten + (ss + rs).uniq + end + + def transitions + @string_states.map { |from, hash| + hash.map { |s, to| [from, s, to] } + }.flatten(1) + @regexp_states.map { |from, hash| + hash.map { |s, to| [from, s, to] } + }.flatten(1) + end + + private + + def move_regexp(t, a) + return [] if t.empty? + + t.map { |s| + @regexp_states[s].map { |re, v| re === a ? v : nil } + }.flatten.compact.uniq + end + + def move_string(t, a) + return [] if t.empty? + + t.map { |s| @string_states[s][a] }.compact + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb new file mode 100644 index 0000000000..ee6494c3e4 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb @@ -0,0 +1,76 @@ +require 'action_dispatch/journey/nfa/transition_table' +require 'action_dispatch/journey/gtg/transition_table' + +module ActionDispatch + module Journey # :nodoc: + module NFA # :nodoc: + class Visitor < Visitors::Visitor # :nodoc: + def initialize(tt) + @tt = tt + @i = -1 + end + + def visit_CAT(node) + left = visit(node.left) + right = visit(node.right) + + @tt.merge(left.last, right.first) + + [left.first, right.last] + end + + def visit_GROUP(node) + from = @i += 1 + left = visit(node.left) + to = @i += 1 + + @tt.accepting = to + + @tt[from, left.first] = nil + @tt[left.last, to] = nil + @tt[from, to] = nil + + [from, to] + end + + def visit_OR(node) + from = @i += 1 + children = node.children.map { |c| visit(c) } + to = @i += 1 + + children.each do |child| + @tt[from, child.first] = nil + @tt[child.last, to] = nil + end + + @tt.accepting = to + + [from, to] + end + + def terminal(node) + from_i = @i += 1 # new state + to_i = @i += 1 # new state + + @tt[from_i, to_i] = node + @tt.accepting = to_i + @tt.add_memo(to_i, node.memo) + + [from_i, to_i] + end + end + + class Builder # :nodoc: + def initialize(ast) + @ast = ast + end + + def transition_table + tt = TransitionTable.new + Visitor.new(tt).accept(@ast) + tt + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb new file mode 100644 index 0000000000..5c33a872e5 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 + +module ActionDispatch + module Journey # :nodoc: + module NFA # :nodoc: + module Dot # :nodoc: + def to_dot + edges = transitions.map { |from, sym, to| + " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];" + } + + #memo_nodes = memos.values.flatten.map { |n| + # label = n + # if Journey::Route === n + # label = "#{n.verb.source} #{n.path.spec}" + # end + # " #{n.object_id} [label=\"#{label}\", shape=box];" + #} + #memo_edges = memos.map { |k, memos| + # (memos || []).map { |v| " #{k} -> #{v.object_id};" } + #}.flatten.uniq + + <<-eodot +digraph nfa { + rankdir=LR; + node [shape = doublecircle]; + #{accepting_states.join ' '}; + node [shape = circle]; +#{edges.join "\n"} +} + eodot + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb new file mode 100644 index 0000000000..5b40da6569 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb @@ -0,0 +1,47 @@ +require 'strscan' + +module ActionDispatch + module Journey # :nodoc: + module NFA # :nodoc: + class MatchData # :nodoc: + attr_reader :memos + + def initialize(memos) + @memos = memos + end + end + + class Simulator # :nodoc: + attr_reader :tt + + def initialize(transition_table) + @tt = transition_table + end + + def simulate(string) + input = StringScanner.new(string) + state = tt.eclosure(0) + until input.eos? + sym = input.scan(%r([/.?]|[^/.?]+)) + + # FIXME: tt.eclosure is not needed for the GTG + state = tt.eclosure(tt.move(state, sym)) + end + + acceptance_states = state.find_all { |s| + tt.accepting?(tt.eclosure(s).sort.last) + } + + return if acceptance_states.empty? + + memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + + MatchData.new(memos) + end + + alias :=~ :simulate + alias :match :simulate + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb new file mode 100644 index 0000000000..a3017aeea1 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -0,0 +1,163 @@ +require 'action_dispatch/journey/nfa/dot' + +module ActionDispatch + module Journey # :nodoc: + module NFA # :nodoc: + class TransitionTable # :nodoc: + include Journey::NFA::Dot + + attr_accessor :accepting + attr_reader :memos + + def initialize + @table = Hash.new { |h,f| h[f] = {} } + @memos = {} + @accepting = nil + @inverted = nil + end + + def accepting?(state) + accepting == state + end + + def accepting_states + [accepting] + end + + def add_memo(idx, memo) + @memos[idx] = memo + end + + def memo(idx) + @memos[idx] + end + + def []=(i, f, s) + @table[f][i] = s + end + + def merge(left, right) + @memos[right] = @memos.delete(left) + @table[right] = @table.delete(left) + end + + def states + (@table.keys + @table.values.map(&:keys).flatten).uniq + end + + # Returns a generalized transition graph with reduced states. The states + # are reduced like a DFA, but the table must be simulated like an NFA. + # + # Edges of the GTG are regular expressions. + def generalized_table + gt = GTG::TransitionTable.new + marked = {} + state_id = Hash.new { |h,k| h[k] = h.length } + alphabet = self.alphabet + + stack = [eclosure(0)] + + until stack.empty? + state = stack.pop + next if marked[state] || state.empty? + + marked[state] = true + + alphabet.each do |alpha| + next_state = eclosure(following_states(state, alpha)) + next if next_state.empty? + + gt[state_id[state], state_id[next_state]] = alpha + stack << next_state + end + end + + final_groups = state_id.keys.find_all { |s| + s.sort.last == accepting + } + + final_groups.each do |states| + id = state_id[states] + + gt.add_accepting(id) + save = states.find { |s| + @memos.key?(s) && eclosure(s).sort.last == accepting + } + + gt.add_memo(id, memo(save)) + end + + gt + end + + # Returns set of NFA states to which there is a transition on ast symbol + # +a+ from some state +s+ in +t+. + def following_states(t, a) + Array(t).map { |s| inverted[s][a] }.flatten.uniq + end + + # Returns set of NFA states to which there is a transition on ast symbol + # +a+ from some state +s+ in +t+. + def move(t, a) + Array(t).map { |s| + inverted[s].keys.compact.find_all { |sym| + sym === a + }.map { |sym| inverted[s][sym] } + }.flatten.uniq + end + + def alphabet + inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s } + end + + # Returns a set of NFA states reachable from some NFA state +s+ in set + # +t+ on nil-transitions alone. + def eclosure(t) + stack = Array(t) + seen = {} + children = [] + + until stack.empty? + s = stack.pop + next if seen[s] + + seen[s] = true + children << s + + stack.concat(inverted[s][nil]) + end + + children.uniq + end + + def transitions + @table.map { |to, hash| + hash.map { |from, sym| [from, sym, to] } + }.flatten(1) + end + + private + + def inverted + return @inverted if @inverted + + @inverted = Hash.new { |h, from| + h[from] = Hash.new { |j, s| j[s] = [] } + } + + @table.each { |to, hash| + hash.each { |from, sym| + if sym + sym = Nodes::Symbol === sym ? sym.regexp : sym.left + end + + @inverted[from][sym] << to + } + } + + @inverted + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb new file mode 100644 index 0000000000..935442ef66 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb @@ -0,0 +1,124 @@ +require 'action_dispatch/journey/visitors' + +module ActionDispatch + module Journey # :nodoc: + module Nodes # :nodoc: + class Node # :nodoc: + include Enumerable + + attr_accessor :left, :memo + + def initialize(left) + @left = left + @memo = nil + end + + def each(&block) + Visitors::Each.new(block).accept(self) + end + + def to_s + Visitors::String.new.accept(self) + end + + def to_dot + Visitors::Dot.new.accept(self) + end + + def to_sym + name.to_sym + end + + def name + left.tr '*:', '' + end + + def type + raise NotImplementedError + end + + def symbol?; false; end + def literal?; false; end + end + + class Terminal < Node # :nodoc: + alias :symbol :left + end + + class Literal < Terminal # :nodoc: + def literal?; true; end + def type; :LITERAL; end + end + + class Dummy < Literal # :nodoc: + def initialize(x = Object.new) + super + end + + def literal?; false; end + end + + %w{ Symbol Slash Dot }.each do |t| + class_eval <<-eoruby, __FILE__, __LINE__ + 1 + class #{t} < Terminal; + def type; :#{t.upcase}; end + end + eoruby + end + + class Symbol < Terminal # :nodoc: + attr_accessor :regexp + alias :symbol :regexp + + DEFAULT_EXP = /[^\.\/\?]+/ + def initialize(left) + super + @regexp = DEFAULT_EXP + end + + def default_regexp? + regexp == DEFAULT_EXP + end + + def symbol?; true; end + end + + class Unary < Node # :nodoc: + def children; [left] end + end + + class Group < Unary # :nodoc: + def type; :GROUP; end + end + + class Star < Unary # :nodoc: + def type; :STAR; end + end + + class Binary < Node # :nodoc: + attr_accessor :right + + def initialize(left, right) + super(left) + @right = right + end + + def children; [left, right] end + end + + class Cat < Binary # :nodoc: + def type; :CAT; end + end + + class Or < Node # :nodoc: + attr_reader :children + + def initialize(children) + @children = children + end + + def type; :OR; end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb new file mode 100644 index 0000000000..bb4cbb00e2 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -0,0 +1,206 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by Racc 1.4.9 +# from Racc grammer file "". +# + +require 'racc/parser.rb' + + +require 'action_dispatch/journey/parser_extras' +module ActionDispatch + module Journey # :nodoc: + class Parser < Racc::Parser # :nodoc: +##### State transition tables begin ### + +racc_action_table = [ + 17, 21, 13, 15, 14, 7, nil, 16, 8, 19, + 13, 15, 14, 7, 23, 16, 8, 19, 13, 15, + 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, + 16, 8, 13, 15, 14, 7, nil, 16, 8 ] + +racc_action_check = [ + 1, 17, 1, 1, 1, 1, nil, 1, 1, 1, + 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, + 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, + 19, 19, 0, 0, 0, 0, nil, 0, 0 ] + +racc_action_pointer = [ + 30, 0, nil, nil, nil, nil, nil, 16, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, + 8, nil, nil, nil ] + +racc_action_default = [ + -18, -18, -2, -3, -4, -5, -6, -18, -9, -10, + -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, + -18, 24, -8, -7 ] + +racc_goto_table = [ + 18, 1, nil, nil, nil, nil, nil, nil, 20, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ] + +racc_goto_check = [ + 2, 1, nil, nil, nil, nil, nil, nil, 1, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + +racc_goto_pointer = [ + nil, 1, -1, nil, nil, nil, nil, nil, nil, nil, + nil ] + +racc_goto_default = [ + nil, nil, 2, 3, 4, 5, 6, 9, 10, 11, + 12 ] + +racc_reduce_table = [ + 0, 0, :racc_error, + 2, 11, :_reduce_1, + 1, 11, :_reduce_2, + 1, 11, :_reduce_none, + 1, 12, :_reduce_none, + 1, 12, :_reduce_none, + 1, 12, :_reduce_none, + 3, 15, :_reduce_7, + 3, 13, :_reduce_8, + 1, 16, :_reduce_9, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 14, :_reduce_none, + 1, 19, :_reduce_14, + 1, 17, :_reduce_15, + 1, 18, :_reduce_16, + 1, 20, :_reduce_17 ] + +racc_reduce_n = 18 + +racc_shift_n = 24 + +racc_token_table = { + false => 0, + :error => 1, + :SLASH => 2, + :LITERAL => 3, + :SYMBOL => 4, + :LPAREN => 5, + :RPAREN => 6, + :DOT => 7, + :STAR => 8, + :OR => 9 } + +racc_nt_base = 10 + +racc_use_result_var = true + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ + "$end", + "error", + "SLASH", + "LITERAL", + "SYMBOL", + "LPAREN", + "RPAREN", + "DOT", + "STAR", + "OR", + "$start", + "expressions", + "expression", + "or", + "terminal", + "group", + "star", + "symbol", + "literal", + "slash", + "dot" ] + +Racc_debug_parser = false + +##### State transition tables end ##### + +# reduce 0 omitted + +def _reduce_1(val, _values, result) + result = Cat.new(val.first, val.last) + result +end + +def _reduce_2(val, _values, result) + result = val.first + result +end + +# reduce 3 omitted + +# reduce 4 omitted + +# reduce 5 omitted + +# reduce 6 omitted + +def _reduce_7(val, _values, result) + result = Group.new(val[1]) + result +end + +def _reduce_8(val, _values, result) + result = Or.new([val.first, val.last]) + result +end + +def _reduce_9(val, _values, result) + result = Star.new(Symbol.new(val.last)) + result +end + +# reduce 10 omitted + +# reduce 11 omitted + +# reduce 12 omitted + +# reduce 13 omitted + +def _reduce_14(val, _values, result) + result = Slash.new('/') + result +end + +def _reduce_15(val, _values, result) + result = Symbol.new(val.first) + result +end + +def _reduce_16(val, _values, result) + result = Literal.new(val.first) + result +end + +def _reduce_17(val, _values, result) + result = Dot.new(val.first) + result +end + +def _reduce_none(val, _values, result) + val[0] +end + + end # class Parser + end # module Journey + end # module ActionDispatch diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y new file mode 100644 index 0000000000..a2e1afed32 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -0,0 +1,47 @@ +class ActionDispatch::Journey::Parser + +token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR + +rule + expressions + : expressions expression { result = Cat.new(val.first, val.last) } + | expression { result = val.first } + | or + ; + expression + : terminal + | group + | star + ; + group + : LPAREN expressions RPAREN { result = Group.new(val[1]) } + ; + or + : expressions OR expression { result = Or.new([val.first, val.last]) } + ; + star + : STAR { result = Star.new(Symbol.new(val.last)) } + ; + terminal + : symbol + | literal + | slash + | dot + ; + slash + : SLASH { result = Slash.new('/') } + ; + symbol + : SYMBOL { result = Symbol.new(val.first) } + ; + literal + : LITERAL { result = Literal.new(val.first) } + dot + : DOT { result = Dot.new(val.first) } + ; + +end + +---- header + +require 'action_dispatch/journey/parser_extras' diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb new file mode 100644 index 0000000000..14892f4321 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb @@ -0,0 +1,23 @@ +require 'action_dispatch/journey/scanner' +require 'action_dispatch/journey/nodes/node' + +module ActionDispatch + module Journey # :nodoc: + class Parser < Racc::Parser # :nodoc: + include Journey::Nodes + + def initialize + @scanner = Scanner.new + end + + def parse(string) + @scanner.scan_setup(string) + do_parse + end + + def next_token + @scanner.next_token + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb new file mode 100644 index 0000000000..4a571ec546 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -0,0 +1,196 @@ +module ActionDispatch + module Journey # :nodoc: + module Path # :nodoc: + class Pattern # :nodoc: + attr_reader :spec, :requirements, :anchored + + def initialize(strexp) + parser = Journey::Parser.new + + @anchored = true + + case strexp + when String + @spec = parser.parse(strexp) + @requirements = {} + @separators = "/.?" + when Router::Strexp + @spec = parser.parse(strexp.path) + @requirements = strexp.requirements + @separators = strexp.separators.join + @anchored = strexp.anchor + else + raise "wtf bro: #{strexp}" + end + + @names = nil + @optional_names = nil + @required_names = nil + @re = nil + @offsets = nil + end + + def ast + @spec.grep(Nodes::Symbol).each do |node| + re = @requirements[node.to_sym] + node.regexp = re if re + end + + @spec.grep(Nodes::Star).each do |node| + node = node.left + node.regexp = @requirements[node.to_sym] || /(.+)/ + end + + @spec + end + + def names + @names ||= spec.grep(Nodes::Symbol).map { |n| n.name } + end + + def required_names + @required_names ||= names - optional_names + end + + def optional_names + @optional_names ||= spec.grep(Nodes::Group).map { |group| + group.grep(Nodes::Symbol) + }.flatten.map { |n| n.name }.uniq + end + + class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: + attr_reader :offsets + + def initialize(matchers) + @matchers = matchers + @capture_count = [0] + end + + def visit(node) + super + @capture_count + end + + def visit_SYMBOL(node) + node = node.to_sym + + if @matchers.key?(node) + re = /#{@matchers[node]}|/ + @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0)) + else + @capture_count << (@capture_count.last || 0) + end + end + end + + class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc: + def initialize(separator, matchers) + @separator = separator + @matchers = matchers + @separator_re = "([^#{separator}]+)" + super() + end + + def accept(node) + %r{\A#{visit node}\Z} + end + + def visit_CAT(node) + [visit(node.left), visit(node.right)].join + end + + def visit_SYMBOL(node) + node = node.to_sym + + return @separator_re unless @matchers.key?(node) + + re = @matchers[node] + "(#{re})" + end + + def visit_GROUP(node) + "(?:#{visit node.left})?" + end + + def visit_LITERAL(node) + Regexp.escape(node.left) + end + alias :visit_DOT :visit_LITERAL + + def visit_SLASH(node) + node.left + end + + def visit_STAR(node) + re = @matchers[node.left.to_sym] || '.+' + "(#{re})" + end + end + + class UnanchoredRegexp < AnchoredRegexp # :nodoc: + def accept(node) + %r{\A#{visit node}} + end + end + + class MatchData # :nodoc: + attr_reader :names + + def initialize(names, offsets, match) + @names = names + @offsets = offsets + @match = match + end + + def captures + (length - 1).times.map { |i| self[i + 1] } + end + + def [](x) + idx = @offsets[x - 1] + x + @match[idx] + end + + def length + @offsets.length + end + + def post_match + @match.post_match + end + + def to_s + @match.to_s + end + end + + def match(other) + return unless match = to_regexp.match(other) + MatchData.new(names, offsets, match) + end + alias :=~ :match + + def source + to_regexp.source + end + + def to_regexp + @re ||= regexp_visitor.new(@separators, @requirements).accept spec + end + + private + + def regexp_visitor + @anchored ? AnchoredRegexp : UnanchoredRegexp + end + + def offsets + return @offsets if @offsets + + viz = RegexpOffsets.new(@requirements) + @offsets = viz.accept(spec) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb new file mode 100644 index 0000000000..d18efd863a --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -0,0 +1,94 @@ +module ActionDispatch + module Journey # :nodoc: + class Route # :nodoc: + attr_reader :app, :path, :verb, :defaults, :ip, :name + + attr_reader :constraints + alias :conditions :constraints + + attr_accessor :precedence + + ## + # +path+ is a path constraint. + # +constraints+ is a hash of constraints to be applied to this route. + def initialize(name, app, path, constraints, defaults = {}) + constraints = constraints.dup + @name = name + @app = app + @path = path + @verb = constraints[:request_method] || // + @ip = constraints.delete(:ip) || // + + @constraints = constraints + @constraints.keep_if { |_,v| Regexp === v || String === v } + @defaults = defaults + @required_defaults = nil + @required_parts = nil + @parts = nil + @decorated_ast = nil + @precedence = 0 + end + + def ast + return @decorated_ast if @decorated_ast + + @decorated_ast = path.ast + @decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self } + @decorated_ast + end + + def requirements # :nodoc: + # needed for rails `rake routes` + path.requirements.merge(@defaults).delete_if { |_,v| + /.+?/ == v + } + end + + def segments + @path.names + end + + def required_keys + path.required_names.map { |x| x.to_sym } + required_defaults.keys + end + + def score(constraints) + required_keys = path.required_names + supplied_keys = constraints.map { |k,v| v && k.to_s }.compact + + return -1 unless (required_keys - supplied_keys).empty? + + score = (supplied_keys & path.names).length + score + (required_defaults.length * 2) + end + + def parts + @parts ||= segments.map { |n| n.to_sym } + end + alias :segment_keys :parts + + def format(path_options) + path_options.delete_if do |key, value| + value.to_s == defaults[key].to_s && !required_parts.include?(key) + end + + Visitors::Formatter.new(path_options).accept(path.spec) + end + + def optional_parts + path.optional_names.map { |n| n.to_sym } + end + + def required_parts + @required_parts ||= path.required_names.map { |n| n.to_sym } + end + + def required_defaults + @required_defaults ||= begin + matches = parts + @defaults.dup.delete_if { |k,_| matches.include?(k) } + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb new file mode 100644 index 0000000000..1fc45a2109 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -0,0 +1,168 @@ +require 'action_dispatch/journey/router/utils' +require 'action_dispatch/journey/router/strexp' +require 'action_dispatch/journey/routes' +require 'action_dispatch/journey/formatter' + +before = $-w +$-w = false +require 'action_dispatch/journey/parser' +$-w = before + +require 'action_dispatch/journey/route' +require 'action_dispatch/journey/path/pattern' + +module ActionDispatch + module Journey # :nodoc: + class Router # :nodoc: + class RoutingError < ::StandardError # :nodoc: + end + + # :nodoc: + VERSION = '2.0.0' + + class NullReq # :nodoc: + attr_reader :env + def initialize(env) + @env = env + end + + def request_method + env['REQUEST_METHOD'] + end + + def path_info + env['PATH_INFO'] + end + + def ip + env['REMOTE_ADDR'] + end + + def [](k); env[k]; end + end + + attr_reader :request_class, :formatter + attr_accessor :routes + + def initialize(routes, options) + @options = options + @params_key = options[:parameters_key] + @request_class = options[:request_class] || NullReq + @routes = routes + end + + def call(env) + env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) + + find_routes(env).each do |match, parameters, route| + script_name, path_info, set_params = env.values_at('SCRIPT_NAME', + 'PATH_INFO', + @params_key) + + unless route.path.anchored + env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') + env['PATH_INFO'] = match.post_match + end + + env[@params_key] = (set_params || {}).merge parameters + + status, headers, body = route.app.call(env) + + if 'pass' == headers['X-Cascade'] + env['SCRIPT_NAME'] = script_name + env['PATH_INFO'] = path_info + env[@params_key] = set_params + next + end + + return [status, headers, body] + end + + return [404, {'X-Cascade' => 'pass'}, ['Not Found']] + end + + def recognize(req) + find_routes(req.env).each do |match, parameters, route| + unless route.path.anchored + req.env['SCRIPT_NAME'] = match.to_s + req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') + end + + yield(route, nil, parameters) + end + end + + def visualizer + tt = GTG::Builder.new(ast).transition_table + groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s } + asts = groups.values.map { |v| v.first } + tt.visualizer(asts) + end + + private + + def partitioned_routes + routes.partitioned_routes + end + + def ast + routes.ast + end + + def simulator + routes.simulator + end + + def custom_routes + partitioned_routes.last + end + + def filter_routes(path) + return [] unless ast + data = simulator.match(path) + data ? data.memos : [] + end + + def find_routes env + req = request_class.new(env) + + routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| + r.path.match(req.path_info) + } + routes.concat get_routes_as_head(routes) + + routes.sort_by!(&:precedence).select! { |r| + r.constraints.all? { |k, v| v === req.send(k) } && + r.verb === req.request_method + } + routes.reject! { |r| req.ip && !(r.ip === req.ip) } + + routes.map! { |r| + match_data = r.path.match(req.path_info) + match_names = match_data.names.map { |n| n.to_sym } + match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } + info = Hash[match_names.zip(match_values).find_all { |_, y| y }] + + [match_data, r.defaults.merge(info), r] + } + end + + def get_routes_as_head(routes) + precedence = (routes.map(&:precedence).max || 0) + 1 + routes = routes.select { |r| + r.verb === "GET" && !(r.verb === "HEAD") + }.map! { |r| + Route.new(r.name, + r.app, + r.path, + r.conditions.merge(request_method: "HEAD"), + r.defaults).tap do |route| + route.precedence = r.precedence + precedence + end + } + routes.flatten! + routes + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb new file mode 100644 index 0000000000..f97f1a223e --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb @@ -0,0 +1,24 @@ +module ActionDispatch + module Journey # :nodoc: + class Router # :nodoc: + class Strexp # :nodoc: + class << self + alias :compile :new + end + + attr_reader :path, :requirements, :separators, :anchor + + def initialize(path, requirements, separators, anchor = true) + @path = path + @requirements = requirements + @separators = separators + @anchor = anchor + end + + def names + @path.scan(/:\w+/).map { |s| s.tr(':', '') } + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb new file mode 100644 index 0000000000..462f1a122d --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -0,0 +1,54 @@ +require 'uri' + +module ActionDispatch + module Journey # :nodoc: + class Router # :nodoc: + class Utils # :nodoc: + # Normalizes URI path. + # + # Strips off trailing slash and ensures there is a leading slash. + # + # normalize_path("/foo") # => "/foo" + # normalize_path("/foo/") # => "/foo" + # normalize_path("foo") # => "/foo" + # normalize_path("") # => "/" + def self.normalize_path(path) + path = "/#{path}" + path.squeeze!('/') + path.sub!(%r{/+\Z}, '') + path = '/' if path == '' + path + end + + # URI path and fragment escaping + # http://tools.ietf.org/html/rfc3986 + module UriEscape # :nodoc: + # Symbol captures can generate multiple path segments, so include /. + reserved_segment = '/' + reserved_fragment = '/?' + reserved_pchar = ':@&=+$,;%' + + safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}" + safe_segment = "#{safe_pchar}#{reserved_segment}" + safe_fragment = "#{safe_pchar}#{reserved_fragment}" + UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze + UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze + end + + Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI + + def self.escape_path(path) + Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) + end + + def self.escape_fragment(fragment) + Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT) + end + + def self.unescape_uri(uri) + Parser.unescape(uri) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb new file mode 100644 index 0000000000..32829a1f20 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -0,0 +1,76 @@ +module ActionDispatch + module Journey # :nodoc: + # The Routing table. Contains all routes for a system. Routes can be + # added to the table by calling Routes#add_route. + class Routes # :nodoc: + include Enumerable + + attr_reader :routes, :named_routes + + def initialize + @routes = [] + @named_routes = {} + @ast = nil + @partitioned_routes = nil + @simulator = nil + end + + def length + @routes.length + end + alias :size :length + + def last + @routes.last + end + + def each(&block) + routes.each(&block) + end + + def clear + routes.clear + end + + def partitioned_routes + @partitioned_routes ||= routes.partition { |r| + r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? } + } + end + + def ast + return @ast if @ast + return if partitioned_routes.first.empty? + + asts = partitioned_routes.first.map { |r| r.ast } + @ast = Nodes::Or.new(asts) + end + + def simulator + return @simulator if @simulator + + gtg = GTG::Builder.new(ast).transition_table + @simulator = GTG::Simulator.new(gtg) + end + + # Add a route to the routing table. + def add_route(app, path, conditions, defaults, name = nil) + route = Route.new(name, app, path, conditions, defaults) + + route.precedence = routes.length + routes << route + named_routes[name] = route if name && !named_routes[name] + clear_cache! + route + end + + private + + def clear_cache! + @ast = nil + @partitioned_routes = nil + @simulator = nil + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb new file mode 100644 index 0000000000..633be11a2d --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/scanner.rb @@ -0,0 +1,61 @@ +require 'strscan' + +module ActionDispatch + module Journey # :nodoc: + class Scanner # :nodoc: + def initialize + @ss = nil + end + + def scan_setup(str) + @ss = StringScanner.new(str) + end + + def eos? + @ss.eos? + end + + def pos + @ss.pos + end + + def pre_match + @ss.pre_match + end + + def next_token + return if @ss.eos? + + until token = scan || @ss.eos?; end + token + end + + private + + def scan + case + # / + when text = @ss.scan(/\//) + [:SLASH, text] + when text = @ss.scan(/\*\w+/) + [:STAR, text] + when text = @ss.scan(/\(/) + [:LPAREN, text] + when text = @ss.scan(/\)/) + [:RPAREN, text] + when text = @ss.scan(/\|/) + [:OR, text] + when text = @ss.scan(/\./) + [:DOT, text] + when text = @ss.scan(/:\w+/) + [:SYMBOL, text] + when text = @ss.scan(/[\w%\-~]+/) + [:LITERAL, text] + # any char + when text = @ss.scan(/./) + [:LITERAL, text] + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb new file mode 100644 index 0000000000..46bd58c178 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -0,0 +1,189 @@ +# encoding: utf-8 +module ActionDispatch + module Journey # :nodoc: + module Visitors # :nodoc: + class Visitor # :nodoc: + DISPATCH_CACHE = Hash.new { |h,k| + h[k] = "visit_#{k}" + } + + def accept(node) + visit(node) + end + + private + + def visit node + send(DISPATCH_CACHE[node.type], node) + end + + def binary(node) + visit(node.left) + visit(node.right) + end + def visit_CAT(n); binary(n); end + + def nary(node) + node.children.each { |c| visit(c) } + end + def visit_OR(n); nary(n); end + + def unary(node) + visit(node.left) + end + def visit_GROUP(n); unary(n); end + def visit_STAR(n); unary(n); end + + def terminal(node); end + %w{ LITERAL SYMBOL SLASH DOT }.each do |t| + class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + end + end + + # Loop through the requirements AST + class Each < Visitor # :nodoc: + attr_reader :block + + def initialize(block) + @block = block + end + + def visit(node) + super + block.call(node) + end + end + + class String < Visitor # :nodoc: + private + + def binary(node) + [visit(node.left), visit(node.right)].join + end + + def nary(node) + node.children.map { |c| visit(c) }.join '|' + end + + def terminal(node) + node.left + end + + def visit_GROUP(node) + "(#{visit(node.left)})" + end + end + + # Used for formatting urls (url_for) + class Formatter < Visitor # :nodoc: + attr_reader :options, :consumed + + def initialize(options) + @options = options + @consumed = {} + end + + private + + def visit_GROUP(node) + if consumed == options + nil + else + route = visit(node.left) + route.include?("\0") ? nil : route + end + end + + def terminal(node) + node.left + end + + def binary(node) + [visit(node.left), visit(node.right)].join + end + + def nary(node) + node.children.map { |c| visit(c) }.join + end + + def visit_SYMBOL(node) + key = node.to_sym + + if value = options[key] + consumed[key] = value + Router::Utils.escape_path(value) + else + "\0" + end + end + end + + class Dot < Visitor # :nodoc: + def initialize + @nodes = [] + @edges = [] + end + + def accept(node) + super + <<-eodot + digraph parse_tree { + size="8,5" + node [shape = none]; + edge [dir = none]; + #{@nodes.join "\n"} + #{@edges.join("\n")} + } + eodot + end + + private + + def binary(node) + node.children.each do |c| + @edges << "#{node.object_id} -> #{c.object_id};" + end + super + end + + def nary(node) + node.children.each do |c| + @edges << "#{node.object_id} -> #{c.object_id};" + end + super + end + + def unary(node) + @edges << "#{node.object_id} -> #{node.left.object_id};" + super + end + + def visit_GROUP(node) + @nodes << "#{node.object_id} [label=\"()\"];" + super + end + + def visit_CAT(node) + @nodes << "#{node.object_id} [label=\"○\"];" + super + end + + def visit_STAR(node) + @nodes << "#{node.object_id} [label=\"*\"];" + super + end + + def visit_OR(node) + @nodes << "#{node.object_id} [label=\"|\"];" + super + end + + def terminal(node) + value = node.left + + @nodes << "#{node.object_id} [label=\"#{value}\"];" + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css new file mode 100644 index 0000000000..50caebaa18 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css @@ -0,0 +1,34 @@ +body { + font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif; + margin: 0; +} + +h1 { + font-size: 2.0em; font-weight: bold; text-align: center; + color: white; background-color: black; + padding: 5px 0; + margin: 0 0 20px; +} + +h2 { + text-align: center; + display: none; + font-size: 0.5em; +} + +div#chart-2 { + height: 350px; +} + +.clearfix {display: inline-block; } +.input { overflow: show;} +.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em} +.instruction p { padding: 0 0 5px; } +.instruction li { padding: 0 10px 5px; } + +.form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px} +.form p, .form form { text-align: center } +.form form {padding: 0 10px 5px; } +.form .fun_routes { font-size: 0.9em;} +.form .fun_routes a { margin: 0 5px 0 0; } + diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.js b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js new file mode 100644 index 0000000000..d9bcaef928 --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js @@ -0,0 +1,134 @@ +function tokenize(input, callback) { + while(input.length > 0) { + callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]); + input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, ''); + } +} + +var graph = d3.select("#chart-2 svg"); +var svg_edges = {}; +var svg_nodes = {}; + +graph.selectAll("g.edge").each(function() { + var node = d3.select(this); + var index = node.select("title").text().split("->"); + var left = parseInt(index[0]); + var right = parseInt(index[1]); + + if(!svg_edges[left]) { svg_edges[left] = {} } + svg_edges[left][right] = node; +}); + +graph.selectAll("g.node").each(function() { + var node = d3.select(this); + var index = parseInt(node.select("title").text()); + svg_nodes[index] = node; +}); + +function reset_graph() { + for(var key in svg_edges) { + for(var mkey in svg_edges[key]) { + var node = svg_edges[key][mkey]; + var path = node.select("path"); + var arrow = node.select("polygon"); + path.style("stroke", "black"); + arrow.style("stroke", "black").style("fill", "black"); + } + } + + for(var key in svg_nodes) { + var node = svg_nodes[key]; + node.select('ellipse').style("fill", "white"); + node.select('polygon').style("fill", "white"); + } + return false; +} + +function highlight_edge(from, to) { + var node = svg_edges[from][to]; + var path = node.select("path"); + var arrow = node.select("polygon"); + + path + .transition().duration(500) + .style("stroke", "green"); + + arrow + .transition().duration(500) + .style("stroke", "green").style("fill", "green"); +} + +function highlight_state(index, color) { + if(!color) { color = "green"; } + + svg_nodes[index].select('ellipse') + .style("fill", "white") + .transition().duration(500) + .style("fill", color); +} + +function highlight_finish(index) { + svg_nodes[index].select('polygon') + .style("fill", "while") + .transition().duration(500) + .style("fill", "blue"); +} + +function match(input) { + reset_graph(); + var table = tt(); + var states = [0]; + var regexp_states = table['regexp_states']; + var string_states = table['string_states']; + var accepting = table['accepting']; + + highlight_state(0); + + tokenize(input, function(token) { + var new_states = []; + for(var key in states) { + var state = states[key]; + + if(string_states[state] && string_states[state][token]) { + var new_state = string_states[state][token]; + highlight_edge(state, new_state); + highlight_state(new_state); + new_states.push(new_state); + } + + if(regexp_states[state]) { + for(var key in regexp_states[state]) { + var re = new RegExp("^" + key + "$"); + if(re.test(token)) { + var new_state = regexp_states[state][key]; + highlight_edge(state, new_state); + highlight_state(new_state); + new_states.push(new_state); + } + } + } + } + + if(new_states.length == 0) { + return; + } + states = new_states; + }); + + for(var key in states) { + var state = states[key]; + if(accepting[state]) { + for(var mkey in svg_edges[state]) { + if(!regexp_states[mkey] && !string_states[mkey]) { + highlight_edge(state, mkey); + highlight_finish(mkey); + } + } + } else { + highlight_state(state, "red"); + } + } + + return false; +} + diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb new file mode 100644 index 0000000000..6aff10956a --- /dev/null +++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> + <head> + <title><%= title %></title> + <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css"> + <style> + <% stylesheets.each do |style| %> + <%= style %> + <% end %> + </style> + <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script> + </head> + <body> + <div id="wrapper"> + <h1>Routes FSM with NFA simulation</h1> + <div class="instruction form"> + <p> + Type a route in to the box and click "simulate". + </p> + <form onsubmit="return match(this.route.value);"> + <input type="text" size="30" name="route" value="/articles/new" /> + <button>simulate</button> + <input type="reset" value="reset" onclick="return reset_graph();"/> + </form> + <p class="fun_routes"> + Some fun routes to try: + <% fun_routes.each do |path| %> + <a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));"> + <%= path %> + </a> + <% end %> + </p> + </div> + <div class='chart' id='chart-2'> + <%= svg %> + </div> + <div class="instruction"> + <p> + This is a FSM for a system that has the following routes: + </p> + <ul> + <% paths.each do |route| %> + <li><%= route %></li> + <% end %> + </ul> + </div> + </div> + <% javascripts.each do |js| %> + <script><%= js %></script> + <% end %> + </body> +</html> diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 1dc51d62e0..6705e531cb 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -16,10 +16,9 @@ module ActionDispatch def call(env) begin - response = @app.call(env) + response = (_, headers, body = @app.call(env)) - if response[1]['X-Cascade'] == 'pass' - body = response[2] + if headers['X-Cascade'] == 'pass' body.close if body.respond_to?(:close) raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" end diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route_wrapper.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route_wrapper.html.erb index 9bf5c96c95..dc17cb77ef 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_route_wrapper.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route_wrapper.html.erb @@ -24,22 +24,22 @@ <script type='text/javascript'> function each(elems, func) { - if (!elems instanceof Array) var elems = [elems]; - for(var i = elems.length; i--; ) { + if (!elems instanceof Array) { elems = [elems]; } + for (var i = elems.length; i--; ) { func(elems[i]); - }; + } } function setValOn(elems, val) { each(elems, function(elem) { elem.innerHTML = val; - }) + }); } - function onClick(elems, func) { + function onClick(elems, func) { each(elems, function(elem) { elem.onclick = func; - }) + }); } // Enables functionality to toggle between `_path` and `_url` helper suffixes @@ -49,7 +49,7 @@ var helperTxt = this.getAttribute("data-route-helper"); var helperElems = document.querySelectorAll('[data-route-name] span.helper'); setValOn(helperElems, helperTxt); - }) + }); } setupRouteToggleHelperLinks(); diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 8d7461ecc3..73b2b4ac1d 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -86,7 +86,7 @@ module ActionDispatch end.collect do |route| collect_engine_routes(route) - {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs } + { name: route.name, verb: route.verb, path: route.path, reqs: route.reqs } end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index eb9d4b24f1..b1959e388c 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,4 +1,4 @@ -require 'journey' +require 'action_dispatch/journey' require 'forwardable' require 'thread_safe' require 'active_support/core_ext/object/to_query' diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 76311c423a..8e19025722 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -130,6 +130,7 @@ module ActionDispatch # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" + # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path. # # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to # +url_for+ is forwarded to the Routes module. @@ -142,6 +143,10 @@ module ActionDispatch # # => 'http://somehost.org/tasks/testing/' # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33' # # => 'http://somehost.org/tasks/testing?number=33' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp" + # # => 'http://somehost.org/myapp/tasks/testing' + # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true + # # => '/myapp/tasks/testing' def url_for(options = nil) case options when nil diff --git a/actionpack/lib/action_view/digestor.rb b/actionpack/lib/action_view/digestor.rb index 8bc69b9246..f3f6b425a8 100644 --- a/actionpack/lib/action_view/digestor.rb +++ b/actionpack/lib/action_view/digestor.rb @@ -2,7 +2,7 @@ require 'thread_safe' module ActionView class Digestor - EXPLICIT_DEPENDENCY = /# Template Dependency: ([^ ]+)/ + EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ # Matches: # render partial: "comments/comment", collection: commentable.comments diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index cf2a117966..11743e36f2 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -53,9 +53,11 @@ module ActionView # def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys + path_options = options.extract!('protocol').symbolize_keys + sources.uniq.map { |source| tag_options = { - "src" => path_to_javascript(source) + "src" => path_to_javascript(source, path_options) }.merge(options) content_tag(:script, "", tag_options) }.join("\n").html_safe @@ -89,11 +91,13 @@ module ActionView # def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys + path_options = options.extract!('protocol').symbolize_keys + sources.uniq.map { |source| tag_options = { "rel" => "stylesheet", "media" => "screen", - "href" => path_to_stylesheet(source) + "href" => path_to_stylesheet(source, path_options) }.merge(options) tag(:link, tag_options) }.join("\n").html_safe diff --git a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb index 45f0bc3d7b..d27df45b5a 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -21,7 +21,7 @@ module ActionView if block_given? yield builder else - builder.check_box + builder.label + render_component(builder) end end @@ -31,6 +31,12 @@ module ActionView rendered_collection + hidden end + + private + + def render_component(builder) + builder.check_box + builder.label + end end end end diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb index 4e33e79a36..e92a318c73 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -44,7 +44,8 @@ module ActionView html_options = @html_options.dup [:checked, :selected, :disabled].each do |option| - next unless current_value = @options[option] + current_value = @options[option] + next if current_value.nil? accept = if current_value.respond_to?(:call) current_value.call(item) diff --git a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb index ba2035f074..81f2ecb2b3 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -20,10 +20,16 @@ module ActionView if block_given? yield builder else - builder.radio_button + builder.label + render_component(builder) end end end + + private + + def render_component(builder) + builder.radio_button + builder.label + end end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 26d2142df9..2e124cf085 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -112,12 +112,12 @@ module ActionView # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>') # # => You searched for: <a href="search?q=rails">rails</a> def highlight(text, phrases, options = {}) - highlighter = options.fetch(:highlighter, '<mark>\1</mark>') - text = sanitize(text) if options.fetch(:sanitize, true) + if text.blank? || phrases.blank? text else + highlighter = options.fetch(:highlighter, '<mark>\1</mark>') match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) end.html_safe diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index fa516cf91b..aeee662071 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -415,40 +415,26 @@ module ActionView # also used as the name of the link unless +name+ is specified. Additional # HTML attributes for the link can be passed in +html_options+. # - # +mail_to+ has several methods for hindering email harvesters and customizing - # the email itself by passing special keys to +html_options+. + # +mail_to+ has several methods for customizing the email itself by + # passing special keys to +html_options+. # # ==== Options - # * <tt>:encode</tt> - This key will accept the strings "javascript" or "hex". - # Passing "javascript" will dynamically create and encode the mailto link then - # eval it into the DOM of the page. This method will not show the link on - # the page if the user has JavaScript disabled. Passing "hex" will hex - # encode the +email_address+ before outputting the mailto link. - # * <tt>:replace_at</tt> - When the link +name+ isn't provided, the - # +email_address+ is used for the link label. You can use this option to - # obfuscate the +email_address+ by substituting the @ sign with the string - # given as the value. - # * <tt>:replace_dot</tt> - When the link +name+ isn't provided, the - # +email_address+ is used for the link label. You can use this option to - # obfuscate the +email_address+ by substituting the . in the email with the - # string given as the value. # * <tt>:subject</tt> - Preset the subject line of the email. # * <tt>:body</tt> - Preset the body of the email. # * <tt>:cc</tt> - Carbon Copy additional recipients on the email. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. # + # ==== Obfuscation + # Prior to Rails 4.0, +mail_to+ provided options for encoding the address + # in order to hinder email harvesters. To take advantage of these options, + # install the +actionview-encoded_mail_to+ gem. + # # ==== Examples # mail_to "me@domain.com" # # => <a href="mailto:me@domain.com">me@domain.com</a> # - # mail_to "me@domain.com", "My email", encode: "javascript" - # # => <script>eval(decodeURIComponent('%64%6f%63...%27%29%3b'))</script> - # - # mail_to "me@domain.com", "My email", encode: "hex" - # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a> - # - # mail_to "me@domain.com", nil, replace_at: "_at_", replace_dot: "_dot_", class: "email" - # # => <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a> + # mail_to "me@domain.com", "My email" + # # => <a href="mailto:me@domain.com">My email</a> # # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com", # subject: "This is an example email" @@ -456,43 +442,15 @@ module ActionView def mail_to(email_address, name = nil, html_options = {}) email_address = ERB::Util.html_escape(email_address) - html_options = html_options.stringify_keys - encode = html_options.delete("encode").to_s + html_options.stringify_keys! extras = %w{ cc bcc body subject }.map { |item| option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) - - email_address_obfuscated = email_address.to_str - email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.key?("replace_at") - email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.key?("replace_dot") - case encode - when "javascript" - string = '' - html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe)) - html = escape_javascript(html.to_str) - "document.write('#{html}');".each_byte do |c| - string << sprintf("%%%x", c) - end - "<script>eval(decodeURIComponent('#{string}'))</script>".html_safe - when "hex" - email_address_encoded = email_address_obfuscated.unpack('C*').map {|c| - sprintf("&#%d;", c) - }.join - - string = 'mailto:'.unpack('C*').map { |c| - sprintf("&#%d;", c) - }.join + email_address.unpack('C*').map { |c| - char = c.chr - char =~ /\w/ ? sprintf("%%%x", c) : char - }.join - - content_tag "a", name || email_address_encoded.html_safe, html_options.merge("href" => "#{string}#{extras}".html_safe) - else - content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe) - end + + content_tag "a", name || email_address.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe) end # True if the current request URI was generated by the given +options+. diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb index 4dd7406798..95fbb112c0 100644 --- a/actionpack/test/active_record_unit.rb +++ b/actionpack/test/active_record_unit.rb @@ -45,19 +45,11 @@ class ActiveRecordTestConnector def setup_connection if Object.const_defined?(:ActiveRecord) defaults = { :database => ':memory:' } - begin - adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' - options = defaults.merge :adapter => adapter, :timeout => 500 - ActiveRecord::Base.establish_connection(options) - ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } - ActiveRecord::Base.connection - rescue Exception # errors from establishing a connection - $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.' - options = defaults.merge :adapter => 'sqlite' - ActiveRecord::Base.establish_connection(options) - ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options } - ActiveRecord::Base.connection - end + adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' + options = defaults.merge :adapter => adapter, :timeout => 500 + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } + ActiveRecord::Base.connection Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) else diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 38598a520c..3d667f0a2f 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -10,16 +10,6 @@ require 'controller/fake_controllers' require 'action_mailer' ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -class SynchronousQueue < Queue - def push(job) - job.run - end - alias << push - alias enq push -end - -ActionMailer::Base.queue = SynchronousQueue.new - class AssertSelectTest < ActionController::TestCase Assertion = ActiveSupport::TestCase::Assertion diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index 8a409d6ed2..faf923e929 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -104,17 +104,40 @@ class HttpTokenAuthenticationTest < ActionController::TestCase assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end - test "authentication request with valid credential" do - @request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty', :algorithm => 'test') - get :display + test "token_and_options returns correct token" do + token = "rcHu+HzSFw89Ypyhn/896A==" + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token" do + token = 'rcHu+=HzSFw89Ypyhn/896A==f34' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end - assert_response :success - assert assigns(:logged_in) - assert_equal 'Definitely Maybe', @response.body + test "token_and_options returns correct token" do + token = 'rcHu+\\\\"/896A' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token" do + token = '\"quote\" pretty' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) end private + def sample_request(token) + @sample_request ||= OpenStruct.new authorization: %{Token token="#{token}"} + end + def encode_credentials(token, options = {}) ActionController::HttpAuthentication::Token.encode_credentials(token, options) end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 5e9053da5b..8ecc1c7d73 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -144,7 +144,7 @@ class SendFileTest < ActionController::TestCase } @controller.headers = {} - assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) } + assert !@controller.send(:send_file_headers!, options) end def test_send_file_headers_guess_type_from_extension diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 263853fb6c..4e59e214c6 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -591,9 +591,18 @@ class RequestTest < ActiveSupport::TestCase request = stub_request request.expects(:parameters).at_least_once.returns({ :format => :unknown }) - assert request.formats.empty? + assert_instance_of Mime::NullType, request.format end + test "format is not nil with unknown format" do + request = stub_request + request.expects(:parameters).at_least_once.returns({ format: :hello }) + assert_equal request.format.nil?, true + assert_equal request.format.html?, false + assert_equal request.format.xml?, false + assert_equal request.format.json?, false + end + test "formats with xhr request" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) diff --git a/actionpack/test/fixtures/digestor/messages/show.html.erb b/actionpack/test/fixtures/digestor/messages/show.html.erb index 9f73345a9f..51b3b61e8e 100644 --- a/actionpack/test/fixtures/digestor/messages/show.html.erb +++ b/actionpack/test/fixtures/digestor/messages/show.html.erb @@ -6,4 +6,8 @@ <%= render @message.history.events %> -<%# render "something_missing" %>
\ No newline at end of file +<%# render "something_missing" %> + +<% + # Template Dependency: messages/form +%>
\ No newline at end of file diff --git a/actionpack/test/journey/gtg/builder_test.rb b/actionpack/test/journey/gtg/builder_test.rb new file mode 100644 index 0000000000..a633c3eea6 --- /dev/null +++ b/actionpack/test/journey/gtg/builder_test.rb @@ -0,0 +1,79 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module GTG + class TestBuilder < MiniTest::Unit::TestCase + def test_following_states_multi + table = tt ['a|a'] + assert_equal 1, table.move([0], 'a').length + end + + def test_following_states_multi_regexp + table = tt [':a|b'] + assert_equal 1, table.move([0], 'fooo').length + assert_equal 2, table.move([0], 'b').length + end + + def test_multi_path + table = tt ['/:a/d', '/b/c'] + + [ + [1, '/'], + [2, 'b'], + [2, '/'], + [1, 'c'], + ].inject([0]) { |state, (exp, sym)| + new = table.move(state, sym) + assert_equal exp, new.length + new + } + end + + def test_match_data_ambiguous + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + sim = NFA::Simulator.new table + + match = sim.match '/articles/new' + assert_equal 2, match.memos.length + end + + ## + # Identical Routes may have different restrictions. + def test_match_same_paths + table = tt %w{ + /articles/new(.:format) + /articles/new(.:format) + } + + sim = NFA::Simulator.new table + + match = sim.match '/articles/new' + assert_equal 2, match.memos.length + end + + private + def ast strings + parser = Journey::Parser.new + asts = strings.map { |string| + memo = Object.new + ast = parser.parse string + ast.each { |n| n.memo = memo } + ast + } + Nodes::Or.new asts + end + + def tt strings + Builder.new(ast(strings)).transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb new file mode 100644 index 0000000000..6d81b72c41 --- /dev/null +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -0,0 +1,115 @@ +require 'abstract_unit' +require 'json' + +module ActionDispatch + module Journey + module GTG + class TestGeneralizedTable < MiniTest::Unit::TestCase + def test_to_json + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + json = JSON.load table.to_json + assert json['regexp_states'] + assert json['string_states'] + assert json['accepting'] + end + + if system("dot -V 2>/dev/null") + def test_to_svg + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + svg = table.to_svg + assert svg + refute_match(/DOCTYPE/, svg) + end + end + + def test_simulate_gt + sim = simulator_for ['/foo', '/bar'] + assert_match sim, '/foo' + end + + def test_simulate_gt_regexp + sim = simulator_for [':foo'] + assert_match sim, 'foo' + end + + def test_simulate_gt_regexp_mix + sim = simulator_for ['/get', '/:method/foo'] + assert_match sim, '/get' + assert_match sim, '/get/foo' + end + + def test_simulate_optional + sim = simulator_for ['/foo(/bar)'] + assert_match sim, '/foo' + assert_match sim, '/foo/bar' + refute_match sim, '/foo/' + end + + def test_match_data + path_asts = asts %w{ /get /:method/foo } + paths = path_asts.dup + + builder = GTG::Builder.new Nodes::Or.new path_asts + tt = builder.transition_table + + sim = GTG::Simulator.new tt + + match = sim.match '/get' + assert_equal [paths.first], match.memos + + match = sim.match '/get/foo' + assert_equal [paths.last], match.memos + end + + def test_match_data_ambiguous + path_asts = asts %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + paths = path_asts.dup + ast = Nodes::Or.new path_asts + + builder = GTG::Builder.new ast + sim = GTG::Simulator.new builder.transition_table + + match = sim.match '/articles/new' + assert_equal [paths[1], paths[3]], match.memos + end + + private + def asts paths + parser = Journey::Parser.new + paths.map { |x| + ast = parser.parse x + ast.each { |n| n.memo = ast} + ast + } + end + + def tt paths + x = asts paths + builder = GTG::Builder.new Nodes::Or.new x + builder.transition_table + end + + def simulator_for paths + GTG::Simulator.new tt(paths) + end + end + end + end +end diff --git a/actionpack/test/journey/nfa/simulator_test.rb b/actionpack/test/journey/nfa/simulator_test.rb new file mode 100644 index 0000000000..9f89329b57 --- /dev/null +++ b/actionpack/test/journey/nfa/simulator_test.rb @@ -0,0 +1,98 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module NFA + class TestSimulator < MiniTest::Unit::TestCase + def test_simulate_simple + sim = simulator_for ['/foo'] + assert_match sim, '/foo' + end + + def test_simulate_simple_no_match + sim = simulator_for ['/foo'] + refute_match sim, 'foo' + end + + def test_simulate_simple_no_match_too_long + sim = simulator_for ['/foo'] + refute_match sim, '/foo/bar' + end + + def test_simulate_simple_no_match_wrong_string + sim = simulator_for ['/foo'] + refute_match sim, '/bar' + end + + def test_simulate_regex + sim = simulator_for ['/:foo/bar'] + assert_match sim, '/bar/bar' + assert_match sim, '/foo/bar' + end + + def test_simulate_or + sim = simulator_for ['/foo', '/bar'] + assert_match sim, '/bar' + assert_match sim, '/foo' + refute_match sim, '/baz' + end + + def test_simulate_optional + sim = simulator_for ['/foo(/bar)'] + assert_match sim, '/foo' + assert_match sim, '/foo/bar' + refute_match sim, '/foo/' + end + + def test_matchdata_has_memos + paths = %w{ /foo /bar } + parser = Journey::Parser.new + asts = paths.map { |x| + ast = parser.parse x + ast.each { |n| n.memo = ast} + ast + } + + expected = asts.first + + builder = Builder.new Nodes::Or.new asts + + sim = Simulator.new builder.transition_table + + md = sim.match '/foo' + assert_equal [expected], md.memos + end + + def test_matchdata_memos_on_merge + parser = Journey::Parser.new + routes = [ + '/articles(.:format)', + '/articles/new(.:format)', + '/articles/:id/edit(.:format)', + '/articles/:id(.:format)', + ].map { |path| + ast = parser.parse path + ast.each { |n| n.memo = ast } + ast + } + + asts = routes.dup + + ast = Nodes::Or.new routes + + nfa = Journey::NFA::Builder.new ast + sim = Simulator.new nfa.transition_table + md = sim.match '/articles' + assert_equal [asts.first], md.memos + end + + def simulator_for paths + parser = Journey::Parser.new + asts = paths.map { |x| parser.parse x } + builder = Builder.new Nodes::Or.new asts + Simulator.new builder.transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/nfa/transition_table_test.rb b/actionpack/test/journey/nfa/transition_table_test.rb new file mode 100644 index 0000000000..72cefe42bf --- /dev/null +++ b/actionpack/test/journey/nfa/transition_table_test.rb @@ -0,0 +1,72 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module NFA + class TestTransitionTable < MiniTest::Unit::TestCase + def setup + @parser = Journey::Parser.new + end + + def test_eclosure + table = tt '/' + assert_equal [0], table.eclosure(0) + + table = tt ':a|:b' + assert_equal 3, table.eclosure(0).length + + table = tt '(:a|:b)' + assert_equal 5, table.eclosure(0).length + assert_equal 5, table.eclosure([0]).length + end + + def test_following_states_one + table = tt '/' + + assert_equal [1], table.following_states(0, '/') + assert_equal [1], table.following_states([0], '/') + end + + def test_following_states_group + table = tt 'a|b' + states = table.eclosure 0 + + assert_equal 1, table.following_states(states, 'a').length + assert_equal 1, table.following_states(states, 'b').length + end + + def test_following_states_multi + table = tt 'a|a' + states = table.eclosure 0 + + assert_equal 2, table.following_states(states, 'a').length + assert_equal 0, table.following_states(states, 'b').length + end + + def test_following_states_regexp + table = tt 'a|:a' + states = table.eclosure 0 + + assert_equal 1, table.following_states(states, 'a').length + assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length + assert_equal 0, table.following_states(states, 'b').length + end + + def test_alphabet + table = tt 'a|:a' + assert_equal [/[^\.\/\?]+/, 'a'], table.alphabet + + table = tt 'a|a' + assert_equal ['a'], table.alphabet + end + + private + def tt string + ast = @parser.parse string + builder = Builder.new ast + builder.transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb new file mode 100644 index 0000000000..f53840274a --- /dev/null +++ b/actionpack/test/journey/nodes/symbol_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module Nodes + class TestSymbol < MiniTest::Unit::TestCase + def test_default_regexp? + sym = Symbol.new nil + assert sym.default_regexp? + + sym.regexp = nil + refute sym.default_regexp? + end + end + end + end +end diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb new file mode 100644 index 0000000000..0f2d0d44c0 --- /dev/null +++ b/actionpack/test/journey/path/pattern_test.rb @@ -0,0 +1,284 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module Path + class TestPattern < MiniTest::Unit::TestCase + x = /.+/ + { + '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z}, + '/:controller/foo' => %r{\A/(#{x})/foo\Z}, + '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)\Z}, + '/:controller' => %r{\A/(#{x})\Z}, + '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}, + '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml\Z}, + '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)\Z}, + '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?\Z}, + '/:controller/*foo' => %r{\A/(#{x})/(.+)\Z}, + '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z}, + }.each do |path, expected| + define_method(:"test_to_regexp_#{path}") do + strexp = Router::Strexp.new( + path, + { :controller => /.+/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_equal(expected, path.to_regexp) + end + end + + { + '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?}, + '/:controller/foo' => %r{\A/(#{x})/foo}, + '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)}, + '/:controller' => %r{\A/(#{x})}, + '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?}, + '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml}, + '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)}, + '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?}, + '/:controller/*foo' => %r{\A/(#{x})/(.+)}, + '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar}, + }.each do |path, expected| + define_method(:"test_to_non_anchored_regexp_#{path}") do + strexp = Router::Strexp.new( + path, + { :controller => /.+/ }, + ["/", ".", "?"], + false + ) + path = Pattern.new strexp + assert_equal(expected, path.to_regexp) + end + end + + { + '/:controller(/:action)' => %w{ controller action }, + '/:controller/foo' => %w{ controller }, + '/:controller/:action' => %w{ controller action }, + '/:controller' => %w{ controller }, + '/:controller(/:action(/:id))' => %w{ controller action id }, + '/:controller/:action.xml' => %w{ controller action }, + '/:controller.:format' => %w{ controller format }, + '/:controller(.:format)' => %w{ controller format }, + '/:controller/*foo' => %w{ controller foo }, + '/:controller/*foo/bar' => %w{ controller foo }, + }.each do |path, expected| + define_method(:"test_names_#{path}") do + strexp = Router::Strexp.new( + path, + { :controller => /.+/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_equal(expected, path.names) + end + end + + def test_to_regexp_with_extended_group + strexp = Router::Strexp.new( + '/page/:name', + { :name => / + #ROFL + (tender|love + #MAO + )/x }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_match(path, '/page/tender') + assert_match(path, '/page/love') + refute_match(path, '/page/loving') + end + + def test_optional_names + [ + ['/:foo(/:bar(/:baz))', %w{ bar baz }], + ['/:foo(/:bar)', %w{ bar }], + ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }], + ].each do |pattern, list| + path = Pattern.new pattern + assert_equal list.sort, path.optional_names.sort + end + end + + def test_to_regexp_match_non_optional + strexp = Router::Strexp.new( + '/:name', + { :name => /\d+/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_match(path, '/123') + refute_match(path, '/') + end + + def test_to_regexp_with_group + strexp = Router::Strexp.new( + '/page/:name', + { :name => /(tender|love)/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_match(path, '/page/tender') + assert_match(path, '/page/love') + refute_match(path, '/page/loving') + end + + def test_ast_sets_regular_expressions + requirements = { :name => /(tender|love)/, :value => /./ } + strexp = Router::Strexp.new( + '/page/:name/:value', + requirements, + ["/", ".", "?"] + ) + + assert_equal requirements, strexp.requirements + + path = Pattern.new strexp + nodes = path.ast.grep(Nodes::Symbol) + assert_equal 2, nodes.length + nodes.each do |node| + assert_equal requirements[node.to_sym], node.regexp + end + end + + def test_match_data_with_group + strexp = Router::Strexp.new( + '/page/:name', + { :name => /(tender|love)/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + match = path.match '/page/tender' + assert_equal 'tender', match[1] + assert_equal 2, match.length + end + + def test_match_data_with_multi_group + strexp = Router::Strexp.new( + '/page/:name/:id', + { :name => /t(((ender|love)))()/ }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + match = path.match '/page/tender/10' + assert_equal 'tender', match[1] + assert_equal '10', match[2] + assert_equal 3, match.length + assert_equal %w{ tender 10 }, match.captures + end + + def test_star_with_custom_re + z = /\d+/ + strexp = Router::Strexp.new( + '/page/*foo', + { :foo => z }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp) + end + + def test_insensitive_regexp_with_group + strexp = Router::Strexp.new( + '/page/:name/aaron', + { :name => /(tender|love)/i }, + ["/", ".", "?"] + ) + path = Pattern.new strexp + assert_match(path, '/page/TENDER/aaron') + assert_match(path, '/page/loVE/aaron') + refute_match(path, '/page/loVE/AAron') + end + + def test_to_regexp_with_strexp + strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"]) + path = Pattern.new strexp + x = %r{\A/([^/.?]+)\Z} + + assert_equal(x.source, path.source) + end + + def test_to_regexp_defaults + path = Pattern.new '/:controller(/:action(/:id))' + expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z} + assert_equal expected, path.to_regexp + end + + def test_failed_match + path = Pattern.new '/:controller(/:action(/:id(.:format)))' + uri = 'content' + + refute path =~ uri + end + + def test_match_controller + path = Pattern.new '/:controller(/:action(/:id(.:format)))' + uri = '/content' + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal 'content', match[1] + assert_nil match[2] + assert_nil match[3] + assert_nil match[4] + end + + def test_match_controller_action + path = Pattern.new '/:controller(/:action(/:id(.:format)))' + uri = '/content/list' + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal 'content', match[1] + assert_equal 'list', match[2] + assert_nil match[3] + assert_nil match[4] + end + + def test_match_controller_action_id + path = Pattern.new '/:controller(/:action(/:id(.:format)))' + uri = '/content/list/10' + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal 'content', match[1] + assert_equal 'list', match[2] + assert_equal '10', match[3] + assert_nil match[4] + end + + def test_match_literal + path = Path::Pattern.new "/books(/:action(.:format))" + + uri = '/books' + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_nil match[1] + assert_nil match[2] + end + + def test_match_literal_with_action + path = Path::Pattern.new "/books(/:action(.:format))" + + uri = '/books/list' + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_equal 'list', match[1] + assert_nil match[2] + end + + def test_match_literal_with_action_and_format + path = Path::Pattern.new "/books(/:action(.:format))" + + uri = '/books/list.rss' + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_equal 'list', match[1] + assert_equal 'rss', match[2] + end + end + end + end +end diff --git a/actionpack/test/journey/route/definition/parser_test.rb b/actionpack/test/journey/route/definition/parser_test.rb new file mode 100644 index 0000000000..580235c6a1 --- /dev/null +++ b/actionpack/test/journey/route/definition/parser_test.rb @@ -0,0 +1,110 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module Definition + class TestParser < MiniTest::Unit::TestCase + def setup + @parser = Parser.new + end + + def test_slash + assert_equal :SLASH, @parser.parse('/').type + assert_round_trip '/' + end + + def test_segment + assert_round_trip '/foo' + end + + def test_segments + assert_round_trip '/foo/bar' + end + + def test_segment_symbol + assert_round_trip '/foo/:id' + end + + def test_symbol + assert_round_trip '/:foo' + end + + def test_group + assert_round_trip '(/:foo)' + end + + def test_groups + assert_round_trip '(/:foo)(/:bar)' + end + + def test_nested_groups + assert_round_trip '(/:foo(/:bar))' + end + + def test_dot_symbol + assert_round_trip('.:format') + end + + def test_dot_literal + assert_round_trip('.xml') + end + + def test_segment_dot + assert_round_trip('/foo.:bar') + end + + def test_segment_group_dot + assert_round_trip('/foo(.:bar)') + end + + def test_segment_group + assert_round_trip('/foo(/:action)') + end + + def test_segment_groups + assert_round_trip('/foo(/:action)(/:bar)') + end + + def test_segment_nested_groups + assert_round_trip('/foo(/:action(/:bar))') + end + + def test_group_followed_by_path + assert_round_trip('/foo(/:action)/:bar') + end + + def test_star + assert_round_trip('*foo') + assert_round_trip('/*foo') + assert_round_trip('/bar/*foo') + assert_round_trip('/bar/(*foo)') + end + + def test_or + assert_round_trip('a|b') + assert_round_trip('a|b|c') + assert_round_trip('(a|b)|c') + assert_round_trip('a|(b|c)') + assert_round_trip('*a|(b|c)') + assert_round_trip('*a|:b|c') + end + + def test_arbitrary + assert_round_trip('/bar/*foo#') + end + + def test_literal_dot_paren + assert_round_trip "/sprockets.js(.:format)" + end + + def test_groups_with_dot + assert_round_trip "/(:locale)(.:format)" + end + + def assert_round_trip str + assert_equal str, @parser.parse(str).to_s + end + end + end + end +end diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb new file mode 100644 index 0000000000..110baf9977 --- /dev/null +++ b/actionpack/test/journey/route/definition/scanner_test.rb @@ -0,0 +1,56 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + module Definition + class TestScanner < MiniTest::Unit::TestCase + def setup + @scanner = Scanner.new + end + + # /page/:id(/:action)(.:format) + def test_tokens + [ + ['/', [[:SLASH, '/']]], + ['*omg', [[:STAR, '*omg']]], + ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]], + ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]], + ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]], + ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]], + ['/(:page)', [ + [:SLASH, '/'], + [:LPAREN, '('], + [:SYMBOL, ':page'], + [:RPAREN, ')'], + ]], + ['(/:action)', [ + [:LPAREN, '('], + [:SLASH, '/'], + [:SYMBOL, ':action'], + [:RPAREN, ')'], + ]], + ['(())', [[:LPAREN, '('], + [:LPAREN, '('], [:RPAREN, ')'], [:RPAREN, ')']]], + ['(.:format)', [ + [:LPAREN, '('], + [:DOT, '.'], + [:SYMBOL, ':format'], + [:RPAREN, ')'], + ]], + ].each do |str, expected| + @scanner.scan_setup str + assert_tokens expected, @scanner + end + end + + def assert_tokens tokens, scanner + toks = [] + while tok = scanner.next_token + toks << tok + end + assert_equal tokens, toks + end + end + end + end +end diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb new file mode 100644 index 0000000000..b205db5fbc --- /dev/null +++ b/actionpack/test/journey/route_test.rb @@ -0,0 +1,103 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + class TestRoute < MiniTest::Unit::TestCase + def test_initialize + app = Object.new + path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + defaults = Object.new + route = Route.new("name", app, path, {}, defaults) + + assert_equal app, route.app + assert_equal path, route.path + assert_equal defaults, route.defaults + end + + def test_route_adds_itself_as_memo + app = Object.new + path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + defaults = Object.new + route = Route.new("name", app, path, {}, defaults) + + route.ast.grep(Nodes::Terminal).each do |node| + assert_equal route, node.memo + end + end + + def test_ip_address + path = Path::Pattern.new '/messages/:id(.:format)' + route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, + { :controller => 'foo', :action => 'bar' }) + assert_equal '192.168.1.1', route.ip + end + + def test_default_ip + path = Path::Pattern.new '/messages/:id(.:format)' + route = Route.new("name", nil, path, {}, + { :controller => 'foo', :action => 'bar' }) + assert_equal(//, route.ip) + end + + def test_format_with_star + path = Path::Pattern.new '/:controller/*extra' + route = Route.new("name", nil, path, {}, + { :controller => 'foo', :action => 'bar' }) + assert_equal '/foo/himom', route.format({ + :controller => 'foo', + :extra => 'himom', + }) + end + + def test_connects_all_match + path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' + route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' }) + + assert_equal '/foo/bar/10', route.format({ + :controller => 'foo', + :action => 'bar', + :id => 10 + }) + end + + def test_extras_are_not_included_if_optional + path = Path::Pattern.new '/page/:id(/:action)' + route = Route.new("name", nil, path, { }, { :action => 'show' }) + + assert_equal '/page/10', route.format({ :id => 10 }) + end + + def test_extras_are_not_included_if_optional_with_parameter + path = Path::Pattern.new '(/sections/:section)/pages/:id' + route = Route.new("name", nil, path, { }, { :action => 'show' }) + + assert_equal '/pages/10', route.format({:id => 10}) + end + + def test_extras_are_not_included_if_optional_parameter_is_nil + path = Path::Pattern.new '(/sections/:section)/pages/:id' + route = Route.new("name", nil, path, { }, { :action => 'show' }) + + assert_equal '/pages/10', route.format({:id => 10, :section => nil}) + end + + def test_score + path = Path::Pattern.new "/page/:id(/:action)(.:format)" + specific = Route.new "name", nil, path, {}, {:controller=>"pages", :action=>"show"} + + path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)" + generic = Route.new "name", nil, path, {} + + knowledge = {:id=>20, :controller=>"pages", :action=>"show"} + + routes = [specific, generic] + + refute_equal specific.score(knowledge), generic.score(knowledge) + + found = routes.sort_by { |r| r.score(knowledge) }.last + + assert_equal specific, found + end + end + end +end diff --git a/actionpack/test/journey/router/strexp_test.rb b/actionpack/test/journey/router/strexp_test.rb new file mode 100644 index 0000000000..9e0337f144 --- /dev/null +++ b/actionpack/test/journey/router/strexp_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + class Router + class TestStrexp < MiniTest::Unit::TestCase + def test_many_names + exp = Strexp.new( + "/:controller(/:action(/:id(.:format)))", + {:controller=>/.+?/}, + ["/", ".", "?"], + true) + + assert_equal ["controller", "action", "id", "format"], exp.names + end + + def test_names + { + "/bar(.:format)" => %w{ format }, + ":format" => %w{ format }, + ":format-" => %w{ format }, + ":format0" => %w{ format0 }, + ":format1,:format2" => %w{ format1 format2 }, + }.each do |string, expected| + exp = Strexp.new(string, {}, ["/", ".", "?"]) + assert_equal expected, exp.names + end + end + end + end + end +end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb new file mode 100644 index 0000000000..97a6449c99 --- /dev/null +++ b/actionpack/test/journey/router/utils_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + class Router + class TestUtils < MiniTest::Unit::TestCase + def test_path_escape + assert_equal "a/b%20c+d", Utils.escape_path("a/b c+d") + end + + def test_fragment_escape + assert_equal "a/b%20c+d?e", Utils.escape_fragment("a/b c+d?e") + end + + def test_uri_unescape + assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") + end + end + end + end +end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb new file mode 100644 index 0000000000..1b64600ba8 --- /dev/null +++ b/actionpack/test/journey/router_test.rb @@ -0,0 +1,575 @@ +# encoding: UTF-8 +require 'abstract_unit' + +module ActionDispatch + module Journey + class TestRouter < MiniTest::Unit::TestCase + attr_reader :routes + + def setup + @routes = Routes.new + @router = Router.new(@routes, {}) + @formatter = Formatter.new(@routes) + end + + def test_request_class_reader + klass = Object.new + router = Router.new(routes, :request_class => klass) + assert_equal klass, router.request_class + end + + class FakeRequestFeeler < Struct.new(:env, :called) + def new env + self.env = env + self + end + + def hello + self.called = true + 'world' + end + + def path_info; env['PATH_INFO']; end + def request_method; env['REQUEST_METHOD']; end + def ip; env['REMOTE_ADDR']; end + end + + def test_dashes + router = Router.new(routes, {}) + + exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?'] + path = Path::Pattern.new exp + + routes.add_route nil, path, {}, {:id => nil}, {} + + env = rails_env 'PATH_INFO' => '/foo-bar-baz' + called = false + router.recognize(env) do |r, _, params| + called = true + end + assert called + end + + def test_unicode + router = Router.new(routes, {}) + + #match the escaped version of /ほげ + exp = Router::Strexp.new '/%E3%81%BB%E3%81%92', {}, ['/.?'] + path = Path::Pattern.new exp + + routes.add_route nil, path, {}, {:id => nil}, {} + + env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' + called = false + router.recognize(env) do |r, _, params| + called = true + end + assert called + end + + def test_request_class_and_requirements_success + klass = FakeRequestFeeler.new nil + router = Router.new(routes, {:request_class => klass }) + + requirements = { :hello => /world/ } + + exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + path = Path::Pattern.new exp + + routes.add_route nil, path, requirements, {:id => nil}, {} + + env = rails_env 'PATH_INFO' => '/foo/10' + router.recognize(env) do |r, _, params| + assert_equal({:id => '10'}, params) + end + + assert klass.called, 'hello should have been called' + assert_equal env.env, klass.env + end + + def test_request_class_and_requirements_fail + klass = FakeRequestFeeler.new nil + router = Router.new(routes, {:request_class => klass }) + + requirements = { :hello => /mom/ } + + exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + path = Path::Pattern.new exp + + router.routes.add_route nil, path, requirements, {:id => nil}, {} + + env = rails_env 'PATH_INFO' => '/foo/10' + router.recognize(env) do |r, _, params| + flunk 'route should not be found' + end + + assert klass.called, 'hello should have been called' + assert_equal env.env, klass.env + end + + class CustomPathRequest < Router::NullReq + def path_info + env['custom.path_info'] + end + end + + def test_request_class_overrides_path_info + router = Router.new(routes, {:request_class => CustomPathRequest }) + + exp = Router::Strexp.new '/bar', {}, ['/.?'] + path = Path::Pattern.new exp + + routes.add_route nil, path, {}, {}, {} + + env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar' + + recognized = false + router.recognize(env) do |r, _, params| + recognized = true + end + + assert recognized, "route should have been recognized" + end + + def test_regexp_first_precedence + add_routes @router, [ + Router::Strexp.new("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), + Router::Strexp.new("/whois/:id(.:format)", {}, ['/', '.', '?']) + ] + + env = rails_env 'PATH_INFO' => '/whois/example.com' + + list = [] + @router.recognize(env) do |r, _, params| + list << r + end + assert_equal 2, list.length + + r = list.first + + assert_equal '/whois/:domain', r.path.spec.to_s + end + + def test_required_parts_verified_are_anchored + add_routes @router, [ + Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) + ] + + assert_raises(Router::RoutingError) do + @formatter.generate(:path_info, nil, { :id => '10' }, { }) + end + end + + def test_required_parts_are_verified_when_building + add_routes @router, [ + Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) + ] + + path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + assert_equal '/foo/10', path + + assert_raises(Router::RoutingError) do + @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + end + end + + def test_only_required_parts_are_verified + add_routes @router, [ + Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) + ] + + path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + assert_equal '/foo/10', path + + path, _ = @formatter.generate(:path_info, nil, { }, { }) + assert_equal '/foo', path + + path, _ = @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + assert_equal '/foo/aa', path + end + + def test_knows_what_parts_are_missing_from_named_route + route_name = "gorby_thunderhorse" + pattern = Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) + path = Path::Pattern.new pattern + @router.routes.add_route nil, path, {}, {}, route_name + + error = assert_raises(Router::RoutingError) do + @formatter.generate(:path_info, route_name, { }, { }) + end + + assert_match(/required keys: \[:id\]/, error.message) + end + + def test_X_Cascade + add_routes @router, [ "/messages(.:format)" ] + resp = @router.call({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }) + assert_equal ['Not Found'], resp.last + assert_equal 'pass', resp[1]['X-Cascade'] + assert_equal 404, resp.first + end + + def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes + strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false) + path = Path::Pattern.new strexp + app = lambda { |env| [200, {}, ['success!']] } + @router.routes.add_route(app, path, {}, {}, {}) + + env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') + resp = @router.call(env) + assert_equal ['success!'], resp.last + assert_equal '', env['SCRIPT_NAME'] + end + + def test_defaults_merge_correctly + path = Path::Pattern.new '/foo(/:id)' + @router.routes.add_route nil, path, {}, {:id => nil}, {} + + env = rails_env 'PATH_INFO' => '/foo/10' + @router.recognize(env) do |r, _, params| + assert_equal({:id => '10'}, params) + end + + env = rails_env 'PATH_INFO' => '/foo' + @router.recognize(env) do |r, _, params| + assert_equal({:id => nil}, params) + end + end + + def test_recognize_with_unbound_regexp + add_routes @router, [ + Router::Strexp.new("/foo", { }, ['/', '.', '?'], false) + ] + + env = rails_env 'PATH_INFO' => '/foo/bar' + + @router.recognize(env) { |*_| } + + assert_equal '/foo', env.env['SCRIPT_NAME'] + assert_equal '/bar', env.env['PATH_INFO'] + end + + def test_bound_regexp_keeps_path_info + add_routes @router, [ + Router::Strexp.new("/foo", { }, ['/', '.', '?'], true) + ] + + env = rails_env 'PATH_INFO' => '/foo' + + before = env.env['SCRIPT_NAME'] + + @router.recognize(env) { |*_| } + + assert_equal before, env.env['SCRIPT_NAME'] + assert_equal '/foo', env.env['PATH_INFO'] + end + + def test_path_not_found + add_routes @router, [ + "/messages(.:format)", + "/messages/new(.:format)", + "/messages/:id/edit(.:format)", + "/messages/:id(.:format)" + ] + env = rails_env 'PATH_INFO' => '/messages/unknown/path' + yielded = false + + @router.recognize(env) do |*whatever| + yielded = true + end + refute yielded + end + + def test_required_part_in_recall + add_routes @router, [ "/messages/:a/:b" ] + + path, _ = @formatter.generate(:path_info, nil, { :a => 'a' }, { :b => 'b' }) + assert_equal "/messages/a/b", path + end + + def test_splat_in_recall + add_routes @router, [ "/*path" ] + + path, _ = @formatter.generate(:path_info, nil, { }, { :path => 'b' }) + assert_equal "/b", path + end + + def test_recall_should_be_used_when_scoring + add_routes @router, [ + "/messages/:action(/:id(.:format))", + "/messages/:id(.:format)" + ] + + path, _ = @formatter.generate(:path_info, nil, { :id => 10 }, { :action => 'index' }) + assert_equal "/messages/index/10", path + end + + def test_nil_path_parts_are_ignored + path = Path::Pattern.new "/:controller(/:action(.:format))" + @router.routes.add_route nil, path, {}, {}, {} + + params = { :controller => "tasks", :format => nil } + extras = { :action => 'lol' } + + path, _ = @formatter.generate(:path_info, nil, params, extras) + assert_equal '/tasks', path + end + + def test_generate_slash + params = [ [:controller, "tasks"], + [:action, "show"] ] + str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true) + path = Path::Pattern.new str + + @router.routes.add_route nil, path, {}, {}, {} + + path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) + assert_equal '/', path + end + + def test_generate_calls_param_proc + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route nil, path, {}, {}, {} + + parameterized = [] + params = [ [:controller, "tasks"], + [:action, "show"] ] + + @formatter.generate( + :path_info, + nil, + Hash[params], + {}, + lambda { |k,v| parameterized << [k,v]; v }) + + assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort + end + + def test_generate_id + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route nil, path, {}, {}, {} + + path, params = @formatter.generate( + :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) + assert_equal '/tasks/show', path + assert_equal({:id => 1}, params) + end + + def test_generate_escapes + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route nil, path, {}, {}, {} + + path, _ = @formatter.generate(:path_info, + nil, { :controller => "tasks", + :action => "a/b c+d", + }, {}) + assert_equal '/tasks/a/b%20c+d', path + end + + def test_generate_extra_params + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route nil, path, {}, {}, {} + + path, params = @formatter.generate(:path_info, + nil, { :id => 1, + :controller => "tasks", + :action => "show", + :relative_url_root => nil + }, {}) + assert_equal '/tasks/show', path + assert_equal({:id => 1, :relative_url_root => nil}, params) + end + + def test_generate_uses_recall_if_needed + path = Path::Pattern.new '/:controller(/:action(/:id))' + @router.routes.add_route nil, path, {}, {}, {} + + path, params = @formatter.generate(:path_info, + nil, + {:controller =>"tasks", :id => 10}, + {:action =>"index"}) + assert_equal '/tasks/index/10', path + assert_equal({}, params) + end + + def test_generate_with_name + path = Path::Pattern.new '/:controller(/:action)' + @router.routes.add_route nil, path, {}, {}, {} + + path, params = @formatter.generate(:path_info, + "tasks", + {:controller=>"tasks"}, + {:controller=>"tasks", :action=>"index"}) + assert_equal '/tasks', path + assert_equal({}, params) + end + + { + '/content' => { :controller => 'content' }, + '/content/list' => { :controller => 'content', :action => 'list' }, + '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" }, + }.each do |request_path, expected| + define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do + path = Path::Pattern.new "/:controller(/:action(/:id))" + app = Object.new + route = @router.routes.add_route(app, path, {}, {}, {}) + + env = rails_env 'PATH_INFO' => request_path + called = false + + @router.recognize(env) do |r, _, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + + assert called + end + end + + { + :segment => ['/a%2Fb%20c+d/splat', { :segment => 'a/b c+d', :splat => 'splat' }], + :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }] + }.each do |name, (request_path, expected)| + define_method("test_recognize_#{name}") do + path = Path::Pattern.new '/:segment/*splat' + app = Object.new + route = @router.routes.add_route(app, path, {}, {}, {}) + + env = rails_env 'PATH_INFO' => request_path + called = false + + @router.recognize(env) do |r, _, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + + assert called + end + end + + def test_namespaced_controller + strexp = Router::Strexp.new( + "/:controller(/:action(/:id))", + { :controller => /.+?/ }, + ["/", ".", "?"] + ) + path = Path::Pattern.new strexp + app = Object.new + route = @router.routes.add_route(app, path, {}, {}, {}) + + env = rails_env 'PATH_INFO' => '/admin/users/show/10' + called = false + expected = { + :controller => 'admin/users', + :action => 'show', + :id => '10' + } + + @router.recognize(env) do |r, _, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + assert called + end + + def test_recognize_literal + path = Path::Pattern.new "/books(/:action(.:format))" + app = Object.new + route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) + + env = rails_env 'PATH_INFO' => '/books/list.rss' + expected = { :controller => 'books', :action => 'list', :format => 'rss' } + called = false + @router.recognize(env) do |r, _, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + + assert called + end + + def test_recognize_head_request_as_get_route + path = Path::Pattern.new "/books(/:action(.:format))" + app = Object.new + conditions = { + :request_method => 'GET' + } + @router.routes.add_route(app, path, conditions, {}) + + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => "HEAD" + + called = false + @router.recognize(env) do |r, _, params| + called = true + end + + assert called + end + + def test_recognize_cares_about_verbs + path = Path::Pattern.new "/books(/:action(.:format))" + app = Object.new + conditions = { + :request_method => 'GET' + } + @router.routes.add_route(app, path, conditions, {}) + + conditions = conditions.dup + conditions[:request_method] = 'POST' + + post = @router.routes.add_route(app, path, conditions, {}) + + env = rails_env 'PATH_INFO' => '/books/list.rss', + "REQUEST_METHOD" => "POST" + + called = false + @router.recognize(env) do |r, _, params| + assert_equal post, r + called = true + end + + assert called + end + + private + + def add_routes router, paths + paths.each do |path| + path = Path::Pattern.new path + router.routes.add_route nil, path, {}, {}, {} + end + end + + RailsEnv = Struct.new(:env) + + def rails_env env + RailsEnv.new rack_env env + end + + def rack_env env + { + "rack.version" => [1, 1], + "rack.input" => StringIO.new, + "rack.errors" => StringIO.new, + "rack.multithread" => true, + "rack.multiprocess" => true, + "rack.run_once" => false, + "REQUEST_METHOD" => "GET", + "SERVER_NAME" => "example.org", + "SERVER_PORT" => "80", + "QUERY_STRING" => "", + "PATH_INFO" => "/content", + "rack.url_scheme" => "http", + "HTTPS" => "off", + "SCRIPT_NAME" => "", + "CONTENT_LENGTH" => "0" + }.merge env + end + end + end +end diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb new file mode 100644 index 0000000000..3b17bd53b7 --- /dev/null +++ b/actionpack/test/journey/routes_test.rb @@ -0,0 +1,53 @@ +require 'abstract_unit' + +module ActionDispatch + module Journey + class TestRoutes < MiniTest::Unit::TestCase + def test_clear + routes = Routes.new + exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] + path = Path::Pattern.new exp + requirements = { :hello => /world/ } + + routes.add_route nil, path, requirements, {:id => nil}, {} + assert_equal 1, routes.length + + routes.clear + assert_equal 0, routes.length + end + + def test_ast + routes = Routes.new + path = Path::Pattern.new '/hello' + + routes.add_route nil, path, {}, {}, {} + ast = routes.ast + routes.add_route nil, path, {}, {}, {} + refute_equal ast, routes.ast + end + + def test_simulator_changes + routes = Routes.new + path = Path::Pattern.new '/hello' + + routes.add_route nil, path, {}, {}, {} + sim = routes.simulator + routes.add_route nil, path, {}, {}, {} + refute_equal sim, routes.simulator + end + + def test_first_name_wins + #def add_route app, path, conditions, defaults, name = nil + routes = Routes.new + + one = Path::Pattern.new '/hello' + two = Path::Pattern.new '/aaron' + + routes.add_route nil, one, {}, {}, 'aaron' + routes.add_route nil, two, {}, {}, 'aaron' + + assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s + end + end + end +end diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index eb1a54a81f..82c9d383ac 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -358,6 +358,17 @@ class AssetTagHelperTest < ActionView::TestCase assert javascript_include_tag("prototype").html_safe? end + def test_javascript_include_tag_relative_protocol + @controller.config.asset_host = "assets.example.com" + assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype', protocol: :relative) + end + + def test_javascript_include_tag_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :relative + assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype') + end + def test_stylesheet_path StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -398,7 +409,18 @@ class AssetTagHelperTest < ActionView::TestCase end def test_stylesheet_link_tag_should_not_output_the_same_asset_twice - assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam') + assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam') + end + + def test_stylesheet_link_tag_with_relative_protocol + @controller.config.asset_host = "assets.example.com" + assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', protocol: :relative) + end + + def test_stylesheet_link_tag_with_default_protocol + @controller.config.asset_host = "assets.example.com" + @controller.config.default_asset_host_protocol = :relative + assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington') end def test_image_path diff --git a/actionpack/test/template/digestor_test.rb b/actionpack/test/template/digestor_test.rb index f493c8201d..02b1fd87a8 100644 --- a/actionpack/test/template/digestor_test.rb +++ b/actionpack/test/template/digestor_test.rb @@ -46,6 +46,12 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_explicit_dependency_in_multiline_erb_tag + assert_digest_difference("messages/show") do + change_template("messages/_form") + end + end + def test_second_level_dependency assert_digest_difference("messages/show") do change_template("comments/_comments") diff --git a/actionpack/test/template/form_collections_helper_test.rb b/actionpack/test/template/form_collections_helper_test.rb index c73e80ed88..2131f81396 100644 --- a/actionpack/test/template/form_collections_helper_test.rb +++ b/actionpack/test/template/form_collections_helper_test.rb @@ -149,6 +149,12 @@ class FormCollectionsHelperTest < ActionView::TestCase assert_select 'label[for=post_category_id_2]', 'Category 2' end + test 'collection radio accepts checked item which has a value of false' do + with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => false + assert_no_select 'input[type=radio][value=true][checked=checked]' + assert_select 'input[type=radio][value=false][checked=checked]' + end + # COLLECTION CHECK BOXES test 'collection check boxes accepts a collection and generate a serie of checkboxes for value method' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 1bb625213d..ba65349b6a 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -517,16 +517,6 @@ class UrlHelperTest < ActiveSupport::TestCase mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin") end - def test_mail_to_with_javascript - snippet = mail_to("me@domain.com", "My email", encode: "javascript") - assert_dom_equal "<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet - end - - def test_mail_to_with_javascript_unicode - snippet = mail_to("unicode@example.com", "únicode", encode: "javascript") - assert_dom_equal "<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%5c%22%3e%c3%ba%6e%69%63%6f%64%65%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet - end - def test_mail_with_options assert_dom_equal( %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&bcc=bccaddress%40example.com&body=This%20is%20the%20body%20of%20the%20message.&subject=This%20is%20an%20example%20email">My email</a>}, @@ -539,54 +529,8 @@ class UrlHelperTest < ActiveSupport::TestCase mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe) end - def test_mail_to_with_hex - assert_dom_equal( - %{<a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>}, - mail_to("me@domain.com", "My email", encode: "hex") - ) - - assert_dom_equal( - %{<a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">me@domain.com</a>}, - mail_to("me@domain.com", nil, encode: "hex") - ) - end - - def test_mail_to_with_replace_options - assert_dom_equal( - %{<a href="mailto:wolfgang@stufenlos.net">wolfgang(at)stufenlos(dot)net</a>}, - mail_to("wolfgang@stufenlos.net", nil, replace_at: "(at)", replace_dot: "(dot)") - ) - - assert_dom_equal( - %{<a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">me(at)domain.com</a>}, - mail_to("me@domain.com", nil, encode: "hex", replace_at: "(at)") - ) - - assert_dom_equal( - %{<a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>}, - mail_to("me@domain.com", "My email", encode: "hex", replace_at: "(at)") - ) - - assert_dom_equal( - %{<a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">me(at)domain(dot)com</a>}, - mail_to("me@domain.com", nil, encode: "hex", replace_at: "(at)", replace_dot: "(dot)") - ) - - assert_dom_equal( - %{<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>}, - mail_to("me@domain.com", "My email", encode: "javascript", replace_at: "(at)", replace_dot: "(dot)") - ) - - assert_dom_equal( - %{<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%5c%2f%61%3e%27%29%3b'))</script>}, - mail_to("me@domain.com", nil, encode: "javascript", replace_at: "(at)", replace_dot: "(dot)") - ) - end - def test_mail_to_returns_html_safe_string assert mail_to("david@loudthinking.com").html_safe? - assert mail_to("me@domain.com", "My email", encode: "javascript").html_safe? - assert mail_to("me@domain.com", "My email", encode: "hex").html_safe? end def protect_against_forgery? |