Skip to content

Commit

Permalink
Change filter on /rails/info/routes to use an actual path regexp from…
Browse files Browse the repository at this point in the history
… rails

Change filter on /rails/info/routes to use an actual path regexp from rails
and not approximate javascript version. Oniguruma supports much more
extensive list of features than javascript regexp engine.

Fixes #18402.
  • Loading branch information
brainopia committed Feb 23, 2015
1 parent e71f5da commit 321db4a
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 99 deletions.
8 changes: 8 additions & 0 deletions actionpack/CHANGELOG.md
Expand Up @@ -4,6 +4,14 @@

*David Ilizarov*

* Change filter on /rails/info/routes to use an actual path regexp from rails
and not approximate javascript version. Oniguruma supports much more
extensive list of features than javascript regexp engine.

Fixes #18402.

*Ravil Bayramgalin*

* Non-string authenticity tokens do not raise NoMethodError when decoding
the masked token.

Expand Down
Expand Up @@ -4,13 +4,13 @@
<%= route[:name] %><span class='helper'>_path</span>
<% end %>
</td>
<td data-route-verb='<%= route[:verb] %>'>
<td>
<%= route[:verb] %>
</td>
<td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'>
<td data-route-path='<%= route[:path] %>'>
<%= route[:path] %>
</td>
<td data-route-reqs='<%= route[:reqs] %>'>
<%= route[:reqs] %>
<td>
<%=simple_format route[:reqs] %>
</td>
</tr>
Expand Up @@ -81,112 +81,108 @@
</table>

<script type='text/javascript'>
// Iterates each element through a function
function each(elems, func) {
if (!elems instanceof Array) { elems = [elems]; }
for (var i = 0, len = elems.length; i < len; i++) {
func(elems[i]);
}
}

// Sets innerHTML for an element
function setContent(elem, text) {
elem.innerHTML = text;
}
// support forEarch iterator on NodeList
NodeList.prototype.forEach = Array.prototype.forEach;

