Module: RestfulJson::Controller

Extended by:
ActiveSupport::Concern
Defined in:
lib/restful_json/controller.rb

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

NILS =
['NULL'.freeze, 'null'.freeze, 'nil'.freeze]
SINGLE_VALUE_ACTIONS =
[:create, :update, :destroy, :show, :new, :edit]

Instance Method Summary collapse

Instance Method Details

#additional_render_or_respond_success_optionsObject

Returns additional rendering options. By default will massage self.action_to_render_options a little and return that, e.g. if you had used serialize_action to specify an array and each serializer for a specific action, if it is that action, it may return something like: MyFooArraySerializer, each_serializer: MyFooSerializer. If you’d like to do something custom in some situations, but default in others, you may also call default_additional_render_or_respond_success_options from within this method to get the defaults.



412
413
414
# File 'lib/restful_json/controller.rb', line 412

def additional_render_or_respond_success_options
  default_additional_render_or_respond_success_options
end

#allowed_paramsObject



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/restful_json/controller.rb', line 317

def allowed_params
  action_sym = params[:action].to_sym
  singular = single_value_response?
  action_specific_params_method = singular ? (@action_to_singular_action_model_params_method[action_sym] ||= "#{action_sym}_#{@model_singular_name}_params".to_sym) : (@action_to_plural_action_model_params_method[action_sym] ||= "#{action_sym}_#{@model_plural_name}_params".to_sym)
  model_name_params_method = singular ? @model_singular_name_params_sym : @model_plural_name_params_sym
  
  if self.actions_that_authorize.include?(action_sym)
    authorize! action_sym, @model_class
  end

  if self.actions_that_permit.include?(action_sym)
    if self.use_permitters
      return permitted_params_using(self.action_to_permitter[action_sym] || permitter_class)
    elsif self.allow_action_specific_params_methods && respond_to?(action_specific_params_method)
      return __send__(action_specific_params_method)
    elsif self.actions_supporting_params_methods.include?(action_sym) && respond_to?(model_name_params_method)
      return __send__(model_name_params_method)
    end
  end

  params
end

#apply_includes(value) ⇒ Object



309
310
311
312
313
314
315
# File 'lib/restful_json/controller.rb', line 309

def apply_includes(value)
  this_includes = current_action_includes
  if this_includes
    value = value.includes(*this_includes)
  end
  value
end

#convert_request_param_value_for_filtering(attr_sym, value) ⇒ Object



234
235
236
# File 'lib/restful_json/controller.rb', line 234

def convert_request_param_value_for_filtering(attr_sym, value)
  value && NILS.include?(value) ? nil : value
end

#createObject

The controller’s create (post) method to create a resource.



580
581
582
583
584
585
586
587
588
# File 'lib/restful_json/controller.rb', line 580

def create
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = @model_class.new(allowed_params)
  @value.save
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(false, :created)
rescue self.rescue_class => e
  handle_or_raise(e)
end

#current_action_includesObject



305
306
307
# File 'lib/restful_json/controller.rb', line 305

def current_action_includes
  self.action_to_query_includes[params[:action].to_sym] || self.query_includes
end

#default_additional_render_or_respond_success_optionsObject



416
417
418
419
420
421
422
423
424
425
# File 'lib/restful_json/controller.rb', line 416

def default_additional_render_or_respond_success_options
  result = {}
  if self.action_to_render_options[params[:action].to_sym]
    custom_action_serializer = self.action_to_render_options[params[:action].to_sym][:restful_json_serialization_default]
    result[(single_value_response? ? :serializer : :each_serializer)] = custom_action_serializer if custom_action_serializer
    custom_action_array_serializer = self.action_to_render_options[params[:action].to_sym][:restful_json_serialization_array]
    result[:serializer] = custom_action_array_serializer if custom_action_array_serializer && !single_value_response?
  end
  result
end

#destroyObject

The controller’s destroy (delete) method to destroy a resource.



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/restful_json/controller.rb', line 605

def destroy
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  # don't raise error- DELETE should be idempotent per REST.
  @value = find_model_instance
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  @value.destroy if @value
  instance_variable_set(@model_at_singular_name_sym, @value)
  if !@value.respond_to?(:errors) || @value.errors.empty? || (request.format != 'text/html' && request.content_type != 'text/html')
    # don't require a destroy view for success, because it isn't implements in Rails by default for json
    respond_to do |format|
      format.any  { head :ok }
    end
  else
    render_or_respond(false)
  end
