├── spec ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── player.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── stylesheets │ │ │ │ ├── search.css │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ ├── search.js │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_controller.rb │ │ │ └── search_controller.rb │ │ ├── views │ │ │ ├── search │ │ │ │ ├── full_search.html.erb │ │ │ │ ├── raw_full_search.html.erb │ │ │ │ ├── full_raw_search_range.html.erb │ │ │ │ ├── full_search_range.html.erb │ │ │ │ ├── method_sanitized_simple_search.html.erb │ │ │ │ ├── controller_sanitized_simple_search.html.erb │ │ │ │ ├── raw_range_search.html.erb │ │ │ │ ├── simple_search.html.erb │ │ │ │ └── range_search.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ └── helpers │ │ │ ├── search_helper.rb │ │ │ └── application_helper.rb │ ├── lib │ │ ├── assets │ │ │ └── .keep │ │ └── my_engine │ │ │ └── engine.rb │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── db │ │ ├── test.sqlite3 │ │ ├── development.sqlite3 │ │ ├── migrate │ │ │ ├── 20150525033153_add_age_to_players.rb │ │ │ └── 20150401172324_create_players.rb │ │ ├── schema.rb │ │ └── seeds.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── config.ru │ ├── config │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── database.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── secrets.yml │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ ├── Rakefile │ └── test │ │ └── test_helper.rb ├── tests │ ├── performance_test.rb │ ├── sanitized_search_test.rb │ ├── search_test.rb │ ├── range_search_test.rb │ ├── full_search_test.rb │ └── parameters_priority_test.rb ├── features │ ├── full_search_form_test.rb │ ├── search_performance_test_in_app.rb │ ├── range_search_form_test.rb │ └── simple_search_form_test.rb ├── tasks │ └── rspec.rake ├── glasses_spec.rb ├── spec_helper.rb └── test_only_methods │ └── example_only_methods.rb ├── lib ├── glasses │ └── version.rb ├── tasks │ └── glasses_tasks.rake └── glasses.rb ├── .gitignore ├── .travis.yml ├── Gemfile ├── Rakefile ├── MIT-LICENSE ├── glasses.gemspec ├── Gemfile.lock └── README.md /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/tests/performance_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/features/full_search_form_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/full_search.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/features/search_performance_test_in_app.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/raw_full_search.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/full_raw_search_range.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/full_search_range.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/glasses/version.rb: -------------------------------------------------------------------------------- 1 | module Glasses 2 | VERSION = "1.7.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/search_helper.rb: -------------------------------------------------------------------------------- 1 | module SearchHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/player.rb: -------------------------------------------------------------------------------- 1 | class Player < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) -------------------------------------------------------------------------------- /spec/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octehren/Glasses/HEAD/spec/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octehren/Glasses/HEAD/spec/dummy/db/development.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /lib/tasks/glasses_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :glasses do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/db/*.sqlite3-journal 5 | spec/dummy/log/*.log 6 | spec/dummy/tmp/ 7 | spec/dummy/.sass-cache 8 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/search.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150525033153_add_age_to_players.rb: -------------------------------------------------------------------------------- 1 | class AddAgeToPlayers < ActiveRecord::Migration 2 | def change 3 | add_column :players, :age, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/search.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/lib/my_engine/engine.rb: -------------------------------------------------------------------------------- 1 | module MyEngine 2 | class Engine < ::Rails::Engine 3 | config.generators do |g| 4 | g.test_framework :rspec, :fixture => false 5 | g.assets false 6 | g.helper false 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | 3 | # Prevent CSRF attacks by raising an exception. 4 | # For APIs, you may want to use :null_session instead. 5 | protect_from_forgery with: :exception 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.1.5" 4 | - "2.1.0" 5 | - "2.0.0" 6 | env: 7 | - DB=mysql 8 | - DB=postgresql 9 | gemfile: 10 | - gemfiles/Gemfile.rails-3.2.x 11 | - gemfiles/Gemfile.rails-4.2.x 12 | - gemfiles/Gemfile.rails-edge 13 | script: 14 | - gem install bundler 15 | - bundle install 16 | - bundle exec rake spec -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'development' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | models :all 11 | end 12 | 13 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'simple_search' => 'search#simple_search' 3 | get 'controller_sanitized_simple_search' => 'search#controller_sanitized_simple_search' 4 | get 'method_sanitized_simple_search' => 'search#method_sanitized_simple_search' 5 | get 'raw_range_search' => 'search#raw_range_search' 6 | get 'range_search' => 'search#range_search' 7 | get 'full_search' => 'search#full_search' 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150401172324_create_players.rb: -------------------------------------------------------------------------------- 1 | class CreatePlayers < ActiveRecord::Migration 2 | def change 3 | create_table :players do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :email 7 | t.string :country 8 | t.string :ip_address 9 | t.datetime :first_win 10 | t.datetime :first_defeat 11 | t.boolean :is_virgin 12 | t.timestamps null: false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in glasses.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/method_sanitized_simple_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:search_params, url: method_sanitized_simple_search_path, method: "get", id: "simple_search_form") do |f| %> 2 | 3 | <%= f.label :first_name, "First Name:" %> 4 | <%= f.text_field :first_name %> 5 | 6 | <%= f.submit "Search" %> 7 | <% end %> 8 | 9 | <% if @players.empty? %> 10 |

WOPS, NOTHING TO SEARCH

11 |

0 entries

12 | <% else %> 13 | 14 | <% @players.each do |p| %> 15 | <% @num_results += 1 %> 16 |

<%= p.first_name %>, GR8 SUCCESS

17 | <% end %> 18 |

<%= "#{@num_results}"%>

19 | <% end %> -------------------------------------------------------------------------------- /spec/glasses_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | #tests in a raw environment with a hash serving as input (the default ruby web app frameworks "params" hash is the emulated input) 3 | require 'tests/search_test' 4 | require 'tests/sanitized_search_test' 5 | require 'tests/full_search_test' 6 | require 'tests/range_search_test' 7 | require 'tests/performance_test' 8 | require 'tests/parameters_priority_test' 9 | #tests in a simulated webpage environment using Capybara 10 | require 'features/simple_search_form_test' 11 | require 'features/full_search_form_test' 12 | require 'features/range_search_form_test' 13 | require 'features/search_performance_test_in_app' -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'glasses' 2 | require 'database_cleaner' 3 | 4 | ENV['RAILS_ENV'] ||= 'test' 5 | 6 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 7 | require 'rspec/rails' 8 | 9 | Rails.backtrace_cleaner.remove_silencers! 10 | 11 | # Load support files 12 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 13 | 14 | RSpec.configure do |config| 15 | config.mock_with :rspec 16 | config.expect_with(:rspec) {|r| r.syntax = :should } 17 | config.before(:suite) do 18 | DatabaseCleaner.strategy = :transaction 19 | DatabaseCleaner.clean_with(:truncation) 20 | Rails.application.load_seed 21 | end 22 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/search/controller_sanitized_simple_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:search_params, url: controller_sanitized_simple_search_path, method: "get", id: "simple_search_form") do |f| %> 2 | 3 | <%= f.label :first_name, "First Name:" %> 4 | <%= f.text_field :first_name %> 5 | 6 | <%= f.submit "Search" %> 7 | <% end %> 8 | 9 | <% if @players.empty? %> 10 |

WOPS, NOTHING TO SEARCH

11 |

0 entries

12 | <%= @test_params %> 13 | <% else %> 14 | <%= @test_params %> 15 | <% @players.each do |p| %> 16 | <% @num_results += 1 %> 17 |

<%= p.first_name %>, GR8 SUCCESS

18 | <% end %> 19 |

<%= "#{@num_results}"%>

