I've inherited a little rails app and I need to extend it slightly. It's actually quite simple, but I want to make sure I'm doing it the right way...
If I visit myapp:3000/api/persons it gives me a full list of people in XML format. I want to pass param in the URL so that I can return users that match the login or the email e.g. yapp:3000/api/persons?login=jsmith would give me the person with the corresponding login. Here's the code:
def index
if params.size > 2 # We have 'action' & 'controller' by default
if params['login']
#person = [Person.find(:first, :conditions => { :login => params['login'] })]
elsif params['email']
#persons = [Person.find(:first, :conditions => { :email => params['email'] })]
end
else
#persons = Person.find(:all)
end
end
Two questions...
Is it safe? Does ActiveRecord protect me from SQL injection attacks (notice I'm trusting the params that are coming in)?
Is this the best way to do it, or is there some automagical rails feature I'm not familiar with?
Yes, the code you listed should be safe from SQL Injection.
Yes, this is generally acceptable rails code...but
There are some oddities.
Your index action talks about #person and #persons. By convention, #persons is expected, and #person is unusual. I suspect you can eliminate #person, and clear things up in one swoop. Something like this (untested):
def index
#persons = if params[:email]
Person.find_all_by_email(params[:email])
elsif params[:login]
Person.find_all_by_login(params[:login])
else
Person.all
end
end
Don't forget to update your view -- I suspect it's still looking for #person. If your view is doing anything "interesting" with #person, you probably want to move it to your show action.
Here's the ruby on rails security guide section on SQL Injection. It looks like what you have, using hash conditions, is pretty safe. Sounds like using Person.find_by_login or Person.find_by_email might be a little better.
Related
I've got this helper method in my application controller:
def current_team
#current_team ||= Team.find(params[:team_id])
end
Problem is, it works for urls of the format:
/teams/20/members/11
but it doesn't work for:
/teams/20
In order to get it to work for those, I have to change :team_id to be :id.
How can I tidy it up so it 'just works'?
Thanks!
Set instance variables (#current_team) in controllers, never in helpers. It's not what helpers are for.
If you follow this advice, you will naturally use params[:id] in TeamsController, but params[:team_id] in MembersController.
(Some people even go on to say that you shouldn't use helpers at all. For facilitating presentation (custom links, buttons, tables, etc), they propose to use Presenter pattern. But you don't have to listen to them. :))
It is not the best thing to do, but to accomplish that you can do the following:
def current_team
#current_team ||= Team.find(params[:team_id].presence || params[:id])
end
Documentation about the Object.presence method:
http://api.rubyonrails.org/classes/Object.html#method-i-presence
#SergioTulentsev is right, you shall not set instance variables in helpers, only in controllers.
I'm assuming you have other resources besides just Team. Rails is going to use the :id param for all of your resources. You will need to look into customizing the routes for your teams#show action. Easier in Rails 4 than in Rails 3.
Have a look at this post for the gory details: Change the name of the :id parameter in Routing resources for Rails
I wouldn't do params[:team_id] || params[:id], because of course in some controller contexts you'd get an id parameter that represents the id for something other than a Team. Assuming that the /teams/:id route is handled by the TeamsController, then you could do the following (to keep your method in ApplicationController and avoid repeating yourself in different controllers):
def current_team
id = controller_name == "teams" ? params[:id] : params[:team_id]
#current_team ||= Team.find(id)
end
Alternatively, you could change your routes so that the url to show a Team is /teams/:team_id and leave your helper as-is, but that would go against the grain of Rails routing conventions.
I have a User model in my app and I would like to "find" a User by the name attribute. I don't believe there is any out-of-the-box support for this in batman.js but it is possible that it is just undocumented. I will also need to make some changes to the Rails side to receive and process this type of request as well. Has anyone already tackled this type of problem?
I was mistaken, this functionality exists in Batman out-of-the-box.
YourApp.User.load({name: "wuliwong"}, (err, results) ->
#do stuff with the results array
The UsersController#index action in Rails now looks like this:
def index
if params[:name]
#users = User.where(:name => params[:name])
else
#users = User.all
end
end
This is likely something easy to accomplish, but I'm having difficulty even articulating clearly, leading me to get all sorts of semi-germaine, but not quite what I'm after posts on SO when searching.
I have a resource, we'll just use User as a simple to discuss use case. I desire SEO friendly URLs, so instead of ~/users/:id, we have ~/users/:name, where the show action on my users controllers is doing a User.find_by_name(:name). Works great.
In various parts of the app, I want to redirect the end user back to my resource. The docs seem to indicate I should do
redirect_to #user, :notice => "your page is now DIAMONDS!"'
The problem here is that this automatically appears to use the :id value of the resource, which isn't what I'm after. I'm wondering if there's a way to just define a route in my routes file which is aware of my desire to use the name property--and then just redirect to the generated route_url helper. But I'm at a loss for how to do that.
In the interim, I'm resorting to this:
flash[:notice] = "Your account is now DIAMONDS. *fist bump*"
redirect_to :action => :show , :id => #user.name
This is less than ideal to me as it's a good bit of repeated code (I have lots of UI elements that will be linking back to my resource) and because I can't seem to figure out how to include the flash as part of the redirect_to method call---every combo of curly bracing w/in the redirect_to that includes a flash bombs on me.
I don't think it's germaine to my problem specifically, but I am doing this on Rails 3 in case it does have some implication in terms of options available to me.
Sorry for the noobishness :)
Pretty simple to do. The standardish way of doing this is:
/app/models/user.rb
def to_param
"#{id}-#{name}"
end
That's nice, and gives you nicer SEO -- http://yoursite.com/users/1345-supercool .... You can tweak it a bit to remove the '1345':
/app/models/user.rb
# add_column :permalink, :string
attr_protected :permalink
before_create :set_permalink
validates_uniqueness_of :permalink
def set_permalink
self.permalink = username.parameterize
end
def to_param
permalink
end
This will give you http://yoursite.com/users/supercool and will be a permanent URL, so that if the user changes their username later, the URL will stay the same and keep search engines happy.
I understand how to create a vanity URL in Rails in order to translate
http://mysite.com/forum/1 into http://mysite.com/some-forum-name
But I'd like to take it a step further and get the following working (if it is possible at all):
Instead of:
http://mysite.com/forum/1/board/99/thread/321
I'd like in the first step to get to something like this: http://mysite.com/1/99/321
and ultimately have it like http://mysite.com/some-forum-name/some-board-name/this-is-the-thread-subject.
Is this possible?
To have this work "nicely" with the Rails URL helpers you have to override to_param in your model:
def to_param
permalink
end
Where permalink is generated by perhaps a before_save
before_save :set_permalink
def set_permalink
self.permalink = title.parameterize
end
The reason you create a permalink is because, eventually, maybe, potentially, you'll have a title that is not URL friendly. That is where parameterize comes in.
Now, as for finding those posts based on what permalink is you can either go the easy route or the hard route.
Easy route
Define to_param slightly differently:
def to_param
id.to_s + permalink
end
Continue using Forum.find(params[:id]) where params[:id] would be something such as 1-my-awesome-forum. Why does this still work? Well, Rails will call to_i on the argument passed to find, and calling to_i on that string will return simply 1.
Hard route
Leave to_param the same. Resort to using find_by_permalink in your controllers, using params[:id] which is passed in form the routes:
Model.find_by_permalink(params[:id])
Now for the fun part
Now you want to take the resource out of the URL. Well, it's a Sisyphean approach. Sure you could stop using the routing helpers Ruby on Rails provides such as map.resources and define them using map.connect but is it really worth that much gain? What "special super powers" does it grant you? None, I'm afraid.
But still if you wanted to do that, here's a great place to start from:
get ':forum_id/:board_id/:topic_id', :to => "topics#show", :as => "forum_board_topic"
Take a look at the Rails Routing from the Outside In guide.
maybe try something like
map.my_thread ':forum_id/:board_od/:thread_id.:format', :controller => 'threads', :action => 'show'
And then in your controller have
#forum = Forum.find(params[:forum_id])
#board = #forum.find(params[:board_id])
#thread = #board.find(params[:thread_id])
Notice that you can have that model_id be anything (the name in this case)
In your view, you can use
<%= link_to my_thread_path(#forum, #board, #thread) %>
I hope this helps
For somewhat complicated reasons, I would like to create something that works like this:
# Controller:
#comments = #page.comments # comments are threaded
# child comments still belong to #page
...
# View:
#comments.each_root {
display #comment {
indent & recurse on #comment.children
} }
# alternatives for how recursion call might work:
# first searches #comments, then actually goes to SQL
comment.in_memory.children
# only looks at #comments, RecordNotFound if not there
# better if we know #comments is complete, and checking for nonexistent
# records would be wasteful
comment.in_memory(:only).children
# the real thing - goes all the way to DB even though target is already in RAM
# ... but there's no way for find() to realize that :(
comment.children
I'm not even sure yet if this is possible, let alone a good idea, but I'm curious, and it'd be helpful.
Basically I want to redirect find() so that it looks first/only at the collection that's already been loaded, using something like a hypothetical #collection.find{|item| item.matches_finder_sql(...)}.
The point is to prevent unnecessarily complex caching and expensive database lookups for stuff that's already been loaded en masse.
If possible, it'd be nice if this played nice with extant mechanisms for staleness, association lazy loading, etc.
The nested-comments thing is just an example; of course this applies to lots of other situations too.
So... how could I do this?
You should not write something that's already in Rails itself! You can easily leverage Rails' caching methods to put your query results in Memcached (or what ever caching framework you configured):
class ArticleController < ApplicationController
def index
#articles = Rails.cache(:articles_with_comments, :expires_in => 30.minutes) do
Article.find(:all, :include => :comments)
end
end
end
BTW. the :expires_in is optional. You can leave the cache 'forever' or expire it manually.
Second example, as by my comment:
class ArticleController < ApplicationController
def index
#page = Page.find(params[:page_id]
#articles = Rails.cache([:articles_with_comments, #page.id], :expires_in => 30.minutes) do
Article.find(:all, :include => :comments, :conditions => { :page_id => #page.id})
end
end
end
This will cache the articles and comments for a given #page object.