]> git.openstreetmap.org Git - rails.git/blob - app/controllers/application_controller.rb
Merge remote-tracking branch 'upstream/pull/6291'
[rails.git] / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   require "timeout"
3
4   include SessionPersistence
5
6   protect_from_forgery :with => :exception
7
8   add_flash_types :warning, :error
9
10   rescue_from CanCan::AccessDenied, :with => :deny_access
11   check_authorization
12
13   rescue_from RailsParam::InvalidParameterError, :with => :invalid_parameter
14
15   after_action :close_body
16
17   attr_accessor :current_user, :oauth_token
18
19   helper_method :current_user
20   helper_method :oauth_token
21
22   def self.allow_thirdparty_images(**options)
23     content_security_policy(**options) do |policy|
24       policy.img_src("*", :data)
25     end
26   end
27
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")
31     end
32   end
33
34   def self.allow_all_form_action(**options)
35     content_security_policy(options) do |policy|
36       policy.form_action(nil)
37     end
38   end
39
40   private
41
42   def site_layout
43     turbo_frame_request? ? "turbo_frame" : "site"
44   end
45
46   def authorize_web(skip_terms: false)
47     if session[:user]
48       self.current_user = User.find_by(:id => session[:user], :status => %w[active confirmed suspended])
49
50       if session[:fingerprint] &&
51          session[:fingerprint] != current_user.fingerprint
52         reset_session
53         self.current_user = nil
54       elsif current_user.status == "suspended"
55         session.delete(:user)
56         session_expires_automatically
57
58         redirect_to :controller => "users", :action => "suspended"
59
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"
64         if params[:referer]
65           redirect_to account_terms_path(:referer => params[:referer])
66         else
67           redirect_to account_terms_path(:referer => request.fullpath)
68         end
69       end
70     end
71
72     session[:fingerprint] = current_user.fingerprint if current_user && session[:fingerprint].nil?
73   rescue StandardError => e
74     logger.info("Exception authorizing user: #{e}")
75     reset_session
76     self.current_user = nil
77   end
78
79   def require_user
80     unless current_user
81       if request.get?
82         redirect_to login_path(:referer => request.fullpath)
83       else
84         head :forbidden
85       end
86     end
87   end
88
89   def require_oauth
90     @oauth_token = current_user.oauth_token(Settings.oauth_application) if current_user && Settings.key?(:oauth_application)
91   end
92
93   ##
94   # require the user to have cookies enabled in their browser
95   def require_cookies
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")
100         false
101       else
102         flash.now[:warning] = t "application.require_cookies.cookies_needed"
103       end
104     else
105       session.delete(:cookie_test)
106     end
107   end
108
109   def check_database_readable(need_api: false)
110     if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
111       if request.xhr?
112         report_error "Database offline for maintenance", :service_unavailable
113       else
114         redirect_to :controller => "site", :action => "offline"
115       end
116     end
117   end
118
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))
122       if request.xhr?
123         report_error "Database offline for maintenance", :service_unavailable
124       else
125         redirect_to :controller => "site", :action => "offline"
126       end
127     end
128   end
129
130   def check_api_readable
131     report_error "Database offline for maintenance", :service_unavailable if api_status == "offline"
132   end
133
134   def check_api_writable
135     report_error "Database offline for maintenance", :service_unavailable unless api_status == "online"
136   end
137
138   def database_status
139     case Settings.status
140     when "database_offline"
141       "offline"
142     when "database_readonly"
143       "readonly"
144     else
145       "online"
146     end
147   end
148
149   def api_status
150     status = database_status
151     if status == "online"
152       case Settings.status
153       when "api_offline"
154         status = "offline"
155       when "api_readonly"
156         status = "readonly"
157       end
158     end
159     status
160   end
161
162   def require_public_data
163     report_error "You must make your edits public to upload new data", :forbidden unless current_user.data_public?
164   end
165
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
174
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)
180
181       render :xml => result.to_s
182     else
183       render :plain => message, :status => status
184     end
185   end
186
187   def preferred_languages
188     @preferred_languages ||= if params[:locale]
189                                Locale.list(params[:locale])
190                              elsif current_user
191                                current_user.preferred_languages
192                              elsif request.cookies["_osm_locale"]
193                                Locale.list(request.cookies["_osm_locale"])
194                              else
195                                Locale.list(http_accept_language.user_preferred_languages)
196                              end
197   end
198
199   helper_method :preferred_languages
200
201   def set_locale
202     if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
203       current_user.languages = http_accept_language.user_preferred_languages
204       current_user.save
205     end
206
207     I18n.locale = Locale.available.preferred(preferred_languages)
208
209     response.headers["Vary"] = "Accept-Language"
210     response.headers["Content-Language"] = I18n.locale.to_s
211   end
212
213   ##
214   # wrap a web page in a timeout
215   def web_timeout(&)
216     raise Timeout::Error if Settings.web_timeout.negative?
217
218     Timeout.timeout(Settings.web_timeout, &)
219   rescue ActionView::Template::Error => e
220     e = e.cause
221
222     if e.is_a?(Timeout::Error) ||
223        (e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired"))
224       respond_to_timeout
225     else
226       raise
227     end
228   rescue Timeout::Error
229     respond_to_timeout
230   end
231
232   def respond_to_timeout
233     ActiveRecord::Base.connection.raw_connection.cancel
234     render :action => "timeout", :status => :gateway_timeout
235   end
236
237   ##
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:
241   #
242   #   https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
243   #
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.
246   def close_body
247     request.body&.close
248   end
249
250   def map_layout
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)
259
260     request.content_security_policy = policy
261
262     flash.now[:warning] = { :partial => "layouts/offline_flash" } unless api_status == "online"
263
264     request.xhr? ? "xhr" : "map"
265   end
266
267   def preferred_editor
268     if params[:editor]
269       params[:editor]
270     elsif current_user&.preferred_editor
271       current_user.preferred_editor
272     else
273       Settings.default_editor
274     end
275   end
276
277   def preferred_color_scheme(subject)
278     if current_user
279       current_user.preferences.find_by(:k => "#{subject}.color_scheme")&.v || "auto"
280     else
281       "auto"
282     end
283   end
284
285   helper_method :preferred_editor, :preferred_color_scheme
286
287   def update_totp
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
293       }
294     end
295   end
296
297   def current_ability
298     Ability.new(current_user)
299   end
300
301   def deny_access(_exception)
302     if current_user
303       set_locale
304       respond_to do |format|
305         format.html { redirect_to :controller => "/errors", :action => "forbidden" }
306         format.any { report_error t("application.permission_denied"), :forbidden }
307       end
308     elsif request.get?
309       respond_to do |format|
310         format.html { redirect_to login_path(:referer => request.fullpath) }
311         format.any { head :forbidden }
312       end
313     else
314       head :forbidden
315     end
316   end
317
318   def invalid_parameter(_exception)
319     if request.get?
320       respond_to do |format|
321         format.html { redirect_to :controller => "/errors", :action => "bad_request" }
322         format.any { head :bad_request }
323       end
324     else
325       head :bad_request
326     end
327   end
328
329   # clean any referer parameter
330   def safe_referer(referer)
331     begin
332       referer = URI.parse(referer)
333
334       if %w[http https].include?(referer.scheme)
335         referer.scheme = nil
336         referer.host = nil
337         referer.port = nil
338       elsif referer.scheme || referer.host || referer.port
339         referer = nil
340       end
341
342       referer = nil if referer&.path&.first != "/"
343     rescue URI::InvalidURIError
344       referer = nil
345     end
346
347     referer&.to_s
348   end
349 end