Rails - escaping backslashes in params causing havoc? - ruby-on-rails

I have a form that passes the same parameters as the form before it:
<%= form_tag({:controller => "workouts", :action => "random"}) do %>
<%= hidden_field_tag :workout, params[:workout] %>
<%= hidden_field_tag :time, params[:time] %>
<%= submit_tag "Get Another", :class => 'btn' %>
The first form works fine, the second form to "get another" gives me the error can't convert Symbol into Integer for this line:
#equipment_ids = params[:workout][:equipment_ids].collect{|s| s.to_i}
The params of the first and second form being passed are:
{"utf8"=>"✓",
"authenticity_token"=>"qj/Q/YWvLKK3A3paAnEom4oTFtq44daX6dvEb8qmgtE=",
"workout"=>{"equipment_ids"=>["",
"508",
"518"]},
"time"=>"25",
"commit"=>"Get Workout"}
{"utf8"=>"✓",
"authenticity_token"=>"qj/Q/YWvLKK3A3paAnEom4oTFtq44daX6dvEb8qmgtE=",
"workout"=>"{\"equipment_ids\"=>[\"\",
\"508\",
\"518\"]}",
"time"=>"25",
"commit"=>"Get Another"}
The only difference is the escaping backslashes. I'm not sure why these would cause a problem?

Changed the hidden field tag to:
<%= hidden_field_tag "workout[equipment_ids][]", params[:workout][:equipment_ids] %>

I just went into the same problem when trying to manually submit a form with a custom POST request. The problem seems to be that net/http post_form method can only handle a single hash where all the values are Strings. If you have hash inside hash (like in the form that scaffold generates), it treats the inner hash as a String, and adds the nasty backslashes that, as you just saw cause havoc :)
The solution for me was to use the lower level "post" method, and to manually encode the hash. Define this module:
module HashToHttpParams
def to_http_params
map do |k, v|
if v.is_a?(Hash)
v.map do |kk, vv|
"#{k}[#{kk}]=#{vv}"
end.join('&')
else
"#{k}=#{v}"
end
end.join('&')
end
end
And then add it to the Hash class in your code:
Hash.send(:include, HashToHttpParams)
Finally encode your params hash before using it. In my code this looks like:
Net::HTTP.start("localhost",3000) do |http|
http.post("/tests", params.to_http_params)
end
Don't know if there's a better solution, but this worked for me.
Source: http://porras.lacoctelera.net/post/2007/10/08/enviando-formularios-con-parametros-compuestos-con-ruby-y-net#c4300080

As Hallucynogenyc pointed out, this is caused by the .post_form (docs) method only wanting a non-nested hash that is strings. I had this same problem, and solved it by switching to use the .post method.
require "net/http"
uri = URI('http://www.yoururl.com')
http = Net::HTTP.new(uri.host)
response = http.post(uri.path, params.to_query)
The .to_query method is also useful for converting hashes.
Another way to solve it is to not use the rails form method to create your params. If you just use straight html, for some reason the .post_form method likes it better.
Email <input name="student_email" type="email" autofocus="true">

Related

How to give a form parameter a name in rails form?

I have a form with several radio buttons. The parameter it submits is an array of hashes. In the server logs, the array of hashes doesn't seem to have a name (probably because I need to give it one). Consequently, I can't tell params.require(:availability).permit(:<HERE>) to permit it, since I don't know what name to put inside .permit().
I've tried tinkering with the private controller method do relax requirements there, but I figure that's a hack rather than a good solution.
Here is my code in the view that generates the array of hashes
<%= form_tag("/availabilities/") do |f| %>
<%= label_tag :availability %>
<% #hours.each do |hour| %>
<%= hour[:time_slot] %>
<%= radio_button_tag hour.to_s, available_in_words(hour[:time_slot_available?]) %>
<%= radio_button_tag hour.to_s, available_in_words(!hour[:time_slot_available?]) %></br>
<% end %> <!-- ends hour loop -->
<div><%= submit_tag 'Save' %></div>
<% end %> <!-- ends form -->
Note: what is being submitted via this form doesn't match the fields in the Availability model - it's more raw and will need work after submission before creating/updating records in Availability. I'm not sure if that affects things
What I know so far
I thought label_tag would give the form parameter a name by which it could be referenced in params.require(:availability) but that leads to error param is missing or the value is empty: availability
Note
This is what params looks like after submission:
<ActionController::Parameters {"authenticity_token"=>"KEuNcLzuAcQj6b+0oQ0FzjOE35f1Xq3MNNomzTnC9SCML9kaWVIFgphCgDRy5cHowxQ/N4kodNIXYCAwtCPGnA==", "{:time_slot=>2020-08-22 18:00:00 +1000, :time_slot_available?=>false}"=>"Unavailable", "{:time_slot=>2020-08-22 20:00:00 +1000, :time_slot_available?=>false}"=>"Available", "commit"=>"Save", "controller"=>"availabilities", "action"=>"create"} permitted: false>
Accessing "authenticity_token" is easy enough: params[:authenticity_token]. But I can't meaningfully access the input from the radio buttons
The name is the first parameter to almost all the FormTagHelper methods.
radio_button_tag(name, value, checked = false, options = {})
https://api.rubyonrails.org/v5.1.7/classes/ActionView/Helpers/FormTagHelper.html#method-i-radio_button_tag
In Rack (which Rails sits on top of) you can pass hashes through the parameters by using brackets:
foo[bar][a]=1&foo[bar][b]=2
Will result in:
{
foo: {
bar: {
a: 1,
b: 2
}
}
}
You can whitelist nested hashes by passing a hash to .permit:
params.require(:foo).permit(bar: [:a, :b])
You can pass arrays through the parameters by using empty brackets:
foo[bar][]=1&foo[bar][]=2
Which will result in:
{
foo: {
bar: [1,2]
}
}
You can then whitelist an array by:
params.require(:foo).permit(bar: [])
[] will allow an array of permitted scalar values.
I'm not sure exactly what #hour.to_s returns but you might need to ensure that it is something actually suited to be used as a key in a formdata key/value pair and does not mess with Rack's parameter parser.

