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