[webui] _deltas is valid too
[opensuse:build-service.git] / src / webui / app / controllers / application_controller.rb
1 # Filters added to this controller will be run for all controllers in the application.
2 # Likewise, all the methods added will be available for all controllers.
3
4 require 'common/activexml/transport'
5 require 'person'
6
7 #Note: This is a SUSE-sepecific debugging extension that saves the last
8 #      exception's scope. This method needs a patched Ruby interpreter.
9 if defined?(set_trace_func_for_raise)
10   $exception_scope = {}
11   set_trace_func_for_raise proc {|event, file, line, id, binding, classname|
12     $exception_scope[:locals] = {}
13     binding.eval('local_variables()').each do |lvar|
14       $exception_scope[:locals][lvar.to_sym] = binding.eval(lvar)
15     end
16     $exception_scope[:globals] = {}
17     binding.eval('global_variables()').each do |gvar|
18       $exception_scope[:globals][gvar.to_sym] = binding.eval(gvar)
19     end
20   }
21 end
22
23 class ApplicationController < ActionController::Base
24
25   Rails.cache.set_domain if Rails.cache.respond_to?('set_domain');
26
27   before_filter :instantiate_controller_and_action_names
28   before_filter :set_return_to, :reset_activexml, :authenticate
29   before_filter :check_user
30   before_filter :require_configuration
31   after_filter :set_charset
32   after_filter :validate_xhtml
33   protect_from_forgery
34   has_mobile_views
35
36   if Rails.env.test?
37      prepend_before_filter :start_test_api
38   end
39
40   # Scrub sensitive parameters from your log
41   filter_parameter_logging :password unless Rails.env.test?
42
43   class InvalidHttpMethodError < Exception; end
44   class MissingParameterError < Exception; end
45   class ValidationError < Exception
46     attr_reader :xml, :errors
47
48     def message
49       errors
50     end
51
52     def initialize( _xml, _errors )
53       @xml = _xml
54       @errors = _errors
55     end
56   end
57
58   def is_advanced_tab?
59     ["prjconf", "attributes", "meta", "status"].include? @action_name
60   end
61
62   protected
63
64   def set_return_to
65     if params['return_to_host']
66       @return_to_host = params['return_to_host']
67     else
68       # we have a proxy in front of us
69       @return_to_host = Object.const_defined?(:EXTERNAL_WEBUI_PROTOCOL) ? EXTERNAL_WEBUI_PROTOCOL : "http"
70       @return_to_host += "://"
71       @return_to_host += Object.const_defined?(:EXTERNAL_WEBUI_HOST) ? EXTERNAL_WEBUI_HOST : request.host
72     end
73     @return_to_path = params['return_to_path'] || request.env['REQUEST_URI'].gsub(/.*:\/\/[^\/]*\//, '/').gsub(/&/, '&amp;')
74     logger.debug "Setting return_to: \"#{@return_to_path}\""
75   end
76
77   def set_charset
78     if !request.xhr? && !headers.has_key?('Content-Type')
79       headers['Content-Type'] = "text/html; charset=utf-8"
80     end
81   end
82
83   def require_login
84     if !session[:login]
85       render :text => 'Please login' and return if request.xhr?
86       flash[:error] = "Please login to access the requested page."
87       mode = :off
88       mode = ICHAIN_MODE if defined? ICHAIN_MODE
89       mode = PROXY_AUTH_MODE if defined? PROXY_AUTH_MODE
90       if (mode == :off)
91         redirect_to :controller => :user, :action => :login, :return_to_host => @return_to_host, :return_to_path => @return_to_path
92       else
93         redirect_to :controller => :main, :return_to_host => @return_to_host, :return_to_path => @return_to_path
94       end
95     end
96   end
97
98   # sets session[:login] if the user is authenticated
99   def authenticate
100     mode = :off
101     mode = ICHAIN_MODE if defined? ICHAIN_MODE
102     mode = PROXY_AUTH_MODE if defined? PROXY_AUTH_MODE
103     logger.debug "Authenticating with iChain mode: #{mode}"
104     if mode == :on || mode == :simulate
105       authenticate_ichain
106     else
107       authenticate_form_auth
108     end
109     if session[:login]
110       logger.info "Authenticated request to \"#{@return_to_path}\" from #{session[:login]}"
111     else
112       logger.info "Anonymous request to #{@return_to_path}"
113     end
114   end
115
116   def authenticate_ichain
117     mode = :off
118     mode = ICHAIN_MODE if defined? ICHAIN_MODE
119     mode = PROXY_AUTH_HOST if defined? PROXY_AUTH_HOST
120     ichain_user = request.env['HTTP_X_USERNAME']
121     ichain_user = ICHAIN_TEST_USER if mode == :simulate and ICHAIN_TEST_USER
122     ichain_email = request.env['HTTP_X_EMAIL']
123     ichain_email = ICHAIN_TEST_EMAIL if mode == :simulate and ICHAIN_TEST_EMAIL
124     if ichain_user
125       session[:login] = ichain_user
126       session[:email] = ichain_email
127       # Set the headers for direct connection to the api, TODO: is this thread safe?
128       transport = ActiveXML::Config.transport_for( :project )
129       transport.set_additional_header( "X-Username", ichain_user )
130       transport.set_additional_header( "X-Email", ichain_email ) if ichain_email
131     else
132       session[:login] = nil
133       session[:email] = nil
134     end
135   end
136
137   def authenticate_form_auth
138     if session[:login] and session[:passwd]
139       # pass credentials to transport plugin, TODO: is this thread safe?
140       ActiveXML::Config.transport_for(:project).login session[:login], session[:passwd]
141     end
142   end
143
144   def frontend
145     FrontendCompat.new
146   end
147
148   def valid_project_name? name
149     name =~ /^[[:alnum:]][-+\w.:]+$/
150   end
151
152   def valid_package_name_read? name
153     return true if name == "_project"
154     return true if name == "_product"
155     return true if name == "_deltas"
156     return true if name =~ /^_product:[-_+\w\.:]*$/
157     return true if name =~ /^_patchinfo:[-_+\w\.:]*$/
158     name =~ /^[[:alnum:]][-_+\w\.:]*$/
159   end
160
161   def valid_package_name_write? name
162     return true if name =~ /^_project$/
163     return true if name =~ /^_product$/
164     name =~ /^[[:alnum:]][-_+\w\.]*$/
165   end
166
167   def valid_file_name? name
168     name =~ /^[-\w_+~ ][-\w_\.+~ ]*$/
169   end
170
171   def valid_role_name? name
172     name =~ /^[\w\-_\.+]+$/
173   end
174
175   def valid_target_name? name
176     name =~ /^\w[-_\.\w&]*$/
177   end
178
179   def reset_activexml
180     transport = ActiveXML::Config.transport_for(:project)
181     transport.delete_additional_header "X-Username"
182     transport.delete_additional_header "X-Email"
183     transport.delete_additional_header 'Authorization'
184   end
185
186   def strip_sensitive_data_from(request)
187     # Strip HTTP_AUTHORIZATION header that contains the user's password
188     # try to get it where mod_rewrite might have put it
189     request.env["X-HTTP_AUTHORIZATION"] = "STRIPPED" if request.env.has_key? "X-HTTP_AUTHORIZATION"
190     # for Apace/mod_fastcgi with -pass-header Authorization
191     request.env["Authorization"] = "STRIPPED" if request.env.has_key? "Authorization"
192     # this is the regular location
193     request.env["HTTP_AUTHORIZATION"] = "STRIPPED" if request.env.has_key? "HTTP_AUTHORIZATION"
194     return request
195   end
196   private :strip_sensitive_data_from
197
198   def rescue_action_locally( exception )
199     rescue_action_in_public( exception )
200   end
201
202   def rescue_action_in_public( exception )
203     logger.error "rescue_action: caught #{exception.class}: #{exception.message}"
204     message, code, api_exception = ActiveXML::Transport.extract_error_message exception
205
206     case exception
207     when ActionController::RoutingError
208       render_error :status => 404, :message => "no such route"
209     when ActionController::UnknownAction
210       render_error :status => 404, :message => "unknown action"
211     when ActiveXML::Transport::ForbiddenError
212       # switch to registration on first access
213       if code == "unregistered_ichain_user"
214         render :template => "user/request_ichain" and return
215       elsif code == "unregistered_user"
216         render :template => "user/login" and return
217       elsif code == "unconfirmed_user"
218         render :template => "user/unconfirmed" and return
219       else
220         #ExceptionNotifier.deliver_exception_notification(exception, self, strip_sensitive_data_from(request), {}) if send_exception_mail?
221         if @user
222           render_error :status => 403, :message => message
223         else
224           render_error :status => 401, :message => message
225         end
226       end
227     when ActiveXML::Transport::UnauthorizedError
228       #ExceptionNotifier.deliver_exception_notification(exception, self, strip_sensitive_data_from(request), {}) if send_exception_mail?
229       render_error :status => 401, :message => 'Unauthorized access, please login'
230     when ActionController::InvalidAuthenticityToken
231       render_error :status => 401, :message => 'Invalid authenticity token'
232     when ActiveXML::Transport::ConnectionError
233       render_error :message => "Unable to connect to API host. (#{FRONTEND_HOST})", :status => 503
234     when Timeout::Error
235       render :template => "timeout" and return
236     when ValidationError
237       ExceptionNotifier.deliver_exception_notification(exception, self, strip_sensitive_data_from(request), {}) if send_exception_mail?
238       render :template => "xml_errors", :locals => { :oldbody => exception.xml, :errors => exception.errors }, :status => 400
239     when MissingParameterError 
240       render_error :status => 400, :message => message
241     when InvalidHttpMethodError
242       render_error :message => "Invalid HTTP method used", :status => 400
243     when Net::HTTPBadResponse
244       # The api sometimes sends responses without a proper "Status:..." line (when it restarts?)
245       render_error :message => "Unable to connect to API host. (#{FRONTEND_HOST})", :status => 503
246     else
247       if code != 404 && send_exception_mail?
248         #Note: This is a SUSE-sepecific debugging extension that saves the last
249         #      exception's scope. This method needs a patched Ruby interpreter.
250         if defined?(set_trace_func_for_raise)
251           ExceptionNotifier.deliver_exception_notification(exception, self, strip_sensitive_data_from(request), $exception_scope)
252         else
253           ExceptionNotifier.deliver_exception_notification(exception, self, strip_sensitive_data_from(request), {})
254         end
255       end
256       render_error :status => 400, :code => code, :message => message,
257         :exception => exception, :api_exception => api_exception
258     end
259   end
260
261   def render_error( opt={} )
262     # workaround an exception in mod_rails, it dies when an answer is send without
263     # reading the body. We trigger passenger to read the entire body via requesting the size
264     if request.put? or request.post?
265       request.body.size if request.body.respond_to? 'size'
266     end
267
268     # :code is a string that comes from the api, :status is the http status code
269     @status = opt[:status] || 400
270     @code = opt[:code] || @status
271     @message = opt[:message] || "No message set"
272     @exception = opt[:exception] if local_request?
273     @api_exception = opt[:api_exception] if local_request?
274     logger.debug "ERROR: #{@code}; #{@message}"
275     if request.xhr?
276       render :text => @message, :status => @status, :layout => false
277     else
278       render :template => 'error', :status => @status, :locals => {:code => @code, :message => @message,
279         :exception => @exception, :status => @status, :api_exception => @api_exception }
280     end
281   end
282
283   def valid_http_methods(*methods)
284     methods.map {|x| x.to_s.downcase.to_s}
285     unless methods.include? request.method
286       raise InvalidHttpMethodError, "Invalid HTTP Method: #{request.method.to_s.upcase}"
287     end
288   end
289
290   def required_parameters(*parameters)
291     parameters.each do |parameter|
292       unless params.include? parameter.to_s
293         raise MissingParameterError, "Required Parameter #{parameter} missing"
294       end
295     end
296   end
297
298   def discard_cache?
299     cc = request.headers['Cache-Control']
300     return false if cc.blank?
301     return true if cc == 'max-age=0'
302     return false unless cc == 'no-cache'
303     return !request.xhr?
304   end
305
306   def find_cached(classname, *args)
307     classname.free_cache( *args ) if discard_cache?
308     classname.find_cached( *args )
309   end
310
311   def send_exception_mail?
312     return !local_request? && !Rails.env.development? && ExceptionNotifier.exception_recipients && ExceptionNotifier.exception_recipients.length > 0
313   end
314
315   def instantiate_controller_and_action_names
316     @current_action = action_name
317     @current_controller = controller_name
318   end
319
320   def check_user
321     return unless session[:login]
322     Rails.cache.delete("person_#{session[:login]}") if discard_cache?
323     @user ||= find_cached(Person, session[:login])
324     if @user
325       Rails.cache.set_domain(@user.to_s) if Rails.cache.respond_to?('set_domain');
326       begin
327         @nr_involved_requests = @user.involved_requests(:cache => !discard_cache?).size
328       # add all temporary errors here, but no catch all
329       rescue Timeout::Error
330       end
331     end
332   end
333
334   def map_to_workers(arch)
335     case arch
336     when 'i586' then 'x86_64'
337     when 'ppc' then 'ppc64'
338     when 's390' then 's390x'
339     else arch
340     end
341   end
342  
343   private
344
345   def assert_xml_validates
346   end
347
348   def put_body_to_tempfile(xmlbody)
349     file = Tempfile.new('xml').path
350     file = File.open(file + ".xml", "w")
351     file.write(xmlbody)
352     file.close
353     return file.path
354   end
355   private :put_body_to_tempfile
356
357   def validate_xhtml
358     return if Rails.env.production? or Rails.env.stage?
359     return if request.xhr?
360     return if mobile_request?
361     return if !(response.status =~ /200/ && response.headers['Content-Type'] =~ /text\/html/i)
362
363     errors = []
364     xmlbody = String.new response.body
365     xmlbody.gsub!(/[\n\r]/, "\n")
366     xmlbody.gsub!(/&[^;]*sp;/, '')
367     
368     begin
369       document = Nokogiri::XML::Document.parse(xmlbody, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
370     rescue Nokogiri::XML::SyntaxError => e
371       errors << e.inspect
372       errors << put_body_to_tempfile(xmlbody)
373     end
374
375     if document
376       ses = XHTML_XSD.validate(document)
377       unless ses.empty?
378         document = nil
379         errors << put_body_to_tempfile(xmlbody) 
380         ses.each do |e|
381           errors << e.inspect
382         end
383       end
384     end
385
386     unless document
387       erase_render_results
388       raise ValidationError.new xmlbody, errors
389     end
390   end
391
392   @@frontend = nil
393   def start_test_api
394     return if @@frontend
395     @@frontend = IO.popen("#{RAILS_ROOT}/script/start_test_api")
396     puts "Starting test API with pid: #{@@frontend.pid}"
397     while true do
398       line = @@frontend.gets
399       raise RuntimeError.new('Frontend died') unless line
400       break if line =~ /Test API ready/
401       logger.debug line.strip
402     end
403     puts "Test API up and running with pid: #{@@frontend.pid}"
404     at_exit do
405        puts "Killing test API with pid: #{@@frontend.pid}"
406        Process.kill "INT", @@frontend.pid
407        @@frontend = nil
408     end
409   end
410
411   def require_configuration
412     @configuration = {}
413     begin
414       @configuration = Rails.cache.fetch('configuration', :expires_in => 30.minutes) do
415         response = ActiveXML::Config::transport_for(:configuration).direct_http(URI('/configuration.json'))
416         ActiveSupport::JSON.decode(response)
417       end
418     rescue ActiveXML::Transport::NotFoundError
419       logger.error 'Site configuration not found'
420     rescue ActiveXML::Transport::UnauthorizedError => e
421       @anonymous_forbidden = true
422       logger.error 'Could not load all frontpage data, probably due to forbidden anonymous access in the api.'
423     end
424   end
425
426   # Before filter to check if current user is administrator
427   def require_admin
428     if @user and not @user.is_admin?
429       flash[:error] = "Requires admin privileges"
430       redirect_back_or_to :controller => 'main', :action => 'index' and return
431     end
432   end
433
434 end