Fragment Caching and User avatars/images - ruby-on-rails

We use Gravatar on our website, but we want to let users upload their profile images directly in an effort to improve user experience similarly to what Stackexchange has been doing.
On our website users can follow each other, comment, 'like' and interact in ways that cause content to be generated directly and indirectly.
This means that cache-fragments with user avatars are scattered all over the place, and we can't find a reasonable way to invalidate it without negatively affecting render performance.
Here are the possible solutions we've looked at:
Option 1) Take the Gravatar approach and set very short Expires/Cache-Control max-age headers and recycle the same image filename.
Option 2) Use a placeholder image for all users with a data attribute containing the user ids that are read by JavaScript and used to make a second Ajax request asking the server for up-to-data avatars.
Is there any better way to solve this problem? Are we overlooking something?

I think I understand your question, but Im not sure I understand option 2 as a solution, which may indicate that its not a great solution. If it was me I would just cache the html surrounding the user gravatar, which is being reused, in a top level cache key, keyed by the users id. I.E.
<% cache("user_#{user.id}_profile_image") do %>
<img src="blahblahblah or gravatar helper code or whatev">
<% end %>
If you're concerned about a user uploading a gravatar, and subsequently having a cached default gravatar image, I would say your best options are:
1) In the users profile area where you send them to upload a gravatar, have a refresh link which points to a refresh action which actually invalidates that users cache fragment. i.e.
expire_fragment "user_#{user.id}_profile_image"
(http://guides.rubyonrails.org/caching_with_rails.html)
2) you could instead of using the default gravatar redirect to upload an image, you could intercept the click, send it to your own controller, schedule a background task to be run in 15 minutes or so, and then render a javascript response which redirects them to the actual gravatar page to upload their pic. The background worker would then clear the fragment at a later time when it ran. This assumes they actually upload their image and is alltogether I would say a terrible idea.
Honestly though, Im not sure why you are concerned about caching the gravatar to begin with. It's hitting gravatars servers so it causes no load on your server, storing it yourself seems a bit self defeating to the point of using gravatar. Hopefully your question was simpler (and was just: how to clear default cached gravatar image when user uploads their own image), which can be solved by #1), which will allow you to expire the cached gravatar image and recache it using your own image, after the user uploads their image. (which, next time the page was rendered would recache the image because youd have some logic like:
<% cache("user_#{user.id}_profile_image") do %>
<% if user.has_uploaded_image? %>
<%= display_profile_image %>
<% else %>
<%= display_gravatar_image %>
<% end %>
<% end %>

Related

Russian doll caching and permission-based links in view fragment

I've got a view that utilizes Russian Doll caching, where the whole collection of items is cached, and each item in the collection is cached individually within that cache.
However, each item in the collection should show edit/delete links based on the current user's permissions granted through CanCan. So, User A would only see the edit/delete links next to her own posts, but not next to User B's posts.
Well, whenever a post is created by User A, it's cached with the appropriate edit/delete links since she should have them visible based on her permissions. But when User B views the collection, he's served User A's cached post, along with the edit/delete links that he shouldn't see. Granted, CanCan prevents these edit/delete actions from occurring, but the links are still present.
Is there anyway around creating individual caches based on current_user.id and prevent having gobs of versions of (almost) identical cached content?
Is there anyway around creating individual caches based on current_user.id and prevent having gobs of versions of (almost) identical cached content?
Instead of including the user's ID in the cache key, you could include the users' permissions. This would still have duplicate copies of the content, but scales as per your permission model, not the number of users. So instead of the typical:
<% cache("posts/all-#{Post.maximum(:updated_at).try(:to_i)}") do %>
...
<% end %>
you can create a cache key like (assuming current_user returns the authenticated user) and you care about only editing vs. reading:
<% cache("posts/all-#{Post.maximum(:updated_at).try(:to_i)}-#{current_user.can?(:edit, Post) ? :edit : :read}") do %>
...
<% end %>
Note that the cache key generation should probably be extracted to a separate class/helper method.

Preventing users of my web application from modifying image id while uploading to cloudinary

