Report abuse


			
#--
# Copyright (c) 2007 Chris Taggart
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
module FacebookUtilities
FACEBOOK_API_KEY     = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
FACEBOOK_API_SECRET  = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
FACEBOOK_API_URL     = "api.facebook.com"
FACEBOOK_API_PATH    = "/restserver.php"
FACEBOOK_API_VERSION = "1.0"

# The Facebook class handles the meat of the interaction with Facebook.
# 
# Unless it makes sense not to I've tended to stay close to the PHP library's functionality, although obviously not the code structure.
# Where I haven't it's because: it didn't make sense; I didn't need that bit of funcionlity yet; I forgot; or some other arbitrary explanation
# 
# Facebook methods are called by making them a bit ruby-ish, and preceding them with "fb_". So to call users.isAppAdded
# we use the method #fb_users_is_app_added (we use the fb_ prefix in order to use method_missing responsibly, which in turn saves masses of code).
# NB In the case where the facebook method has a word in all caps, e.g. profile.setFBML, you can either call it with fb_profile_set_fBML, or the 
# slightly less unattractive fb_profile_set_FBML
# 
# If a call to the Facebook API returns an error response from Facebook (see http://wiki.developers.facebook.com/index.php/Error_codes) a
# FacebookUtilities::Facebook::FacebookApiError exception is raised and the error logged using the Rails default logger. How your app handles 
# it is up to you :-)

  class Facebook
    attr_reader   :session_key, :user, :fb_params

    # Define custom error class for Facebook API errors
    class FacebookApiError < StandardError  
    end

    # We initialize with given params hash, extracting the fb_params from it and using them to set session key and facebook user
    def initialize(params={})
      @fb_params   = get_fb_params(params)
      @session_key = @fb_params[:session_key]
      @user        = @fb_params[:user]
      get_new_session(params[:auth_token]) if params[:auth_token]&&!@session_key
    end

    # Returns whether user has added this application
    def added_app?
      @fb_params[:added] == "1"
    end

    # Returns the url to add the application to the user's list of apps
    def add_app_url(options={})
      "http://www.facebook.com/add.php?api_key=#{FACEBOOK_API_KEY}" + (options[:next] ? "&next=#{CGI.escape(options[:next])}" : "")
    end

    # Returns the login url for this application. Various params can be passed through the options hash. If these have a 
    # string as a value then that string is URL-escaped, if it is false the param is ingored, otherwise it's just added as 
    # a key=value pair
    def login_to_app_url(options={})
      base_url = "http://www.facebook.com/login.php?v=#{FACEBOOK_API_VERSION}&api_key=#{FACEBOOK_API_KEY}"
      options.each { |k,v| (base_url += "&#{k}=#{v.is_a?(String) ? CGI.escape(v) : v}") if v } 
      base_url
    end

    # Converts the rubyish method into Facebook method, makes the request and processes the XML returned
    def call(method, params={})
      split_method = method.to_s.split("_",2)
      fb_method = "facebook.#{split_method.first}.#{split_method[1].camelize(:lower)}"
      response = post_request(fb_method, params)
      RAILS_DEFAULT_LOGGER.debug "****Facebook method #{fb_method} called with #{params.inspect}. \n Response: #{response.inspect}" # Helpful when trying to track down problems
      xml = XmlSimple.xml_in(response)
      RAILS_DEFAULT_LOGGER.debug "***Facebook API error: #{xml.inspect}" and raise FacebookApiError if xml["error_code"]
      xml
    end

    # Returns only the facebook parameters from the parameters hash (these are the ones starting with 'fb_sig_'). Also renames the keys to these parameters by removing the 'fb_sig_'.
    # Checks we have valid facebook params by verifying the signature and also checking the params are not stale (>48 hours old). Returns an empty hash if no fb_parameters or if
    # signature is invalid or if params are stale
    def get_fb_params(orig_params={})
      new_params = {}
      orig_params.each { |k,v| m = /^fb_sig_(.+)/.match(k.to_s); new_params.update({m[1].to_sym => v}) if m&&m[1]}
      new_params[:time]&&(Time.at(new_params[:time].to_f) >= 48.hours.ago)&&verify_signature(new_params, orig_params[:fb_sig]) ? new_params : {}
    end

    # Gets a new session given an auth code
    def get_new_session(auth_token)
      response = fb_auth_get_session(:auth_token => auth_token)
      @session_key = response["session_key"].first
      @user = response["uid"].first
    end

    # Returns true if we're in a facebook canvas
    def in_canvas?
      @fb_params[:in_canvas]
    end

    # Returns true if we're in a facebook iframe
    def in_iframe?
      @fb_params[:in_iframe]
    end

    # Returns true if we're in any sort of facebook frame -- canvas or iframe
    def in_frame?
      in_canvas? || in_iframe?
    end

    # Checks whether user is logged into app. If they are there will be a session and a user. Returns user id if true, false otherwise
    def logged_in_to_app?
      @session_key&&@user
    end

    # Build the required parameters for a Facebook API call, including the signature, and calls post_http_request to submit request to Facebook
    def post_request(method, params)
      post_params = { "api_key" => FACEBOOK_API_KEY, 
                      "call_id" => Time.now.to_f.to_s, 
                      "method" => method, 
                      "v" => FACEBOOK_API_VERSION,
                      "session_key" => session_key }.merge(params.stringify_keys) # Net::HTTP (and Facebook prob) expects keys to be strings
      post_http_request(FACEBOOK_API_URL, FACEBOOK_API_PATH, post_params.merge({ "sig" => signature_from(post_params) } ))
    end

    # Generates a facebook signature from the params hash passed
    def signature_from(params={})
      request_str = params.collect {|p| "#{p[0].to_s}=#{p[1]}"}.sort.join # build key value pairs, sort in alpha order then join them
      return Digest::MD5.hexdigest("#{request_str}#{FACEBOOK_API_SECRET}")
    end

    # Verifies that the signature given matches the signature that would be generated for the passed parameters
    def verify_signature(params, given_signature)
      given_signature == signature_from(params)
    end

    protected
    # Separating this out makes mocking easier
    def post_http_request(url, path, params)
      return false if RAILS_ENV=="test" #make sure we don't call make calls to external services. Mock this method to simulate response instead
      http_server = Net::HTTP.new(url, 80)
      http_request = Net::HTTP::Post.new(path)
      http_request.form_data = params
      RAILS_DEFAULT_LOGGER.debug "****facebook API call: #{params.inspect}"
      http_server.start{|http| http.request(http_request)}.body      
    end

    private
    # All API methods are implemented with this method. It extracts the remote method API name
    # based on the ruby method name. That method should be preceded with fb_ (we don't want just
    # any unknown method scooped up -- makes bug-tracking v difficult). So we call the Facebook method
    # users.isAppAdded by the more rubyish fb_users_is_app_added 
    def method_missing(unknown_method_name, *args)
      if match = /fb_(.+)/.match(unknown_method_name.to_s)
        call(match[1], *args)
      else 
        super
      end
    end    
  end

  # #
  # This module adds extra functionality to Rails controllers. In particular it adds an interface to the 
  # Facebook object, and adds some private methods which make it all a bit easier. Simply "include FacebookUtilities::ControllerUtilities" 
  # in the controllers you want to eb facebook aware (or in the application controller if you want all of them to have access to these methods)
  # #
  module ControllerUtilities
    private

    # provides interface to facebook object, instantiating with the params hash if it does not yet exist
    def facebook
      @facebook ||= FacebookUtilities::Facebook.new(params)
    end

    # Provides redirection to given facebook page (e.g. to login to app or to facebook itself).
    # If we are in an fb_canvas page we perform redirection by returning text in the form of '' 
    def fb_redirect_to(url)
      if facebook.in_canvas?
        render :text => "" 
      elsif url=~/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i
        render :text => ""
      else
        redirect_to url
      end
    end

    # Call this method with a before_filter for actions that need facebook user to have added your application
    def require_added_fb_app
      return true if facebook.added_app?
      fb_redirect_to facebook.add_app_url 
    end

    # Call this method with a before_filter for actions that need facebook user to be logged into your application (i.e. where you need a facebook session and/or their user id)
    def require_logged_in_to_fb_app
      return true if facebook.logged_in_to_app?
      fb_redirect_to facebook.login_to_app_url(:canvas => facebook.in_frame?)
    end

    # Call this method with a before_filter for actions that need can only be accessed from within a facebook frame
    def require_fb_frame
      return true if facebook.in_frame?
      redirect_to facebook.login_to_app_url
    end    
  end  
end