Pass a PORO from view to controller

What would be the best way to pass a Plain Old Ruby Object that I have in a view to a controller method?
It is not an object that is persisted in the DB. I would rather not refactor things and just want some ideas on best way to pass this object.
view
link_to "activate", activate_apis_path(my_ip_instance: #my_ip), class: "btn btn-primary btn-xs"
controller
#my_ip = params[:my_ip_instance]
#my_ip is just a string... want the whole object
(Rails 4.2)
Usually the best way is through a form. Consider creating a form with hidden fields for all of your #my_ip attributes.
<%= form_tag activate_apis_path do %>
<%= hidden_field_tag "my_ip_instance[foo]", #my_ip.foo %>
<%= hidden_field_tag "my_ip_instance[tomato]", #my_ip.tomato %>
<%= submit_tag "Activate", class: "btn btn-primary btn-xs" %>
<% end %>
(Extra credit: you could also loop over #my_ip's attributes to generate the hidden fields.)
Another way is to serialize #my_ip as JSON and then deserialize it in the controller. I think that is much messier though.
link_to "activate", activate_apis_path(my_ip_instance: #my_ip.to_json)
To make this work for a more complex object, you would need to write your own serializer/deserializer logic into the class as described in this post.
require "json"
class A
def initialize(string, number)
#string = string
#number = number
end
def to_json(*a)
{
"json_class" => self.class.name,
"data" => {"string" => #string, "number" => #number }
}.to_json(*a)
end
def self.json_create(o)
new(o["data"]["string"], o["data"]["number"])
end
end
What you basically need to do is to serialize the object. Rails provides built in options to serialize objects and Object.to_query which can be used to convert to query parameters.
But, passing around a bunch of state like that violates rest and is indicative of a poor application design. If it's a non-persisted object it should be passed as form parameters in a POST/PATCH request. In a GET request you should strive to initialize all objects needed from the headers, params from the request url and session.

Rails params (url_params vs form_data)

This is just a question out of curiosity.
Rails is magical in the way that it does a lot of things for us, but it comes with a curse of knowledge.
When Rails receives an http request, we can access the inputs from client via params[]. However, I notice that params can accept inputs from both the url_params, and the form_data. For e.g.:
# Get users/:id (param comes from url)
# Post users (param comes from form)
Is there a rule to how params[] works? Will Rails just put all the parameters from url and form to params[]?
In the case of NodeJS, there is a distinction between
request.params
request.body
request.query
The answer can be found in the Rails Guides, in the chapter about Action Controller Overview -> Parameters:
Rails does not make any distinction between query string parameters and POST parameters, and both are available in the params hash in your controller:
What are params?
params are nothing but the parameters which are send to your controller when you send a HTTP request from your browser.
Types of params?
If you look at rail guides it says
There are two kinds of parameters possible in a web application. The first are parameters that are sent as part of the URL, called query string parameters. The query string is everything after "?" in the URL. The second type of parameter is usually referred to as POST data. This information usually comes from an HTML form which has been filled in by the user
Is there a rule to how params works?
As #zwippie pointed out rails doesn't make any distinction whether your params are coming from a form or a query string, but they do differ in the way rails put these params in a hash and hence different ways to access them in controller
For query string:
If your url is something like:
http://www.example.com/?vote[item_id]=1&vote[user_id]=2
then your params will look like:
{"item_id" => "1", "user_id" => "2"}
and hence you can access them in your controller by params[:item_id] and params[:user_id]
For POST data or from a form:
Lets say your form is like
<%= form_for #person do |f| %>
<%= f.label :first_name %>:
<%= f.text_field :first_name %><br />
<%= f.label :last_name %>:
<%= f.text_field :last_name %><br />
<%= f.submit %>
<% end %>
and when you submit your form the parameters will look like this
{"person"=> {"first_name"=>"xyz", "last_name"=>"abc"}}
notice how a form has nested your parameters in a hash so to access them in your controller you'll have to do params[:person] and to get individual values you can do params[:person][:first_name]

Get url for current page, but with a different format

Using rails 2. I want a link to the current page (whatever it is) that keeps all of the params the same but changes the format to 'csv'. (setting the format can be done by having format=csv in the params or by putting .csv at the end of the path). Eg
posts/1
=> posts/1.csv OR posts/1?format=csv
posts?name=jim
=> posts.csv?name=jim OR posts?name=jim&format=csv
I tried this as a hacky attempt
request.url+"&format=csv"
and that works fine if there are params in the current url (case 2 above) but breaks if there aren't (case 1). I could come up with more hacky stuff along these lines, eg testing if the request has params, but i'm thinking there must be a nicer way.
cheers, max
EDIT - btw, it's not guaranteed that the current page could have a named route associated with it, in case that's relevant: we could have got there via the generic "/:controller/:action/:id" route.
<%= link_to "This page in CSV", {:format => :csv } %>
<%= link_to "This page in PDF", {:format => :pdf } %>
<%= link_to "This page in JPEG", {:format => :jpeg } %>
EDIT
Add helper
def current_url(new_params)
url_for :params => params.merge(new_params)
end
then use this
<%= link_to "This page in CSV", current_url(:format => :csv ) %>
EDIT 2
Or improve your hack:
def current_url(new_params)
params.merge!(new_params)
string = params.map{ |k,v| "#{k}=#{v}" }.join("&")
request.uri.split("?")[0] + "?" + string
end
EDIT
IMPORTANT! #floor - your approach above has a serious problem - it directly modifies params, so if you've got anything after a call to this method which uses params (such as will_paginate links for example) then that will get the modified version which you used to build your link. I changed it to call .dup on params and then modify the duplicated object rather than modifying params directly. – #Max Williams
You can use:
link_to "text of link", your_controller_path(format:'csv',params: request.query_parameters)
#floor's answer was great, I found it very useful.
Although the method can be improved by using the to_params method rather than contructing your own, like so:
def current_url(new_params)
params.merge!(new_params)
"#{request.uri}#{params.to_params}"
end

How can you pass an object from the form_for helper to a method?

So let's say I have a form which is being sent somewhere strange (and by strange we mean, NOT the default route:
<% form_for #form_object, :url => {:controller => 'application',
:action => 'form_action_thing'} do |f| %>
<%= f.text_field :email %>
<%= submit_tag 'Login' %>
<% end %>
Now let's say that we have the method that accepts it.
def form_action_thing
User.find(????? :email ?????)
end
My questions are thus:
How can I make the object #form_object available to the receiving method (in this case, form_action_tag)?
I've tried params[:form_object], and I've scoured this site and the API, which I have to post below because SO doesn't believe I'm not a spammer (I'm a new member), as well as Googled as many permutations of this idea as I could think of. Nothing. Sorry if I missed something, i'm really trying.
How do I address the object, once I've made it accessible to the method? Not params[:form_object], I'm guessing.
EDIT
Thanks so much for the responses, guys! I really appreciate it. I learned my lesson, which is that you shouldn't deep-copy an object from a form, and that the parameters of a form are actually included when you submit it.
I will admit it's sort of disheartening to not know stuff that seems so obvious though...
you need to pass the "id" of your "#form_object" in the url and then lookup that object (assuming you have a model and using ActiveRecord)
It depends on how do you set up your routes. If you're using the default /:controller/:action/:id route, you can pass it as a parameter in the URL. Note that not the whole #form_object can/should be passed, but it's id or some other attribute to identify it instead. In this case, you should make your URL:
<% form_for #form_object, :url => {:controller => 'application',
:action => 'form_action_thing', :email => some_email} do |f| %>
<%= f.text_field :email %>
<%= submit_tag 'Login' %>
<% end %>
And in your controller
def form_action_thing
#user = User.find_by_email(params[:email])
end
You can pass parameters through the url, but when submitting a form the only thing that should (probably) be passed through the url is the record id for a RESTful record.
And it appears you didn't find out yet where your form data can be found in the params.
So
All the data from your form should end up in params[:form_object]. The actual value for :form_object is selected by Rails, it's probably coming from the object's class (too lazy to look that up right now)
In any case, you can easily find out where your form values are submitted by looking at your console/log output. All the params for each requests are dumped there.
The form fields will be inside the params like params[:form_object][:email] - each field that is submitted has an entry corresponding to the field name.
The params hash not contain all the original values from your #form_object. There will be only those values that you included in the form.
If you need to pass non-editable values to the controller with your form, use hidden_field(s) These will be submitted with the form, but are not visible to the user.

Resources