rescue self.rescue_class => e
  handle_or_raise(e)
end

#do_find_model_instance(first_method) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/restful_json/controller.rb', line 275

def do_find_model_instance(first_method)
  # to_s as safety measure for vulnerabilities similar to CVE-2013-1854.
  # primary_key array support for composite_primary_keys.
  if @model_class.primary_key.is_a? Array
    c = @model_class
    c.primary_key.each {|pkey|c.where(pkey.to_sym => params[pkey].to_s)}
  else
    c = @model_class.where(@model_class.primary_key.to_sym => params[@model_class.primary_key].to_s)
  end

  c = apply_includes(c)
  @value = c.send first_method
end

#editObject

The controller’s edit method (e.g. used for edit record in html format).



567
568
569
570
571
572
573
574
575
576
577
# File 'lib/restful_json/controller.rb', line 567

def edit
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  instance_variable_set(@model_at_singular_name_sym, @value)
  @value
rescue self.rescue_class => e
  handle_or_raise(e)
end

#exception_handling_data(e) ⇒ Object

Searches through self.rescue_handlers for appropriate handler. self.rescue_handlers is an array of hashes where there is key :exception_classes and/or :exception_ancestor_classes along with :i18n_key and :status keys. :exception_classes contains an array of classes to exactly match the exception. :exception_ancestor_classes contains an array of classes that can match an ancestor of the exception. If exception handled, returns hash, hopefully containing keys :i18n_key and :status. Otherwise, returns nil which indicates that this exception should not be handled.



251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/restful_json/controller.rb', line 251

def exception_handling_data(e)
  self.rescue_handlers.each do |handler|
    return handler if (handler.key?(:exception_classes) && handler[:exception_classes].include?(e.class))
    if handler.key?(:exception_ancestor_classes)
      handler[:exception_ancestor_classes].each do |ancestor|
        return handler if e.class.ancestors.include?(ancestor)
      end
    elsif !handler.key?(:exception_classes) && !handler.key?(:exception_ancestor_classes)
      return handler
    end
  end
  nil
end

#find_model_instanceObject

Finds model using provided info in params, prior to any permittance, via where()…first.

Supports composite_keys.



293
294
295
# File 'lib/restful_json/controller.rb', line 293

def find_model_instance
  do_find_model_instance(:first)
end

#find_model_instance!Object

Finds model using provided info in params, prior to any permittance, via where()…first! with exception raise if does not exist.

Supports composite_keys.



301
302
303
# File 'lib/restful_json/controller.rb', line 301

def find_model_instance!
  do_find_model_instance(:first!)
end

#handle_or_raise(e) ⇒ Object



265
266
267
268
269
270
271
272
273
# File 'lib/restful_json/controller.rb', line 265

def handle_or_raise(e)
  raise e if self.rescue_class.nil?
  handling_data = exception_handling_data(e)
  raise e unless handling_data
  # this is something we intended to rescue, so log it
  logger.error(e)
  # render error only if we haven't rendered response yet
  render_error(e, handling_data) unless @performed_render
end

#include_error_data?Boolean

