Is there a way to know if a page request came from the same application? - ruby-on-rails

In Rails3, is there a way to check if the page I'm rendering now was requested from the same application, without the use of the hardcoded domain name?
I currently have:
def back_link(car_id = '')
# Check if search exists
uri_obj = URI.parse(controller.request.env["HTTP_REFERER"]) if controller.request.env["HTTP_REFERER"].present?
if uri_obj.present? && ["my_domain.com", "localhost"].include?(uri_obj.host) && uri_obj.query.present? && uri_obj.query.include?('search')
link_to '◀ '.html_safe + t('back_to_search'), url_for(:back) + (car_id.present? ? '#' + car_id.to_s : ''), :class => 'button grey back'
end
end
But this doesn't check for the "www." in front of the domain and all other possible situations.
It would also be nice if I could find out the specific controller and action that were used in the previous page (the referrer).

I think you're looking at this the wrong way.
If you look around the web, find a site with a search feature, and follow the link you'll see a param showing what was searched for.
That's a good way to do it.
Doing it by HTTP_REFERER seems a bit fragile, and won't work, for example, from a bookmark, or posted link.
eg.
/cars/12?from_search=sports+cars
then you can just look up the params[:from_search]
If you really need to do it by HTTP_REFERER then you probably dont have to worry about subdomains. Just;
def http_referer_uri
request.env["HTTP_REFERER"] && URI.parse(request.env["HTTP_REFERER"])
end
def refered_from_our_site?
if uri = http_referer_uri
uri.host == request.host
end
end
def refered_from_a_search?
if refered_from_our_site?
http_referer_uri.try(:query)['search']
end
end

Try something like this:
ref = URI.parse(controller.request.env["HTTP_REFERER"])
if ref.host == ENV["HOSTNAME"]
# do something
To try and get the controller/action from the referring page:
ActionController::Routing::Routes.recognize_path(url.path)
#=> {:controller => "foo", :action => "bar"}

Create an internal_request? method utilizing request.referrer.
Compare the host and port of the request.referrer with your Application's host and port.
require 'uri' # Might be necesseary.
def internal_request?
return false if request.referrer.blank?
referrer = URI.parse( request.referrer )
application_host = Rails.application.config.action_mailer.default_url_options[ :host ]
application_port = Rails.application.config.action_mailer.default_url_options[ :port ]
return true if referrer.host == application_host && referrer.port == application_port
false
end
And then call it like this where you need it, most likely in application_controller.rb:
if internal_request?
do_something
end
Some caveats:
This might need to be modified if you're using subdomains. Easy, though.
This will require you to be setting your host and port for ActionMailer in your configuration, which is common.
You might want to make it the reverse, like external_request? since you're likely handling those situations uniquely. This would allow you to do something like this:
do_something_unique if external_request?

Related

Modify routes in existing Rails Application

I try to modify existing routes in rails application to make it more readable for human and for google.
Existing route example: http://localhost:3000/search_adv?locale=de&q[home_type_eq]=1
To: http://localhost:3000/bowling?locale=de
How to create these routes without 'big' code modifying?
Where home_type=1 parameter corresponds to bowling.
home_type=2 to restaurant and so on.
Altogether six such parameters.
In routes.rb: get 'search_adv' => 'pages#search_adv'
In controller:
def search_adv
if params[:search_adv].present? && params[:search_adv].strip != ""
session[:loc_search] = params[:search_adv]
end
if session[:loc_search] && session[:loc_search] != ""
#rooms_address = Room.where(active: true).paginate(page: params[:page], per_page: 10).near(session[:loc_search], 1000, order: 'distance')
else
#rooms_address = Room.where(active: true).paginate(page: params[:page], per_page: 10)
end
Your question shows how you are thinking about rails which is not the correct way and I would also suggest what Tom Lord suggested but there is a way to do what you want to do, although it would require major refactoring of your code base and not worth it:
You can add a M, V and C each for the home_types (restaurant, bowling etc.) and then redirect from search_adv method to that controller route based on params.
For example:
You hit http://localhost:3000/search_adv?locale=de&q[home_type_eq]=1 and then in search_adv you can
if params[the exact params containing your value] == 1
redirect_to bowlings_path(locale: 'de')
end
The user will not feel it as the redirection will happen on the back-end but the route later will look like:
http://localhost:3000/bowlings?locale=de

Capybara + remote form request

