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
- #allowed_params ⇒ Object
- #apply_includes(action_sym, value) ⇒ Object
- #convert_request_param_value_for_filtering(attr_sym, value) ⇒ Object
-
#create ⇒ Object
The controller’s create (post) method to create a resource.
-
#destroy ⇒ Object
The controller’s destroy (delete) method to destroy a resource.
-
#edit ⇒ Object
The controller’s edit method (e.g. used for edit record in html format).
-
#exception_handling_data(e) ⇒ Object
Searches through self.rescue_handlers for appropriate handler.
-
#find_model_instance ⇒ Object
Finds model using provided info in params, prior to any permittance, via where()…first.
-
#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.
- #handle_or_raise(e) ⇒ Object
-
#include_error_data? ⇒ Boolean
Returns self.return_error_data by default.
-
#index ⇒ Object
The controller’s index (list) method to list resources.
-
#initialize ⇒ Object
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.
-
#new ⇒ Object
The controller’s new method (e.g. used for new record in html format).
-
#render_error(e, handling_data) ⇒ Object
Renders error using handling data options (where options are probably hash from self.rescue_handlers that was matched).
- #render_or_respond(read_only_action, success_code = :ok) ⇒ Object
-
#show ⇒ Object
The controller’s show (get) method to return a resource.
- #single_value_response? ⇒ Boolean
-
#update ⇒ Object
The controller’s update (put) method to update a resource.
Instance Method Details
#allowed_params ⇒ Object
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..include?(action_sym) 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 |
#create ⇒ Object
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 |
#destroy ⇒ Object
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 |
#edit ⇒ Object
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_instance ⇒ Object
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`
237 238 239 |
# File 'lib/restful_json/controller.rb', line 237 def include_error_data? self.return_error_data end |
#index ⇒ Object
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| = 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 || [: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 |
#initialize ⇒ Object
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 |
#new ⇒ Object
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.) 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., 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 |
#show ⇒ Object
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
401 402 403 |
# File 'lib/restful_json/controller.rb', line 401 def single_value_response? SINGLE_VALUE_ACTIONS.include?(params[:action].to_sym) end |
#update ⇒ Object
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 |