privly/privly-web

View on GitHub
app/controllers/posts_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# Posts are the central storage endpoint for Privly content. They optionally
# store cleartext markdown content and serialized JSON of any schema. Currently
# two posting applications use the Post endpoint: ZeroBins push encrypted content
# to the serialized JSON storage, and Privly "posts" use the rendered Markdown
# storage.
class PostsController < ApplicationController
  
  require 'csv'
  
  # Force the user to authenticate using Devise
  before_filter :authenticate_user!, :except => [:show, :update, 
                                                 :destroy, :user_account_data]
  
  # Determines whether the user has access to the 
  # resource and assigns the @post variable
  before_filter :load_and_authorize_resource, :except => [:destroy_all, 
    :user_account_data, :index, :create]
  
  # Obscure whether the record exists when not found
  rescue_from ActiveRecord::RecordNotFound do |exception|
    obscure_existence
  end
  
  # == Get the Index
  # 
  # Get a list of all the user's posts. The user must be authenticated.
  # This listing should only be used by a privly-application so it is
  # data-only (JSON).
  #  
  # ==== Routing  
  #
  # +GET+: /posts
  # +GET+: /posts.:format
  #
  # ==== Cookies
  #
  # User must be authenticated via a session cookie
  #
  # ==== Formats  
  #  
  # * +html+ (deprecated)
  # * +json+ Example:
  #          [{"created_at":"2012-09-05T04:08:31Z",
  #            "burn_after_date":"2012-09-19T04:08:31Z",
  #            "public":false,"updated_at":"2012-09-05T04:08:31Z",
  #            "structured_content":{"salt":"ytyzBr2OkEc",
  #            "iv":"RSBeCnAklAbi0qvq/P8twA","ct":"23hqJJ7QKNkxpLVtfp9uEg"},
  #            "id":149,"user_id":2,"content":null,"random_token":"a53642b006"}]
  # * +jsonp+
  # * +csv+ Returns a comma separated value file. Headers:
  #      content,created_at,updated_at,public
  #
  # ==== Parameters  
  # 
  # * *format* - _string_ - Optional
  # ** Values: html, json
  # ** Default: html
  #
  def index
    
    @posts = current_user.posts.all
    
    respond_to do |format|
      format.html {
        redirect_to "/apps/Index/new.html"
      }
      format.json {
        render :json => @posts.to_json(:methods => :privly_URL) }
      format.csv {
        @filename = "posts_" + Time.now.strftime("%m-%d-%Y") + ".csv"
        csv_data = CSV.generate do |csv|
          csv << ["content", "created_at", "updated_at", "public"]
          @posts.each do |post|
            csv << [post.content, post.created_at, post.updated_at, post.public]
          end
        end
        send_data csv_data, :type => 'text/csv; charset=iso-8859-1; header=present',
          :disposition => "attachment; filename=#{@filename}"
      }
    end
  end
  
  # == Shows an individual post.
  #
  # This endpoint is data-only, meaning you should only use the
  # JSON format. Privly-applications integrate with this endpoint
  # using the JSON format.
  #  
  # === Routing  
  #
  # +GET+: /posts
  # +GET+: /posts/:id.:format
  #
  # === Formats  
  #  
  # * +html+ (deprecated) 
  # * +json+ Example:
  #          {"created_at":"2012-09-05T04:08:31Z",
  #            "burn_after_date":"2012-09-19T04:08:31Z",
  #            "public":false,"updated_at":"2012-09-05T04:08:31Z",
  #            "structured_content":{"salt":"ytyzBr2OkEc",
  #            "iv":"RSBeCnAklAbi0qvq/P8twA","ct":"23hqJJ7QKNkxpLVtfp9uEg"},
  #            "id":149,"user_id":2,"content":null,"random_token":"a53642b006",
  #            "permissions":
  #                {canshow: true, canupdate: false, candestroy: false, 
  #                canshare: false}
  #          }
  # * +jsonp+
  #
  # === Parameters  
  #
  # <b>random_token</b> - _string_ - Required
  # * Values: Any string of non-whitespace characters
  # * Default: None 
  # Either the user owns the post, or they must supply this parameter.
  # Without this parameter the user will not be able to access this endpoint.
  #
  # *format* - _string_ - Optional
  # * Values: html, json
  # * Default: html
  #
  # === Response Headers
  # * +X-Privly-Url+ The URL for this content which should be posted to other
  # websites.
  def show
    
    # Count the number of permissioned requests the post has.
    # Note that users could use this to indicate the number of times content
    # has been read. Do not expose this lightly.
    if not @post.user.nil?
      User.increment_counter(:permissioned_requests_served, @post.user.id)
    end
    
    @injectable_url = @post.privly_URL
    response.headers["X-Privly-Url"] = @injectable_url
    
    respond_to do |format|
      format.html {
        redirect_to @injectable_url # Deprecated
        return
      }
      format.json {
        render :json => get_json, :callback => params[:callback]
      }
    end
  end
  
  # == Create a post.
  #  
  # === Routing  
  #
  # Create a post
  # POST /posts
  # POST /posts.:format
  #
  # === Formats  
  #  
  # * +html+
  # * +json+
  # * +jsonp+
  #
  # === Parameters  
  #
  # <b>post [content]</b> - _string_ - Optional
  # * Values: Any Markdown formatted string. No images supported.
  # * Default: nil
  # The content is rendered on the website, or for injection into web pages.
  #
  # <b>post [structured_content]</b> - _JSON_ - Optional
  # * Values: Any JSON document
  # * Default: nil
  # Structured content is for the storage of serialized JSON in the database.
  #
  # <b>post [privly_application]</b> - string - Optional
  # * Values: Any of the currently supported Privly application identifiers can
  # be set here. Current examples include "PlainPost" and "ZeroBin", but no
  # validation is performed on the string. It is only used to generate URLs
  # into the static folder of the server.
  # * Default: nil
  #
  # <b>post [public]</b> - _boolean_ - Optional
  # * Values: true, false
  # * Default: nil
  # A public post is viewable by any user.
  #
  # <b>post [random_token]</b> - _string_ - Optional
  # * Values: Any string
  # * Default: A random sequence of Base64 characters
  # The random token is used to permission requests to content
  # not owned by the requesting user. It ensures the user has access to the link,
  # and not didn't crawl the resource identifiers.
  #
  # <b>post [seconds_until_burn]</b> - _integer_ - Optional
  # * Values: 1 to 99999999
  # * Default: nil
  # The number of seconds until the post is destroyed.
  # If this parameter is specified, then the burn_after_date
  # is ignored.
  #
  # <b>post [burn_after_date(1i)]</b> - _integer_ - Required
  # * Values: 2012
  # * Default: 2012
  # The year in which the content will be destroyed
  #
  # <b>post [burn_after_date(2i)]</b> - _integer_ - Required
  # * Values: 1 to 12
  # * Default: current month
  # The month in which the content will be destroyed
  #
  # <b>post [burn_after_date(3i)]</b> - _integer_ - Required
  # * Values: 1 to 31
  # Default: Defaults to two days from now if the user
  # is not logged in, otherwise it defaults to 14 days from now
  # The day after which the content will be destroyed. The combined day, 
  # month, and year must be within the next 14 days for users with
  # posting permission, or 2 days for users without posting permission.
  #
  # === Response Headers
  # * +X-Privly-Url+ The URL for this content which should be posted to other
  # websites.
  def create
    
    unless current_user.can_post
      redirect_to welcome_page_path
    end
    
    @post = Post.new
    @post.user = current_user
    @post.privly_application = params[:post][:privly_application]

    # Posts default to Private
    if params[:post][:public]
      @post.public = params[:post][:public]
    else
      @post.public = false
    end

    if params[:post][:structured_content]
      @post.structured_content = params[:post][:structured_content]
    end

    set_burn_date
    
    # The random token will be required for users other than the owner
    # to access the content. The model will generate a token before saving
    # if it is not assigned here.
    @post.random_token = params[:post][:random_token]
    
    @post.update_attributes(post_params_create)
    
    respond_to do |format|
      if @post.save
        response.headers["X-Privly-Url"] = @post.privly_URL
        format.any { render :json => get_json, 
          :status => :created, :location => @post }
      else
        format.any { render :json => @post.errors, 
          :status => :unprocessable_entity }
      end
    end
  end
  
  # == Update a post.
  #
  # Requires update permission. 
  #
  # === Routing  
  #
  # Create a post
  # PUT /posts/:id
  # PUT /posts/:id.:format
  #
  # === Formats  
  #  
  # * +html+
  # * +json+
  # * +jsonp+
  #
  # === Parameters  
  #
  # <b>id</b> - _integer_ - Required
  # * Values: 0 to 9999999
  # * Default: None 
  # The identifier of the post.
  #
  # <b>random_token</b> - _string_ - Required
  # * Values: Any string of non-whitespace characters
  # * Default: None 
  # Either the user owns the post, or they must supply this parameter.
  # Without this parameter the user will not be able to access this endpoint.
  #
  # <b>post [content]</b> - _string_ - Optional
  # * Values: Any Markdown formatted string. No images supported.
  # * Default: nil 
  # The content is rendered on the website, or for injection into web pages.
  #
  # <b>post [structured_content]</b> - _JSON_ - Optional
  # * Values: Any JSON document
  # * Default: nil
  # Structured content is for the storage of serialized JSON in the database.
  #
  # <b>post [public]</b> - _boolean_ - Optional
  # * Values: true, false
  # * Default: nil
  # A public post is viewable by any user.
  #
  # <b>post [random_token]</b> - _string_ - Optional
  # * Values: Any string
  # * Default: A random sequence of Base64 characters
  # The random token is used to permission requests to content
  # not owned by the requesting user. It ensures the user has access to the link,
  # and not didn't crawl the resource identifiers.
  #
  # <b>post [seconds_until_burn]</b> - _integer_ - Optional
  # * Values: 1 to 99999999
  # * Default: nil
  # The number of seconds until the post is destroyed.
  # If this parameter is specified, then the burn_after_date
  # is ignored. Requires destroy permission.
  #
  # <b>post [burn_after_date(1i)]</b> - _integer_ - optional
  # * Values: 2012
  # * Default: 2012
  # The year in which the content will be destroyed
  # Requires destroy permission.
  #
  # <b>post [burn_after_date(2i)]</b> - _integer_ - optional
  # * Values: 1 to 12
  # * Default: current month
  # The month in which the content will be destroyed
  # Requires destroy permission.
  #
  # <b>post [burn_after_date(3i)]</b> - _integer_ - optional
  # * Values: 1 to 31
  # Default: Defaults to two days from now if the user
  # is not logged in, otherwise it defaults to 14 days from now
  # The day after which the content will be destroyed. The combined day, 
  # month, and year must be within the next 14 days for users with
  # posting permission, or 2 days for users without posting permission.
  #
  # === Response Headers
  # * +X-Privly-Url+ The URL for this content which should be posted to other
  # websites.
  def update
    
    unless current_user == @post.user
      return
    end
    
    unless params[:post][:public].nil?
      @post.public = params[:post][:public]
    end
    
    unless params[:post][:random_token].nil?
      @post.random_token = params[:post][:random_token]
    end

    if params[:post][:structured_content]
      @post.structured_content = params[:post][:structured_content]
    end

    set_burn_date
    
    respond_to do |format|
      if @post.update_attributes(post_params_update)
        format.json { render :json => get_json, :callback => params[:callback] }
      else
        format.json { render :json => @post.errors,
          :status => :unprocessable_entity }
      end
    end
  end
  
  # == Destroy a post.
  #
  # Requires destroy permission, or content ownership.
  #
  # === Routing  
  #
  # Destroy a post
  # DELETE /posts/:id
  # DELETE /posts/:id.:format
  #
  # === Formats  
  #  
  # * +html+
  # * +json+
  # * +jsonp+
  #
  # === Parameters  
  #
  # <b>id</b> - _integer_ - Required
  # * Values: 0 to 9999999
  # * Default: None 
  # The identifier of the post.
  #
  # <b>random_token</b> - _string_ - Required
  # * Values: Any string of non-whitespace characters
  # * Default: None 
  # Either the user owns the post, or they must supply this parameter.
  # Without this parameter the user will not be able to access this endpoint.
  def destroy
    @post.destroy
    respond_to do |format|
      format.html { 
        if user_signed_in? and current_user.can_post
          redirect_to posts_url
        else
          redirect_to root_url
        end
        }
      format.json { head :ok }
    end
  end
  
  # == Destroy all of the User's owned posts.
  #
  # Requires content ownership.
  #
  # === Routing  
  #
  # Destroy a post
  # DELETE /posts/destroy_all
  #
  # === Formats  
  #  
  # * +html+
  #
  # === Parameters  
  #
  # <b>No Parameters</b>
  def destroy_all
    posts = current_user.posts

    posts.each do |post|
      post.destroy
    end

    redirect_to posts_url, :notice => "Destroyed all Posts."
  end
  
  # == Get User Account Data
  #
  # Returns JSON with CSRF token, and information about the
  # user account's current permissions.
  # This should only be called by posting applications
  # before submitting forms. This endpoint is included to make it easier to
  # generate applications without rendering a template.
  #
  # Elements of the JSON include:  
  #
  # csrf: The CSRF if a token which is expected in all posts to the
  # content server. This is a counter measure for Cross Site Request
  # forgery.
  #
  # burntAfter: The maximum lifetime of posts for the current user
  #
  # canPost: Whether or not the user can create content
  #
  # signedIn: Boolean indicating whether the user us signed into the server
  #
  # === Routing  
  #
  # GET /posts/user_account_data
  #
  # === Formats  
  #  
  # * +json+
  # * +jsonp+
  #
  # === Parameters  
  #
  # <b>No Parameters</b>
  def user_account_data
    
    render :json => {
                      :csrf => form_authenticity_token,
                      :burntAfter => Time.now + 30.days, # Current recommended max life
                      :canPost => user_signed_in?,
                      :signedIn => user_signed_in?
                     }, 
                     :callback => params[:callback]
  end
  
  private
    
    # Load the post if the user has access to it
    def load_and_authorize_resource
      user = current_user
      user ||= User.new # guest user (not logged in)
      
      post = Post.find(params[:id])
      
      # If the post will be destroyed in the next cron job, tell the user
      # it is already gone.
      if not post.burn_after_date.nil? and post.burn_after_date < Time.now
        obscure_existence
        return
      end
      
      if post.user == current_user
        @post = post
        return
      end
      
      if post.public and post.random_token == params[:random_token]
        @post = post
        return
        # has access
      end
      
      obscure_existence
      
    end
    
    # This helper gives a JSON document containing only the 
    # attributes the requestor has access to
    def get_json
      
      if not @post.user.nil? and @post.user == current_user
        post_json = @post.as_json
      else
        post_json = @post.as_json(:except => [:user_id, :updated_at, 
           :created_at])
      end
      
      if not @post.content.nil?
        post_json.merge!(
        :rendered_markdown => @post.content.safe_markdown)
      end
      
      permissioned = (@post.user == current_user)
      injectable_url = @post.privly_URL
      post_json.merge!(
         "X-Privly-Url" => injectable_url, 
         :permissions => {
           :canshow => true, 
           :canupdate => permissioned, 
           :candestroy => permissioned,
           :canshare => permissioned
           }
          )
      post_json
    end
    
    # Converts rails 3-part date form object to a Ruby Date object
    def convert_date(hash, date_symbol_or_string)
      attribute = date_symbol_or_string.to_s
      return Date.new(hash[attribute + '(1i)'].to_i, hash[attribute + '(2i)'].to_i, hash[attribute + '(3i)'].to_i)   
    end
    
    # Set the burn date on the model.
    # The user must have destroy permissions.
    # The burn_after_date(1i) parameter has higher precedence than the
    # seconds_until_burn parameter.
    def set_burn_date
      
      unless @post.user == current_user
        return
      end
      
      if params[:post]["burn_after_date(1i)"]
        @post.burn_after_date = convert_date params[:post], "burn_after_date"
        return
      end
      
      if params[:post][:seconds_until_burn]
        seconds_until_burn = params[:post][:seconds_until_burn]
        if seconds_until_burn == "" or seconds_until_burn == "nil"
          @post.burn_after_date = nil
        else
          seconds_until_burn = params[:post][:seconds_until_burn].to_i
          @post.burn_after_date = Time.now + seconds_until_burn.seconds
        end
      end
      
    end

    def post_params_create
      #all_structured_content = params.require(:post).fetch(:structured_content, nil).try(:permit!)
      params.require(:post).permit(
        :content,
        :public,
        :privly_application)#.merge(:structured_content => all_structured_content)
    end

    def post_params_update
      #all_structured_content = params.require(:post).fetch(:structured_content, nil).try(:permit!)
      params.require(:post).permit(
        :content)#.merge(:structured_content => all_structured_content)
    end

end