February 18, 2011

#44 A Contact Form - Implementation

In this post, we'll add some configuration options to our contact_form gem that we created in Post 43. Then we will incorporate the gem into our app.

ContactForm Configuration With Blocks

Before we add our contact_form gem, let's create two configuration options. The first will allow us to set a before filter for our ContactController so that we can require a user to login before contacting us. The second configuration will allow us to set the Form email attribute if we have that information available.

To do this, we will borrow a technique from the RailsAdmin gem. We will create two module methods for ContactForm. Each method will act as both an accessor and a getter depending on whether a block is given to the method. If a block is given, the block is stored. If a block is not given, the stored block is returned. (Depending on your preference, you can write these methods with an explicit block parameter or not.)

In our ContactController, we can reference these configuration options. The trick here is that the blocks that we pass to our configuration methods will contain code that may reference methods defined in ApplicationController, which is defined in our Rails app. Therefore, we need to use #instance_eval to specify the scope/context of the code in the block to reference ContactController (and thus ApplicationController.)

For example, when we call ContactForm.before_filter with a block, that block will be stored as a Proc object in @filter. Later when we call ContactForm.before_filter without a block, the saved Proc object is returned. When this Proc object is called prefixed with an ampersand, '&', the Proc object is turned back into a block. This block is then evaluated in the context of the ContactController instance that is generated by Rails. Thus the code in the block can make reference to instance methods defined in ApplicationController.

contact_form/lib/contact_form.rb, contact_form/app/controllers/contact_form/contact_controller.rb
# contact_form.rb
module ContactForm
  ...
  def self.before_filter(&block)
    @filter = block if block
    @filter || Proc.new {}
  end

  def self.form_email
    @email = Proc.new if block_given?
    @email || Proc.new {}
  end
end
_____________________________________________________

# contact_controller.rb
module ContactForm
  class ContactController < ::ApplicationController
    before_filter :_before_filter

    def new
      @form = ContactForm::Form.new(:email => _email)
      ...

    private

    def _before_filter
      instance_eval &ContactForm.before_filter
    end

    def _email
      instance_eval &ContactForm.form_email
    end

Setting Up ContactFrom

With the ContactForm configurations available, we can add contact_form to our application's Gemfile and install it. (While here, we will upgrade to Rails 3.0.4. BTW: During development of a gem, we can use our local gem source code by specifying a :path option.)

