Noosfero/noosfero

View on GitHub
app/api/helpers.rb

Summary

Maintainability
D
2 days
Test Coverage
require "base64"
require "tempfile"
require "recaptcha"

module Api
  module Helpers
    PRIVATE_TOKEN_PARAM = :private_token
    ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type, :author_id, :identifier, :archived, :status]
    ALLOWED_KEY_PARAMETERS = {
      Article => [:path]
    }

    include SanitizeParams
    include Noosfero::Plugin::HotSpot
    include ForgotPasswordHelper
    include SearchTermHelper
    include Recaptcha::Adapters::ControllerMethods

    def set_locale
      I18n.locale = (params[:lang] || request.env["HTTP_ACCEPT_LANGUAGE"] || "en")
    end

    def init_noosfero_plugins
      plugins
    end

    def session
      @session ||= Session.find_by(session_id: cookies[:_noosfero_session])
      @session
    end

    def reset_session
      cookies.delete("_noosfero_api_session")
      cookies.delete(:auth_token)
      session.destroy unless session.nil?
      logout
    end

    def set_current_user
      private_token = (params[PRIVATE_TOKEN_PARAM] || headers["Private-Token"] || headers["Authorization"]).to_s
      @current_user ||= User.where(private_token: private_token).includes(:person).first unless private_token.blank?
      @current_user ||= plugins.dispatch("api_custom_login", request).first
      @current_user = session.user if @current_user.blank? && session.present?
      @current_user
    end

    def current_user
      @current_user
    end

    def current_person
      @person ||= current_user.person unless current_user.nil?
      @person
    end

    def is_admin?(environment)
      return false unless current_user

      return current_person.is_admin?(environment)
    end

    def logout
      @current_user = nil
    end

    def environment
      @environment
    end

    def present_partial(model, options)
      if (params[:fields].present?)
        begin
          fields = JSON.parse((params.to_hash[:fields] || params.to_hash["fields"]).to_json)
          if fields.present?
            fields = fields.symbolize_keys
            options.merge!(only: fields[:only]) if fields[:only].present?
            options.merge!(except: fields[:except]) if fields[:except].present?
          end
        rescue
          fields = params[:fields]
          fields = fields.split(",") if fields.kind_of?(String)
          options[:only] = Array.wrap(fields)
        end
      end
      if params[:count].to_s == "true" && model.respond_to?(:size)
        value = { count: model.size }
        present value
      else
        present model, options
      end
    end

    include FindByContents

    ####################################################################
    #### SEARCH
    ####################################################################
    def multiple_search?(searches = nil)
      ["index", "category_index"].include?(params[:action]) || (searches && searches.size > 1)
    end
    ####################################################################

    def logger
      logger = Logger.new(File.join(Rails.root, "log", "#{ENV['RAILS_ENV'] || 'production'}_api.log"))
      logger.formatter = GrapeLogging::Formatters::Default.new
      logger
    end

    def limit
      limit = params[:limit].to_i
      limit = default_limit if limit <= 0
      limit
    end

    def period(from_date, until_date)
      return nil if from_date.nil? && until_date.nil?

      begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
      end_period = until_date.nil? ? DateTime.now : until_date

      begin_period..end_period
    end

    def parse_content_type(content_type)
      return nil if content_type.blank?

      content_type.split(",").map do |content_type|
        content_type.camelcase
      end
    end

    def parse_parent_id(parent_id)
      return nil if parent_id.blank?

      parent_id
    end

    def find_article(articles, params)
      conditions = make_conditions_with_parameter(params, Article)
      article = articles.find_by(conditions)
      not_found! if article.nil?
      article.display_to?(current_person) ? article : forbidden!
    end

    def post_article(asset, params)
      return forbidden! unless current_person.can_post_content?(asset)

      klass_type = params[:content_type] || params[:article].delete(:type) || TextArticle.name
      return forbidden! unless klass_type.constantize <= Article

      if params[:article][:uploaded_data].present?
        params[:article][:uploaded_data] = ActionDispatch::Http::UploadedFile.new(params[:article][:uploaded_data])
      end

      article = klass_type.constantize.new(params[:article])
      article.last_changed_by = current_person
      article.created_by = current_person
      article.profile = asset

      if !article.save
        render_model_errors!(article.errors)
      end
      present_partial article, with: Entities::Article
    end

    def present_article(asset)
      article = find_article(asset.articles, params)
      present_partial article, with: Entities::Article, params: params, current_person: current_person
    end

    def present_articles_for_asset(asset, method_or_relation = "articles")
      articles = find_articles(asset, method_or_relation)
      present_articles(articles)
    end

    def present_articles(articles)
      present_partial paginate(articles), with: Entities::Article, params: params, current_person: current_person
    end

    def find_articles(asset, method_or_relation = "articles")
      articles = select_filtered_collection_of(asset, method_or_relation, params)
      if current_person.present?
        articles = articles.accessible_to(current_person)
      else
        articles = articles.published
      end
      articles
    end

    def find_task(asset, method_or_relation, id)
      task = is_a_relation?(method_or_relation) ? method_or_relation : asset.send(method_or_relation)
      task = task.find_by_id(id)
      not_found! if task.blank?
      current_person.has_permission?(task.permission, asset) ? task : forbidden!
    end

    def post_task(asset, params)
      klass_type = params[:content_type].nil? ? "Task" : params[:content_type]
      return forbidden! unless klass_type.constantize <= Task
      return forbidden! if !current_person.has_permission?(:perform_task, asset)

      task = klass_type.constantize.new(params[:task])
      task.requestor_id = current_person.id
      task.target_id = asset.id
      task.target_type = "Profile"

      if !task.save
        render_model_errors!(task.errors)
      end
      present_partial task, with: Entities::Task
    end

    def find_tasks(asset, method_or_relation = "tasks")
      return forbidden! if !current_person.has_permission?(:perform_task, asset)

      tasks = select_filtered_collection_of(asset, method_or_relation, params)
      tasks = tasks.select { |t| current_person.has_permission?(t.permission, asset) }
      tasks
    end

    def present_task(asset, method_or_relation = "tasks")
      task = find_task(asset, method_or_relation, params[:id])
      present_partial task, with: Entities::Task
      if task.kind_of?(AbuseComplaint)
        present_partial task, with: Entities::AbuseComplaint
      else
        present_partial task, with: Entities::Task
      end
    end

    def present_tasks_for_asset(asset, method_or_relation = "tasks")
      tasks = find_tasks(asset, method_or_relation)
      present_tasks(tasks)
    end

    def present_tasks(tasks)
      present_partial tasks, with: Entities::Task
    end

    ###########################
    #        Activities       #
    ###########################
    def find_activities(asset, method_or_relation = "tracked_notifications")
      not_found! if asset.blank? || asset.secret || !asset.visible
      forbidden! unless asset.display_private_info_to?(current_person)
      if method_or_relation == "activities"
        activities = select_filtered_collection_of(asset, method_or_relation, params)
        activities = activities.map(&:activity)
      else
        activities = select_filtered_collection_of(asset, method_or_relation, params)
      end
      activities
    end

    def present_activities_for_asset(asset, method_or_relation = "tracked_notifications")
      tasks = find_activities(asset, method_or_relation)
      present_activities(tasks)
    end

    def present_activities(activities)
      present_partial activities, with: Entities::Activity, current_person: current_person
    end

    ###########################
    #          Tags           #
    ###########################

    def find_tags(asset, method_or_relation = "tags")
      tags = select_filtered_collection_of(asset, method_or_relation, params)
      tags
    end

    def present_tags_for_asset(asset, method_or_relation = "tags")
      tags = find_tags(asset, method_or_relation)
      present_tags(tags)
    end

    def present_tags(tags)
      present_partial tags, with: Entities::Tag, current_person: current_person
    end

    ###########################
    #      Common Methods     #
    ###########################

    def make_conditions_with_parameter(params = {}, class_type = nil)
      parsed_params = class_type.nil? ? parser_params(params) : parser_params_by_type(class_type, params)
      conditions = {}
      from_date = DateTime.parse(parsed_params.delete(:from)) if parsed_params[:from]
      until_date = DateTime.parse(parsed_params.delete(:until)) if parsed_params[:until]

      conditions[:type] = parse_content_type(parsed_params.delete(:content_type)) unless parsed_params[:content_type].nil?

      conditions[:created_at] = period(from_date, until_date) if from_date || until_date

      conditions[:parent_id] = parse_parent_id(parsed_params.delete(:parent_id)) if parsed_params.key? :parent_id

      conditions.merge!(parsed_params)

      conditions
    end

    # changing make_order_with_parameters to avoid sql injection
    def make_order_with_parameters(params, class_type)
      return_type = class_type == "" ? "" : (class_type.respond_to?(:table_name) ? class_type.table_name + "." : "")

      order = "#{return_type}created_at DESC"
      unless params[:order].blank?
        if params[:order].include?("'") || params[:order].include?('"')
          order = "#{return_type}created_at DESC"
        elsif ["RANDOM()", "RANDOM"].include? params[:order].upcase
          order = "RANDOM()"
        else
          field_name, direction = params[:order].split(" ")
          if !field_name.blank? && class_type
            if class_type.respond_to?(:attribute_names) && (class_type.attribute_names.include? field_name)
              if direction.present? && ["ASC", "DESC"].include?(direction.upcase)
                order = "#{return_type}#{field_name} #{direction.upcase}"
              end
            end
          end
        end
      end
      return order
    end

    def make_timestamp_with_parameters_and_method(params, class_type)
      timestamp = nil
      if params[:timestamp]
        datetime = DateTime.parse(params[:timestamp]).utc
        table_name = class_type.table_name
        date_atrr = class_type.attribute_names.include?("updated_at") ? "updated_at" : "created_at"
        timestamp = "#{table_name}.#{date_atrr} >= '#{datetime}'"
      end

      timestamp
    end

    def by_period(scope, params, class_type, attribute)
      return scope if (class_type == NilClass || class_type.is_a?(String))

      from_param = "from_#{attribute}".to_sym
      until_param = "until_#{attribute}".to_sym
      from_date = DateTime.parse(params.delete(from_param)) if params[from_param]
      until_date = DateTime.parse(params.delete(until_param)) if params[until_param]
      table_name = class_type.table_name

      if class_type.new.is_a?(Event)
        scope = scope.where(" (#{table_name}.#{attribute} >= ?) OR (#{table_name}.#{attribute} iS NULL)", from_date) unless from_date.nil?
        scope = scope.where("#{table_name}.#{attribute} <= ?", until_date) unless until_date.nil?
      else
        scope = scope.where("#{table_name}.created_at >= ?", from_date) if !from_date.nil? && until_date.nil?
        scope = scope.where("#{table_name}.created_at <= ?", until_date) if !until_date.nil? && from_date.nil?
        scope = scope.where("#{table_name}.created_at BETWEEN ? AND ?", from_date, until_date) if !until_date.nil? && !from_date.nil?
      end
      scope
    end

    def by_roles(scope, params)
      role_ids = params[:roles]
      if role_ids.nil?
        scope
      else
        scope.by_role(role_ids)
      end
    end

    def by_reference(scope, params)
      reference_id = params[:reference_id].to_i == 0 ? nil : params[:reference_id].to_i
      if reference_id.nil?
        scope
      else
        created_at = scope.find(reference_id).created_at
        scope.send("#{params.key?(:oldest) ? 'older_than' : 'younger_than'}", created_at)
      end
    end

    def by_categories(scope, params)
      category_ids = params[:category_ids]
      if category_ids.nil?
        scope
      else
        scope.joins(:categories).where(categories: { id: category_ids })
      end
    end

    def select_filtered_collection_of(object, method_or_relation, params)
      conditions = make_conditions_with_parameter(params)
      assoc_class = extract_associated_classname(object, method_or_relation, conditions)

      order = make_order_with_parameters(params, assoc_class)
      timestamp = make_timestamp_with_parameters_and_method(params, assoc_class)

      objects = is_a_relation?(method_or_relation) ? method_or_relation : object.send(method_or_relation)
      objects = by_reference(objects, params)
      objects = by_categories(objects, params)
      objects = by_roles(objects, params)

      [:start_date, :end_date].each { |attribute| objects = by_period(objects, params, assoc_class, attribute) }

      objects = objects.where(conditions).where(timestamp)

      if params[:search].present? || params[:tag].present?
        asset = objects.model.name.underscore.pluralize
        objects = find_by_contents(asset, object, objects, params[:search], { page: 1 }, { tag: params[:tag] })[:results].reorder(order)
      else
        objects = objects.reorder(order)
      end

      params[:page] ||= 1
      params[:per_page] ||= limit
      paginate(objects)
    end

    def authenticate!
      unauthorized! unless current_user
    end

    # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash
    # or a Bad Request error is invoked.
    #
    # Parameters:
    #   keys (unique) - A hash consisting of keys that must be unique
    def unique_attributes!(obj, keys)
      keys.each do |key|
        cant_be_saved_request!(key) if obj.find_by(key.to_s => params[key])
      end
    end

    def attributes_for_keys(keys)
      attrs = {}
      keys.each do |key|
        attrs[key] = params[key] if params[key].present? || (params.has_key?(key) && (params[key] == false))
      end
      attrs
    end

    ##########################################
    #           response helpers             #
    ##########################################

    def not_found!
      render_api_error!("404 Not found", Api::Status::Http::NOT_FOUND)
    end

    def forbidden!
      render_api_error!("403 Forbidden", Api::Status::Http::FORBIDDEN)
    end

    def cant_be_saved_request!(attribute)
      message = _("(Invalid request) %s can't be saved").html_safe % attribute
      render_api_error!(message, Api::Status::Http::BAD_REQUEST)
    end

    def bad_request!(attribute)
      message = _("(Invalid request) %s not given").html_safe % attribute
      render_api_error!(message, Api::Status::Http::BAD_REQUEST)
    end

    def something_wrong!
      message = _("Something wrong happened")
      render_api_error!(message, Api::Status::Http::BAD_REQUEST)
    end

    def unauthorized!
      render_api_error!(_("Unauthorized"), Api::Status::Http::UNAUTHORIZED)
    end

    def not_allowed!
      render_api_error!(_("Method Not Allowed"), Api::Status::Http::METHOD_NOT_ALLOWED)
    end

    def render_api_error!(user_message, status = Api::Status::Http::BAD_REQUEST)
      log_message = "#{status}, User message: #{user_message}"
      logger.error log_message unless Rails.env.test?
      msg = {
        success: false,
        message: user_message,
        code: status
      }
      error!(msg, status)
    end

    def render_model_errors!(active_record_errors)
      message_hash = {}
      if active_record_errors.details
        message_hash[:errors] = active_record_errors.details
        message_hash[:errors].each do |field, errors|
          full_messages = active_record_errors.full_messages_for(field)
          errors.each_with_index { |error, i| error[:full_message] = full_messages[i] }
        end
      end
      error!(message_hash, Api::Status::Http::UNPROCESSABLE_ENTITY)
    end

    protected

      def set_session_cookie
        if @current_user.present? && session.present?
          session.data["user"] = @current_user.id
          session.save!
        end
      end

      def setup_multitenancy
        Noosfero::MultiTenancy.setup!(request.host)
      end

      def detect_stuff_by_domain
        @domain_hash ||= {}
        @domain_hash[request.host] ||= { domain: Domain.by_name(request.host) }
        @domain = @domain_hash[request.host][:domain]
        if @domain.nil?
          @domain_hash[request.host][:environment] ||= Environment.default
          @environment = @domain_hash[request.host][:environment]
          if @environment.nil? && Rails.env.development?
            # This should only happen in development ...
            @environment = Environment.create!(name: "Noosfero", is_default: true)
          end
        else
          @domain_hash[request.host][:environment] ||= @domain.environment
          @environment = @domain_hash[request.host][:environment]
        end
      end

      def filter_disabled_plugins_endpoints
        not_found! if Api::App.endpoint_unavailable?(self, @environment)
      end

      def asset_with_image(params)
        asset_with_custom_image(:image, params)
      end

      def asset_with_custom_image(field, params)
        builder_field = "#{field}_builder".to_sym
        if !params.nil? && params.has_key?(builder_field)
          asset_api_params = params
          asset_api_params[builder_field] = base64_to_uploadedfile(asset_api_params[builder_field])
          return asset_api_params
        end
        params
      end

      def asset_with_images(params)
        return params if params.nil? || !params.has_key?(:images_builder)

        asset_api_params = params
        asset_api_params[:images_builder] = asset_api_params[:images_builder].map do |image_builder|
          image_builder[:tempfile] ? base64_to_uploadedfile(image_builder) : image_builder
        end
        asset_api_params
      end

      def base64_to_uploadedfile(base64_image)
        tempfile = base64_to_tempfile base64_image
        converted_image = base64_image
        converted_image[:tempfile] = tempfile
        return { uploaded_data: ActionDispatch::Http::UploadedFile.new(converted_image) }
      end

      def base64_to_tempfile(base64_image)
        base64_img_str = base64_image[:tempfile]
        decoded_base64_str = Base64.decode64(base64_img_str)
        tempfile = Tempfile.new(base64_image[:filename])
        tempfile.write(decoded_base64_str.encode("ascii-8bit").force_encoding("utf-8"))
        tempfile.rewind
        tempfile
      end

    private

      def extract_associated_classname(object, method_or_relation, conditions)
        if is_a_relation?(method_or_relation)
          method_or_relation.blank? ? "" : method_or_relation.where(conditions).first.class
        else
          object.send(method_or_relation).where(conditions).first.class
        end
      end

      def is_a_relation?(method_or_relation)
        method_or_relation.kind_of?(ActiveRecord::Relation)
      end

      def parser_params(params)
        parsed_params = {}
        params.map do |k, v|
          parsed_params[k.to_sym] = v if ALLOWED_PARAMETERS.include?(k.to_sym)
        end
        parsed_params
      end

      def parser_params_by_type(class_type, params)
        parsed_params = parser_params(params)
        key = params[:key].to_sym if params[:key].present?
        if key.present? && ALLOWED_KEY_PARAMETERS[class_type].include?(key)
          parsed_params[key] = params[:id]
        else
          parsed_params[:id] = params[:id]
        end
        parsed_params
      end

      def default_limit
        20
      end

      def parse_content_type(content_type)
        return nil if content_type.blank?

        content_type.split(",").map do |content_type|
          content_type.camelcase
        end
      end

      def period(from_date, until_date)
        begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
        end_period = until_date.nil? ? DateTime.now : until_date
        begin_period..end_period
      end

      def settings(owner)
        blocks = owner.available_blocks(current_person)
        settings = { available_blocks: blocks }
        settings
      end
  end
end