// Enables path search functionality
function setupMatchPaths() {
// Check if the user input (sanitized as a path) matches the regexp data attribute
function checkExactMatch(section, elem, value) {
var string = sanitizePath(value),
regexp = elem.getAttribute("data-regexp");

showMatch(string, regexp, section, elem);
// Check if there are any matched results in a section
function checkNoMatch(section, noMatchText) {
if (section.children.length <= 1) {
section.innerHTML += noMatchText;
}
}

// Check if the route path data attribute contains the user input
function checkFuzzyMatch(section, elem, value) {
var string = elem.getAttribute("data-route-path"),
regexp = value;

showMatch(string, regexp, section, elem);
// get JSON from url and invoke callback with result
function getJSON(url, success) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (this.status == 200)
success(JSON.parse(this.response));
};
xhr.send();
}

// Display the parent <tr> element in the appropriate section when there's a match
function showMatch(string, regexp, section, elem) {
if(string.match(RegExp(regexp))) {
section.appendChild(elem.parentNode.cloneNode(true));
function delayedKeyup(input, callback) {
var timeout;
input.onkeyup = function(){
if (timeout) clearTimeout(timeout);
timeout = setTimeout(callback, 300);
}
}

// Check if there are any matched results in a section
function checkNoMatch(section, defaultText, noMatchText) {
if (section.innerHTML === defaultText) {
setContent(section, defaultText + noMatchText);
}
}

// Ensure path always starts with a slash "/" and remove params or fragments
// remove params or fragments
function sanitizePath(path) {
var path = path.charAt(0) == '/' ? path : "/" + path;
return path.replace(/\#.*|\?.*/, '');
return path.replace(/[#?].*/, '');
}

var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
searchElem = document.querySelector('#search'),
exactMatches = document.querySelector('#exact_matches'),
fuzzyMatches = document.querySelector('#fuzzy_matches');
var pathElements = document.querySelectorAll('#route_table [data-route-path]'),
searchElem = document.querySelector('#search'),
exactSection = document.querySelector('#exact_matches'),
fuzzySection = document.querySelector('#fuzzy_matches');

// Remove matches when no search value is present
searchElem.onblur = function(e) {
if (searchElem.value === "") {
setContent(exactMatches, "");
setContent(fuzzyMatches, "");
exactSection.innerHTML = "";
fuzzySection.innerHTML = "";
}
}

// On key press perform a search for matching paths
searchElem.onkeyup = function(e){
var userInput = searchElem.value,
defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>',
defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>',
delayedKeyup(searchElem, function() {
var path = sanitizePath(searchElem.value),
defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>',
defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>',
noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';

// Clear out results section
setContent(exactMatches, defaultExactMatch);
setContent(fuzzyMatches, defaultFuzzyMatch);
if (!path)
return searchElem.onblur();

// Display exact matches and fuzzy matches
each(regexpElems, function(elem) {
checkExactMatch(exactMatches, elem, userInput);
checkFuzzyMatch(fuzzyMatches, elem, userInput);
})
getJSON('/rails/info/routes?path=' + path, function(matches){
// Clear out results section
exactSection.innerHTML = defaultExactMatch;
fuzzySection.innerHTML = defaultFuzzyMatch;

// Display 'No Matches' message when no matches are found
checkNoMatch(exactMatches, defaultExactMatch, noExactMatch);
checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch);
}
// Display exact matches and fuzzy matches
pathElements.forEach(function(elem) {
var elemPath = elem.getAttribute('data-route-path');

if (matches['exact'].indexOf(elemPath) != -1)
exactSection.appendChild(elem.parentNode.cloneNode(true));

if (matches['fuzzy'].indexOf(elemPath) != -1)
fuzzySection.appendChild(elem.parentNode.cloneNode(true));
})

// Display 'No Matches' message when no matches are found
checkNoMatch(exactSection, noExactMatch);
checkNoMatch(fuzzySection, noFuzzyMatch);
})
})
}

// Enables functionality to toggle between `_path` and `_url` helper suffixes
function setupRouteToggleHelperLinks() {

// Sets content for each element
function setValOn(elems, val) {
each(elems, function(elem) {
setContent(elem, val);
elems.forEach(function(elem) {
elem.innerHTML = val;
});
}

// Sets onClick event for each element
function onClick(elems, func) {
each(elems, function(elem) {
elems.forEach(function(elem) {
elem.onclick = func;
});
}

var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');

onClick(toggleLinks, function(){
var helperTxt = this.getAttribute("data-route-helper"),
helperElems = document.querySelectorAll('[data-route-name] span.helper');
Expand Down
26 changes: 4 additions & 22 deletions actionpack/lib/action_dispatch/routing/inspector.rb
Expand Up @@ -28,23 +28,6 @@ def name
super.to_s
end

def regexp
__getobj__.path.to_regexp
end

def json_regexp
str = regexp.inspect.
sub('\\A' , '^').
sub('\\Z' , '$').
sub('\\z' , '$').
sub(/^\// , '').
sub(/\/[a-z]*$/ , '').
gsub(/\(\?#.+\)/ , '').
gsub(/\(\?-\w+:/ , '(').
gsub(/\s/ , '')
Regexp.new(str).source
end

def reqs
@reqs ||= begin
reqs = endpoint
Expand Down Expand Up @@ -117,11 +100,10 @@ def collect_routes(routes)
end.reject(&:internal?).collect do |route|
collect_engine_routes(route)

{ name: route.name,
verb: route.verb,
path: route.path,
reqs: route.reqs,
regexp: route.json_regexp }
{ name: route.name,
verb: route.verb,
path: route.path,
reqs: route.reqs }
end
end

Expand Down
8 changes: 0 additions & 8 deletions actionpack/test/dispatch/routing/inspector_test.rb
Expand Up @@ -26,14 +26,6 @@ def draw(options = {}, &block)
inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n")
end

def test_json_regexp_converter
@set.draw do
get '/cart', :to => 'cart#show'
end
route = ActionDispatch::Routing::RouteWrapper.new(@set.routes.first)
assert_equal "^\\/cart(?:\\.([^\\/.?]+))?$", route.json_regexp
end

def test_displaying_routes_for_engines
engine = Class.new(Rails::Engine) do
def self.inspect
Expand Down
25 changes: 23 additions & 2 deletions railties/lib/rails/info_controller.rb
Expand Up @@ -17,7 +17,28 @@ def properties
end

def routes
@routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
@page_title = 'Routes'
if path = params[:path]
path = URI.escape path
normalized_path = with_leading_slash path
render json: {
exact: match_route {|it| it.match normalized_path },
fuzzy: match_route {|it| it.spec.to_s.match path }
}
else
@routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
@page_title = 'Routes'
end
end

private

def match_route
_routes.routes.select {|route|
yield route.path
}.map {|route| route.path.spec.to_s }
end

def with_leading_slash(path)
('/' + path).squeeze('/')
end
end
25 changes: 25 additions & 0 deletions railties/test/rails_info_controller_test.rb
Expand Up @@ -53,4 +53,29 @@ def setup
assert_response :success
end

test "info controller returns exact matches" do
exact_count = -> { JSON(response.body)['exact'].size }

get :routes, path: 'rails/info/route'
assert exact_count.call == 0, 'should not match incomplete routes'

get :routes, path: 'rails/info/routes'
assert exact_count.call == 1, 'should match complete routes'

get :routes, path: 'rails/info/routes.html'
assert exact_count.call == 1, 'should match complete routes with optional parts'
end

test "info controller returns fuzzy matches" do
fuzzy_count = -> { JSON(response.body)['fuzzy'].size }

get :routes, path: 'rails/info'
assert fuzzy_count.call == 2, 'should match incomplete routes'

get :routes, path: 'rails/info/routes'
assert fuzzy_count.call == 1, 'should match complete routes'

get :routes, path: 'rails/info/routes.html'
assert fuzzy_count.call == 0, 'should match optional parts of route literally'
end
end

0 comments on commit 321db4a

Please sign in to comment.