1 class ApplicationController < ActionController::Base
4 include SessionPersistence
6 protect_from_forgery :with => :exception
8 add_flash_types :warning, :error
10 rescue_from CanCan::AccessDenied, :with => :deny_access
13 rescue_from RailsParam::InvalidParameterError, :with => :invalid_parameter
15 after_action :close_body
17 attr_accessor :current_user, :oauth_token
19 helper_method :current_user
20 helper_method :oauth_token
22 def self.allow_thirdparty_images(**options)
23 content_security_policy(**options) do |policy|
24 policy.img_src("*", :data)
28 def self.allow_social_login(**options)
29 content_security_policy(options) do |policy|
30 policy.form_action(*policy.form_action, "accounts.google.com", "*.facebook.com", "login.microsoftonline.com", "github.com", "meta.wikimedia.org")
34 def self.allow_all_form_action(**options)
35 content_security_policy(options) do |policy|
36 policy.form_action(nil)
43 turbo_frame_request? ? "turbo_frame" : "site"
46 def authorize_web(skip_terms: false)
48 self.current_user = User.find_by(:id => session[:user], :status => %w[active confirmed suspended])
50 if session[:fingerprint] &&
51 session[:fingerprint] != current_user.fingerprint
53 self.current_user = nil
54 elsif current_user.status == "suspended"
56 session_expires_automatically
58 redirect_to :controller => "users", :action => "suspended"
60 # don't allow access to any auth-requiring part of the site unless
61 # the new CTs have been seen (and accept/decline chosen).
62 elsif !current_user.terms_seen && !skip_terms
63 flash[:notice] = t "accounts.terms.show.you need to accept or decline"
65 redirect_to account_terms_path(:referer => params[:referer])
67 redirect_to account_terms_path(:referer => request.fullpath)
72 session[:fingerprint] = current_user.fingerprint if current_user && session[:fingerprint].nil?
73 rescue StandardError => e
74 logger.info("Exception authorizing user: #{e}")
76 self.current_user = nil
82 redirect_to login_path(:referer => request.fullpath)
90 @oauth_token = current_user.oauth_token(Settings.oauth_application) if current_user && Settings.key?(:oauth_application)
94 # require the user to have cookies enabled in their browser
96 if request.cookies["_osm_session"].to_s == ""
97 if params[:cookie_test].nil?
98 session[:cookie_test] = true
99 redirect_to params.to_unsafe_h.merge(:only_path => true, :cookie_test => "true")
102 flash.now[:warning] = t "application.require_cookies.cookies_needed"
105 session.delete(:cookie_test)
109 def check_database_readable(need_api: false)
110 if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
112 report_error "Database offline for maintenance", :service_unavailable
114 redirect_to :controller => "site", :action => "offline"
119 def check_database_writable(need_api: false)
120 if Settings.status == "database_offline" || Settings.status == "database_readonly" ||
121 (need_api && %w[api_offline api_readonly].include?(Settings.status))
123 report_error "Database offline for maintenance", :service_unavailable
125 redirect_to :controller => "site", :action => "offline"
130 def check_api_readable
131 report_error "Database offline for maintenance", :service_unavailable if api_status == "offline"
134 def check_api_writable
135 report_error "Database offline for maintenance", :service_unavailable unless api_status == "online"
140 when "database_offline"
142 when "database_readonly"
150 status = database_status
151 if status == "online"
162 def require_public_data
163 report_error "You must make your edits public to upload new data", :forbidden unless current_user.data_public?
166 # Report and error to the user
167 # (If anyone ever fixes Rails so it can set a http status "reason phrase",
168 # rather than only a status code and having the web engine make up a
169 # phrase from that, we can also put the error message into the status
170 # message. For now, rails won't let us)
171 def report_error(message, status = :bad_request)
172 # TODO: some sort of escaping of problem characters in the message
173 response.headers["Error"] = message
175 if request.headers["X-Error-Format"]&.casecmp?("xml")
176 result = OSM::API.new.xml_doc
177 result.root.name = "osmError"
178 result.root << (XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
179 result.root << (XML::Node.new("message") << message)
181 render :xml => result.to_s
183 render :plain => message, :status => status
187 def preferred_languages
188 @preferred_languages ||= if params[:locale]
189 Locale.list(params[:locale])
191 current_user.preferred_languages
192 elsif request.cookies["_osm_locale"]
193 Locale.list(request.cookies["_osm_locale"])
195 Locale.list(http_accept_language.user_preferred_languages)
199 helper_method :preferred_languages
202 if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
203 current_user.languages = http_accept_language.user_preferred_languages
207 I18n.locale = Locale.available.preferred(preferred_languages)
209 response.headers["Vary"] = "Accept-Language"
210 response.headers["Content-Language"] = I18n.locale.to_s
214 # wrap a web page in a timeout
216 raise Timeout::Error if Settings.web_timeout.negative?
218 Timeout.timeout(Settings.web_timeout, &)
219 rescue ActionView::Template::Error => e
222 if e.is_a?(Timeout::Error) ||
223 (e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired"))
228 rescue Timeout::Error
232 def respond_to_timeout
233 ActiveRecord::Base.connection.raw_connection.cancel
234 render :action => "timeout", :status => :gateway_timeout
238 # Unfortunately if a PUT or POST request that has a body fails to
239 # read it then Apache will sometimes fail to return the response it
240 # is given to the client properly, instead erroring:
242 # https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
244 # To work round this we call close on the body here, which is added
245 # as a filter, to let Apache know we are done with it.
251 policy = request.content_security_policy.clone
252 policy.connect_src(*policy.connect_src, "http://127.0.0.1:8111", "https://vector.openstreetmap.org", "https://api.maptiler.com",
253 "https://tile.thunderforest.com", "https://render.openstreetmap.org", Settings.nominatim_url, Settings.overpass_url,
254 Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url, Settings.wikidata_api_url)
255 policy.form_action(*policy.form_action, "render.openstreetmap.org", "tile.thunderforest.com")
256 policy.img_src(*policy.img_src, Settings.wikimedia_commons_url, "upload.wikimedia.org")
257 policy.script_src(*policy.script_src, :wasm_unsafe_eval)
258 policy.style_src(*policy.style_src, :unsafe_inline)
260 request.content_security_policy = policy
262 flash.now[:warning] = { :partial => "layouts/offline_flash" } unless api_status == "online"
264 request.xhr? ? "xhr" : "map"
270 elsif current_user&.preferred_editor
271 current_user.preferred_editor
273 Settings.default_editor
277 def preferred_color_scheme(subject)
279 current_user.preferences.find_by(:k => "#{subject}.color_scheme")&.v || "auto"
285 helper_method :preferred_editor, :preferred_color_scheme
288 if Settings.key?(:totp_key)
289 cookies["_osm_totp_token"] = {
290 :value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
291 :domain => "openstreetmap.org",
292 :expires => 1.hour.from_now
298 Ability.new(current_user)
301 def deny_access(_exception)
304 respond_to do |format|
305 format.html { redirect_to :controller => "/errors", :action => "forbidden" }
306 format.any { report_error t("application.permission_denied"), :forbidden }
309 respond_to do |format|
310 format.html { redirect_to login_path(:referer => request.fullpath) }
311 format.any { head :forbidden }
318 def invalid_parameter(_exception)
320 respond_to do |format|
321 format.html { redirect_to :controller => "/errors", :action => "bad_request" }
322 format.any { head :bad_request }
329 # clean any referer parameter
330 def safe_referer(referer)
332 referer = URI.parse(referer)
334 if %w[http https].include?(referer.scheme)
338 elsif referer.scheme || referer.host || referer.port
342 referer = nil if referer&.path&.first != "/"
343 rescue URI::InvalidURIError