]> git.openstreetmap.org Git - rails.git/blob - app/controllers/application_controller.rb
8ccd7706018f8fca2fe2e0f1d2f7958e95f5246c
[rails.git] / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   include SessionPersistence
3
4   protect_from_forgery :with => :exception
5
6   rescue_from CanCan::AccessDenied, :with => :deny_access
7   check_authorization
8
9   before_action :fetch_body
10   around_action :better_errors_allow_inline, :if => proc { Rails.env.development? }
11
12   attr_accessor :current_user
13   helper_method :current_user
14
15   private
16
17   def authorize_web
18     if session[:user]
19       self.current_user = User.where(:id => session[:user]).where("status IN ('active', 'confirmed', 'suspended')").first
20
21       if current_user.status == "suspended"
22         session.delete(:user)
23         session_expires_automatically
24
25         redirect_to :controller => "users", :action => "suspended"
26
27       # don't allow access to any auth-requiring part of the site unless
28       # the new CTs have been seen (and accept/decline chosen).
29       elsif !current_user.terms_seen && flash[:skip_terms].nil?
30         flash[:notice] = t "users.terms.you need to accept or decline"
31         if params[:referer]
32           redirect_to :controller => "users", :action => "terms", :referer => params[:referer]
33         else
34           redirect_to :controller => "users", :action => "terms", :referer => request.fullpath
35         end
36       end
37     elsif session[:token]
38       session[:user] = current_user.id if self.current_user = User.authenticate(:token => session[:token])
39     end
40   rescue StandardError => ex
41     logger.info("Exception authorizing user: #{ex}")
42     reset_session
43     self.current_user = nil
44   end
45
46   def require_user
47     unless current_user
48       if request.get?
49         redirect_to :controller => "users", :action => "login", :referer => request.fullpath
50       else
51         head :forbidden
52       end
53     end
54   end
55
56   def require_oauth
57     @oauth = current_user.access_token(Settings.oauth_key) if current_user && Settings.key?(:oauth_key)
58   end
59
60   ##
61   # require the user to have cookies enabled in their browser
62   def require_cookies
63     if request.cookies["_osm_session"].to_s == ""
64       if params[:cookie_test].nil?
65         session[:cookie_test] = true
66         redirect_to params.to_unsafe_h.merge(:cookie_test => "true")
67         false
68       else
69         flash.now[:warning] = t "application.require_cookies.cookies_needed"
70       end
71     else
72       session.delete(:cookie_test)
73     end
74   end
75
76   ##
77   # sets up the current_user for use by other methods. this is mostly called
78   # from the authorize method, but can be called elsewhere if authorisation
79   # is optional.
80   def setup_user_auth
81     # try and setup using OAuth
82     unless Authenticator.new(self, [:token]).allow?
83       username, passwd = get_auth_data # parse from headers
84       # authenticate per-scheme
85       self.current_user = if username.nil?
86                             nil # no authentication provided - perhaps first connect (client should retry after 401)
87                           elsif username == "token"
88                             User.authenticate(:token => passwd) # preferred - random token for user from db, passed in basic auth
89                           else
90                             User.authenticate(:username => username, :password => passwd) # basic auth
91                           end
92     end
93
94     # have we identified the user?
95     if current_user
96       # check if the user has been banned
97       user_block = current_user.blocks.active.take
98       unless user_block.nil?
99         set_locale
100         if user_block.zero_hour?
101           report_error t("application.setup_user_auth.blocked_zero_hour"), :forbidden
102         else
103           report_error t("application.setup_user_auth.blocked"), :forbidden
104         end
105       end
106
107       # if the user hasn't seen the contributor terms then don't
108       # allow editing - they have to go to the web site and see
109       # (but can decline) the CTs to continue.
110       if !current_user.terms_seen && flash[:skip_terms].nil?
111         set_locale
112         report_error t("application.setup_user_auth.need_to_see_terms"), :forbidden
113       end
114     end
115   end
116
117   def check_database_readable(need_api = false)
118     if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
119       if request.xhr?
120         report_error "Database offline for maintenance", :service_unavailable
121       else
122         redirect_to :controller => "site", :action => "offline"
123       end
124     end
125   end
126
127   def check_database_writable(need_api = false)
128     if Settings.status == "database_offline" || Settings.status == "database_readonly" ||
129        (need_api && (Settings.status == "api_offline" || Settings.status == "api_readonly"))
130       if request.xhr?
131         report_error "Database offline for maintenance", :service_unavailable
132       else
133         redirect_to :controller => "site", :action => "offline"
134       end
135     end
136   end
137
138   def check_api_readable
139     if api_status == :offline
140       report_error "Database offline for maintenance", :service_unavailable
141       false
142     end
143   end
144
145   def check_api_writable
146     unless api_status == :online
147       report_error "Database offline for maintenance", :service_unavailable
148       false
149     end
150   end
151
152   def database_status
153     if Settings.status == "database_offline"
154       :offline
155     elsif Settings.status == "database_readonly"
156       :readonly
157     else
158       :online
159     end
160   end
161
162   def api_status
163     status = database_status
164     if status == :online
165       if Settings.status == "api_offline"
166         status = :offline
167       elsif Settings.status == "api_readonly"
168         status = :readonly
169       end
170     end
171     status
172   end
173
174   def gpx_status
175     status = database_status
176     status = :offline if status == :online && Settings.status == "gpx_offline"
177     status
178   end
179
180   def require_public_data
181     unless current_user.data_public?
182       report_error "You must make your edits public to upload new data", :forbidden
183       false
184     end
185   end
186
187   # Report and error to the user
188   # (If anyone ever fixes Rails so it can set a http status "reason phrase",
189   #  rather than only a status code and having the web engine make up a
190   #  phrase from that, we can also put the error message into the status
191   #  message. For now, rails won't let us)
192   def report_error(message, status = :bad_request)
193     # TODO: some sort of escaping of problem characters in the message
194     response.headers["Error"] = message
195
196     if request.headers["X-Error-Format"]&.casecmp("xml")&.zero?
197       result = OSM::API.new.get_xml_doc
198       result.root.name = "osmError"
199       result.root << (XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
200       result.root << (XML::Node.new("message") << message)
201
202       render :xml => result.to_s
203     else
204       render :plain => message, :status => status
205     end
206   end
207
208   def preferred_languages(reset = false)
209     @preferred_languages = nil if reset
210     @preferred_languages ||= if params[:locale]
211                                Locale.list(params[:locale])
212                              elsif current_user
213                                current_user.preferred_languages
214                              else
215                                Locale.list(http_accept_language.user_preferred_languages)
216                              end
217   end
218
219   helper_method :preferred_languages
220
221   def set_locale(reset = false)
222     if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
223       current_user.languages = http_accept_language.user_preferred_languages
224       current_user.save
225     end
226
227     I18n.locale = Locale.available.preferred(preferred_languages(reset))
228
229     response.headers["Vary"] = "Accept-Language"
230     response.headers["Content-Language"] = I18n.locale.to_s
231   end
232
233   def api_call_handle_error
234     yield
235   rescue ActiveRecord::RecordNotFound => ex
236     head :not_found
237   rescue LibXML::XML::Error, ArgumentError => ex
238     report_error ex.message, :bad_request
239   rescue ActiveRecord::RecordInvalid => ex
240     message = "#{ex.record.class} #{ex.record.id}: "
241     ex.record.errors.each { |attr, msg| message << "#{attr}: #{msg} (#{ex.record[attr].inspect})" }
242     report_error message, :bad_request
243   rescue OSM::APIError => ex
244     report_error ex.message, ex.status
245   rescue AbstractController::ActionNotFound => ex
246     raise
247   rescue StandardError => ex
248     logger.info("API threw unexpected #{ex.class} exception: #{ex.message}")
249     ex.backtrace.each { |l| logger.info(l) }
250     report_error "#{ex.class}: #{ex.message}", :internal_server_error
251   end
252
253   ##
254   # asserts that the request method is the +method+ given as a parameter
255   # or raises a suitable error. +method+ should be a symbol, e.g: :put or :get.
256   def assert_method(method)
257     ok = request.send((method.to_s.downcase + "?").to_sym)
258     raise OSM::APIBadMethodError, method unless ok
259   end
260
261   ##
262   # wrap an api call in a timeout
263   def api_call_timeout
264     OSM::Timer.timeout(Settings.api_timeout, Timeout::Error) do
265       yield
266     end
267   rescue Timeout::Error
268     raise OSM::APITimeoutError
269   end
270
271   ##
272   # wrap a web page in a timeout
273   def web_timeout
274     OSM::Timer.timeout(Settings.web_timeout, Timeout::Error) do
275       yield
276     end
277   rescue ActionView::Template::Error => ex
278     ex = ex.cause
279
280     if ex.is_a?(Timeout::Error) ||
281        (ex.is_a?(ActiveRecord::StatementInvalid) && ex.message =~ /execution expired/)
282       render :action => "timeout"
283     else
284       raise
285     end
286   rescue Timeout::Error
287     render :action => "timeout"
288   end
289
290   ##
291   # ensure that there is a "user" instance variable
292   def lookup_user
293     render_unknown_user params[:display_name] unless @user = User.active.find_by(:display_name => params[:display_name])
294   end
295
296   ##
297   # render a "no such user" page
298   def render_unknown_user(name)
299     @title = t "users.no_such_user.title"
300     @not_found_user = name
301
302     respond_to do |format|
303       format.html { render :template => "users/no_such_user", :status => :not_found }
304       format.all { head :not_found }
305     end
306   end
307
308   ##
309   # Unfortunately if a PUT or POST request that has a body fails to
310   # read it then Apache will sometimes fail to return the response it
311   # is given to the client properly, instead erroring:
312   #
313   #   https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
314   #
315   # To work round this we call rewind on the body here, which is added
316   # as a filter, to force it to be fetched from Apache into a file.
317   def fetch_body
318     request.body.rewind
319   end
320
321   def map_layout
322     append_content_security_policy_directives(
323       :child_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
324       :frame_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
325       :connect_src => [Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url],
326       :form_action => %w[render.openstreetmap.org],
327       :style_src => %w['unsafe-inline']
328     )
329
330     if Settings.status == "database_offline" || Settings.status == "api_offline"
331       flash.now[:warning] = t("layouts.osm_offline")
332     elsif Settings.status == "database_readonly" || Settings.status == "api_readonly"
333       flash.now[:warning] = t("layouts.osm_read_only")
334     end
335
336     request.xhr? ? "xhr" : "map"
337   end
338
339   def allow_thirdparty_images
340     append_content_security_policy_directives(:img_src => %w[*])
341   end
342
343   def preferred_editor
344     editor = if params[:editor]
345                params[:editor]
346              elsif current_user&.preferred_editor
347                current_user.preferred_editor
348              else
349                Settings.default_editor
350              end
351
352     editor
353   end
354
355   helper_method :preferred_editor
356
357   def update_totp
358     if Settings.key?(:totp_key)
359       cookies["_osm_totp_token"] = {
360         :value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
361         :domain => "openstreetmap.org",
362         :expires => 1.hour.from_now
363       }
364     end
365   end
366
367   def better_errors_allow_inline
368     yield
369   rescue StandardError
370     append_content_security_policy_directives(
371       :script_src => %w['unsafe-inline'],
372       :style_src => %w['unsafe-inline']
373     )
374
375     raise
376   end
377
378   def current_ability
379     # Use capabilities from the oauth token if it exists and is a valid access token
380     if Authenticator.new(self, [:token]).allow?
381       Ability.new(nil).merge(Capability.new(current_token))
382     else
383       Ability.new(current_user)
384     end
385   end
386
387   def deny_access(_exception)
388     if current_token
389       set_locale
390       report_error t("oauth.permissions.missing"), :forbidden
391     elsif current_user
392       set_locale
393       respond_to do |format|
394         format.html { redirect_to :controller => "errors", :action => "forbidden" }
395         format.any { report_error t("application.permission_denied"), :forbidden }
396       end
397     elsif request.get?
398       respond_to do |format|
399         format.html { redirect_to :controller => "users", :action => "login", :referer => request.fullpath }
400         format.any { head :forbidden }
401       end
402     else
403       head :forbidden
404     end
405   end
406
407   # extract authorisation credentials from headers, returns user = nil if none
408   def get_auth_data
409     if request.env.key? "X-HTTP_AUTHORIZATION" # where mod_rewrite might have put it
410       authdata = request.env["X-HTTP_AUTHORIZATION"].to_s.split
411     elsif request.env.key? "REDIRECT_X_HTTP_AUTHORIZATION" # mod_fcgi
412       authdata = request.env["REDIRECT_X_HTTP_AUTHORIZATION"].to_s.split
413     elsif request.env.key? "HTTP_AUTHORIZATION" # regular location
414       authdata = request.env["HTTP_AUTHORIZATION"].to_s.split
415     end
416     # only basic authentication supported
417     user, pass = Base64.decode64(authdata[1]).split(":", 2) if authdata && authdata[0] == "Basic"
418     [user, pass]
419   end
420
421   # override to stop oauth plugin sending errors
422   def invalid_oauth_response; end
423 end