Returns self.return_error_data by default. To only return error_data in dev and test, use this: ‘def enable_long_error?; Rails.env.development? || Rails.env.test?; end`

Returns:

  • (Boolean)


240
241
242
# File 'lib/restful_json/controller.rb', line 240

def include_error_data?
  self.return_error_data
end

#indexObject

The controller’s index (list) method to list resources.

Note: this method be alias_method’d by query_for, so it is more than just index.



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'lib/restful_json/controller.rb', line 430

def index
  # could be index or another action if alias_method'd by query_for
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  p_params = allowed_params
  t = @model_class.arel_table
  value = model_class_scoped
  custom_query = self.action_to_query[params[:action].to_sym]
  if custom_query
    value = custom_query.call(t, value)
  end

  value = apply_includes(value)

  self.param_to_query.each do |param_name, param_query|
    if params[param_name]
      # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
      value = param_query.call(t, value, p_params[param_name].to_s)
    end
  end

  self.param_to_through.each do |param_name, through_array|
    if p_params[param_name]
      # build query
      # e.g. SomeModel.all.joins({:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}).where(sub_sub_sub_assoc_model_table_name: {column_name: value})
      last_model_class = @model_class
      joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
      through_array.each do |association_or_attribute|
        if association_or_attribute == through_array.last
          # must convert param value to string before possibly using with ARel because of CVE-2013-1854, fixed in: 3.2.13 and 3.1.12 
          # https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
          value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => p_params[param_name].to_s})
        else
          found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
          if found_classes.size > 0
            last_model_class = found_classes[0]
          else
            # bad can_filter_by :through found at runtime
            raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
          end

          if joins.nil?
            joins = association_or_attribute
          else
            joins = {association_or_attribute => joins}
          end
        end
      end
    end
  end

  self.param_to_attr_and_arel_predicate.keys.each do |param_name|
    options = param_to_attr_and_arel_predicate[param_name][2]
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854 
    param = p_params[param_name].to_s || options[:with_default]

    if param.present? && param_to_attr_and_arel_predicate[param_name]
      attr_sym = param_to_attr_and_arel_predicate[param_name][0]
      predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
      if predicate_sym == :eq
        value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
      else
        one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
        value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
      end
    end
  end

  if p_params[:page] && self.supported_functions.include?(:page)
    page = p_params[:page].to_i
    page = 1 if page < 1 # to avoid people using this as a way to get all records unpaged, as that probably isn't the intent?
    #TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
    value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
    value = value.take((self.number_of_records_in_a_page).to_s)
  end

  if p_params[:skip] && self.supported_functions.include?(:skip)
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
    value = value.skip(p_params[:skip].to_s)
  end

  if p_params[:take] && self.supported_functions.include?(:take)
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
    value = value.take(p_params[:take].to_s)
  end

  if p_params[:uniq] && self.supported_functions.include?(:uniq)
    value = value.uniq
  end

  # these must happen at the end and are independent
  if p_params[:count] && self.supported_functions.include?(:count)
    value = value.count.to_i
  elsif p_params[:page_count] && self.supported_functions.include?(:page_count)
    count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
    value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
  else
    #TODO: also declaratively specify order via order=attr1,attr2, etc. like can_filter_by w/queries, subattrs, and direction.
    self.ordered_by.each do |attr_to_direction|
      # this looks nasty, but makes no sense to iterate keys if only single of each
      value = value.order(t[attr_to_direction.keys[0]].send(attr_to_direction.values[0]))
    end
    value = value.to_a
  end      
  @value = value
  instance_variable_set(@model_at_plural_name_sym, @value)
  render_or_respond(true)
rescue self.rescue_class => e
  handle_or_raise(e)
end

#initializeObject

In initialize we:

  • guess model name, if unspecified, from controller name

  • define instance variables containing model name

  • define the (model_plural_name)_url method, needed if controllers are not in the same module as the models

Note: if controller name is not based on model name and controller is in different module than model, you’ll need to redefine the appropriate method(s) to return urls if needed.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/restful_json/controller.rb', line 198

def initialize
  super

  # if not set, use controller classname
  qualified_controller_name = self.class.name.chomp('Controller')
  @model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize

  raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
  raise "#{self.class.name} assumes that #{self.model_class} extends ActiveRecord::Base, but it didn't. Please fix, or remove this constraint." unless @model_class.ancestors.include?(ActiveRecord::Base)

  @model_singular_name = self.model_singular_name || self.model_class.name.underscore
  @model_plural_name = self.model_plural_name || @model_singular_name.pluralize
  @model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
  @model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
  
  # default methods for strong parameters
  @model_plural_name_params_sym = "#{@model_plural_name}_params".to_sym
  @model_singular_name_params_sym = "#{@model_singular_name}_params".to_sym

  @action_to_singular_action_model_params_method = {}
  @action_to_plural_action_model_params_method = {}

  underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore

  # This is a workaround for controllers that are in a different module than the model only works if the controller's base part of the unqualified name in the plural model name.
  # If the model name is different than the controller name, you will need to define methods to return the right urls.
  class_eval "def #{@model_plural_name}_url;#{underscored_modules_and_underscored_plural_model_name}_url;end" unless @model_plural_name == underscored_modules_and_underscored_plural_model_name
  singularized_underscored_modules_and_underscored_plural_model_name = underscored_modules_and_underscored_plural_model_name
  class_eval "def #{@model_singular_name}_url(record);#{singularized_underscored_modules_and_underscored_plural_model_name}_url(record);end" unless @model_singular_name == singularized_underscored_modules_and_underscored_plural_model_name
end

#model_class_scopedObject

If Rails 3, returns @model_class.scoped. If not, returns @model_class.all. This helps avoid a deprecation warning.



230
231
232
# File 'lib/restful_json/controller.rb', line 230

def model_class_scoped
  Rails::VERSION::MAJOR == 3 ? @model_class.scoped : @model_class.all
end

#newObject

The controller’s new method (e.g. used for new record in html format).



554
555
556
557
558
559
560
561
562
563
564
# File 'lib/restful_json/controller.rb', line 554

def new
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  @value = @model_class.new
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true)
rescue self.rescue_class => e
  handle_or_raise(e)
end

#render_error(e, handling_data) ⇒ Object

Renders error using handling data options (where options are probably hash from self.rescue_handlers that was matched).

If include_error_data? is true, it returns something like the following (with the appropriate HTTP status code via setting appropriate status in respond_do: “not_found”,

"error": "Internationalized error message or e.message",
"error_data": {"type": "ActiveRecord::RecordNotFound", "message": "Couldn't find Bar with id=23423423", "trace": ["backtrace line 1", ...]

}

If include_error_data? is false, it returns something like: {“status”: “not_found”, “error”, “Couldn’t find Bar with id=23423423”}

It handles any format in theory that is supported by respond_to and has a ‘to_(some format)` method.



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/restful_json/controller.rb', line 352