In order to configure ContactForm, we create an initializer file. We will restrict access to our contact form to users who are logged in. (#logged_in? will be defined in ApplicationController.) If the user is not logged in, we redirect him to the login page. Before we redirect, we set a session variable with information that we'll use to redirect the user to the contact form page after he has logged in.

Depending on which strategy the user chooses to login with, we may have the user's email available to us. If so, we'll store that email in another session variable. Then we can populate that field in the form.

Lastly, we overwrite some of the default ContactForm YAML settings.

Gemfile, Terminal, config/initializers/contact_form.rb, config/locales/en.yml
# Gemfile
...
gem 'rails', '3.0.4'
gem 'contact_form', :git => "git://github.com/arailsdemo/contact_form.git"
# gem 'contact_form', :path => "/local/path/to/contact_form"
_____________________________________________________

# Terminal
$ bundle
_____________________________________________________

# contact_form.rb
ContactForm.before_filter do
  unless logged_in?
   session[:redirect_after_login] =
     {
       :url => new_contact_form_url,
       :message => 'Thanks for logging in.'
     }
   redirect_to login_path, :notice => 'Please login first in order to contact us.'
  end
end

ContactForm.form_email do
  session[:email]
end
_____________________________________________________

# en.yml
en:
  contact_form:
    headers:
      to: 'arailsdemo@example.com'
    redirect_url: 'posts_url'

The Controllers

In our ApplicationController, we define the #logged_in? method. A user is logged in if he has a :provider session variable. This variable is set after he has successfully authenticated through OmniAuth (see Post #19.)

In our SessionsController#create action, we save the session[:redirect_after_login] information to a local variable before resetting the session. After saving the relevant user information retrieved by OmniAuth, we can redirect the user to the contact form page.

app/controllers/application_controller.rb and sessions_controller.rb
# application_controller.rb
class ApplicationController < ActionController::Base
  ...
  protected

  def logged_in?
    session[:provider]
  end
_____________________________________________________

# sessions_controller.rb
class SessionsController < ApplicationController
  ...
  def create
    redirect_after_login = session[:redirect_after_login]
    reset_session

    info = request.env["omniauth.auth"]
    session[:email] = info["email"]
    session[:provider] = info["proivider"]
    # store other OmniAuth related user information here

    if redirect_after_login
      redirect_to redirect_after_login[:url],
        :notice => redirect_after_login[:message]
    else
      redirect_to posts_url, :notice => "Welcome #{session[:name]}!"
    end
  end
  ...

  protected

  def handle_unverified_request
    true
  end

CSRF Protection In Rails 3.0.4

Rails 3.0.4 addressed a security vulnerability that could allow CSRF. The authenticity "token will now be required for all non-GET requests." If it is not present, the default behavior will be to reset the session. This affects our ability to persist session information when using certain OmniAuth strategies. For our app, using Google via OpenId would clear our session information. Authenticating through Twitter does not reset the session. (We're using OmniAuth version 0.1.6 here.)

Since we are resetting the session explicitly in our SessionsController, we can remove the call to #reset_session in #handle_unverified_request for that controller (see previous snippet.)

(Another approach to dealing with this issue would be to set the :redirect_after_login information in a cookie and avoid session variables.)

rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
# request_forgery_protection.rb
module ActionController
  module RequestForgeryProtection
    ...
    protected
      # The actual before_filter that is used.  Modify this to change how you handle unverified requests.
      def verify_authenticity_token
        verified_request? || handle_unverified_request
      end

      def handle_unverified_request
        reset_session
      end

Sendgrid

Since our app is on Heroku, we can use the Sendgrid add-on to enable emails to be sent via ActionMailer through Sendgrid. With the add-on in place, our production environment is automatically configured to work properly. However, we will need to setup some settings in development.

When we installed the Sendgrid add-on, it created some Heroku configuration variables which we can find using the 'heroku' command. We can store this information in a YAML file which we can load when our Rails app boots up. (See Railscasts #85.) For convenience, we can use a Hashie::Mash object to access our YAML data instead of a hash object. (The Twitter gem that we have in our Gemfile has a dependency on the Hashie gem, so we didn't have to explicitly include the Hashie gem.)

With our Sendgrid credentials available in development, we manually configure ActionMailer in another initializer file. (Note that we're using port 587 instead of 25.)

The final thing we do is have Rails raise delivery errors in development.

Terminal, config/config.yml, config/initializers/load_config.rb and setup_mail.rb, config/environments/development.rb
# Terminal
$ heroku addons:add sendgrid:free
$ heroku config vars
 SENDGRID_DOMAIN   => your_domain
 SENDGRID_PASSWORD => your_password
 SENDGRID_USERNAME => your_username
_____________________________________________________

# config.yml
development:
  sendgrid:
    domain: your_domain
    password: your_password
    user_name: your_username
_____________________________________________________

# load_config.rb
CONFIG = Hashie::Mash.new(YAML.load_file("#{Rails.root}/config/config.yml")[Rails.env])
_____________________________________________________

# setup_mail.rb
if Rails.env.development?
  ActionMailer::Base.smtp_settings = {
    :address        => "smtp.sendgrid.net",
    :port           => 587,
    :authentication => :plain,
    :user_name      => CONFIG.sendgrid.username,
    :password       => CONFIG.sendgrid.password,
    :domain         => CONFIG.sendgrid.domain
  }
end
_____________________________________________________

# development.rb
Arailsdemo::Application.configure do
  config.action_mailer.raise_delivery_errors = true

The View

The only thing left now is to include a link to our contact form. With some CSS, we can make this link into a tab.
app/views/shared/_side_bar.html.haml, public/stylesheets/sass/application.sass
-# _side_bar.html.haml
%h3#contact= link_to 'Contact', new_contact_form_url

_____________________________________________________
# application.sass

#contact
  :position fixed
  :right -22px
  :right -42px\9   // target IE8 and below
  :*right 0       // target IE7 and below
  :top 45%
  :display block
  :background $logo
  :height 25px
  :padding 0 6px 2px
  :z-index 10001
  :border inset 1px $rust
  :border-bottom none
  +transform(1, -90deg)
  :filter progid:DXImageTransform.Microsoft.BasicImage(rotation=3)
  a
    :color $bkg1
    :font
      :size 16px
  a:hover
    :background none
#contact:hover
  :height 30px

    Comments

  1. Hi congrats for your blog it's very useful. I'd like to know would manage race-conditions in rails, here is an example http://stackoverflow.com/questions/4939349/find-or-create-and-race-condition-in-rails-theory-and-production/4940469#4940469
    By Zuigi M Feb 20, 2011 04:25
  2. Thanks. I'm glad you like it. About race conditions, I'm not too familiar with the issue, but maybe this will help http://guides.rubyonrails.org/active_record_querying.html#locking-records-for-update
    By aRailsDemo Feb 20, 2011 20:29
  3. The part about CSRF Protection in Rails 3.0.4 is CRITICAL! I was trying to log in with Google and found myself logged out after the redirect. Finally figured out the cause was in Devise when handle_unverified_request is called the session is destroyed. Not sure what the permanent fix is but your method saved me.
    By Jason Thomas Feb 21, 2011 21:54

(Please login to submit comments.)



View All Posts