Figuring out facebook iFrame clients - ruby-on-rails

I am in the process of building a facebook app that works through iFrame with Ruby On Rails.
My App does serve multiple clients, web, mobile, and facebook. And depending the type of client the UI renders different kind of views.
When the user connects to my app using the facebook page tab, I do get enough information (in params collection) to identify the user came from facebook. Based on that I can customize the views to fit into the iFrame.
But for subsequent requests, because they happens through iframe, there is nothing that tells this is a facebook request (as far as I can tell unless there is something in the headers which I dont know of).
I tried setting a cookie during the first request and that worked great. But the problem is when the user requested my app directly from another browser tab (not through facebook) the cookie was still present and the user ended up seeing the facebook(ised) UI, instead of Normal UI.
Anyone has a solution to this?

I recommend using a different route for Facebook tabs. So if your regular URLs look like this:
/foobar
Then you may want to use something like this for Facebook, by adding the "/fb" prefix (or similar) to your Facebook app's tab or canvas URL:
/fb/foobar
In your routes.rb, you can then pass a parameter to indicate that the user is viewing the page on Facebook, such as "channel=facebook" or "facebook=true".
Your routes.rb might look something like this:
scope 'fb', :channel => 'facebook' do
# ... your routes ...
end
scope :channel => 'web' do
# ... your routes ...
end
With these routes, each request originating from Facebook will automatically have a "channel=facebook" parameter. Of course, you'd still be responsible for making sure to generate the appropriate URLs within your app. You could add :as => 'facebook' and :as => web to the scopes above and use this to generate URLs using dedicated helpers (e.g. facebook_foobar_url, web_foobar_url). But the best way to do this depends a lot on the complexity of your app.
Alternatively, you could also use default_url_options in your controller to add a "channel=facebook" or "facebook=true" parameter to every generated URL if you detect that the current request originated from Facebook (either from the existence of a signed_request or from channel/facebook param you added). Note that this method is deprecated in Rails 3, and I'm not quite sure what (if any) the official replacement is in Rails 3.1 or 3.2.

I needed to solve this with the least amount of intrusion into the existing code.
And am posting my solution here for the benefit of anyone looking to solve similar problem.
I have a unique layout for facebook and this was invoked during the first request (like I mentioned above, the initial request posted from facebook tab has facebook params).
To make sure there was facebook param for subsequent requests, inside the facebook layout, I bound all the form submission and anchor click events to a method that would add a hidden form element and query string param respectively.
this is how the client side code looks like:
<script type="text/javascript">
$(document).ready(function() {
$('form').submit(function(e) {
$('<input>').attr({
type: 'hidden',
id: 'fb_param',
name: 'fb_param',
value: 'true'
}).appendTo($(this));
return true;
});
$('a').click(function(e) {
var newUrl = $(this).attr("href");
if (newUrl.indexOf("?") != -1) {
newUrl = newUrl + '&fb_param=true';
} else {
newUrl = newUrl + '?fb_param=true';
}
$(this).attr("href", newUrl);
return true;
});
});
</script>
Now to handle the server side redirects (typical when you update a resource etc), needed to extend the redirect_to method like so:
/app/conntrollers/application_controller.rb
class ApplicationController < ActionController::Base
def redirect_to(options = {}, response_status = {})
if params[:fb_param] == 'true'
if options
if options.is_a?(Hash)
options["fb_param"] = "true"
else
if options.include? "?"
options = "#{options}&fb_param=true"
else
options = "#{options}?fb_param=true"
end
end
end
end
super
end
end

Related

Autosubmit a language dropdow in Rails 4

I'm wondering how to get a dropdown that switches the page's language to work. I currently have the following:
<%= select('locale', 'id', %w(English Español Italiano 日本語 한국어), :onchange => "this.form.submit()") %>
Any idea how I can get this to switch the page's locale?
I am currently passing the locale via the params hash.
There are several problems:
You're trying to submit a select box..... but where does it submit to?
How are you managing the languages on your page?
--
When submitting with JS, you have to remember that it needs to somewhere to submit to.
Like a <form>'s action attribute, your JS needs a location to send the submit request. This is normally handled with a form, but can also be manually inputted with Ajax.
You may wish to use this:
#app/assets/javascripts/application.js
$(document).on("change", "select", function(e) {
$.ajax({
url: "your/url",
data: $(this).value(),
success: function(data) { ... },
error: function(data) { ... }
});
});
This will take the inputted value of the select box and send the data to a preformatted route in your Rails app. This leads nicely onto my second question -- how you get the language to remain in the app...
-
Regardless of your structure, you'll need to give your Rails app a way to determine the language you have selected.
If only to set a session variable, you have to pass your data through to your Rails controller.
To do this, you first need to ensure you have the routes and then the controller action required to handle the request:
#config/routes.rb
post :language, to: "application#language"
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def language
session[:language] = params[:language] #-> this needs to be refactored
end
end
You'll then be able to pass back some response or command to invoke the language change on the page.
Although setting the language in the backend, as described above, may seem tedious, doing so will give you the ability to set the entire language parameter for the site -- IE if you wanted to use it for currency etc, it will be available in all the actions you need.
Hope this helps.

