]> git.openstreetmap.org Git - rails.git/blob - app/controllers/application_controller.rb
Merge remote-tracking branch 'upstream/pull/6492'
[rails.git] / app / controllers / application_controller.rb
1 # frozen_string_literal: true
2
3 class ApplicationController < ActionController::Base
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", "appleid.apple.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 << (LibXML::XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
179       result.root << (LibXML::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   helper_method :preferred_editor
278
279   def update_totp
280     if Settings.key?(:totp_key)
281       cookies["_osm_totp_token"] = {
282         :value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
283         :domain => "openstreetmap.org",
284         :expires => 1.hour.from_now
285       }
286     end
287   end
288
289   def current_ability
290     Ability.new(current_user)
291   end
292
293   def deny_access(_exception)
294     if current_user
295       set_locale
296       respond_to do |format|
297         format.html { redirect_to :controller => "/errors", :action => "forbidden" }
298         format.any { report_error t("application.permission_denied"), :forbidden }
299       end
300     elsif request.get?
301       respond_to do |format|
302         format.html { redirect_to login_path(:referer => request.fullpath) }
303         format.any { head :forbidden }
304       end
305     else
306       head :forbidden
307     end
308   end
309
310   def invalid_parameter(_exception)
311     if request.get?
312       respond_to do |format|
313         format.html { redirect_to :controller => "/errors", :action => "bad_request" }
314         format.any { head :bad_request }
315       end
316     else
317       head :bad_request
318     end
319   end
320
321   # clean any referer parameter
322   def safe_referer(referer)
323     begin
324       referer = URI.parse(referer)
325
326       if %w[http https].include?(referer.scheme)
327         referer.scheme = nil
328         referer.host = nil
329         referer.port = nil
330       elsif referer.scheme || referer.host || referer.port
331         referer = nil
332       end
333
334       referer = nil if referer&.path&.first != "/"
335     rescue URI::InvalidURIError
336       referer = nil
337     end
338
339     referer&.to_s
340   end
341 end