def render_error(e, handling_data)
  i18n_key = handling_data[:i18n_key]
  msg = t(i18n_key, default: e.message)
  status = handling_data[:status] || :internal_server_error
  if include_error_data?
    respond_to do |format|
      format.html { render notice: msg }
      format.any { render request.format.to_sym => {status: status, error: msg, error_data: {type: e.class.name, message: e.message, trace: Rails.backtrace_cleaner.clean(e.backtrace)}}, status: status }
    end
  else
    respond_to do |format|
      format.html { render notice: msg }
      format.any { render request.format.to_sym => {status: status, error: msg}, status: status }
    end
  end
  # return exception so we know it was handled
  e
end

#render_or_respond(read_only_action, success_code = :ok) ⇒ Object



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/restful_json/controller.rb', line 371

def render_or_respond(read_only_action, success_code = :ok)
  if self.render_enabled
    # 404/not found is just for update (not destroy, because idempotent destroy = no 404)
    if success_code == :not_found
      respond_to do |format|
        format.html { render file: "#{Rails.root}/public/404.html", status: :not_found }
        format.any  { head :not_found }
      end
    elsif !@value.nil? && ((read_only_action && RestfulJson.return_resource) || RestfulJson.avoid_respond_with)
      respond_with(@value) do |format|
        format.json do
          if !@value.respond_to?(:errors) || @value.errors.empty?
            result = {json: @value, status: success_code}
            result.merge!(additional_render_or_respond_success_options)
          else
            result = {json: {errors: @value.errors}, status: :unprocessable_entity}
          end
          render result
        end
      end
    else
      if !@value.respond_to?(:errors) || @value.errors.empty?
        respond_with @value, additional_render_or_respond_success_options
      else
        respond_with @value
      end
    end
  else
    @value
  end
end

#showObject

The controller’s show (get) method to return a resource.



541
542
543
544
545
546
547
548
549
550
551
# File 'lib/restful_json/controller.rb', line 541

def show
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true, @value.nil? ? :not_found : :ok)
rescue self.rescue_class => e
  handle_or_raise(e)
end

#single_value_response?Boolean

Returns:

  • (Boolean)


403
404
405
# File 'lib/restful_json/controller.rb', line 403

def single_value_response?
  SINGLE_VALUE_ACTIONS.include?(params[:action].to_sym)
end

#updateObject

The controller’s update (put) method to update a resource.



591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/restful_json/controller.rb', line 591

def update
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  p_params = allowed_params
  @value.update_attributes(p_params) unless @value.nil?
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true, @value.nil? ? :not_found : :ok)
rescue self.rescue_class => e
  handle_or_raise(e)
end