20 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/raw_range_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:search_params, url: raw_range_search_path, method: "get", id: "simple_search_form") do |f| %> 2 | 3 | <%= f.label :first_name, "First Name:" %> 4 | <%= f.text_field :first_name %> 5 | 6 | <%= f.label :age_min, "Minimum Age:" %> 7 | <%= f.text_field :age_min %> 8 | 9 | <%= f.label :age_max, "Maximum Age:" %> 10 | <%= f.text_field :age_max %> 11 | 12 | <%= f.submit "Search" %> 13 | <% end %> 14 | 15 | <% if @players.empty? %> 16 |

WOPS, NOTHING TO SEARCH

17 |

0 entries

18 | <% else %> 19 | 20 | <% @players.each do |p| %> 21 | <% @num_results += 1 %> 22 |

<%= p.first_name %>, GR8 SUCCESS

23 | <% end %> 24 |

<%= "#{@num_results}"%>

25 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/views/search/simple_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:search_params, url: simple_search_path, method: "get", id: "simple_search_form") do |f| %> 2 | 3 | <%= f.label :first_name, "First Name:" %> 4 | <%= f.text_field :first_name %> 5 | 6 | 9 | <%= f.submit "Search" %> 10 | <% end %> 11 | 12 | <% if @players.empty? %> 13 |

WOPS, NOTHING TO SEARCH

14 |

0 entries

15 | <% else %> 16 | <% @players.each do |p| %> 17 | <% @num_results += 1 %> 18 |

<%= p.first_name %>,<%= p.is_virgin.to_s %>,<%= "LOOOSERR," if p.is_virgin %> GR8 SUCCESS

19 | <% end %> 20 |

<%= "#{@num_results}"%> entries

21 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | Dir.glob('tasks/**/*.rake').each(&method(:import)) 8 | 9 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 10 | load 'rails/tasks/engine.rake' 11 | 12 | Bundler::GemHelper.install_tasks 13 | 14 | Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f } 15 | 16 | require 'rspec/core' 17 | require 'rspec/core/rake_task' 18 | 19 | desc "Run all specs in spec directory (excluding plugin specs)" 20 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare') 21 | 22 | task :default => :spec 23 | 24 | 25 | require 'rdoc/task' 26 | 27 | RDoc::Task.new(:rdoc) do |rdoc| 28 | rdoc.rdoc_dir = 'rdoc' 29 | rdoc.title = 'Glasses' 30 | rdoc.options << '--line-numbers' 31 | rdoc.rdoc_files.include('README.rdoc') 32 | rdoc.rdoc_files.include('lib/**/*.rb') 33 | end 34 | 35 | Bundler::GemHelper.install_tasks -------------------------------------------------------------------------------- /spec/dummy/app/views/search/range_search.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:search_params, url: range_search_path, method: "get", id: "simple_search_form") do |f| %> 2 | 3 | <%= f.label :first_name, "First Name:" %> 4 | <%= f.text_field :first_name %> 5 | 6 | <%= f.label :age_min, "Minimum Age:" %> 7 | <%= f.text_field :age_min %> 8 | 9 | <%= f.label :age_max, "Maximum Age:" %> 10 | <%= f.text_field :age_max %> 11 | 12 | 16 | 17 | <%= f.submit "Search" %> 18 | <% end %> 19 | 20 | <% if @players.empty? %> 21 |

WOPS, NOTHING TO SEARCH

22 |

0 entries

23 | <% else %> 24 | 25 | <% @players.each do |p| %> 26 | <% @num_results += 1 %> 27 |

<%= p.first_name %>, GR8 SUCCESS

28 | <% end %> 29 |

<%= "#{@num_results}"%>