Rails 4 external redirection and sessions issue

I am trying to build a website in Rails 4 to track users redirects and site element views.
I decided to use session ids which I believe are quite unique in the short term but I'm having a strange issue.
Example procedure:
user follows a redirect, the system stores this action with a Session ID, let's say xxx
user reaches destination page, which contains a tracker, the system stores this action with ANOTHER Session ID, yyy
user reaches another page which also contains a tracker, the system stores this action with Session ID yyy
After the second action is stored, the session ID stays the same yyy for every request after that, but I need to have the same session ID every time.
In session I also store a SecureRandom.hex generated code, which also changes from the first to the second request (which is not a surprise, since the session ID changes).
I also tried using a cookie, same result.
Please notice that these redirects are external, but all the requests are then made to the same domain (exactly the same, without www and in https).
Any idea?
Thanks in advance.
Update
this is the source code responsible for managing redirects:
before_action :load_redirect, :only => [:http_redirect]
def http_redirect
raise ActionController::RoutingError.new('Redirect has been disabled') unless #redir.enabled
ua = UserAction.create(
:session_id => session.id,
:user_agent => request.user_agent,
:trackable => #redir,
:ip_address => request.remote_ip,
:referer => request.referer
)
redirect_to #redir.destination_url
end
private
def load_redirect
#redir = Redirect.find(params[:id])
end
UPDATE:
Since you are using an iframe (per comment discussion below) for tracking code, the issue is likely that on the external site cookies are not being passed from parent page to the iframe because the iframes origin (domain) is different from the parent page.
OLD ANSWER:
(Still could be helpful for others debugging similar issues)
Source code would help. Without that, here are a few things to try:
Try disabling CSRF protection for the external tracking link action (I'm assuming it POSTs or PUTs data from an external source). CSRF protection could be creating a new or null session for those requests. Put this in the controller that contains the action accepting data from the external source:
protect_from_forgery :except => [:your_action]
The redirect (especially if it's a 301) could be cached in the browser you are using, hence having a different cookie and session than the request your tracking code makes. The stale cookie would be part of the cached redirect.
Try putting cache control headers on your controller action that does the redirect.
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
Your browser may not support setting cookies on a redirect, or possibly third-party cookies. Try in a different modern browser?
There could be a bug in your code. If these solutions don't work, maybe post it?

Facebook app: within Canvas, Facebook treats GETs as POSTs?

In my Rails FB app, when running in the canvas, if I go to "/users", instead of GETting the index page, it POSTs and creates a new user.
That only happens when I try to access the URL directly. When I use the "List of users" link on the app, it works as expected.
Why is that?
that's just how FB does it. They post data with each query as well.
Facebook sends everything as POST which brakes RESTful routes. There is a way to fix it though. If incoming POST request contains signed_request parameter you can assume it was converted from GET to POST by Facebook.
Rack::Facebook::MethodFix middleware fixes the problem automagically. You can use it with something like:
# Basic usage
use Rack::Facebook::MethodFix
# Also validate signed_request
use Rack::Facebook::MethodFix, :secret_id => "c561df165eacdd6e32672c9eaee10318"
# Do not apply request method fix to admin urls.
use Rack::Facebook::MethodFix, :exclude => proc { |env| env["PATH_INFO"].match(/^\/admin/) }
or if you are using Rails then
config.middleware.use Rack::Facebook::MethodFix

Passing parameters through OmniAuth

I need to pass some parameters to callback action. Judging from the source code, OmniAuth should add query string to callback URL but strangely it does not. When I open
/auth/facebook?from=partner
...and get redirected to Facebook, return_url is just
/auth/facebook/callback
...without any parameters.
After struggling with all the above answers, I figured out what to do regarding Facebook, which by default does not display the params in request.env["omniauth.auth"].
So -- If you are using a query string for the callback, similar to something like this:
"/auth/facebook?website_id=#{#website.id}"
The only way to get that website_id param is by using request.env["omniauth.params"]. NOTE: MAKE SURE YOU USE omniauth.params and not omniauth.auth -- this one tripped me up for a while.
Then, to test this out, you can inspect it within your controller action (notice the RAISE line...):
def create
raise request.env["omniauth.params"].to_yaml
# the rest of your create action code...
end
You should see your parameter there. Great. Now, go back to your controller and remove that RAISE line. Then, you can access the param as follows in your controller action:
params = request.env["omniauth.params"]
website_id = params["website_id"]
NOTE: in params["website_id"] you need to use quotes and NOT a symbol.
I guess the cookie thing works but why do all that when you can use the state variable as documented here: https://github.com/mkdynamic/omniauth-facebook
This is how I used it:
when creating the url you can just add state in the Query String and it will be available in the callback url as well.
user_omniauth_authorize_path(:facebook, :display => 'page', :state=>'123') %>
now the callback url will be
http://localhost:3000/users/auth/facebook/callback?state=123&code=ReallyLongCode#_=_
Now in the callback handler you can process the state
You can use the :params options, as in
omniauth_authorize_path(:user, :facebook, var: 'value', var2: 'value2' )
and later in the callback you can access request.env['omniauth.params'] to get the hash! :)
(copied from this answer)
What you want to do is dynamically set your callback to include the partner name in the url (not the url parameters), on a per authentication transaction basis, depending on which partner was involved. This means setting the callback url dynamically, for each authentication request. See this blog post to get started. The callback url automatically drops the url parameters, as you've noticed, so doing this with parameters won't work.
So, if instead of trying to pass the partner name/id in as a parameter (which is dropped), you structured your routes so that the partner_id and OmniAuth provider were part of the callback url, then you'd have something like:
/auth/:omniauth_provider/callback/:partner_id
...where a valid callback would be something like
/auth/facebook/callback/123456
...then you would know that a given callback came in from facebook, with partner id 123456
OmniAuth already has a built-in way to know where the user was, it's called "origin" as documented here:
https://github.com/intridea/omniauth/wiki/Saving-User-Location
You know, I think I might be trying to solve this the hard way.
Cookies might be the answer. I think you can solve this by having your login action store a cookie, and then redirecting to the proper /auth/:provider path for authentication, and when the callback is triggered (in SessionsController#create), you just read the cookie back to know where to redirect them to.
So, right now, your "login with facebook" link (or whatever you have you in your app) probably goes to /auth/facebook. Instead if you created a custom action like
POST /partner_auth
...and called it with the url...
POST example.com/partner_auth?from=partner&provider=facebook
Then you might have a controller like:
class PartnerAuth < ApplicationController
def create
cookies[:from] = params[:from] # creates a cookie storing the "from" value
redirect_to "auth/#{params[:provider]"
end
end
Then in the SessionsController#create action, you would have...
def create
...
destination = cookies[:from]
cookies[:from].delete
redirect_to destination # or whatever the appropriate thing is for your
# app to do with the "from" information
end
I tried to build a demo app to accomplish what I'd outlined in the other answer, but you're right - it was too complicated to try to dynamically inject a custom callback into the OmniAuth code. There is a configuration option to override the default callback, but it doesn't appear to be easy to set it dynamically.
So, it dawned on me that cookies would be way simpler, user-specific, and since you theoretically only need to store this from information for a very short time (between when the user tries to authenticate, and when the callback is triggered), it's no big deal to create a cookie, and then delete it when the callback gets hit.
Use the 'state' Variable. Facebook allows the user to set a STATE variable.
Here is how I did it, I appended the AUTH URL with ?state=providername
http://localhost/users/auth/facebook?state=providername
This param is returned to me at Callback as params['providername']
I devised the solution from the original Omniauth Path Method
user_omniauth_authorize_path(:facebook, :display => 'page', :state=>'123') %>

How can I preserve querystring values across requests in Ruby on Rails?

I'm coming up against one of those moments working with Rails when I feel that there must be a better way than what I'm ending up with.
I have four querystring parameters that I want to preserve across requests through various parts of a rails app - different controllers and actions, some rendered via javascript - so that the user ends up at a URL with the same querystring parameters that they started with.
I'm finding it hard to believe that the best way is through hidden form fields and manually adding the params back in as part of a redirect_to, or using session vars for each - it just seems to un-rails like.
Does anyone know of a better way to manage this?
Thanks!
In cases like this, I'll often use a helper function to create urls based on the current set of params. In other words, I'd define this:
def merge_params(p={})
params.merge(p).delete_if{|k,v| v.blank?}
end
And then use it with url_for to create the urls for your forms and links. If you need to modify and of the params, just pass them into the merge:
# url for the current page
url_for(merge_params)
# url for a different action
url_for(merge_params(:controller => 'bar', :action => 'bar'))
# et cetera
url_for(merge_params(:action => 'pasta', :sauce => 'secret'))
This'll preserve existing params plus whatever overrides you merge in.
If you want to always preserve certain parameters accross every generated URL, you can implement a default_url_parameters method in your app.
This is poorly documented, but mentioned in the Rails 18n Guide
Implement in your application_controller.rb:
def default_url_options(*args)
super.merge(
:foo = params[:foo],
:bar = params[:bar]
)
end
Now every URL generated by Rails will include the current request's values for foo and bar (unless a url is being generated that specifies those specifically to be something else).
Please note this is not a rails solution but works for me in 80% cases. This is how I am able to achieve this with jQuery, obviously not the best approach. And I am unable to make it work past 2nd level of links:
function mergeGPS(){
var lat = getParam('lat');//localStorage.getItem('lat');
var lng = getParam('lng');//localStorage.getItem('lng');
if(lat == null || lng == null){return false;}
var links = jQuery("a");
jQuery.each(links, function(y, lnk){
//debugger;
var uri = jQuery(lnk).attr('href') + "?lat="+lat+"&lng="+lng;
jQuery(lnk).attr('href',uri);
//debugger;
})
}
function getParam(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
mergeGPS needs to be called on page load.

Resources