I am building a rails web application where users can upload images from the browser to cloudinary. I am thinking a bit ahead, and want to prevent users from playing with the parameters being passed, in order not to have undesirable output in may pages (empty images).
In my view I have the code for the cloudinary uploader:
<%= form_tag(some_path, :method => :post) do %>
<%= cl_image_upload_tag(:image_id) %>
...
<% end %>
Now, when the user upload the image it goes directly to cloudinary, and the process returns :image_id. When the user accepts the image, I receive it in my controller like this:
if params[:image_id].present?
preloaded = Cloudinary::PreloadedFile.new(params[:image_id])
raise "Invalid upload signature" if !preloaded.valid?
#model.image_id = preloaded.identifier
end
That image ID gets saved in my database for future retrieve.
Now what if the user uses a tool such as "curl", or any other method that would allow him to modify the returned :image_id before submitting it to the controller ? I will have a wrong value in my database that would be difficult to find and an empty image when I try to show it in my pages. What is the best method of avoiding this ?
Regards,
When using cl_image_upload_tag, the value of params[:image_id] will contain a signature. The signature is validated by PreloadedFile#valid? You can know for sure that this public_id and version were returned by Cloudinary. If needed, you can also verify the version (unix timestamp) is reasonably recent.

Pass XML value from one page other page