30 | <% end %> -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: f7f48394a0d417c29ce3b5ed5a634b811b7aa3c1cad703a15ebc30e1da720d8e79fd3e7339ce8ba98fb36cf5b376e41fe3f1a66f9b1d344580df450e4284585e 15 | 16 | test: 17 | secret_key_base: c3aecb562eef3dd2bfccde0413b7f9a4466ba7f35a7e44c6ebf5fae2d19bd8dd0a68d422374eab1bcefcd508f2eec838ebf795e2676e9782956146bf4fa9a7fa 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 otamm 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/tests/sanitized_search_test.rb: -------------------------------------------------------------------------------- 1 | describe '.sanitized_search()' do 2 | 3 | it 'is vulnerable to SQL injection when totally unprotected' do 4 | results = Glasses.raw_search(Player, {first_name: "' OR 1 = 1 ) --"}) 5 | (results.size).should == 10 6 | end 7 | 8 | it 'is not vulnerable to SQL injection when protected' do 9 | results = Glasses.search(Player, {first_name: "' OR 1 = 1 ) --"}) 10 | (results.size).should == 0 11 | end 12 | 13 | it 'correctly searches for a result in a sanitized_search' do 14 | results = Glasses.search(Player, {first_name: "Jane"}) 15 | (results.size).should == 1 16 | end 17 | 18 | #it 'ensures that search has best performance than sanitized_search' do 19 | # start1 = Time.now 20 | # Glasses.search(Player, {first_name: "Jane"}) 21 | # end1 = Time.now 22 | # start2 = Time.now 23 | # Glasses.sanitized_search(Player, {first_name: "Jane"}) 24 | # end2 = Time.now 25 | # puts (end1.to_f * 1000.0).to_i 26 | # puts ((start1.to_f) * 1000.0).to_i 27 | # puts (end2.to_f * 1000.0).to_i 28 | # puts ((start2.to_f) * 1000.0).to_i 29 | # puts ((end1 - start1) * 1000).to_i 30 | # puts ((end2.to_f - start2.to_f) * 1000.0).to_i 31 | # ((end1 - start1) < (end2 - start2)).should == true 32 | #end 33 | 34 | end -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150525033153) do 15 | 16 | create_table "players", force: :cascade do |t| 17 | t.string "first_name" 18 | t.string "last_name" 19 | t.string "email" 20 | t.string "country" 21 | t.string "ip_address" 22 | t.datetime "first_win" 23 | t.datetime "first_defeat" 24 | t.boolean "is_virgin" 25 | t.datetime "created_at", null: false 26 | t.datetime "updated_at", null: false 27 | t.integer "age" 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/tests/search_test.rb: -------------------------------------------------------------------------------- 1 | describe 'search' do 2 | 3 | it 'should search for an ActiveRecord model' do 4 | ((Glasses.search(Player,{first_name: "Jane"})).size).should == 1 5 | end 6 | 7 | it 'should search for an ActiveRecord model when input is only partial' do 8 | ((Glasses.search(Player,{first_name: "J"})).size).should == 2 # Jane & Jessica 9 | end 10 | 11 | it 'should return an empty array if no matches are found' do 12 | (Glasses.search(Player,{first_name: "HELLOOOOOOO IZE A PLAYAH"})).should == [] 13 | end 14 | 15 | it 'should return an empty array if no matches are found' do 16 | (Glasses.search(Player,{first_name: " "})).should == [] 17 | end 18 | 19 | context 'search with boolean' do 20 | it 'should search for an ActiveRecord model' do 21 | ((Glasses.search(Player,{first_name: "Jane", is_virgin_bool: '1'})).size).should == 1 22 | end 23 | 24 | it 'should search for an ActiveRecord model when input is only partial' do 25 | ((Glasses.search(Player,{first_name: "J", is_virgin_bool: '1'})).size).should == 2 # Jane & Jessica 26 | end 27 | 28 | it 'should return an empty array if no matches are found' do 29 | (Glasses.search(Player,{first_name: "Bobby", is_virgin_bool: '1'}).size).should == 0 30 | end 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "glasses" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | 28 | # Do not swallow errors in after_commit/after_rollback callbacks. 29 | config.active_record.raise_in_transactional_callbacks = true 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /glasses.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "glasses/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |spec| 8 | spec.name = "glasses" 9 | spec.version = Glasses::VERSION 10 | spec.authors = ["Otávio Monteagudo"] 11 | spec.email = ["oivatom@gmail.com"] 12 | spec.summary = %q{ Simplify searches on Ruby web frameworks which utilize ActiveRecord as a DB interface. } 13 | spec.description = %q{ Running through ActiveRecord's relations, this gem makes it very simple to quickly search through a given model's columns and return an array of objects that match the searched criteria. 14 | See the README available in the 'Documentation' repository found in the link below.} 15 | spec.homepage = "https://github.com/otamm/Glasses" 16 | spec.license = "MIT" 17 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | #spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.test_files = Dir["spec/**/*"] 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "bundler", "~> 1.7" 24 | spec.add_development_dependency "rspec" 25 | spec.add_development_dependency "sqlite3" 26 | spec.add_development_dependency "rails", "~> 4.2.1" 27 | spec.add_development_dependency "rspec-rails" 28 | spec.add_development_dependency "database_cleaner" 29 | spec.add_development_dependency "capybara" 30 | spec.add_dependency "rake", "~> 10.0" 31 | spec.add_dependency "activerecord", ">= 3.0.18" #must have .where() method 32 | end 33 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /spec/tests/range_search_test.rb: -------------------------------------------------------------------------------- 1 | describe 'search entries which have a value within a specific range' do 2 | 3 | context 'search with params sanitized by ActiveRecord' do 4 | 5 | it 'returns all the results with a field bigger than or equal to some arbitrary value' do 6 | (Glasses.search_range(Player, {age_min: '110'}).size).should == 3 7 | end 8 | 9 | it 'returns all the results with a field lesser than or equal to some arbitrary value' do 10 | (Glasses.search_range(Player, {age_max: '110'}).size).should == 8 11 | end 12 | 13 | it 'returns all the results with a field bigger than or equal to some arbitrary value and with another field lesser than or equal to some other arbitrary value' do 14 | (Glasses.search_range(Player, {age_min: '20', age_max: '110'}).size).should == 4 15 | end 16 | 17 | it 'returns all the results which satisfies both "field in the range" and "field with same prefix" conditions' do 18 | (Glasses.search_range(Player, {first_name: 'Jane', age_min: '110'}).size).should == 1 19 | end 20 | 21 | end 22 | 23 | context 'search without params sanitized by ActiveRecord' do 24 | 25 | it 'returns all the results with a field bigger than or equal to some arbitrary value' do 26 | (Glasses.raw_search_range(Player, {age_min: '110'}).size).should == 3 27 | end 28 | 29 | it 'returns all the results with a field lesser than or equal to some arbitrary value' do 30 | (Glasses.raw_search_range(Player, {age_max: '110'}).size).should == 8 31 | end 32 | 33 | it 'returns all the results with a field bigger than or equal to some arbitrary value and with another field lesser than or equal to some other arbitrary value' do 34 | (Glasses.raw_search_range(Player, {age_min: '20', age_max: '110'}).size).should == 4 35 | end 36 | 37 | it 'returns all the results which satisfies both "field in the range" and "field with same prefix" conditions' do 38 | (Glasses.raw_search_range(Player, {first_name: 'Jane', age_min: '110'}).size).should == 1 39 | end 40 | 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /spec/test_only_methods/example_only_methods.rb: -------------------------------------------------------------------------------- 1 | module BrokenGlasses 2 | def search_within_range(model_to_search,params_hash) 3 | params_array = params_hash.to_a 4 | query = "" 5 | consecutive_ints = 0 6 | params_array.each_with_index do |pair, index| 7 | if !pair[1].empty? 8 | if pair[0].to_s[key.size-3,key.size] == "_id" || pair[] ~= /\A[-+]?[0-9]+\z/ # checks if value is basically an integer within quotes 9 | 10 | possible_query = "#{key} = #{val} AND " 11 | consecutive_ints += 1 12 | if consecutive_ints % 2 == 0 #check if the case is of an integer field followed by another 13 | 14 | end 15 | query += possible_query 16 | else 17 | query += "#{key} LIKE '#{val}%' AND " # percent sign matches any string of 0 or more chars. 18 | end 19 | end 20 | end 21 | if !query.empty? 22 | query = query[0,query.size-5] 23 | model_to_search.where(query) 24 | else 25 | [] 26 | end 27 | end 28 | 29 | def self.full_search_range(model_to_search, params_hash) 30 | query = [] 31 | query_params = [] 32 | params_hash.each do |key,val| 33 | if !val.empty? 34 | key_suffix = key.to_s[key.size-3,key.size] 35 | if key_suffix == "min" 36 | query.push("#{key[0,key.size-4]} >= ?") 37 | query_params.push(val.to_s) 38 | elsif key_suffix == "max" 39 | query.push("#{key[0,key.size-4]} <= ?") 40 | query_params.push(val.to_s) 41 | elsif key_suffix == "_id" 42 | query.push("#{key} = ?") 43 | query_params.push(val.to_s) 44 | else 45 | query.push("#{key} LIKE ?") # percent sign matches any string of 0 or more chars. 46 | query_params.push("%" + val + "%") 47 | end 48 | end 49 | end 50 | if !query.empty? 51 | if query.size == 1 52 | return model_to_search.where(query.pop(), query_params.pop()) 53 | else 54 | results = model_to_search.where(query.pop(), query_params.pop()) 55 | (results.size).times do |search| 56 | results = results.where(query.pop(), query_params.pop()) # important to note that ".where()" is not hitting the database on this line. 57 | end 58 | return results 59 | end 60 | else 61 | [] 62 | end 63 | end 64 | 65 | end -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | players = [ 2 | {"id" => 1, "first_name"=>"Jane","last_name"=>"Sullivan","email"=>"jsullivan0@macromedia.com","country"=>"Colombia","ip_address"=>"65.3.175.51","first_win"=>"22/03/2008","first_defeat"=>"31/05/2010","is_virgin"=>true, "age" => 200}, 3 | {"id"=>2,"first_name"=>"Jessica","last_name"=>"Young","email"=>"jyoung1@rambler.ru","country"=>"Russia","ip_address"=>"207.235.206.31","first_defeat"=>"03/07/2009", "is_virgin" => true, "age" => 700}, 4 | {"id"=>3,"last_name"=>"Day","email"=>"sday2@unesco.org","country"=>"Tanzania","ip_address"=>"68.232.175.35","first_win"=>"23/03/2005","first_defeat"=>"26/03/2014","is_virgin"=>true, "age" => 7}, 5 | {"id"=>4,"first_name"=>"Anne","last_name"=>"Foster","email"=>"afoster3@virginia.edu","country"=>"China","ip_address"=>"2.52.95.238","first_win"=>"03/07/2006","is_virgin"=>true, "age" => 16}, 6 | {"id"=>5,"last_name"=>"Campbell","email"=>"ecampbell4@ebay.co.uk","country"=>"Switzerland","ip_address"=>"65.69.156.218","first_win"=>"25/01/2008","first_defeat"=>"21/10/2010","is_virgin"=>false, "age" => 3}, 7 | {"id"=>6,"first_name"=>"Bobby","last_name"=>"Bowman","email"=>"bbowman5@blog.com","country"=>"China","ip_address"=>"229.55.170.164","first_win"=>"19/11/2003","first_defeat"=>"22/09/2007","is_virgin"=>false, "age" => 1}, 8 | {"id"=>7,"first_name"=>"Benjamin","last_name"=>"Armstrong","email"=>"barmstrong6@sfgate.com","country"=>"China","first_win"=>"31/05/2002","first_defeat"=>"08/10/2006","is_virgin"=>true, "age" => 40}, 9 | {"id"=>8,"first_name"=>"Rose","last_name"=>"Ryan","email"=>"rryan7@ftc.gov","country"=>"Peru","ip_address"=>"86.13.46.13","first_win"=>"27/01/2014","first_defeat"=>"24/06/2005","is_virgin"=>true, "age" => 110}, 10 | {"id"=>9,"first_name"=>"Michelle","last_name"=>"Tucker","email"=>"mtucker8@google.cn","ip_address"=>"126.157.85.231","first_defeat"=>"20/11/2007","is_virgin"=>false, "age" => 50}, 11 | {"id"=>10,"first_name"=>"Rachel","last_name"=>"Lopez","email"=>"rlopez9@apple.com","country"=>"Pakistan","ip_address"=>"151.11.208.144","first_win"=>"23/05/2009","first_defeat"=>"29/01/2015","is_virgin"=>false, "age" => 20} 12 | ] 13 | 14 | for p in players 15 | player = Player.new(first_name: p["first_name"], last_name: p["last_name"], email: p["email"], country: p["country"], ip_address: p["ip_address"], first_win: p["first_win"], first_defeat: p["first_defeat"], is_virgin: p["is_virgin"], age: p["age"]) 16 | player.save! 17 | end -------------------------------------------------------------------------------- /spec/dummy/app/controllers/search_controller.rb: -------------------------------------------------------------------------------- 1 | class SearchController < ApplicationController 2 | 3 | #before_action(only: [:controller_sanitized_simple_search]) do |c| 4 | # c.search_params_sanitizer_defined_in_the_app(params[:action]) # parameters passed in the controller action 5 | #end 6 | #before_action(:search_params_sanitizer_defined_in_the_app, only: [:controller_sanitized_simple_search]) 7 | 8 | def simple_search 9 | if params[:search_params] 10 | @players = Glasses.search(Player, params[:search_params]) 11 | @num_results = 0 12 | else 13 | @players = [] 14 | end 15 | end 16 | 17 | def method_sanitized_simple_search 18 | if params[:search_params] 19 | @players = Glasses.raw_search(Player, params[:search_params]) 20 | @num_results = 0 21 | else 22 | @players = [] 23 | end 24 | end 25 | 26 | def controller_sanitized_simple_search 27 | if params[:search_params] 28 | #sanitized_params = search_params_sanitizer_defined_in_the_app(params[:search_params]) 29 | begin 30 | @players = caughts_injected_code_in_search(params[:search_params]) 31 | rescue 32 | @players = [] 33 | ensure 34 | @players 35 | end 36 | @test_params = params[:search_params] 37 | @num_results = 0 38 | else 39 | @players = [] 40 | end 41 | end 42 | 43 | def range_search 44 | if params[:search_params] 45 | @players = Glasses.search_range(Player, params[:search_params]) 46 | @num_results = 0 47 | else 48 | @players = [] 49 | end 50 | end 51 | 52 | def raw_range_search 53 | if params[:search_params] 54 | @players = Glasses.raw_search_range(Player, params[:search_params]) 55 | @num_results = 0 56 | else 57 | @players = [] 58 | end 59 | end 60 | 61 | def full_search_range 62 | 63 | end 64 | 65 | protected 66 | 67 | def search_params_sanitizer_defined_in_the_app(search_params, escape_char = "\\") 68 | sanitized_params = {} 69 | pattern = Regexp.union(escape_char, "%", "_", "=") 70 | search_params.each do |key, val| 71 | # sanitizing data directly is not that a good idea and should be done when no other solution is available 72 | sanitized_params[key] = val.gsub(/\\/, '\&\&').gsub(/'/, "''") 73 | end 74 | begin 75 | return sanitized_params 76 | rescue 77 | return [] 78 | end 79 | end 80 | 81 | def caughts_injected_code_in_search(search_params, escape_char = "\\") 82 | sanitized_params = search_params_sanitizer_defined_in_the_app(search_params, escape_char = "\\") 83 | begin 84 | results = Glasses.raw_search(Player, sanitized_params) 85 | rescue SQLException 86 | results = [] 87 | end 88 | 89 | return results 90 | 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /spec/tests/full_search_test.rb: -------------------------------------------------------------------------------- 1 | describe 'performs a full search on strings, looking for fragments of text instead of prefixes.' do 2 | 3 | context 'performs a full search that is sanitized by ActiveRecord' do 4 | 5 | it 'returns results which contain the fragment' do 6 | (Glasses.full_search(Player, {first_name: "bb"}).size).should == 1 7 | end 8 | 9 | it 'returns results which contain the fragment and other parameters' do 10 | (Glasses.full_search(Player, {first_name: "bb", email: ".com"}).size).should == 1 11 | end 12 | 13 | it 'does not return anything when no params match any text fragment for that column' do 14 | (Glasses.full_search(Player, {first_name: "zidbb"}).size).should == 0 15 | end 16 | 17 | it 'does not return anything when there is no record which matches all the params' do 18 | (Glasses.full_search(Player, {first_name: "zidbb", email: "@"}).size).should == 0 19 | end 20 | 21 | it 'returns all the results which satisfies both "field in the range" and "field with text fragment" conditions' do 22 | (Glasses.full_search_range(Player, {first_name: 'ane', age_min: '110'}).size).should == 1 23 | end 24 | 25 | context 'performs a full search which includes a range that is not sanitized by ActiveRecord' do 26 | 27 | it 'returns all the results which satisfies both "field in the range" and "field with text fragment" conditions' do 28 | (Glasses.full_search_range(Player, {first_name: 'ane', age_min: '110'}).size).should == 1 29 | end 30 | 31 | end 32 | 33 | end 34 | 35 | context 'performs a full search that is not sanitized by ActiveRecord' do 36 | 37 | it 'returns results which contain the fragment' do 38 | (Glasses.full_raw_search(Player, {first_name: "bb"}).size).should == 1 39 | end 40 | 41 | it 'returns results which contain the fragment and other parameters' do 42 | (Glasses.full_raw_search(Player, {first_name: "bb", email: ".com"}).size).should == 1 43 | end 44 | 45 | it 'does not return anything when no params match any text fragment for that column' do 46 | (Glasses.full_raw_search(Player, {first_name: "zidbb"}).size).should == 0 47 | end 48 | 49 | it 'does not return anything when there is no record which matches all the params' do 50 | (Glasses.full_raw_search(Player, {first_name: "zidbb", email: "@"}).size).should == 0 51 | end 52 | 53 | context 'performs a full search which includes a range that is not sanitized by ActiveRecord' do 54 | 55 | it 'returns all the results which satisfies both "field in the range" and "field with text fragment" conditions' do 56 | (Glasses.full_raw_search_range(Player, {first_name: 'ane', age_min: '110'}).size).should == 1 57 | end 58 | 59 | end 60 | 61 | end 62 | 63 | end -------------------------------------------------------------------------------- /spec/features/range_search_form_test.rb: -------------------------------------------------------------------------------- 1 | describe "search with a range constraint within web app", type: :feature do 2 | 3 | context 'search with params sanitized by ActiveRecord' do 4 | 5 | before :each do 6 | visit 'range_search' 7 | end 8 | 9 | it 'returns all the results with a field bigger than or equal to some arbitrary value' do 10 | fill_in 'Minimum Age:', with: '110' 11 | click_button "Search" 12 | page.should have_content 'GR8 SUCCESS' 13 | page.should have_content '3' 14 | end 15 | 16 | it 'returns all the results with a field lesser than or equal to some arbitrary value' do 17 | fill_in 'Maximum Age:', with: '110' 18 | click_button "Search" 19 | page.should have_content 'GR8 SUCCESS' 20 | page.should have_content '8' 21 | end 22 | 23 | it 'returns all the results with a field bigger than or equal to some arbitrary value and with another field lesser than or equal to some other arbitrary value' do 24 | fill_in 'Minimum Age:', with: '20' 25 | fill_in 'Maximum Age:', with: '110' 26 | click_button "Search" 27 | page.should have_content 'GR8 SUCCESS' 28 | page.should have_content '8' 29 | end 30 | 31 | it 'returns all the results which satisfies both "field in the range" and "field with same prefix" conditions' do 32 | fill_in 'First Name:', with: 'Jane' 33 | fill_in 'Minimum Age:', with: '110' 34 | click_button "Search" 35 | page.should have_content 'GR8 SUCCESS' 36 | page.should have_content '1' 37 | end 38 | 39 | end 40 | 41 | context 'search without params sanitized by ActiveRecord' do 42 | 43 | before :each do 44 | visit 'raw_range_search' 45 | end 46 | 47 | it 'returns all the results with a field bigger than or equal to some arbitrary value' do 48 | fill_in 'Minimum Age:', with: '110' 49 | click_button "Search" 50 | page.should have_content 'GR8 SUCCESS' 51 | page.should have_content '3' 52 | end 53 | 54 | it 'returns all the results with a field lesser than or equal to some arbitrary value' do 55 | fill_in 'Maximum Age:', with: '110' 56 | click_button "Search" 57 | page.should have_content 'GR8 SUCCESS' 58 | page.should have_content '8' 59 | end 60 | 61 | it 'returns all the results with a field bigger than or equal to some arbitrary value and with another field lesser than or equal to some other arbitrary value' do 62 | fill_in 'Minimum Age:', with: '20' 63 | fill_in 'Maximum Age:', with: '110' 64 | click_button "Search" 65 | page.should have_content 'GR8 SUCCESS' 66 | page.should have_content '8' 67 | end 68 | 69 | it 'returns all the results which satisfies both "field in the range" and "field with same prefix" conditions' do 70 | fill_in 'First Name:', with: 'Jane' 71 | fill_in 'Minimum Age:', with: '110' 72 | click_button "Search" 73 | page.should have_content 'GR8 SUCCESS' 74 | page.should have_content '1' 75 | end 76 | 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /spec/features/simple_search_form_test.rb: -------------------------------------------------------------------------------- 1 | describe "search within web app", type: :feature do 2 | 3 | context "search params sanitized in ActiveRecord" do 4 | 5 | before :each do 6 | visit 'simple_search' 7 | end 8 | 9 | it "correctly searches and displays matches" do 10 | fill_in 'First Name:', with: 'Jane' 11 | click_button "Search" 12 | page.should have_content 'GR8 SUCCESS' 13 | page.should have_content '1' 14 | end 15 | 16 | #it "is vulnerable to SQL injections when no defenses are present" do 17 | # fill_in "First Name:", with: "' OR 1 = 1 ) --" 18 | # click_button "Search" 19 | # page.should have_content "GR8 SUCCESS" 20 | # page.should have_content "10 entries" 21 | #end 22 | 23 | #context "search with a boolean value as one of the params" do 24 | 25 | it "correctly return values when a boolean value is passed to the form and there are matches" do 26 | fill_in 'First Name:', with: 'Jane' 27 | #all('input[type=checkbox]').each { |checkbox| check(checkbox) } 28 | # forms checkboxes are checked "true" by default. 29 | click_button 'Search' 30 | page.should have_content 'GR8 SUCCESS' 31 | page.should have_content '1' 32 | end 33 | 34 | it "correctly return values when a boolean value is passed to the form and there are no matches" do 35 | fill_in 'First Name:', with: 'Rachel' 36 | #all('input[type=checkbox]').each { |checkbox| check(checkbox) } 37 | # forms checkboxes are checked "true" by default. 38 | click_button 'Search' 39 | #puts page.body 40 | page.should have_content 'WOPS' 41 | page.should have_content '0' 42 | end 43 | 44 | #end 45 | 46 | end 47 | 48 | =begin 49 | commented out. This part has become redundant. 50 | context "search params sanitized in a search method of this gem with built-in defense" do 51 | 52 | before :each do 53 | visit 'method_sanitized_simple_search' 54 | end 55 | 56 | it "correctly searches and display matches" do 57 | fill_in 'First Name:', with: 'Jane' 58 | click_button 'Search' 59 | page.should have_content 'GR8 SUCCESS' 60 | page.should have_content '1' 61 | end 62 | 63 | it "is not vulnerable to SQL injections when defenses are present" do 64 | fill_in 'First Name:', with: "' OR 1 = 1 ) --" 65 | click_button "Search" 66 | page.should have_content 'WOPS' 67 | page.should have_content '0' 68 | end 69 | 70 | end 71 | =end 72 | 73 | context "search params sanitized in a controller method" do 74 | 75 | before :each do 76 | visit 'controller_sanitized_simple_search' 77 | end 78 | 79 | it "correctly searches and displays matches" do 80 | fill_in 'First Name:', with: 'Jan' 81 | click_button "Search" 82 | page.should have_content 'GR8 SUCCESS' 83 | page.should have_content '1' 84 | end 85 | 86 | it "correctly prevents a SQL injection" do 87 | fill_in "First Name:", with: "' OR 1 = 1 ) --" 88 | click_button "Search" 89 | page.should have_content "WOPS" 90 | page.should have_content "0 entries" 91 | end 92 | 93 | end 94 | 95 | end -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /spec/tests/parameters_priority_test.rb: -------------------------------------------------------------------------------- 1 | describe "search params are reordered in a way to give first entrance to quicker queries; the arrays will be .pop()ed, so 'prioritized' == 'last_element' here." do 2 | 3 | context "params include strings and ints/bools" do 4 | 5 | it "puts integers before strings" do 6 | Glasses.prioritize_ints_over_strings(["user_id = ?","first_name LIKE ?", "last_name LIKE ?"], ["666", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "user_id = ?"],["Rotten", "Johnny", "666"]] 7 | end 8 | 9 | it "puts booleans before strings" do 10 | Glasses.prioritize_ints_over_strings(["is_cokehead_bool = ?", "first_name LIKE ?", "last_name LIKE ?"], ["1", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "is_cokehead_bool = ?"],["Rotten", "Johnny", "1"]] 11 | end 12 | 13 | it "puts both integers and booleans before strings" do 14 | Glasses.prioritize_ints_over_strings(["user_id = ?", "is_cokehead_bool = ?", "first_name LIKE ?", "last_name LIKE ?"], ["666", "1", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "user_id = ?", "is_cokehead_bool = ?"],["Rotten", "Johnny", "666", "1"]] 15 | end 16 | 17 | it "returns the same query with inverted params if it does not contain more than one kind of input (strings)" do 18 | Glasses.prioritize_ints_over_strings(["first_name LIKE ?", "last_name LIKE ?"], ["Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?"], ["Rotten", "Johnny"]] 19 | end 20 | 21 | end 22 | 23 | context "params include strings, ints/bools and ranges" do 24 | 25 | it "puts integers before strings" do 26 | Glasses.prioritize_ints_over_strings_and_ranges(["user_id = ?","first_name LIKE ?", "last_name LIKE ?"], ["666", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "user_id = ?"],["Rotten", "Johnny", "666"]] 27 | end 28 | 29 | it "puts booleans before strings" do 30 | Glasses.prioritize_ints_over_strings_and_ranges(["is_cokehead_bool = ?", "first_name LIKE ?", "last_name LIKE ?"], ["1", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "is_cokehead_bool = ?"],["Rotten", "Johnny", "1"]] 31 | end 32 | 33 | it "puts both integers and booleans before strings" do 34 | Glasses.prioritize_ints_over_strings_and_ranges(["user_id = ?", "is_cokehead_bool = ?", "first_name LIKE ?", "last_name LIKE ?"], ["666", "1", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "user_id = ?", "is_cokehead_bool = ?"],["Rotten", "Johnny", "666", "1"]] 35 | end 36 | 37 | it "puts integers, booleans and ranges before strings" do 38 | Glasses.prioritize_ints_over_strings_and_ranges(["user_id = ?", "is_cokehead_bool = ?", "age_min >= ?", "age_max <= ?", "first_name LIKE ?", "last_name LIKE ?"], ["666", "1", "2", "456", "Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?", "age_max <= ?", "age_min >= ?", "user_id = ?", "is_cokehead_bool = ?"],["Rotten", "Johnny", "456", "2", "666", "1"]] 39 | end 40 | 41 | it "puts ints/booleans before ranges" do 42 | Glasses.prioritize_ints_over_strings_and_ranges(["user_id = ?", "is_cokehead_bool = ?", "age_min >= ?", "age_max <= ?"], ["666", "1", "2", "456"]).should == [["age_max <= ?", "age_min >= ?", "user_id = ?", "is_cokehead_bool = ?"],["456", "2", "666", "1"]] 43 | end 44 | 45 | it "returns the same query with inverted params if it does not contain more than one kind of input (strings)" do 46 | Glasses.prioritize_ints_over_strings_and_ranges(["first_name LIKE ?", "last_name LIKE ?"], ["Johnny", "Rotten"]).should == [["last_name LIKE ?", "first_name LIKE ?"], ["Rotten", "Johnny"]] 47 | end 48 | 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | glasses (1.7.0) 5 | activerecord (>= 3.0.18) 6 | rake (~> 10.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (4.2.1) 12 | actionpack (= 4.2.1) 13 | actionview (= 4.2.1) 14 | activejob (= 4.2.1) 15 | mail (~> 2.5, >= 2.5.4) 16 | rails-dom-testing (~> 1.0, >= 1.0.5) 17 | actionpack (4.2.1) 18 | actionview (= 4.2.1) 19 | activesupport (= 4.2.1) 20 | rack (~> 1.6) 21 | rack-test (~> 0.6.2) 22 | rails-dom-testing (~> 1.0, >= 1.0.5) 23 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 24 | actionview (4.2.1) 25 | activesupport (= 4.2.1) 26 | builder (~> 3.1) 27 | erubis (~> 2.7.0) 28 | rails-dom-testing (~> 1.0, >= 1.0.5) 29 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 30 | activejob (4.2.1) 31 | activesupport (= 4.2.1) 32 | globalid (>= 0.3.0) 33 | activemodel (4.2.1) 34 | activesupport (= 4.2.1) 35 | builder (~> 3.1) 36 | activerecord (4.2.1) 37 | activemodel (= 4.2.1) 38 | activesupport (= 4.2.1) 39 | arel (~> 6.0) 40 | activesupport (4.2.1) 41 | i18n (~> 0.7) 42 | json (~> 1.7, >= 1.7.7) 43 | minitest (~> 5.1) 44 | thread_safe (~> 0.3, >= 0.3.4) 45 | tzinfo (~> 1.1) 46 | arel (6.0.0) 47 | builder (3.2.2) 48 | capybara (2.4.4) 49 | mime-types (>= 1.16) 50 | nokogiri (>= 1.3.3) 51 | rack (>= 1.0.0) 52 | rack-test (>= 0.5.4) 53 | xpath (~> 2.0) 54 | database_cleaner (1.4.1) 55 | diff-lcs (1.2.5) 56 | erubis (2.7.0) 57 | globalid (0.3.5) 58 | activesupport (>= 4.1.0) 59 | i18n (0.7.0) 60 | json (1.8.2) 61 | loofah (2.0.2) 62 | nokogiri (>= 1.5.9) 63 | mail (2.6.3) 64 | mime-types (>= 1.16, < 3) 65 | mime-types (2.5) 66 | mini_portile (0.6.2) 67 | minitest (5.6.1) 68 | nokogiri (1.6.6.2) 69 | mini_portile (~> 0.6.0) 70 | rack (1.6.1) 71 | rack-test (0.6.3) 72 | rack (>= 1.0) 73 | rails (4.2.1) 74 | actionmailer (= 4.2.1) 75 | actionpack (= 4.2.1) 76 | actionview (= 4.2.1) 77 | activejob (= 4.2.1) 78 | activemodel (= 4.2.1) 79 | activerecord (= 4.2.1) 80 | activesupport (= 4.2.1) 81 | bundler (>= 1.3.0, < 2.0) 82 | railties (= 4.2.1) 83 | sprockets-rails 84 | rails-deprecated_sanitizer (1.0.3) 85 | activesupport (>= 4.2.0.alpha) 86 | rails-dom-testing (1.0.6) 87 | activesupport (>= 4.2.0.beta, < 5.0) 88 | nokogiri (~> 1.6.0) 89 | rails-deprecated_sanitizer (>= 1.0.1) 90 | rails-html-sanitizer (1.0.2) 91 | loofah (~> 2.0) 92 | railties (4.2.1) 93 | actionpack (= 4.2.1) 94 | activesupport (= 4.2.1) 95 | rake (>= 0.8.7) 96 | thor (>= 0.18.1, < 2.0) 97 | rake (10.4.2) 98 | rspec (3.2.0) 99 | rspec-core (~> 3.2.0) 100 | rspec-expectations (~> 3.2.0) 101 | rspec-mocks (~> 3.2.0) 102 | rspec-core (3.2.3) 103 | rspec-support (~> 3.2.0) 104 | rspec-expectations (3.2.1) 105 | diff-lcs (>= 1.2.0, < 2.0) 106 | rspec-support (~> 3.2.0) 107 | rspec-mocks (3.2.1) 108 | diff-lcs (>= 1.2.0, < 2.0) 109 | rspec-support (~> 3.2.0) 110 | rspec-rails (3.2.1) 111 | actionpack (>= 3.0, < 4.3) 112 | activesupport (>= 3.0, < 4.3) 113 | railties (>= 3.0, < 4.3) 114 | rspec-core (~> 3.2.0) 115 | rspec-expectations (~> 3.2.0) 116 | rspec-mocks (~> 3.2.0) 117 | rspec-support (~> 3.2.0) 118 | rspec-support (3.2.2) 119 | sprockets (3.1.0) 120 | rack (~> 1.0) 121 | sprockets-rails (2.3.1) 122 | actionpack (>= 3.0) 123 | activesupport (>= 3.0) 124 | sprockets (>= 2.8, < 4.0) 125 | sqlite3 (1.3.10) 126 | thor (0.19.1) 127 | thread_safe (0.3.5) 128 | tzinfo (1.2.2) 129 | thread_safe (~> 0.1) 130 | xpath (2.0.0) 131 | nokogiri (~> 1.3) 132 | 133 | PLATFORMS 134 | ruby 135 | 136 | DEPENDENCIES 137 | bundler (~> 1.7) 138 | capybara 139 | database_cleaner 140 | glasses! 141 | rails (~> 4.2.1) 142 | rspec 143 | rspec-rails 144 | sqlite3 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glasses 2 | [![Gem Version](https://badge.fury.io/rb/glasses.svg)](http://badge.fury.io/rb/glasses) [![Build Status](https://travis-ci.org/otamm/Glasses.svg?branch=master)](https://travis-ci.org/otamm/Glasses) 3 | 4 | Glasses is a micro search framework to be used within Ruby web applications which utilize Active Record as a part of the middleware between the app's logic and database. 5 | The gem's methods are based upon ActiveRecord's querying methods, so you can consider them to be Database agnostic, at least in an environment using a relational system such as SQLite, PostgreSQL or MySQL; the gem have not been tested in a NoSQL system such as MongoDB and its functioning cannot be guaranteed in this kind of environment. 6 | Also, the parameters are sanitized by ActiveRecord itself, so the search will be SQL-Injection-protected by default. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'glasses' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install glasses 23 | 24 | ## Set Up 25 | This gem is intended to provide a quick way to enable searching in an app. 26 | However, a minimal set up is required. Also, please note that Glasses inserts raw SQL strings directly into ActiveRecord's ```ModelName.where()``` method of the Base class, so make sure you are protected against SQL injection attacks before launching it on a production environment; a sanitizing 'before_filter' for the controller method which will be using Glasses should suffice. 27 | 28 | If you are a beginner web dev, take a look [here](http://guides.rubyonrails.org/security.html#sql-injection) for some basic security understanding. If you are still insecure, keep reading as Glasses has a method with pre-built parameters sanitizing in case you are looking for a quick fix. 29 | 30 | The examples below use Rails as the example environment. 31 | 32 | First, let's set up a controller method: 33 | 34 | ```ruby 35 | def search_message 36 | if params[:search] 37 | @messages = Glasses.search(Message,params[:search]) 38 | else 39 | @messages = [] 40 | end 41 | end 42 | ``` 43 | 44 | And also add a route which uses the HTTP method GET: 45 | 46 | ```ruby 47 | # located in your_rails_app/config/routes.rb 48 | get 'search_for_a_message' => 'messages#search_message' 49 | ``` 50 | 51 | ## Usage 52 | 53 | This method should of course be serving as an interface between 54 | the application's DB and user's input through a search form 55 | (both forms are given here because I've had problems building 56 | my first one, consider it as a gift): 57 | 58 | ```html 59 | <%= form_for(:search, url: search_message_path, method: "get") do |f| %> 60 |

