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