2961: Changed index methods on some controllers to use render_index instead of
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   include ArvadosApiClientHelper
3
4   respond_to :html, :json, :js
5   protect_from_forgery
6
7   ERROR_ACTIONS = [:render_error, :render_not_found]
8
9   around_filter :thread_clear
10   around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS
11   around_filter :thread_with_optional_api_token
12   before_filter :check_user_agreements, except: ERROR_ACTIONS
13   before_filter :check_user_notifications, except: ERROR_ACTIONS
14   before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS
15   theme :select_theme
16
17   begin
18     rescue_from Exception,
19     :with => :render_exception
20     rescue_from ActiveRecord::RecordNotFound,
21     :with => :render_not_found
22     rescue_from ActionController::RoutingError,
23     :with => :render_not_found
24     rescue_from ActionController::UnknownController,
25     :with => :render_not_found
26     rescue_from ::AbstractController::ActionNotFound,
27     :with => :render_not_found
28   end
29
30   def unprocessable(message=nil)
31     @errors ||= []
32
33     @errors << message if message
34     render_error status: 422
35   end
36
37   def render_error(opts)
38     opts = {status: 500}.merge opts
39     respond_to do |f|
40       # json must come before html here, so it gets used as the
41       # default format when js is requested by the client. This lets
42       # ajax:error callback parse the response correctly, even though
43       # the browser can't.
44       f.json { render opts.merge(json: {success: false, errors: @errors}) }
45       f.html { render opts.merge(controller: 'application', action: 'error') }
46     end
47   end
48
49   def render_exception(e)
50     logger.error e.inspect
51     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
52     if @object.andand.errors.andand.full_messages.andand.any?
53       @errors = @object.errors.full_messages
54     else
55       @errors = [e.to_s]
56     end
57     self.render_error status: 422
58   end
59
60   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
61     logger.error e.inspect
62     @errors = ["Path not found"]
63     self.render_error status: 404
64   end
65
66   def render_index
67     respond_to do |f|
68       f.json { render json: @objects }
69       f.html {
70         if params['tab_pane']
71           comparable = self.respond_to? :compare
72           render(partial: 'show_' + params['tab_pane'].downcase,
73                  locals: { comparable: comparable, objects: @objects })
74         else
75           render
76         end
77       }
78       f.js { render }
79     end
80   end
81
82   def index
83     @limit ||= 200
84     if params[:limit]
85       @limit = params[:limit].to_i
86     end
87
88     @offset ||= 0
89     if params[:offset]
90       @offset = params[:offset].to_i
91     end
92
93     @filters ||= []
94     if params[:filters]
95       filters = params[:filters]
96       if filters.is_a? String
97         filters = Oj.load filters
98       end
99       @filters += filters
100     end
101
102     @objects ||= model_class
103     @objects = @objects.filter(@filters).limit(@limit).offset(@offset).all
104     render_index
105   end
106
107   def show
108     if !@object
109       return render_not_found("object not found")
110     end
111     respond_to do |f|
112       f.json { render json: @object.attributes.merge(href: url_for(@object)) }
113       f.html {
114         if params['tab_pane']
115           comparable = self.respond_to? :compare
116           render(partial: 'show_' + params['tab_pane'].downcase,
117                  locals: { comparable: comparable, objects: @objects })
118         else
119           if request.method == 'GET'
120             render
121           else
122             redirect_to params[:return_to] || @object
123           end
124         end
125       }
126       f.js { render }
127     end
128   end
129
130   def render_content
131     if !@object
132       return render_not_found("object not found")
133     end
134   end
135
136   def new
137     @object = model_class.new
138   end
139
140   def update
141     @updates ||= params[@object.class.to_s.underscore.singularize.to_sym]
142     @updates.keys.each do |attr|
143       if @object.send(attr).is_a? Hash
144         if @updates[attr].is_a? String
145           @updates[attr] = Oj.load @updates[attr]
146         end
147         if params[:merge] || params["merge_#{attr}".to_sym]
148           # Merge provided Hash with current Hash, instead of
149           # replacing.
150           @updates[attr] = @object.send(attr).with_indifferent_access.
151             deep_merge(@updates[attr].with_indifferent_access)
152         end
153       end
154     end
155     if @object.update_attributes @updates
156       show
157     else
158       self.render_error status: 422
159     end
160   end
161
162   def create
163     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
164     @new_resource_attrs ||= {}
165     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
166     @object ||= model_class.new @new_resource_attrs, params["options"]
167     if @object.save
168       respond_to do |f|
169         f.json { render json: @object.attributes.merge(href: url_for(@object)) }
170         f.html {
171           redirect_to @object
172         }
173         f.js { render }
174       end
175     else
176       self.render_error status: 422
177     end
178   end
179
180   def destroy
181     if @object.destroy
182       respond_to do |f|
183         f.json { render json: @object }
184         f.html {
185           redirect_to(params[:return_to] || :back)
186         }
187         f.js { render }
188       end
189     else
190       self.render_error status: 422
191     end
192   end
193
194   def current_user
195     return Thread.current[:user] if Thread.current[:user]
196
197     if Thread.current[:arvados_api_token]
198       if session[:user]
199         if session[:user][:is_active] != true
200           Thread.current[:user] = User.current
201         else
202           Thread.current[:user] = User.new(session[:user])
203         end
204       else
205         Thread.current[:user] = User.current
206       end
207     else
208       logger.error "No API token in Thread"
209       return nil
210     end
211   end
212
213   def model_class
214     controller_name.classify.constantize
215   end
216
217   def breadcrumb_page_name
218     (@breadcrumb_page_name ||
219      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
220      action_name)
221   end
222
223   def index_pane_list
224     %w(Recent)
225   end
226
227   def show_pane_list
228     %w(Attributes Metadata JSON API)
229   end
230
231   protected
232
233   def redirect_to_login
234     respond_to do |f|
235       f.html {
236         if request.method == 'GET'
237           redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
238         else
239           flash[:error] = "Either you are not logged in, or your session has timed out. I can't automatically log you in and re-attempt this request."
240           redirect_to :back
241         end
242       }
243       f.json {
244         @errors = ['You do not seem to be logged in. You did not supply an API token with this request, and your session (if any) has timed out.']
245         self.render_error status: 422
246       }
247     end
248     false  # For convenience to return from callbacks
249   end
250
251   def using_specific_api_token(api_token)
252     start_values = {}
253     [:arvados_api_token, :user].each do |key|
254       start_values[key] = Thread.current[key]
255     end
256     Thread.current[:arvados_api_token] = api_token
257     Thread.current[:user] = nil
258     begin
259       yield
260     ensure
261       start_values.each_key { |key| Thread.current[key] = start_values[key] }
262     end
263   end
264
265   def find_object_by_uuid
266     if params[:id] and params[:id].match /\D/
267       params[:uuid] = params.delete :id
268     end
269     if not model_class
270       @object = nil
271     elsif params[:uuid].is_a? String
272       if params[:uuid].empty?
273         @object = nil
274       else
275         @object = model_class.find(params[:uuid])
276       end
277     else
278       @object = model_class.where(uuid: params[:uuid]).first
279     end
280   end
281
282   def thread_clear
283     Thread.current[:arvados_api_token] = nil
284     Thread.current[:user] = nil
285     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
286     yield
287     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
288   end
289
290   def thread_with_api_token(login_optional = false)
291     begin
292       try_redirect_to_login = true
293       if params[:api_token]
294         try_redirect_to_login = false
295         Thread.current[:arvados_api_token] = params[:api_token]
296         # Before copying the token into session[], do a simple API
297         # call to verify its authenticity.
298         if verify_api_token
299           session[:arvados_api_token] = params[:api_token]
300           u = User.current
301           session[:user] = {
302             email: u.email,
303             first_name: u.first_name,
304             last_name: u.last_name,
305             is_active: u.is_active,
306             is_admin: u.is_admin,
307             prefs: u.prefs
308           }
309           if !request.format.json? and request.method == 'GET'
310             # Repeat this request with api_token in the (new) session
311             # cookie instead of the query string.  This prevents API
312             # tokens from appearing in (and being inadvisedly copied
313             # and pasted from) browser Location bars.
314             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
315           else
316             yield
317           end
318         else
319           @errors = ['Invalid API token']
320           self.render_error status: 401
321         end
322       elsif session[:arvados_api_token]
323         # In this case, the token must have already verified at some
324         # point, but it might have been revoked since.  We'll try
325         # using it, and catch the exception if it doesn't work.
326         try_redirect_to_login = false
327         Thread.current[:arvados_api_token] = session[:arvados_api_token]
328         begin
329           yield
330         rescue ArvadosApiClient::NotLoggedInException
331           try_redirect_to_login = true
332         end
333       else
334         logger.debug "No token received, session is #{session.inspect}"
335       end
336       if try_redirect_to_login
337         unless login_optional
338           redirect_to_login
339         else
340           # login is optional for this route so go on to the regular controller
341           Thread.current[:arvados_api_token] = nil
342           yield
343         end
344       end
345     ensure
346       # Remove token in case this Thread is used for anything else.
347       Thread.current[:arvados_api_token] = nil
348     end
349   end
350
351   def thread_with_mandatory_api_token
352     thread_with_api_token do
353       yield
354     end
355   end
356
357   # This runs after thread_with_mandatory_api_token in the filter chain.
358   def thread_with_optional_api_token
359     if Thread.current[:arvados_api_token]
360       # We are already inside thread_with_mandatory_api_token.
361       yield
362     else
363       # We skipped thread_with_mandatory_api_token. Use the optional version.
364       thread_with_api_token(true) do
365         yield
366       end
367     end
368   end
369
370   def verify_api_token
371     begin
372       Link.where(uuid: 'just-verifying-my-api-token')
373       true
374     rescue ArvadosApiClient::NotLoggedInException
375       false
376     end
377   end
378
379   def ensure_current_user_is_admin
380     unless current_user and current_user.is_admin
381       @errors = ['Permission denied']
382       self.render_error status: 401
383     end
384   end
385
386   def check_user_agreements
387     if current_user && !current_user.is_active && current_user.is_invited
388       signatures = UserAgreement.signatures
389       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
390       @required_user_agreements = UserAgreement.all.map do |ua|
391         if not @signed_ua_uuids.index ua.uuid
392           Collection.find(ua.uuid)
393         end
394       end.compact
395       if @required_user_agreements.empty?
396         # No agreements to sign. Perhaps we just need to ask?
397         current_user.activate
398         if !current_user.is_active
399           logger.warn "#{current_user.uuid.inspect}: " +
400             "No user agreements to sign, but activate failed!"
401         end
402       end
403       if !current_user.is_active
404         render 'user_agreements/index'
405       end
406     end
407     true
408   end
409
410   def select_theme
411     return Rails.configuration.arvados_theme
412   end
413
414   @@notification_tests = []
415
416   @@notification_tests.push lambda { |controller, current_user|
417     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
418       return nil
419     end
420     return lambda { |view|
421       view.render partial: 'notifications/ssh_key_notification'
422     }
423   }
424
425   #@@notification_tests.push lambda { |controller, current_user|
426   #  Job.limit(1).where(created_by: current_user.uuid).each do
427   #    return nil
428   #  end
429   #  return lambda { |view|
430   #    view.render partial: 'notifications/jobs_notification'
431   #  }
432   #}
433
434   @@notification_tests.push lambda { |controller, current_user|
435     Collection.limit(1).where(created_by: current_user.uuid).each do
436       return nil
437     end
438     return lambda { |view|
439       view.render partial: 'notifications/collections_notification'
440     }
441   }
442
443   @@notification_tests.push lambda { |controller, current_user|
444     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
445       return nil
446     end
447     return lambda { |view|
448       view.render partial: 'notifications/pipelines_notification'
449     }
450   }
451
452   def check_user_notifications
453     return if params['tab_pane']
454
455     @notification_count = 0
456     @notifications = []
457
458     if current_user
459       @showallalerts = false
460       @@notification_tests.each do |t|
461         a = t.call(self, current_user)
462         if a
463           @notification_count += 1
464           @notifications.push a
465         end
466       end
467     end
468
469     if @notification_count == 0
470       @notification_count = ''
471     end
472   end
473
474   helper_method :my_folders
475   def my_folders
476     return @my_folders if @my_folders
477     @my_folders = []
478     root_of = {}
479     Group.filter([['group_class','=','folder']]).each do |g|
480       root_of[g.uuid] = g.owner_uuid
481       @my_folders << g
482     end
483     done = false
484     while not done
485       done = true
486       root_of = root_of.each_with_object({}) do |(child, parent), h|
487         if root_of[parent]
488           h[child] = root_of[parent]
489           done = false
490         else
491           h[child] = parent
492         end
493       end
494     end
495     @my_folders = @my_folders.select do |g|
496       root_of[g.uuid] == current_user.uuid
497     end
498   end
499 end