Search Messages


61 | 62 | <%= f.label :name, "From: " %>
63 | <%= f.text_field :name %>
64 | 65 | <%= f.label :email, "Sender's Email: " %>
66 | <%= f.email_field :email %>
67 | 68 | <%= f.label :subject, "Subject: " %>
69 | <%= f.text_field :subject %>
70 | 71 | <%= f.label :body, "Text body: " %>
72 | <%= f.text_area :body %>
73 | 74 | <%= f.submit "Search Messages" %> 75 | <% end %> 76 | ``` 77 | 78 | The method's output will be an array of instances of the class 79 | passed in the first parameter. The second parameter should be a 80 | hash with the fields and values to be searched inside that specific 81 | relation represented by the class that goes in the first parameter 82 | (probably the parameters hash, but could be any). 83 | 84 | Is the form above too trivial? The same method can also be used with an advanced search form 85 | like the one below: 86 | 87 | ```html 88 | <%= form_for(:search, url: search_portfolio_path, method: "get") do |f| %> 89 | Select by: 90 | 91 | <%= f.label :job_name, "Job: " %> 92 | <%= f.text_field :job_name %>
93 | 94 | <%= f.label :category_id, "Category: " %> 95 | <%= f.collection_select :category_id, @categories, :id, :name, include_blank: "All" %>
96 | 97 | <%= f.label "Look for award-winning jobs only" %> 98 | <%= f.check_box :is_award_winning_bool %> 99 | 100 | <%= f.submit "Search with criteria" %> 101 | <% end %> 102 | ``` 103 | 104 | The algorithm differentiates between ids, booleans and raw text search input types, 105 | so no trouble at all. However, this differentiation needs some really basic specifications. 106 | 107 | ## Paremeter Constraints 108 | Did you notice that the parameter being passed with the checkbox has its symbol ending with ```"_bool" ```? 109 | Well, actually the column being searched on is named ```"is_award_winning" ```, not ```"is_award_winning_bool" ```. 110 | The suffix ```"_bool" ``` is added only in the form so Glasses can detect that it should be searching for a boolean. Also, pass the value ```"1" ``` for a checked box to represent the value ```true ``` . Any number can be used when passing an id. 111 | 112 | Glasses realizes what is the specific data type it should be looking for according to the suffix of the field being passed as one of the keys in the 'params' hash. 113 | 114 | The only two other constraints are ```"_min" ``` and ```"_max" ``` as suffixes in range searches. However, to make a range search, the correct method to be utilized is ```Glasses.search_range() ``` , not ```Glasses.search() ```. 115 | 116 | ####Example: 117 | 118 | ```ruby 119 | def search_user 120 | if params[:search] 121 | @users = Glasses.search_range(Message,params[:search]) 122 | else 123 | @users = [] 124 | end 125 | end 126 | ``` 127 | 128 | ```html 129 | <%= form_for(:search, url: search_user_path, method: "get") do |f| %> 130 | 131 | Select by:
132 | 133 | <%= f.label :first_name, "First Name: " %> 134 | <%= f.text_field :first_name %>
135 | 136 | <%= f.label :last_name, "Last Name: " %> 137 | <%= f.text_field :last_name %>
138 | 139 | <%= f.label :age_min, "Minimum Age: " %> 140 | <%= f.text_field :age_min %>
141 | 142 | <%= f.label :age_max, "Maximum Age: " %> 143 | <%= f.text_field :age_max %>
144 | 145 | 148 | 149 | <%= f.submit "Search with criteria" %> 150 | 151 | <% end %> 152 | ``` 153 | 154 | Done! Now that's a form which will return all the users who fit in the specified criteria which includes a string, a boolean and a range parameter. 155 | 156 | ## Contributing 157 | 158 | That's about it. For further info on each gem method best fit for each specific scenarion, check the (soon to debut) gem's wiki. 159 | Source code is located in lib/glasses.rb; if you want to run tests locally, clone this repository and run ``` $bundle exec rake spec``` in the root project's directory in your terminal. 160 | 161 | 1. Fork it ( https://github.com/otamm/glasses/fork ) 162 | 2. Create your feature branch (`git checkout -b my-new-feature`) 163 | 3. Commit your changes (`git commit -am 'Add some feature'`) 164 | 4. Push to the branch (`git push origin my-new-feature`) 165 | 5. Create a new Pull Request 166 | -------------------------------------------------------------------------------- /lib/glasses.rb: -------------------------------------------------------------------------------- 1 | require "glasses/version" 2 | 3 | module Glasses 4 | 5 | def self.prioritize_ints_over_strings(columns_to_search, values_to_search) 6 | # Order params, prioritize ids and bools over over strings to boost performance at database hits. 7 | for i in (0..columns_to_search.size-1) do 8 | if columns_to_search[i][columns_to_search[i].size-3] == "E" #if it's a "like" statement 9 | columns_to_search.insert(0, columns_to_search.delete_at(i)) 10 | values_to_search.insert(0, values_to_search.delete_at(i)) 11 | end 12 | end 13 | return [columns_to_search, values_to_search] 14 | end 15 | 16 | def self.prioritize_ints_over_strings_and_ranges(columns_to_search, values_to_search) 17 | # Order params, prioritize ids and bools over ranges and both of these over strings to boost performance at database hits. 18 | string_shifts = 0 19 | total_columns = columns_to_search.size - 1 20 | for i in (0..total_columns) do 21 | if columns_to_search[i][columns_to_search[i].size-3] == "E" #if it's a "like" statement 22 | columns_to_search.insert(0, columns_to_search.delete_at(i)) 23 | values_to_search.insert(0, values_to_search.delete_at(i)) 24 | string_shifts += 1 25 | end 26 | end 27 | if string_shifts < total_columns # string_shifts will begin at the 'last index which contains a string' + 1 28 | for i in (string_shifts..columns_to_search.size-1) do 29 | if columns_to_search[i][columns_to_search[i].size-4] != " " #if it's empty at this index, value is a range; 30 | columns_to_search.insert(string_shifts, columns_to_search.delete_at(i)) 31 | values_to_search.insert(string_shifts, values_to_search.delete_at(i)) 32 | end 33 | end 34 | end 35 | return [columns_to_search, values_to_search] 36 | end 37 | 38 | def self.search(model_to_search,params_hash) 39 | query = [] 40 | query_params = [] 41 | params_hash.each do |key,val| 42 | if !val.empty? 43 | #key = key.to_s 44 | key_suffix = key[key.size-3,key.size] 45 | if key_suffix == "ool" 46 | if val == "1" 47 | query.push("#{key[0,key.size-5]} = ?") # excludes 'bool' 48 | query_params.push(true) 49 | #else 50 | # query.push("#{key[0,key.size-5]} = ?") # excludes 'bool' 51 | # query_params.push(false) 52 | end 53 | elsif key_suffix == "min" 54 | query.push("#{key[0,key.size-4]} >= ?") # excludes 'min' 55 | query_params.push(val.to_s) 56 | elsif key_suffix == "max" 57 | query.push("#{key[0,key.size-4]} <= ?") # excludes 'max' 58 | query_params.push(val.to_s) 59 | elsif key_suffix == "_id" 60 | query.push("#{key} = ?") 61 | query_params.push(val.to_s) 62 | else 63 | query.push("#{key} LIKE ?") # percent sign matches any string of 0 or more chars. 64 | query_params.push(val + "%") 65 | end 66 | end 67 | end 68 | if !query.empty? 69 | if query.size == 1 70 | return model_to_search.where(query.pop(), query_params.pop()) 71 | else 72 | prioritized = Glasses.prioritize_ints_over_strings(query, query_params) 73 | query = prioritized[0] 74 | query_params = prioritized[1] 75 | results = model_to_search.where(query.pop(), query_params.pop()) 76 | (query.size).times do |search| 77 | results = results.where(query.pop(), query_params.pop()) # important to note that ".where()" is not hitting the database on this line. 78 | end 79 | return results 80 | end 81 | else 82 | [] 83 | end 84 | end 85 | 86 | def self.full_search(model_to_search,params_hash) 87 | query = [] 88 | query_params = [] 89 | params_hash.each do |key,val| 90 | if !val.empty? 91 | if key.to_s[key.size-3,key.size] == "_id" 92 | query.push("#{key} = ?") 93 | query_params.push(val.to_s) 94 | else 95 | query.push("#{key} LIKE ?") # percent sign matches any string of 0 or more chars. 96 | query_params.push("%" + val + "%") 97 | end 98 | end 99 | end 100 | if !query.empty? 101 | if query.size == 1 102 | return model_to_search.where(query.pop(), query_params.pop()) 103 | else 104 | results = model_to_search.where(query.pop(), query_params.pop()) 105 | (query.size).times do |search| 106 | results = results.where(query.pop(), query_params.pop()) # important to note that ".where()" is not hitting the database on this line. 107 | end 108 | return results 109 | end 110 | else 111 | [] 112 | end 113 | end 114 | 115 | def self.search_range(model_to_search,params_hash) 116 | query = [] 117 | query_params = [] 118 | params_hash.each do |key,val| 119 | if !val.empty? 120 | #key = key.to_s 121 | key_suffix = key[key.size-3,key.size] 122 | if key_suffix == "ool" 123 | if val == "1" 124 | query.push("#{key[0,key.size-5]} = ?") # excludes 'bool' 125 | query_params.push(true) 126 | #else 127 | # query.push("#{key[0,key.size-5]} = ?") # excludes 'bool' 128 | # query_params.push(false) 129 | end 130 | elsif key_suffix == "min" 131 | query.push("#{key[0,key.size-4]} >= ?") 132 | query_params.push(val.to_s) 133 | elsif key_suffix == "max" 134 | query.push("#{key[0,key.size-4]} <= ?") 135 | query_params.push(val.to_s) 136 | elsif key_suffix == "_id" 137 | query.push("#{key} = ?") 138 | query_params.push(val.to_s) 139 | else 140 | query.push("#{key} LIKE ?") # percent sign matches any string of 0 or more chars. 141 | query_params.push(val + "%") 142 | end 143 | end 144 | end 145 | if !query.empty? 146 | if query.size == 1 147 | return model_to_search.where(query.pop(), query_params.pop()) 148 | else 149 | prioritized = Glasses.prioritize_ints_over_strings_and_ranges(query, query_params) 150 | query = prioritized[0] 151 | query_params = prioritized[1] 152 | results = model_to_search.where(query.pop(), query_params.pop()) 153 | (query.size).times do |search| 154 | results = results.where(query.pop(), query_params.pop()) # important to note that ".where()" is not hitting the database on this line. 155 | end 156 | return results 157 | end 158 | else 159 | [] 160 | end 161 | end 162 | 163 | def self.full_search_range(model_to_search, params_hash) 164 | query = [] 165 | query_params = [] 166 | params_hash.each do |key,val| 167 | if !val.empty? 168 | key_suffix = key.to_s[key.size-3,key.size] 169 | if key_suffix == "min" 170 | query.push("#{key[0,key.size-4]} >= ?") 171 | query_params.push(val.to_s) 172 | elsif key_suffix == "max" 173 | query.push("#{key[0,key.size-4]} <= ?") 174 | query_params.push(val.to_s) 175 | elsif key_suffix == "_id" 176 | query.push("#{key} = ?") 177 | query_params.push(val.to_s) 178 | else 179 | query.push("#{key} LIKE ?") # percent sign matches any string of 0 or more chars. 180 | query_params.push("%" + val + "%") 181 | end 182 | end 183 | end 184 | if !query.empty? 185 | if query.size == 1 186 | return model_to_search.where(query.pop(), query_params.pop()) 187 | else 188 | prioritized = Glasses.prioritize_ints_over_strings_and_ranges(query, query_params) 189 | query = prioritized[0] 190 | query_params = prioritized[1] 191 | results = model_to_search.where(query.pop(), query_params.pop()) 192 | (query.size).times do |search| 193 | results = results.where(query.pop(), query_params.pop()) # important to note that ".where()" is not hitting the database on this line. 194 | end 195 | return results 196 | end 197 | else 198 | [] 199 | end 200 | end 201 | 202 | def self.raw_search(model_to_search,params_hash) 203 | query = "" 204 | params_hash.each do |key,val| 205 | if val 206 | key = key.to_s 207 | key_suffix = key.to_s[key.size-3,key.size] 208 | if key_suffix == "ool" 209 | query += "#{key[0,key.size-5]} = '1' AND " 210 | elsif key_suffix == "_id" 211 | query += "#{key} = #{val} AND " 212 | else 213 | query += "#{key} LIKE '#{val}%' AND " # percent sign matches any string of 0 or more chars. 214 | end 215 | end 216 | end 217 | if !query.empty? 218 | query = query[0,query.size-5] 219 | model_to_search.where(query) 220 | else 221 | [] 222 | end 223 | end 224 | 225 | def self.full_raw_search(model_to_search,params_hash) 226 | query = "" 227 | params_hash.each do |key,val| 228 | if !val.empty? 229 | if key.to_s[key.size-3,key.size] == "_id" 230 | query += "#{key} = #{val} AND " 231 | else 232 | query += "#{key} LIKE '%#{val}%' AND " # percent sign matches any string of 0 or more chars. 233 | end 234 | end 235 | end 236 | if !query.empty? 237 | query = query[0,query.size-5] 238 | model_to_search.where(query) 239 | else 240 | [] 241 | end 242 | end 243 | 244 | def self.raw_search_range(model_to_search,params_hash) 245 | query = "" 246 | params_hash.each do |key,val| 247 | if !val.empty? 248 | key_suffix = key.to_s[key.size-3,key.size] 249 | if key_suffix == "min" 250 | query += "#{key[0,key.size-4]} >= #{val} AND " 251 | elsif key_suffix == "max" 252 | query += "#{key[0,key.size-4]} <= #{val} AND " 253 | elsif key_suffix == "_id" 254 | query += "#{key} = #{val} AND " 255 | else 256 | query += "#{key} LIKE '#{val}%' AND " # percent sign matches any string of 0 or more chars. 257 | end 258 | end 259 | end 260 | if !query.empty? 261 | query = query[0,query.size-5] 262 | model_to_search.where(query) 263 | else 264 | [] 265 | end 266 | end 267 | 268 | def self.full_raw_search_range(model_to_search, params_hash) 269 | query = "" 270 | params_hash.each do |key,val| 271 | if !val.empty? 272 | key_suffix = key.to_s[key.size-3,key.size] 273 | if key_suffix == "min" 274 | query += "#{key[0,key.size-4]} >= #{val} AND " 275 | elsif key_suffix == "max" 276 | query += "#{key[0,key.size-4]} <= #{val} AND " 277 | elsif key_suffix == "_id" 278 | query += "#{key} = #{val} AND " 279 | else 280 | query += "#{key} LIKE '%#{val}%' AND " # percent sign matches any string of 0 or more chars. 281 | end 282 | end 283 | end 284 | if !query.empty? 285 | query = query[0,query.size-5] 286 | model_to_search.where(query) 287 | else 288 | [] 289 | end 290 | end 291 | 292 | end 293 | --------------------------------------------------------------------------------