I have a form that I'm testing using Capybara. This form's URL goes to my Braintree sandbox, although I suspect the problem would happen for any remote URL. When Capybara clicks the submit button for the form, the request is routed to the dummy application rather than the remote service.
Here's an example app that reproduces this issue: https://github.com/radar/capybara_remote. Run bundle exec ruby test/form_test.rb and the test will pass, which is not what I'd typically expect.
Why does this happen and is this behaviour that I can rely on always happening?
Mario Visic points out this description in the Capybara documentation:
Furthermore, you cannot use the RackTest driver to test a remote application, or to access remote URLs (e.g., redirects to external sites, external APIs, or OAuth services) that your application might interact with.
But I wanted to know why, so I source dived. Here's my findings:
lib/capybara/node/actions.rb
def click_button(locator)
find(:button, locator).click
end
I don't care about the find here because that's working. It's the click that's more interesting. That method is defined like this:
lib/capybara/node/element.rb
def click
wait_until { base.click }
end
I don't know what base is, but I see the method is defined twice more in lib/capybara/rack_test/node.rb and lib/capybara/selenium/node.rb. The tests are using Rack::Test and not Selenium, so it's probably the former:
lib/capybara/rack_test/node.rb
def click
if tag_name == 'a'
method = self["data-method"] if driver.options[:respect_data_method]
method ||= :get
driver.follow(method, self[:href].to_s)
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
((tag_name == 'button') and type.nil? or type == "submit")
Capybara::RackTest::Form.new(driver, form).submit(self)
end
end
The tag_name is probably not a link -- because it's a button we're clicking -- so it falls to the elsif. It's definitely an input tag with type == "submit", so then let's see what Capybara::RackTest::Form does:
lib/capybara/rack_test/form.rb
def submit(button)
driver.submit(method, native['action'].to_s, params(button))
end
Ok then. driver is probably the Rack::Test driver for Capybara. What's that doing?
lib/capybara/rack_test/driver.rb
def submit(method, path, attributes)
browser.submit(method, path, attributes)
end
What is this mysterious browser? It's defined in the same file thankfully:
def browser
#browser ||= Capybara::RackTest::Browser.new(self)
end
Let's look at what this class's submit method does.
lib/capybara/rack_test/browser.rb
def submit(method, path, attributes)
path = request_path if not path or path.empty?
process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
end
process_and_follow_redirects does what it says on the box:
def process_and_follow_redirects(method, path, attributes = {}, env = {})
process(method, path, attributes, env)
5.times do
process(:get, last_response["Location"], {}, env) if last_response.redirect?
end
raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if last_response.redirect?
end
So does process:
def process(method, path, attributes = {}, env = {})
new_uri = URI.parse(path)
method.downcase! unless method.is_a? Symbol
if new_uri.host
#current_host = "#{new_uri.scheme}://#{new_uri.host}"
#current_host << ":#{new_uri.port}" if new_uri.port != new_uri.default_port
end
if new_uri.relative?
if path.start_with?('?')
path = request_path + path
elsif not path.start_with?('/')
path = request_path.sub(%r(/[^/]*$), '/') + path
end
path = current_host + path
end
reset_cache!
send(method, path, attributes, env.merge(options[:headers] || {}))
end
Time to break out the debugger and see what method is here. Sticking a binding.pry before the final line in that method, and a require 'pry' in the test. It turns out method is :post and, for interest's sake, new_uri is a URI object with our remote form's URL.
Where's this post method coming from? method(:post).source_location tells me:
["/Users/ryan/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/forwardable.rb", 199]
That doesn't seem right... Does Capybara have a def post somewhere?
capybara (master)★ack "def post"
lib/capybara/rack_test/driver.rb
76: def post(*args, &block); browser.post(*args, &block); end
Cool. We know that browser is aCapybara::RackTest::Browser` object. The class beginning gives the next hint:
class Capybara::RackTest::Browser
include ::Rack::Test::Methods
I know that Rack::Test::Methods comes with a post method. Time to dive into that gem.
lib/rack/test.rb
def post(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(:method => "POST", :params => params))
process_request(uri, env, &block)
end
Ignoring env_for for the time being, what does process_request do?
lib/rack/test.rb
def process_request(uri, env)
uri = URI.parse(uri)
uri.host ||= #default_host
#rack_mock_session.request(uri, env)
if retry_with_digest_auth?(env)
auth_env = env.merge({
"HTTP_AUTHORIZATION" => digest_auth_header,
"rack-test.digest_auth_retry" => true
})
auth_env.delete('rack.request')
process_request(uri.path, auth_env)
else
yield last_response if block_given?
last_response
end
end
Hey, #rack_mock_session looks interesting. Where's that defined?
rack-test (master)★ack "#rack_mock_session ="
lib/rack/test.rb
40: #rack_mock_session = mock_session
42: #rack_mock_session = MockSession.new(mock_session)
In two places, very close to each other. What's on and around these lines?
def initialize(mock_session)
#headers = {}
if mock_session.is_a?(MockSession)
#rack_mock_session = mock_session
else
#rack_mock_session = MockSession.new(mock_session)
end
#default_host = #rack_mock_session.default_host
end
Ok then, so it ensures it is a MockSession object. What's MockSession and how is its request method defined?
def request(uri, env)
env["HTTP_COOKIE"] ||= cookie_jar.for(uri)
#last_request = Rack::Request.new(env)
status, headers, body = #app.call(#last_request.env)
headers["Referer"] = env["HTTP_REFERER"] || ""
#last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush)
body.close if body.respond_to?(:close)
cookie_jar.merge(last_response.headers["Set-Cookie"], uri)
#after_request.each { |hook| hook.call }
if #last_response.respond_to?(:finish)
#last_response.finish
else
#last_response
end
end
I'm going to go right ahead here and assume #app is the Rack application stack. By calling the call method, the request is routed directly to this stack, rather going out to the world.
I conclude that this behaviour looks like its intentional and that I can indeed rely on it being that way.

How do I set a custom ETag value in a Rails controller response?

For one controller (only), I'd like to use an ETag value generated outside of rails caching logic, and manage 304-vs-200 responses myself. It seems that nothing I do to set the ETag header works:
response.etag = myEtag
headers['ETag'] = myEtag
render :text => myText, :etag => myEtag
Rails always uses its own version.
I know I could disable caching app-wide, but I don't want that - just want to override it in the responses for one ActionController subclass.
fresh_when, etc. didn't quite suit my needs - in my case the solution was to refuse caching via
def caching_allowed?
false
end
then set just the headers['ETag'] member on my response - setting any of the .etag options seems to cause Rails to MD5 All The Things.
Also if you would like to overwrite it directly, you can set the etag value like below:
headers['ETag'] = 'xxxxxx'
Code reference from Rack
(rack-1.6.11/lib/rack/etag.rb)
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
original_body = body
digest, new_body = digest_body(body)
body = Rack::BodyProxy.new(new_body) do
original_body.close if original_body.respond_to?(:close)
end
headers[ETAG_STRING] = %(W/"#{digest}") if digest
end
private
def skip_caching?(headers)
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
end

Rails 3 - default_url_options based on url being generated

In rails 3, is it possible to gain access to the controller/action of the URL being generated inside of default_url_options()? In rails 2 you were passed a Hash of the options that were about to be passed to url_for() that you could of course alter.
E.g. Rails 2 code:
==== config/routes.rb
map.foo '/foo/:shard/:id', :controller => 'foo', :action => 'show'
==== app/controllers/application.rb
def default_url_options options = {}
options = super(options)
if options[:controller] == 'some_controller' and options[:id]
options[:shard] = options[:id].to_s[0..2]
end
options
end
==== anywhere
foo_path(:id => 12345) # => /foo/12/12345
However, in rails 3, that same code fails due to the fact that default_url_options is not passed any options hash, and I have yet to find out how to test what the controller is.
FWIW, the above "sharding" is due to when you turn caching on, if you have a large number of foo rows in your DB, then you're going to hit the inode limit on unix based systems for number of files in 1 folder at some point. The correct "fix" here is to probably alter the cache settings to store the file in the sharded path rather than shard the route completely. At the time of writing the above code though, part of me felt it was nice to always have the cached file in the same structure as the route, in case you ever wanted something outside of rails to serve the cache.
But alas, I'd still be interested in a solution for the above, purely because it's eating at me that I couldn't figure it out.
Edit: Currently I have the following which I'll have to ditch, since you lose all other named_route functionality.
==== config/routes.rb
match 'foo/:shard/:id' => 'foo#show', :as => 'original_foo'
==== app/controllers/application.rb
helpers :foo_path
def foo_path *args
opts = args.first if opts.is_a?(Array)
args = opts.merge(:shard => opts[:id].to_s[0..2]) if opts.is_a?(Hash) and opts[:id]
original_foo_path(args)
end
define a helper like
# app/helpers/foo_helper.rb
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
end
and then do the following in your view, seems to work for me
<%= link_to("my shard", id: 12345) %>
edit: or customize the foo_path as
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
def foo_path options = {}
options[:shard] = options[:id].to_s[0..1] if options[:id]
super options
end
end

How to detect language from URL in Sinatra

I have a multi-language website and I'm puting the language in the URL like domain.com/en/. When the user doesn't put the language in the URL I want to redirect him to the page in the main language like "domain.com/posts" to "domain.com/en/posts". Is there an easy way to do this with Sinatra?
I have more than one hundred routes. So doing this for every route is not a very good option.
get "/:locale/posts" do... end
get "/posts" do... end
Can someone help me?
Thanks
Use a before filter, somewhat like this:
set :locales, %w[en sv de]
set :default_locale, 'en'
set :locale_pattern, /^\/?(#{Regexp.union(settings.locals)})(\/.+)$/
helpers do
def locale
#locale || settings.default_locale
end
end
before do
#locale, request.path_info = $1, $2 if request.path_info =~ settings.locale_pattern
end
get '/example' do
case locale
when 'en' then 'Hello my friend!'
when 'de' then 'Hallo mein Freund!'
when 'sv' then 'Hallå min vän!'
else '???'
end
end
With the upcoming release of Sinatra, you will be able to do this:
before('/:locale/*') { #locale = params[:locale] }

Resources