1 # frozen_string_literal: true
3 class ApplicationController < ActionController::Base
6 include SessionPersistence
8 protect_from_forgery :with => :exception
10 add_flash_types :warning, :error
12 rescue_from CanCan::AccessDenied, :with => :deny_access
15 rescue_from RailsParam::InvalidParameterError, :with => :invalid_parameter
17 after_action :close_body
19 attr_accessor :current_user, :oauth_token
21 helper_method :current_user
22 helper_method :oauth_token
24 def self.allow_thirdparty_images(**options)
25 content_security_policy(**options) do |policy|
26 policy.img_src("*", :data)
30 def self.allow_social_login(**options)
31 content_security_policy(options) do |policy|
32 policy.form_action(*policy.form_action, "accounts.google.com", "appleid.apple.com", "*.facebook.com", "login.microsoftonline.com", "github.com", "meta.wikimedia.org")
36 def self.allow_all_form_action(**options)
37 content_security_policy(options) do |policy|
38 policy.form_action(nil)
45 turbo_frame_request? ? "turbo_frame" : "site"
48 def authorize_web(skip_terms: false)
50 self.current_user = User.find_by(:id => session[:user], :status => %w[active confirmed suspended])
52 if session[:fingerprint] &&
53 session[:fingerprint] != current_user.fingerprint
55 self.current_user = nil
56 elsif current_user.status == "suspended"
58 session_expires_automatically
60 redirect_to :controller => "users", :action => "suspended"
62 # don't allow access to any auth-requiring part of the site unless
63 # the new CTs have been seen (and accept/decline chosen).
64 elsif !current_user.terms_seen && !skip_terms
65 flash[:notice] = t "accounts.terms.show.you need to accept or decline"
67 redirect_to account_terms_path(:referer => params[:referer])
69 redirect_to account_terms_path(:referer => request.fullpath)
74 session[:fingerprint] = current_user.fingerprint if current_user && session[:fingerprint].nil?
75 rescue StandardError => e
76 logger.info("Exception authorizing user: #{e}")
78 self.current_user = nil
84 redirect_to login_path(:referer => request.fullpath)
92 @oauth_token = current_user.oauth_token(Settings.oauth_application) if current_user && Settings.key?(:oauth_application)
96 # require the user to have cookies enabled in their browser
98 if request.cookies["_osm_session"].to_s == ""
99 if params[:cookie_test].nil?
100 session[:cookie_test] = true
101 redirect_to params.to_unsafe_h.merge(:only_path => true, :cookie_test => "true")
104 flash.now[:warning] = t "application.require_cookies.cookies_needed"
107 session.delete(:cookie_test)
111 def check_database_readable(need_api: false)
112 if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
114 report_error "Database offline for maintenance", :service_unavailable
116 redirect_to :controller => "site", :action => "offline"
121 def check_database_writable(need_api: false)
122 if Settings.status == "database_offline" || Settings.status == "database_readonly" ||
123 (need_api && %w[api_offline api_readonly].include?(Settings.status))
125 report_error "Database offline for maintenance", :service_unavailable
127 redirect_to :controller => "site", :action => "offline"
132 def check_api_readable
133 report_error "Database offline for maintenance", :service_unavailable if api_status == "offline"
136 def check_api_writable
137 report_error "Database offline for maintenance", :service_unavailable unless api_status == "online"
142 when "database_offline"
144 when "database_readonly"
152 status = database_status
153 if status == "online"
164 def require_public_data
165 report_error "You must make your edits public to upload new data", :forbidden unless current_user.data_public?
168 # Report and error to the user
169 # (If anyone ever fixes Rails so it can set a http status "reason phrase",
170 # rather than only a status code and having the web engine make up a
171 # phrase from that, we can also put the error message into the status
172 # message. For now, rails won't let us)
173 def report_error(message, status = :bad_request)
174 # TODO: some sort of escaping of problem characters in the message
175 response.headers["Error"] = message
177 if request.headers["X-Error-Format"]&.casecmp?("xml")
178 result = OSM::API.new.xml_doc
179 result.root.name = "osmError"
180 result.root << (XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
181 result.root << (XML::Node.new("message") << message)
183 render :xml => result.to_s
185 render :plain => message, :status => status
189 def preferred_languages
190 @preferred_languages ||= if params[:locale]
191 Locale.list(params[:locale])
193 current_user.preferred_languages
194 elsif request.cookies["_osm_locale"]
195 Locale.list(request.cookies["_osm_locale"])
197 Locale.list(http_accept_language.user_preferred_languages)
201 helper_method :preferred_languages
204 if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
205 current_user.languages = http_accept_language.user_preferred_languages
209 I18n.locale = Locale.available.preferred(preferred_languages)
211 response.headers["Vary"] = "Accept-Language"
212 response.headers["Content-Language"] = I18n.locale.to_s
216 # wrap a web page in a timeout
218 raise Timeout::Error if Settings.web_timeout.negative?
220 Timeout.timeout(Settings.web_timeout, &)
221 rescue ActionView::Template::Error => e
224 if e.is_a?(Timeout::Error) ||
225 (e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired"))
230 rescue Timeout::Error
234 def respond_to_timeout
235 ActiveRecord::Base.connection.raw_connection.cancel
236 render :action => "timeout", :status => :gateway_timeout
240 # Unfortunately if a PUT or POST request that has a body fails to
241 # read it then Apache will sometimes fail to return the response it
242 # is given to the client properly, instead erroring:
244 # https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
246 # To work round this we call close on the body here, which is added
247 # as a filter, to let Apache know we are done with it.
253 policy = request.content_security_policy.clone
254 policy.connect_src(*policy.connect_src, "http://127.0.0.1:8111", "https://vector.openstreetmap.org", "https://api.maptiler.com",
255 "https://tile.thunderforest.com", "https://render.openstreetmap.org", Settings.nominatim_url, Settings.overpass_url,
256 Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url, Settings.wikidata_api_url)
257 policy.form_action(*policy.form_action, "render.openstreetmap.org", "tile.thunderforest.com")
258 policy.img_src(*policy.img_src, Settings.wikimedia_commons_url, "upload.wikimedia.org")
259 policy.script_src(*policy.script_src, :wasm_unsafe_eval)
260 policy.style_src(*policy.style_src, :unsafe_inline)
262 request.content_security_policy = policy
264 flash.now[:warning] = { :partial => "layouts/offline_flash" } unless api_status == "online"
266 request.xhr? ? "xhr" : "map"
272 elsif current_user&.preferred_editor
273 current_user.preferred_editor
275 Settings.default_editor
279 def preferred_color_scheme(subject)
281 current_user.preferences.find_by(:k => "#{subject}.color_scheme")&.v || "auto"
287 helper_method :preferred_editor, :preferred_color_scheme
290 if Settings.key?(:totp_key)
291 cookies["_osm_totp_token"] = {
292 :value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
293 :domain => "openstreetmap.org",
294 :expires => 1.hour.from_now
300 Ability.new(current_user)
303 def deny_access(_exception)
306 respond_to do |format|
307 format.html { redirect_to :controller => "/errors", :action => "forbidden" }
308 format.any { report_error t("application.permission_denied"), :forbidden }
311 respond_to do |format|
312 format.html { redirect_to login_path(:referer => request.fullpath) }
313 format.any { head :forbidden }
320 def invalid_parameter(_exception)
322 respond_to do |format|
323 format.html { redirect_to :controller => "/errors", :action => "bad_request" }
324 format.any { head :bad_request }
331 # clean any referer parameter
332 def safe_referer(referer)
334 referer = URI.parse(referer)
336 if %w[http https].include?(referer.scheme)
340 elsif referer.scheme || referer.host || referer.port
344 referer = nil if referer&.path&.first != "/"
345 rescue URI::InvalidURIError