rubygems/rubygems.org

View on GitHub
app/controllers/application_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
class ApplicationController < ActionController::Base
  include Clearance::Authentication
  include ApplicationMultifactorMethods
  include TraceTagger

  helper ActiveSupport::NumberHelper

  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
  rescue_from ActionController::InvalidAuthenticityToken, with: :render_forbidden
  rescue_from ActionController::UnpermittedParameters, with: :render_bad_request

  before_action :set_locale
  before_action :reject_null_char_param
  before_action :reject_path_params_param
  before_action :reject_null_char_cookie
  before_action :set_error_context_user
  before_action :set_user_tag
  before_action :set_current_request

  add_flash_types :notice_html

  ###
  # Content security policy override for script-src
  # This is necessary because we use a SHA256 for the importmap script tag
  # because caching behavior of the mostly static pages could mean longer lived nonces
  # being served from cache instead of unique nonces for each request.
  # This ensures that importmap passes CSP and can be cached safely.
  content_security_policy do |policy|
    policy.script_src(
      :self,
      "'sha256-#{Digest::SHA256.base64digest(Rails.application.importmap.to_json(resolver: ApplicationController.helpers))}'",
      "https://secure.gaug.es",
      "https://www.fastly-insights.com"
    )
  end

  def set_locale
    I18n.locale = user_locale

    # after store current locale
    session[:locale] = params[:locale] if params[:locale]
  rescue I18n::InvalidLocale
    I18n.locale = I18n.default_locale
  end

  def set_user_tag
    set_tag "gemcutter.user.id", current_user.id if signed_in?
  end

  rescue_from(ActionController::ParameterMissing) do |e|
    render_bad_request "Request is missing param '#{e.param}'"
  end

  def self.http_basic_authenticate_with(**options)
    before_action(options.except(:name, :password, :realm)) do
      raise "Invalid authentication options" unless http_basic_authentication_options_valid?(**options)
    end
    super
  end

  protected

  def http_basic_authentication_options_valid?(**options)
    options[:password].present? && options[:name].present?
  end

  def cache_expiry_headers(expiry: 60, fastly_expiry: 3600)
    expires_in expiry, public: true
    fastly_expires_in fastly_expiry
  end

  def fastly_expires_in(seconds, stale_while_revalidate: seconds / 2, stale_if_error: seconds / 2)
    response.headers["Surrogate-Control"] = {
      "max-age" => seconds,
      "stale-while-revalidate" => stale_while_revalidate,
      "stale-if-error" => stale_if_error
    }.compact.map { |k, v| "#{k}=#{v}" }.join(", ")
  end

  def set_surrogate_key(*surrogate_keys)
    response.headers["Surrogate-Key"] = surrogate_keys.join(" ")
  end

  def redirect_to_signin
    response.headers["Cache-Control"] = "private, max-age=0"
    redirect_to sign_in_path, alert: t("please_sign_in")
  end

  def redirect_to_root
    redirect_to root_path
  end

  def find_rubygem
    @rubygem = Rubygem.find_by_name(params[:rubygem_id] || params[:id])
    return if @rubygem
    respond_to do |format|
      format.any do
        render plain: t(:this_rubygem_could_not_be_found), status: :not_found
      end
      format.html do
        render file: Rails.public_path.join("404.html"), status: :not_found, layout: false, formats: [:html]
      end
    end
  end

  def owner?
    @rubygem.owned_by?(current_user)
  end

  def find_versioned_links
    @versioned_links = @rubygem.links(@latest_version)
  end

  def set_page(max_page = Gemcutter::MAX_PAGES)
    sanitize_params
    @page = Gemcutter::DEFAULT_PAGE && return unless params.key?(:page)
    redirect_to_page_with_error && return unless valid_page_param?(max_page)

    @page = params[:page].to_i
  end

  def user_locale
    params[:locale] || session[:locale] || http_head_locale || I18n.default_locale
  end

  def http_head_locale
    http_accept_language.language_region_compatible_from(I18n.available_locales)
  end

  def render_not_found
    respond_to do |format|
      format.html { render file: Rails.public_path.join("404.html"), status: :not_found, layout: false }
      format.json { render json: { error: t(:not_found) }, status: :not_found }
      format.yaml { render yaml: { error: t(:not_found) }, status: :not_found }
      format.any(:all) { render plain: t(:not_found), status: :not_found }
    end
  end

  def render_forbidden(error = "forbidden")
    render plain: error, status: :forbidden
  end

  def render_bad_request(error = "bad request")
    error = error.message if error.is_a?(Exception)
    render plain: error.to_s, status: :bad_request
  end

  def redirect_to_page_with_error
    flash[:error] = t("invalid_page") unless controller_path.starts_with? "api"
    page_params = params.except(:controller, :action, :page)
      .permit(:query, :to, :from, :format, :letter)
      .merge(page: Gemcutter::DEFAULT_PAGE)
    redirect_to url_for(page_params)
  end

  def valid_page_param?(max_page)
    params[:page].respond_to?(:to_i) && params[:page].to_i.between?(Gemcutter::DEFAULT_PAGE, max_page)
  end

  def reject_null_char_param
    render_bad_request if params.to_s.include?("\\u0000")
  end

  # Fix for https://github.com/kaminari/kaminari/pull/1123, remove after this is merged and in use.
  def reject_path_params_param
    params.delete(:path_params)
  end

  def reject_null_char_cookie
    contains_null_char = cookies.map { |cookie| cookie.join("=") }.join(";").include?("\u0000")
    render_bad_request if contains_null_char
  end

  def sanitize_params
    params.delete(:params)
  end

  def params_fetch(keys, default = :required)
    if default == :required
      params.permit(keys).require(keys)
    else
      params.permit(keys).fetch(keys, default)
    end
  end

  def disable_cache
    response.headers["Cache-Control"] = "no-cache, no-store"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
  end

  def set_error_context_user
    return unless current_user

    Rails.error.set_context(
      user_id: current_user.id,
      user_email: current_user.email
    )
  end

  def set_current_request
    Current.request = request
    Current.user = current_user
  end

  def browser
    Browser.new(request.user_agent)
  end
end