sharetribe/sharetribe

View on GitHub
app/controllers/application_controller.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'will_paginate/array'

class ApplicationController < ActionController::Base

  module DefaultURLOptions
    # Adds locale to all links
    def default_url_options
      { :locale => I18n.locale }
    end
  end

  include ApplicationHelper
  include IconHelper
  include DefaultURLOptions
  include Analytics
  include RefererHider
  include HSTS::Concern
  include EnsureAdmin
  protect_from_forgery
  layout 'application'

  before_action :check_http_auth,
    :check_auth_token,
    :fetch_community,
    :fetch_community_plan_expiration_status,
    :perform_redirect,
    :fetch_logged_in_user,
    :initialize_feature_flags,
    :save_current_host_with_port,
    :fetch_community_membership,
    :redirect_removed_locale,
    :set_locale,
    :redirect_locale_param,
    :setup_seo_service,
    :fetch_community_admin_status,
    :warn_about_missing_payment_info,
    :set_homepage_path,
    :maintenance_warning,
    :cannot_access_if_banned,
    :cannot_access_without_confirmation,
    :ensure_consent_given,
    :ensure_user_belongs_to_community,
    :set_display_expiration_notice,
    :setup_intercom_user,
    :setup_custom_footer,
    :disarm_custom_head_script

  # This updates translation files from WTI on every page load. Only useful in translation test servers.
  before_action :fetch_translations if APP_CONFIG.update_translations_on_every_page_load == "true"

  helper_method :root, :logged_in?, :current_user?, :get_full_locale_name

  attr_reader :current_user

  def redirect_removed_locale
    if params[:locale] && Rails.application.config.REMOVED_LOCALES.include?(params[:locale])
      fallback = Rails.application.config.REMOVED_LOCALE_FALLBACKS[params[:locale]]
      redirect_to_locale(fallback, :moved_permanently)
    end
  end

  def set_locale
    user_locale = Maybe(@current_user).locale.or_else(nil)

    # We should fix this -- START
    #
    # There are a couple of controllers (amazon ses bounces, etc.) that
    # inherit application controller, even though they shouldn't. ApplicationController
    # has a lot of community specific filters and those controllers do not have community.
    # Thus, we need to add this kind of additional logic to make sure whether we have
    # community or not
    #
    m_community = Maybe(@current_community)
    community_locales = m_community.locales.or_else([])
    community_default_locale = m_community.default_locale.or_else("en")
    community_id = m_community[:id].or_else(nil)

    I18nHelper.initialize_community_backend!(community_id, community_locales) if community_id

    # We should fix this -- END

    locale = I18nHelper.select_locale(
      user_locale: user_locale,
      param_locale: params[:locale],
      community_locales: community_locales,
      community_default: community_default_locale,
      all_locales: Sharetribe::AVAILABLE_LOCALES
    )

    raise ArgumentError.new("Locale #{locale} not available. Check your community settings") unless available_locales.collect { |l| l[1] }.include?(locale)

    I18n.locale = locale
    @facebook_locale_code = I18nHelper.facebook_locale_code(Sharetribe::AVAILABLE_LOCALES, locale)

    # Store to thread the service_name used by current community, so that it can be included in all translations
    ApplicationHelper.store_community_service_name_to_thread(service_name)

    # A hack to get the path where the user is
    # redirected after the locale is changed
    new_path = request.fullpath.dup
    new_path.slice!("/#{params[:locale]}")
    new_path.slice!(0,1) if new_path =~ /^\//
    @return_to = new_path

    Maybe(@current_community).each { |community|
      @community_customization = community.community_customizations.where(locale: locale).first
    }
  end

  def set_homepage_path
    present = ->(x) { x.present? }

    @homepage_path =
      case [@current_community, @current_user, params[:locale]]
      when matches([nil, __, __])
        # FIXME We still have controllers that inherit application controller even though
        # they do not have @current_community
        #
        # Return nil, do nothing, but don't break
        nil

      when matches([present, nil, present])
        # We don't have @current_user.
        # Take the locale from URL param, and keep it in the URL if the locale
        # differs from community default
        if params[:locale] != @current_community.default_locale.to_s
          homepage_with_locale_path
        else
          homepage_without_locale_path(locale: nil)
        end

      else
        homepage_without_locale_path(locale: nil)
      end
  end


  # If URL contains locale parameter that doesn't match with the selected locale,
  # redirect to the selected locale
  def redirect_locale_param
    param_locale_not_selected = params[:locale].present? && params[:locale] != I18n.locale.to_s

    redirect_to_locale(I18n.locale, :temporary_redirect) if param_locale_not_selected
  end

  def redirect_to_locale(new_locale, status)
    if @current_community.default_locale == new_locale.to_s
      redirect_to url_for(params.to_unsafe_hash.symbolize_keys.except(:locale).merge(only_path: true)), :status => status
    else
      redirect_to url_for(params.to_unsafe_hash.symbolize_keys.merge(locale: new_locale, only_path: true)), :status => status
    end
  end

  #Creates a URL for root path (i18n breaks root_path helper)
  def root
    ActiveSupport::Deprecation.warn("Call to root is deprecated and will be removed in the future. Use search_path or landing_page_path instead.")
    "#{request.protocol}#{request.host_with_port}/#{params[:locale]}"
  end

  def fetch_logged_in_user
    if person_signed_in?
      @current_user = current_person
      setup_logger!(user_id: @current_user.id, username: @current_user.username)
    end
  end

  def initialize_feature_flags
    # Skip this if there is no current marketplace.
    # This allows to avoid skipping this filter in many places.
    return unless @current_community

    FeatureFlagHelper.init(community_id: @current_community.id,
                           user_id: @current_user&.id,
                           request: request,
                           is_admin: Maybe(@current_user).is_admin?.or_else(false),
                           is_marketplace_admin: Maybe(@current_user).is_marketplace_admin?(@current_community).or_else(false))
  end

  # Ensure that user accepts terms of community and has a valid email
  #
  # When user is created through Facebook, terms are not yet accepted
  # and email address might not be validated if addresses are limited
  # for current community. This filter ensures that user takes these
  # actions.
  def ensure_consent_given
    # Not logged in
    return unless @current_user

    # Admin can access
    return if @current_user.is_admin?

    if @current_user.community_membership.pending_consent?
      redirect_to pending_consent_path
    end
  end

  # Ensure that user belongs to community
  #
  # This check is in most cases useless: When user logs in we already
  # check that the user belongs to the community she is trying to log
  # in. However, after the user account separation migration in March
  # 2016, there was a possibility that user had an existing session
  # which pointed to a person_id that belonged to another
  # community. That's why we need to check the community membership
  # even after logging in.
  #
  # This extra check can be removed when we are sure that all the
  # sessions which potentially had a person_id pointing to another
  # community are all expired.
  def ensure_user_belongs_to_community
    return unless @current_user

    if !@current_user.has_admin_rights?(@current_community) && @current_user.accepted_community != @current_community

      logger.info(
        "Automatically logged out user that doesn't belong to community",
        :autologout,
        current_user_id: @current_user.id,
        current_community_id: @current_community.id,
        current_user_community_ids: @current_user.communities.map(&:id)
      )

      sign_out
      flash[:notice] = t("layouts.notifications.automatically_logged_out_please_sign_in")

      redirect_to search_path
    end
  end

  # A before filter for views that only users that are logged in can access
  #
  # Takes one parameter: A warning message that will be displayed in flash notification
  #
  # Sets the `return_to` variable to session, so that we can redirect user back to this
  # location after the user signed up.
  #
  # Returns true if user is logged in, false otherwise
  def ensure_logged_in(warning_message)
    if logged_in?
      true
    else
      session[:return_to] = request.fullpath
      flash[:warning] = warning_message
      redirect_to login_path

      false
    end
  end

  def logged_in?
    @current_user.present?
  end

  def current_user?(person)
    @current_user && @current_user.id.eql?(person.id)
  end

  # Saves current path so that the user can be
  # redirected back to that path when needed.
  def save_current_path
    session[:return_to_content] = request.fullpath
  end

  def save_current_host_with_port
    # store the host of the current request (as sometimes needed in views)
    @current_host_with_port = request.host_with_port
  end

  # This can be overriden by controllers, if they have
  # another strategy for resolving the community
  def resolve_community
    request.env[:current_marketplace]
  end

  # Before filter to get the current community
  def fetch_community
    @current_community = resolve_community()
    m_community = Maybe(@current_community)

    # Save current community id in request env to be used
    # by Devise and our custom community authenticatable strategy
    request.env[:community_id] = m_community.id.or_else(nil)

    setup_logger!(marketplace_id: m_community.id.or_else(nil), marketplace_ident: m_community.ident.or_else(nil))
  end

  # Performs redirect to correct URL, if needed.
  # Note: This filter is safe to run even if :fetch_community
  # filter is skipped
  def perform_redirect
    MarketplaceRouter.perform_redirect(community: @current_community,
                                       plan: @current_plan,
                                       request: request) do |target|
      if target[:message] && params[:action] != 'not_available'
        redirect_to community_not_available_path
      elsif target[:message]
        render 'layouts/marketplace_router_error', layout: false, locals: {message: target[:message]}
      else
        url = target[:url] || send(target[:route_name], protocol: target[:protocol])
        redirect_to(url, status: target[:status])
      end
    end
  end

  # plain stub for routes, intercepted in perfom_redirect
  def not_available
    render 'errors/community_not_found', layout: false, status: :not_found, locals: { status: 404, title: "Marketplace not found", host: request.host }
  end

  def fetch_community_membership
    if @current_user
      @current_community_membership = CommunityMembership.where(person_id: @current_user.id, community_id: @current_community.id, status: "accepted").first
    end
  end

  def cannot_access_if_banned
    # Not logged in
    return unless @current_user

    # Admin can access
    return if @current_user.is_admin?

    # Check if banned
    if @current_user.banned?
      flash.keep
      redirect_to access_denied_path
    end
  end

  def cannot_access_without_confirmation
    # Not logged in
    return unless @current_user

    # Admin can access
    return if @current_user.is_admin?

    if @current_user.community_membership.pending_email_confirmation?
      # Check if requirements are already filled, but the membership just hasn't been updated yet
      # (This might happen if unexpected error happens during page load and it shouldn't leave people in loop of of
      # having email confirmed but not the membership)
      #
      # TODO Remove this. Find the issue that causes this and fix it, don't fix the symptoms.
      if @current_user.has_valid_email_for_community?(@current_community)
        @current_community.approve_pending_membership(@current_user)
        redirect_to search_path and return
      end

      redirect_to confirmation_pending_path
    end
  end

  def fetch_community_admin_status
    @is_current_community_admin = (@current_user&.has_admin_rights?(@current_community))
  end

  def fetch_community_plan_expiration_status
    @current_plan = request.env[:current_plan]
  end

  # Before filter for payments, shows notification if user is not ready for payments
  def warn_about_missing_payment_info
    if @current_user
      has_paid_listings = PaymentHelper.open_listings_with_payment_process?(@current_community.id, @current_user.id)
      paypal_community  = PaypalHelper.community_ready_for_payments?(@current_community.id)
      stripe_community  = StripeHelper.community_ready_for_payments?(@current_community.id)
      paypal_ready      = PaypalHelper.account_prepared_for_user?(@current_user.id, @current_community.id)
      stripe_ready      = StripeHelper.user_stripe_active?(@current_community.id, @current_user.id)

      accept_payments = []
      if paypal_community && paypal_ready
        accept_payments << :paypal
      end
      if stripe_community && stripe_ready
        accept_payments << :stripe
      end

      if has_paid_listings && accept_payments.blank? && !admin_controller?
        payment_settings_link = view_context.link_to(t("paypal_accounts.from_your_payment_settings_link_text"),
          person_payment_settings_path(@current_user), target: "_blank", rel: "noopener")

        flash.now[:warning] = t("stripe_accounts.missing_payment", settings_link: payment_settings_link).html_safe
      end
    end
  end

  def maintenance_warning
    now = Time.now
    @show_maintenance_warning = NextMaintenance.show_warning?(15.minutes, now)
    @minutes_to_maintenance = NextMaintenance.minutes_to(now)
  end

  # This hook will be called by Devise after successful Facebook
  # login.
  #
  # Return path where you want the user to be redirected to.
  #
  def after_sign_in_path_for(resourse)
    return_to_path = session[:return_to] || session[:return_to_content]

    if return_to_path
      flash[:notice] = flash.alert if flash.alert # Devise sets flash.alert in case already logged in
      session[:return_to] = nil
      session[:return_to_content] = nil
      return_to_path
    else
      search_path
    end
  end

  def set_display_expiration_notice
    ext_service_active = PlanService::API::Api.plans.active?
    is_expired = Maybe(@current_plan)[:expired].or_else(false)

    @display_expiration_notice = ext_service_active && is_expired
  end

  private

  # Override basic instrumentation and provide additional info for
  # lograge to consume. These are pulled and logged in environment
  # configs.
  def append_info_to_payload(payload)
    super
    payload[:community_id] = Maybe(@current_community).id.or_else("")
    payload[:current_user_id] = Maybe(@current_user).id.or_else("")

    ControllerLogging.append_request_info_to_payload!(request, payload)
  end

  def date_equals?(date, comp)
    date && date.to_date.eql?(comp)
  end

  def fetch_translations
    WebTranslateIt.fetch_translations
  end

  def check_http_auth
    return true unless APP_CONFIG.use_http_auth.to_s.downcase == 'true'

    if authenticate_with_http_basic { |u, p| u == APP_CONFIG.http_auth_username && p == APP_CONFIG.http_auth_password }
      true
    else
      request_http_basic_authentication
    end
  end

  def check_auth_token
    user_to_log_in = UserService::API::AuthTokens::use_token_for_login(params[:auth])
    person = Person.find(user_to_log_in[:id]) if user_to_log_in

    if person
      sign_in(person)
      @current_user = person
      force_hide_referer

      # Clean the URL from the used token
      path_without_auth_token = URLUtils.remove_query_param(request.fullpath, "auth")
      redirect_to path_without_auth_token
    end

  end

  def logger
    if @logger.nil?
      metadata = [:marketplace_id, :marketplace_ident, :user_id, :username, :request_uuid]
      @logger = SharetribeLogger.new(:controller, metadata)
      @logger.add_metadata(request_uuid: request.uuid)
    end

    @logger
  end

  def setup_logger!(metadata)
    logger.add_metadata(metadata)
  end

  helper_method def custom_script_enabled?
    @current_plan && @current_plan[:features][:custom_script]
  end

  def display_branding_info?
    !admin_controller? && !(@current_plan && @current_plan[:features][:whitelabel])
  end
  helper_method :display_branding_info?

  def display_onboarding_topbar?
    false

    # Don't show if user is not logged in
    # return false unless @current_user
    #
    # # Show for super admins
    # return true if @current_user.is_admin?
    #
    # # Show for admins if their status is accepted
    # @current_user.is_marketplace_admin?(@current_community) &&
    #   @current_user.community_membership.accepted?
  end

  helper_method :display_onboarding_topbar?

  def onboarding_topbar_props
    community_id = @current_community.id
    onboarding_status = Admin::OnboardingWizard.new(community_id).setup_status
    {
      progress: OnboardingViewUtils.progress(onboarding_status),
      next_step: OnboardingViewUtils.next_incomplete_step(onboarding_status)
    }
  end

  helper_method :onboarding_topbar_props

  def topbar_props
    TopbarHelper.topbar_props(
      community: @current_community,
      path_after_locale_change: @return_to,
      user: @current_user,
      search_placeholder: @community_customization&.search_placeholder,
      current_path: request.fullpath,
      locale_param: params[:locale],
      host_with_port: request.host_with_port)
  end

  helper_method :topbar_props

  def notifications_to_react
    # Different way to display flash messages on React pages
    if (params[:controller] == "homepage" && params[:action] == "index" && FeatureFlagHelper.feature_enabled?(:searchpage_v1))
      notifications = [:notice, :warning, :error].each_with_object({}) do |level, acc|
        if flash[level]
          acc[level] = flash[level]
          flash.delete(level)
        end
      end.compact
    end
  end

  helper_method :notifications_to_react

  def header_props
    user = Maybe(@current_user).map { |u|
      {
        unread_count: InboxService.notification_count(u.id, @current_community.id),
        avatar_url: u.image.present? && !u.image_processing ? u.image.url(:thumb) : view_context.image_path("profile_image/thumb/missing.png"),
        current_user_name: PersonViewUtils.person_display_name(u, @current_community),
        inbox_path: person_inbox_path(u),
        profile_path: person_path(u),
        manage_listings_path: person_path(u, show_closed: true),
        settings_path: person_settings_path(u),
        logout_path: logout_path
      }
    }.or_else({})

    locale_change_links = available_locales.map { |(title, locale_code)|
      {
        url: PathHelpers.change_locale_path(is_logged_in: @current_user.present?,
                                            locale: locale_code,
                                            redirect_uri: @return_to),
        title: title,
        locale_code: locale_code.upcase
      }
    }

    common = {
      logged_in: @current_user.present?,
      homepage_path: @homepage_path,
      current_locale_name: get_full_locale_name(I18n.locale),
      current_locale_code: I18n.locale.upcase,
      sign_up_path: sign_up_path,
      login_path: login_path,
      new_listing_path: new_listing_path,
      locale_change_links: locale_change_links,
      icons: pick_icons(
        APP_CONFIG.icon_pack,
        %w[dropdown mail user list settings logout rows home new_listing information feedback invite redirect admin])
    }

    common.merge(user)
  end

  helper_method :header_props

  def get_full_locale_name(locale)
    Maybe(Sharetribe::AVAILABLE_LOCALES.find { |l| l[:ident] == locale.to_s })[:name].or_else(locale).to_s
  end

  def render_not_found!(msg = "Not found")
    redirect_to error_not_found_path, status: :not_found, flash: { error: msg }
  end

  def make_onboarding_popup
    @onboarding_popup = OnboardingViewUtils.popup_locals(
      flash[:show_onboarding_popup],
      admin_getting_started_guide_path,
      Admin::OnboardingWizard.new(@current_community.id).setup_status)
  end

  def setup_intercom_user
    if admin_controller? && !request.xhr?
      AnalyticService::API::Intercom.setup_person(person: @current_user, community: @current_community)
    end
  end

  def setup_custom_footer
    @custom_footer = admin_controller? ? nil : FooterPresenter.new(@current_community, @current_plan)
  end

  def admin_controller?
    self.class.name =~ /^Admin/
  end

  def disarm_custom_head_script
    if params[:disarm].present? && !ActiveModel::Type::Boolean::FALSE_VALUES.include?(params[:disarm])
      @disable_custom_head_script = true
    end
  end

  def setup_seo_service
    @seo_service = SeoService.new(@current_community, params)
  end

  helper_method :show_location?

  def show_location?
    @current_community.show_location?
  end
end