I want to pass an XML value from one page to another in a better way.
I am getting this XML value from API:
<hotelist>
<hotel>
<hotelId>109</hotelId>
<hotelName>Hotel Sara</hotelName>
<location>UK,london</location>
</hotel>
<hotel>
<hotelId>110</hotelId>
<hotelName>Radha park</hotelName>
<location>UK,london</location>
</hotel>
<hotel>
<hotelId>111</hotelId>
<hotelName>Hotel Green park</hotelName>
<location>chennai,India</location>
</hotel>
<hotel>
<hotelId>112</hotelId>
<hotelName>Hotel Mauria</hotelName>
<location>paris,France</location>
</hotel>
</hotelist>
I want to pass one hotel:
<hotel>
<hotelId>112</hotelId>
<hotelName>Hotel Mauria</hotelName>
<location>paris,France</location>
</hotel>
to next page.
I am using the Nokogiri gem for parsing XML. For the API next call I have to pass the one hotel to the next page. Which is the best method?
Note: This is just a sample. There are a lot of information bound with the hotel including available room, discount and so on.
So as far as I'm getting this, you are searching for some hotels through a third party service, and then displaying a list. After the user clicks on an item you displaying the detail info
for the hotel.
The easiest way would be having another API endpoint, which can provide the detail information for a specific Hotel id. I guess you're dealing with some really bad API and that's not the case.
There are couple of other options (ordered by complexity level):
There is really not much data and it should fit an simple GET request, so you can just encode the respective hotel information into the URL parameter for the detail
page. Assuming you have set up resourcefull routing and have already parsed the XML into #hotels array of some Hotel models/structs or the like:
<% #hotels.each |hotel| do %>
<% # generates <a href=/hotels/112?hotelName=Hotel+Mauria&location=paris%2C+France'>Hotel Mauria</a>
<%= link_to hotel.hotelName hotel_path(hotel.hotelId, hotelName: hotel.hotelName, location: hotel.location) %>
<% end %>
Encode the info into the respective Hotel DOM elements as data-attributes:
<div class="hotel" data-id="112" data-hotel-name="Mauria" ... />
Then render the detail page on the client side without the server entirely by subscribing to a click event, reading the info stored in the respective data attributes and replace the list with the detail div.
If the third party API is public you could even move the search problem entirely to the client.
Introduce caching of search requests on the server. Then just pick a hotel from the cache
by its id. This would be saving you from doing to much third party requests from your Rails app, which is a weak spot of Rails if deployed on a typical multi-process server.
The simplest way of doing this, would be storing the last search result in a user session, but that
would be probably too memory heavy. If you can expect the hotel information not to change frequently, you could cache it by the query parameters. You could also use some smart caching store like redis and index the entire hotel information, than performing the search on the cache and only in case of the cache miss hit the third party API. But always remember, caching is easy, expiring is hard.
"Everyone should be using low level caching in Rails" could be interesting for implementing a cache.
If you don't mind passing all that information in query parameters:
links = doc.xpath('//hotel').map do |hotel|
hash = Hash.from_xml(hotel.to_xml)
url_for({controller: 'x', action: 'y'}.merge(hash))
# or if you have your link as a string
# "#{link_string}?#{hash.to_param}"
end
If you want to create a link for just one hotel, extract the relevant XML (e.g., using the process described in Uri's answer), and then generate the link as above.
Assuming you have the API XML ready before you render the current page, you could render the relevant hotel data into form fields so that you could post to the next page, something like:
<%= fields_for :hotel do |hf| %>
<% hf.hidden_field :hotelId, value: hash['hotel']['hotelId'] %>
# ...
<% end %>
One optimum way to achieve this is as suggested by Mark Thomas.
However if you still need to pass data between pages you can put all the xml information as a string in a session variable and use it on next page.

How to update avatar/photo of a model directly?

This is rails 3.0.x. I was just wondering how to update an attribute of a model's photo/avatar directly without prompting the user to upload from his/her system?
I am using Paperclip to attach a photo to a model; It has the extension so that a user can use a URL to upload a photo. This works.
Now, I was looking at Canvas2Image, and it returns an image of the canvas so user's can download them.
I was wondering how to use the URL that Canvas2Image returns so I can update the current model's photo directly?
For now, I have this form that prompts the user to update the current model's avatar/photo:
<% form_for(#prototype,
:url=>{:controller=>"prototypes",
:action=>"update"},
:html => { :multipart => true }) do |f| %>
<%= f.file_field :avatar %><br><br>
<%= f.text_field :image_url %><br><br>
<%= submit_tag "Save Prototype" %>
<% end %>
Any help is appreciated, thanks!
From your link to Canvas2Image above it looks to me like you are trying to handle Base64-encoded images (like data:image/octet-stream;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wB).
Paperclip expects a Tempfile object, which is created behind the scenes for you when you upload an image. It is possible to fake a Tempfile by using a StringIO object and adding content_type and original_filename instance methods.
I posted a gist with the code I used to accomplish this in one of my projects. https://gist.github.com/1012107.
For you to do something similar you'd need to remove the beginning portion of the Canvas2Image URIs data:image/octet-stream;base64, and use the remaining Base64 code as the image_data. You'd need a way to provide or work around the content-type and filename.
If you just want to use normal URLs like http://example.com/image.jpg then you have several available options.
You could simply save the URL and use it as an image source on your pages. I know this isn't always an option, and you have no control over the availability of the image.
Otherwise, there are several utilities such as net/http and curl that you could use to download the image. If you think you want to go this route then you might also consider downloading the images using delayed jobs or a similar background process. That way your UI doesn't hang if the image download is slow.
If you want to fetch the gravatar using users' email address you can use the following code snippet.
In a helper:
def avatar_url(user)
gravatar_id = Digest::MD5.hexdigest(user.email.downcase)
return "http://gravatar.com/avatar/#{gravatar_id}.png?s=48&d=#{CGI.escape(default_url)}"
end
In your view:
<%= link_to(image_tag(avatar_url(user)), user_path(user))%>
That's it! You need not to ask user to upload your photo in your system. It will show the photo of his gravatar account. Please let me know whether it solves your problem.
This link might be what you're looking for
However, if you haven't done much work with paperclip yet, I would recommend you to swap to CarrierWave.
The same thing is done with two lines of code:
#prototype.image_remote_url = canvas2image_url #you should change this
#prototype.save

Rails: Obfuscating Image URLs on Amazon S3? (security concern)

To make a long explanation short, suffice it to say that my Rails app allows users to upload images to the app that they will want to keep in the app (meaning, no hotlinking).
So I'm trying to come up with a way to obfuscate the image URLs so that the address of the image depends on whether or not that user is logged in to the site, so if anyone tried hotlinking to the image, they would get a 401 access denied error.
I was thinking that if I could route the request through a controller, I could re-use a lot of the authorization I've already built into my app, but I'm stuck there.
What I'd like is for my images to be accessible through a URL to one of my controllers, like:
http://railsapp.com/images/obfuscated?member_id=1234&pic_id=7890
If the user where to right-click on the image displayed on the website and select "Copy Address", then past it in, it would be the SAME url (as in, wouldn't betray where the image is actually hosted).
The actual image would be living on a URL like this:
http://s3.amazonaws.com/s3username/assets/member_id/pic_id.extension
Is this possible to accomplish? Perhaps using Rails' render method? Or something else? I know it's possible for PHP to return the correct headers to make the browser think it's an image, but I don't know how to do this in Rails...
UPDATE: I want all users of the app to be able to view the images if and ONLY if they are currently logged on to the site. If the user does not have a currently active session on the site, accessing the images directly should yield a generic image, or an error message.
S3 allows you to construct query strings for requests which allow a time-limited download of an otherwise private object. You can generate the URL for the image uniquely for each user, with a short timeout to prevent reuse.
See the documentation, look for the section "Query String Request Authentication Alternative". I'd link directly, but the frame-busting javascript prevents it.
Should the images be available to only that user or do you want to make it available to a group of users (friends)?
In any case if you want to stop hotlinking you should not store the image files under DocumentRoot of your webserver.
If the former, you could store the image on the server as MD5(image_file_name_as_exposed_to_user + logged_in_username_from_cookie). When the user requests image_file_name_as_exposed_to_user, in your rails app, construct the image filename as previously mentioned and then open the file in rails app and write it out (after first setting Content-Type in response header appropriately). This is secure by design.
If the image could be shared with friends, then you should not incorporate username in constructed filename but rest of the advice should work.
This is late in the day to be answering, but another option altogether would be to store the files in MongoDB's GridFS, served through a bit of Rack Middleware that requires auth to be passed. Pretty much as secure as you like, and the URLs don't even need obfuscation.
The other benefit of this is in the availability of the files and the future scalability of the system.
Thanks for your responses, but I'm still skeptical as to whether or not "timing out" the URL from Amazon is a very effective way to go.
I've updated my question above to be a little more clear about what I'm trying to do, and trying to prevent.
After some experimentation, I've come up with a way to do what I want to do in my Rails App, though this solution is not without downsides. Effectively what I've done is to construct my image_tag with a URL that points to a controller, and takes a path parameter. That controller first tests whether or not the user is authorized to see the image, then it fetches the content of the image in a separate request, and stores the content in an instance variable, which is then passed to a repond_to view to return the image, successfully obfuscating the actual image's URL (since that request is made separately).
Cons:
Adds to request time (I feel that the additional time it takes to do this double-request is acceptable considering the privacy this method gives me)
Adds some clutter to views and routes (a small amount, maybe a bit more than I'd like)
If the user is authorized, and tries to access the image directly, the image is downloaded immediately rather than displayed in the browser (anyone know how to fix this? Modify HTTP headers? Only seems to do this with the jpg, though...)
You have to make a separate view for each file format you intend to serve (two for me, jpg and png)
Are there any other cons or considerations I should be aware of with this method? So far what I've listed, I can live with...
(Refactoring welcome.)
application_controller.rb
class ApplicationController < ActionController::Base
def obfuscate_image
respond_to do |format|
if current_user
format.jpg { #obfuscated_image = fetch_url "http://s3.amazonaws.com/#{Settings.bucket}/#{params[:path]}" }
else
format.png { #obfuscated_image = fetch_url "#{root_url}/images/assets/profile/placeholder.png" }
end
end
end
protected
# helps us fetch an image, obfuscated
def fetch_url(url)
r = Net::HTTP.get_response(URI.parse(url))
if r.is_a? Net::HTTPSuccess
r.body
else
nil
end
end
end
views/application/obfuscate_image.png.haml & views/application/obfuscate_image.jpg.haml
= #obfuscated_image
routes.rb
map.obfuscate_image 'obfuscate_image', :controller => 'application', :action => 'obfuscate_image'
config/environment.rb
Mime::Type.register "image/png", :png
Mime::Type.register "image/jpg", :jpg
Calling an obfuscated image
= image_tag "/obfuscate_image?path=#{#user.profile_pic.path}"
The problem you have is that as far as I know you need the images on S3 to be World-readable for them to be accessible. At some point in the process an HTTP GET is going to have to be performed to retrieve the image, which is going to expose the real URL to tools that can sniff HTTP, such as Firebug.
Incidentally, 37signals don't consider this to be a huge problem because if I view an image in my private Backpack account I can see the public S3 URL in the browser address bar. Your mileage may vary...

Resources