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

#allowed_paramsObject



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/restful_json/controller.rb', line 313

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(action_sym, value) ⇒ Object



306
307
308
309
310
311
# File 'lib/restful_json/controller.rb', line 306

def apply_includes(action_sym, value)
  this_includes = self.action_to_query_includes[action_sym] || self.query_includes
  if this_includes
    value.includes(*this_includes)
  end
end

#convert_request_param_value_for_filtering(attr_sym, value) ⇒ Object



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

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.



559
560
561
562
563
564
565
566
567
# File 'lib/restful_json/controller.rb', line 559

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

#destroyObject

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



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/restful_json/controller.rb', line 584

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)
  p_params = 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

#editObject

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



546
547
548
549
550
551
552
553
554
555
556
# File 'lib/restful_json/controller.rb', line 546

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.



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/restful_json/controller.rb', line 248

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.



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

def find_model_instance
  # 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)}
    @value = c.first
  else
    @value = @model_class.where(@model_class.primary_key.to_sym => params[@model_class.primary_key].to_s).first
  end
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.



292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/restful_json/controller.rb', line 292

def find_model_instance!
  # 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
    apply_includes params[:action].to_sym, value
    c.primary_key.each {|pkey|c.where(pkey.to_sym => params[pkey].to_s)}
    # raise exception if not found
    @value = c.first!
  else
    @value = @model_class.where(@model_class.primary_key.to_sym => params[@model_class.primary_key].to_s).first! # raise exception if not found
  end
end

#handle_or_raise(e) ⇒ Object



262
263
264
265
266
267
268
269
270
# File 'lib/restful_json/controller.rb', line 262

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)


237
238
239
# File 'lib/restful_json/controller.rb', line 237

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.



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
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
# File 'lib/restful_json/controller.rb', line 408

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
  action_sym = params[:action].to_sym
  p_params = allowed_params
  t = @model_class.arel_table
  value = @model_class.scoped # returns ActiveRecord::Relation equivalent to select with no where clause
  custom_query = self.action_to_query[action_sym]
  if custom_query
    value = custom_query.call(t, value)
  end

  apply_includes action_sym, 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.scoped.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.



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
228
229
# File 'lib/restful_json/controller.rb', line 200

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

#newObject

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



533
534
535
536
537
538
539
540
541
542
543
# File 'lib/restful_json/controller.rb', line 533

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.



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/restful_json/controller.rb', line 348

def render_error(e, handling_data)
  i18n_key = handling_data[:i18n_key]
  msg = result = 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



367
368
369
370
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
# File 'lib/restful_json/controller.rb', line 367

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
          # define local variables in blocks, not outside of them, to be safe, even though would work in this case              
          custom_action_serializer = self.action_to_serializer[params[:action].to_sym]
          custom_action_serializer_for = self.action_to_serializer_for[params[:action].to_sym]
          serialization_key = single_value_response? ? (custom_action_serializer_for == :each ? :each_serializer : :serializer) : (custom_action_serializer_for == :array ? :serializer : :each_serializer)
          if !@value.respond_to?(:errors) || @value.errors.empty?
            render custom_action_serializer ? {json: @value, status: success_code, serialization_key => custom_action_serializer} : {json: @value, status: success_code}
          else
            render custom_action_serializer ? {json: {errors: @value.errors}, status: :unprocessable_entity, serialization_key => custom_action_serializer} : {json: {errors: @value.errors}, status: :unprocessable_entity}
          end
        end
      end
    else
      # code duplicated from above because local vars don't always traverse well into block (based on wierd ruby-proc bug experienced)
      custom_action_serializer = self.action_to_serializer[params[:action].to_sym]
      custom_action_serializer_for = self.action_to_serializer_for[params[:action].to_sym]
      serialization_key = single_value_response? ? (custom_action_serializer_for == :array ? :serializer : :each_serializer) : (custom_action_serializer_for == :each ? :each_serializer : :serializer)
      respond_with @value, custom_action_serializer ? {(self.action_to_serializer_for[params[:action].to_sym] == :each ? :each_serializer : :serializer) => custom_action_serializer} : {}
    end
  else
    @value
  end
end

#showObject

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



520
521
522
523
524
525
526
527
528
529
530
# File 'lib/restful_json/controller.rb', line 520

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)


401
402
403
# File 'lib/restful_json/controller.rb', line 401

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

#updateObject

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



570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/restful_json/controller.rb', line 570

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