openstreetmap-website/app/controllers/application_controller.rb
Andy Allan 9b8f2bbcbe Remove code complexity around resetting language preferences
This was originally introduced since we saved the user and showed
the result on the same action. Now that the preferences controller
saves and redirects, the user model and associated language preferences
are reloaded between requests, and this code is no longer required.
2021-07-14 17:40:20 +01:00

399 lines
12 KiB
Ruby

class ApplicationController < ActionController::Base
require "timeout"
include SessionPersistence
protect_from_forgery :with => :exception
add_flash_types :warning, :error
rescue_from CanCan::AccessDenied, :with => :deny_access
check_authorization
before_action :fetch_body
around_action :better_errors_allow_inline, :if => proc { Rails.env.development? }
attr_accessor :current_user, :oauth_token
helper_method :current_user
helper_method :oauth_token
helper_method :preferred_langauges
private
def authorize_web
if session[:user]
self.current_user = User.where(:id => session[:user]).where("status IN ('active', 'confirmed', 'suspended')").first
if session[:fingerprint] &&
session[:fingerprint] != current_user.fingerprint
reset_session
self.current_user = nil
elsif current_user.status == "suspended"
session.delete(:user)
session_expires_automatically
redirect_to :controller => "users", :action => "suspended"
# don't allow access to any auth-requiring part of the site unless
# the new CTs have been seen (and accept/decline chosen).
elsif !current_user.terms_seen && flash[:skip_terms].nil?
flash[:notice] = t "users.terms.you need to accept or decline"
if params[:referer]
redirect_to :controller => "users", :action => "terms", :referer => params[:referer]
else
redirect_to :controller => "users", :action => "terms", :referer => request.fullpath
end
end
elsif session[:token]
session[:user] = current_user.id if self.current_user = User.authenticate(:token => session[:token])
end
session[:fingerprint] = current_user.fingerprint if current_user && session[:fingerprint].nil?
rescue StandardError => e
logger.info("Exception authorizing user: #{e}")
reset_session
self.current_user = nil
end
def require_user
unless current_user
if request.get?
redirect_to login_path(:referer => request.fullpath)
else
head :forbidden
end
end
end
def require_oauth
@oauth_token = current_user.access_token(Settings.oauth_key) if current_user && Settings.key?(:oauth_key)
end
##
# require the user to have cookies enabled in their browser
def require_cookies
if request.cookies["_osm_session"].to_s == ""
if params[:cookie_test].nil?
session[:cookie_test] = true
redirect_to params.to_unsafe_h.merge(:only_path => true, :cookie_test => "true")
false
else
flash.now[:warning] = t "application.require_cookies.cookies_needed"
end
else
session.delete(:cookie_test)
end
end
def check_database_readable(need_api: false)
if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
if request.xhr?
report_error "Database offline for maintenance", :service_unavailable
else
redirect_to :controller => "site", :action => "offline"
end
end
end
def check_database_writable(need_api: false)
if Settings.status == "database_offline" || Settings.status == "database_readonly" ||
(need_api && (Settings.status == "api_offline" || Settings.status == "api_readonly"))
if request.xhr?
report_error "Database offline for maintenance", :service_unavailable
else
redirect_to :controller => "site", :action => "offline"
end
end
end
def check_api_readable
if api_status == "offline"
report_error "Database offline for maintenance", :service_unavailable
false
end
end
def check_api_writable
unless api_status == "online"
report_error "Database offline for maintenance", :service_unavailable
false
end
end
def database_status
case Settings.status
when "database_offline"
"offline"
when "database_readonly"
"readonly"
else
"online"
end
end
def api_status
status = database_status
if status == "online"
case Settings.status
when "api_offline"
status = "offline"
when "api_readonly"
status = "readonly"
end
end
status
end
def require_public_data
unless current_user.data_public?
report_error "You must make your edits public to upload new data", :forbidden
false
end
end
# Report and error to the user
# (If anyone ever fixes Rails so it can set a http status "reason phrase",
# rather than only a status code and having the web engine make up a
# phrase from that, we can also put the error message into the status
# message. For now, rails won't let us)
def report_error(message, status = :bad_request)
# TODO: some sort of escaping of problem characters in the message
response.headers["Error"] = message
if request.headers["X-Error-Format"]&.casecmp("xml")&.zero?
result = OSM::API.new.get_xml_doc
result.root.name = "osmError"
result.root << (XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
result.root << (XML::Node.new("message") << message)
render :xml => result.to_s
else
render :plain => message, :status => status
end
end
def preferred_languages
@preferred_languages ||= if params[:locale]
Locale.list(params[:locale])
elsif current_user
current_user.preferred_languages
else
Locale.list(http_accept_language.user_preferred_languages)
end
end
helper_method :preferred_languages
def set_locale
if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
current_user.languages = http_accept_language.user_preferred_languages
current_user.save
end
I18n.locale = Locale.available.preferred(preferred_languages)
response.headers["Vary"] = "Accept-Language"
response.headers["Content-Language"] = I18n.locale.to_s
end
def api_call_handle_error
yield
rescue ActionController::UnknownFormat
head :not_acceptable
rescue ActiveRecord::RecordNotFound => e
head :not_found
rescue LibXML::XML::Error, ArgumentError => e
report_error e.message, :bad_request
rescue ActiveRecord::RecordInvalid => e
message = "#{e.record.class} #{e.record.id}: "
e.record.errors.each { |error| message << "#{error.attribute}: #{error.message} (#{e.record[error.attribute].inspect})" }
report_error message, :bad_request
rescue OSM::APIError => e
report_error e.message, e.status
rescue AbstractController::ActionNotFound => e
raise
rescue StandardError => e
logger.info("API threw unexpected #{e.class} exception: #{e.message}")
e.backtrace.each { |l| logger.info(l) }
report_error "#{e.class}: #{e.message}", :internal_server_error
end
##
# asserts that the request method is the +method+ given as a parameter
# or raises a suitable error. +method+ should be a symbol, e.g: :put or :get.
def assert_method(method)
ok = request.send(:"#{method.to_s.downcase}?")
raise OSM::APIBadMethodError, method unless ok
end
##
# wrap an api call in a timeout
def api_call_timeout(&block)
Timeout.timeout(Settings.api_timeout, Timeout::Error, &block)
rescue Timeout::Error
raise OSM::APITimeoutError
end
##
# wrap a web page in a timeout
def web_timeout(&block)
Timeout.timeout(Settings.web_timeout, Timeout::Error, &block)
rescue ActionView::Template::Error => e
e = e.cause
if e.is_a?(Timeout::Error) ||
(e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired"))
render :action => "timeout"
else
raise
end
rescue Timeout::Error
render :action => "timeout"
end
##
# ensure that there is a "user" instance variable
def lookup_user
render_unknown_user params[:display_name] unless @user = User.active.find_by(:display_name => params[:display_name])
end
##
# render a "no such user" page
def render_unknown_user(name)
@title = t "users.no_such_user.title"
@not_found_user = name
respond_to do |format|
format.html { render :template => "users/no_such_user", :status => :not_found }
format.all { head :not_found }
end
end
##
# Unfortunately if a PUT or POST request that has a body fails to
# read it then Apache will sometimes fail to return the response it
# is given to the client properly, instead erroring:
#
# https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
#
# To work round this we call rewind on the body here, which is added
# as a filter, to force it to be fetched from Apache into a file.
def fetch_body
request.body.rewind
end
def map_layout
append_content_security_policy_directives(
:child_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
:frame_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
:connect_src => [Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url],
:form_action => %w[render.openstreetmap.org],
:style_src => %w['unsafe-inline']
)
case Settings.status
when "database_offline", "api_offline"
flash.now[:warning] = t("layouts.osm_offline")
when "database_readonly", "api_readonly"
flash.now[:warning] = t("layouts.osm_read_only")
end
request.xhr? ? "xhr" : "map"
end
def allow_thirdparty_images
append_content_security_policy_directives(:img_src => %w[*])
end
def preferred_editor
if params[:editor]
params[:editor]
elsif current_user&.preferred_editor
current_user.preferred_editor
else
Settings.default_editor
end
end
helper_method :preferred_editor
def update_totp
if Settings.key?(:totp_key)
cookies["_osm_totp_token"] = {
:value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
:domain => "openstreetmap.org",
:expires => 1.hour.from_now
}
end
end
def better_errors_allow_inline
yield
rescue StandardError
append_content_security_policy_directives(
:script_src => %w['unsafe-inline'],
:style_src => %w['unsafe-inline']
)
raise
end
def current_ability
Ability.new(current_user)
end
def deny_access(_exception)
if doorkeeper_token || current_token
set_locale
report_error t("oauth.permissions.missing"), :forbidden
elsif current_user
set_locale
respond_to do |format|
format.html { redirect_to :controller => "errors", :action => "forbidden" }
format.any { report_error t("application.permission_denied"), :forbidden }
end
elsif request.get?
respond_to do |format|
format.html { redirect_to login_path(:referer => request.fullpath) }
format.any { head :forbidden }
end
else
head :forbidden
end
end
# extract authorisation credentials from headers, returns user = nil if none
def get_auth_data
if request.env.key? "X-HTTP_AUTHORIZATION" # where mod_rewrite might have put it
authdata = request.env["X-HTTP_AUTHORIZATION"].to_s.split
elsif request.env.key? "REDIRECT_X_HTTP_AUTHORIZATION" # mod_fcgi
authdata = request.env["REDIRECT_X_HTTP_AUTHORIZATION"].to_s.split
elsif request.env.key? "HTTP_AUTHORIZATION" # regular location
authdata = request.env["HTTP_AUTHORIZATION"].to_s.split
end
# only basic authentication supported
user, pass = Base64.decode64(authdata[1]).split(":", 2) if authdata && authdata[0] == "Basic"
[user, pass]
end
# override to stop oauth plugin sending errors
def invalid_oauth_response; end
# clean any referer parameter
def safe_referer(referer)
referer = URI.parse(referer)
if referer.scheme == "http" || referer.scheme == "https"
referer.scheme = nil
referer.host = nil
referer.port = nil
elsif referer.scheme || referer.host || referer.port
referer = nil
end
referer = nil if referer&.path&.first != "/"
referer.to_s
end
end