Repository: presidentbeef/brakeman Branch: main Commit: 2e55d45a9770 Files: 1364 Total size: 2.5 MB Directory structure: gitextract_9gn_whxm/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── feature-request.md │ │ ├── hanging-or-slow-scans.md │ │ ├── parsing-error.md │ │ ├── report-a-false-positive.md │ │ └── something-else.md │ └── workflows/ │ └── docker-hub-push.yml ├── .gitignore ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING.md ├── Dockerfile ├── Dockerfile.codeclimate ├── FEATURES ├── Gemfile ├── LICENSE.md ├── MIT-LICENSE ├── OPTIONS.md ├── README.md ├── Rakefile ├── SECURITY.md ├── bin/ │ ├── brakeman │ └── codeclimate-brakeman ├── brakeman-lib.gemspec ├── brakeman-min.gemspec ├── brakeman-public_cert.pem ├── brakeman.gemspec ├── build.rb ├── docs/ │ └── warning_types/ │ ├── CVE-2010-3933/ │ │ └── index.markdown │ ├── CVE-2011-0446/ │ │ └── index.markdown │ ├── CVE-2011-3186/ │ │ └── index.markdown │ ├── attribute_restriction/ │ │ └── index.markdown │ ├── authentication/ │ │ └── index.markdown │ ├── authentication_whitelist/ │ │ └── index.markdown │ ├── basic_auth/ │ │ └── index.markdown │ ├── command_injection/ │ │ └── index.markdown │ ├── content_tag/ │ │ └── index.markdown │ ├── cross-site_request_forgery/ │ │ └── index.markdown │ ├── cross_site_scripting/ │ │ └── index.markdown │ ├── cross_site_scripting_to_json/ │ │ └── index.markdown │ ├── dangerous_eval/ │ │ └── index.markdown │ ├── dangerous_send/ │ │ └── index.markdown │ ├── default_routes/ │ │ └── index.markdown │ ├── denial_of_service/ │ │ └── index.markdown │ ├── dynamic_render_path/ │ │ └── index.markdown │ ├── file_access/ │ │ └── index.markdown │ ├── format_validation/ │ │ └── index.markdown │ ├── information_disclosure/ │ │ └── index.markdown │ ├── link_to/ │ │ └── index.markdown │ ├── link_to_href/ │ │ └── index.markdown │ ├── mass_assignment/ │ │ └── index.markdown │ ├── redirect/ │ │ └── index.markdown │ ├── remote_code_execution/ │ │ └── index.markdown │ ├── remote_code_execution_yaml_load/ │ │ └── index.markdown │ ├── session_manipulation/ │ │ └── index.markdown │ ├── session_setting/ │ │ └── index.markdown │ ├── sql_injection/ │ │ └── index.markdown │ ├── ssl_verification_bypass/ │ │ └── index.markdown │ ├── template_injection/ │ │ └── index.markdown │ ├── unsafe_deserialization/ │ │ └── index.markdown │ └── unscoped_find/ │ └── index.markdown ├── gem_common.rb ├── lib/ │ ├── brakeman/ │ │ ├── app_tree.rb │ │ ├── call_index.rb │ │ ├── checks/ │ │ │ ├── base_check.rb │ │ │ ├── check_basic_auth.rb │ │ │ ├── check_basic_auth_timing_attack.rb │ │ │ ├── check_content_tag.rb │ │ │ ├── check_cookie_serialization.rb │ │ │ ├── check_create_with.rb │ │ │ ├── check_cross_site_scripting.rb │ │ │ ├── check_csrf_token_forgery_cve.rb │ │ │ ├── check_default_routes.rb │ │ │ ├── check_deserialize.rb │ │ │ ├── check_detailed_exceptions.rb │ │ │ ├── check_digest_dos.rb │ │ │ ├── check_divide_by_zero.rb │ │ │ ├── check_dynamic_finders.rb │ │ │ ├── check_eol_rails.rb │ │ │ ├── check_eol_ruby.rb │ │ │ ├── check_escape_function.rb │ │ │ ├── check_evaluation.rb │ │ │ ├── check_execute.rb │ │ │ ├── check_file_access.rb │ │ │ ├── check_file_disclosure.rb │ │ │ ├── check_filter_skipping.rb │ │ │ ├── check_force_ssl.rb │ │ │ ├── check_forgery_setting.rb │ │ │ ├── check_header_dos.rb │ │ │ ├── check_i18n_xss.rb │ │ │ ├── check_jruby_xml.rb │ │ │ ├── check_json_encoding.rb │ │ │ ├── check_json_entity_escape.rb │ │ │ ├── check_json_parsing.rb │ │ │ ├── check_link_to.rb │ │ │ ├── check_link_to_href.rb │ │ │ ├── check_mail_to.rb │ │ │ ├── check_mass_assignment.rb │ │ │ ├── check_mime_type_dos.rb │ │ │ ├── check_model_attr_accessible.rb │ │ │ ├── check_model_attributes.rb │ │ │ ├── check_model_serialize.rb │ │ │ ├── check_nested_attributes.rb │ │ │ ├── check_nested_attributes_bypass.rb │ │ │ ├── check_number_to_currency.rb │ │ │ ├── check_page_caching_cve.rb │ │ │ ├── check_pathname.rb │ │ │ ├── check_permit_attributes.rb │ │ │ ├── check_quote_table_name.rb │ │ │ ├── check_ransack.rb │ │ │ ├── check_redirect.rb │ │ │ ├── check_regex_dos.rb │ │ │ ├── check_render.rb │ │ │ ├── check_render_dos.rb │ │ │ ├── check_render_inline.rb │ │ │ ├── check_render_rce.rb │ │ │ ├── check_response_splitting.rb │ │ │ ├── check_reverse_tabnabbing.rb │ │ │ ├── check_route_dos.rb │ │ │ ├── check_safe_buffer_manipulation.rb │ │ │ ├── check_sanitize_config_cve.rb │ │ │ ├── check_sanitize_methods.rb │ │ │ ├── check_secrets.rb │ │ │ ├── check_select_tag.rb │ │ │ ├── check_select_vulnerability.rb │ │ │ ├── check_send.rb │ │ │ ├── check_send_file.rb │ │ │ ├── check_session_manipulation.rb │ │ │ ├── check_session_settings.rb │ │ │ ├── check_simple_format.rb │ │ │ ├── check_single_quotes.rb │ │ │ ├── check_skip_before_filter.rb │ │ │ ├── check_sprockets_path_traversal.rb │ │ │ ├── check_sql.rb │ │ │ ├── check_sql_cves.rb │ │ │ ├── check_ssl_verify.rb │ │ │ ├── check_strip_tags.rb │ │ │ ├── check_symbol_dos.rb │ │ │ ├── check_symbol_dos_cve.rb │ │ │ ├── check_template_injection.rb │ │ │ ├── check_translate_bug.rb │ │ │ ├── check_unsafe_reflection.rb │ │ │ ├── check_unsafe_reflection_methods.rb │ │ │ ├── check_unscoped_find.rb │ │ │ ├── check_validation_regex.rb │ │ │ ├── check_verb_confusion.rb │ │ │ ├── check_weak_hash.rb │ │ │ ├── check_weak_rsa_key.rb │ │ │ ├── check_without_protection.rb │ │ │ ├── check_xml_dos.rb │ │ │ ├── check_yaml_parsing.rb │ │ │ └── eol_check.rb │ │ ├── checks.rb │ │ ├── codeclimate/ │ │ │ └── engine_configuration.rb │ │ ├── commandline.rb │ │ ├── differ.rb │ │ ├── file_parser.rb │ │ ├── file_path.rb │ │ ├── format/ │ │ │ └── style.css │ │ ├── logger.rb │ │ ├── messages.rb │ │ ├── options.rb │ │ ├── parsers/ │ │ │ ├── haml6_embedded.rb │ │ │ ├── haml_embedded.rb │ │ │ ├── rails_erubi.rb │ │ │ ├── slim_embedded.rb │ │ │ └── template_parser.rb │ │ ├── processor.rb │ │ ├── processors/ │ │ │ ├── alias_processor.rb │ │ │ ├── base_processor.rb │ │ │ ├── config_processor.rb │ │ │ ├── controller_alias_processor.rb │ │ │ ├── controller_processor.rb │ │ │ ├── erb_template_processor.rb │ │ │ ├── erubi_template_procesor.rb │ │ │ ├── gem_processor.rb │ │ │ ├── haml6_template_processor.rb │ │ │ ├── haml_template_processor.rb │ │ │ ├── lib/ │ │ │ │ ├── basic_processor.rb │ │ │ │ ├── call_conversion_helper.rb │ │ │ │ ├── file_type_detector.rb │ │ │ │ ├── find_all_calls.rb │ │ │ │ ├── find_call.rb │ │ │ │ ├── find_return_value.rb │ │ │ │ ├── module_helper.rb │ │ │ │ ├── processor_helper.rb │ │ │ │ ├── rails2_config_processor.rb │ │ │ │ ├── rails2_route_processor.rb │ │ │ │ ├── rails3_config_processor.rb │ │ │ │ ├── rails3_route_processor.rb │ │ │ │ ├── rails4_config_processor.rb │ │ │ │ ├── render_helper.rb │ │ │ │ ├── render_path.rb │ │ │ │ ├── route_helper.rb │ │ │ │ └── safe_call_helper.rb │ │ │ ├── library_processor.rb │ │ │ ├── model_processor.rb │ │ │ ├── output_processor.rb │ │ │ ├── route_processor.rb │ │ │ ├── slim_template_processor.rb │ │ │ ├── template_alias_processor.rb │ │ │ └── template_processor.rb │ │ ├── report/ │ │ │ ├── config/ │ │ │ │ └── remediation.yml │ │ │ ├── ignore/ │ │ │ │ ├── config.rb │ │ │ │ └── interactive.rb │ │ │ ├── pager.rb │ │ │ ├── renderer.rb │ │ │ ├── report_base.rb │ │ │ ├── report_codeclimate.rb │ │ │ ├── report_csv.rb │ │ │ ├── report_github.rb │ │ │ ├── report_hash.rb │ │ │ ├── report_html.rb │ │ │ ├── report_json.rb │ │ │ ├── report_junit.rb │ │ │ ├── report_markdown.rb │ │ │ ├── report_sarif.rb │ │ │ ├── report_sonar.rb │ │ │ ├── report_table.rb │ │ │ ├── report_tabs.rb │ │ │ ├── report_text.rb │ │ │ └── templates/ │ │ │ ├── controller_overview.html.erb │ │ │ ├── controller_warnings.html.erb │ │ │ ├── error_overview.html.erb │ │ │ ├── header.html.erb │ │ │ ├── ignored_warnings.html.erb │ │ │ ├── model_warnings.html.erb │ │ │ ├── overview.html.erb │ │ │ ├── security_warnings.html.erb │ │ │ ├── template_overview.html.erb │ │ │ ├── view_warnings.html.erb │ │ │ └── warning_overview.html.erb │ │ ├── report.rb │ │ ├── rescanner.rb │ │ ├── scanner.rb │ │ ├── tracker/ │ │ │ ├── collection.rb │ │ │ ├── config.rb │ │ │ ├── constants.rb │ │ │ ├── controller.rb │ │ │ ├── file_cache.rb │ │ │ ├── library.rb │ │ │ ├── method_info.rb │ │ │ ├── model.rb │ │ │ └── template.rb │ │ ├── tracker.rb │ │ ├── util.rb │ │ ├── version.rb │ │ ├── warning.rb │ │ └── warning_codes.rb │ ├── brakeman.rb │ └── ruby_parser/ │ ├── bm_sexp.rb │ └── bm_sexp_processor.rb └── test/ ├── README.md ├── apps/ │ ├── active_record_only/ │ │ ├── Gemfile │ │ ├── app/ │ │ │ └── models/ │ │ │ └── book.rb │ │ └── script/ │ │ └── .gitkeep │ ├── rails2/ │ │ ├── README │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ ├── emails_controller.rb │ │ │ │ ├── home_controller.rb │ │ │ │ └── other_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ ├── home_helper.rb │ │ │ │ └── other_helper.rb │ │ │ ├── models/ │ │ │ │ ├── account.rb │ │ │ │ ├── email.rb │ │ │ │ ├── protected.rb │ │ │ │ ├── unprotected.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── home/ │ │ │ │ ├── _models.html.erb │ │ │ │ ├── index.html.erb │ │ │ │ ├── test_command.html.erb │ │ │ │ ├── test_content_tag.html.erb │ │ │ │ ├── test_cookie.html.erb │ │ │ │ ├── test_dynamic_render.html.erb │ │ │ │ ├── test_eval.html.erb │ │ │ │ ├── test_filter.html.erb │ │ │ │ ├── test_link_to.html.erb │ │ │ │ ├── test_mass_assignment.html.erb │ │ │ │ ├── test_model.html.erb │ │ │ │ ├── test_params.html.erb │ │ │ │ ├── test_redirect.html.erb │ │ │ │ ├── test_render.html.erb │ │ │ │ ├── test_render_template.html.haml │ │ │ │ ├── test_sanitized_param.html.erb │ │ │ │ ├── test_send_target.html.erb │ │ │ │ ├── test_sql.html.erb │ │ │ │ ├── test_strip_tags.html.erb │ │ │ │ ├── test_to_json.html.erb │ │ │ │ └── test_xss_with_or.html.erb │ │ │ ├── layouts/ │ │ │ │ └── thing.html.erb │ │ │ └── other/ │ │ │ ├── _account.html.haml │ │ │ ├── _user.html.erb │ │ │ ├── ignore_me.html.erb │ │ │ ├── not_used.html.erb │ │ │ ├── test_collection.html.erb │ │ │ ├── test_env.html.erb │ │ │ ├── test_haml_stuff.html.haml │ │ │ ├── test_iteration.html.erb │ │ │ ├── test_locals.html.erb │ │ │ ├── test_object.html.erb │ │ │ ├── test_to_i.html.erb │ │ │ ├── test_trim_mode.html.erb │ │ │ └── xss_dupes.html.erb │ │ ├── config/ │ │ │ ├── boot.rb │ │ │ ├── brakeman.ignore │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── cookie_verification_secret.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── new_rails_defaults.rb │ │ │ │ ├── security_defaults.rb │ │ │ │ └── session_store.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── routes.rb │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ ├── 20110520193611_create_users.rb │ │ │ │ └── 20110523184125_create_accounts.rb │ │ │ └── seeds.rb │ │ ├── doc/ │ │ │ └── README_FOR_APP │ │ ├── lib/ │ │ │ └── generators/ │ │ │ └── test_generator/ │ │ │ └── templates/ │ │ │ └── model.rb │ │ ├── log/ │ │ │ ├── development.log │ │ │ ├── production.log │ │ │ ├── server.log │ │ │ └── test.log │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ ├── index.html │ │ │ ├── javascripts/ │ │ │ │ ├── application.js │ │ │ │ ├── controls.js │ │ │ │ ├── dragdrop.js │ │ │ │ ├── effects.js │ │ │ │ └── prototype.js │ │ │ └── robots.txt │ │ ├── script/ │ │ │ ├── about │ │ │ ├── console │ │ │ ├── dbconsole │ │ │ ├── destroy │ │ │ ├── generate │ │ │ ├── performance/ │ │ │ │ ├── benchmarker │ │ │ │ └── profiler │ │ │ ├── plugin │ │ │ ├── runner │ │ │ └── server │ │ └── test/ │ │ ├── fixtures/ │ │ │ ├── accounts.yml │ │ │ └── users.yml │ │ ├── functional/ │ │ │ ├── home_controller_test.rb │ │ │ └── other_controller_test.rb │ │ ├── performance/ │ │ │ └── browsing_test.rb │ │ ├── test_helper.rb │ │ └── unit/ │ │ ├── account_test.rb │ │ ├── helpers/ │ │ │ ├── home_helper_test.rb │ │ │ └── other_helper_test.rb │ │ └── user_test.rb │ ├── rails3/ │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── README │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ ├── base_thing.rb │ │ │ │ ├── before_controller.rb │ │ │ │ ├── child_controller.rb │ │ │ │ ├── home_controller.rb │ │ │ │ ├── nested_controller.rb │ │ │ │ ├── other_controller.rb │ │ │ │ └── products_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ ├── home_helper.rb │ │ │ │ ├── other_helper.rb │ │ │ │ └── products_helper.rb │ │ │ ├── models/ │ │ │ │ ├── account.rb │ │ │ │ ├── bill.rb │ │ │ │ ├── noticia.rb │ │ │ │ ├── notifier.rb │ │ │ │ ├── product.rb │ │ │ │ ├── purchase.rb │ │ │ │ ├── underline_model.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── before/ │ │ │ │ ├── use_filter12345.html.erb │ │ │ │ └── use_filters12.html.erb │ │ │ ├── child/ │ │ │ │ └── action_in_child.html.erb │ │ │ ├── home/ │ │ │ │ ├── index.html.erb │ │ │ │ ├── test_command.html.erb │ │ │ │ ├── test_content_tag.html.erb │ │ │ │ ├── test_cookie.html.erb │ │ │ │ ├── test_dynamic_render.html.erb │ │ │ │ ├── test_eval.html.erb │ │ │ │ ├── test_file_access.html.erb │ │ │ │ ├── test_filter.html.erb │ │ │ │ ├── test_mass_assignment.html.erb │ │ │ │ ├── test_model.html.erb │ │ │ │ ├── test_newlines.html.erb │ │ │ │ ├── test_params.html.erb │ │ │ │ ├── test_redirect.html.erb │ │ │ │ ├── test_render.html.erb │ │ │ │ └── test_sql.html.erb │ │ │ ├── layouts/ │ │ │ │ └── application.html.erb │ │ │ ├── other/ │ │ │ │ ├── _account.html.haml │ │ │ │ ├── _user.html.erb │ │ │ │ ├── test_collection.html.erb │ │ │ │ ├── test_iteration.html.erb │ │ │ │ ├── test_locals.html.erb │ │ │ │ ├── test_mail_to.html.erb │ │ │ │ ├── test_object.html.erb │ │ │ │ ├── test_select_tag.html.erb │ │ │ │ ├── test_send_file.html.erb │ │ │ │ └── test_strip_tags.html.erb │ │ │ ├── products/ │ │ │ │ ├── _form.html.erb │ │ │ │ ├── edit.html.erb │ │ │ │ ├── index.html.erb │ │ │ │ ├── new.html.erb │ │ │ │ └── show.html.erb │ │ │ └── whatever/ │ │ │ └── wherever/ │ │ │ └── nested/ │ │ │ └── so_nested.html.erb │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── brakeman.yml │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── disable_xml_parsing.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ └── session_store.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── routes.rb │ │ ├── config.ru │ │ ├── db/ │ │ │ └── seeds.rb │ │ ├── doc/ │ │ │ └── README_FOR_APP │ │ ├── lib/ │ │ │ ├── controller_filter.rb │ │ │ └── tasks/ │ │ │ └── .gitkeep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ ├── index.html │ │ │ ├── javascripts/ │ │ │ │ ├── application.js │ │ │ │ ├── controls.js │ │ │ │ ├── dragdrop.js │ │ │ │ ├── effects.js │ │ │ │ ├── prototype.js │ │ │ │ └── rails.js │ │ │ ├── robots.txt │ │ │ └── stylesheets/ │ │ │ └── .gitkeep │ │ ├── script/ │ │ │ └── rails │ │ ├── test/ │ │ │ ├── functional/ │ │ │ │ ├── home_controller_test.rb │ │ │ │ └── other_controller_test.rb │ │ │ ├── performance/ │ │ │ │ └── browsing_test.rb │ │ │ ├── test_helper.rb │ │ │ └── unit/ │ │ │ └── helpers/ │ │ │ ├── home_helper_test.rb │ │ │ └── other_helper_test.rb │ │ └── vendor/ │ │ └── plugins/ │ │ └── .gitkeep │ ├── rails3.1/ │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── README │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── javascripts/ │ │ │ │ │ ├── application.js │ │ │ │ │ └── users.js.coffee │ │ │ │ └── stylesheets/ │ │ │ │ ├── application.css │ │ │ │ ├── scaffolds.css.scss │ │ │ │ └── users.css.scss │ │ │ ├── controllers/ │ │ │ │ ├── admin_controller.rb │ │ │ │ ├── application_controller.rb │ │ │ │ ├── mixins/ │ │ │ │ │ └── user_mixin.rb │ │ │ │ ├── other_controller.rb │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── mailers/ │ │ │ │ └── .gitkeep │ │ │ ├── models/ │ │ │ │ ├── .gitkeep │ │ │ │ ├── account.rb │ │ │ │ ├── product.rb │ │ │ │ ├── some_model.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ └── application.html.erb │ │ │ ├── other/ │ │ │ │ ├── _partial.html.erb │ │ │ │ ├── a.html.erb │ │ │ │ ├── b.html.erb │ │ │ │ ├── c.html.erb │ │ │ │ ├── d.html.erb │ │ │ │ ├── e.html.erb │ │ │ │ ├── f.html.erb │ │ │ │ ├── g.html.erb │ │ │ │ ├── test_model_in_haml.html.haml │ │ │ │ ├── test_partial.html.erb │ │ │ │ ├── test_select_tag.html.erb │ │ │ │ ├── test_string_interp.html.erb │ │ │ │ └── test_strip_tags.html.erb │ │ │ └── users/ │ │ │ ├── _bio.html.erb │ │ │ ├── _circular.html.erb │ │ │ ├── _circular_too.html.erb │ │ │ ├── _form.html.erb │ │ │ ├── _test_layout.html.erb │ │ │ ├── _user.html.erb │ │ │ ├── circular_render.html.erb │ │ │ ├── drape.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── interpolated_value.html.haml │ │ │ ├── json_test.html.erb │ │ │ ├── mixin_default.html.erb │ │ │ ├── mixin_template.html.erb │ │ │ ├── new.html.erb │ │ │ ├── show.html.erb │ │ │ ├── test_assign_if.html.erb │ │ │ ├── test_assign_twice.html.erb │ │ │ ├── test_less_simple_helpers.html.erb │ │ │ └── test_simple_helper.html.erb │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_type_fix.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── secret_token.rb │ │ │ │ ├── session_store.rb │ │ │ │ ├── set_escape_json.rb │ │ │ │ ├── unset_escape_json.rb │ │ │ │ ├── wrap_parameters.rb │ │ │ │ ├── xml_parsing.rb │ │ │ │ └── yaml_parsing.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── routes.rb │ │ ├── config.ru │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ └── 20110908172338_create_users.rb │ │ │ └── seeds.rb │ │ ├── doc/ │ │ │ └── README_FOR_APP │ │ ├── lib/ │ │ │ ├── alib.rb │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── somelib.rb │ │ │ └── tasks/ │ │ │ └── .gitkeep │ │ ├── log/ │ │ │ └── .gitkeep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ ├── index.html │ │ │ └── robots.txt │ │ ├── script/ │ │ │ └── rails │ │ ├── test/ │ │ │ ├── fixtures/ │ │ │ │ ├── .gitkeep │ │ │ │ └── users.yml │ │ │ ├── functional/ │ │ │ │ ├── .gitkeep │ │ │ │ └── users_controller_test.rb │ │ │ ├── integration/ │ │ │ │ └── .gitkeep │ │ │ ├── performance/ │ │ │ │ └── browsing_test.rb │ │ │ ├── test_helper.rb │ │ │ └── unit/ │ │ │ ├── .gitkeep │ │ │ ├── helpers/ │ │ │ │ └── users_helper_test.rb │ │ │ └── user_test.rb │ │ └── vendor/ │ │ ├── assets/ │ │ │ └── stylesheets/ │ │ │ └── .gitkeep │ │ └── plugins/ │ │ └── .gitkeep │ ├── rails3.2/ │ │ ├── Gemfile │ │ ├── README.rdoc │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── javascripts/ │ │ │ │ │ ├── application.js │ │ │ │ │ └── users.js.coffee │ │ │ │ └── stylesheets/ │ │ │ │ ├── application.css │ │ │ │ ├── scaffolds.css.scss │ │ │ │ └── users.css.scss │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ ├── exec_controller/ │ │ │ │ │ └── command_dependency.rb │ │ │ │ ├── exec_controller.rb │ │ │ │ ├── removal_controller.rb │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── models/ │ │ │ │ ├── .gitkeep │ │ │ │ ├── account.rb │ │ │ │ ├── multi_model.rb │ │ │ │ ├── no_protection.rb │ │ │ │ ├── user/ │ │ │ │ │ └── command_dependency.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ └── application.html.erb │ │ │ ├── removal/ │ │ │ │ ├── _partial.html.erb │ │ │ │ ├── controller_removed.html.erb │ │ │ │ └── implicit_render.html.erb │ │ │ └── users/ │ │ │ ├── _form.html.erb │ │ │ ├── _slimmer.html.slim │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── mixed_in.html.erb │ │ │ ├── new.html.erb │ │ │ ├── sanitized.html.erb │ │ │ ├── show.html.erb │ │ │ └── slimming.html.slim │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── header_dos_protection.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── secret_token.rb │ │ │ │ ├── session_store.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── routes.rb │ │ ├── config.ru │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── tasks/ │ │ │ │ └── .gitkeep │ │ │ └── user_controller_mixin.rb │ │ └── script/ │ │ └── rails │ ├── rails4/ │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── README.rdoc │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── api/ │ │ │ │ └── api.rb │ │ │ ├── assets/ │ │ │ │ ├── javascripts/ │ │ │ │ │ └── application.js │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── controllers/ │ │ │ │ ├── another_controller.rb │ │ │ │ ├── application_controller.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── friendly_controller.rb │ │ │ │ ├── mixed_controller.rb │ │ │ │ ├── mixed_in_proxy.rb │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ ├── .keep │ │ │ │ ├── account.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── email.rb │ │ │ │ ├── phone.rb │ │ │ │ ├── recursive/ │ │ │ │ │ └── stack_level.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── _global_partial.html.erb │ │ │ ├── another/ │ │ │ │ ├── html_safe_is_not.html.erb │ │ │ │ ├── overflow.html.erb │ │ │ │ ├── use_params_in_regex.html.erb │ │ │ │ └── various_xss.html.erb │ │ │ ├── layouts/ │ │ │ │ └── application.html.erb │ │ │ └── users/ │ │ │ ├── eval_it.html.erb │ │ │ ├── haml_test.html.haml │ │ │ ├── index.html.erb │ │ │ ├── more_haml.html.haml │ │ │ └── test_parse.html.erb │ │ ├── bin/ │ │ │ ├── rails │ │ │ └── rake │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── brakeman.ignore │ │ │ ├── brakeman.yml │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── i18n.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── secret_token.rb │ │ │ │ ├── session_store.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── routes.rb │ │ │ └── secrets.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ └── seeds.rb │ │ ├── external_checks/ │ │ │ └── check_external_check_test.rb │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ ├── sweet_lib.rb │ │ │ └── tasks/ │ │ │ ├── .keep │ │ │ └── some_task.rb │ │ ├── log/ │ │ │ └── .keep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ └── robots.txt │ │ ├── test/ │ │ │ ├── controllers/ │ │ │ │ └── .keep │ │ │ ├── fixtures/ │ │ │ │ └── .keep │ │ │ ├── helpers/ │ │ │ │ └── .keep │ │ │ ├── integration/ │ │ │ │ └── .keep │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ └── .keep │ │ │ └── test_helper.rb │ │ └── vendor/ │ │ └── assets/ │ │ ├── javascripts/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ └── .keep │ ├── rails4_non_standard_structure/ │ │ ├── .gitignore │ │ ├── README.rdoc │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ ├── javascripts/ │ │ │ │ │ └── application.js │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ └── concerns/ │ │ │ │ └── .keep │ │ │ ├── foo_team/ │ │ │ │ ├── controllers/ │ │ │ │ │ └── api/ │ │ │ │ │ └── foo_controller.rb │ │ │ │ ├── models/ │ │ │ │ │ └── foo.rb │ │ │ │ └── views/ │ │ │ │ └── foo.html.erb │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ ├── .keep │ │ │ │ └── concerns/ │ │ │ │ └── .keep │ │ │ └── views/ │ │ │ └── layouts/ │ │ │ └── application.html.erb │ │ ├── bin/ │ │ │ ├── rails │ │ │ ├── rake │ │ │ └── spring │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── session_store.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── routes.rb │ │ │ └── secrets.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ └── seeds.rb │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ └── tasks/ │ │ │ └── .keep │ │ ├── log/ │ │ │ └── .keep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ └── robots.txt │ │ ├── rails4test.gemspec │ │ ├── test/ │ │ │ ├── controllers/ │ │ │ │ └── .keep │ │ │ ├── fixtures/ │ │ │ │ └── .keep │ │ │ ├── helpers/ │ │ │ │ └── .keep │ │ │ ├── integration/ │ │ │ │ └── .keep │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ └── .keep │ │ │ └── test_helper.rb │ │ └── vendor/ │ │ └── assets/ │ │ ├── javascripts/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ └── .keep │ ├── rails4_with_engines/ │ │ ├── README.rdoc │ │ ├── Rakefile │ │ ├── alt_engines/ │ │ │ └── admin_stuff/ │ │ │ └── app/ │ │ │ ├── controllers/ │ │ │ │ └── admin_controller.rb │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ └── views/ │ │ │ └── admin/ │ │ │ └── debug.html.erb │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── javascripts/ │ │ │ │ │ └── application.js │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ └── concerns/ │ │ │ │ └── .keep │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ ├── .keep │ │ │ │ └── concerns/ │ │ │ │ └── .keep │ │ │ └── views/ │ │ │ └── layouts/ │ │ │ └── application.html.erb │ │ ├── bin/ │ │ │ ├── rails │ │ │ └── rake │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── brakeman.yml │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── nested_attributes_bypass_fix.rb │ │ │ │ ├── secret_token.rb │ │ │ │ ├── session_store.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── routes.rb │ │ ├── config.ru │ │ ├── db/ │ │ │ └── seeds.rb │ │ ├── engines/ │ │ │ └── user_removal/ │ │ │ ├── app/ │ │ │ │ ├── assets/ │ │ │ │ │ ├── javascripts/ │ │ │ │ │ │ └── users.js.coffee │ │ │ │ │ └── stylesheets/ │ │ │ │ │ └── users.css.scss │ │ │ │ ├── controllers/ │ │ │ │ │ ├── base_controller.rb │ │ │ │ │ ├── removal_controller.rb │ │ │ │ │ └── users_controller.rb │ │ │ │ ├── helpers/ │ │ │ │ │ ├── application_helper.rb │ │ │ │ │ └── users_helper.rb │ │ │ │ ├── models/ │ │ │ │ │ ├── .gitkeep │ │ │ │ │ ├── account.rb │ │ │ │ │ ├── no_protection.rb │ │ │ │ │ └── user.rb │ │ │ │ └── views/ │ │ │ │ ├── removal/ │ │ │ │ │ ├── _partial.html.erb │ │ │ │ │ ├── controller_removed.html.erb │ │ │ │ │ └── implicit_render.html.erb │ │ │ │ └── users/ │ │ │ │ ├── _form.html.erb │ │ │ │ ├── _slimmer.html.slim │ │ │ │ ├── edit.html.erb │ │ │ │ ├── index.html.erb │ │ │ │ ├── mixed_in.html.erb │ │ │ │ ├── new.html.erb │ │ │ │ ├── sanitized.html.erb │ │ │ │ ├── show.html.erb │ │ │ │ └── slimming.html.slim │ │ │ ├── config/ │ │ │ │ └── routes.rb │ │ │ └── lib/ │ │ │ └── user_removal.rb │ │ ├── gems.rb │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ └── tasks/ │ │ │ └── .keep │ │ ├── log/ │ │ │ └── .keep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ └── robots.txt │ │ ├── script/ │ │ │ └── .keep │ │ ├── test/ │ │ │ ├── controllers/ │ │ │ │ └── .keep │ │ │ ├── fixtures/ │ │ │ │ └── .keep │ │ │ ├── helpers/ │ │ │ │ └── .keep │ │ │ ├── integration/ │ │ │ │ └── .keep │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ └── .keep │ │ │ └── test_helper.rb │ │ └── vendor/ │ │ └── assets/ │ │ ├── javascripts/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ └── .keep │ ├── rails5/ │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── README.md │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ ├── javascripts/ │ │ │ │ │ ├── application.js │ │ │ │ │ ├── cable.coffee │ │ │ │ │ ├── channels/ │ │ │ │ │ │ └── .keep │ │ │ │ │ └── users.coffee │ │ │ │ └── stylesheets/ │ │ │ │ ├── application.css │ │ │ │ ├── scaffold.css │ │ │ │ └── users.css │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ ├── concerns/ │ │ │ │ │ ├── .keep │ │ │ │ │ ├── concerning.rb │ │ │ │ │ └── forgery_protection.rb │ │ │ │ ├── file_controller.rb │ │ │ │ ├── mixed_controller.rb │ │ │ │ ├── users_controller.rb │ │ │ │ └── widget_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── jobs/ │ │ │ │ └── application_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── thing.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ ├── application.html.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ ├── mailer.text.erb │ │ │ │ └── users.html.erb │ │ │ ├── users/ │ │ │ │ ├── _form.html.erb │ │ │ │ ├── edit.html.erb │ │ │ │ ├── find_and_preserve.html.haml │ │ │ │ ├── if_thing.html.haml │ │ │ │ ├── index.html.erb │ │ │ │ ├── index.json.jbuilder │ │ │ │ ├── new.html.erb │ │ │ │ ├── safe_call_params.html.haml │ │ │ │ ├── sanitizing.html.erb │ │ │ │ ├── show.html.erb │ │ │ │ └── show.json.jbuilder │ │ │ └── widget/ │ │ │ ├── attributes.html.haml │ │ │ ├── content_tag.html.erb │ │ │ ├── graphql.html.erb │ │ │ ├── haml_test.html.haml │ │ │ ├── no_html.haml │ │ │ └── show.html.erb │ │ ├── bin/ │ │ │ ├── rails │ │ │ ├── rake │ │ │ ├── setup │ │ │ ├── spring │ │ │ └── update │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── brakeman.yml │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── active_record_belongs_to_required_by_default.rb │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── callback_terminator.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── cors.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── request_forgery_protection.rb │ │ │ │ ├── secrets.rb │ │ │ │ ├── session_store.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── redis/ │ │ │ │ └── cable.yml │ │ │ ├── routes.rb │ │ │ └── secrets.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ └── 20160127223106_create_users.rb │ │ │ └── seeds.rb │ │ ├── external_checks/ │ │ │ └── check_external_check_test.rb │ │ ├── lib/ │ │ │ ├── a_lib.rb │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ ├── lib.rb │ │ │ └── tasks/ │ │ │ └── .keep │ │ ├── log/ │ │ │ └── .keep │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ └── robots.txt │ │ ├── test/ │ │ │ ├── controllers/ │ │ │ │ ├── .keep │ │ │ │ └── users_controller_test.rb │ │ │ ├── fixtures/ │ │ │ │ ├── .keep │ │ │ │ ├── files/ │ │ │ │ │ └── .keep │ │ │ │ └── users.yml │ │ │ ├── helpers/ │ │ │ │ └── .keep │ │ │ ├── integration/ │ │ │ │ └── .keep │ │ │ ├── mailers/ │ │ │ │ └── .keep │ │ │ ├── models/ │ │ │ │ ├── .keep │ │ │ │ └── user_test.rb │ │ │ └── test_helper.rb │ │ ├── tmp/ │ │ │ └── .keep │ │ └── vendor/ │ │ └── assets/ │ │ ├── javascripts/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ └── .keep │ ├── rails5.2/ │ │ ├── .ruby-version │ │ ├── Gemfile │ │ ├── README.md │ │ ├── Rakefile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ ├── javascripts/ │ │ │ │ │ ├── application.js │ │ │ │ │ ├── cable.js │ │ │ │ │ └── channels/ │ │ │ │ │ └── .keep │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── jobs/ │ │ │ │ ├── application_job.rb │ │ │ │ └── delete_stuff_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── home/ │ │ │ │ └── index.html.erb │ │ │ ├── layouts/ │ │ │ │ ├── application.html.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ └── mailer.text.erb │ │ │ └── users/ │ │ │ ├── _empty_partial_name.html.erb │ │ │ ├── _foo.html.haml │ │ │ ├── _foo2.html.haml │ │ │ ├── kwsplat.html.haml │ │ │ ├── link.html.erb │ │ │ ├── not_not.html.erb │ │ │ ├── one.html.haml │ │ │ ├── smart.html.slim │ │ │ ├── test_empty_partial_name.html.erb │ │ │ └── two.html.slim │ │ ├── bin/ │ │ │ ├── rails │ │ │ ├── rake │ │ │ ├── setup │ │ │ ├── spring │ │ │ ├── update │ │ │ └── yarn │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── cable.yml │ │ │ ├── credentials.yml.enc │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── content_security_policy.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── mime_types.rb │ │ │ │ ├── oj.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ ├── secrets.yml │ │ │ ├── spring.rb │ │ │ └── storage.yml │ │ ├── config.ru │ │ ├── db/ │ │ │ ├── migrate/ │ │ │ │ └── 20171208205700_create_active_storage_tables.active_storage.rb │ │ │ └── seeds.rb │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ ├── factory_bot.rb │ │ │ ├── initthing.rb │ │ │ ├── shell.rb │ │ │ └── tasks/ │ │ │ └── .keep │ │ ├── log/ │ │ │ └── .keep │ │ ├── package.json │ │ ├── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ ├── 500.html │ │ │ └── robots.txt │ │ └── vendor/ │ │ ├── .keep │ │ └── vendored_thing.rb │ ├── rails6/ │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── Rakefile │ │ ├── another_lib_dir/ │ │ │ └── some_lib.rb │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ └── stylesheets/ │ │ │ │ ├── application.css │ │ │ │ ├── scaffolds.scss │ │ │ │ └── users.scss │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── components/ │ │ │ │ ├── base_component.rb │ │ │ │ ├── test_component.rb │ │ │ │ ├── test_view_component.rb │ │ │ │ ├── test_view_component_contrib.rb │ │ │ │ ├── test_view_component_fully_qualified_ancestor.rb │ │ │ │ └── text_phlex_component.rb │ │ │ ├── controllers/ │ │ │ │ ├── accounts_controller.rb │ │ │ │ ├── application_controller.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── groups_controller.rb │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── javascript/ │ │ │ │ ├── channels/ │ │ │ │ │ ├── consumer.js │ │ │ │ │ └── index.js │ │ │ │ └── packs/ │ │ │ │ └── application.js │ │ │ ├── jobs/ │ │ │ │ └── application_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── group.rb │ │ │ │ └── user.rb │ │ │ ├── views/ │ │ │ │ ├── layouts/ │ │ │ │ │ ├── application.html.erb │ │ │ │ │ ├── mailer.html.erb │ │ │ │ │ └── mailer.text.erb │ │ │ │ └── users/ │ │ │ │ ├── _form.html.erb │ │ │ │ ├── _user.json.jbuilder │ │ │ │ ├── edit.html.erb │ │ │ │ ├── index.html.erb │ │ │ │ ├── index.json.jbuilder │ │ │ │ ├── new.html.erb │ │ │ │ ├── show.html.erb │ │ │ │ └── show.json.jbuilder │ │ │ └── widgets/ │ │ │ └── widget.rb │ │ ├── babel.config.js │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── cable.yml │ │ │ ├── credentials.yml.enc │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── initializers/ │ │ │ │ ├── allow_all_parameters.rb │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ ├── assets.rb │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ ├── content_security_policy.rb │ │ │ │ ├── cookies_serializer.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── json_escape.rb │ │ │ │ ├── mime_types.rb │ │ │ │ └── wrap_parameters.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ ├── spring.rb │ │ │ ├── storage.yml │ │ │ ├── webpack/ │ │ │ │ ├── development.js │ │ │ │ ├── environment.js │ │ │ │ ├── production.js │ │ │ │ └── test.js │ │ │ └── webpacker.yml │ │ ├── config.ru │ │ ├── lib/ │ │ │ ├── assets/ │ │ │ │ └── .keep │ │ │ ├── run_stuff.rb │ │ │ ├── tasks/ │ │ │ │ └── .keep │ │ │ └── view_component/ │ │ │ └── base.rb │ │ ├── package.json │ │ └── postcss.config.js │ ├── rails7/ │ │ ├── MyGemfile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ ├── config/ │ │ │ │ │ └── manifest.js │ │ │ │ ├── images/ │ │ │ │ │ └── .keep │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── controllers/ │ │ │ │ ├── admin_controller.rb │ │ │ │ ├── application_controller.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ └── application_helper.rb │ │ │ ├── javascript/ │ │ │ │ ├── application.js │ │ │ │ └── controllers/ │ │ │ │ ├── application.js │ │ │ │ ├── hello_controller.js │ │ │ │ └── index.js │ │ │ ├── jobs/ │ │ │ │ └── application_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── book.rb │ │ │ │ ├── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── thing.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ └── layouts/ │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── cable.yml │ │ │ ├── credentials.yml.enc │ │ │ ├── database.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── importmap.rb │ │ │ ├── initializers/ │ │ │ │ ├── assets.rb │ │ │ │ ├── content_security_policy.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ ├── permissions_policy.rb │ │ │ │ └── sanitizers.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── master.key │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ └── storage.yml │ │ └── lib/ │ │ ├── assets/ │ │ │ └── .keep │ │ ├── some_lib.rb │ │ └── tasks/ │ │ └── .keep │ ├── rails8/ │ │ ├── Gemfile │ │ ├── app/ │ │ │ ├── assets/ │ │ │ │ └── stylesheets/ │ │ │ │ └── application.css │ │ │ ├── channels/ │ │ │ │ └── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ ├── controllers/ │ │ │ │ ├── application_controller.rb │ │ │ │ └── users_controller.rb │ │ │ ├── helpers/ │ │ │ │ ├── application_helper.rb │ │ │ │ └── users_helper.rb │ │ │ ├── javascript/ │ │ │ │ ├── application.js │ │ │ │ └── controllers/ │ │ │ │ ├── application.js │ │ │ │ ├── hello_controller.js │ │ │ │ └── index.js │ │ │ ├── jobs/ │ │ │ │ └── application_job.rb │ │ │ ├── mailers/ │ │ │ │ └── application_mailer.rb │ │ │ ├── models/ │ │ │ │ ├── application_record.rb │ │ │ │ ├── thing.rb │ │ │ │ └── user.rb │ │ │ └── views/ │ │ │ ├── layouts/ │ │ │ │ ├── application.html.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ └── mailer.text.erb │ │ │ ├── pwa/ │ │ │ │ ├── manifest.json.erb │ │ │ │ └── service-worker.js │ │ │ ├── things/ │ │ │ │ ├── _thing.html.erb │ │ │ │ └── index.html.erb │ │ │ └── users/ │ │ │ ├── _form.html.erb │ │ │ ├── _user.html.erb │ │ │ ├── _user.json.jbuilder │ │ │ ├── dom_id.haml │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── index.json.jbuilder │ │ │ ├── new.html.erb │ │ │ ├── show.html.erb │ │ │ └── show.json.jbuilder │ │ ├── bin/ │ │ │ ├── brakeman │ │ │ ├── importmap │ │ │ ├── kamal │ │ │ ├── rails │ │ │ ├── rake │ │ │ ├── rubocop │ │ │ └── setup │ │ ├── config/ │ │ │ ├── application.rb │ │ │ ├── boot.rb │ │ │ ├── cable.yml │ │ │ ├── credentials.yml.enc │ │ │ ├── database.yml │ │ │ ├── deploy.yml │ │ │ ├── environment.rb │ │ │ ├── environments/ │ │ │ │ ├── development.rb │ │ │ │ ├── production.rb │ │ │ │ └── test.rb │ │ │ ├── importmap.rb │ │ │ ├── initializers/ │ │ │ │ ├── assets.rb │ │ │ │ ├── content_security_policy.rb │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ ├── inflections.rb │ │ │ │ └── permissions_policy.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── master.key │ │ │ ├── puma.rb │ │ │ ├── routes.rb │ │ │ └── storage.yml │ │ ├── config.ru │ │ └── lib/ │ │ ├── evals.rb │ │ └── masgn.rb │ └── rails_with_xss_plugin/ │ ├── Gemfile │ ├── README │ ├── Rakefile │ ├── app/ │ │ ├── controllers/ │ │ │ ├── application_controller.rb │ │ │ ├── posts_controller.rb │ │ │ └── users_controller.rb │ │ ├── helpers/ │ │ │ ├── application_helper.rb │ │ │ ├── posts_helper.rb │ │ │ └── users_helper.rb │ │ ├── models/ │ │ │ ├── post.rb │ │ │ └── user.rb │ │ └── views/ │ │ ├── layouts/ │ │ │ ├── posts.html.erb │ │ │ └── users.html.erb │ │ ├── posts/ │ │ │ ├── _show.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── new.html.erb │ │ │ ├── show.html.erb │ │ │ └── show_topic.html.erb │ │ └── users/ │ │ ├── _user.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── login.html.erb │ │ ├── new.html.erb │ │ ├── results.html.erb │ │ ├── search.html.erb │ │ ├── show.html.erb │ │ ├── test_sanitize.html.erb │ │ └── to_json.html.erb │ ├── config/ │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments/ │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers/ │ │ │ ├── backtrace_silencers.rb │ │ │ ├── cookie_verification_secret.rb │ │ │ ├── inflections.rb │ │ │ ├── json_parsing.rb │ │ │ ├── mime_types.rb │ │ │ ├── new_rails_defaults.rb │ │ │ ├── session_store.rb │ │ │ ├── single_quote_workaround.rb │ │ │ └── yaml_parsing.rb │ │ ├── locales/ │ │ │ └── en.yml │ │ └── routes.rb │ ├── db/ │ │ ├── migrate/ │ │ │ ├── 20120312064721_create_users.rb │ │ │ └── 20120312065023_create_posts.rb │ │ ├── schema.rb │ │ └── seeds.rb │ ├── doc/ │ │ └── README_FOR_APP │ ├── public/ │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── javascripts/ │ │ │ ├── application.js │ │ │ ├── controls.js │ │ │ ├── dragdrop.js │ │ │ ├── effects.js │ │ │ └── prototype.js │ │ ├── robots.txt │ │ └── stylesheets/ │ │ └── scaffold.css │ ├── script/ │ │ ├── about │ │ ├── console │ │ ├── dbconsole │ │ ├── destroy │ │ ├── generate │ │ ├── performance/ │ │ │ ├── benchmarker │ │ │ └── profiler │ │ ├── plugin │ │ ├── runner │ │ └── server │ ├── test/ │ │ ├── fixtures/ │ │ │ ├── posts.yml │ │ │ └── users.yml │ │ ├── functional/ │ │ │ ├── posts_controller_test.rb │ │ │ └── users_controller_test.rb │ │ ├── performance/ │ │ │ └── browsing_test.rb │ │ ├── test_helper.rb │ │ └── unit/ │ │ ├── helpers/ │ │ │ ├── posts_helper_test.rb │ │ │ └── users_helper_test.rb │ │ ├── post_test.rb │ │ └── user_test.rb │ └── vendor/ │ └── plugins/ │ └── rails_xss/ │ └── README ├── test.rb ├── tests/ │ ├── active_record_only.rb │ ├── alias_processor.rb │ ├── app_tree.rb │ ├── brakeman.rb │ ├── call_index.rb │ ├── checks.rb │ ├── codeclimate_engine_configuration.rb │ ├── codeclimate_output.rb │ ├── commandline.rb │ ├── config.rb │ ├── constants.rb │ ├── cves.rb │ ├── differ.rb │ ├── file_cache.rb │ ├── file_parser.rb │ ├── file_path.rb │ ├── find_return_value.rb │ ├── github_output.rb │ ├── ignore.rb │ ├── json_compare.rb │ ├── json_output.rb │ ├── junit_output.rb │ ├── logger.rb │ ├── markdown_output.rb │ ├── mass_assign_disable.rb │ ├── oj.rb │ ├── only_files_option.rb │ ├── options.rb │ ├── output_processor.rb │ ├── pager.rb │ ├── parser_timeout.rb │ ├── rails2.rb │ ├── rails3.rb │ ├── rails31.rb │ ├── rails32.rb │ ├── rails4.rb │ ├── rails4_with_engines.rb │ ├── rails5.rb │ ├── rails52.rb │ ├── rails52_csrf.rb │ ├── rails6.rb │ ├── rails7.rb │ ├── rails7_redirect.rb │ ├── rails8.rb │ ├── rails_61_sql.rb │ ├── rails_lts.rb │ ├── rails_with_xss_plugin.rb │ ├── render_path.rb │ ├── report_generation.rb │ ├── rescanner.rb │ ├── routes_error.rb │ ├── sarif_output.rb │ ├── sexp.rb │ ├── sonar_output.rb │ ├── tabs_output.rb │ ├── tracker.rb │ └── warning.rb └── to_test.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 orbs: qlty: qltysh/qlty-orb@0.1 jobs: default: &default docker: - image: cimg/ruby:4.0 steps: - checkout - run: ruby --version && bundle check || bundle install - run: command: bundle exec rake - store_test_results: path: test-results test-3-2: <<: *default docker: - image: cimg/ruby:3.2 steps: - checkout - attach_workspace: at: ~/repo/tmp - run: bundle check || bundle install - run: name: Run tests and generate coverage command: | # This triggers SimpleCov to generate a coverage.json file export CC_TEST_REPORTER_ID=CC_TEST_REPORTER_ID bundle exec rake RUBYOPT='--enable-frozen-string-literal --debug-frozen-string-literal' mkdir -p tmp/ mv coverage/coverage.json tmp/coverage.json - store_test_results: path: test-results - persist_to_workspace: root: tmp paths: - coverage.json test-3-3: <<: *default docker: - image: cimg/ruby:3.3 test-3-4: <<: *default docker: - image: cimg/ruby:3.4 upload-coverage: <<: *default working_directory: ~/repo steps: - checkout - attach_workspace: at: ~/repo/tmp - qlty/coverage_publish: files: tmp/coverage.json strip_prefix: /home/circleci/project workflows: version: 2 tests: jobs: - default - test-3-2 - test-3-3 - test-3-4 - upload-coverage: requires: - test-3-2 ================================================ FILE: .dockerignore ================================================ # ignore .git and .cache folders .git .cache ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug Report about: Create a report to help us improve --- ### Background Brakeman version: ? Rails version: ? Ruby version: ? Link to Rails application code: ? ### Issue What problem are you seeing? #### Other Error Run Brakeman with `--debug` to see the full stack trace. Stack trace: ``` ? ``` ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature Request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/hanging-or-slow-scans.md ================================================ --- name: Hanging or Slow Scans about: Let us know if Brakeman is too slow --- ### Background Brakeman version: ? Rails version: ? Ruby version: ? Link to Rails application code: ? #### Hanging or Slowness _Consult https://brakemanscanner.org/docs/troubleshooting/hanging/ first_ _Please run Brakeman with `--debug` to see which file may be causing the issue._ Code example: ```ruby ? ================================================ FILE: .github/ISSUE_TEMPLATE/parsing-error.md ================================================ --- name: Parsing Error about: Report a parse error --- ### Background Brakeman version: ? Rails version: ? Ruby version: ? Link to Rails application code: ? #### Parse Error (Consult https://brakemanscanner.org/docs/troubleshooting/parse_errors/ first. Note that (most) parsing errors are from the ruby_parser library, not Brakeman itself.) Minimal example that does not parse: ```ruby ? ``` ================================================ FILE: .github/ISSUE_TEMPLATE/report-a-false-positive.md ================================================ --- name: Report a False Positive about: When Brakeman warns about something that may not be a vulnerability --- ### Background Brakeman version: ? Rails version: ? Ruby version: ? Link to Rails application code: ? #### False Positive *Full* warning from Brakeman: `?` Relevant code: ```ruby ? ``` _Why might this be a false positive?_ ================================================ FILE: .github/ISSUE_TEMPLATE/something-else.md ================================================ --- name: Something Else about: Something not covered by an existing issue type --- ================================================ FILE: .github/workflows/docker-hub-push.yml ================================================ name: docker-hub-push on: push: tags: - '*' jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64, linux/arm64 push: true tags: | presidentbeef/brakeman:latest presidentbeef/brakeman:${{ github.ref_name }} ================================================ FILE: .gitignore ================================================ Gemfile.lock coverage/ test/coverage/ .bundle bundle *.gem ================================================ FILE: CHANGES.md ================================================ # 8.0.4 - 2026-02-26 * Load 'date' library for `--ensure-latest` # 8.0.3 - 2026-02-26 * Fix `polymorphic_name` SQLi false positive (Fredrico Franco) * Fix logger behavior when loading config files * Handle application names with module prefixes * Add release age option for `--ensure-latest` # 8.0.2 - 2026-02-03 * Reline console control should use stderr * Fix logger cleanup based method (Imran Iqbal) # 8.0.1 - 2026-01-29 * Make sure to reset the cursor even when exit code is 0 # 8.0.0 - 2026-01-29 * No longer produce weak dynamic render path warnings * `--skip-libs` removed * `--index-libs` removed * Revamp of scan progress output and logging * Faster file globbing for templates (Mikael Henriksson) * Fix singleton method prefixes (viralpraxis) * Fix qualified constant lookup to respect module/class context (Mike Dalessio) * Replace Erubis with Erubi # 7.1.2 - 2025-12-25 * Update `ruby_parser` to remove version restriction (Chedli Bourguiba) * Raise minimum required Ruby to 3.2.0 * Use Minitest 6.0 * Reduce SQL injection false positives from `count` calls * Ignore more Haml attribute builder methods # 7.1.1 - 2025-11-03 * Fix false positive when calling `with_content` on ViewComponents (Peer Allan) * Word wrap text output in pager * Consider Tempfile.create.path as safe input (Ali Ismayilov) * Exclude directories before searching for files * Check each side of `or` SQL arguments * Ignore attribute builder in Haml 6 * Add `FilePath#to_path` for Ruby 3.5 compatibility (S-H-GAMELINKS) * Fix SQL injection check for calculate method (Rohan Sharma) * Fix missing `td` in HTML report (John Hawthorn) * Check for unsafe SQL when two arguments are passed to AR methods (Patrick Brinich-Langlois) # 7.1.0 - 2025-07-18 * Add EOL dates for Rails 8.0 and Ruby 3.4 * Support render model shortcut * Use lazy file lists for AppTree * Add Haml 6.x support * Improve ignored warnings layout in HTML report (Sebastien Savater) * Update JUnit report for CircleCI (Philippe Bernery) * Only load escape functionality from cgi library (Earlopain) * Add `--ensure-no-obsolete-ignore-entries` option (viralpraxis) # 7.0.2 - 2025-04-04 * Fix error with empty `BUNDLE_GEMFILE` env variable # 7.0.1 - 2025-04-03 * Avoid warning on evaluation of plain strings * Enable use of custom/alternative Gemfiles * Fix error on directory with `rb` extension (viralpraxis) * Support `terminal-table` 4.0 (Chedli Bourguiba) * Better support Prism 1.4.0 * Only output timing for each file when using `--debug` # 7.0.0 - 2024-12-30 * Always warn about deserializing from Marshal * Output `originalBaseUriIds` for SARIF format report * Default to using Prism parser if available (disable with `--no-prism`) * Update `terminal-table` version to use latest * Update `eval` check to be a little noisier * Fix array/hash unknown index handling * Disable following symbolic links by default, re-enable with --follow-symlinks * Add step (and timing) for finding files * Add CSV library as explicit dependency for Ruby 3.4 support * Major changes to how rescanning works * Raise minimum Ruby version to 3.1 * Fix hardcoded globally excluded paths * Remove updated entry in Brakeman ignore files (Toby Hsieh) * Fix recursion when handling multiple assignment expressions # 6.2.2 - 2024-10-15 * Ignore more native gems when building gem * Revamp command injection in `pipeline*` calls * New end-of-support dates for Rails # 6.2.1 - 2024-08-22 Just a packaging fix for brakeman.gem # 6.2.0 - 2024-08-22 * Add `--show-ignored` option (Gabriel Zayas) * Add optional support for Prism parser * Warn about unscoped finds with `find_by!` * Treat `::X` and `X` the same, for now (Jill Klang) * Fix compatibility with default frozen string literals (Jean Boussier) * Remediation advice for command injection (Nicholas Barone) * Fix Ruby warnings in test suite (Jean Boussier) * Support YAML aliases in secret configs (Chedli Bourguiba) * Add initial Rails 8 support (Ron Shinall) * Handle mass assignment with splats * Add support for symbolic links (Lu Zhu) # 6.1.2 - 2024-02-01 * Update Highline to 3.0 * Add EOL date for Ruby 3.3.0 * Avoid copying Sexps that are too large * Avoid detecting `ViewComponentContrib::Base` as dynamic render paths (vividmuimui) * Remove deprecated use of `Kernel#open("|...")` * Remove `safe_yaml` gem dependency * Avoid detecting Phlex components as dynamic render paths (Máximo Mussini) # 6.1.1 - 2023-12-24 * Handle racc as a default gem in Ruby 3.3.0 # 6.1.0 - 2023-12-04 * Add `--timing` to add timing duration for scan steps * Fix keyword splats in filter arguments * Add check for unfiltered search with Ransack * Fix class method lookup in parent classes * Handle `class << self` * Add `PG::Connection.escape_string` as a SQL sanitization method (Joévin Soulenq) # 6.0.1 - 2023-07-20 * Accept strings for `load_defaults` version # 6.0.0 - 2023-05-24 * Add obsolete fingerprints to comparison report * Warn about missing CSRF protection when defaults are not loaded (Chris Kruger) * Scan directories that include the word `public` * Raise minimum Ruby version to 3.0 * Drop support for Ruby 1.8/1.9 syntax * Fix end-of-life dates for Ruby * Fix false positive with `content_tag` in newer Rails # 5.4.1 - 2023-02-21 * Fix file/line location for EOL software warnings * Revise checking for request.env to only consider request headers * Add `redirect_back` and `redirect_back_or_to` to open redirect check * Support Rails 7 redirect options * Add Rails 6.1 and 7.0 default configuration values * Prevent redirects using `url_from` being marked as unsafe (Lachlan Sylvester) * Warn about unscoped find for `find_by(id: ...)` * Support `presence`, `presence_in` and `in?` * Fix issue with `if` expressions in `when` clauses # 5.4.0 - 2022-11-17 * Use relative paths for CodeClimate report format (Mike Poage) * Add check for weak RSA key sizes and padding modes * Handle multiple values and splats in case/when * Ignore more model methods in redirects * Add check for absolute paths issue with Pathname * Fix `load_rails_defaults` overwriting settings in the Rails application (James Gregory-Monk) # 5.3.1 - 2022-08-09 * Fix version range for CVE-2022-32209 # 5.3.0 - 2022-08-09 * Include explicit engine or lib paths in vendor/ (Joe Rafaniello) * Load rexml as a Brakeman dependency * Fix "full call" information propagating unnecessarily * Add check for CVE-2022-32209 * Add CWE information to warnings (Stephen Aghaulor) # 5.2.3 - 2022-05-01 * Fix error with hash shorthand syntax * Match order of interactive options with help message (Rory O'Kane) # 5.2.2 - 2022-04-06 * Update `ruby_parser` for Ruby 3.1 support (Merek Skubela) * Handle `nil` when joining values (Dan Buettner) * Update message for unsafe reflection (Pedro Baracho) * Add additional String methods for SQL injection check * Respect equality in `if` conditions # 5.2.1 - 2022-01-30 * Add warning codes for EOL software warnings # 5.2.0 - 2021-12-15 * Initial Rails 7 support * Require Ruby 2.5.0+ * Fix issue with calls to `foo.root` in routes * Ignore `I18n.locale` in SQL queries * Do not treat `sanitize_sql_like` as safe * Add new checks for unsupported Ruby and Rails versions # 5.1.2 - 2021-10-28 * Handle cases where enums are not symbols * Support newer Haml with ::Haml::AttributeBuilder.build * Fix issue where the previous output is still visible (Jason Frey) * Fix warning sorting with nil line numbers * Update for latest RubyParser (Ryan Davis) # 5.1.1 - 2021-07-19 * Unrefactor IgnoreConfig's use of `Brakeman::FilePath` # 5.1.0 - 2021-07-19 * Initial support for ActiveRecord enums * Support `Hash#include?` * Interprocedural dataflow from very simple class methods * Fix SARIF report when checks have no description (Eli Block) * Add ignored warnings to SARIF report (Eli Block) * Add `--sql-safe-methods` option (Esty Scheiner) * Update SQL injection check for Rails 6.0/6.1 * Fix false positive in command injection with `Open3.capture` (Richard Fitzgerald) * Fix infinite loop on mixin self-includes (Andrew Szczepanski) * Ignore dates in SQL * Refactor `cookie?`/`param?` methods (Keenan Brock) * Ignore renderables in dynamic render path check (Brad Parker) * Support `Array#push` * Better `Array#join` support * Adjust copy of `--interactive` menu (Elia Schito) * Support `Array#*` * Better method definition tracking and lookup * Support `Hash#values` and `Hash#values_at` * Check for user-controlled evaluation even if it's a call target * Support `Array#fetch` and `Hash#fetch` * Ignore `sanitize_sql_like` in SQL * Ignore method calls on numbers in SQL * Add GitHub Actions format (Klaus Badelt) * Read and parse files in parallel # 5.0.4 - 2021-06-08 (brakeman gem release only) * Update bundled `ruby_parser` to include argument forwarding support # 5.0.2 - 2021-06-07 * Fix Loofah version check # 5.0.1 - 2021-04-27 * Detect `::Rails.application.configure` too * Set more line numbers on Sexps * Support loading `slim/smart` * Don't fail if $HOME/$USER are not defined * Always ignore slice/only calls for mass assignment * Convert splat array arguments to arguments # 5.0.0 - 2021-01-26 * Ignore `uuid` as a safe attribute * Collapse `__send__` calls * Ignore `Tempfile#path` in shell commands * Ignore development environment * Revamp CSV report to a CSV list of warnings * Set Rails configuration defaults based on `load_defaults` version * Add check for (more) unsafe method reflection * Suggest using `--force` if no Rails application is detected * Add Sonarqube report format (Adam England) * Add check for potential HTTP verb confusion * Add `--[no-]skip-vendor` option * Scan (almost) all Ruby files in project # 4.10.1 - 2020-12-24 * Declare REXML as a dependency (Ruby 3.0 compatibility) * Use `Sexp#sexp_body` instead of `Sexp#[..]` (Ruby 3.0 compatibility) * Prevent render loops when template names are absolute paths * Ensure RubyParser is passed file path as a String * Support new Haml 5.2.0 escaping method # 5.0.0.pre1 - 2020-11-17 * Add check for (more) unsafe method reflection * Suggest using `--force` if no Rails application is detected * Add Sonarqube report format (Adam England) * Add check for potential HTTP verb confusion * Add `--[no-]skip-vendor` option * Scan (almost) all Ruby files in project * Add support for Haml 5.2.0 # 4.10.0 - 2020-09-28 * Add SARIF report format (Steve Winton) # 4.9.1 - 2020-09-04 * Check `chomp`ed strings for SQL injection * Use version from `active_record` for non-Rails apps (Ulysse Buonomo) * Always set line number for joined arrays * Avoid warning about missing `attr_accessible` if `protected_attributes` gem is used # 4.9.0 - 2020-08-04 * Add check for CVE-2020-8166 (Jamie Finnigan) * Avoid warning when `safe_yaml` is used via `YAML.load(..., safe: true)` * Add check for user input in `ERB.new` (Matt Hickman) * Add `--ensure-ignore-notes` (Eli Block) * Remove whitelist/blacklist language, add clarifications * Do not warn about mass assignment with `params.permit!.slice` * Add "full call" information to call index results * Ignore `params.permit!` in path helpers * Treat `Dir.glob` as safe source of values in guards * Always scan `environment.rb` # 4.8.2 - 2020-05-12 * Add check for CVE-2020-8159 * Fix `authenticate_or_request_with_http_basic` check for passed blocks (Hugo Corbucci) * Add `--text-fields` option * Add check for escaping HTML entities in JSON configuration # 4.8.1 - 2020-04-06 * Check SQL query strings using `String#strip` or `String.squish` * Handle non-symbol keys in locals hash for render() * Warn about global(!) mass assignment * Index calls in render arguments # 4.8.0 - 2020-02-18 * Add JUnit-XML report format (Naoki Kimura) * Sort ignore files by fingerprint and line (Ngan Pham) * Freeze call index results * Fix output test when using newer Minitest * Properly render confidence in Markdown report * Report old warnings as fixed if zero warnings reported * Catch dangerous concatenation in `CheckExecute` (Jacob Evelyn) * Show user-friendly message when ignore config file has invalid JSON (D. Hicks) * Initialize Rails version with `nil` (Carsten Wirth) # 4.7.2 - 2019-11-25 * Remove version guard for `named_scope` vs. `scope` * Find SQL injection in `String#strip_heredoc` target * Handle more `permit!` cases * Ensure file name is set when processing model * Add `request.params` as query parameters # 4.7.1 - 2019-10-29 * Check string length against limit before joining * Fix errors from frozen `Symbol#to_s` in Ruby 2.7 * Fix flaky rails4 test (Adam Kiczula) * Added release dates to each version in CHANGES (TheSpartan1980) * Catch reverse tabnabbing with `:_blank` symbol (Jacob Evelyn) * Convert `s(:lambda)` to `s(:call)` in `Sexp#block_call` * Sort text report by file and line (Jacob Evelyn) # 4.7.0 - 2019-10-16 * Refactor `Brakeman::Differ#second_pass` (Benoit Côté-Jodoin) * Ignore interpolation in `%W[]` * Fix `version_between?` (Andrey Glushkov) * Add support for `ruby_parser` 3.14.0 * Ignore `form_for` for XSS check * Update Haml support to Haml 5.x * Catch shell injection from `-c` shell commands (Jacob Evelyn) * Correctly handle non-symbols in `CheckCookieSerialization` (Phil Turnbull) # 4.6.1 - 2019-07-24 * Fix Reverse Tabnabbing warning message (Steffen Schildknecht / Jörg Schiller) # 4.6.0 - 2019-07-23 * Skip calls to `dup` * Add reverse tabnabbing check (Linos Giannopoulos) * Better handling of gems with no version declared * Warn people that Haml 5 is not fully supported (Jared Beck) * Avoid warning about file access with `ActiveStorage::Filename#sanitized` (Tejas Bubane) * Update loofah version for fixing CVE-2018-8048 (Markus Nölle) * Restore `Warning#relative_path` * Add check for cookie serialization with Marshal * Index calls in initializers * Improve template output handling in conditional branches * Avoid assigning `nil` line numbers to `Sexp`s * Add special warning code for custom checks * Add call matching by regular expression # 4.5.1 - 2019-05-11 * Add `Brakeman::FilePath` to represent file paths * Handle trailing comma in block args * Properly handle empty partial name * Use relative paths for `__FILE__` * Convert `!!` calls to boolean value * Add optional check for `config.force_ssl` * Remove code for Ruby versions prior to 1.9 * Check `link_to` with block for href XSS * Add SQL injection checks for `find_or_create_by` and friends * Add deserialization warning for `Oj.load/object_load` * Add initial Rails 6 support * Add SQL injection checks for `destroy_by`/`delete_by` # 4.5.0 - 2019-03-16 * Update `ruby_parser`, use `ruby_parser-legacy` * More thoroughly handle `Shellwords` escaping * Handle non-integer version number comparisons * Use `FileParser` in `Scanner` to parse files * Add original exception to `Tracker#errors` list * Add support for CoffeeScript in Slim templates * Improve support for embedded template "filters" * Remove Sass dependency * Set location information in `CheckContentTag` * Stop swallowing exceptions in `AliasProcessor` * Avoid joining strings with different encodings * Handle `**` inside Hash literals * Better handling of splat/kwsplat arguments * Improve "user input" reported for SQL injection # 4.4.0 - 2019-01-17 * Set default encoding to UTF-8 * Update to Slim 4.0.1 (Jake Peterson) * Update to RubyParser 3.12.0 * Add rendered template information to render paths * Fix trim mode for ERb templates in old Rails versions * Fix thread-safety issue in CallIndex * Add `--enable` option to enable optional checks * Support reading gem versions from gemspecs * Support gem versions which are just major.minor (e.g. 3.0) * Treat `if not` like `unless` * Handle empty `secrets.yml` files (Naoki Kimura) * Correctly set `rel="noreferrer"` in HTML reports * Avoid warning about command injection when `String#shellescape` and `Shellwords.shelljoin` are used (George Ogata) * Add Dockerfile to run Brakeman inside Docker (Ryan Kemper) * Trim some unnecessary files from bundled gems * Add check for CVE-2018-3760 * Avoid nils when concatenating arrays * Ignore Tempfiles in FileAccess warnings (Christina Koller) * Complete overhaul of warning message construction * Deadcode and typo fixes found via Coverity # 4.3.1 - 2018-06-07 * Ignore `Object#freeze`, use the target instead * Ignore `foreign_key` calls in SQL * Handle `included` calls outside of classes/modules * Add `:BRAKEMAN_SAFE_LITERAL` to represent known-safe literals * Handle `Array#map` and `Array#each` over literal arrays * Use safe literal when accessing literal hash with unknown key * Avoid deprecated use of ERB in Ruby 2.6 (Koichi ITO) * Allow `symbolize_keys` to be called on `params` in SQL (Jacob Evelyn) * Improve handling of conditionals in shell commands (Jacob Evelyn) * Fix error when setting line number in implicit renders # 4.3.0 - 2018-05-11 * Check exec-type calls even if they are targets * Convert `Array#join` to string interpolation * `BaseCheck#include_interp?` should return first string interpolation * Add `--parser-timeout` option * Track parent calls in CallIndex * Warn about dangerous `link_to` href with `sanitize()` * Ignore `params#to_h` and `params#to_hash` in SQL checks * Change "".freeze to just "" * Ignore `Process.pid` in system calls * Index Kernel#\` calls even if they are targets * Code Climate: omit leading dot from `only_files` (Todd Mazierski) * `--color` can be used to force color output * Fix reported line numbers for CVE-2018-3741 and CVE-2018-8048 # 4.2.1 - 2018-03-24 * Add warning for CVE-2018-3741 * Add warning for CVE-2018-8048 * Scan `app/jobs/` directory * Handle `template_exists?` in controllers # 4.2.0 - 2018-02-22 * Avoid warning about symbol DoS on `Model#attributes` * Avoid warning about open redirects with model methods ending with `_path` * Avoid warning about command injection with `Shellwords.escape` * Use ivars from `initialize` in libraries * `Sexp#body=` can accept `:rlist` from `Sexp#body_list` * Update RubyParser to 3.11.0 * Fix multiple assignment of globals * Warn about SQL injection in `not` * Exclude template folders in `lib/` (kru0096) * Handle ERb use of `String#<<` method for Ruby 2.5 (Pocke) # 4.1.1 - 2017-12-19 * Remove check for use of `permit` with `*_id` keys * Avoid duplicate warnings about permitted attributes # 4.1.0 - 2017-12-14 * Process models as root sexp instead of each sexp * Avoid CSRF warning in Rails 5.2 default config * Show better location for Sass errors (Andrew Bromwich) * Warn about dynamic values in `Arel.sql` * Fix `include_paths` for Code Climate engine (Will Fleming) * Add check for dangerous keys in `permit` * Try to guess options for `less` pager * Better processing of op_asgn1 (e.g. x[:y] += 1) * Add optional check for divide by zero * Remove errors about divide by zero * Avoid warning about file access for temp files * Do not warn on params.permit with safe values * Add Sexp#call_chain * Use HTTPS for warning links * Handle nested destructuring/multiple assignment * Leave results on screen after paging * Do not page if results fit on screen * Support `app_path` configuration for Code Climate engine (Noah Davis) * Refactor Code Climate engine options parsing (Noah Davis) * Fix upgrade version for CVE-2016-6316 # 4.0.1 - 2017-09-25 * Disable pager when `CI` environment variable is set * Fix output when pager fails # 4.0.0 - 2017-09-25 * Add simple pager for reports output to terminal * Rename "Cross Site Scripting" to "Cross-Site Scripting" (Paul Tetreau) * Rearrange tests a little bit * Treat `request.cookies` like `cookies` * Treat `fail`/`raise` like early returns * Remove reliance on `CONFIDENCE` constant in checks * Remove low confidence mass assignment warnings * Reduce warnings about XSS in `link_to` * "Plain" report output is now the default * --exit-on-error and --exit-on-warn are now the default * Fix --exit-on-error and --exit-on-warn in config files # 3.7.2 - 2017-08-16 * Fix --ensure-latest (David Guyon) # 3.7.1 - 2017-08-16 * Handle simple guard with return at end of branch * Modularize bin/brakeman * Improve multi-value Sexp error message * Add more collection methods for iteration detection * Update ruby2ruby and ruby_parser # 3.7.0 - 2017-06-30 * Improve support for rails4/rails5 options in config file * Track more information about constant assignments * Show progress indicator in interactive mode * Handle simple conditional guards that use `return` * Fix false positive for redirect_to in Rails 4 (Mário Areias) * Avoid interpolating hashes/arrays on failed access # 3.6.2 - 2017-05-19 * Handle safe call operator in checks * Better handling of `if` expressions in HAML rendering * Remove `--rake` option * Properly handle template names without `.html` or `.js` * Set template file names during rendering for better errors * Limit Slim dependency to before 3.0.8 * Catch YAML parsing errors in session settings check * Avoid warning about SQLi with `to_s` in `exists?` * Update RubyParser to 3.9.0 * Do not honor additional check paths in config by default * Handle empty `if` expressions when finding return values * Fix finding return value from empty `if` # 3.6.1 - 2017-03-24 * Fix error when using `--compare` (Sean Gransee) # 3.6.0 - 2017-03-23 * Avoid recursive Concerns * Branch inside of `case` expressions * Print command line option errors without modification * Fix issue with nested interpolation inside SQL strings * Ignore GraphQL tags inside ERB templates * Add `--exit-on-error` (Michael Grosser) * Only report CVE-2015-3227 when exact version is known * Check targetless SQL calls outside of known models # 3.5.0 - 2017-02-01 * Allow `-t None` * Fail on invalid checks specified by `-x` or `-t` * Avoid warning about all, first, or last after Rails 4.0 * Avoid warning about models in SQLi * Lower confidence of SQLi when maybe not on models * Warn about SQLi even potentially on non-models * Report check name in JSON and plain reports * Treat templates without `.html` as HTML anyway * Add `--ensure-latest` option (tamgrosser / Michael Grosser) * Add `--no-summary` to hide summaries in HTML/text reports * Handle `included` block in concerns * Process concerns before controllers # 3.4.1 - 2016-11-02 * Show action help at start of interactive ignore * Check CSRF setting in direct subclasses of `ActionController::Base` (Jason Yeo) * Configurable engines path (Jason Yeo) * Use Ruby version to turn off SymbolDoS check * Pull Ruby version from `.ruby-version` or Gemfile * Avoid warning about `where_values_hash` in SQLi * Fix ignoring link interpolation not at beginning of string # 3.4.0 - 2016-09-08 * Add new `plain` report format * Add option to prune ignore file with `-I` * Improved Slim template support * Show obsolete ignore entries in reports (Jonathan Cheatham) * Support creating reports in non-existent paths * Add `--no-exit-warn` # 3.3.5 - 2016-08-12 * Fix bug in reports when using --debug option # 3.3.4 - 2016-08-12 * Add generic warning for CVE-2016-6316 * Warn about dangerous use of `content_tag` with CVE-2016-6316 * Add warning for CVE-2016-6317 * Use Minitest # 3.3.3 - 2016-07-21 * Show path when no Rails app found (Neil Matatall) * Index calls in view helpers * Process inline template renders * Avoid warning about hashes in link_to hrefs * Add documentation for authentication category * Ignore boolean methods in render paths * Reduce open redirect duplicates * Fix SymbolDoS error with unknown Rails version * Sexp#value returns nil when there is no value * Improve return value estimation # 3.3.2 - 2016-06-10 * Fix serious performance regression with global constant tracking # 3.3.1 - 2016-06-03 * Delay loading vendored gems and modifying load path * Avoid warning about SQL injection with `quoted_primary_key` * Support more safe `&.` operations * Allow multiple line regex in `validates_format_of` (Dmitrij Fedorenko) * Only consider `if` branches in templates * Avoid overwriting instance/class methods with same name (Tim Wade) * Add `--force-scan` option (Neil Matatall) * Improved line number accuracy in ERB templates (Patrick Toomey) # 3.3.0 - 2016-05-05 * Skip processing obviously false if branches (more broadly) * Skip if branches with `Rails.env.test?` * Return exit code `4` if no Rails application is detected * Avoid warning about mass assignment with `params.slice` * Avoid warning about `u` helper (Chad Dollins) * Add optional check for secrets in source code * Process `Array#first` * Allow non-Hash arguments in `protect_from_forgery` (Jason Yeo) * Avoid warning on `popen` with array * Bundle all dependencies in gem * Track constants globally * Handle HAML `find_and_preserve` with a block * [Code Climate engine] When possible, output to /dev/stdout (Gordon Diggs) * [Code Climate engine] Remove nil entries from include_paths (Gordon Diggs) * [Code Climate engine] Report end lines for issues (Gordon Diggs) # 3.2.1 - 2016-02-25 * Remove `multi_json` dependency from `bin/brakeman` # 3.2.0 - 2016-02-25 * Skip Symbol DoS check on Rails 5 * Only update ignore config file on changes * Sort ignore config file * Support calls using `&.` operator * Update ruby_parser dependency to 3.8.1 * Remove `fastercsv` dependency * Fix finding calls with `targets: nil` * Remove `multi_json` dependency * Handle CoffeeScript in HAML * Avoid render warnings about params[:action]/params[:controller] * Index calls in class bodies but outside methods # 3.1.5 - 2016-01-28 * Fix CodeClimate construction of --only-files (Will Fleming) * Add check for denial of service via routes (CVE-2015-7581) * Warn about RCE with `render params` (CVE-2016-0752) * Add check for `strip_tags` XSS (CVE-2015-7579) * Add check for `sanitize` XSS (CVE-2015-7578/80) * Add check for `reject_if` proc bypass (CVE-2015-7577) * Add check for mime-type denial of service (CVE-2016-0751) * Add check for basic auth timing attack (CVE-2015-7576) * Add initial Rails 5 support * Check for implicit integer comparison in dynamic finders * Support directories better in --only-files and --skip-files (Patrick Toomey) * Avoid warning about `permit` in SQL * Handle guards using `detect` * Avoid warning on user input in comparisons * Handle module names with self methods * Add session manipulation documentation # 3.1.4 - 2015-12-22 * Emit brakeman's native fingerprints for Code Climate engine (Noah Davis) * Ignore secrets.yml if in .gitignore * Clean up Ruby warnings (Andy Waite) * Increase test coverage for option parsing (Zander Mackie) * Work around safe_yaml error # 3.1.3 - 2015-12-03 * Check for session secret in secrets.yml * Respect `exit_on_warn` in config file * Avoid warning on `without_protection: true` with hash literals * Make sure before_filter call with block is still a call * CallIndex improvements * Restore minimum Highline version (Kevin Glowacz) * Add Code Climate output format (Ashley Baldwin-Hunter/Devon Blandin/John Pignata/Michael Bernstein) * Iteratively replace values * Output nil instead of false for user_input in JSON * Depend on safe_yaml 1.0 or later * Test coverage improvements for Brakema module (Bethany Rentz) # 3.1.2 - 2015-10-28 * Treat `current_user` like a model * Set user input value for inline renders * Avoid warning on inline renders with safe content types * Handle empty interpolation in HAML filters * Ignore filters that are not method names * Avoid warning about model find/find_by* in hrefs * Use SafeYAML to load configuration files * Warn on SQL query keys, not values in hashes * Allow inspection of recursive Sexps * Add line numbers to class-level warnings * Handle `private def ...` * Catch divide-by-zero in alias processing * Reduce string allocations in Warning#initialize * Sortable tables in HTML report (David Lanner) * Search for config file relative to application root # 3.1.1 - 2015-09-23 * Add optional check for use of MD5 and SHA1 * Avoid warning when linking to decorated models * Add check for user input in session keys * Fix chained assignment * Treat a.try(&:b) like a.b() * Consider j/escape_javascript safe inside HAML JavaScript blocks * Better HAML processing of find_and_preserve calls * Add more Arel methods to be ignored in SQL * Fix absolute paths for Windows (Cody Frederick) * Support newer terminal-table releases * Allow searching call index methods by regex (Alex Ianus) # 3.1.0 - 2015-08-31 * Add support for gems.rb/gems.locked * Update render path information in JSON reports * Remove renaming of several Sexp nodes * Convert YAML config keys to symbols (Karl Glaser) * Use railties version if rails gem is missing (Lucas Mazza) * Warn about unverified SSL mode in Net::HTTP.start * Add Model, Controller, Template, Config classes internally * Report file being parsed in debug output * Update dependencies to Ruby 1.8 incompatible versions * Treat Array.new and Hash.new as arrays/hashes * Fix handling of string concatenation with existing string * Treat html_safe like raw() * Fix low confidence XSS warning code * Avoid warning on path creation methods in link_to * Expand safe methods to match methods with targets * Avoid duplicate eval() warnings # 3.0.5 - 2015-06-20 * Fix check for CVE-2015-3227 # 3.0.4 - 2015-06-18 * Add check for CVE-2015-3226 (XSS via JSON keys) * Add check for CVE-2015-3227 (XML DoS) * Treat `<%==` as unescaped output * Update `ruby_parser` dependency to 3.7.0 # 3.0.3 - 2015-04-20 * Ignore more Arel methods in SQL * Warn about protect_from_forgery without exceptions (Neil Matatall) * Handle lambdas as filters * Ignore quoted_table_name in SQL (Gabriel Sobrinho) * Warn about RCE and file access with `open` * Handle array include? guard conditionals * Do not ignore targets of `to_s` in SQL * Add Rake task to exit with error code on warnings (masarakki) # 3.0.2 - 2015-03-09 * Alias process methods called in class scope on models * Treat primary_key, table_name_prefix, table_name_suffix as safe in SQL * Fix using --compare and --add-checks-path together * Avoid warning about mass assignment with string literals * Only report original regex DoS locations * Improve render path information implementation * Report correct file for simple_format usage CVE warning * Remove URI.escape from HTML reports with GitHub repos * Update ruby_parser to ~> 3.6.2 * Remove formatting newlines in HAML template output * Ignore case value in XSS checks * Fix CSV output when there are no warnings * Handle processing of explicitly shadowed block arguments # 3.0.1 - 2015-01-23 * Avoid protect_from_forgery warning unless ApplicationController inherits from ActionController::Base * Properly format command interpolation (again) * Remove Slim dependency (Casey West) * Allow for controllers/models/templates in directories under `app/` (Neal Harris) * Add `--add-libs-path` for additional libraries (Patrick Toomey) * Properly process libraries (Patrick Toomey) # 3.0.0 - 2015-01-03 * Add check for CVE-2014-7829 * Add check for cross-site scripting via inline renders * Fix formatting of command interpolation * Local variables are no longer formatted as `(local var)` * Actually skip skipped before filters * `--exit-on-warn --compare` only returns error code on new warnings (Jeff Yip) * Fix parsing of `<%==` in ERB * Sort warnings by fingerprint in JSON report (Jeff Yip) * Handle symmetric multiple assignment * Do not branch for self attribute assignment `x = x.y` * Fix CVE for CVE-2011-2932 * Remove "fake filters" from warning fingerpints * Index calls in `lib/` files * Move Symbol DoS to optional checks * CVEs report correct line and file name (Gemfile/Gemfile.lock) (Rob Fletcher) * Change `--separate-models` to be the default # 2.6.3 - 2014-10-14 * Whitelist `exists` arel method from SQL injection check * Avoid warning about Symbol DoS on safe parameters as method targets * Fix stack overflow in ProcessHelper#class_name * Add optional check for unscoped find queries (Ben Toews) * Add framework for optional checks * Fix stack overflow for cycles in class ancestors (Jeff Rafter) # 2.6.2 - 2014-08-18 * Add check for CVE-2014-3415 * Avoid warning about symbolizing safe parameters * Update ruby2ruby dependency to 2.1.1 * Expand app path in one place instead of all over (Jeff Rafter) * Add `--add-checks-path` option for external checks (Clint Gibler) * Fix SQL injection detection in deep nested string building * Add `-4` option to force Rails 4 mode * Check entire call for `send` * Check for .gitignore of secrets in subdirectories * Fix block statement endings in Erubis * Fix undefined variable in controller processing error (Jason Barnabe) # 2.6.1 - 2014-07-02 * Add check for CVE-2014-3482 and CVE-2014-3483 * Add support for keyword arguments in blocks * Remove unused warning codes (Bill Fischer) # 2.6.0 - 2014-06-06 * Fix detection of `:host` setting in redirects with chained calls * Add check for CVE-2014-0130 * Add `find_by`/`find_by!` to SQLi check for Rails 4 * Parse most files upfront instead of on demand * Do not branch values for `+=` * Update to use RubyParser 3.5.0 (Patrick Toomey) * Improve default route detection in Rails 3/4 (Jeff Jarmoc) * Handle controllers and models split across files (Patrick Toomey) * Fix handling of `protected_attributes` gem in Rails 4 (Geoffrey Hichborn) * Ignore more model methods in redirects * Fix CheckRender with nested render calls # 2.5.0 - 2014-04-30 * Add support for RailsLTS 2.3.18.7 and 2.3.18.8 * Add support for Rails 4 `before_actions` and friends * Move SQLi CVE checks to `CheckSQLCVEs` * Check for protected_attributes gem * Fix SQLi detection in chain calls in scopes * Add GitHub-flavored Markdown output format (Greg Ose) * Fix false positives when sanitize() is used in SQL (Jeff Yip) * Add String#intern and Hash#symbolize_keys DoS check (Jan Rusnacko) * Check all arguments in Model.select for SQLi * Fix false positive when :host is specified in redirect * Handle more non-literals in routes * Add check for regex denial of service (Ben Toews) # 2.4.3 - 2014-03-23 No changes. 2.4.2 gem release was unsigned, 2.4.3 is signed. # 2.4.2 - 2014-03-21 * Remove `rescue Exception` * Fix duplicate warnings about sanitize CVE * Reuse duplicate call location information * Only track original template output locations * Skip identically rendered templates * Fix HAML template processing # 2.4.1 - 2014-02-19 * Add check for CVE-2014-0082 * Add check for CVE-2014-0081, replaces CVE-2013-6415 * Add check for CVE-2014-0080 # 2.4.0 - 2014-02-05 * Detect Rails LTS versions * Reduce false positives for SQL injection in string building * More accurate user input marking for SQL injection warnings * Detect SQL injection in `delete_all`/`destroy_all` * Detect SQL injection raw SQL queries using `connection` * Parse exact versions from Gemfile.lock for all gems * Ignore generators * Update to RubyParser 3.4.0 * Fix false positives when SQL methods are not called on AR models (Aaron Bedra) * Add check for uses of OpenSSL::SSL::VERIFY_NONE (Aaron Bedra) * No longer raise exceptions if a class name cannot be determined * Fingerprint attribute warnings individually (Case Taintor) # 2.3.1 - 2013-12-13 * Fix check for CVE-2013-4491 (i18n XSS) to detect workaround * Fix link for CVE-2013-6415 (number_to_currency) # 2.3.0 - 2013-12-12 * Add check for Parameters#permit! * Add check for CVE-2013-4491 (i18n XSS) * Add check for CVE-2013-6414 (header DoS) * Add check for CVE-2013-6415 (number_to_currency) * Add check for CVE-2013-6416 (simple_format XSS) * Add check for CVE-2013-6417 (query generation) * Fix typos in reflection and translate bug messages * Collapse send/try calls * Fix Slim XSS false positives (Noah Davis) * Whitelist `Model#create` for redirects * Fix scoping issues with instance variables and blocks # 2.2.0 - 2013-10-28 * Reduce command injection false positives * Use Rails version from Gemfile if it is available * Only add routes with actual names * Ignore redirects to models using friendly_id (AJ Ostrow) * Support scanning Rails engines (Geoffrey Hichborn) * Add check for detailed exceptions in production # 2.1.2 - 2013-09-18 * Do not attempt to load custom Haml filters * Do not warn about `to_json` XSS in Rails 4 * Add --table-width option to set width of text reports (ssendev) * Remove fuzzy matching on dangerous attr_accessible values # 2.1.1 - 2013-08-21 * New warning code for dangerous attributes in attr_accessible * Do not warn on attr_accessible using roles * More accurate results for model attribute warnings * Use exit code zero with `-z` if all warnings ignored * Respect ignored warnings in rescans * Ignore dynamic controller names in routes * Fix infinite loop when run as rake task (Matthew Shanley) * Respect ignored warnings in tabs format reports # 2.1.0 - 2013-07-17 * Support non-native line endings in Gemfile.lock (Paul Deardorff) * Support for ignoring warnings * Check for dangerous model attributes defined in attr_accessible (Paul Deardorff) * Update to ruby_parser 3.2.2 * Add brakeman-min gemspec * Load gem dependencies on-demand * Output JSON diff to file if -o option is used * Add check for authenticate_or_request_with_http_basic * Refactor of SQL injection check code (Bart ten Brinke) * Fix detection of duplicate XSS warnings * Refactor reports into separate classes * Allow use of Slim 2.x (Ian Zabel) * Return error exit code when application path is not found * Add `--branch-limit` option, limit to 5 by default * Add more methods to check for command injection * Fix output format detection to be more strict again * Allow empty Brakeman configuration file # 2.0.0 - 2013-05-20 * Add `--only-files` option to specify files/paths to scan (Ian Ehlert) * Add Marshal/CSV deserialization check * Combine deserialization checks into single check * Avoid duplicate "Dangerous Send" and "Unsafe Reflection" warnings * Avoid duplicate results for Symbol DoS check * Medium confidence for mass assignment to attr_protected models * Remove "timestamp" key from JSON reports * Remove deprecated config file locations * Relative paths are used by default in JSON reports * `--absolute-paths` replaces `--relative-paths` * Only treat classes with names containing `Controller` like controllers * Better handling of classes nested inside controllers * Better handling of controller classes nested in classes/modules * Handle `->` lambdas with no arguments * Handle explicit block argument destructuring * Skip Rails config options that are real objects * Detect Rails 3 JSON escape config option * Much better tracking of warning file names * Fix errors when using `--separate-models` (Noah Davis) * Fix fingerprint generation to actually use the file path * Fix text report console output in JRuby * Fix false positives on `Model#id` * Fix false positives on `params.to_json` * Fix model path guesses to use "models/" instead of "controllers/" * Clean up SQL CVE warning messages * Use exceptions instead of abort in brakeman lib * Update to Ruby2Ruby 2.0.5 # 1.9.5 - 2013-04-05 * Add check for unsafe symbol creation * Do not warn on mass assignment with `slice`/`only` * Do not warn on session secret if in `.gitignore` * Fix scoping for blocks and block arguments * Fix error when modifying blocks in templates * Fix session secret check for Rails 4 * Fix crash on `before_filter` outside controller * Fix `Sexp` hash cache invalidation * Respect `quiet` option in configuration file * Convert assignment to simple `if` expressions to `or` * More fixes for assignments inside branches * Pin to ruby2ruby version 2.0.3 # 1.9.4 - 2013-03-19 * Add check for CVE-2013-1854 * Add check for CVE-2013-1855 * Add check for CVE-2013-1856 * Add check for CVE-2013-1857 * Fix `--compare` to work with older versions * Add "no-referrer' to HTML report links * Don't warn when invoking `send` on user input * Slightly faster cloning of Sexps * Detect another way to add `strong_parameters` # 1.9.3 - 2013-03-01 * Add render path to JSON report * Add warning fingerprints * Add check for unsafe reflection (Gabriel Quadros) * Add check for skipping authentication methods with blacklist * Add support for Slim templates * Remove empty tables from reports (Owen Ben Davies) * Handle `prepend/append_before_filter` * Performance improvements when handling branches * Fix processing of `production.rb` * Fix version check for Ruby 2.0 * Expand HAML dependency to include 4.0 * Scroll errors into view when expanding in HTML report # 1.9.2 - 2013-02-14 * Add check for CVE-2013-0269 * Add check for CVE-2013-0276 * Add check for CVE-2013-0277 * Add check for CVE-2013-0333 * Check for more send-like methods * Check for more SQL injection locations * Check for more dangerous YAML methods * Support MultiJSON 1.2 for Rails 3.0 and 3.1 # 1.9.1 - 2013-01-19 * Update to RubyParser 3.1.1 (neersighted) * Remove ActiveSupport dependency (Neil Matatall) * Do not warn on arrays passed to `link_to` (Neil Matatall) * Warn on secret tokens * Warn on more mass assignment methods * Add check for CVE-2012-5664 * Add check for CVE-2013-0155 * Add check for CVE-2013-0156 * Add check for unsafe `YAML.load` # 1.9.0 - 2012-12-25 * Update to RubyParser 3 * Ignore route information by default * Support `strong_parameters` * Support newer `validates :format` call * Add scan time to reports * Add Brakeman version to reports * Fix `CheckExecute` to warn on all string interpolation * Fix false positive on `to_sql` calls * Don't mangle whitespace in JSON code formatting * Add AppTree as facade for filesystem (brynary) * Add link for translate vulnerability warning (grosser) * Rename LICENSE to MIT-LICENSE, remove from README (grosser) * Add Rakefile to run tests (grosser) * Better default config file locations (grosser) * Reduce Sexp creation * Handle empty model files * Remove "find by regex" feature from `CallIndex` # 1.8.3 - 2012-11-13 * Use `multi_json` gem for better harmony * Performance improvement for call indexing * Fix issue with processing HAML files * Handle pre-release versions when processing `Gemfile.lock` * Only check first argument of `redirect_to` * Fix false positives from `Model.arel_table` accesses * Fix false positives on redirects to models decorated with Draper gem * Fix false positive on redirect to model association * Fix false positive on `YAML.load` * Fix false positive XSS on any `to_i` output * Fix error on Rails 2 name routes with no args * Fix error in rescan of mixins with symbols in method name * Do not rescan non-Ruby files in config/ # 1.8.2 - 2012-10-17 * Fixed rescanning problems caused by 1.8.0 changes * Fix scope calls with single argument * Report specific model name in rendered collections * Handle overwritten JSON escape settings * Much improved test coverage * Add CHANGES to gemspec # 1.8.1 - 2012-09-24 * Recover from errors in output formatting * Fix false positive in redirect_to (Neil Matatall) * Fix problems with removal of `Sexp#method_missing` * Fix array indexing in alias processing * Fix old mail_to vulnerability check * Fix rescans when only controller action changes * Allow comparison of versions with unequal lengths * Handle super calls with blocks * Respect `-q` flag for "Rails 3 detected" message # 1.8.0 - 2012-09-05 * Support relative paths in reports (fsword) * Allow Brakeman to be run without tty (fsword) * Fix exit code with `--compare` (fsword) * Fix `--rake` option (Deepak Kumar) * Add high confidence warnings for `to_json` XSS (Neil Matatall) * Fix `redirect_to` false negative * Fix duplicate warnings with `raw` calls * Fix shadowing of rendered partials * Add "render chain" to HTML reports * Add check for XSS in `content_tag` * Add full backtrace for errors in debug mode * Treat model attributes in `or` expressions as immediate values * Switch to method access for Sexp nodes # 1.7.1 - 2012-08-13 * Add check for CVE-2012-3463 * Add check for CVE-2012-3464 * Add check for CVE-2012-3465 * Add charset to HTML report (hooopo) * Report XSS in select() for Rails 2 # 1.7.0 - 2012-07-31 * Add check for CVE-2012-3424 * Link report types to descriptions on website * Report errors raised while running check * Improve processing of Rails 3 routes * Fix "empty char-class" error * Improve file access check * Avoid warning on non-ActiveModel models * Speed improvements by stripping down SexpProcessor * Fix how `params[:x] ||=` is handled * Treat user input in `or` expressions as immediate values * Fix processing of negative array indexes * Add line breaks to truncated table rows # 1.6.2 - 2012-06-13 * Add checks for CVE-2012-2660, CVE-2012-2661, CVE-2012-2694, CVE-2012-2695 (Dave Worth) * Avoid warning when redirecting to a model instance * Add `request.parameters` as a parameters hash * Raise confidence level for model attributes in redirects * Return non-zero exit code when missing dependencies * Fix `before_filter :except` logic * Only accept symbol literals as before_filter names * Cache before_filter lookups * Turn off quiet mode by default for `--compare` # 1.6.1 - 2012-05-23 * Major rewrite of CheckSQL * Fix rescanning of deleted templates * Process actions mixed into controllers * Handle `render :template => ...` * Check for inherited attr_accessible (Neil Matatall) * Fix highlighting of HTML escaped values in HTML report * Report line number of highlighted value, if available # 1.6.0 - 2012-04-20 * Remove the Ruport dependency (Neil Matatall) * Add more informational JSON output (Neil Matatall) * Add comparison to previous JSON report (Neil Matatall) * Add highlighting of dangerous values in HTML/text reports * Model#update_attribute should not raise mass assignment warning (Dave Worth) * Don't check `find_by_*` method for SQL injection * Fix duplicate reporting of mass assignment and SQL injection * Fix rescanning of deleted files * Properly check for rails_xss in Gemfile # 1.5.3 - 2012-04-10 * Add check for user input in Object#send (Neil Matatall) * Handle render :layout in views * Support output to multiple formats (Nick Green) * Prevent infinite loops in mutually recursive templates * Only check eval arguments for user input, not targets * Search subdirectories for models * Set values in request hashes and propagate to views * Add rake task file to gemspec (Anton Ageev) * Filter rescanning of templates (Neil Matatall) * Improve handling of modules and nesting * Test for zero errors in test reports # 1.5.2 - 2012-03-22 * Fix link_to checks for Rails 2.0 and 2.3 * Fix rescanning of lib files (Neil Matatall) * Output stack trace on interrupt when debugging * Ignore user input in if statement conditions * Fix --skip-files option * Only warn on user input in render paths * Fix handling of views when using rails_xss * Revert to ruby_parser 2.3.1 for Ruby 1.8 parsing # 1.5.1- 2012-03-06 * Fix detection of global mass assignment setting * Fix partial rendering in Rails 3 * Show backtrace when interrupt received (Ruby 1.9 only) * More debug output * Remove duplicate method in Brakeman::Rails2XSSErubis * Add tracking of module and class to Brakeman::BaseProcessor * Report module when using Brakeman::FindCall # 1.5.0 - 2012-03-02 * Add version check for SafeBuffer vulnerability * Add check for select vulnerability in Rails 3 * select() is no longer considered safe in Rails 2 * Add check for skipping CSRF protection with a blacklist * Add JSON report format * Model#id should not be considered XSS * Standardize methods to check for SQL injection * Fix Rails 2 route parsing issue with nested routes # 1.4.0 - 2012-02-24 * Add check for user input in link_to href parameter * Match ERB processing to rails_xss plugin when plugin used * Add Brakeman::Report#to_json, Brakeman::Warning#to_json * Warnings below minimum confidence are dropped completely * Brakeman.run always returns a Tracker # 1.3.0 - 2012-02-09 * Add file paths to HTML report * Add caching of filters * Add --skip-files option * Add support for attr_protected * Add detection of request.env as user input * Descriptions of checks in -k output * Improved processing of named scopes * Check for mass assignment in ActiveRecord::Associations::AssociationCollection#build * Better variable substitution * Table output option for rescan reports # 1.2.2 - 2012-01-26 * --no-progress works again * Make CheckLinkTo a separate check * Don't fail on unknown options to resource(s) * Handle empty resource(s) blocks * Add RescanReport#existing_warnings ## 1.2.1 - 2012-01-20 * Remove link_to warning for Rails 3.x or when using rails_xss * Don't warn if first argument to link_to is escaped * Detect usage of attr_accessible with no arguments * Fix error when rendering a partial from a view but not through a controller * Fix some issues with rails_xss, CheckCrossSiteScripting, and CheckTranslateBug * Simplify Brakeman Rake task * Avoid modifying $VERBOSE * Add Brakeman::RescanReport#to_s * Add Brakeman::Warning#to_s ## 1.2.0 - 2012-01-14 * Speed improvements for CheckExecute and CheckRender * Check named_scope() and scope() for SQL injection * Add --rake option to create rake task to run Brakeman * Add experimental support for rescanning a subset of files * Add --summary option to only output summary * Fix a problem with Rails 3 routes ## 1.1.0 - 2011-12-22 * Relax required versions for dependencies * Performance improvements for source processing * Better progress reporting * Handle basic operators like << + - * / * Rescue more errors to prevent complete crashes * Compatibility with newer Haml versions * Fix some warnings ## 1.0.0 - 2011-12-08 * Better handling of assignments inside ifs * Check more expressions for SQL injection * Use latest ruby_parser for better 1.9 syntax support * Better behavior for Brakeman as a library ## 1.0.0rc1 - 2011-12-06 * Brakeman can now be used as a library * Faster call search * Add option to return error code if warnings are found (tw-ngreen) * Allow truncated messages to be expanded in HTML * Fix summary when using warning thresholds * Better support for Rails 3 routes * Reduce SQL injection duplicate warnings * Lower confidence on mass assignment with no user input * Ignore mass assignment using all literal arguments * Keep expanded context in view with HTML output ## 0.9.2 - 2011-11-22 * Fix Rails 3 configuration parsing * Add t() helper to check for translate XSS bug ## 0.9.1 - 2011-11-18 * Add warning for translator helper XSS vulnerability ## 0.9.0 - 2011-11-17 * Process Rails 3 configuration files * Fix CSV output * Check for config.active_record.whitelist_attributes = true * Always produce a warning for without_protection => true ## 0.8.4 - 2011-11-04 * Option for separate attr_accessible warnings * Option to set CSS file for HTML output * Add file names for version-specific warnings * Add line number for default routes in a controller * Fix hash_insert() * Remove use of Queue from threaded checks ## 0.8.3 - 2011-10-25 * Respect -w flag in .tabs format (tw-ngreen) * Escape HTML output of error messages * Add --skip-libs option ## 0.8.2 - 2011-10-01 * Run checks in parallel threads by default * Fix compatibility with ruby_parser 2.3.1 ## 0.8.1 - 2011-09-28 * Add option to assume all controller methods are actions * Recover from errors when parsing routes ## 0.8.0 - 2011-09-15 * Add check for mass assignment using without_protection * Add check for password in http_basic_authenticate_with * Warn on user input in hash argument with mass assignment * auto_link is now considered safe for Rails >= 3.0.6 * Output detected Rails version in report * Keep track of methods called in class definition * Add ruby_parser hack for Ruby 1.9 hash syntax * Add a few Rails 3.1 tests ## 0.7.2 - 2011-08-27 * Fix handling of params and cookies with nested access * Add CVEs for checks added in 0.7.0 ## 0.7.1 - 2011-08-18 * Require BaseProcessor for GemProcessor ## 0.7.0 - 2011-08-17 * Allow local variable as a class name * Add checks for vulnerabilities fixed in Rails 2.3.14 and 3.0.10 * Check for default routes in Rails 3 apps * Look in Gemfile or Gemfile.lock for Rails version ## 0.6.1 - 2011-07-29 * Fix XSS check for cookies as parameters in output * Don't bother calling super in CheckSessionSettings * Add escape_once as a safe method * Accept '\Z' or '\z' in model validations ## 0.6.0 - 2011-07-20 * Tests are in place and fully functional * Hide errors by default in HTML output * Warn if routes.rb cannot be found * Narrow methods assumed to be file access * Increase confidence for methods known to not escape output * Fixes to output processing for Erubis * Fixes for Rails 3 XSS checks * Fixes to line numbers with Erubis * Fixes to escaped output scanning * Update CSRF CVE-2011-0447 message to be less assertive ## 0.5.2 - 2011-06-29 * Output report file name when finished * Add initial tests for Rails 2.x * Fix ERB line numbers when using Ruby 1.9 ## 0.5.1 - 2011-06-17 * Fix issue with 'has_one' => in routes ## 0.5.0 - 2011-06-08 * Add support for routes like get 'x/y', :to => 'ctrlr#whatever' * Allow empty blocks in Rails 3 routes * Check initializer for session settings * Add line numbers to session setting warnings * Add --checks option to list checks ## 0.4.1 - 2011-05-23 * Fix reported line numbers when using new Erubis parser (Mostly affects Rails 3 apps) ## 0.4.0 - 2011-05-19 * Handle Rails XSS protection properly * More detection options for rails_xss * Add --escape-html option ## 0.3.2 - 2011-05-12 * Autodetect Rails 3 applications * Turn on auto-escaping for Rails 3 apps * Check Model.create() for mass assignment ## 0.3.1 - 2011-05-03 * Always output a line number in tabbed output format * Restrict characters in category name in tabbed output format to word characters and spaces, for Hudson/Jenkins plugin ## 0.3.0 - 2011-03-21 * Check for SQL injection in calls using constantize() * Check for SQL injection in calls to count_by_sql() ## 0.2.2 - 2011-02-22 * Fix version_between? when no Rails version is specified ## 0.2.1 - 2011-02-18 * Add code snippet to tab output messages ## 0.2.0 - 2011-02-16 * Add check for mail_to vulnerability - CVE-2011-0446 * Add check for CSRF weakness - CVE-2011-0447 ## 0.1.1 - 2011-01-25 * Be more permissive with ActiveSupport version ## 0.1.0 - 2011-01-18 * Check link_to for XSS (because arguments are not escaped) * Process layouts better (although not perfectly yet) * Load custom Haml filters if they are in lib/ * Tab separated output via .tabs output extension * Switch to normal versioning scheme ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@brakeman.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ ## Copyright Assignment By opening a pull request to https://github.com/presidentbeef/brakeman, you agree to assign all rights to the code to Synopsys, Inc. under the [Brakeman Public Use License](LICENSE.md). ## Submitting a Pull Request Pull requests are welcome! Please follow the typical GitHub flow: * Fork Brakeman * Clone locally `git clone your_new_fork` * Create a new branch `git checkout -b fix_some_broken_stuff` * Add new tests * Make fixes, follow coding conventions of project * Run tests with `ruby test/test.rb` or just `rake` * Push your changes `git push origin fix_some_broken_stuff` * Go to *your* fork, click "Submit pull request" * Provide a description of the bug and fix * Submit! ### Code Conventions These are some code conventions to follow so your code fits into the rest of Brakeman. * Must use typical Ruby 2 space indentation * Must work with Ruby 2.4.0 * Prefer to wrap lines near 80 characters but it's not a hard rule ### Preparing Tests Tests are very important to ensure fixes actually work, and to make it obvious what your changes are supposed to fix. They also protect against breaking features in the future. #### Run Tests To run Brakeman tests: ruby test/test.rb or rake To check test coverage, install `simplecov` before running tests. Then open `coverage/index.html` in a browser. For a correct report, run the tests from the root directory. #### Add a Test Case Brakeman has several Rails applications in the `test/apps` directory. Choose the one that best matches your situation and modify it to reproduce the issue. It is preferable to modify the application in such a way that the fewest existing tests are broken. In particular, the tests for "expected number of reported warnings" will probably change, but no other tests should. Unless the tests or expected behavior are broken. In the `test/tests` directory, each application has its own set of tests. Most of these consist of `assert_warning` or `assert_no_warning`, which test for warnings generated by Brakeman. When adding a test for a false positive, use `assert_no_warning` so the expected behavior is clear. #### Generating Tests Writing the `assert_warning` tests can be tedious, especially in bulk. There is a tool which will convert Brakeman reports to tests in `tests/to_test.rb`. This file takes exactly the same options as Brakeman. This makes it easy to generate a smaller set of tests (as opposed to tests for every Brakeman warning, which probably already have tests). Example: ``` ruby to_test.rb apps/rails2 -t Execute ``` will generate some boilerplate and then a set of methods: ```ruby #... def test_command_injection_1 assert_warning :type => :warning, :warning_type => "Command Injection", :line => 34, :message => /^Possible\ command\ injection/, :confidence => 0, :file => /home_controller\.rb/ end def test_command_injection_2 assert_warning :type => :warning, :warning_type => "Command Injection", :line => 36, :message => /^Possible\ command\ injection/, :confidence => 0, :file => /home_controller\.rb/ end #... ``` The boilerplate is unnecessary unless you are adding a whole new test application. When adding a single test or set of tests, copy the tests from here, change the names to something descriptive, and you are done! Note that when adding an `assert_no_warning` test for false positives, you can still generate the test with the false positive, then change the assertion. ================================================ FILE: COPYING.md ================================================ Code committed on or after June 15, 2018 is licensed under the [Brakeman Public Use License](https://github.com/presidentbeef/brakeman/blob/main/LICENSE.md) and is owned by Synopsys, Inc. Code committed prior to June 15, 2018 is licensed under the MIT license and is owned by the respective copyright holders. The code available on [GitHub](https://github.com/presidentbeef/brakeman/) and as packaged on [RubyGems](https://rubygems.org/gems/brakeman) is distributed under the [Brakeman Public Use License](https://github.com/presidentbeef/brakeman/blob/main/LICENSE.md), sublicensed as necessary under MIT. ================================================ FILE: Dockerfile ================================================ FROM ruby:3.3-alpine LABEL maintainer="Justin Collins " WORKDIR /usr/src/app RUN apk --update add build-base # Copy our Gemfile (and related files) *without* copying our actual source code yet COPY Gemfile* *.gemspec gem_common.rb ./ # Copy lib/brakeman/version.rb so that bundle install works COPY lib/brakeman/version.rb ./lib/brakeman/ # Install the necessary gems RUN bundle install --jobs 4 --without "development test" # Copy in the latest Brakeman source code as the final stage COPY . /usr/src/app # Default to looking for source in /code WORKDIR /code ENTRYPOINT ["/usr/src/app/bin/brakeman"] ================================================ FILE: Dockerfile.codeclimate ================================================ FROM ruby:3.0-alpine LABEL maintainer="Justin Collins" WORKDIR /usr/src/app # Create user named app with uid=9000, give it ownership of /usr/src/app RUN adduser -u 9000 -D app && \ chown -R app:app /usr/src/app USER app # Copy our Gemfile (and related files) *without* copying our actual source code yet COPY Gemfile* *.gemspec gem_common.rb ./ # Copy lib/brakeman/version.rb so that bundle install works COPY lib/brakeman/version.rb ./lib/brakeman/ # Install the necessary gems RUN bundle install --jobs 4 --without "development test" # Copy in the latest Brakeman source code as the final stage COPY . /usr/src/app # Default to looking for source in /code WORKDIR /code CMD ["/usr/src/app/bin/codeclimate-brakeman"] ================================================ FILE: FEATURES ================================================ Can detect: -Possibly unescaped model attributes or parameters in views (Cross-Site Scripting) -Bad string interpolation in calls to Model.find, Model.last, Model.first, etc., as well as chained calls (SQL Injection) -String interpolation in find_by_sql (SQL Injection) -String interpolation or params in calls to system, exec, and syscall and `` (Command Injection) -Unrestricted mass assignments -Global restriction of mass assignment -Missing call to protect_from_forgery in ApplicationController (CSRF protection) -Default routes, per-controller and globally -Redirects based on params (probably too broad currently) -Validation regexes not using \A and \z -Calls to render with dynamic paths General capabilities: -Search for method calls based on target class and/or method name -Determine 'output' of templates using ERB, Erubis, or HAML. Can handle automatic escaping ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gemspec :name => "brakeman" unless ENV['BM_PACKAGE'] group :test do gem 'rake' gem 'minitest', '>= 6.0' end end ================================================ FILE: LICENSE.md ================================================ **LICENSE** # Brakeman Public Use License Synopsys, Inc. is willing to authorize use of the Software pursuant to the terms and conditions of this License by Licensee only upon the condition that Licensee accepts that the Agreement governs Licensee's use of the Software. By accepting this Agreement or installing or using the Software (directly or through the actions of an authorized representative), Licensee confirms its acceptance of the License and the Agreement and its agreement to comply with the License terms. The Brakeman software (the "***Software***") is licensed for use by third parties according to the terms and conditions set forth in this license agreement (the "***Agreement***"). The copyright to the Software and this license agreement is owned by Synopsys, Inc. and its global affiliates ("***Synopsys***"). **Copyright 2019 Synopsys, Inc. All rights not granted in this Agreement are expressly reserved.** Commercial Uses (as defined below) of the Software for commercial purposes require a commercial, non-free license. Otherwise, the Software may be used by the party that has downloaded the Software and accepted the terms of this Agreement without charge. ## 1. Definitions 1.1 "***License***" means this Agreement. 1.2 "***Licensee***" means you, the end user of the Software. 1.3 "***Contributor***" means each individual or legal entity that creates, contributes to the creation of, or owns the Software. 1.4 "***Contribution***" means the creation of and/or contribution to the development of the Software 1.5 "***Software***" has the meaning set forth in the recital to this Agreement. ## 2. Commercial Uses A "***Commercial Use***" of the Software is one intended for commercial advantage or monetary compensation. Examples of Commercial Uses include (but are not limited to): * Using the Software to provide commercial managed/Software-as-a-Service services. * Distributing the Software as a commercial product or as part of one. * Using the Software as a component of a value-added service/product. Example of uses that are not Commercial Uses, and are subject to the terms of this License, include (but are not limited to): * Using the Software to analyze Licensee's software. * Any non-commercial use of the Software. To purchase a license to the Software for Commercial Use, or if Licensee is unsure whether it needs to purchase a Commercial Use license, contact Synopsys at \[sig-sales-ww@synopsys.com\]. Synopsys may grant commercial licenses at no monetary cost at its own discretion if the commercial usage is deemed by Synopsys to significantly benefit the development of the Software. ## 3. License Grant Synopsys grants Licensee a nonexclusive, nontransferable (except as permitted in this Agreement), limited license to use and modify the Software, subject to the terms and conditions stated in this Agreement, only for the purpose of non-Commercial Use and not for any other purpose. Licensee may make copies of the Software to the extent reasonably necessary to exercise the License granted in this Agreement. As a condition to the grant of the foregoing License, Licensee agrees not to do or undertake to do the following: * Use the Software for any Commercial Use; * Remove or modify any trademarks or any copyright notice in the Software; or * Assign the License or the Agreement, or distribute, give, or transfer the Software to any third party, except as expressly permitted in this Agreement. All rights not expressly granted in this Agreement are reserved by Synopsys. Synopsys or its licensors retain all ownership and intellectual property rights to the Software. ## 4. Redistribution Redistribution is permitted solely under the following conditions: * A copy of this License, without modification, is provided with the Software. * All Copyright notices to the Software and this Agreement are provided with the Software. * Redistribution and subsequent use does not conflict with the Commercial Uses clause above. ## 4. Copying Copying of the Software is permitted so long as it does not conflict with the Redistribution and Commercial Uses clauses. ## 5. Modification Modification of the Software is permitted so long as it does not conflict with the Redistribution clause. ## 6. Contributions All right, title, and interest in any Contributions to the Software are hereby assigned to Synopsys, effective upon the date of creation of any such Contribution. Synopsys shall have the unlimited, exclusive right to reuse, modify and relicense any Contributions. ## 7. Support The Software is provided under an AS-IS basis and without any support, updates or maintenance. Updates to the Software may be provided by Synopsys at its the sole discretion. ## 8. Disclaimer of Warranty The Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Software is free of defects, merchantable, fit for a particular purpose or non-infringing. ## 9. Disclaimer of Liability To the extent permitted under law, the Software is provided under an AS-IS basis. Synopsys shall never be liable for any damage (including without limitation indirect, incidental, special, punitive or consequential damage, or damage for loss of profits, revenue, data, of data use), cost, expense or any other payment incurred as a result of Licensee's use of the Software for any purpose, even if Synopsys has been advised of the possibility of such damages and regardless of whether the action for such damage arises in contract or tort. The entire liability of Synopsys under the Agreement shall not exceed one hundred dollars (USD $100). ## 10. Trademark "Synopsys" is a registered trademark of Synopsys, Inc. All rights are reserved to Synopsys, Inc. This Agreement does not grant the use of the trademark or the use of the Synopsys logo to you for any purpose. ## 11. Export Controls Export laws and regulations of the United States and any other relevant local export laws and regulations apply to the Software. Licensee agrees that United States export control laws govern the use of the Software (including any corresponding documentation). Licensee also agrees to comply with all United States export laws and regulations (including "deemed export" and "deemed re-export" regulations). Licensee agrees that no results of analysis created or derived from the use of the Software will be exported, directly or indirectly, in violation of these laws, or will be used for any purpose prohibited by these laws including, without limitation, nuclear, chemical, or biological weapons proliferation, or development of missile technologies. Licensee confirms and agrees: * Licensee will not download, provide, make available or otherwise export or re-export the Software, directly or indirectly, to countries prohibited by applicable laws and regulations nor to citizens, nationals or residents of those countries. * Licensee is not listed on the United States Department of Treasury lists of Specially Designated Nationals and Blocked Persons, Specially Designated Terrorists, and Specially Designated Narcotic Traffickers, nor is Licensee listed on the United States Department of Commerce Table of Denial Orders. * Licensee will not export or re-export the Software, directly or indirectly, to persons on the above mentioned lists. * Licensee will not use or allow the Software to be used for, any purposes prohibited by applicable law, including, without limitation, for the development, design, manufacture or production of nuclear, chemical or biological weapons of mass destruction. ## 12. Relationship Between the Parties Licensee is an independent contractor and hereby agrees that no partnership, joint venture, or agency relationship exists between Licensee and Synopsys. ## 13. Entire Agreement; Governing Law The Agreement is the complete agreement for the Software. The Agreement supersedes all prior or contemporaneous agreements or representations, including any license agreements for prior versions of the Software. This Agreement may not be modified and the rights and restrictions may not be altered or waived except in a writing signed by authorized representatives of the parties. If any term of the License or the Agreement is found to be invalid or unenforceable, the remaining provisions will remain effective. The Agreement is governed by California law. The parties agree to submit to the exclusive jurisdiction of, and venue in, the courts of Santa Clara county, California with respect to any action arising out of or relating to the License or the Agreement. ## 14. Notices Any questions concerning the License and/or the Agreement and any notices to Synopsys under this agreement shall be directed to: Synopsys, Inc. 800 E. Middlefield Road Mountain View, CA 94045 End of Agreement ================================================ FILE: MIT-LICENSE ================================================ The MIT License Copyright (c) 2010-2012, YELLOWPAGES.COM, LLC Copyright (c) 2012, Twitter, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: OPTIONS.md ================================================ This file may or may not be up-to-date. For best results but less information, run `brakeman --help`. ## Scanning Options There are some checks which are not run by default. To run all checks, use: brakeman -A Each check will be run in a separate thread by default. To disable this behavior: brakeman -n By default, Brakeman scans the current directory. A path can also be specified as a bare argument, like: brakeman some/path/to/app But to be even more specific, the `-p` or `--path` option may be used: brakeman -p path/to/app To suppress informational warnings and just output the report: brakeman -q Note all Brakeman output except reports are sent to stderr, making it simple to redirect stdout to a file and just get the report. By default, Brakeman will return a non-zero exit code if any security warnings are found or scanning errors are encountered. To disable this: brakeman --no-exit-on-warn --no-exit-on-error To force Brakeman into Rails 3 mode: brakeman -3 Or to force Brakeman into Rails 4 mode: brakeman -4 Beware some behavior and checks rely on knowing the exact version name. This shouldn't be a problem with any modern Rails app using a `Gemfile.lock` though. Brakeman used to parse `routes.rb` and attempt to infer which controller methods are used as actions. However, this is not perfect (especially for Rails 3/4), so now it assumes all controller methods are actions. To disable this behavior: brakeman --no-assume-routes While this shouldn't be necessary, it is possible to force Brakeman to assume output is escaped by default: brakeman --escape-html If Brakeman is running a bit slow, try brakeman --faster This will disable some features, but will probably be much faster (currently it is the same as `--no-branching`). *WARNING*: This may cause Brakeman to miss some vulnerabilities. To disable flow sensitivity in `if` expressions: brakeman --no-branching To instead limit the number of branches tracked for a given value: brakeman --branch-limit LIMIT `LIMIT` should be an integer value. `0` is almost the same as `--no-branching` but `--no-branching` is preferred. The default value is `5`. Lower values generally make Brakeman go faster. `-1` is the same as unlimited. To skip certain files or directories use: brakeman --skip-files file1,/path1/,path2/ Directories are matched relative to the root path of your application and must end in a path separator for your platform (ex. `/`). The above invocation would match and skip the following: * Any file named `file1`. Any file that has `file1` as a path component would still be scanned. * Any file within `/path1`. Because of the leading `/`, only directories from the application's root directory will match. For example, `/lib/path1/some_path1_file.rb` would still be scanned. * Any directory named `path2`. Because there is no leading `/`, any directory with `path2` as a path component will be skipped. For example, `/lib/path2/some_lib_for_testing.rb` would not be scanned. Note Brakeman does "whole program" analysis, therefore skipping a file may affect warning results from more than just that one file. The inverse but even more dangerous option is to choose specific files or directories to scan: brakeman --only-files file1,/path2/,path2/ Again, since Brakeman looks at the whole program, it is very likely not going to behave as expected when scanning a subset of files. Also, if certain files are excluded Brakeman may not function at all. To run a subset of checks: brakeman --test Check1,Check2,etc To exclude certain checks: brakeman --except Check1,Check2,etc Note it is not necessary to include the `Check` part of the check. For example, these are equivalent: brakeman --test CheckSQL brakeman --test SQL ## Output Options To see all kinds of debugging information: brakeman -d To specify an output file for the results: brakeman -o output_file The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `junit`, `markdown`, `csv`, `codeclimate`, `github`, `sarif` and `sonar`. Multiple output files can be specified: brakeman -o output.html -o output.json To output to both a file and to the console, with color: brakeman --color -o /dev/stdout -o output.json To specify a CSS stylesheet to use with the HTML report: brakeman --css-file my_cool_styling By default, Brakeman will only report a single warning of a given type for the same line of code. This can be disabled using brakeman --no-combine-locations To disable highlighting of "dangerous" or "user input" values in warnings: brakeman --no-highlights To report controller and route information: brakeman --routes However, if you really want to know what routes an app has, use rake routes To set the limit on message length in HTML reports, use brakeman --message-limit LIMIT The default LIMIT is 100. To limit width of the tables output in text reports, use brakeman --table-width LIMIT By default, there is no limit. Brakeman will warn about each model without `attr_accessible`. In the HTML report it may be nicer to get all models in one warning with brakeman --no-separate-models Sometimes you don't need a big report, just the summary: brakeman --summary Reports show relative paths by default. To use absolute paths instead: brakeman --absolute-paths This does not affect HTML or tab-separated reports. To output Markdown with nice links to files on GitHub, use brakeman --github-repo USER/REPO[/PATH][@REF] For example, brakeman --github-repo presidentbeef/inject-some-sql To compare results of a scan with a previous scan, use the JSON output option and then: brakeman --compare old_report.json This will output JSON with two lists: one of fixed warnings and one of new warnings. By default, brakeman opens output in `less` pager. To have brakeman output directly to terminal, use brakeman --no-pager ## Ignoring Stuff Brakeman will ignore warnings if configured to do so. By default, it looks for a configuration file in `config/brakeman.ignore`. To specify a file to use: brakeman -i path/to/config.ignore To create and manage this file, use: brakeman -I To ignore possible XSS from model attributes: brakeman --ignore-model-output Brakeman will raise warnings on models that use `attr_protected`. To suppress these warnings: brakeman --ignore-protected To show all ignored warnings without affecting the exit code (i.e. - Will return `0` if the application shows no warnings when simply running `brakeman`): brakeman --show-ignored Brakeman will assume that unknown methods involving untrusted data are dangerous. For example, this would cause a warning (Rails 2): <%= some_method(:option => params[:input]) %> To only raise warnings only when untrusted data is being directly used: brakeman --report-direct This option is not supported very consistently, though. To indicate certain methods return properly escaped output and should not be warned about in XSS checks: brakeman --safe-methods benign_method_escapes_output,totally_safe_from_xss To indicate certain methods return properly escaped output and should not be warned about in SQL checks: brakeman --sql-safe-methods benign_method_escapes_output,totally_safe_from_sql Brakeman warns about use of user input in URLs generated with `link_to`. Since Rails does not provide anyway of making these URLs really safe (e.g. limiting protocols to HTTP(S)), safe methods can be ignored with brakeman --url-safe-methods ensure_safe_protocol_or_something ## Confidence Levels Brakeman assigns a confidence level to each warning. This provides a rough estimate of how certain the tool is that a given warning is actually a problem. Naturally, these ratings should not be taken as absolute truth. There are three levels of confidence: + High - Either this is a simple warning (boolean value) or user input is very likely being used in unsafe ways. + Medium - This generally indicates an unsafe use of a variable, but the variable may or may not be user input. + Weak - Typically means user input was indirectly used in a potentially unsafe manner. To only get warnings above a given confidence level: brakeman -w3 The `-w` switch takes a number from 1 to 3, with 1 being low (all warnings) and 3 being high (only highest confidence warnings). ## Configuration Files Brakeman options can stored and read from YAML files. To simplify the process of writing a configuration file, the `-C` option will output the currently set options. Options passed in on the commandline have priority over configuration files. The default config locations are `./config/brakeman.yml`, `~/.brakeman/config.yml`, and `/etc/brakeman/config.yml` The `-c` option can be used to specify a configuration file to use. ## Miscellaneous To list available checks with short descriptions: brakeman --checks To show checks which are optional (not run by default): brakeman --optional-checks To see Brakeman's version: brakeman --version To see the real list of options: brakeman --help ================================================ FILE: README.md ================================================ [![Brakeman Logo](http://brakemanscanner.org/images/logo_medium.png)](http://brakemanscanner.org/) [![Build Status](https://circleci.com/gh/presidentbeef/brakeman.svg?style=svg)](https://circleci.com/gh/presidentbeef/brakeman) [![Code Coverage](https://qlty.sh/gh/presidentbeef/projects/brakeman/coverage.svg)](https://qlty.sh/gh/presidentbeef/projects/brakeman) # Brakeman Brakeman is a static analysis tool which checks Ruby on Rails applications for security vulnerabilities. # Installation Using RubyGems: gem install brakeman Using Bundler: ```ruby group :development do gem 'brakeman', require: false end ``` Using Docker: docker pull presidentbeef/brakeman Using Docker to build from source: git clone https://github.com/presidentbeef/brakeman.git cd brakeman docker build . -t brakeman # Usage #### Running locally From a Rails application's root directory: brakeman Outside of Rails root: brakeman /path/to/rails/application #### Running with Docker From a Rails application's root directory: docker run -v "$(pwd)":/code presidentbeef/brakeman With a little nicer color: docker run -v "$(pwd)":/code presidentbeef/brakeman --color For an HTML report: docker run -v "$(pwd)":/code presidentbeef/brakeman -o brakeman_results.html Outside of Rails root (note that the output file is relative to path/to/rails/application): docker run -v 'path/to/rails/application':/code presidentbeef/brakeman -o brakeman_results.html # Compatibility Brakeman should work with any version of Rails from 2.3.x to 8.x. Brakeman can analyze code written with Ruby 2.0 syntax and newer, but requires at least Ruby 3.2.0 to run. # Basic Options For a full list of options, use `brakeman --help` or see the [OPTIONS.md](OPTIONS.md) file. To specify an output file for the results: brakeman -o output_file The output format is determined by the file extension or by using the `-f` option. Current options are: `text`, `html`, `tabs`, `json`, `junit`, `markdown`, `csv`, `codeclimate`, `github`, `sarif`, and `sonar`. Multiple output files can be specified: brakeman -o output.html -o output.json To output to both a file and to the console, with color: brakeman --color -o /dev/stdout -o output.json To suppress informational warnings and just output the report: brakeman -q Note all Brakeman output except reports are sent to stderr, making it simple to redirect stdout to a file and just get the report. To see all kinds of debugging information: brakeman -d Specific checks can be skipped, if desired. The name needs to be the correct case. For example, to skip looking for default routes (`DefaultRoutes`): brakeman -x DefaultRoutes Multiple checks should be separated by a comma: brakeman -x DefaultRoutes,Redirect To do the opposite and only run a certain set of tests: brakeman -t SQL,ValidationRegex If Brakeman is running a bit slow, try brakeman --faster This will disable some features, but will probably be much faster (currently it is the same as `--skip-libs --no-branching`). *WARNING*: This may cause Brakeman to miss some vulnerabilities. By default, Brakeman will return a non-zero exit code if any security warnings are found or scanning errors are encountered. To disable this: brakeman --no-exit-on-warn --no-exit-on-error To skip certain files or directories that Brakeman may have trouble parsing, use: brakeman --skip-files file1,/path1/,path2/ To compare results of a scan with a previous scan, use the JSON output option and then: brakeman --compare old_report.json This will output JSON with two lists: one of fixed warnings and one of new warnings. Brakeman will ignore warnings if configured to do so. By default, it looks for a configuration file in `config/brakeman.ignore`. To create and manage this file, use: brakeman -I If you want to temporarily see the warnings you ignored without affecting the exit code, use: brakeman --show-ignored # Warning information See [warning\_types](docs/warning_types) for more information on the warnings reported by this tool. # Warning context The HTML output format provides an excerpt from the original application source where a warning was triggered. Due to the processing done while looking for vulnerabilities, the source may not resemble the reported warning and reported line numbers may be slightly off. However, the context still provides a quick look into the code which raised the warning. # Confidence levels Brakeman assigns a confidence level to each warning. This provides a rough estimate of how certain the tool is that a given warning is actually a problem. Naturally, these ratings should not be taken as absolute truth. There are three levels of confidence: + High - Either this is a simple warning (boolean value) or user input is very likely being used in unsafe ways. + Medium - This generally indicates an unsafe use of a variable, but the variable may or may not be user input. + Weak - Typically means user input was indirectly used in a potentially unsafe manner. To only get warnings above a given confidence level: brakeman -w3 The `-w` switch takes a number from 1 to 3, with 1 being low (all warnings) and 3 being high (only highest confidence warnings). # Configuration files Brakeman options can be stored and read from YAML files. To simplify the process of writing a configuration file, the `-C` option will output the currently set options: ```sh $ brakeman -C --skip-files plugins/ --- :skip_files: - plugins/ ``` Options passed in on the commandline have priority over configuration files. The default config locations are `./config/brakeman.yml`, `~/.brakeman/config.yml`, and `/etc/brakeman/config.yml` The `-c` option can be used to specify a configuration file to use. # Continuous Integration There is a [plugin available](http://brakemanscanner.org/docs/jenkins/) for Jenkins/Hudson. For even more continuous testing, try the [Guard plugin](https://github.com/guard/guard-brakeman). There are a couple [GitHub Actions](https://github.com/marketplace?type=actions&query=brakeman) available. # Building git clone git://github.com/presidentbeef/brakeman.git cd brakeman gem build brakeman.gemspec gem install brakeman*.gem # Who is Using Brakeman? * [Code Climate](https://codeclimate.com/) * [GitHub](https://github.com/) * [Groupon](http://www.groupon.com/) * [New Relic](http://newrelic.com) * [Twitter](https://twitter.com/) [..and more!](http://brakemanscanner.org/brakeman_users) # Homepage/News Website: http://brakemanscanner.org/ Twitter: https://twitter.com/brakeman Chat: https://gitter.im/presidentbeef/brakeman # License Brakeman is free for non-commercial use. See [COPYING](COPYING.md) for details. ================================================ FILE: Rakefile ================================================ require 'bundler/setup' require 'rake/testtask' Rake::TestTask.new do |t| t.pattern = 'test/tests/*.rb' end task default: :test ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions The following versions are supported for security updates. | Version | Supported | | ------- | ------------------ | | >= 4.4.0 | :white_check_mark:| | < 4.4.0 | :x: | ## Reporting a Vulnerability To report a vulnerability, email security@brakeman.org. We will work as quickly as possible to investigate and address the issue, if necessary. We do not have a vulnerability reward program. ================================================ FILE: bin/brakeman ================================================ #!/usr/bin/env ruby #Adjust path in case called directly and not through gem $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/../lib" Encoding.default_external = 'UTF-8' require 'brakeman' require 'brakeman/commandline' Brakeman::Commandline.start ================================================ FILE: bin/codeclimate-brakeman ================================================ #!/usr/bin/env ruby $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/../lib" require "brakeman" require "json" require "brakeman/codeclimate/engine_configuration" engine_options = {} if File.exist?("/config.json") engine_options = JSON.parse(File.read("/config.json")) end Brakeman.run Brakeman::Codeclimate::EngineConfiguration.new(engine_options).options ================================================ FILE: brakeman-lib.gemspec ================================================ require './lib/brakeman/version' require './gem_common' Gem::Specification.new do |s| s.name = %q{brakeman-lib} s.version = Brakeman::Version s.authors = ["Justin Collins"] s.email = "gem@brakeman.org" s.summary = "Security vulnerability scanner for Ruby on Rails." s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis. This package declares gem dependencies instead of bundling them." s.homepage = "http://brakemanscanner.org" s.files = ["bin/brakeman", "CHANGES.md", "FEATURES", "README.md"] + Dir["lib/**/*"] s.executables = ["brakeman"] s.license = "Brakeman Public Use License" s.required_ruby_version = '>= 3.2.0' s.metadata = { "bug_tracker_uri" => "https://github.com/presidentbeef/brakeman/issues", "changelog_uri" => "https://github.com/presidentbeef/brakeman/releases", "documentation_uri" => "https://brakemanscanner.org/docs/", "homepage_uri" => "https://brakemanscanner.org/", "mailing_list_uri" => "https://gitter.im/presidentbeef/brakeman", "source_code_uri" => "https://github.com/presidentbeef/brakeman", "wiki_uri" => "https://github.com/presidentbeef/brakeman/wiki" } Brakeman::GemDependencies.dev_dependencies(s) Brakeman::GemDependencies.base_dependencies(s) Brakeman::GemDependencies.extended_dependencies(s) end ================================================ FILE: brakeman-min.gemspec ================================================ require './lib/brakeman/version' require './gem_common' Gem::Specification.new do |s| s.name = %q{brakeman-min} s.version = Brakeman::Version s.authors = ["Justin Collins"] s.email = "gem@brakeman.org" s.summary = "Security vulnerability scanner for Ruby on Rails." s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis. This version of the gem only requires the minimum number of dependencies. Use the 'brakeman' gem for a full install." s.homepage = "http://brakemanscanner.org" s.files = ["bin/brakeman", "CHANGES.md", "FEATURES", "README.md"] + Dir["lib/**/*"] s.executables = ["brakeman"] s.license = "Brakeman Public Use License" s.required_ruby_version = '>= 3.2.0' s.metadata = { "bug_tracker_uri" => "https://github.com/presidentbeef/brakeman/issues", "changelog_uri" => "https://github.com/presidentbeef/brakeman/releases", "documentation_uri" => "https://brakemanscanner.org/docs/", "homepage_uri" => "https://brakemanscanner.org/", "mailing_list_uri" => "https://gitter.im/presidentbeef/brakeman", "source_code_uri" => "https://github.com/presidentbeef/brakeman", "wiki_uri" => "https://github.com/presidentbeef/brakeman/wiki" } Brakeman::GemDependencies.dev_dependencies(s) Brakeman::GemDependencies.base_dependencies(s) end ================================================ FILE: brakeman-public_cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDijCCAnKgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQ8wDQYDVQQDDAZqdXN0 aW4xHTAbBgoJkiaJk/IsZAEZFg1wcmVzaWRlbnRiZWVmMRMwEQYKCZImiZPyLGQB GRYDY29tMB4XDTE1MDEwMzAxMjI0NFoXDTE2MDEwMzAxMjI0NFowRTEPMA0GA1UE AwwGanVzdGluMR0wGwYKCZImiZPyLGQBGRYNcHJlc2lkZW50YmVlZjETMBEGCgmS JomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMjt xjn8ArkEqQNrRjEeyZAOyr0O8+WZ54AcObsKg2osrcAW6iFd7tjnTFclQHmZgje+ cwxeF/YG4PbA72ElmCvjn8vQJkdgHspKds1otSozvTF2VDnyAEg0nDTMgkQGQy4R HX3NHXMJ8UCAJv2IV/FsItzcPzPmhhf6vu/QaNrmAm3/nF52EsMSEJNC9eTPWudC kPgt19T9LRKMk5YbXDM6jWGRubusE03bTwY3RThqYM5ra1DwI/HpWKsKdmNrBbse f065WyR7RNAxindc2wMyq1EaInmO7Vds+rsOFZ4ZnO90z046ywmTLTadqlfuc9Qo CEw/AhYB6f6DLH8ICkMCAwEAAaOBhDCBgTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE sDAdBgNVHQ4EFgQUmIuIvxLr7ziB52LOpVgd694EfaEwIwYDVR0RBBwwGoEYanVz dGluQHByZXNpZGVudGJlZWYuY29tMCMGA1UdEgQcMBqBGGp1c3RpbkBwcmVzaWRl bnRiZWVmLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAbgSKdn/VSDdl5H2ayE+OM662 gTJWP1CWfbcRVJW/UDjDucEF42t6V/dZTDmwyYTR8Qv+5FsQoPHsDsD3Jr1E62dl VYDeUkbmiV5f8fANbvnGUknzrHwp2T0/URxiIY8oFcaCGT+iua9zlNU20+XhB9JN fsOSUNBuuE/MYGA37MR1sP7lFHr5e7I1Qk1x3HvjNB/kSv1+Cj26Lde1ehvMqpmi bxoxp9KNxkO+709YwLO1rYfmcGghg8WV6MYz3PSHdlgWF4KrjRFc/00hXHqVk0Sf mREEv2LPwHH2SgpSSab+iawnX4l6lV8XcIrmp/HSMySsPVFBeOmB0c05LpEN8w== -----END CERTIFICATE----- ================================================ FILE: brakeman.gemspec ================================================ require './lib/brakeman/version' require './gem_common' Gem::Specification.new do |s| s.name = %q{brakeman} s.version = Brakeman::Version s.authors = ["Justin Collins"] s.email = "gem@brakeman.org" s.summary = "Security vulnerability scanner for Ruby on Rails." s.description = "Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis." s.homepage = "https://brakemanscanner.org" s.files = ["bin/brakeman", "CHANGES.md", "FEATURES", "README.md"] + Dir["lib/**/*"] s.executables = ["brakeman"] s.license = "Brakeman Public Use License" s.required_ruby_version = '>= 3.2.0' s.metadata = { "bug_tracker_uri" => "https://github.com/presidentbeef/brakeman/issues", "changelog_uri" => "https://github.com/presidentbeef/brakeman/releases", "documentation_uri" => "https://brakemanscanner.org/docs/", "homepage_uri" => "https://brakemanscanner.org/", "source_code_uri" => "https://github.com/presidentbeef/brakeman", "wiki_uri" => "https://github.com/presidentbeef/brakeman/wiki" } if File.exist? 'bundle/load.rb' # Pull in vendored dependencies s.files << 'bundle/load.rb' s.files += Dir['bundle/ruby/*/gems/**/*'].reject do |path| # Skip unnecessary files in dependencies path =~ %r{^bundle/ruby/\d\.\d\.\d/gems/[^\/]+/(Rakefile|benchmark|bin|doc|example|man|site|spec|test)} or path =~ %r{/gems/(io-console|prism|racc|strscan)/} end # racc is not only a built-in gem, but also has native code which we cannot # bundle with Brakeman, so leaving it as a regular dependency s.add_dependency "racc" else Brakeman::GemDependencies.dev_dependencies(s) unless ENV['BM_PACKAGE'] Brakeman::GemDependencies.base_dependencies(s) Brakeman::GemDependencies.extended_dependencies(s) end end ================================================ FILE: build.rb ================================================ #!/usr/bin/env ruby require 'fileutils' bundle_exclude = %w[io-console prism racc strscan thor] puts 'Packaging Brakeman gem...' system 'rm -rf bundle Gemfile.lock brakeman-*.gem' and system 'BM_PACKAGE=true bundle install --standalone' abort "No bundle installed" unless Dir.exist? 'bundle' File.delete "bundle/bundler/setup.rb" Dir.delete "bundle/bundler" File.open "bundle/load.rb", "w" do |f| f.puts "path = File.expand_path('../..', __FILE__)" Dir["bundle/ruby/**/lib"].each do |dir| if bundle_exclude.any? { |gem_name| dir.include? gem_name } FileUtils.rm_rf(File.expand_path('..', dir)) else f.puts %Q[$:.unshift "\#{path}/#{dir}"] end end end system "BM_PACKAGE=true gem build brakeman.gemspec" ================================================ FILE: docs/warning_types/CVE-2010-3933/index.markdown ================================================ Rails 2.3.9 and 3.0.0 are vulnerable to an attack on nested attributes wherein a malicious user could alter data in any record in the system. It is recommended to upgrade to at least 2.3.10 or 3.0.1. For more details see [CVE-2011-0446](http://groups.google.com/group/rubyonrails-security/browse_thread/thread/f9f913d328dafe0c). ================================================ FILE: docs/warning_types/CVE-2011-0446/index.markdown ================================================ Certain versions of Rails were vulnerable to a cross-site scripting vulnerability mail\_to. Versions of Rails after 2.3.10 or 3.0.3 are not affected. Updating or removing the mail\_to links is advised. For more details see [CVE-2011-0446](http://groups.google.com/group/rubyonrails-security/browse_thread/thread/f02a48ede8315f81). ================================================ FILE: docs/warning_types/CVE-2011-3186/index.markdown ================================================ Response splitting is a simple attack that can be used as part or a larger exploit chain. A malicious user sends data that causes the HTTP response header to include unintended newline characters which are interpreted as the end of the header. The attacker may then forge their own response body and an entirely false HTTP response, essentially hijacking the entire page load. Versions of Rails 2 previous to 2.3.13 were vulnerable to this type of attack. The Rails 3 branch is not affected. For more details see [CVE-2011-3186](http://groups.google.com/group/rubyonrails-security/browse_thread/thread/6ffc93bde0298768). ================================================ FILE: docs/warning_types/attribute_restriction/index.markdown ================================================ This warning comes up if a model does not limit what attributes can be set through mass assignment. In particular, this check looks for `attr_accessible` inside model definitions. If it is not found, this warning will be issued. Brakeman also warns on use of `attr_protected` - especially since it was found to be [vulnerable to bypass](https://groups.google.com/d/topic/rubyonrails-security/AFBKNY7VSH8/discussion). Warnings for mass assignment on models using `attr_protected` will be reported, but at a lower confidence level. Note that disabling mass assignment globally will suppress these warnings. ================================================ FILE: docs/warning_types/authentication/index.markdown ================================================ "Authentication" is the act of verifying that a user or client is who they say they are. Right now, the only Brakeman warning in the authentication category is regarding hardcoded passwords. Brakeman will warn about constants with literal string values that appear to be passwords. Hardcoded passwords are security issues since they imply a single password and that password is stored in the source code. Typically source code is available to a wide number of people inside an organization, and there have been many instances of source code leaking to the public. Passwords and secrets should be stored in a separate, secure location to limit access. Additionally, it is recommended not to use a single password for accessing sensitive information. Each user should have their own password to make it easier to audit and revoke access. ================================================ FILE: docs/warning_types/authentication_whitelist/index.markdown ================================================ When skipping `before_filter`s with security implications, a "whitelist" approach using `only` should be used instead of `except`. This ensures actions are protected by default, and unprotected only by exception. ================================================ FILE: docs/warning_types/basic_auth/index.markdown ================================================ In Rails 3.1, a new feature was added to simplify basic authentication. The example provided in the official [Rails Guide](http://guides.rubyonrails.org/getting_started.html) looks like this: class PostsController < ApplicationController http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index #... end This warning will be raised if `http_basic_authenticate_with` is used and the password is found to be a string (i.e., stored somewhere in the code). ================================================ FILE: docs/warning_types/command_injection/index.markdown ================================================ Injection is #1 on the 2010 [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2010-A1) web security risks. Command injection occurs when shell commands unsafely include user-manipulatable values. There are many ways to run commands in Ruby: `ls #{params[:file]}` system("ls #{params[:dir]}") exec("md5sum #{params[:input]}") Brakeman will warn on any method like these that uses user input or unsafely interpolates variables. You can use [`shellescape`](https://apidock.com/ruby/Shellwords/shellescape) to render a variable safe: `ls #{params[:file].shellescape}` See [the Ruby Security Guide](http://guides.rubyonrails.org/security.html#command-line-injection) for details. ================================================ FILE: docs/warning_types/content_tag/index.markdown ================================================ Cross-site scripting (or XSS) is #2 on the 2010 [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2010-A2) web security risks and it pops up nearly everywhere. XSS occurs when a user-manipulatable value is displayed on a web page without escaping it, allowing someone to inject Javascript or HTML into the page. [content\_tag](http://apidock.com/rails/ActionView/Helpers/TagHelper/content_tag) is a view helper which generates an HTML tag with some content: >> content_tag :p, "Hi!" => "

Hi!

" In Rails 2, this content is unescaped (although attribute values are escaped): >> content_tag :p, "" => "

" In Rails 3, the content is escaped. However, only the *content* and the tag attribute *values* are escaped. The tag and attribute names are never escaped in Rails 2 or 3. This is more dangerous than a typical method call because `content_tag` marks its output as "HTML safe", meaning the `rails_xss` plugin and Rails 3 auto-escaping will not escape its output. Due to this, `content_tag` should be used carefully if user input is provided as an argument. Note that while `content_tag` does have an `escape` parameter, this only applies to tag attribute *values* and is true by default. ================================================ FILE: docs/warning_types/cross-site_request_forgery/index.markdown ================================================ Cross-site request forgery is #5 on the [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2010-A5). CSRF allows an attacker to perform actions on a website as if they are an authenticated user. This warning is raised when no call to `protect_from_forgery` is found in `ApplicationController`. This method prevents CSRF. For Rails 4 applications, it is recommended that you use `protect_from_forgery :with => :exception`. This code is inserted into newly generated applications. The default is to `nil` out the session object, which has been a source of many CSRF bypasses due to session memoization. See [the Ruby Security Guide](http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) for details. ================================================ FILE: docs/warning_types/cross_site_scripting/index.markdown ================================================ Cross-site scripting (or XSS) is #3 on the 2013 [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2013-A3-Cross-Site_Scripting_(XSS\)) web security risks and it pops up nearly everywhere. XSS occurs when a user-controlled value is displayed on a web page without properly escaping it, allowing someone to inject Javascript or HTML into the page which will be interpreted and executed by the browser.. In Rails 2.x, values need to be explicitly escaped (e.g., by using the `h` method). Since Rails 3.x, auto-escaping in views is enabled by default. However, one can still use the `raw` or `html_safe` methods to output a value directly. See [the Ruby Security Guide](http://guides.rubyonrails.org/security.html#cross-site-scripting-xss) for more details. ### Query Parameters and Cookies ERB example: <%= params[:query].html_safe %> Brakeman looks for several situations that can allow XSS. The simplest is like the example above: a value from the `params` or `cookies` is being directly output to a view. In such cases, it will issue a warning like: Unescaped parameter value near line 3: params[:query] By default, Brakeman will also warn when a parameter or cookie value is used as an argument to a method, the result of which is output unescaped to a view. For example: <%= raw some_method(cookie[:name]) %> This raises a warning like: Unescaped cookie value near line 5: some_method(cookies[:oreo]) However, the confidence level for this warning will be weak, because it is not directly outputting the cookie value. Some methods are known to Brakeman to either be dangerous (`link_to` is one) or safe (`escape_once`). Users can specify safe methods using the `--safe-methods` option. Alternatively, Brakeman can be set to _only_ warn when values are used directly with the `--report-direct` option. ### Model Attributes Because (many) models come from database values, Brakeman mistrusts them by default. For example, if `@user` is an instance of a model set in an action like def set_user @user = User.first end and there is a view with <%= @user.name.html_safe %> Brakeman will raise a warning like Unescaped model attribute near line 3: User.first.name If you trust all your data (although you probably shouldn't), this can be disabled with `--ignore-model-output`. ================================================ FILE: docs/warning_types/cross_site_scripting_to_json/index.markdown ================================================ Cross-site scripting (or XSS) is #2 on the 2010 [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2010-A2) web security risks and it pops up nearly everywhere. XSS occurs when a user-manipulatable value is displayed on a web page without escaping it, allowing someone to inject Javascript or HTML into the page. Calls to `Hash#to_json` can be used to trigger XSS. Brakeman will check to see if there are any calls to `Hash#to_json` with `ActiveSupport#escape_html_entities_in_json` set to false (or if you are running Rails < 2.1.0 which did not have this functionality). `ActiveSupport#escape_html_entities_in_json` was introduced in the "new\_rails\_defaults" initializer in Rails 2.1.0 which is set to `false` by default. In Rails 3.0.0, `true` became the default setting. Setting this value to `true` will automatically escape '<', '>', '&' which are commonly used to break out of code generated by a to\_json call. See [ActiveSupport#escape\_html\_entities\_in\_json](http://rubydoc.info/docs/rails/ActiveSupport/JSON/Encoding.escape_html_entities_in_json=) for more details. ### Exploiting to\_json Consider the following snippet of Rails 2.x ERB: # controller @attrs = {:email => 'some@email.com Which generates the following html: While the generated Javascript appears valid, the browser parses the script tags first, so it sees something like this: The attribute assignment causes a Javascript error, but the alert triggers just fine! With `escape_html_entities_in_json = true`, you will receive the following innocuous output: ================================================ FILE: docs/warning_types/dangerous_eval/index.markdown ================================================ User input in an `eval` statement is VERY dangerous, so this will always raise a warning. Brakeman looks for calls to `eval`, `instance_eval`, `class_eval`, and `module_eval`. ================================================ FILE: docs/warning_types/dangerous_send/index.markdown ================================================ Using unfiltered user data to select a Class or Method to be dynamically sent is dangerous. It is much safer to whitelist the desired target or method. Unsafe use of method: method = params[:method] @result = User.send(method.to_sym) Safe: method = params[:method] == 1 ? :method_a : :method_b @result = User.send(method, *args) Unsafe use of target: table = params[:table] model = table.classify.constantize @result = model.send(:method) Safe: target = params[:target] == 1 ? Account : User @result = target.send(:method, *args) Including user data in the arguments passed to an Object#send is safe, as long as the method can properly handle potentially bad data. Safe: args = params["args"] || [] @result = User.send(:method, *args) ================================================ FILE: docs/warning_types/default_routes/index.markdown ================================================ The general default routes warning means there is a call to #Rails 2.x map.connect ":controller/:action/:id" or Rails 3.x match ':controller(/:action(/:id(.:format)))' in `config/routes.rb`. This allows any public method on any controller to be called as an action. If this warning is reported for a particular controller, it means there is a route to that controller containing `:action`. Default routes can be dangerous if methods are made public which are not intended to be used as URLs or actions. ================================================ FILE: docs/warning_types/denial_of_service/index.markdown ================================================ Denial of Service (DoS) is any attack which causes a service to become unavailable for legitimate clients. For issues that Brakeman detects, this typically arises in the form of memory leaks. ### Symbol DoS Since Symbols are not garbage collected in Ruby versions prior to 2.2.0, creation of large numbers of Symbols could lead to a server running out of memory. Brakeman checks for instances of user input which is converted to a Symbol. When this is not restricted, an attacker could create an unlimited number of Symbols. The best approach is to simply never convert user-controlled input to a Symbol. If this cannot be avoided, use a whitelist of acceptable values. For example: valid_values = ["valid", "values", "here"] if valid_values.include? params[:value] symbolized = params[:value].to_sym end ### Regex DoS Regular expressions can be used for DoS if the pattern and input requires exponential time to process. Brakeman will warn about dynamic regular expressions which may be controlled by an attacker. The attacker can create an "[evil regex](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS)" and then supply input which causes the server to use a large amount of resources. It is recommended to avoid interpolating user input into regular expressions. ================================================ FILE: docs/warning_types/dynamic_render_path/index.markdown ================================================ When a call to `render` uses a dynamically generated path, template name, file name, or action, there is the possibility that a user can access templates that should be restricted. The issue may be worse if those templates execute code or modify the database. This warning is shown whenever the path to be rendered is not a static string or symbol. These warnings are often false positives, however, because it can be difficult to manipulate Rails' assumptions about paths to perform malicious behavior. Reports of dynamic render paths should be checked carefully to see if they can actually be manipulated maliciously by the user. ================================================ FILE: docs/warning_types/file_access/index.markdown ================================================ Using user input when accessing files (local or remote) will raise a warning in Brakeman. For example File.open("/tmp/#{cookie[:file]}") will raise an error like Cookie value used in file name near line 4: File.open("/tmp/#{cookie[:file]}") This type of vulnerability can be used to access arbitrary files on a server (including `/etc/passwd`. If you are using `ActiveStorage`, use [sanitized](https://api.rubyonrails.org/classes/ActiveStorage/Filename.html#method-i-sanitized) URLs: ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg" Note: It replaces `/` with `-`. ================================================ FILE: docs/warning_types/format_validation/index.markdown ================================================ Calls to `validates_format_of ..., :with => //` which do not use `\A` and `\z` as anchors will cause this warning. Using `^` and `$` is not sufficient, as they will only match up to a new line. This allows an attacker to put whatever malicious input they would like before or after a new line character. See [the Ruby Security Guide](http://guides.rubyonrails.org/security.html#regular-expressions) for details. ================================================ FILE: docs/warning_types/information_disclosure/index.markdown ================================================ Also known as [information leakage](https://www.owasp.org/index.php/Information_Leakage) or [information exposure](http://cwe.mitre.org/data/definitions/200.html), this vulnerability refers to system or internal information (such as debugging output, stack traces, error messages, etc.) which is displayed to an end user. For example, Rails provides detailed exception reports by default in the development environment, but it is turned off by default in production: # Full error reports are disabled config.consider_all_requests_local = false Brakeman warns if this setting is `true` in production or there is a `show_detailed_exceptions?` method in a controller which does not return `false`. ================================================ FILE: docs/warning_types/link_to/index.markdown ================================================ In the 2.x versions of Rails, `link_to` would not escape the body of the HREF. For example, this will popup an alert box: link_to "", "http://google.com" Brakeman warns on cases where the first parameter contains user input. ================================================ FILE: docs/warning_types/link_to_href/index.markdown ================================================ Even though Rails will escape the link provided to `link_to`, values starting with `javascript:` or `data:` are unescaped and dangerous. Brakeman will warn on if user values are used to provide the HREF value in `link_to` or if they are interpolated at the beginning of a string. The `--url-safe-methods` option can be used to specify methods which make URLs safe. See [here](https://github.com/presidentbeef/brakeman/pull/45) for more details. ================================================ FILE: docs/warning_types/mass_assignment/index.markdown ================================================ Mass assignment is a feature of Rails which allows an application to create a record from the values of a hash. Example: User.new(params[:user]) Unfortunately, if there is a user field called `admin` which controls administrator access, now any user can make themselves an administrator. `attr_accessible` and `attr_protected` can be used to limit mass assignment. However, Brakeman will warn unless `attr_accessible` is used, or mass assignment is completely disabled. There are two different mass assignment warnings which can arise. The first is when mass assignment actually occurs, such as the example above. This results in a warning like Unprotected mass assignment near line 61: User.new(params[:user]) The other warning is raised whenever a model is found which does not use `attr_accessible`. This produces generic warnings like Mass assignment is not restricted using attr_accessible with a list of affected models. In Rails 3.1 and newer, mass assignment can easily be disabled: config.active_record.whitelist_attributes = true Unfortunately, it can also easily be bypassed: User.new(params[:user], :without_protection => true) Brakeman will warn on uses of `without_protection`. ================================================ FILE: docs/warning_types/redirect/index.markdown ================================================ Unvalidated redirects and forwards are #10 on the [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2010-A10). Redirects which rely on user-supplied values can be used to "spoof" websites or hide malicious links in otherwise harmless-looking URLs. They can also allow access to restricted areas of a site if the destination is not validated. Brakeman will raise warnings whenever `redirect_to` appears to be used with a user-supplied value that may allow them to change the `:host` option. For example, redirect_to params.merge(:action => :home) will create a warning like Possible unprotected redirect near line 46: redirect_to(params) This is because `params` could contain `:host => 'evilsite.com'` which would redirect away from your site and to a malicious site. If the first argument to `redirect_to` is a hash, then adding `:only_path => true` will limit the redirect to the current host. Another option is to specify the host explicitly. redirect_to params.merge(:only_path => true) redirect_to params.merge(:host => 'myhost.com') If the first argument is a string, then it is possible to parse the string and extract the path: redirect_to URI.parse(some_url).path If the URL does not contain a protocol (e.g., `http://`), then you will probably get unexpected results, as `redirect_to` will prepend the current host name and a protocol. ================================================ FILE: docs/warning_types/remote_code_execution/index.markdown ================================================ Brakeman reports on several cases of remote code execution, in which a user is able to control and execute code in ways unintended by application authors. The obvious form of this is the use of `eval` with user input. However, Brakeman also reports on dangerous uses of `send`, `constantize`, and other methods which allow creation of arbitrary objects or calling of arbitrary methods. ================================================ FILE: docs/warning_types/remote_code_execution_yaml_load/index.markdown ================================================ As seen in [CVE-2013-0156](https://groups.google.com/d/topic/rubyonrails-security/61bkgvnSGTQ/discussion), calling `YAML.load` with user input can lead to remote execution of arbitrary code. (To see a real point-and-fire exploit, see the [Metasploit payload](https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/http/rails_xml_yaml_code_exec.rb)). While upgrading Rails, disabling XML parsing, or disabling YAML types in XML request parsing will fix the Rails vulnerability, manually passing user input to `YAML.load` remains unsafe. For example: #Do not do this! YAML.load(params[:file]) ================================================ FILE: docs/warning_types/session_manipulation/index.markdown ================================================ Session manipulation can occur when an application allows user-input in session keys. Since sessions are typically considered a source of truth (e.g. to check the logged-in user or to match CSRF tokens), allowing an attacker to manipulate the session may lead to unintended behavior. For example: user_id = session[params[:name]] current_user = User.find(user_id) In this scenario, the attacker can point the `name` parameter to some other session value (for example, `_csrf_token`) that will be interpreted as a user ID. If the ID matches an existing account, the attacker will now have access to that account. To prevent this type of session manipulation, avoid using user-supplied input as session keys. ================================================ FILE: docs/warning_types/session_setting/index.markdown ================================================ Brakeman warns about several different session-related issues. ### HTTP Only It is recommended that session cookies be set to `http-only`. This helps prevent stealing of cookies via cross-site scripting. ### Secret Length Brakeman will warn if the key length for the session cookies is less than 30 characters. ### Session Secret in Version Control Brakeman will warn if the `config/initializers/secret_token.rb` is included in the version control. It is recommended to exclude `secret_token.rb` from version control and include it in `.gitignore`. ================================================ FILE: docs/warning_types/sql_injection/index.markdown ================================================ Injection is #1 on the 2013 [OWASP Top Ten](https://www.owasp.org/index.php/Top_10_2013-A1-Injection) web security risks. SQL injection is when a user is able to manipulate a value which is used unsafely inside a SQL query. This can lead to data leaks, data loss, elevation of privilege, and other unpleasant outcomes. Brakeman focuses on ActiveRecord methods dealing with building SQL statements. A basic (Rails 2.x) example looks like this: User.first(:conditions => "username = '#{params[:username]}'") Brakeman would produce a warning like this: Possible SQL injection near line 30: User.first(:conditions => ("username = '#{params[:username]}'")) The safe way to do this query is to use a parameterized query: User.first(:conditions => ["username = ?", params[:username]]) Brakeman also understands the new Rails 3.x way of doing things (and local variables and concatenation): username = params[:user][:name].downcase password = params[:user][:password] User.first.where("username = '" + username + "' AND password = '" + password + "'") This results in this kind of warning: Possible SQL injection near line 37: User.first.where((((("username = '" + params[:user][:name].downcase) + "' AND password = '") + params[:user][:password]) + "'")) See [the Ruby Security Guide](http://guides.rubyonrails.org/security.html#sql-injection) for more information and [Rails-SQLi.org](http://rails-sqli.org) for many examples of SQL injection in Rails. ================================================ FILE: docs/warning_types/ssl_verification_bypass/index.markdown ================================================ Simply using SSL isn't enough to ensure the data you are sending is secure. Man in the middle (MITM) attacks are well known and widely used. In some cases, these attacks rely on the client to establish a connection that doesn't check the validity of the SSL certificate presented by the server. In this case, the attacker can present their own certificate and act as a man in the middle. In Ruby, this happens when the OpenSSL verification mode is set to `VERIFY_NONE` require "net/https" require "uri" uri = URI.parse("https://ssl-site.com/") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(uri.request_uri) response = http.request(request) In this case, if an invalid certificate was presented, no verification would occur, providing an opportunity for attack. When successful, the data transmitted (cookies, request parameters, POST bodies, etc.) would all be able to be intercepted by the MITM. Brakeman would produce a warning like this: SSL certificate verification was bypassed near line 24: http.verify_mode = OpenSSL::SSL::VERIFY_NONE To ensure that SSL verification happens use the following mode: http.verify_mode = OpenSSL::SSL::VERIFY_PEER If the server certificate is invalid or context.ca_file is not set when verifying peers an OpenSSL::SSL::SSLError will be raised. For more information on the impact of this issue, see the paper [The Most Dangerous Code in the World](https://www.cs.utexas.edu/~shmat/shmat_ccs12.pdf). ================================================ FILE: docs/warning_types/template_injection/index.markdown ================================================ User input passed into ruby templates that are evaluated is VERY dangerous, so this will always raise a warning. Brakeman looks foir calls of the form: ```ruby ERB.new(user_input).result ``` ================================================ FILE: docs/warning_types/unsafe_deserialization/index.markdown ================================================ Objects in Ruby may be serialized to strings. The main method for doing so is the built-in `Marshal` class. The `YAML`, `JSON`, and `CSV` libraries also have methods for dumping Ruby objects into strings, and then creating objects from those strings. Deserialization of arbitrary objects can lead to [remote code execution](/docs/warning_types/remote_code_execution), as was demonstrated with [CVE-2013-0156](https://groups.google.com/d/msg/rubyonrails-security/61bkgvnSGTQ/nehwjA8tQ8EJ). Brakeman warns when loading user input with `Marshal`, `YAML`, or `CSV`. `JSON` is covered by the checks for [CVE-2013-0333](https://groups.google.com/d/msg/rubyonrails-security/1h2DR63ViGo/GOUVafeaF1IJ) ================================================ FILE: docs/warning_types/unscoped_find/index.markdown ================================================ Unscoped `find` (and related methods) are a form of [Direct Object Reference](https://www.owasp.org/index.php/Top_10_2013-A4-Insecure_Direct_Object_References). Models which belong to another model should typically be accessed via a scoped query. For example, if an `Account` belongs to a `User`, then this may be an unsafe unscoped find: Account.find(params[:id]) Depending on the action, this could allow an attacker to access any account they wish. Instead, it should be scoped to the currently logged-in user: current_user = User.find(session[:user_id]) current_user.accounts.find(params[:id]) ================================================ FILE: gem_common.rb ================================================ module Brakeman module GemDependencies def self.dev_dependencies spec spec.add_development_dependency "minitest", ">= 6.0" spec.add_development_dependency "minitest-ci" spec.add_development_dependency "minitest-mock" spec.add_development_dependency "simplecov" end def self.base_dependencies spec spec.add_dependency "parallel", "~>1.20" spec.add_dependency "ruby_parser", "~>3.22.0" spec.add_dependency "sexp_processor", "~> 4.7" spec.add_dependency "ruby2ruby", "~>2.5.1" spec.add_dependency "racc" end def self.extended_dependencies spec spec.add_dependency "csv" spec.add_dependency "terminal-table", "< 5.0" spec.add_dependency "highline", "~>3.0" spec.add_dependency "erubi", "~>1.13" spec.add_dependency "haml", "< 7.0" spec.add_dependency "slim", ">=1.3.6", "< 5.3" spec.add_dependency "rexml", "~>3.0" spec.add_dependency "reline", "~>0.6" spec.add_dependency "prism", "~>1.0" end end end ================================================ FILE: lib/brakeman/app_tree.rb ================================================ require 'pathname' require 'brakeman/file_path' module Brakeman class AppTree VIEW_EXTENSIONS = %w[html.erb html.haml rhtml js.erb html.slim].join(",") attr_reader :root def self.from_options(options) root = File.expand_path options[:app_path] # Convert files into Regexp for matching init_options = {} if options[:skip_files] init_options[:skip_files] = regex_for_paths(options[:skip_files]) end if options[:only_files] init_options[:only_files] = regex_for_paths(options[:only_files]) end init_options[:additional_libs_path] = options[:additional_libs_path] init_options[:engine_paths] = options[:engine_paths] init_options[:skip_vendor] = options[:skip_vendor] init_options[:follow_symlinks] = options[:follow_symlinks] new(root, init_options) end # Accepts an array of filenames and paths with the following format and # returns a Regexp to match them: # * "path1/file1.rb" - Matches a specific filename in the project directory. # * "path1/" - Matches any path that contains "path1" in the project directory. # * "/path1/ - Matches any path that is rooted at "path1" in the project directory. # # TODO: This is wacky and I don't like it. def self.regex_for_paths(paths) path_regexes = paths.map do |f| # If path ends in a file separator then we assume it is a path rather # than a filename. if f.end_with?(File::SEPARATOR) # If path starts with a file separator then we assume that they # want the project relative path to start with this path prefix. if f.start_with?(File::SEPARATOR) "\\A#{Regexp.escape f}" # If it ends in a file separator, but does not begin with a file # separator then we assume the path can match any path component in # the project. else Regexp.escape f end else "#{Regexp.escape f}\\z" end end Regexp.new("(?:#{path_regexes.join("|")})") end private_class_method(:regex_for_paths) def initialize(root, init_options = {}) @root = root @project_root_path = Pathname.new(@root) @skip_files = init_options[:skip_files] @only_files = init_options[:only_files] @additional_libs_path = init_options[:additional_libs_path] || [] @engine_paths = init_options[:engine_paths] || [] @absolute_engine_paths = @engine_paths.select { |path| path.start_with?(File::SEPARATOR) } @relative_engine_paths = @engine_paths - @absolute_engine_paths @skip_vendor = init_options[:skip_vendor] @follow_symlinks = init_options[:follow_symlinks] @gemspec = nil @root_search_pattern = nil end # Create a new Brakeman::FilePath def file_path(path) Brakeman::FilePath.from_app_tree(self, path) end # Should only be used by Brakeman::FilePath. # Use AppTree#file_path(path).absolute instead. def expand_path(path) File.expand_path(path, @root) end # Should only be used by Brakeman::FilePath # Use AppTree#file_path(path).relative instead. def relative_path(path) pname = Pathname.new path if path and not path.empty? and pname.absolute? pname.relative_path_from(Pathname.new(self.root)).to_s else path end end def exists?(path) if path.is_a? Brakeman::FilePath path.exists? else File.exist?(File.join(@root, path)) end end def ruby_file_paths find_paths(".").uniq end def initializer_paths @initializer_paths ||= prioritize_concerns(find_paths("config/initializers")) end def controller_paths @controller_paths ||= prioritize_concerns(find_paths("app/**/controllers")) end def model_paths @model_paths ||= prioritize_concerns(find_paths("app/**/models")) end def template_paths @template_paths ||= find_paths(".", "*.{#{VIEW_EXTENSIONS}}") + find_paths(".", "*.{erb,haml,slim}").reject { |path| File.basename(path).count(".") > 1 } end def layout_exists?(name) !Dir.glob("#{root_search_pattern}app/views/layouts/#{name}.html.{erb,haml,slim}").empty? end def lib_paths @lib_files ||= find_paths("lib").reject { |path| path.relative.include? "/generators/" or path.relative.include? "lib/tasks/" or path.relative.include? "lib/templates/" } + find_additional_lib_paths + find_helper_paths + find_job_paths end def gemspec return @gemspec unless @gemspec.nil? gemspecs = Dir.glob(File.join(@root, "*.gemspec")) if gemspecs.length > 1 or gemspecs.empty? @gemspec = false else @gemspec = file_path(File.basename(gemspecs.first)) end end # Call this to be able to marshall the AppTree def marshallable @initializer_paths = @initializer_paths.to_a @controller_paths = @controller_paths.to_a @template_paths = @template_paths.to_a @lib_files = @file_paths.to_a self end private def find_helper_paths find_paths "app/helpers" end def find_job_paths find_paths "app/jobs" end def find_additional_lib_paths @additional_libs_path.collect{ |path| find_paths path }.flatten end def find_paths(directory, extensions = ".rb") select_files(glob_files(directory, "*", extensions)) end def glob_files(directory, name, extensions = ".rb") if @follow_symlinks root_directory = "#{root_search_pattern}#{directory}" patterns = ["#{root_directory}/**/#{name}#{extensions}"] Dir.glob("#{root_directory}/**/*", File::FNM_DOTMATCH).each do |path| if File.symlink?(path) && File.directory?(path) symlink_target = File.readlink(path) if Pathname.new(symlink_target).relative? symlink_target = File.join(File.dirname(path), symlink_target) end patterns << "#{search_pattern(symlink_target)}/**/#{name}#{extensions}" end end files = patterns.flat_map { |pattern| Dir.glob(pattern) } files.uniq.lazy else if directory == '.' pattern = File.join(top_directories_pattern, '**', "#{name}#{extensions}") else pattern = "#{root_search_pattern}#{directory}/**/#{name}#{extensions}" end Dir.glob(pattern).lazy end end def select_files(paths) paths = select_only_files(paths) paths = reject_skipped_files(paths) paths = convert_to_file_paths(paths) paths = reject_global_excludes(paths) paths = reject_directories(paths) paths end def reject_directories(paths) paths.reject do |path| Brakeman.logger.spin File.directory?(path) end end def select_only_files(paths) return paths unless @only_files paths.select do |path| Brakeman.logger.spin match_path @only_files, path end end def reject_skipped_files(paths) return paths unless @skip_files paths.reject do |path| Brakeman.logger.spin match_path @skip_files, path end end EXCLUDED_PATHS = regex_for_paths %w[ generators/ lib/tasks/ lib/templates/ db/ spec/ test/ tmp/ ] def reject_global_excludes(paths) paths.reject do |path| relative_path = path.relative if @skip_vendor and relative_path.include? 'vendor/' and !in_engine_paths?(path) and !in_add_libs_paths?(path) true else match_path EXCLUDED_PATHS, path end end end def in_engine_paths?(path) @engine_paths.any? { |p| path.absolute.include?(p) } end def in_add_libs_paths?(path) @additional_libs_path.any? { |p| path.absolute.include?(p) } end def match_path files, path # TODO: Converting to Pathnames and Strings seems like a lot # of converting that could perhaps all be handled in Brakeman::FilePath # instead? absolute_path = Pathname.new(path) # relative root never has a leading separator. But, we use a leading # separator in a @skip_files entry to imply that a directory is # "absolute" with respect to the project directory. # # Also directories need a trailing separator. project_relative_path = if File.directory?(path) File.join( File::SEPARATOR, absolute_path.relative_path_from(@project_root_path).to_s, File::SEPARATOR ) else File.join( File::SEPARATOR, absolute_path.relative_path_from(@project_root_path).to_s ) end files.match(project_relative_path) end def top_directories_pattern top_dirs = convert_to_file_paths(Dir.glob(File.join(root_search_pattern, '*/'))) top_dirs.reject! { |d| File.symlink?(d) or !File.directory?(d) } top_dirs = reject_global_excludes(top_dirs) top_dirs = reject_skipped_files(top_dirs) if top_dirs.empty? # Fall back to searching everything, otherwise the empty pattern # will start searching from the global root root_search_pattern else "{#{top_dirs.join(',')}}" end end def root_search_pattern return @root_search_pattern if @root_search_pattern @root_search_pattern = search_pattern(@root) end def search_pattern(root_dir) abs = @absolute_engine_paths.to_a.map { |path| path.gsub(/#{File::SEPARATOR}+$/, '') } rel = @relative_engine_paths.to_a.map { |path| path.gsub(/#{File::SEPARATOR}+$/, '') } roots = ([root_dir] + abs).join(",") rel_engines = (rel + [""]).join("/,") "{#{roots}}/{#{rel_engines}}" end def prioritize_concerns paths paths.partition { |path| path.relative.include? "concerns" }.flatten end def convert_to_file_paths paths paths.map { |path| file_path(path) } end end end ================================================ FILE: lib/brakeman/call_index.rb ================================================ require 'set' #Stores call sites to look up later. class Brakeman::CallIndex #Initialize index with calls from FindAllCalls def initialize calls @calls_by_method = {} @calls_by_target = {} index_calls calls end #Find calls matching specified option hash. # #Options: # # * :target - symbol, array of symbols, or regular expression to match target(s) # * :method - symbol, array of symbols, or regular expression to match method(s) # * :chained - boolean, whether or not to match against a whole method chain (false by default) # * :nested - boolean, whether or not to match against a method call that is a target itself (false by default) def find_calls options target = options[:target] || options[:targets] method = options[:method] || options[:methods] nested = options[:nested] if options[:chained] return find_chain options #Find by narrowest category elsif target.is_a? Array and method.is_a? Array if target.length > method.length calls = filter_by_target calls_by_methods(method), target else calls = calls_by_targets(target) calls = filter_by_method calls, method end elsif target.is_a? Regexp and method calls = filter_by_target(calls_by_method(method), target) elsif method.is_a? Regexp and target calls = filter_by_method(calls_by_target(target), method) #Find by target, then by methods, if provided elsif target calls = calls_by_target target if calls and method calls = filter_by_method calls, method end #Find calls with no explicit target #with either :target => nil or :target => false elsif (options.key? :target or options.key? :targets) and not target and method calls = calls_by_method method calls = filter_by_target calls, nil #Find calls by method elsif method calls = calls_by_method method else raise "Invalid arguments to CallCache#find_calls: #{options.inspect}" end return [] if calls.nil? #Remove calls that are actually targets of other calls #Unless those are explicitly desired calls = filter_nested calls unless nested calls end def remove_template_indexes template_name = nil [@calls_by_method, @calls_by_target].each do |calls_by| calls_by.each do |_name, calls| calls.delete_if do |call| from_template call, template_name end end end end def remove_indexes_by_class classes [@calls_by_method, @calls_by_target].each do |calls_by| calls_by.each do |_name, calls| calls.delete_if do |call| call[:location][:type] == :class and classes.include? call[:location][:class] end end end end def remove_indexes_by_file file [@calls_by_method, @calls_by_target].each do |calls_by| calls_by.each do |_name, calls| calls.delete_if do |call| call[:location][:file] == file end end end end def index_calls calls calls.each do |call| @calls_by_method[call[:method]] ||= [] @calls_by_method[call[:method]] << call target = call[:target] if not target.is_a? Sexp @calls_by_target[target] ||= [] @calls_by_target[target] << call elsif target.node_type == :params or target.node_type == :session @calls_by_target[target.node_type] ||= [] @calls_by_target[target.node_type] << call end end end private def find_chain options target = options[:target] || options[:targets] method = options[:method] || options[:methods] calls = calls_by_method method return [] if calls.nil? calls = filter_by_chain calls, target end def calls_by_target target case target when Array calls_by_targets target when Regexp calls_by_targets_regex target else @calls_by_target[target] || [] end end def calls_by_targets targets calls = [] targets.each do |target| calls.concat @calls_by_target[target] if @calls_by_target.key? target end calls end def calls_by_targets_regex targets_regex calls = [] @calls_by_target.each do |key, value| case key when String, Symbol calls.concat value if key.match targets_regex end end calls end def calls_by_method method case method when Array calls_by_methods method when Regexp calls_by_methods_regex method else @calls_by_method[method.to_sym] || [] end end def calls_by_methods methods methods = methods.map { |m| m.to_sym } calls = [] methods.each do |method| calls.concat @calls_by_method[method] if @calls_by_method.key? method end calls end def calls_by_methods_regex methods_regex calls = [] @calls_by_method.each do |key, value| calls.concat value if key.match methods_regex end calls end def filter calls, key, value case value when Array values = Set.new value calls.select do |call| values.include? call[key] end when Regexp calls.select do |call| case call[key] when String, Symbol call[key].match value end end else calls.select do |call| call[key] == value end end end def filter_by_method calls, method filter calls, :method, method end def filter_by_target calls, target filter calls, :target, target end def filter_nested calls filter calls, :nested, false end def filter_by_chain calls, target case target when Array targets = Set.new target calls.select do |call| targets.include? call[:chain].first end when Regexp calls.select do |call| case call[:chain].first when String, Symbol call[:chain].first.match target end end else calls.select do |call| call[:chain].first == target end end end def from_template call, template_name return false unless call[:location][:type] == :template return true if template_name.nil? call[:location][:template] == template_name end end ================================================ FILE: lib/brakeman/checks/base_check.rb ================================================ require 'brakeman/processors/output_processor' require 'brakeman/processors/lib/processor_helper' require 'brakeman/warning' require 'brakeman/util' require 'brakeman/messages' #Basis of vulnerability checks. class Brakeman::BaseCheck < Brakeman::SexpProcessor include Brakeman::ProcessorHelper include Brakeman::SafeCallHelper include Brakeman::Util include Brakeman::Messages attr_reader :tracker, :warnings # This is for legacy support. # Use :high, :medium, or :low instead when creating warnings. CONFIDENCE = Brakeman::Warning::CONFIDENCE Match = Struct.new(:type, :match) class << self attr_accessor :name def inherited(subclass) subclass.name = subclass.to_s.match(/^Brakeman::(.*)$/)[1] end end #Initialize Check with Checks. def initialize(tracker) super() @app_tree = tracker.app_tree @results = [] #only to check for duplicates @warnings = [] @tracker = tracker @string_interp = false @current_set = nil @current_template = @current_module = @current_class = @current_method = nil @active_record_models = nil @mass_assign_disabled = nil @has_user_input = nil @in_array = false @safe_input_attributes = Set[:to_i, :to_f, :arel_table, :id, :uuid] @comparison_ops = Set[:==, :!=, :>, :<, :>=, :<=] end #Add result to result list, which is used to check for duplicates def add_result result location = get_location result location, line = get_location result @results << [line, location, result] end #Default Sexp processing. Iterates over each value in the Sexp #and processes them if they are also Sexps. def process_default exp exp.each do |e| process e if sexp? e end exp end #Process calls and check if they include user input def process_call exp unless @comparison_ops.include? exp.method process exp.target if sexp? exp.target process_call_args exp end target = exp.target unless always_safe_method? exp.method if params? target @has_user_input = Match.new(:params, exp) elsif cookies? target @has_user_input = Match.new(:cookies, exp) elsif request_headers? target @has_user_input = Match.new(:request, exp) elsif sexp? target and model_name? target[1] #TODO: Can this be target.target? @has_user_input = Match.new(:model, exp) end end exp end def process_if exp #This is to ignore user input in condition current_user_input = @has_user_input process exp.condition @has_user_input = current_user_input process exp.then_clause if sexp? exp.then_clause process exp.else_clause if sexp? exp.else_clause exp end #Note that params are included in current expression def process_params exp @has_user_input = Match.new(:params, exp) exp end #Note that cookies are included in current expression def process_cookies exp @has_user_input = Match.new(:cookies, exp) exp end def process_array exp @in_array = true process_default exp ensure @in_array = false end #Does not actually process string interpolation, but notes that it occurred. def process_dstr exp unless array_interp? exp or @string_interp # don't overwrite existing value @string_interp = Match.new(:interp, exp) end process_default exp end private # Checking for # # %W[#{a}] # # which will be parsed as # # s(:array, s(:dstr, "", s(:evstr, s(:call, nil, :a)))) def array_interp? exp @in_array and string_interp? exp and exp[1] == "".freeze and exp.length == 3 # only one interpolated value end def always_safe_method? meth @safe_input_attributes.include? meth or @comparison_ops.include? meth end def boolean_method? method method[-1] == "?" end TEMP_FILE_PATH = [ s(:call, s(:call, s(:const, :Tempfile), :new), :path).freeze, s(:call, s(:call, s(:const, :Tempfile), :create), :path).freeze ].freeze def temp_file_path? exp TEMP_FILE_PATH.include? exp end #Report a warning def warn options extra_opts = { :check => self.class.to_s } if options[:file] options[:file] = @app_tree.file_path(options[:file]) end @warnings << Brakeman::Warning.new(options.merge(extra_opts)) end #Run _exp_ through OutputProcessor to get a nice String. def format_output exp Brakeman::OutputProcessor.new.format(exp).gsub(/\r|\n/, "") end #Checks if mass assignment is disabled globally in an initializer. def mass_assign_disabled? return @mass_assign_disabled unless @mass_assign_disabled.nil? @mass_assign_disabled = false if version_between?("3.1.0", "3.9.9") and tracker.config.whitelist_attributes? @mass_assign_disabled = true elsif tracker.options[:rails4] && (!tracker.config.has_gem?(:protected_attributes) || tracker.config.whitelist_attributes?) @mass_assign_disabled = true else #Check for ActiveRecord::Base.send(:attr_accessible, nil) tracker.find_call(target: :"ActiveRecord::Base", method: :attr_accessible).each do |result| call = result[:call] if call? call if call.first_arg == Sexp.new(:nil) @mass_assign_disabled = true break end end end unless @mass_assign_disabled #Check for # class ActiveRecord::Base # attr_accessible nil # end tracker.check_initializers([], :attr_accessible).each do |result| if result.module == "ActiveRecord" and result.result_class == :Base arg = result.call.first_arg if arg.nil? or node_type? arg, :nil @mass_assign_disabled = true break end end end end end #There is a chance someone is using Rails 3.x and the `strong_parameters` #gem and still using hack above, so this is a separate check for #including ActiveModel::ForbiddenAttributesProtection in #ActiveRecord::Base in an initializer. if not @mass_assign_disabled and version_between?("3.1.0", "3.9.9") and tracker.config.has_gem? :strong_parameters matches = tracker.check_initializers([], :include) forbidden_protection = Sexp.new(:colon2, Sexp.new(:const, :ActiveModel), :ForbiddenAttributesProtection) matches.each do |result| if call? result.call and result.call.first_arg == forbidden_protection @mass_assign_disabled = true end end unless @mass_assign_disabled tracker.find_call(target: :"ActiveRecord::Base", method: [:send, :include]).each do |result| call = result[:call] if call? call and (call.first_arg == forbidden_protection or call.second_arg == forbidden_protection) @mass_assign_disabled = true end end end end @mass_assign_disabled end def original? result return false if result[:call].original_line or duplicate? result add_result result true end #This is to avoid reporting duplicates. Checks if the result has been #reported already from the same line number. def duplicate? result, location = nil location, line = get_location result @results.each do |r| if r[0] == line and r[1] == location if tracker.options[:combine_locations] return true elsif r[2] == result return true end end end false end def get_location result if result.is_a? Hash line = result[:call].original_line || result[:call].line elsif sexp? result line = result.original_line || result.line else raise ArgumentError end location ||= (@current_template && @current_template.name) || @current_class || @current_module || @current_set || result[:location][:class] || result[:location][:template] || result[:location][:file].to_s location = location[:name] if location.is_a? Hash location = location.name if location.is_a? Brakeman::Collection location = location.to_sym return location, line end #Checks if _exp_ includes user input in the form of cookies, parameters, #request environment, or model attributes. # #If found, returns a struct containing a type (:cookies, :params, :request, :model) and #the matching expression (Match#type and Match#match). # #Returns false otherwise. def include_user_input? exp @has_user_input = false process exp @has_user_input end #This is used to check for user input being used directly. # ##If found, returns a struct containing a type (:cookies, :params, :request) and #the matching expression (Match#type and Match#match). # #Returns false otherwise. def has_immediate_user_input? exp if exp.nil? false elsif call? exp and not always_safe_method? exp.method if params? exp return Match.new(:params, exp) elsif cookies? exp return Match.new(:cookies, exp) elsif request_headers? exp return Match.new(:request, exp) else has_immediate_user_input? exp.target end elsif sexp? exp case exp.node_type when :dstr exp.each do |e| if sexp? e match = has_immediate_user_input?(e) return match if match end end false when :evstr if sexp? exp.value if exp.value.node_type == :rlist exp.value.each_sexp do |e| match = has_immediate_user_input?(e) return match if match end false else has_immediate_user_input? exp.value end end when :format has_immediate_user_input? exp.value when :if (sexp? exp.then_clause and has_immediate_user_input? exp.then_clause) or (sexp? exp.else_clause and has_immediate_user_input? exp.else_clause) when :or has_immediate_user_input? exp.lhs or has_immediate_user_input? exp.rhs when :splat, :kwsplat exp.each_sexp do |e| match = has_immediate_user_input?(e) return match if match end false when :hash if kwsplat? exp exp[1].each_sexp do |e| match = has_immediate_user_input?(e) return match if match end false end else false end end end #Checks for a model attribute at the top level of the #expression. def has_immediate_model? exp, out = nil out = exp if out.nil? if sexp? exp and exp.node_type == :output exp = exp.value end if call? exp target = exp.target method = exp.method if always_safe_method? method false elsif call? target and not method.to_s[-1,1] == "?" if has_immediate_model?(target, out) exp else false end elsif model_name? target exp else false end elsif sexp? exp case exp.node_type when :dstr exp.each do |e| if sexp? e and match = has_immediate_model?(e, out) return match end end false when :evstr if sexp? exp.value if exp.value.node_type == :rlist exp.value.each_sexp do |e| if match = has_immediate_model?(e, out) return match end end false else has_immediate_model? exp.value, out end end when :format has_immediate_model? exp.value, out when :if ((sexp? exp.then_clause and has_immediate_model? exp.then_clause, out) or (sexp? exp.else_clause and has_immediate_model? exp.else_clause, out)) when :or has_immediate_model? exp.lhs or has_immediate_model? exp.rhs else false end end end #Checks if +exp+ is a model name. # #Prior to using this method, either @tracker must be set to #the current tracker, or else @models should contain an array of the model #names, which is available via tracker.models.keys def model_name? exp @models ||= @tracker.models.keys if exp.is_a? Symbol @models.include? exp elsif call? exp and exp.target.nil? and exp.method == :current_user true elsif sexp? exp @models.include? class_name(exp) else false end end #Returns true if +target+ is in +exp+ def include_target? exp, target return false unless call? exp exp.each do |e| return true if e == target or include_target? e, target end false end def lts_version? version tracker.config.has_gem? :'railslts-version' and version_between? version, "2.3.18.99", tracker.config.gem_version(:'railslts-version') end def version_between? low_version, high_version, current_version = nil tracker.config.version_between? low_version, high_version, current_version end def gemfile_or_environment gem_name = :rails if gem_name and info = tracker.config.get_gem(gem_name.to_sym) info elsif @app_tree.exists?("Gemfile") @app_tree.file_path "Gemfile" elsif @app_tree.exists?("gems.rb") @app_tree.file_path "gems.rb" else @app_tree.file_path "config/environment.rb" end end def self.description @description end def active_record_models return @active_record_models if @active_record_models @active_record_models = {} tracker.models.each do |name, model| if model.ancestor? :"ActiveRecord::Base" @active_record_models[name] = model end end @active_record_models end STRING_METHODS = Set[:<<, :+, :concat, :prepend] private_constant :STRING_METHODS def string_building? exp return false unless call? exp and STRING_METHODS.include? exp.method node_type? exp.target, :str, :dstr or node_type? exp.first_arg, :str, :dstr or string_building? exp.target or string_building? exp.first_arg end I18N_CLASS = s(:const, :I18n) def locale_call? exp return unless call? exp (exp.target == I18N_CLASS and exp.method == :locale) or locale_call? exp.target end end ================================================ FILE: lib/brakeman/checks/check_basic_auth.rb ================================================ require 'brakeman/checks/base_check' #Checks if password is stored in controller #when using http_basic_authenticate_with # #Only for Rails >= 3.1 class Brakeman::CheckBasicAuth < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for the use of http_basic_authenticate_with" def run_check return if version_between? "0.0.0", "3.0.99" check_basic_auth_filter check_basic_auth_request end def check_basic_auth_filter controllers = tracker.controllers.select do |_name, c| c.options[:http_basic_authenticate_with] end Hash[controllers].each do |name, controller| controller.options[:http_basic_authenticate_with].each do |call| if pass = get_password(call) and string? pass warn :controller => name, :warning_type => "Basic Auth", :warning_code => :basic_auth_password, :message => "Basic authentication password stored in source code", :code => call, :confidence => :high, :file => controller.file, :cwe_id => [259] break end end end end # Look for # authenticate_or_request_with_http_basic do |username, password| # username == "foo" && password == "bar" # end def check_basic_auth_request tracker.find_call(:target => nil, :method => :authenticate_or_request_with_http_basic).each do |result| if include_password_literal? result warn :result => result, :code => @include_password, :warning_type => "Basic Auth", :warning_code => :basic_auth_password, :message => "Basic authentication password stored in source code", :confidence => :high, :cwe_id => [259] end end end # Check if the block of a result contains a comparison of password to string def include_password_literal? result return false if result[:block_args].nil? @password_var = result[:block_args].last @include_password = false process result[:block] @include_password end # Looks for :== calls on password var def process_call exp target = exp.target if node_type?(target, :lvar) and target.value == @password_var and exp.method == :== and string? exp.first_arg @include_password = exp end exp end def get_password call arg = call.first_arg return false if arg.nil? or not hash? arg hash_access(arg, :password) end end ================================================ FILE: lib/brakeman/checks/check_basic_auth_timing_attack.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckBasicAuthTimingAttack < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for timing attack in basic auth (CVE-2015-7576)" def run_check @upgrade = case when version_between?("0.0.0", "3.2.22") "3.2.22.1" when version_between?("4.0.0", "4.1.14") "4.1.14.1" when version_between?("4.2.0", "4.2.5") "4.2.5.1" else return end check_basic_auth_call end def check_basic_auth_call tracker.find_call(target: nil, method: :http_basic_authenticate_with).each do |result| warn :result => result, :warning_type => "Timing Attack", :warning_code => :CVE_2015_7576, :message => msg("Basic authentication in ", msg_version(rails_version), " is vulnerable to timing attacks. Upgrade to ", msg_version(@upgrade)), :confidence => :high, :link => "https://groups.google.com/d/msg/rubyonrails-security/ANv0HDHEC3k/mt7wNGxbFQAJ", :cwe_id => [1254] end end end ================================================ FILE: lib/brakeman/checks/check_content_tag.rb ================================================ require 'brakeman/checks/check_cross_site_scripting' #Checks for unescaped values in `content_tag` # # content_tag :tag, body # ^-- Unescaped in Rails 2.x # # content_tag, :tag, body, attribute => value # ^-- Unescaped in all versions # # content_tag, :tag, body, attribute => value # ^ # | # Escaped by default, can be explicitly escaped # or not by passing in (true|false) as fourth argument class Brakeman::CheckContentTag < Brakeman::CheckCrossSiteScripting Brakeman::Checks.add self @description = "Checks for XSS in calls to content_tag" def run_check @ignore_methods = Set[:button_to, :check_box, :escapeHTML, :escape_once, :field_field, :fields_for, :h, :hidden_field, :hidden_field, :hidden_field_tag, :image_tag, :label, :mail_to, :radio_button, :select, :submit_tag, :text_area, :text_field, :text_field_tag, :url_encode, :u, :url_for, :will_paginate].merge tracker.options[:safe_methods] @known_dangerous = [] @content_tags = tracker.find_call :target => false, :method => :content_tag @models = tracker.models.keys @inspect_arguments = tracker.options[:check_arguments] @mark = nil Brakeman.debug "Checking for XSS in content_tag" @content_tags.each do |call| process_result call end check_cve_2016_6316 end def process_result result return if duplicate? result case result[:location][:type] when :template @current_template = result[:location][:template] when :class @current_class = result[:location][:class] @current_method = result[:location][:method] end @current_file = result[:location][:file] call = result[:call] args = call.arglist tag_name = args[1] content = args[2] attributes = args[3] escape_attr = args[4] @matched = false #Silly, but still dangerous if someone uses user input in the tag type check_argument result, tag_name #Versions before 3.x do not escape body of tag, nor does the rails_xss gem unless @matched or (tracker.options[:rails3] and not raw? content) check_argument result, content end # This changed in Rails 6.1.6 if version_between? '0.0.0', '6.1.5' #Attribute keys are never escaped, so check them for user input if not @matched and hash? attributes and not request_value? attributes hash_iterate(attributes) do |k, _v| check_argument result, k return if @matched end end end #By default, content_tag escapes attribute values passed in as a hash. #But this behavior can be disabled. So only check attributes hash #if they are explicitly not escaped. if not @matched and attributes and (false? escape_attr or cve_2016_6316?) if request_value? attributes or not hash? attributes check_argument result, attributes else #check hash values hash_iterate(attributes) do |_k, v| check_argument result, v return if @matched end end end ensure @current_template = @current_class = @current_method = @current_file = nil end def check_argument result, exp #Check contents of raw() calls directly if raw? exp arg = process exp.first_arg else arg = process exp end if input = has_immediate_user_input?(arg) message = msg("Unescaped ", msg_input(input), " in ", msg_code("content_tag")) add_result result warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_content_tag, :message => message, :user_input => input, :confidence => :high, :link_path => "content_tag", :cwe_id => [79] elsif not tracker.options[:ignore_model_output] and match = has_immediate_model?(arg) unless IGNORE_MODEL_METHODS.include? match.method add_result result if likely_model_attribute? match confidence = :high else confidence = :medium end warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_content_tag, :message => msg("Unescaped model attribute in ", msg_code("content_tag")), :user_input => match, :confidence => confidence, :link_path => "content_tag", :cwe_id => [79] end elsif @matched return if @matched.type == :model and tracker.options[:ignore_model_output] message = msg("Unescaped ", msg_input(@matched), " in ", msg_code("content_tag")) add_result result warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_content_tag, :message => message, :user_input => @matched, :confidence => :medium, :link_path => "content_tag", :cwe_id => [79] end end def process_call exp if @mark actually_process_call exp else @mark = true actually_process_call exp @mark = false end exp end def check_cve_2016_6316 if cve_2016_6316? confidence = if @content_tags.any? :high else :medium end fix_version = case when version_between?("3.0.0", "3.2.22.3") "3.2.22.4" when version_between?("4.0.0", "4.2.7.0") "4.2.7.1" when version_between?("5.0.0", "5.0.0") "5.0.0.1" when (version.nil? and tracker.options[:rails3]) "3.2.22.4" when (version.nil? and tracker.options[:rails4]) "4.2.7.2" else return end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2016_6316, :message => msg(msg_version(rails_version), " ", msg_code("content_tag"), " does not escape double quotes in attribute values ", msg_cve("CVE-2016-6316"), ". Upgrade to ", msg_version(fix_version)), :confidence => confidence, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/8B2iV2tPRSE/JkjCJkSoCgAJ", :cwe_id => [79] end end def raw? exp call? exp and exp.method == :raw end def cve_2016_6316? version_between? "3.0.0", "3.2.22.3" or version_between? "4.0.0", "4.2.7.0" or version_between? "5.0.0", "5.0.0.0" end end ================================================ FILE: lib/brakeman/checks/check_cookie_serialization.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckCookieSerialization < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for use of Marshal for cookie serialization" def run_check tracker.find_call(target: :'Rails.application.config.action_dispatch', method: :cookies_serializer=).each do |result| setting = result[:call].first_arg if symbol? setting and [:marshal, :hybrid].include? setting.value warn :result => result, :warning_type => "Remote Code Execution", :warning_code => :unsafe_cookie_serialization, :message => msg("Use of unsafe cookie serialization strategy ", msg_code(setting.value.inspect), " might lead to remote code execution"), :confidence => :medium, :link_path => "unsafe_deserialization", :cwe_id => [565, 502] end end end end ================================================ FILE: lib/brakeman/checks/check_create_with.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckCreateWith < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for strong params bypass in CVE-2014-3514" def run_check @warned = false if version_between? "4.0.0", "4.0.8" suggested_version = "4.0.9" elsif version_between? "4.1.0", "4.1.4" suggested_version = "4.1.5" else return end @message = msg(msg_code("create_with"), " is vulnerable to strong params bypass. Upgrade to ", msg_version(suggested_version), " or patch") tracker.find_call(:method => :create_with, :nested => true).each do |result| process_result result end generic_warning unless @warned end def process_result result return unless original? result arg = result[:call].first_arg confidence = danger_level arg if confidence @warned = true warn :warning_type => "Mass Assignment", :warning_code => :CVE_2014_3514_call, :result => result, :message => @message, :confidence => confidence, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/M4chq5Sb540/CC1Fh0Y_NWwJ", :cwe_id => [915] end end #For a given create_with call, set confidence level. #Ignore calls that use permit() def danger_level exp return unless sexp? exp if call? exp and exp.method == :permit nil elsif request_value? exp :high elsif hash? exp nil elsif has_immediate_user_input?(exp) :high elsif include_user_input? exp :medium else :weak end end def generic_warning warn :warning_type => "Mass Assignment", :warning_code => :CVE_2014_3514, :message => @message, :gem_info => gemfile_or_environment, :confidence => :medium, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/M4chq5Sb540/CC1Fh0Y_NWwJ", :cwe_id => [915] end end ================================================ FILE: lib/brakeman/checks/check_cross_site_scripting.rb ================================================ require 'brakeman/checks/base_check' require 'brakeman/processors/lib/find_call' require 'brakeman/processors/lib/processor_helper' require 'brakeman/util' require 'set' #This check looks for unescaped output in templates which contains #parameters or model attributes. # #For example: # # <%= User.find(:id).name %> # <%= params[:id] %> class Brakeman::CheckCrossSiteScripting < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for unescaped output in views" #Model methods which are known to be harmless IGNORE_MODEL_METHODS = Set[:average, :count, :maximum, :minimum, :sum, :id] MODEL_METHODS = Set[:all, :find, :first, :last, :new] IGNORE_LIKE = /^link_to_|(_path|_tag|_url)$/ HAML_HELPERS = Sexp.new(:colon2, Sexp.new(:const, :Haml), :Helpers) XML_HELPER = Sexp.new(:colon2, Sexp.new(:const, :Erubis), :XmlHelper) URI = Sexp.new(:const, :URI) CGI = Sexp.new(:const, :CGI) FORM_BUILDER = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new) def initialize *args super @matched = @mark = false end #Run check def run_check setup tracker.each_template do |name, template| Brakeman.debug "Checking #{name} for XSS" @current_template = template template.each_output do |out| unless check_for_immediate_xss out @matched = false @mark = false process out end end end end def check_for_immediate_xss exp return :duplicate if duplicate? exp if exp.node_type == :output out = exp.value end if raw_call? exp out = exp.value.first_arg elsif html_safe_call? exp out = exp.value.target end return if call? out and ignore_call? out.target, out.method if input = has_immediate_user_input?(out) add_result exp message = msg("Unescaped ", msg_input(input)) warn :template => @current_template, :warning_type => "Cross-Site Scripting", :warning_code => :cross_site_scripting, :message => message, :code => input.match, :confidence => :high, :cwe_id => [79] elsif not tracker.options[:ignore_model_output] and match = has_immediate_model?(out) method = if call? match match.method else nil end unless IGNORE_MODEL_METHODS.include? method add_result exp if likely_model_attribute? match confidence = :high else confidence = :medium end message = "Unescaped model attribute" link_path = "cross_site_scripting" warning_code = :cross_site_scripting if node_type?(out, :call, :safe_call, :attrasgn, :safe_attrasgn) && out.method == :to_json message += " in JSON hash" link_path += "_to_json" warning_code = :xss_to_json end warn :template => @current_template, :warning_type => "Cross-Site Scripting", :warning_code => warning_code, :message => message, :code => match, :confidence => confidence, :link_path => link_path, :cwe_id => [79] end else false end end #Call already involves a model, but might not be acting on a record def likely_model_attribute? exp return false unless call? exp method = exp.method if MODEL_METHODS.include? method or method.to_s.start_with? "find_by_" true else likely_model_attribute? exp.target end end #Process an output Sexp def process_output exp process exp.value.dup end #Look for calls to raw() #Otherwise, ignore def process_escaped_output exp unless check_for_immediate_xss exp if not duplicate? exp if raw_call? exp process exp.value.first_arg elsif html_safe_call? exp process exp.value.target end end end exp end #Check a call for user input # # #Since we want to report an entire call and not just part of one, use @mark #to mark when a call is started. Any dangerous values inside will then #report the entire call chain. def process_call exp if @mark actually_process_call exp else @mark = true actually_process_call exp message = nil if @matched unless @matched.type and tracker.options[:ignore_model_output] message = msg("Unescaped ", msg_input(@matched)) end if message and not duplicate? exp add_result exp link_path = "cross_site_scripting" warning_code = :cross_site_scripting if @known_dangerous.include? exp.method confidence = :high if exp.method == :to_json message << msg_plain(" in JSON hash") link_path += "_to_json" warning_code = :xss_to_json end else confidence = :weak end warn :template => @current_template, :warning_type => "Cross-Site Scripting", :warning_code => warning_code, :message => message, :code => exp, :user_input => @matched, :confidence => confidence, :link_path => link_path, :cwe_id => [79] end end @mark = @matched = false end exp end def actually_process_call exp return if @matched target = exp.target if sexp? target target = process target end method = exp.method #Ignore safe items if ignore_call? target, method @matched = false elsif sexp? target and model_name? target[1] #TODO: use method call? @matched = Match.new(:model, exp) elsif cookies? exp @matched = Match.new(:cookies, exp) elsif @inspect_arguments and params? exp @matched = Match.new(:params, exp) elsif @inspect_arguments process_call_args exp end end #Note that params have been found def process_params exp @matched = Match.new(:params, exp) exp end #Note that cookies have been found def process_cookies exp @matched = Match.new(:cookies, exp) exp end #Ignore calls to render def process_render exp exp end #Process as default def process_dstr exp process_default exp end #Process as default def process_format exp process_default exp end #Ignore output HTML escaped via HAML def process_format_escaped exp exp end #Ignore condition in if Sexp def process_if exp process exp.then_clause if sexp? exp.then_clause process exp.else_clause if sexp? exp.else_clause exp end def process_case exp #Ignore user input in case value #TODO: also ignore when values current = 2 while current < exp.length process exp[current] if exp[current] current += 1 end exp end def setup @ignore_methods = Set[:==, :!=, :button_to, :check_box, :content_tag, :escapeHTML, :escape_once, :field_field, :fields_for, :form_for, :h, :hidden_field, :hidden_field, :hidden_field_tag, :image_tag, :label, :link_to, :mail_to, :radio_button, :select, :submit_tag, :text_area, :text_field, :text_field_tag, :url_encode, :u, :url_for, :will_paginate].merge tracker.options[:safe_methods] @models = tracker.models.keys @inspect_arguments = tracker.options[:check_arguments] @known_dangerous = Set[:truncate, :concat] if version_between? "2.0.0", "3.0.5" @known_dangerous << :auto_link elsif version_between? "3.0.6", "3.0.99" @ignore_methods << :auto_link end if version_between? "2.0.0", "2.3.14" or tracker.config.gem_version(:'rails-html-sanitizer') == '1.0.2' @known_dangerous << :strip_tags end if tracker.config.has_gem? :'rails-html-sanitizer' and version_between? "1.0.0", "1.0.2", tracker.config.gem_version(:'rails-html-sanitizer') @known_dangerous << :sanitize end json_escape_on = false initializers = tracker.find_call(target: :ActiveSupport, method: :escape_html_entities_in_json=) initializers.each {|result| json_escape_on = true?(result[:call].first_arg) } if tracker.config.escape_html_entities_in_json? json_escape_on = true elsif version_between? "4.0.0", "9.9.9" json_escape_on = true end if !json_escape_on or version_between? "0.0.0", "2.0.99" @known_dangerous << :to_json Brakeman.debug("Automatic to_json escaping not enabled, consider to_json dangerous") else @safe_input_attributes << :to_json Brakeman.debug("Automatic to_json escaping is enabled.") end end def raw_call? exp exp.value.node_type == :call and exp.value.method == :raw end def html_safe_call? exp call? exp.value and exp.value.method == :html_safe end def ignore_call? target, method ignored_method?(target, method) or safe_input_attribute?(target, method) or ignored_model_method?(target, method) or form_builder_method?(target, method) or haml_escaped?(target, method) or boolean_method?(method) or cgi_escaped?(target, method) or xml_escaped?(target, method) end def ignored_model_method? target, method ((@matched and @matched.type == :model) or model_name? target) and IGNORE_MODEL_METHODS.include? method end def ignored_method? target, method @ignore_methods.include? method or method.to_s =~ IGNORE_LIKE end def cgi_escaped? target, method method == :escape and (target == URI or target == CGI) end def haml_escaped? target, method method == :html_escape and target == HAML_HELPERS end def xml_escaped? target, method method == :escape_xml and target == XML_HELPER end def form_builder_method? target, method target == FORM_BUILDER and @ignore_methods.include? method end def safe_input_attribute? target, method target and always_safe_method? method end end ================================================ FILE: lib/brakeman/checks/check_csrf_token_forgery_cve.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckCSRFTokenForgeryCVE < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions with CSRF token forgery vulnerability (CVE-2020-8166)" def run_check fix_version = case when version_between?('0.0.0', '5.2.4.2') '5.2.4.3' when version_between?('6.0.0', '6.0.3') '6.0.3.1' else nil end if fix_version warn :warning_type => "Cross-Site Request Forgery", :warning_code => :CVE_2020_8166, :message => msg(msg_version(rails_version), " has a vulnerability that may allow CSRF token forgery. Upgrade to ", msg_version(fix_version), " or patch"), :confidence => :medium, :gem_info => gemfile_or_environment, :link => "https://groups.google.com/g/rubyonrails-security/c/NOjKiGeXUgw", :cwe_id => [352] end end end ================================================ FILE: lib/brakeman/checks/check_default_routes.rb ================================================ require 'brakeman/checks/base_check' #Checks if default routes are allowed in routes.rb class Brakeman::CheckDefaultRoutes < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for default routes" def initialize *args super @actions_allowed_on_controller = nil end #Checks for :allow_all_actions globally and for individual routes #if it is not enabled globally. def run_check check_for_default_routes check_for_action_globs check_for_cve_2014_0130 end def check_for_default_routes if allow_all_actions? #Default routes are enabled globally warn :warning_type => "Default Routes", :warning_code => :all_default_routes, :message => msg("All public methods in controllers are available as actions in ", msg_file("routes.rb")), :line => tracker.routes[:allow_all_actions].line, :confidence => :high, :file => "#{tracker.app_path}/config/routes.rb", :cwe_id => [22] end end def check_for_action_globs return if allow_all_actions? Brakeman.debug "Checking each controller for default routes" tracker.routes.each do |name, actions| if actions.is_a? Array and actions[0] == :allow_all_actions @actions_allowed_on_controller = true if actions[1].is_a? Hash and actions[1][:allow_verb] verb = actions[1][:allow_verb] else verb = "any" end warn :controller => name, :warning_type => "Default Routes", :warning_code => :controller_default_routes, :message => msg("Any public method in ", msg_code(name), " can be used as an action for ", msg_code(verb), " requests."), :line => actions[2], :confidence => :medium, :file => "#{tracker.app_path}/config/routes.rb", :cwe_id => [22] end end end def check_for_cve_2014_0130 case when lts_version?("2.3.18.9") #TODO: Should support LTS 3.0.20 too return when version_between?("2.0.0", "2.3.18") upgrade = "3.2.18" when version_between?("3.0.0", "3.2.17") upgrade = "3.2.18" when version_between?("4.0.0", "4.0.4") upgrade = "4.0.5" when version_between?("4.1.0", "4.1.0") upgrade = "4.1.1" else return end if allow_all_actions? or @actions_allowed_on_controller confidence = :high else confidence = :medium end warn :warning_type => "Remote Code Execution", :warning_code => :CVE_2014_0130, :message => msg(msg_version(rails_version), " with globbing routes is vulnerable to directory traversal and remote code execution. Patch or upgrade to ", msg_version(upgrade)), :confidence => confidence, :file => "#{tracker.app_path}/config/routes.rb", :link => "http://matasano.com/research/AnatomyOfRailsVuln-CVE-2014-0130.pdf", :cwe_id => [22] end def allow_all_actions? tracker.routes[:allow_all_actions] end end ================================================ FILE: lib/brakeman/checks/check_deserialize.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckDeserialize < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for unsafe deserialization of objects" def run_check check_yaml check_csv check_marshal check_oj end def check_yaml check_methods :YAML, :load_documents, :load_stream, :parse_documents, :parse_stream # Check for safe_yaml gem use with YAML.load(..., safe: true) if uses_safe_yaml? tracker.find_call(target: :YAML, method: :load).each do |result| call = result[:call] options = call.second_arg if hash? options and true? hash_access(options, :safe) next else check_deserialize result, :YAML end end else check_methods :YAML, :load end end def check_csv check_methods :CSV, :load end def check_marshal check_methods :Marshal, :load, :restore end def check_oj check_methods :Oj, :object_load # Always unsafe, regardless of mode unsafe_mode = :object safe_default = oj_safe_default? tracker.find_call(:target => :Oj, :method => :load).each do |result| call = result[:call] options = call.second_arg if options and hash? options and mode = hash_access(options, :mode) if symbol? mode and mode.value == unsafe_mode check_deserialize result, :Oj end elsif not safe_default check_deserialize result, :Oj end end end def check_methods target, *methods tracker.find_call(:target => target, :methods => methods ).each do |result| check_deserialize result, target end end def check_deserialize result, target, arg = nil return unless original? result arg ||= result[:call].first_arg method = result[:call].method if input = has_immediate_user_input?(arg) confidence = :high elsif input = include_user_input?(arg) confidence = :medium elsif target == :Marshal confidence = :low message = msg("Use of ", msg_code("#{target}.#{method}"), " may be dangerous") end if confidence message ||= msg(msg_code("#{target}.#{method}"), " called with ", msg_input(input)) warn :result => result, :warning_type => "Remote Code Execution", :warning_code => :unsafe_deserialize, :message => message, :user_input => input, :confidence => confidence, :link_path => "unsafe_deserialization", :cwe_id => [502] end end private def oj_safe_default? safe_default = false if tracker.find_call(target: :Oj, method: :mimic_JSON).any? safe_default = true elsif result = tracker.find_call(target: :Oj, method: :default_options=).first options = result[:call].first_arg if oj_safe_mode? options safe_default = true end end safe_default end def oj_safe_mode? options if hash? options and mode = hash_access(options, :mode) if symbol? mode and mode != :object return true end end false end def uses_safe_yaml? tracker.config.has_gem? :safe_yaml end end ================================================ FILE: lib/brakeman/checks/check_detailed_exceptions.rb ================================================ require 'brakeman/checks/base_check' # Check for detailed exceptions enabled for production class Brakeman::CheckDetailedExceptions < Brakeman::BaseCheck Brakeman::Checks.add self LOCAL_REQUEST = s(:call, s(:call, nil, :request), :local?) @description = "Checks for information disclosure displayed via detailed exceptions" def run_check check_local_request_config check_detailed_exceptions end def check_local_request_config if true? tracker.config.rails[:consider_all_requests_local] warn :warning_type => "Information Disclosure", :warning_code => :local_request_config, :message => "Detailed exceptions are enabled in production", :confidence => :high, :file => "config/environments/production.rb", :cwe_id => [200] end end def check_detailed_exceptions tracker.controllers.each do |_name, controller| controller.methods_public.each do |method_name, definition| src = definition.src body = src.body.last next unless body if method_name == :show_detailed_exceptions? and not safe? body if true? body confidence = :high else confidence = :medium end warn :warning_type => "Information Disclosure", :warning_code => :detailed_exceptions, :message => msg("Detailed exceptions may be enabled in ", msg_code("show_detailed_exceptions?")), :confidence => confidence, :code => src, :file => definition[:file], :cwe_id => [200] end end end end def safe? body false? body or body == LOCAL_REQUEST end end ================================================ FILE: lib/brakeman/checks/check_digest_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckDigestDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for digest authentication DoS vulnerability" def run_check message = msg("Vulnerability in digest authentication ", msg_cve("CVE-2012-3424"), ". Upgrade to ") if version_between? "3.0.0", "3.0.15" message << msg_version("3.0.16") elsif version_between? "3.1.0", "3.1.6" message << msg_version("3.1.7") elsif version_between? "3.2.0", "3.2.5" message << msg_version("3.2.7") else return end if with_http_digest? confidence = :high else confidence = :weak end warn :warning_type => "Denial of Service", :warning_code => :CVE_2012_3424, :message => message, :confidence => confidence, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/vxJjrc15qYM/discussion", :gem_info => gemfile_or_environment, :cwe_id => [287] end def with_http_digest? not tracker.find_call(:target => false, :method => [:authenticate_or_request_with_http_digest, :authenticate_with_http_digest]).empty? end end ================================================ FILE: lib/brakeman/checks/check_divide_by_zero.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckDivideByZero < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Warns on potential division by zero" def run_check tracker.find_call(:method => :"/").each do |result| check_division result end end def check_division result return unless original? result call = result[:call] denominator = call.first_arg if number? denominator and denominator.value == 0 numerator = call.target if number? numerator if numerator.value.is_a? Float return # 0.0 / 0 is NaN and 1.0 / 0 is Infinity else confidence = :medium end else confidence = :weak end warn :result => result, :warning_type => "Divide by Zero", :warning_code => :divide_by_zero, :message => "Potential division by zero", :confidence => confidence, :user_input => denominator, :cwe_id => [369] end end end ================================================ FILE: lib/brakeman/checks/check_dynamic_finders.rb ================================================ require 'brakeman/checks/base_check' #This check looks for regexes that include user input. class Brakeman::CheckDynamicFinders < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check unsafe usage of find_by_*" def run_check if tracker.config.has_gem? :mysql and version_between? '2.0.0', '4.1.99' tracker.find_call(:method => /^find_by_/).each do |result| process_result result end end end def process_result result return unless original? result call = result[:call] if potentially_dangerous? call.method call.each_arg do |arg| if params? arg and not safe_call? arg warn :result => result, :warning_type => "SQL Injection", :warning_code => :sql_injection_dynamic_finder, :message => "MySQL integer conversion may cause 0 to match any string", :confidence => :medium, :user_input => arg, :cwe_id => [89] break end end end end def safe_call? arg return false unless call? arg meth = arg.method meth == :to_s or meth == :to_i end def potentially_dangerous? method_name method_name.match(/^find_by_.*(token|guid|password|api_key|activation|code|private|reset)/) end end ================================================ FILE: lib/brakeman/checks/check_eol_rails.rb ================================================ require_relative 'eol_check' class Brakeman::CheckEOLRails < Brakeman::EOLCheck Brakeman::Checks.add self @description = "Checks for unsupported versions of Rails" def run_check return unless tracker.config.rails_version check_eol_version :rails, RAILS_EOL_DATES end # https://rubyonrails.org/maintenance # https://endoflife.date/rails RAILS_EOL_DATES = { ['2.0.0', '2.3.99'] => Date.new(2013, 6, 25), ['3.0.0', '3.2.99'] => Date.new(2016, 6, 30), ['4.0.0', '4.2.99'] => Date.new(2017, 4, 27), ['5.0.0', '5.0.99'] => Date.new(2018, 5, 9), ['5.1.0', '5.1.99'] => Date.new(2019, 8, 25), ['5.2.0', '5.2.99'] => Date.new(2022, 6, 1), ['6.0.0', '6.0.99'] => Date.new(2023, 6, 1), ['6.1.0', '6.1.99'] => Date.new(2024, 10, 1), ['7.0.0', '7.0.99'] => Date.new(2025, 4, 1), ['7.1.0', '7.1.99'] => Date.new(2025, 10, 1), ['7.2.0', '7.2.99'] => Date.new(2026, 8, 9), ['8.0.0', '8.0.99'] => Date.new(2026, 10, 7), } end ================================================ FILE: lib/brakeman/checks/check_eol_ruby.rb ================================================ require_relative 'eol_check' class Brakeman::CheckEOLRuby < Brakeman::EOLCheck Brakeman::Checks.add self @description = "Checks for unsupported versions of Ruby" def run_check return unless tracker.config.ruby_version check_eol_version :ruby, RUBY_EOL_DATES end RUBY_EOL_DATES = { ['0.0.0', '1.9.3'] => Date.new(2015, 2, 23), ['2.0.0', '2.0.99'] => Date.new(2016, 2, 24), ['2.1.0', '2.1.99'] => Date.new(2017, 3, 31), ['2.2.0', '2.2.99'] => Date.new(2018, 3, 31), ['2.3.0', '2.3.99'] => Date.new(2019, 3, 31), ['2.4.0', '2.4.99'] => Date.new(2020, 3, 31), ['2.5.0', '2.5.99'] => Date.new(2021, 3, 31), ['2.6.0', '2.6.99'] => Date.new(2022, 3, 31), ['2.7.0', '2.7.99'] => Date.new(2023, 3, 31), ['3.0.0', '3.0.99'] => Date.new(2024, 3, 31), ['3.1.0', '3.1.99'] => Date.new(2025, 3, 31), ['3.2.0', '3.2.99'] => Date.new(2026, 3, 31), ['3.3.0', '3.3.99'] => Date.new(2027, 3, 31), ['3.4.0', '3.4.99'] => Date.new(2028, 3, 31), } end ================================================ FILE: lib/brakeman/checks/check_escape_function.rb ================================================ require 'brakeman/checks/base_check' #Check for versions with vulnerable html escape method #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/56bffb5923ab1195 class Brakeman::CheckEscapeFunction < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions before 2.3.14 which have a vulnerable escape method" def run_check if version_between?('2.0.0', '2.3.13') and RUBY_VERSION < '1.9.0' warn :warning_type => 'Cross-Site Scripting', :warning_code => :CVE_2011_2932, :message => msg("Rails versions before 2.3.14 have a vulnerability in the ", msg_code("escape"), " method when used with Ruby 1.8 ", msg_cve("CVE-2011-2932")), :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/Vr_7WSOrEZU/discussion", :cwe_id => [79] end end end ================================================ FILE: lib/brakeman/checks/check_evaluation.rb ================================================ require 'brakeman/checks/base_check' #This check looks for calls to +eval+, +instance_eval+, etc. which include #user input. class Brakeman::CheckEvaluation < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Searches for evaluation of user input" #Process calls def run_check Brakeman.debug "Finding eval-like calls" calls = tracker.find_call methods: [:eval, :instance_eval, :class_eval, :module_eval], nested: true Brakeman.debug "Processing eval-like calls" calls.each do |call| process_result call end end #Warns if eval includes user input def process_result result return unless original? result first_arg = result[:call].first_arg unless safe_value? first_arg if input = include_user_input?(first_arg) confidence = :high message = msg(msg_input(input), " evaluated as code") elsif string_evaluation? first_arg confidence = :low message = "Dynamic string evaluated as code" elsif result[:call].method == :eval confidence = :low message = "Dynamic code evaluation" end if confidence warn :result => result, :warning_type => "Dangerous Eval", :warning_code => :code_eval, :message => message, :user_input => input, :confidence => confidence, :cwe_id => [913, 95] end end end def string_evaluation? exp string_interp? exp or (call? exp and string? exp.target) end def safe_value? exp return true unless sexp? exp case exp.sexp_type when :dstr exp.all? { |e| safe_value? e} when :evstr safe_value? exp.value when :str, :lit true when :call always_safe_method? exp.method else false end end end ================================================ FILE: lib/brakeman/checks/check_execute.rb ================================================ require 'brakeman/checks/base_check' #Checks for string interpolation and parameters in calls to #Kernel#system, Kernel#exec, Kernel#syscall, and inside backticks. # #Examples of command injection vulnerabilities: # # system("rf -rf #{params[:file]}") # exec(params[:command]) # `unlink #{params[:something}` class Brakeman::CheckExecute < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Finds instances of possible command injection" SAFE_VALUES = [s(:const, :RAILS_ROOT), s(:call, s(:const, :Rails), :root), s(:call, s(:const, :Rails), :env), s(:call, s(:const, :Process), :pid)] SHELL_ESCAPE_MODULE_METHODS = Set[:escape, :join, :shellescape, :shelljoin] SHELL_ESCAPE_MIXIN_METHODS = Set[:shellescape, :shelljoin] # These are common shells that are known to allow the execution of commands # via a -c flag. See dash_c_shell_command? for more info. KNOWN_SHELL_COMMANDS = Set["sh", "bash", "ksh", "csh", "tcsh", "zsh"] SHELLWORDS = s(:const, :Shellwords) #Check models, controllers, and views for command injection. def run_check Brakeman.debug "Finding system calls using ``" check_for_backticks tracker check_open_calls Brakeman.debug "Finding other system calls" calls = tracker.find_call :targets => [:IO, :Open3, :Kernel, :'POSIX::Spawn', :Process, nil], :methods => [:capture2, :capture2e, :capture3, :exec, :pipeline, :pipeline_r, :pipeline_rw, :pipeline_start, :pipeline_w, :popen, :popen2, :popen2e, :popen3, :spawn, :syscall, :system], :nested => true Brakeman.debug "Processing system calls" calls.each do |result| process_result result end end private #Processes results from Tracker#find_call. def process_result result call = result[:call] args = call.arglist first_arg = call.first_arg failure = nil case call.method when :popen # Normally, if we're in a `popen` call, we only are worried about shell # injection when the argument is not an array, because array elements # are always escaped by Ruby. However, an exception is when the array # contains two values are something like "bash -c" because then the third # element is effectively the command being run and might be a malicious # executable if it comes (partially or fully) from user input. if !array?(first_arg) failure = include_user_input?(first_arg) || dangerous_interp?(first_arg) || dangerous_string_building?(first_arg) elsif dash_c_shell_command?(first_arg[1], first_arg[2]) failure = include_user_input?(first_arg[3]) || dangerous_interp?(first_arg[3]) || dangerous_string_building?(first_arg[3]) end when :pipeline, :pipline_r, :pipeline_rw, :pipeline_w, :pipeline_start # Since these pipeline commands pipe together several commands, # need to check each argument. If it's an array, check first argument # (the command) and also check for `bash -c`. Otherwise check the argument # as a unit. args.each do |arg| next unless sexp? arg if array?(arg) # Check first element of array failure = include_user_input?(arg[1]) || dangerous_interp?(arg[1]) || dangerous_string_building?(arg[1]) # Check for ['bash', '-c', user_input] if dash_c_shell_command?(arg[1], arg[2]) failure = include_user_input?(arg[3]) || dangerous_interp?(arg[3]) || dangerous_string_building?(arg[3]) end else failure = include_user_input?(arg) end break if failure end when :system, :exec # Normally, if we're in a `system` or `exec` call, we only are worried # about shell injection when there's a single argument, because comma- # separated arguments are always escaped by Ruby. However, an exception is # when the first two arguments are something like "bash -c" because then # the third argument is effectively the command being run and might be # a malicious executable if it comes (partially or fully) from user input. if dash_c_shell_command?(first_arg, call.second_arg) failure = include_user_input?(args[3]) || dangerous_interp?(args[3]) || dangerous_string_building?(args[3]) else failure = include_user_input?(first_arg) || dangerous_interp?(first_arg) || dangerous_string_building?(first_arg) end when :capture2, :capture2e, :capture3 # Open3 capture methods can take a :stdin_data argument which is used as the # the input to the called command so it is not succeptable to command injection. # As such if the last argument is a hash (and therefore execution options) it # should be ignored args.pop if hash?(args.last) && args.length > 2 failure = include_user_input?(args) || dangerous_interp?(args) || dangerous_string_building?(args) else failure = include_user_input?(args) || dangerous_interp?(args) || dangerous_string_building?(args) end if failure and original? result if failure.type == :interp #Not from user input confidence = :medium else confidence = :high end warn :result => result, :warning_type => "Command Injection", :warning_code => :command_injection, :message => "Possible command injection", :code => call, :user_input => failure, :confidence => confidence, :cwe_id => [77] end end # @return [Boolean] true iff the command given by `first_arg`, `second_arg` # invokes a new shell process via ` -c` (like `bash -c`) def dash_c_shell_command?(first_arg, second_arg) string?(first_arg) && KNOWN_SHELL_COMMANDS.include?(first_arg.value) && string?(second_arg) && second_arg.value == "-c" end def check_open_calls tracker.find_call(:targets => [nil, :Kernel], :method => :open).each do |result| if match = dangerous_open_arg?(result[:call].first_arg) warn :result => result, :warning_type => "Command Injection", :warning_code => :command_injection, :message => msg("Possible command injection in ", msg_code("open")), :user_input => match, :confidence => :high, :cwe_id => [77] end end end def include_user_input? exp if node_type? exp, :arglist, :dstr, :evstr, :dxstr exp.each_sexp do |e| if res = include_user_input?(e) return res end end false else if shell_escape? exp false else super exp end end end def dangerous_open_arg? exp if string_interp? exp # Check for input at start of string exp[1] == "" and node_type? exp[2], :evstr and has_immediate_user_input? exp[2] else has_immediate_user_input? exp end end #Looks for calls using backticks such as # # `rm -rf #{params[:file]}` def check_for_backticks tracker tracker.find_call(:target => nil, :method => :`).each do |result| process_backticks result end end #Processes backticks. def process_backticks result return unless original? result exp = result[:call] if input = include_user_input?(exp) confidence = :high elsif input = dangerous?(exp) confidence = :medium else return end warn :result => result, :warning_type => "Command Injection", :warning_code => :command_injection, :message => "Possible command injection", :code => exp, :user_input => input, :confidence => confidence, :cwe_id => [77] end # This method expects a :dstr or :evstr node def dangerous? exp exp.each_sexp do |e| if call? e and e.method == :to_s e = e.target end next if node_type? e, :lit, :str next if SAFE_VALUES.include? e next if shell_escape? e next if temp_file_path? e if node_type? e, :if # If we're in a conditional, evaluate the `then` and `else` clauses to # see if they're dangerous. if res = dangerous?(e.sexp_body.sexp_body) return res end elsif node_type? e, :or, :evstr, :dstr if res = dangerous?(e) return res end else return e end end false end def dangerous_interp? exp match = include_interp? exp return unless match interp = match.match interp.each_sexp do |e| if res = dangerous?(e) return Match.new(:interp, res) end end false end #Checks if an expression contains string interpolation. # #Returns Match with :interp type if found. def include_interp? exp @string_interp = false process exp @string_interp end def dangerous_string_building? exp if string_building?(exp) && res = dangerous?(exp) return Match.new(:interp, res) end false end def shell_escape? exp return false unless call? exp if exp.target == SHELLWORDS and SHELL_ESCAPE_MODULE_METHODS.include? exp.method true elsif SHELL_ESCAPE_MIXIN_METHODS.include?(exp.method) true else false end end end ================================================ FILE: lib/brakeman/checks/check_file_access.rb ================================================ require 'brakeman/checks/base_check' require 'brakeman/processors/lib/processor_helper' #Checks for user input in methods which open or manipulate files class Brakeman::CheckFileAccess < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Finds possible file access using user input" def run_check Brakeman.debug "Finding possible file access" methods = tracker.find_call :targets => [:Dir, :File, :IO, :Kernel, :"Net::FTP", :"Net::HTTP", :PStore, :Pathname, :Shell], :methods => [:[], :chdir, :chroot, :delete, :entries, :foreach, :glob, :install, :lchmod, :lchown, :link, :load, :load_file, :makedirs, :move, :new, :open, :read, :readlines, :rename, :rmdir, :safe_unlink, :symlink, :syscopy, :sysopen, :truncate, :unlink] methods.concat tracker.find_call :target => :YAML, :methods => [:load_file, :parse_file] methods.concat tracker.find_call :target => nil, :method => [:open] Brakeman.debug "Finding calls to load()" methods.concat tracker.find_call :target => false, :method => :load Brakeman.debug "Finding calls using FileUtils" methods.concat tracker.find_call :target => :FileUtils Brakeman.debug "Processing found calls" methods.each do |call| process_result call end end def process_result result return unless original? result call = result[:call] file_name = call.first_arg return if called_on_tempfile?(file_name) || sanitized?(file_name) if match = has_immediate_user_input?(file_name) confidence = :high elsif match = has_immediate_model?(file_name) match = Match.new(:model, match) confidence = :medium elsif tracker.options[:check_arguments] and match = include_user_input?(file_name) #Check for string building in file name if call?(file_name) and (file_name.method == :+ or file_name.method == :<<) confidence = :high else confidence = :weak end end if match and not temp_file_method? match.match message = msg(msg_input(match), " used in file name") warn :result => result, :warning_type => "File Access", :warning_code => :file_access, :message => message, :confidence => confidence, :code => call, :user_input => match, :cwe_id => [22] end end # When using Tempfile, there is no risk of unauthorized file access, since # Tempfile adds a unique string onto the end of every provided filename, and # ensures that the filename does not already exist in the system. def called_on_tempfile? file_name call?(file_name) && file_name.target == s(:const, :Tempfile) end def sanitized? file call?(file) && call?(file.target) && class_name(file.target.target) == :"ActiveStorage::Filename" end def temp_file_method? exp if call? exp return true if exp.call_chain.include? :tempfile params? exp.target and exp.method == :path end end end ================================================ FILE: lib/brakeman/checks/check_file_disclosure.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckFileDisclosure < Brakeman::BaseCheck Brakeman::Checks.add self @description = 'Checks for versions with file existence disclosure vulnerability' def run_check fix_version = case when version_between?('2.0.0', '2.3.18') '3.2.21' when version_between?('3.0.0', '3.2.20') '3.2.21' when version_between?('4.0.0', '4.0.11') '4.0.12' when version_between?('4.1.0', '4.1.7') '4.1.8' else nil end if fix_version and serves_static_assets? warn :warning_type => "File Access", :warning_code => :CVE_2014_7829, :message => msg(msg_version(rails_version), " has a file existence disclosure vulnerability. Upgrade to ", msg_version(fix_version), " or disable serving static assets"), :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/23fiuwb1NBA/MQVM1-5GkPMJ", :cwe_id => [22] end end def serves_static_assets? true? tracker.config.rails[:serve_static_assets] end end ================================================ FILE: lib/brakeman/checks/check_filter_skipping.rb ================================================ require 'brakeman/checks/base_check' #Check for filter skipping vulnerability #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/3420ac71aed312d6 class Brakeman::CheckFilterSkipping < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions 3.0-3.0.9 which had a vulnerability in filters" def run_check if version_between?('3.0.0', '3.0.9') and uses_arbitrary_actions? warn :warning_type => "Default Routes", :warning_code => :CVE_2011_2929, :message => msg("Rails versions before 3.0.10 have a vulnerability which allows filters to be bypassed", msg_cve("CVE-2011-2929")), :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/NCCsca7TEtY/discussion", :cwe_id => [20] end end def uses_arbitrary_actions? tracker.routes.each do |_name, actions| if actions.include? :allow_all_actions return true end end false end end ================================================ FILE: lib/brakeman/checks/check_force_ssl.rb ================================================ class Brakeman::CheckForceSSL < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Check that force_ssl setting is enabled in production" def run_check return if tracker.config.rails.empty? or tracker.config.rails_version.nil? return if tracker.config.rails_version < "3.1.0" force_ssl = tracker.config.rails[:force_ssl] if false? force_ssl or force_ssl.nil? line = if sexp? force_ssl force_ssl.line else 1 end warn :warning_type => "Missing Encryption", :warning_code => :force_ssl_disabled, :message => msg("The application does not force use of HTTPS: ", msg_code("config.force_ssl"), " is not enabled"), :confidence => :high, :file => "config/environments/production.rb", :line => line, :cwe_id => [311] end end end ================================================ FILE: lib/brakeman/checks/check_forgery_setting.rb ================================================ require 'brakeman/checks/base_check' #Checks that +protect_from_forgery+ is set in the ApplicationController. # #Also warns for CSRF weakness in certain versions of Rails: #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/2d95a3cc23e03665 class Brakeman::CheckForgerySetting < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Verifies that protect_from_forgery is enabled in direct subclasses of ActionController::Base" def run_check return if tracker.config.default_protect_from_forgery? tracker.controllers .select { |_, controller| controller.parent == :"ActionController::Base" } .each do |name, controller| if controller and not controller.protect_from_forgery? csrf_warning :controller => name, :warning_code => :csrf_protection_missing, :message => msg(msg_code("protect_from_forgery"), " should be called in ", msg_code(name)), :file => controller.file, :line => controller.top_line elsif version_between? "4.0.0", "100.0.0" and forgery_opts = controller.options[:protect_from_forgery] unless forgery_opts.is_a?(Array) and sexp?(forgery_opts.first) and access_arg = hash_access(forgery_opts.first.first_arg, :with) and symbol? access_arg and access_arg.value == :exception args = { :controller => name, :warning_type => "Cross-Site Request Forgery", :warning_code => :csrf_not_protected_by_raising_exception, :message => msg(msg_code("protect_from_forgery"), " should be configured with ", msg_code("with: :exception")), :confidence => :medium, :file => controller.file } args.merge!(:code => forgery_opts.first) if forgery_opts.is_a?(Array) csrf_warning args end end if controller.options[:protect_from_forgery] check_cve_2011_0447 end end end def csrf_warning opts opts = { :controller => :ApplicationController, :warning_type => "Cross-Site Request Forgery", :confidence => :high, :cwe_id => [352] }.merge opts warn opts end def check_cve_2011_0447 @warned_cve_2011_0447 ||= false return if @warned_cve_2011_0447 if version_between? "2.1.0", "2.3.10" new_version = "2.3.11" elsif version_between? "3.0.0", "3.0.3" new_version = "3.0.4" else return end @warned_cve_2011_0447 = true # only warn once csrf_warning :warning_code => :CVE_2011_0447, :message => msg("CSRF protection is flawed in unpatched versions of ", msg_version(rails_version), " ", msg_cve("CVE-2011-0447"), ". Upgrade to ", msg_version(new_version), " or apply patches as needed"), :gem_info => gemfile_or_environment, :file => nil, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/LZWjzCPgNmU/discussion", :cwe_id => [352] end end ================================================ FILE: lib/brakeman/checks/check_header_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckHeaderDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for header DoS (CVE-2013-6414)" def run_check if (version_between? "3.0.0", "3.2.15" or version_between? "4.0.0", "4.0.1") and not has_workaround? message = msg(msg_version(rails_version), " has a denial of service vulnerability ", msg_cve("CVE-2013-6414"), ". Upgrade to ") if version_between? "3.0.0", "3.2.15" message << msg_version("3.2.16") else message << msg_version("4.0.2") end warn :warning_type => "Denial of Service", :warning_code => :CVE_2013_6414, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/A-ebV4WxzKg/KNPTbX8XAQUJ", :cwe_id => [20] end end def has_workaround? tracker.find_call(target: :ActiveSupport, method: :on_load).any? and tracker.find_call(target: :"ActionView::LookupContext::DetailsKey", method: :class_eval).any? end end ================================================ FILE: lib/brakeman/checks/check_i18n_xss.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckI18nXSS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for i18n XSS (CVE-2013-4491)" def run_check if (version_between? "3.0.6", "3.2.15" or version_between? "4.0.0", "4.0.1") and not has_workaround? i18n_gem = tracker.config.gem_version :i18n message = msg(msg_version(rails_version), " has an XSS vulnerability in ", msg_version(i18n_gem, "i18n"), " ", msg_cve("CVE-2013-4491"), ". Upgrade to ") if version_between? "3.0.6", "3.1.99" and version_before i18n_gem, "0.5.1" message << msg_version("3.2.16 or i18n 0.5.1") elsif version_between? "3.2.0", "4.0.1" and version_before i18n_gem, "0.6.6" message << msg_version("4.0.2 or i18n 0.6.6") else return end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2013_4491, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment(:i18n), :link_path => "https://groups.google.com/d/msg/ruby-security-ann/pLrh6DUw998/bLFEyIO4k_EJ", :cwe_id => [79] end end def version_before gem_version, target return true unless gem_version gem_version.split('.').map(&:to_i).zip(target.split('.').map(&:to_i)).each do |gv, t| if gv < t return true elsif gv > t return false end end false end def has_workaround? tracker.find_call(target: :I18n, method: :const_defined?, chained: true).any? do |match| match[:call].first_arg == s(:lit, :MissingTranslation) end end end ================================================ FILE: lib/brakeman/checks/check_jruby_xml.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckJRubyXML < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions with JRuby XML parsing backend" def run_check return unless RUBY_PLATFORM == "java" fix_version = case when version_between?('3.0.0', '3.0.99') '3.2.13' when version_between?('3.1.0', '3.1.11') '3.1.12' when version_between?('3.2.0', '3.2.12') '3.2.13' else return end #Check for workaround tracker.find_call(target: :"ActiveSupport::XmlMini", method: :backend=, chained: true).each do |result| arg = result[:call].first_arg return if string? arg and arg.value == "REXML" end warn :warning_type => "File Access", :warning_code => :CVE_2013_1856, :message => msg(msg_version(rails_version), " with JRuby has a vulnerability in XML parser. Upgrade to ", msg_version(fix_version), " or patch"), :confidence => :high, :gem_info => gemfile_or_environment, :link => "https://groups.google.com/d/msg/rubyonrails-security/KZwsQbYsOiI/5kUV7dSCJGwJ", :cwe_id => [20] end end ================================================ FILE: lib/brakeman/checks/check_json_encoding.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckJSONEncoding < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for missing JSON encoding (CVE-2015-3226)" def run_check if (version_between? "4.1.0", "4.1.10" or version_between? "4.2.0", "4.2.1") and not has_workaround? message = msg(msg_version(rails_version), " does not encode JSON keys ", msg_cve("CVE-2015-3226"), ". Upgrade to ") if version_between? "4.1.0", "4.1.10" message << msg_version("4.1.11") else message << msg_version("4.2.2") end if tracker.find_call(:methods => [:to_json, :encode]).any? confidence = :high else confidence = :medium end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2015_3226, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/7VlB_pck3hU/3QZrGIaQW6cJ", :cwe_id => [79] end end def has_workaround? workaround = s(:module, :ActiveSupport, s(:module, :JSON, s(:module, :Encoding, s(:call, nil, :private), s(:class, :EscapedString, nil, s(:defn, :to_s, s(:args), s(:self)))))) tracker.initializers.any? do |_name, initializer| initializer == workaround end end end ================================================ FILE: lib/brakeman/checks/check_json_entity_escape.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckJSONEntityEscape < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check if HTML escaping is disabled for JSON output" def run_check check_config_setting check_manual_disable end def check_config_setting if false? tracker.config.rails.dig(:active_support, :escape_html_entities_in_json) warn :warning_type => "Cross-Site Scripting", :warning_code => :json_html_escape_config, :message => msg("HTML entities in JSON are not escaped by default"), :confidence => :medium, :file => "config/environments/production.rb", :line => 1, :cwe_id => [79] end end def check_manual_disable tracker.find_call(targets: [:ActiveSupport, :'ActiveSupport::JSON::Encoding'], method: :escape_html_entities_in_json=).each do |result| setting = result[:call].first_arg if false? setting warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :json_html_escape_module, :message => msg("HTML entities in JSON are not escaped by default"), :confidence => :medium, :file => "config/environments/production.rb", :cwe_id => [79] end end end end ================================================ FILE: lib/brakeman/checks/check_json_parsing.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckJSONParsing < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for JSON parsing vulnerabilities CVE-2013-0333 and CVE-2013-0269" def initialize *args super @uses_json_parse = nil end def run_check check_cve_2013_0333 check_cve_2013_0269 end def check_cve_2013_0333 return unless version_between? "0.0.0", "2.3.15" or version_between? "3.0.0", "3.0.19" unless uses_yajl? or uses_gem_backend? new_version = if version_between? "0.0.0", "2.3.14" "2.3.16" elsif version_between? "3.0.0", "3.0.19" "3.0.20" end message = msg(msg_version(rails_version), " has a serious JSON parsing vulnerability. Upgrade to ", msg_version(new_version), " or patch") gem_info = gemfile_or_environment warn :warning_type => "Remote Code Execution", :warning_code => :CVE_2013_0333, :message => message, :confidence => :high, :gem_info => gem_info, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/1h2DR63ViGo/discussion", :cwe_id => [74] # TODO: is this the best CWE for this? end end #Check if `yajl` is included in Gemfile def uses_yajl? tracker.config.has_gem? :yajl end #Check for `ActiveSupport::JSON.backend = "JSONGem"` def uses_gem_backend? matches = tracker.find_call(target: :'ActiveSupport::JSON', method: :backend=, chained: true) unless matches.empty? json_gem = s(:str, "JSONGem") matches.each do |result| if result[:call].first_arg == json_gem return true end end end false end def check_cve_2013_0269 [:json, :json_pure].each do |name| gem_hash = tracker.config.get_gem name check_json_version name, gem_hash[:version] if gem_hash and gem_hash[:version] end end def check_json_version name, version return if version >= "1.7.7" or (version >= "1.6.8" and version < "1.7.0") or (version >= "1.5.5" and version < "1.6.0") warning_type = "Denial of Service" confidence = :medium gem_name = "#{name} gem" message = msg(msg_version(version, gem_name), " has a symbol creation vulnerability. Upgrade to ") if version >= "1.7.0" confidence = :high warning_type = "Remote Code Execution" message = msg(msg_version(version, "json gem"), " has a remote code execution vulnerability. Upgrade to ", msg_version("1.7.7", "json gem")) elsif version >= "1.6.0" message << msg_version("1.6.8", gem_name) elsif version >= "1.5.0" message << msg_version("1.5.5", gem_name) else confidence = :weak message << msg_version("1.5.5", gem_name) end if confidence == :medium and uses_json_parse? confidence = :high end warn :warning_type => warning_type, :warning_code => :CVE_2013_0269, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment(name), :link => "https://groups.google.com/d/topic/rubyonrails-security/4_YvCpLzL58/discussion", :cwe_id => [74] # TODO: is this the best CWE for this? end def uses_json_parse? return @uses_json_parse unless @uses_json_parse.nil? not tracker.find_call(:target => :JSON, :method => :parse).empty? end end ================================================ FILE: lib/brakeman/checks/check_link_to.rb ================================================ require 'brakeman/checks/check_cross_site_scripting' #Checks for calls to link_to in versions of Ruby where link_to did not #escape the first argument. # #See https://rails.lighthouseapp.com/projects/8994/tickets/3518-link_to-doesnt-escape-its-input class Brakeman::CheckLinkTo < Brakeman::CheckCrossSiteScripting Brakeman::Checks.add self @description = "Checks for XSS in link_to in versions before 3.0" def run_check return unless version_between?("2.0.0", "2.9.9") and not tracker.config.escape_html? @ignore_methods = Set[:button_to, :check_box, :escapeHTML, :escape_once, :field_field, :fields_for, :h, :hidden_field, :hidden_field, :hidden_field_tag, :image_tag, :label, :mail_to, :radio_button, :select, :submit_tag, :text_area, :text_field, :text_field_tag, :url_encode, :u, :url_for, :will_paginate].merge tracker.options[:safe_methods] @known_dangerous = [] #Ideally, I think this should also check to see if people are setting #:escape => false @models = tracker.models.keys @inspect_arguments = tracker.options[:check_arguments] tracker.find_call(:target => false, :method => :link_to).each {|call| process_result call} end def process_result result return if duplicate? result #Have to make a copy of this, otherwise it will be changed to #an ignored method call by the code above. call = result[:call] first_arg = call.first_arg second_arg = call.second_arg @matched = false #Skip if no arguments(?) or first argument is a hash return if first_arg.nil? or hash? first_arg if version_between? "2.0.0", "2.2.99" check_argument result, first_arg if second_arg and not hash? second_arg check_argument result, second_arg end elsif second_arg #Only check first argument if there is a second argument #in Rails 2.3.x check_argument result, first_arg end end # Check the argument for possible xss exploits def check_argument result, exp argument = process(exp) !check_user_input(result, argument) && !check_method(result, argument) && !check_matched(result, @matched) end # Check we should warn about the user input def check_user_input(result, argument) input = has_immediate_user_input?(argument) return false unless input message = msg("Unescaped ", msg_input(input), " in ", msg_code("link_to")) warn_xss(result, message, input, :high) end # Check if we should warn about the specified method def check_method(result, argument) return false if tracker.options[:ignore_model_output] match = has_immediate_model?(argument) return false unless match method = match.method return false if IGNORE_MODEL_METHODS.include? method confidence = :medium confidence = :high if likely_model_attribute? match warn_xss(result, msg("Unescaped model attribute in ", msg_code("link_to")), match, confidence) end # Check if we should warn about the matched result def check_matched(result, matched = nil) return false unless matched return false if matched.type == :model and tracker.options[:ignore_model_output] message = msg("Unescaped ", msg_input(matched), " in ", msg_code("link_to")) warn_xss(result, message, @matched, :medium) end # Create a warn for this xss def warn_xss(result, message, user_input, confidence) add_result(result) warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_link_to, :message => message, :user_input => user_input, :confidence => confidence, :link_path => "link_to", :cwe_id => [79] true end def process_call exp @mark = true actually_process_call exp exp end def actually_process_call exp return if @matched target = exp.target target = process target.dup if sexp? target #Bare records create links to the model resource, #not a string that could have injection #TODO: Needs test? I think this is broken? return exp if model_name? target and context == [:call, :arglist] super end end ================================================ FILE: lib/brakeman/checks/check_link_to_href.rb ================================================ require 'brakeman/checks/check_cross_site_scripting' #Checks for calls to link_to which pass in potentially hazardous data #to the second argument. While this argument must be html_safe to not break #the html, it must also be url safe as determined by calling a #:url_safe_method. This prevents attacks such as javascript:evil() or #data: which is html_safe, but not safe as an href #Props to Nick Green for the idea. class Brakeman::CheckLinkToHref < Brakeman::CheckLinkTo Brakeman::Checks.add self @description = "Checks to see if values used for hrefs are sanitized using a :url_safe_method to protect against javascript:/data: XSS" def run_check @ignore_methods = Set[:button_to, :check_box, :field_field, :fields_for, :hidden_field, :hidden_field, :hidden_field_tag, :image_tag, :label, :mail_to, :polymorphic_url, :radio_button, :select, :slice, :submit_tag, :text_area, :text_field, :text_field_tag, :url_encode, :u, :will_paginate].merge(tracker.options[:url_safe_methods] || []) @models = tracker.models.keys @inspect_arguments = tracker.options[:check_arguments] methods = tracker.find_call :target => false, :method => :link_to methods.each do |call| process_result call end end def process_result result call = result[:call] @matched = false url_arg = if result[:block] process call.first_arg else process call.second_arg end if check_argument? url_arg url_arg = url_arg.first_arg end return if call? url_arg and ignore_call? url_arg.target, url_arg.method if input = has_immediate_user_input?(url_arg) message = msg("Unsafe ", msg_input(input), " in ", msg_code("link_to"), " href") unless duplicate? result or call_on_params? url_arg or ignore_interpolation? url_arg, input.match add_result result warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_link_to_href, :message => message, :user_input => input, :confidence => :high, :link_path => "link_to_href", :cwe_id => [79] end elsif not tracker.options[:ignore_model_output] and input = has_immediate_model?(url_arg) return if ignore_model_call? url_arg, input or duplicate? result add_result result message = msg("Potentially unsafe model attribute in ", msg_code("link_to"), " href") warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :xss_link_to_href, :message => message, :user_input => input, :confidence => :weak, :link_path => "link_to_href", :cwe_id => [79] end end CHECK_INSIDE_METHODS = [:url_for, :h, :sanitize] def check_argument? url_arg return unless call? url_arg target = url_arg.target method = url_arg.method CHECK_INSIDE_METHODS.include? method or cgi_escaped? target, method end def ignore_model_call? url_arg, exp return true unless call? exp target = exp.target method = exp.method return true unless model_find_call? target return true unless method.to_s =~ /url|uri|link|page|site/ ignore_call? target, method or IGNORE_MODEL_METHODS.include? method or ignore_interpolation? url_arg, exp end #Ignore situations where the href is an interpolated string #with something before the user input def ignore_interpolation? arg, suspect return unless string_interp? arg return true unless arg[1].chomp.empty? # plain string before interpolation first_interp = arg.find_nodes(:evstr).first return unless first_interp first_interp[1].deep_each do |e| if suspect == e return false end end true end def ignore_call? target, method decorated_model? method or super end def decorated_model? method tracker.config.has_gem? :draper and method == :decorate end def ignored_method? target, method @ignore_methods.include? method or method.to_s =~ /_path$/ or (target.nil? and method.to_s =~ /_url$/) end def model_find_call? exp return unless call? exp MODEL_METHODS.include? exp.method or exp.method.to_s =~ /^find_by_/ end def call_on_params? exp call? exp and params? exp.target and exp.method != :[] end end ================================================ FILE: lib/brakeman/checks/check_mail_to.rb ================================================ require 'brakeman/checks/base_check' #Check for cross-site scripting vulnerability in mail_to :encode => :javascript #with certain versions of Rails (< 2.3.11 or < 3.0.4). # #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/f02a48ede8315f81 class Brakeman::CheckMailTo < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for mail_to XSS vulnerability in certain versions" def run_check if (version_between? "2.3.0", "2.3.10" or version_between? "3.0.0", "3.0.3") and result = mail_to_javascript? message = msg("Vulnerability in ", msg_code("mail_to"), " using javascript encoding ", msg_cve("CVE-2011-0446"), ". Upgrade to ") if version_between? "2.3.0", "2.3.10" message << msg_version("2.3.11") else message << msg_version("3.0.4") end warn :result => result, :warning_type => "Mail Link", :warning_code => :CVE_2011_0446, :message => message, :confidence => :high, :gem_info => gemfile_or_environment, # Probably ignored now :link_path => "https://groups.google.com/d/topic/rubyonrails-security/8CpI7egxX4E/discussion", :cwe_id => [79] end end #Check for javascript encoding of mail_to address # mail_to email, name, :encode => :javascript def mail_to_javascript? Brakeman.debug "Checking calls to mail_to for javascript encoding" tracker.find_call(:target => false, :method => :mail_to).each do |result| result[:call].each_arg do |arg| if hash? arg if option = hash_access(arg, :encode) return result if symbol? option and option.value == :javascript end end end end false end end ================================================ FILE: lib/brakeman/checks/check_mass_assignment.rb ================================================ require 'brakeman/checks/base_check' require 'set' #Checks for mass assignments to models. # #See http://guides.rubyonrails.org/security.html#mass-assignment for details class Brakeman::CheckMassAssignment < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Finds instances of mass assignment" def initialize(*) super @mass_assign_calls = nil end def run_check check_mass_assignment check_permit! check_permit_all_parameters end def find_mass_assign_calls return @mass_assign_calls if @mass_assign_calls models = [] tracker.models.each do |name, m| if m.is_a? Hash p m end if m.unprotected_model? models << name end end return [] if models.empty? Brakeman.debug "Finding possible mass assignment calls on #{models.length} models" @mass_assign_calls = tracker.find_call :chained => true, :targets => models, :methods => [:new, :attributes=, :update_attributes, :update_attributes!, :create, :create!, :build, :first_or_create, :first_or_create!, :first_or_initialize!, :assign_attributes, :update ] end def check_mass_assignment return if mass_assign_disabled? Brakeman.debug "Processing possible mass assignment calls" find_mass_assign_calls.each do |result| process_result result end end #All results should be Model.new(...) or Model.attributes=() calls def process_result res call = res[:call] check = check_call call if check and original? res model = tracker.models[res[:chain].first] attr_protected = (model and model.attr_protected) first_arg = call.first_arg if attr_protected and tracker.options[:ignore_attr_protected] return elsif call? first_arg and (first_arg.method == :slice or first_arg.method == :only) return elsif input = include_user_input?(call.arglist) if not node_type? first_arg, :hash if attr_protected confidence = :medium else confidence = :high end else return end elsif node_type? call.first_arg, :lit, :str return else confidence = :weak input = nil end warn :result => res, :warning_type => "Mass Assignment", :warning_code => :mass_assign_call, :message => "Unprotected mass assignment", :code => call, :user_input => input, :confidence => confidence, :cwe_id => [915] end res end #Want to ignore calls to Model.new that have no arguments def check_call call process_call_args call if call.method == :update arg = call.second_arg else arg = call.first_arg end if arg.nil? #empty new() false elsif hash? arg and not include_user_input? arg false elsif all_literal_args? call false else true end end LITERALS = Set[:lit, :true, :false, :nil, :string] def all_literal_args? exp if call? exp exp.each_arg do |arg| return false unless literal? arg end true else exp.all? do |arg| literal? arg end end end def literal? exp if sexp? exp if exp.node_type == :hash all_literal_args? exp else LITERALS.include? exp.node_type end else true end end # Look for and warn about uses of Parameters#permit! for mass assignment def check_permit! tracker.find_call(:method => :permit!, :nested => true).each do |result| if params? result[:call].target unless inside_safe_method? result or calls_slice? result warn_on_permit! result end end end end # Ignore blah_some_path(params.permit!) def inside_safe_method? result parent_call = result.dig(:parent, :call) call? parent_call and parent_call.method.match(/_path$/) end def calls_slice? result result[:chain].include? :slice or (result[:full_call] and result[:full_call][:chain].include? :slice) end # Look for actual use of params in mass assignment to avoid # warning about uses of Parameters#permit! without any mass assignment # or when mass assignment is restricted by model instead. def subsequent_mass_assignment? result location = result[:location] line = result[:call].line find_mass_assign_calls.any? do |call| call[:location] == location and params? call[:call].first_arg and call[:call].line >= line end end def warn_on_permit! result return unless original? result confidence = if subsequent_mass_assignment? result :high else :medium end warn :result => result, :warning_type => "Mass Assignment", :warning_code => :mass_assign_permit!, :message => msg('Specify exact keys allowed for mass assignment instead of using ', msg_code('permit!'), ' which allows any keys'), :confidence => confidence, :cwe_id => [915] end def check_permit_all_parameters tracker.find_call(target: :"ActionController::Parameters", method: :permit_all_parameters=).each do |result| call = result[:call] if true? call.first_arg warn :result => result, :warning_type => "Mass Assignment", :warning_code => :mass_assign_permit_all, :message => msg('Mass assignment is globally enabled. Disable and specify exact keys using ', msg_code('params.permit'), ' instead'), :confidence => :high, :cwe_id => [915] end end end end ================================================ FILE: lib/brakeman/checks/check_mime_type_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckMimeTypeDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for mime type denial of service (CVE-2016-0751)" def run_check fix_version = case when version_between?("3.0.0", "3.2.22") "3.2.22.1" when version_between?("4.0.0", "4.1.14") "4.1.14.1" when version_between?("4.2.0", "4.2.5") "4.2.5.1" else return end return if has_workaround? message = msg(msg_version(rails_version), " is vulnerable to denial of service via mime type caching ", msg_cve("CVE-2016-0751"), ". Upgrade to ", msg_version(fix_version)) warn :warning_type => "Denial of Service", :warning_code => :CVE_2016_0751, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/9oLY_FCzvoc/w9oI9XxbFQAJ", :cwe_id => [399] end def has_workaround? tracker.find_call(target: :Mime, method: :const_set).any? do |match| arg = match[:call].first_arg symbol? arg and arg.value == :LOOKUP end end end ================================================ FILE: lib/brakeman/checks/check_model_attr_accessible.rb ================================================ require 'brakeman/checks/base_check' # Author: Paul Deardorff (themetric) # Checks models to see if important foreign keys # or attributes are exposed as attr_accessible when # they probably shouldn't be. class Brakeman::CheckModelAttrAccessible < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Reports models which have dangerous attributes defined via attr_accessible" SUSP_ATTRS = [ [:admin, :high], # Very dangerous unless some Rails authorization used [:role, :medium], [:banned, :medium], [:account_id, :high], [/\S*_id(s?)\z/, :weak] # All other foreign keys have weak/low confidence ] def run_check check_models do |name, model| model.attr_accessible.each do |attribute| next if role_limited? model, attribute SUSP_ATTRS.each do |susp_attr, confidence| if susp_attr.is_a?(Regexp) and susp_attr =~ attribute.to_s or susp_attr == attribute warn :model => model, :file => model.file, :warning_type => "Mass Assignment", :warning_code => :dangerous_attr_accessible, :message => "Potentially dangerous attribute available for mass assignment", :confidence => confidence, :code => Sexp.new(:lit, attribute), :cwe_id => [915] break # Prevent from matching single attr multiple times end end end end end def role_limited? model, attribute role_accessible = model.role_accessible return if role_accessible.nil? role_accessible.include? attribute end def check_models tracker.models.each do |name, model| if !model.attr_accessible.nil? yield name, model end end end end ================================================ FILE: lib/brakeman/checks/check_model_attributes.rb ================================================ require 'brakeman/checks/base_check' #Check if mass assignment is used with models #which inherit from ActiveRecord::Base. class Brakeman::CheckModelAttributes < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Reports models which do not use attr_restricted and warns on models that use attr_protected" def run_check return if mass_assign_disabled? or tracker.config.has_gem?(:protected_attributes) #Roll warnings into one warning for all models if tracker.options[:collapse_mass_assignment] Brakeman.alert "The `collapse_mass_assignment` option has been removed." end check_models do |name, model| if model.attr_protected.nil? warn :model => model, :file => model.file, :line => model.top_line, :warning_type => "Attribute Restriction", :warning_code => :no_attr_accessible, :message => msg("Mass assignment is not restricted using ", msg_code("attr_accessible")), :confidence => :high, :cwe_id => [915] # TODO: Should this be mass assignment? elsif not tracker.options[:ignore_attr_protected] message, confidence, link = check_for_attr_protected_bypass if link warning_code = :CVE_2013_0276 else warning_code = :attr_protected_used end warn :model => model, :file => model.file, :line => model.attr_protected.first.line, :warning_type => "Attribute Restriction", :warning_code => warning_code, :message => message, :confidence => confidence, :cwe_id => [915] # TODO: Should this be mass assignment? end end end def check_models tracker.models.each do |name, model| if model.unprotected_model? yield name, model end end end def check_for_attr_protected_bypass upgrade_version = case when version_between?("2.0.0", "2.3.16") "2.3.17" when version_between?("3.0.0", "3.0.99") "3.2.11" when version_between?("3.1.0", "3.1.10") "3.1.11" when version_between?("3.2.0", "3.2.11") "3.2.12" else nil end if upgrade_version message = msg(msg_code("attr_protected"), " is bypassable in ", msg_version(rails_version), ". Use ", msg_code("attr_accessible"), " or upgrade to ", msg_version(upgrade_version)) confidence = :high link = "https://groups.google.com/d/topic/rubyonrails-security/AFBKNY7VSH8/discussion" else message = msg(msg_code("attr_accessible"), " is recommended over ", msg_code("attr_protected")) confidence = :medium link = nil end return message, confidence, link end end ================================================ FILE: lib/brakeman/checks/check_model_serialize.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckModelSerialize < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Report uses of serialize in versions vulnerable to CVE-2013-0277" def run_check @upgrade_version = case when version_between?("2.0.0", "2.3.16") "2.3.17" when version_between?("3.0.0", "3.0.99") "3.2.11" else nil end return unless @upgrade_version tracker.models.each do |_name, model| check_for_serialize model end end #High confidence warning on serialized, unprotected attributes. #Medium confidence warning for serialized, protected attributes. def check_for_serialize model if serialized_attrs = model.options[:serialize] attrs = Set.new serialized_attrs.each do |arglist| arglist.each do |arg| attrs << arg if symbol? arg end end if unsafe_attrs = model.attr_accessible attrs.delete_if { |attr| not unsafe_attrs.include? attr.value } elsif protected_attrs = model.attr_protected safe_attrs = Set.new protected_attrs.each do |arglist| arglist.each do |arg| safe_attrs << arg if symbol? arg end end attrs.delete_if { |attr| safe_attrs.include? attr } end if attrs.empty? confidence = :medium else confidence = :high end warn :model => model, :warning_type => "Remote Code Execution", :warning_code => :CVE_2013_0277, :message => msg("Serialized attributes are vulnerable in ", msg_version(rails_version), ", upgrade to ", msg_version(@upgrade_version), " or patch"), :confidence => confidence, :link => "https://groups.google.com/d/topic/rubyonrails-security/KtmwSbEpzrU/discussion", :file => model.file, :line => model.top_line, :cwe_id => [502] end end end ================================================ FILE: lib/brakeman/checks/check_nested_attributes.rb ================================================ require 'brakeman/checks/base_check' #Check for vulnerability in nested attributes in Rails 2.3.9 and 3.0.0 #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/f9f913d328dafe0c class Brakeman::CheckNestedAttributes < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for nested attributes vulnerability in Rails 2.3.9 and 3.0.0" def run_check version = rails_version if (version == "2.3.9" or version == "3.0.0") and uses_nested_attributes? message = msg("Vulnerability in nested attributes ", msg_cve("CVE-2010-3933"), ". Upgrade to ") if version == "2.3.9" message << msg_version("2.3.10") else message << msg_version("3.0.1") end warn :warning_type => "Nested Attributes", :warning_code => :CVE_2010_3933, :message => message, :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/-fkT0yja_gw/discussion", :cwe_id => [20] end end def uses_nested_attributes? active_record_models.each do |_name, model| return true if model.options[:accepts_nested_attributes_for] end false end end ================================================ FILE: lib/brakeman/checks/check_nested_attributes_bypass.rb ================================================ require 'brakeman/checks/base_check' #https://groups.google.com/d/msg/rubyonrails-security/cawsWcQ6c8g/tegZtYdbFQAJ class Brakeman::CheckNestedAttributesBypass < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for nested attributes vulnerability (CVE-2015-7577)" def run_check if version_between? "3.1.0", "3.2.22" or version_between? "4.0.0", "4.1.14" or version_between? "4.2.0", "4.2.5" unless workaround? check_nested_attributes end end end def check_nested_attributes active_record_models.each do |name, model| if opts = model.options[:accepts_nested_attributes_for] opts.each do |args| if args.any? { |a| allow_destroy? a } and args.any? { |a| reject_if? a } warn_about_nested_attributes model, args end end end end end def warn_about_nested_attributes model, args message = msg(msg_version(rails_version), " does not call ", msg_code(":reject_if"), " option when ", msg_code(":allow_destroy"), " is ", msg_code("false"), " ", msg_cve("CVE-2015-7577")) warn :model => model, :warning_type => "Nested Attributes", :warning_code => :CVE_2015_7577, :message => message, :file => model.file, :line => args.line, :confidence => :medium, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/cawsWcQ6c8g/tegZtYdbFQAJ", :cwe_id => [284] end def allow_destroy? arg hash? arg and false? hash_access(arg, :allow_destroy) end def reject_if? arg hash? arg and hash_access(arg, :reject_if) end def workaround? tracker.find_call(method: :will_be_destroyed?).any? end end ================================================ FILE: lib/brakeman/checks/check_number_to_currency.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckNumberToCurrency < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for number helpers XSS vulnerabilities in certain versions" def initialize(*) super @found_any = false end def run_check if version_between? "2.0.0", "2.3.18" or version_between? "3.0.0", "3.2.16" or version_between? "4.0.0", "4.0.2" return if lts_version? "2.3.18.8" check_number_helper_usage generic_warning unless @found_any end end def generic_warning message = msg(msg_version(rails_version), " has a vulnerability in number helpers ", msg_cve("CVE-2014-0081"), ". Upgrade to ") if version_between? "2.3.0", "3.2.16" message << msg_version("3.2.17") else message << msg_version("4.0.3") end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2014_0081, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/9WiRn2nhfq0/2K2KRB4LwCMJ", :cwe_id => [79] end def check_number_helper_usage number_methods = [:number_to_currency, :number_to_percentage, :number_to_human] tracker.find_call(:target => false, :methods => number_methods).each do |result| arg = result[:call].second_arg next unless arg if not check_helper_option(result, arg) and hash? arg hash_iterate(arg) do |_key, value| break if check_helper_option(result, value) end end end end def check_helper_option result, exp if match = (has_immediate_user_input? exp or has_immediate_model? exp) warn_on_number_helper result, match @found_any = true else false end end def warn_on_number_helper result, match warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2014_0081_call, :message => msg("Format options in ", msg_code(result[:call].method), " are not safe in ", msg_version(rails_version)), :confidence => :high, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/9WiRn2nhfq0/2K2KRB4LwCMJ", :user_input => match, :cwe_id => [79] end end ================================================ FILE: lib/brakeman/checks/check_page_caching_cve.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckPageCachingCVE < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for page caching vulnerability (CVE-2020-8159)" def run_check gem_name = 'actionpack-page_caching' gem_version = tracker.config.gem_version(gem_name.to_sym) upgrade_version = '1.2.2' cve = 'CVE-2020-8159' return unless gem_version and version_between?('0.0.0', '1.2.1', gem_version) message = msg("Directory traversal vulnerability in ", msg_version(gem_version, gem_name), " ", msg_cve(cve), ". Upgrade to ", msg_version(upgrade_version, gem_name)) if uses_caches_page? confidence = :high else confidence = :weak end warn :warning_type => 'Directory Traversal', :warning_code => :CVE_2020_8159, :message => message, :confidence => confidence, :link_path => 'https://groups.google.com/d/msg/rubyonrails-security/CFRVkEytdP8/c5gmICECAgAJ', :gem_info => gemfile_or_environment(gem_name), :cwe_id => [22] end def uses_caches_page? tracker.controllers.any? do |name, controller| controller.options.has_key? :caches_page end end end ================================================ FILE: lib/brakeman/checks/check_pathname.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckPathname < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for unexpected Pathname behavior" def run_check check_rails_root_join check_pathname_join end def check_rails_root_join tracker.find_call(target: :'Rails.root', method: :join, nested: true).each do |result| check_result result end end def check_pathname_join pathname_methods = [ :'Pathname.new', :'Pathname.getwd', :'Pathname.glob', :'Pathname.pwd', ] tracker.find_call(targets: pathname_methods, method: :join, nested: true).each do |result| check_result result end end def check_result result return unless original? result result[:call].each_arg do |arg| if match = has_immediate_user_input?(arg) warn :result => result, :warning_type => "Path Traversal", :warning_code => :pathname_traversal, :message => "Absolute paths in `Pathname#join` cause the entire path to be relative to the absolute path, ignoring any prior values", :user_input => match, :confidence => :high, :cwe_id => [22] end end end end ================================================ FILE: lib/brakeman/checks/check_permit_attributes.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckPermitAttributes < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Warn on potentially dangerous attributes allowed via permit" SUSPICIOUS_KEYS = { admin: :high, account_id: :high, role: :medium, banned: :medium, } def run_check tracker.find_call(:method => :permit).each do |result| check_permit result end end def check_permit result return unless original? result call = result[:call] call.each_arg do |arg| if symbol? arg if SUSPICIOUS_KEYS.key? arg.value warn_on_permit_key result, arg end end end end def warn_on_permit_key result, key, confidence = nil warn :result => result, :warning_type => "Mass Assignment", :warning_code => :dangerous_permit_key, :message => "Potentially dangerous key allowed for mass assignment", :confidence => (confidence || SUSPICIOUS_KEYS[key.value]), :user_input => key, :cwe_id => [915] end end ================================================ FILE: lib/brakeman/checks/check_quote_table_name.rb ================================================ require 'brakeman/checks/base_check' #Check for uses of quote_table_name in Rails versions before 2.3.13 and 3.0.10 #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/6a1e473744bc389b class Brakeman::CheckQuoteTableName < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for quote_table_name vulnerability in versions before 2.3.14 and 3.0.10" def run_check if (version_between?('2.0.0', '2.3.13') or version_between?('3.0.0', '3.0.9')) if uses_quote_table_name? confidence = :high else confidence = :medium end if rails_version =~ /^3/ message = msg("Rails versions before 3.0.10 have a vulnerability in ", msg_code("quote_table_name"), " ", msg_cve("CVE-2011-2930")) else message = msg("Rails versions before 2.3.14 have a vulnerability in ", msg_code("quote_table_name"), " ", msg_cve("CVE-2011-2930")) end warn :warning_type => "SQL Injection", :warning_code => :CVE_2011_2930, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/ah5HN0S8OJs/discussion", :cwe_id => [89] end end def uses_quote_table_name? Brakeman.debug "Finding calls to quote_table_name()" not tracker.find_call(:target => false, :method => :quote_table_name).empty? end end ================================================ FILE: lib/brakeman/checks/check_ransack.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckRansack < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for dangerous use of the Ransack library" def run_check return unless version_between? "0.0.0", "3.99", tracker.config.gem_version(:ransack) check_ransack_calls end def check_ransack_calls tracker.find_call(method: :ransack, nested: true).each do |result| next unless original? result call = result[:call] arg = call.first_arg # If an allow list is defined anywhere in the # class or super classes, consider it safe class_name = result[:chain].first next if ransackable_allow_list?(class_name) if input = has_immediate_user_input?(arg) confidence = if tracker.find_class(class_name).nil? confidence = :low elsif result[:location][:file].relative.include? 'admin' confidence = :medium else confidence = :high end message = msg('Unrestricted search using ', msg_code('ransack'), ' library called with ', msg_input(input), '. Limit search by defining ', msg_code('ransackable_attributes'), ' and ', msg_code('ransackable_associations'), ' methods in class or upgrade Ransack to version 4.0.0 or newer') warn result: result, warning_type: 'Missing Authorization', warning_code: :ransack_search, message: message, user_input: input, confidence: confidence, cwe_id: [862], link: 'https://positive.security/blog/ransack-data-exfiltration' end end end def ransackable_allow_list? class_name tracker.find_method(:ransackable_attributes, class_name, :class) and tracker.find_method(:ransackable_associations, class_name, :class) end end ================================================ FILE: lib/brakeman/checks/check_redirect.rb ================================================ require 'brakeman/checks/base_check' #Reports any calls to +redirect_to+ which include parameters in the arguments. # #For example: # # redirect_to params.merge(:action => :elsewhere) class Brakeman::CheckRedirect < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Looks for calls to redirect_to with user input as arguments" def run_check @model_find_calls = Set[:all, :create, :create!, :find, :find_by_sql, :first, :first!, :last, :last!, :new, :sole] if tracker.options[:rails3] @model_find_calls.merge [:from, :group, :having, :joins, :lock, :order, :reorder, :select, :where] end if version_between? "4.0.0", "9.9.9" @model_find_calls.merge [:find_by, :find_by!, :take] end if version_between? "7.0.0", "9.9.9" @model_find_calls << :find_sole_by end methods = [:redirect_to, :redirect_back, :redirect_back_or_to] @tracker.find_call(:target => false, :methods => methods).each do |res| process_result res end end def process_result result return unless original? result call = result[:call] opt = call.first_arg # Location is specified with `fallback_location:` # otherwise the arguments do not contain a location and # the call can be ignored if call.method == :redirect_back if hash? opt and location = hash_access(opt, :fallback_location) opt = location else return end end if not protected_by_raise?(call) and not only_path?(call) and not explicit_host?(opt) and not slice_call?(opt) and not safe_permit?(opt) and not disallow_other_host?(call) and res = include_user_input?(opt) if res.type == :immediate and not allow_other_host?(call) confidence = :high else confidence = :weak end warn :result => result, :warning_type => "Redirect", :warning_code => :open_redirect, :message => "Possible unprotected redirect", :code => call, :user_input => res, :confidence => confidence, :cwe_id => [601] end end #Custom check for user input. First looks to see if the user input #is being output directly. This is necessary because of tracker.options[:check_arguments] #which can be used to enable/disable reporting output of method calls which use #user input as arguments. def include_user_input? opt, immediate = :immediate Brakeman.debug "Checking if call includes user input" # if the first argument is an array, rails assumes you are building a # polymorphic route, which will never jump off-host return false if array? opt if tracker.options[:ignore_redirect_to_model] if model_instance?(opt) or decorated_model?(opt) return false end end if res = has_immediate_model?(opt) unless call? opt and opt.method.to_s =~ /_path/ return Match.new(immediate, res) end elsif call? opt if request_value? opt return Match.new(immediate, opt) elsif opt.method == :url_for and include_user_input? opt.first_arg return Match.new(immediate, opt) #Ignore helpers like some_model_url? elsif opt.method.to_s =~ /_(url|path)\z/ return false elsif opt.method == :url_from return false end elsif request_value? opt return Match.new(immediate, opt) elsif node_type? opt, :or return (include_user_input?(opt.lhs) or include_user_input?(opt.rhs)) end if tracker.options[:check_arguments] and call? opt include_user_input? opt.first_arg, false #I'm doubting if this is really necessary... else false end end #Checks +redirect_to+ arguments for +only_path => true+ which essentially #nullifies the danger posed by redirecting with user input def only_path? call arg = call.first_arg if hash? arg return has_only_path? arg elsif call? arg and arg.method == :url_for return check_url_for(arg) elsif call? arg and hash? arg.first_arg and use_unsafe_hash_method? arg return has_only_path? arg.first_arg end false end def use_unsafe_hash_method? arg return call_has_param(arg, :to_unsafe_hash) || call_has_param(arg, :to_unsafe_h) end def call_has_param arg, key if call? arg and call? arg.target target = arg.target method = target.method node_type? target.target, :params and method == key else false end end def has_only_path? arg if value = hash_access(arg, :only_path) return true if true?(value) end false end def explicit_host? arg return unless sexp? arg if hash? arg if value = hash_access(arg, :host) return !has_immediate_user_input?(value) end elsif call? arg target = arg.target if hash? target and value = hash_access(target, :host) return !has_immediate_user_input?(value) elsif call? arg return explicit_host? target end end false end #+url_for+ is only_path => true by default. This checks to see if it is #set to false for some reason. def check_url_for call arg = call.first_arg if hash? arg if value = hash_access(arg, :only_path) return false if false?(value) end end true end #Returns true if exp is (probably) a model instance def model_instance? exp if node_type? exp, :or model_instance? exp.lhs or model_instance? exp.rhs elsif call? exp if model_target? exp and (@model_find_calls.include? exp.method or exp.method.to_s.match(/^find_by_/)) true else association?(exp.target, exp.method) end end end def model_target? exp return false unless call? exp model_name? exp.target or friendly_model? exp.target or model_target? exp.target end #Returns true if exp is (probably) a friendly model instance #using the FriendlyId gem def friendly_model? exp call? exp and model_name? exp.target and exp.method == :friendly end #Returns true if exp is (probably) a decorated model instance #using the Draper gem def decorated_model? exp if node_type? exp, :or decorated_model? exp.lhs or decorated_model? exp.rhs else tracker.config.has_gem? :draper and call? exp and node_type?(exp.target, :const) and exp.target.value.to_s.match(/Decorator$/) and exp.method == :decorate end end #Check if method is actually an association in a Model def association? model_name, meth if call? model_name return association? model_name.target, meth elsif model_name? model_name model = tracker.models[class_name(model_name)] else return false end return false unless model model.association? meth end def slice_call? exp return unless call? exp exp.method == :slice end DANGEROUS_KEYS = [:host, :subdomain, :domain, :port] def safe_permit? exp if call? exp and params? exp.target and exp.method == :permit exp.each_arg do |opt| if symbol? opt and DANGEROUS_KEYS.include? opt.value return false end end return true end false end def protected_by_raise? call raise_on_redirects? and not allow_other_host? call end def raise_on_redirects? @raise_on_redirects ||= true?(tracker.config.rails.dig(:action_controller, :raise_on_open_redirects)) end def allow_other_host? call opt = call.last_arg hash? opt and true? hash_access(opt, :allow_other_host) end def disallow_other_host? call opt = call.last_arg hash? opt and false? hash_access(opt, :allow_other_host) end end ================================================ FILE: lib/brakeman/checks/check_regex_dos.rb ================================================ require 'brakeman/checks/base_check' #This check looks for regexes that include user input. class Brakeman::CheckRegexDoS < Brakeman::BaseCheck Brakeman::Checks.add self ESCAPES = { s(:const, :Regexp) => [ :escape, :quote ] } @description = "Searches regexes including user input" #Process calls def run_check Brakeman.debug "Finding dynamic regexes" calls = tracker.find_call :method => [:brakeman_regex_interp] Brakeman.debug "Processing dynamic regexes" calls.each do |call| process_result call end end #Warns if regex includes user input def process_result result return unless original? result call = result[:call] components = call.sexp_body components.any? do |component| next unless sexp? component if match = has_immediate_user_input?(component) confidence = :high elsif match = has_immediate_model?(component) match = Match.new(:model, match) confidence = :medium elsif match = include_user_input?(component) confidence = :weak end if match message = msg(msg_input(match), " used in regular expression") warn :result => result, :warning_type => "Denial of Service", :warning_code => :regex_dos, :message => message, :confidence => confidence, :user_input => match, :cwe_id => [20, 185] end end end def process_call(exp) if escape_methods = ESCAPES[exp.target] if escape_methods.include? exp.method return exp end end super end end ================================================ FILE: lib/brakeman/checks/check_render.rb ================================================ require 'brakeman/checks/base_check' #Check calls to +render()+ for dangerous values class Brakeman::CheckRender < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Finds calls to render that might allow file access or code execution" def run_check tracker.find_call(:target => nil, :method => :render).each do |result| process_render_result result end end def process_render_result result return unless node_type? result[:call], :render case result[:call].render_type when :partial, :template, :action, :file check_for_dynamic_path(result) when :inline when :js when :json when :text when :update when :xml end end #Check if path to action or file is determined dynamically def check_for_dynamic_path result view = result[:call][2] if sexp? view and original? result return if renderable?(view) if input = has_immediate_user_input?(view) if string_interp? view confidence = :medium else confidence = :high end else return end return if input.type == :model #skip models return if safe_param? input.match message = msg("Render path contains ", msg_input(input)) warn :result => result, :warning_type => "Dynamic Render Path", :warning_code => :dynamic_render_path, :message => message, :user_input => input, :confidence => confidence, :cwe_id => [22] end end def safe_param? exp if params? exp and call? exp method_name = exp.method if method_name == :[] arg = exp.first_arg symbol? arg and [:controller, :action].include? arg.value else boolean_method? method_name end end end def renderable? exp return false unless call?(exp) and constant?(exp.target) if exp.method == :with_content exp = exp.target end return false unless constant?(exp.target) target_class_name = class_name(exp.target) known_renderable_class?(target_class_name) or tracker.find_method(:render_in, target_class_name) end def known_renderable_class? class_name klass = tracker.find_class(class_name) return false if klass.nil? knowns = [ :"ViewComponent::Base", :"ViewComponentContrib::Base", :"Phlex::HTML" ] knowns.any? { |k| klass.ancestor? k } end end ================================================ FILE: lib/brakeman/checks/check_render_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckRenderDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Warn about denial of service with render :text (CVE-2014-0082)" def run_check if version_between? "3.0.0", "3.0.20" or version_between? "3.1.0", "3.1.12" or version_between? "3.2.0", "3.2.16" tracker.find_call(:target => nil, :method => :render).each do |result| if text_render? result warn_about_text_render break end end end end def text_render? result node_type? result[:call], :render and result[:call].render_type == :text end def warn_about_text_render message = msg(msg_version(rails_version), " has a denial of service vulnerability ", msg_cve("CVE-2014-0082"), ". Upgrade to ", msg_version("3.2.17")) warn :warning_type => "Denial of Service", :warning_code => :CVE_2014_0082, :message => message, :confidence => :high, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/LMxO_3_eCuc/ozGBEhKaJbIJ", :gem_info => gemfile_or_environment, :cwe_id => [20] end end ================================================ FILE: lib/brakeman/checks/check_render_inline.rb ================================================ class Brakeman::CheckRenderInline < Brakeman::CheckCrossSiteScripting Brakeman::Checks.add self @description = "Checks for cross-site scripting in render calls" def run_check setup tracker.find_call(:target => nil, :method => :render).each do |result| check_render result end end def check_render result return unless original? result call = result[:call] if node_type? call, :render and (call.render_type == :text or call.render_type == :inline) unless call.render_type == :text and content_type_set? call[3] render_value = call[2] if input = has_immediate_user_input?(render_value) warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :cross_site_scripting_inline, :message => msg("Unescaped ", msg_input(input), " rendered inline"), :user_input => input, :confidence => :high, :cwe_id => [79] elsif input = has_immediate_model?(render_value) warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :cross_site_scripting_inline, :message => "Unescaped model attribute rendered inline", :user_input => input, :confidence => :medium, :cwe_id => [79] end end end end CONTENT_TYPES = ["text/html", "text/javascript", "application/javascript"] def content_type_set? opts if hash? opts content_type = hash_access(opts, :content_type) string? content_type and not CONTENT_TYPES.include? content_type.value end end end ================================================ FILE: lib/brakeman/checks/check_render_rce.rb ================================================ require 'brakeman/checks/check_render' class Brakeman::CheckRenderRCE < Brakeman::CheckRender Brakeman::Checks.add self @description = "Finds calls to render that might be vulnerable to CVE-2016-0752" def run_check tracker.find_call(:target => nil, :method => :render).each do |result| process_render_result result end end def process_render_result result return unless node_type? result[:call], :render case result[:call].render_type when :partial, :template, :action, :file check_for_rce(result) end end def check_for_rce result return unless version_between? "0.0.0", "3.2.22" or version_between? "4.0.0", "4.1.14" or version_between? "4.2.0", "4.2.5" view = result[:call][2] if sexp? view and not duplicate? result if params? view and not safe_param? view add_result result warn :result => result, :warning_type => "Remote Code Execution", :warning_code => :dynamic_render_path_rce, :message => msg("Passing query parameters to ", msg_code("render"), " is vulnerable in ", msg_version(rails_version), " ", msg_cve("CVE-2016-0752")), :user_input => view, :confidence => :high, :cwe_id => [22] end end end end ================================================ FILE: lib/brakeman/checks/check_response_splitting.rb ================================================ require 'brakeman/checks/base_check' #Warn about response splitting in Rails versions before 2.3.13 #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/6ffc93bde0298768 class Brakeman::CheckResponseSplitting < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Report response splitting in Rails 2.3.0 - 2.3.13" def run_check if version_between?('2.3.0', '2.3.13') warn :warning_type => "Response Splitting", :warning_code => :CVE_2011_3186, :message => msg("Rails versions before 2.3.14 have a vulnerability content type handling allowing injection of headers ", msg_cve("CVE-2011-3186")), :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/b_yTveAph2g/discussion", :cwe_id => [94] end end end ================================================ FILE: lib/brakeman/checks/check_reverse_tabnabbing.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckReverseTabnabbing < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Checks for reverse tabnabbing cases on 'link_to' calls" def run_check calls = tracker.find_call :methods => :link_to calls.each do |call| process_result call end end def process_result result return unless original? result and result[:call].last_arg html_opts = result[:call].last_arg return unless hash? html_opts target = hash_access html_opts, :target unless target && (string?(target) && target.value == "_blank" || symbol?(target) && target.value == :_blank) return end target_url = result[:block] ? result[:call].first_arg : result[:call].second_arg # `url_for` and `_path` calls lead to urls on to the same origin. # That means that an adversary would need to run javascript on # the victim application's domain. If that is the case, the adversary # already has the ability to redirect the victim user anywhere. # Also statically provided URLs (interpolated or otherwise) are also # ignored as they produce many false positives. return if !call?(target_url) || target_url.method.match(/^url_for$|_path$/) rel = hash_access html_opts, :rel confidence = :medium if rel && string?(rel) then rel_opt = rel.value return if rel_opt.include?("noopener") && rel_opt.include?("noreferrer") if rel_opt.include?("noopener") ^ rel_opt.include?("noreferrer") then confidence = :weak end end warn :result => result, :warning_type => "Reverse Tabnabbing", :warning_code => :reverse_tabnabbing, :message => msg("When opening a link in a new tab without setting ", msg_code('rel: "noopener noreferrer"'), ", the new tab can control the parent tab's location. For example, an attacker could redirect to a phishing page."), :confidence => confidence, :user_input => rel, :cwe_id => [1022] end end ================================================ FILE: lib/brakeman/checks/check_route_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckRouteDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for route DoS (CVE-2015-7581)" def run_check fix_version = case when version_between?("4.0.0", "4.1.14") "4.1.14.1" when version_between?("4.2.0", "4.2.5") "4.2.5.1" else return end if controller_wildcards? message = msg(msg_version(rails_version), " has a denial of service vulnerability with ", msg_code(":controller"), " routes ", msg_cve("CVE-2015-7581"), ". Upgrade to ", msg_version(fix_version)) warn :warning_type => "Denial of Service", :warning_code => :CVE_2015_7581, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/dthJ5wL69JE/YzPnFelbFQAJ", :cwe_id => [399] end end def controller_wildcards? tracker.routes.each do |name, actions| if name == :':controllerController' # awful hack for routes with :controller in them return true elsif string? actions and actions.value.include? ":controller" return true end end false end end ================================================ FILE: lib/brakeman/checks/check_safe_buffer_manipulation.rb ================================================ require 'brakeman/checks/base_check' #Check for unsafe manipulation of strings #Right now this is just a version check for #https://groups.google.com/group/rubyonrails-security/browse_thread/thread/edd28f1e3d04e913?pli=1 class Brakeman::CheckSafeBufferManipulation < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for Rails versions with SafeBuffer bug" def run_check if version_between? "3.0.0", "3.0.11" suggested_version = "3.0.12" elsif version_between? "3.1.0", "3.1.3" suggested_version = "3.1.4" elsif version_between? "3.2.0", "3.2.1" suggested_version = "3.2.2" else return end message = msg(msg_version(rails_version), " has a vulnerability in ", msg_code("SafeBuffer"), ". Upgrade to ", msg_version(suggested_version), " or apply patches") warn :warning_type => "Cross-Site Scripting", :warning_code => :safe_buffer_vuln, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :cwe_id => [79] end end ================================================ FILE: lib/brakeman/checks/check_sanitize_config_cve.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSanitizeConfigCve < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for vunerable uses of sanitize (CVE-2022-32209)" def run_check @specific_warning = false @gem_version = tracker.config.gem_version :'rails-html-sanitizer' if version_between? "0.0.0", "1.4.2", @gem_version check_config check_sanitize_calls check_safe_list_allowed_tags unless @specific_warning # General warning about the vulnerable version cve_warning end end end def cve_warning confidence: :weak, result: nil return if result and not original? result message = msg(msg_version(@gem_version, 'rails-html-sanitizer'), " is vulnerable to cross-site scripting when ", msg_code('select'), " and ", msg_code("style"), " tags are allowed ", msg_cve("CVE-2022-32209") ) unless result message << ". Upgrade to 1.4.3 or newer" end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2022_32209, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment(:'rails-html-sanitizer'), :link_path => "https://groups.google.com/g/rubyonrails-security/c/ce9PhUANQ6s/m/S0fJfnkmBAAJ", :cwe_id => [79], :result => result end # Look for # config.action_view.sanitized_allowed_tags = ["select", "style"] def check_config sanitizer_config = tracker.config.rails.dig(:action_view, :sanitized_allowed_tags) if sanitizer_config and include_both_tags? sanitizer_config @specific_warning = true cve_warning confidence: :high end end # Look for # sanitize ..., tags: ["select", "style"] # and # Rails::Html::SafeListSanitizer.new.sanitize(..., tags: ["select", "style"]) def check_sanitize_calls tracker.find_call(method: :sanitize, target: nil).each do |result| check_tags_option result end tracker.find_call(method: :sanitize, target: :'Rails::Html::SafeListSanitizer.new').each do |result| check_tags_option result end end # Look for # Rails::Html::SafeListSanitizer.allowed_tags = ["select", "style"] def check_safe_list_allowed_tags tracker.find_call(target: :'Rails::Html::SafeListSanitizer', method: :allowed_tags=).each do |result| check_result result, result[:call].first_arg end end private def check_tags_option result options = result[:call].last_arg if options check_result result, hash_access(options, :tags) end end def check_result result, arg if include_both_tags? arg @specific_warning = true cve_warning confidence: :high, result: result end end def include_both_tags? exp return unless sexp? exp has_tag? exp, 'select' and has_tag? exp, 'style' end def has_tag? exp, tag tag_sym = tag.to_sym exp.each_sexp do |e| if string? e and e.value == tag return true elsif symbol? e and e.value == tag_sym return true end end false end end ================================================ FILE: lib/brakeman/checks/check_sanitize_methods.rb ================================================ require 'brakeman/checks/base_check' #sanitize and sanitize_css are vulnerable: #CVE-2013-1855 and CVE-2013-1857 class Brakeman::CheckSanitizeMethods < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions with vulnerable sanitize and sanitize_css" def run_check @fix_version = case when version_between?('2.0.0', '2.3.17') '2.3.18' when version_between?('3.0.0', '3.0.99') '3.2.13' when version_between?('3.1.0', '3.1.11') '3.1.12' when version_between?('3.2.0', '3.2.12') '3.2.13' end if @fix_version check_cve_2013_1855 check_cve_2013_1857 end if tracker.config.has_gem? :'rails-html-sanitizer' check_rails_html_sanitizer end check_cve_2018_8048 end def check_cve_2013_1855 check_for_cve :sanitize_css, :CVE_2013_1855, "https://groups.google.com/d/msg/rubyonrails-security/4_QHo4BqnN8/_RrdfKk12I4J" end def check_cve_2013_1857 check_for_cve :sanitize, :CVE_2013_1857, "https://groups.google.com/d/msg/rubyonrails-security/zAAU7vGTPvI/1vZDWXqBuXgJ" end def check_for_cve method, code, link tracker.find_call(:target => false, :method => method).each do |result| next if duplicate? result add_result result message = msg(msg_version(rails_version), " has a vulnerability in ", msg_code(method), ". Upgrade to ", msg_version(@fix_version), " or patch") warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => code, :message => message, :confidence => :high, :link_path => link, :cwe_id => [79] end end def check_rails_html_sanitizer rhs_version = tracker.config.gem_version(:'rails-html-sanitizer') if version_between? "1.0.0", "1.0.2", rhs_version warn_sanitizer_cve "CVE-2015-7578", "https://groups.google.com/d/msg/rubyonrails-security/uh--W4TDwmI/JbvSRpdbFQAJ", "1.0.3" warn_sanitizer_cve "CVE-2015-7580", "https://groups.google.com/d/msg/rubyonrails-security/uh--W4TDwmI/m_CVZtdbFQAJ", "1.0.3" end if version_between? "1.0.0", "1.0.3", rhs_version warn_sanitizer_cve "CVE-2018-3741", "https://groups.google.com/d/msg/rubyonrails-security/tP7W3kLc5u4/uDy2Br7xBgAJ", "1.0.4" end end def check_cve_2018_8048 if loofah_vulnerable_cve_2018_8048? message = msg(msg_version(tracker.config.gem_version(:loofah), "loofah gem"), " is vulnerable (CVE-2018-8048). Upgrade to 2.2.1") if tracker.find_call(:target => false, :method => :sanitize).any? confidence = :high else confidence = :medium end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2018_8048, :message => message, :gem_info => gemfile_or_environment(:loofah), :confidence => confidence, :link_path => "https://github.com/flavorjones/loofah/issues/144", :cwe_id => [79] end end def loofah_vulnerable_cve_2018_8048? loofah_version = tracker.config.gem_version(:loofah) # 2.2.1 is fix version loofah_version and version_between?("0.0.0", "2.2.0", loofah_version) end def warn_sanitizer_cve cve, link, upgrade_version message = msg(msg_version(tracker.config.gem_version(:'rails-html-sanitizer'), "rails-html-sanitizer"), " is vulnerable ", msg_cve(cve), ". Upgrade to ", msg_version(upgrade_version, "rails-html-sanitizer")) if tracker.find_call(:target => false, :method => :sanitize).any? confidence = :high else confidence = :medium end warn :warning_type => "Cross-Site Scripting", :warning_code => cve.tr('-', '_').to_sym, :message => message, :gem_info => gemfile_or_environment(:'rails-html-sanitizer'), :confidence => confidence, :link_path => link, :cwe_id => [79] end end ================================================ FILE: lib/brakeman/checks/check_secrets.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSecrets < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Checks for secrets stored in source code" def run_check check_constants end def check_constants @warned = Set.new @tracker.constants.each do |constant| name = constant.name_array.last value = constant.value if string? value and not value.value.empty? and looks_like_secret? name match = [name, value, value.line] unless @warned.include? match @warned << match warn :warning_code => :secret_in_source, :warning_type => "Authentication", :message => msg("Hardcoded value for ", msg_code(name), " in source code"), :confidence => :medium, :file => constant.file, :line => constant.line, :cwe_id => [798] end end end end def looks_like_secret? name # REST_AUTH_SITE_KEY is the pepper in Devise name.match(/password|secret|(rest_auth_site|api)_key$/i) end end ================================================ FILE: lib/brakeman/checks/check_select_tag.rb ================================================ require 'brakeman/checks/base_check' #Checks for CVE-2012-3463, unescaped input in :prompt option of select_tag: #https://groups.google.com/d/topic/rubyonrails-security/fV3QUToSMSw/discussion class Brakeman::CheckSelectTag < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Looks for unsafe uses of select_tag() in some versions of Rails 3.x" def run_check if version_between? "3.0.0", "3.0.16" suggested_version = "3.0.17" elsif version_between? "3.1.0", "3.1.7" suggested_version = "3.1.8" elsif version_between? "3.2.0", "3.2.7" suggested_version = "3.2.8" else return end @ignore_methods = Set[:escapeHTML, :escape_once, :h].merge tracker.options[:safe_methods] @message = msg("Upgrade to ", msg_version(suggested_version), ". In ", msg_version(rails_version), " ", msg_code("select_tag"), " is vulnerable ", msg_cve("CVE-2012-3463")) calls = tracker.find_call(:target => nil, :method => :select_tag).select do |result| result[:location][:type] == :template end calls.each do |result| process_result result end end #Check if select_tag is called with user input in :prompt option def process_result result return unless original? result #Only concerned if user input is supplied for :prompt option last_arg = result[:call].last_arg if hash? last_arg prompt_option = hash_access last_arg, :prompt if call? prompt_option and @ignore_methods.include? prompt_option.method return elsif sexp? prompt_option and input = include_user_input?(prompt_option) warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2012_3463, :result => result, :message => @message, :confidence => :high, :user_input => input, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/fV3QUToSMSw/discussion", :cwe_id => [79] end end end end ================================================ FILE: lib/brakeman/checks/check_select_vulnerability.rb ================================================ require 'brakeman/checks/base_check' #Checks for select() helper vulnerability in some versions of Rails 3 #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/9da0c515a6c4664 class Brakeman::CheckSelectVulnerability < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Looks for unsafe uses of select() helper" def run_check if lts_version? "2.3.18.7" return elsif version_between? "3.0.0", "3.0.11" suggested_version = "3.0.12" elsif version_between? "3.1.0", "3.1.3" suggested_version = "3.1.4" elsif version_between? "3.2.0", "3.2.1" suggested_version = "3.2.2" elsif version_between? "2.0.0", "2.3.14" suggested_version = "3 or use options_for_select" else return end @message = msg("Upgrade to ", msg_version(suggested_version), ". In ", msg_version(rails_version), " ", msg_code("select"), " helper is vulnerable") calls = tracker.find_call(:target => nil, :method => :select).select do |result| result[:location][:type] == :template end calls.each do |result| process_result result end end def process_result result return if duplicate? result third_arg = result[:call].third_arg #Check for user input in options parameter if sexp? third_arg and include_user_input? third_arg add_result result if string_interp? third_arg confidence = :medium else confidence = :weak end warn :template => result[:location][:template], :warning_type => "Cross-Site Scripting", :warning_code => :select_options_vuln, :result => result, :message => @message, :confidence => confidence, :cwe_id => [79] end end end ================================================ FILE: lib/brakeman/checks/check_send.rb ================================================ require 'brakeman/checks/base_check' #Checks if user supplied data is passed to send class Brakeman::CheckSend < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for unsafe use of Object#send" def run_check @send_methods = [:send, :try, :__send__, :public_send] Brakeman.debug("Finding instances of #send") calls = tracker.find_call :methods => @send_methods, :nested => true calls.each do |call| process_result call end end def process_result result return unless original? result send_call = get_send result[:call] process_call_args send_call process send_call.target if input = has_immediate_user_input?(send_call.first_arg) warn :result => result, :warning_type => "Dangerous Send", :warning_code => :dangerous_send, :message => "User controlled method execution", :user_input => input, :confidence => :high, :cwe_id => [77] end end # Recursively check call chain for send call def get_send exp if call? exp if @send_methods.include? exp.method return exp else get_send exp.target end end end end ================================================ FILE: lib/brakeman/checks/check_send_file.rb ================================================ require 'brakeman/checks/check_file_access' require 'brakeman/processors/lib/processor_helper' #Checks for user input in send_file() class Brakeman::CheckSendFile < Brakeman::CheckFileAccess Brakeman::Checks.add self @description = "Check for user input in uses of send_file" def run_check Brakeman.debug "Finding all calls to send_file()" methods = tracker.find_call :target => false, :method => :send_file methods.each do |call| process_result call end end end ================================================ FILE: lib/brakeman/checks/check_session_manipulation.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSessionManipulation < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for user input in session keys" def run_check tracker.find_call(:method => :[]=, :target => :session).each do |result| process_result result end end def process_result result return unless original? result index = result[:call].first_arg if input = has_immediate_user_input?(index) if params? index confidence = :high else confidence = :medium end warn :result => result, :warning_type => "Session Manipulation", :warning_code => :session_key_manipulation, :message => msg(msg_input(input), " used as key in session hash"), :user_input => input, :confidence => confidence, :cwe_id => [20] # TODO: what cwe should this be? it seems like it's looking for authz bypass end end end ================================================ FILE: lib/brakeman/checks/check_session_settings.rb ================================================ require 'brakeman/checks/base_check' #Checks for session key length and http_only settings class Brakeman::CheckSessionSettings < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for session key length and http_only settings" def initialize *args super unless tracker.options[:rails3] @session_settings = Sexp.new(:colon2, Sexp.new(:const, :ActionController), :Base) else @session_settings = nil end end def run_check settings = tracker.config.session_settings check_for_issues settings, @app_tree.file_path("config/environment.rb") session_store = @app_tree.file_path("config/initializers/session_store.rb") secret_token = @app_tree.file_path("config/initializers/secret_token.rb") [session_store, secret_token].each do |file| if tracker.initializers[file] and not ignored? file.basename process tracker.initializers[file] end end if tracker.options[:rails4] check_secrets_yaml end end #Looks for ActionController::Base.session = { ... } #in Rails 2.x apps # #and App::Application.config.secret_token = #in Rails 3.x apps # #and App::Application.config.secret_key_base = #in Rails 4.x apps def process_attrasgn exp if not tracker.options[:rails3] and exp.target == @session_settings and exp.method == :session= check_for_issues exp.first_arg, @app_tree.file_path("config/initializers/session_store.rb") end if tracker.options[:rails3] and settings_target?(exp.target) and (exp.method == :secret_token= or exp.method == :secret_key_base=) and string? exp.first_arg warn_about_secret_token exp.line, @app_tree.file_path("config/initializers/secret_token.rb") end exp end #Looks for Rails3::Application.config.session_store :cookie_store, { ... } #in Rails 3.x apps def process_call exp if tracker.options[:rails3] and settings_target?(exp.target) and exp.method == :session_store check_for_rails3_issues exp.second_arg, @app_tree.file_path("config/initializers/session_store.rb") end exp end private def settings_target? exp call? exp and exp.method == :config and node_type? exp.target, :colon2 and exp.target.rhs == :Application end def check_for_issues settings, file if settings and hash? settings if value = (hash_access(settings, :session_http_only) || hash_access(settings, :http_only) || hash_access(settings, :httponly)) if false? value warn_about_http_only value.line, file end end if value = hash_access(settings, :secret) if string? value warn_about_secret_token value.line, file end end end end def check_for_rails3_issues settings, file if settings and hash? settings if value = hash_access(settings, :httponly) if false? value warn_about_http_only value.line, file end end if value = hash_access(settings, :secure) if false? value warn_about_secure_only value.line, file end end end end def check_secrets_yaml secrets_file = @app_tree.file_path("config/secrets.yml") if secrets_file.exists? and not ignored? "secrets.yml" and not ignored? "config/*.yml" yaml = secrets_file.read require 'yaml' begin secrets = YAML.safe_load yaml, aliases: true rescue Psych::SyntaxError, RuntimeError => e Brakeman.alert "#{self.class}: Unable to parse `#{secrets_file}`" Brakeman.debug "Failed to parse #{secrets_file}: #{e.inspect}" return end if secrets && secrets["production"] and secret = secrets["production"]["secret_key_base"] unless secret.include? "<%=" line = yaml.lines.find_index { |l| l.include? secret } + 1 warn_about_secret_token line, @app_tree.file_path(secrets_file) end end end end def warn_about_http_only line, file warn :warning_type => "Session Setting", :warning_code => :http_cookies, :message => "Session cookies should be set to HTTP only", :confidence => :high, :line => line, :file => file, :cwe_id => [1004] end def warn_about_secret_token line, file warn :warning_type => "Session Setting", :warning_code => :session_secret, :message => "Session secret should not be included in version control", :confidence => :high, :line => line, :file => file, :cwe_id => [798] end def warn_about_secure_only line, file warn :warning_type => "Session Setting", :warning_code => :secure_cookies, :message => "Session cookie should be set to secure only", :confidence => :high, :line => line, :file => file, :cwe_id => [614] end def ignored? file [".", "config", "config/initializers"].each do |dir| ignore_file = @app_tree.file_path("#{dir}/.gitignore") if @app_tree.exists? ignore_file input = ignore_file.read return true if input.include? file end end false end end ================================================ FILE: lib/brakeman/checks/check_simple_format.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSimpleFormat < Brakeman::CheckCrossSiteScripting Brakeman::Checks.add self @description = "Checks for simple_format XSS vulnerability (CVE-2013-6416) in certain versions" def initialize *args super @found_any = false end def run_check if version_between? "4.0.0", "4.0.1" @inspect_arguments = true @ignore_methods = Set[:h, :escapeHTML] check_simple_format_usage generic_warning unless @found_any end end def generic_warning message = msg(msg_version(rails_version), " has a vulnerability in ", msg_code("simple_format"), " ", msg_cve("CVE-2013-6416"), ". Upgrade to ", msg_version("4.0.2")) warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2013_6416, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/5ZI1-H5OoIM/ZNq4FoR2GnIJ", :cwe_id => [79] end def check_simple_format_usage tracker.find_call(:target => false, :method => :simple_format).each do |result| @matched = false process_call result[:call] if @matched warn_on_simple_format result, @matched end end end def process_call exp @mark = true actually_process_call exp exp end def warn_on_simple_format result, match return unless original? result @found_any = true warn :result => result, :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2013_6416_call, :message => msg("Values passed to ", msg_code("simple_format"), " are not safe in ", msg_version(rails_version)), :confidence => :high, :link_path => "https://groups.google.com/d/msg/ruby-security-ann/5ZI1-H5OoIM/ZNq4FoR2GnIJ", :user_input => match, :cwe_id => [79] end end ================================================ FILE: lib/brakeman/checks/check_single_quotes.rb ================================================ require 'brakeman/checks/base_check' #Checks for versions which do not escape single quotes. #https://groups.google.com/d/topic/rubyonrails-security/kKGNeMrnmiY/discussion class Brakeman::CheckSingleQuotes < Brakeman::BaseCheck Brakeman::Checks.add self RACK_UTILS = Sexp.new(:colon2, Sexp.new(:const, :Rack), :Utils) @description = "Check for versions which do not escape single quotes (CVE-2012-3464)" def initialize *args super @inside_erb = @inside_util = @inside_html_escape = @uses_rack_escape = false end def run_check return if uses_rack_escape? if version_between? '2.0.0', '2.3.14' message = msg("All Rails 2.x versions do not escape single quotes ", msg_cve("CVE-2012-3464")) else message = msg(msg_version(rails_version), " does not escape single quotes ", msg_cve("CVE-2012-3464"), ". Upgrade to ") case when version_between?('3.0.0', '3.0.16') message << msg_version('3.0.17') when version_between?('3.1.0', '3.1.7') message << msg_version('3.1.8') when version_between?('3.2.0', '3.2.7') message << msg_version('3.2.8') else return end end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2012_3464, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/kKGNeMrnmiY/discussion", :cwe_id => [79] end #Process initializers to see if they use workaround #by replacing Erb::Util.html_escape def uses_rack_escape? @tracker.initializers.each do |_name, src| process src end @uses_rack_escape end #Look for # # class ERB def process_class exp if exp.class_name == :ERB @inside_erb = true process_all exp.body @inside_erb = false end exp end #Look for # # module Util def process_module exp if @inside_erb and exp.module_name == :Util @inside_util = true process_all exp.body @inside_util = false end exp end #Look for # # def html_escape def process_defn exp if @inside_util and exp.method_name == :html_escape @inside_html_escape = true process_all exp.body @inside_html_escape = false end exp end #Look for # # Rack::Utils.escape_html def process_call exp if @inside_html_escape and exp.target == RACK_UTILS and exp.method == :escape_html @uses_rack_escape = true else process exp.target if exp.target end exp end end ================================================ FILE: lib/brakeman/checks/check_skip_before_filter.rb ================================================ require 'brakeman/checks/base_check' #At the moment, this looks for # # skip_before_filter :verify_authenticity_token, :except => [...] # #which is essentially a skip-by-default approach (no actions are checked EXCEPT the #ones listed) versus a enforce-by-default approach (ONLY the actions listed will skip #the check) class Brakeman::CheckSkipBeforeFilter < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Warn when skipping CSRF or authentication checks by default" def run_check tracker.controllers.each do |_name, controller| controller.skip_filters.each do |filter| process_skip_filter filter, controller end end end def process_skip_filter filter, controller case skip_except_value filter when :verify_authenticity_token warn :class => controller.name, #ugh this should be a controller warning, too :warning_type => "Cross-Site Request Forgery", :warning_code => :csrf_blacklist, :message => msg("List specific actions (", msg_code(":only => [..]"), ") when skipping CSRF check"), :code => filter, :confidence => :medium, :file => controller.file, :cwe_id => [352] when :login_required, :authenticate_user!, :require_user warn :controller => controller.name, :warning_code => :auth_blacklist, :warning_type => "Authentication", :message => msg("List specific actions (", msg_code(":only => [..]"), ") when skipping authentication"), :code => filter, :confidence => :medium, :link_path => "authentication_whitelist", :file => controller.file, :cwe_id => [287] end end def skip_except_value filter return false unless call? filter first_arg = filter.first_arg last_arg = filter.last_arg if symbol? first_arg and hash? last_arg if hash_access(last_arg, :except) return first_arg.value end end false end end ================================================ FILE: lib/brakeman/checks/check_sprockets_path_traversal.rb ================================================ class Brakeman::CheckSprocketsPathTraversal < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for CVE-2018-3760" def run_check sprockets_version = tracker.config.gem_version(:sprockets) return unless sprockets_version return if has_workaround? case when version_between?("0.0.0", "2.12.4", sprockets_version) upgrade_version = "2.12.5" confidence = :weak when version_between?("3.0.0", "3.7.1", sprockets_version) upgrade_version = "3.7.2" confidence = :high when version_between?("4.0.0.beta1", "4.0.0.beta7", sprockets_version) upgrade_version = "4.0.0.beta8" confidence = :high else return end message = msg(msg_version(sprockets_version, "sprockets"), " has a path traversal vulnerability ", msg_cve("CVE-2018-3760"), ". Upgrade to ", msg_version(upgrade_version, "sprockets"), " or newer") warn :warning_type => "Path Traversal", :warning_code => :CVE_2018_3760, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment(:sprockets), :link_path => "https://groups.google.com/d/msg/rubyonrails-security/ft_J--l55fM/7roDfQ50BwAJ", :cwe_id => [22, 200] end def has_workaround? false? (tracker.config.rails[:assets] and tracker.config.rails[:assets][:compile]) end end ================================================ FILE: lib/brakeman/checks/check_sql.rb ================================================ require 'brakeman/checks/base_check' #This check tests for find calls which do not use Rails' auto SQL escaping # #For example: # Project.find(:all, :conditions => "name = '" + params[:name] + "'") # # Project.find(:all, :conditions => "name = '#{params[:name]}'") # # User.find_by_sql("SELECT * FROM projects WHERE name = '#{params[:name]}'") class Brakeman::CheckSQL < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for SQL injection" def run_check # Avoid reporting `user_input` on silly values when generating warning. # Note that we retroactively find `user_input` inside the "dangerous" value. @safe_input_attributes.merge IGNORE_METHODS_IN_SQL @sql_targets = [:average, :calculate, :count, :count_by_sql, :delete_all, :destroy_all, :find_by_sql, :maximum, :minimum, :pluck, :sum, :update_all] @sql_targets.concat [:from, :group, :having, :joins, :lock, :order, :reorder, :where] if tracker.options[:rails3] @sql_targets.concat [:find_by, :find_by!, :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :not] if tracker.options[:rails4] if tracker.options[:rails6] @sql_targets.concat [:delete_by, :destroy_by, :rewhere, :reselect] @sql_targets.delete :delete_all @sql_targets.delete :destroy_all end if version_between?("6.1.0", "9.9.9") @sql_targets.delete :order @sql_targets.delete :reorder @sql_targets.delete :pluck end if version_between?("2.0.0", "3.9.9") or tracker.config.rails_version.nil? @sql_targets << :first << :last << :all end if version_between?("2.0.0", "4.0.99") or tracker.config.rails_version.nil? @sql_targets << :find end @connection_calls = [:delete, :execute, :insert, :select_all, :select_one, :select_rows, :select_value, :select_values] if tracker.options[:rails3] @connection_calls.concat [:exec_delete, :exec_insert, :exec_query, :exec_update] else @connection_calls.concat [:add_limit!, :add_offset_limit!, :add_lock!] end @expected_targets = active_record_models.keys + [:connection, :"ActiveRecord::Base", :Arel] Brakeman.debug "Finding possible SQL calls on models" calls = tracker.find_call(:methods => @sql_targets, :nested => true) narrow_targets = [:exists?, :select] calls.concat tracker.find_call(:targets => active_record_models.keys, :methods => narrow_targets, :chained => true) Brakeman.debug "Finding possible SQL calls with no target" calls.concat tracker.find_call(:target => nil, :methods => @sql_targets) Brakeman.debug "Finding possible SQL calls using constantized()" calls.concat tracker.find_call(:methods => @sql_targets).select { |result| constantize_call? result } calls.concat tracker.find_call(:targets => @expected_targets, :methods => @connection_calls, :chained => true).select { |result| connect_call? result } calls.concat tracker.find_call(:target => :Arel, :method => :sql) Brakeman.debug "Finding calls to named_scope or scope" calls.concat find_scope_calls Brakeman.debug "Processing possible SQL calls" calls.each { |call| process_result call } end #Find calls to named_scope() or scope() in models #RP 3 TODO def find_scope_calls scope_calls = [] # Used in pre-3.1.0 versions of Rails ar_scope_calls(:named_scope) do |model, args| call = make_call(nil, :named_scope, args).line(args.line) scope_calls << scope_call_hash(call, model, :named_scope) end # Use in 3.1.0 and later ar_scope_calls(:scope) do |model, args| second_arg = args[2] next unless sexp? second_arg if second_arg.node_type == :iter and node_type? second_arg.block, :block, :call, :safe_call process_scope_with_block(model, args) elsif call? second_arg call = second_arg scope_calls << scope_call_hash(call, model, call.method) else call = make_call(nil, :scope, args).line(args.line) scope_calls << scope_call_hash(call, model, :scope) end end scope_calls end def ar_scope_calls(symbol_name, &block) active_record_models.each do |name, model| model_args = model.options[symbol_name] if model_args model_args.each do |args| yield model, args end end end end def scope_call_hash(call, model, method) { :call => call, :location => { :type => :class, :class => model.name, :file => model.file }, :method => :named_scope } end def process_scope_with_block model, args scope_name = args[1][1] block = args[-1][-1] # Search lambda for calls to query methods if block.node_type == :block find_calls = Brakeman::FindAllCalls.new(tracker) find_calls.process_source(block, :class => model.name, :method => scope_name, :file => model.file) find_calls.calls.each { |call| process_result(call) if @sql_targets.include?(call[:method]) } elsif call? block while call? block process_result :target => block.target, :method => block.method, :call => block, :location => { :type => :class, :class => model.name, :method => scope_name, :file => model.file } block = block.target end end end #Process possible SQL injection sites: # # Model#find # # Model#(named_)scope # # Model#(find|count)_by_sql # # Model#all # ### Rails 3 # # Model#(where|having) # Model#(order|group) # ### Find Options Hash # # Dangerous keys that accept SQL: # # * conditions # * order # * having # * joins # * select # * from # * lock # def process_result result return if duplicate?(result) or result[:call].original_line call = result[:call] method = call.method dangerous_value = case method when :find check_find_arguments call.second_arg when :exists? check_exists call.first_arg when :delete_all, :destroy_all check_find_arguments call.first_arg when :named_scope, :scope check_scope_arguments call when :find_by_sql, :count_by_sql check_by_sql_arguments call.first_arg when :calculate if call.num_args > 2 unsafe_sql?(call.second_arg) or check_find_arguments(call.third_arg) elsif call.num_args > 1 unsafe_sql?(call.second_arg) end when :last, :first, :all check_find_arguments call.first_arg when :average, :count, :maximum, :minimum, :sum if call.num_args > 1 if version_between?("0.0.0", "4.9.9") # In Rails 5+ these do not accept multiple arguments check_find_arguments(call.first_arg) or check_find_arguments(call.second_arg) end else check_find_arguments call.first_arg end when :where, :rewhere, :having, :find_by, :find_by!, :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,:not, :delete_by, :destroy_by check_query_arguments call.arglist when :order, :group, :reorder check_order_arguments call.arglist when :joins check_joins_arguments call.first_arg when :from unsafe_sql? call.first_arg when :lock check_lock_arguments call.first_arg when :pluck unsafe_sql? call.first_arg when :sql unsafe_sql? call.first_arg when :update_all, :select, :reselect check_update_all_arguments call.args when *@connection_calls check_by_sql_arguments call.first_arg else Brakeman.debug "Unhandled SQL method: #{method}" end if dangerous_value add_result result input = include_user_input? dangerous_value if input confidence = :high user_input = input else confidence = :medium user_input = dangerous_value end if result[:call].target and result[:chain] and not @expected_targets.include? result[:chain].first confidence = case confidence when :high :medium when :medium :weak else confidence end end warn :result => result, :warning_type => "SQL Injection", :warning_code => :sql_injection, :message => "Possible SQL injection", :user_input => user_input, :confidence => confidence, :cwe_id => [89] end if check_for_limit_or_offset_vulnerability call.last_arg if include_user_input? call.last_arg confidence = :high else confidence = :weak end warn :result => result, :warning_type => "SQL Injection", :warning_code => :sql_injection_limit_offset, :message => msg("Upgrade to Rails >= 2.1.2 to escape ", msg_code(":limit"), " and ", msg_code("offset"), ". Possible SQL injection"), :confidence => confidence, :cwe_id => [89] end end #The 'find' methods accept a number of different types of parameters: # # * The first argument might be :all, :first, or :last # * The first argument might be an integer ID or an array of IDs # * The second argument might be a hash of options, some of which are # dangerous and some of which are not # * The second argument might contain SQL fragments as values # * The second argument might contain properly parameterized SQL fragments in arrays # * The second argument might contain improperly parameterized SQL fragments in arrays # #This method should only be passed the second argument. def check_find_arguments arg return nil if not sexp? arg or node_type? arg, :lit, :string, :str, :true, :false, :nil unsafe_sql? arg end def check_scope_arguments call scope_arg = call.second_arg #first arg is name of scope node_type?(scope_arg, :iter) ? unsafe_sql?(scope_arg.block) : unsafe_sql?(scope_arg) end def check_query_arguments arg return unless sexp? arg first_arg = arg[1] if node_type? arg, :arglist if arg.length > 2 and string_interp? first_arg # Model.where("blah = ?", blah) return check_string_interp first_arg else arg = first_arg end end if request_value? arg unless call? arg and params? arg.target and [:permit, :slice, :to_h, :to_hash, :symbolize_keys].include? arg.method # Model.where(params[:where]) arg end elsif hash? arg and not kwsplat? arg #This is generally going to be a hash of column names and values, which #would escape the values. But the keys _could_ be user input. check_hash_keys arg elsif node_type? arg, :lit, :str nil elsif node_type? arg, :or check_query_arguments(arg.lhs) or check_query_arguments(arg.rhs) else #Hashes are safe...but we check above for hash, so...? unsafe_sql? arg, :ignore_hash end end #Checks each argument to order/reorder/group for possible SQL. #Anything used with these methods is passed in verbatim. def check_order_arguments args return unless sexp? args if node_type? args, :arglist check_update_all_arguments(args) else unsafe_sql? args end end #find_by_sql and count_by_sql can take either a straight SQL string #or an array with values to bind. def check_by_sql_arguments arg return unless sexp? arg #This is kind of unnecessary, because unsafe_sql? will handle an array #correctly, but might be better to be explicit. array?(arg) ? unsafe_sql?(arg[1]) : unsafe_sql?(arg) end #joins can take a string, hash of associations, or an array of both(?) #We only care about the possible string values. def check_joins_arguments arg return unless sexp? arg and not node_type? arg, :hash, :string, :str if array? arg arg.each do |a| unsafe_arg = check_joins_arguments a return unsafe_arg if unsafe_arg end nil else unsafe_sql? arg end end def check_update_all_arguments args args.each do |arg| unsafe_arg = unsafe_sql? arg return unsafe_arg if unsafe_arg end nil end #Model#lock essentially only cares about strings. But those strings can be #any SQL fragment. This does not apply to all databases. (For those who do not #support it, the lock method does nothing). def check_lock_arguments arg return unless sexp? arg and not node_type? arg, :hash, :array, :string, :str unsafe_sql?(arg, :ignore_hash) end #Check hash keys for user input. #(Seems unlikely, but if a user can control the column names queried, that #could be bad) def check_hash_keys exp hash_iterate(exp) do |key, _value| unless symbol?(key) unsafe_key = unsafe_sql? key return unsafe_key if unsafe_key end end false end #Check an interpolated string for dangerous values. # #This method assumes values interpolated into strings are unsafe by default, #unless safe_value? explicitly returns true. def check_string_interp arg arg.each do |exp| if dangerous = unsafe_string_interp?(exp) return dangerous end end nil end TO_STRING_METHODS = [:chomp, :chop, :lstrip, :rstrip, :scrub, :squish, :strip, :strip_heredoc, :to_s, :tr] #Returns value if interpolated value is not something safe def unsafe_string_interp? exp if node_type? exp, :evstr value = exp.value else value = exp end if not sexp? value nil elsif call? value and TO_STRING_METHODS.include? value.method unsafe_string_interp? value.target elsif call? value and safe_literal_target? value nil else case value.node_type when :or unsafe_string_interp?(value.lhs) || unsafe_string_interp?(value.rhs) when :dstr if dangerous = check_string_interp(value) return dangerous end else if safe_value? value nil elsif string_building? value check_for_string_building value else value end end end end #Checks the given expression for unsafe SQL values. If an unsafe value is #found, returns that value (may be the given _exp_ or a subexpression). # #Otherwise, returns false/nil. def unsafe_sql? exp, ignore_hash = false return unless sexp?(exp) dangerous_value = find_dangerous_value exp, ignore_hash safe_value?(dangerous_value) ? false : dangerous_value end #Check _exp_ for dangerous values. Used by unsafe_sql? def find_dangerous_value exp, ignore_hash case exp.node_type when :lit, :str, :const, :colon2, :true, :false, :nil nil when :array #Assume this is an array like # # ["blah = ? AND thing = ?", ...] # #and check first value unsafe_sql? exp[1] when :dstr check_string_interp exp when :hash if kwsplat? exp and has_immediate_user_input? exp exp elsif not ignore_hash check_hash_values exp else nil end when :if unsafe_sql? exp.then_clause or unsafe_sql? exp.else_clause when :call unless IGNORE_METHODS_IN_SQL.include? exp.method if has_immediate_user_input? exp exp elsif TO_STRING_METHODS.include? exp.method find_dangerous_value exp.target, ignore_hash else check_call exp end end when :or if unsafe = (unsafe_sql?(exp.lhs) || unsafe_sql?(exp.rhs)) unsafe else nil end when :block, :rlist unsafe_sql? exp.last else if has_immediate_user_input? exp exp else nil end end end #Checks hash values associated with these keys: # # * conditions # * order # * having # * joins # * select # * from # * lock def check_hash_values exp hash_iterate(exp) do |key, value| if symbol? key unsafe = case key.value when :conditions, :having, :select check_query_arguments value when :order, :group check_order_arguments value when :joins check_joins_arguments value when :lock check_lock_arguments value when :from unsafe_sql? value else nil end return unsafe if unsafe end end false end def check_for_string_building exp return unless call? exp target = exp.target method = exp.method arg = exp.first_arg if STRING_METHODS.include? method check_str_target_or_arg(target, arg) or check_interp_target_or_arg(target, arg) or check_for_string_building(target) or check_for_string_building(arg) else nil end end def check_str_target_or_arg target, arg if string? target check_string_arg arg elsif string? arg check_string_arg target end end def check_interp_target_or_arg target, arg if string_interp? target or string_interp? arg check_string_arg target and check_string_arg arg end end def check_string_arg exp if safe_value? exp nil elsif string_building? exp check_for_string_building exp elsif string_interp? exp check_string_interp exp elsif call? exp and exp.method == :to_s check_string_arg exp.target else exp end end IGNORE_METHODS_IN_SQL = Set[:id, :merge_conditions, :table_name, :quoted_table_name, :quoted_primary_key, :to_i, :to_f, :sanitize_sql, :sanitize_sql_array, :sanitize_sql_for_assignment, :sanitize_sql_for_conditions, :sanitize_sql_hash, :sanitize_sql_hash_for_assignment, :sanitize_sql_hash_for_conditions, :to_sql, :sanitize, :primary_key, :table_name_prefix, :table_name_suffix, :where_values_hash, :foreign_key, :uuid, :escape, :escape_string, :polymorphic_name ] def ignore_methods_in_sql @ignore_methods_in_sql ||= IGNORE_METHODS_IN_SQL + (tracker.options[:sql_safe_methods] || []) end def safe_value? exp return true unless sexp? exp case exp.node_type when :str, :lit, :const, :colon2, :nil, :true, :false true when :call if exp.method == :to_s or exp.method == :to_sym safe_value? exp.target else ignore_call? exp end when :if safe_value? exp.then_clause and safe_value? exp.else_clause when :block, :rlist safe_value? exp.last when :or safe_value? exp.lhs and safe_value? exp.rhs when :dstr not unsafe_string_interp? exp else false end end def ignore_call? exp return unless call? exp ignore_methods_in_sql.include? exp.method or quote_call? exp or arel? exp or exp.method.to_s.end_with? "_id" or number_target? exp or date_target? exp or locale_call? exp end QUOTE_METHODS = [:quote, :quote_column_name, :quoted_date, :quote_string, :quote_table_name] def quote_call? exp if call? exp.target exp.target.method == :connection and QUOTE_METHODS.include? exp.method elsif exp.target.nil? exp.method == :quote_value end end AREL_METHODS = [:all, :and, :arel_table, :as, :eq, :eq_any, :exists, :group, :gt, :gteq, :having, :in, :join_sources, :limit, :lt, :lteq, :not, :not_eq, :on, :or, :order, :project, :skip, :take, :where, :with] def arel? exp call? exp and (AREL_METHODS.include? exp.method or arel? exp.target) end #Check call for string building def check_call exp return unless call? exp unsafe = check_for_string_building exp if unsafe unsafe elsif call? exp.target check_call exp.target else nil end end def check_exists arg if call? arg and arg.method == :to_s false else check_find_arguments arg end end #Prior to Rails 2.1.1, the :offset and :limit parameters were not #escaping input properly. # #http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/ def check_for_limit_or_offset_vulnerability options return false if rails_version.nil? or rails_version >= "2.1.1" or not hash?(options) return true if hash_access(options, :limit) or hash_access(options, :offset) false end #Look for something like this: # # params[:x].constantize.find('something') # # s(:call, # s(:call, # s(:call, # s(:call, nil, :params, s(:arglist)), # :[], # s(:arglist, s(:lit, :x))), # :constantize, # s(:arglist)), # :find, # s(:arglist, s(:str, "something"))) def constantize_call? result call = result[:call] call? call.target and call.target.method == :constantize end SELF_CLASS = s(:call, s(:self), :class) def connect_call? result call = result[:call] target = call.target if call? target and target.method == :connection target = target.target klass = class_name(target) target.nil? or target == SELF_CLASS or node_type? target, :self or klass == :"ActiveRecord::Base" or active_record_models.include? klass end end def number_target? exp return unless call? exp if number? exp.target true elsif call? exp.target number_target? exp.target else false end end DATE_CLASS = s(:const, :Date) def date_target? exp return unless call? exp if exp.target == DATE_CLASS true elsif call? exp.target date_target? exp.target else false end end end ================================================ FILE: lib/brakeman/checks/check_sql_cves.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSQLCVEs < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for several SQL CVEs" def run_check check_rails_versions_against_cve_issues check_cve_2014_0080 end def check_rails_versions_against_cve_issues issues = [ { :cve => "CVE-2012-2660", :versions => [%w[2.0.0 2.3.14 2.3.17], %w[3.0.0 3.0.12 3.0.13], %w[3.1.0 3.1.4 3.1.5], %w[3.2.0 3.2.3 3.2.4]], :url => "https://groups.google.com/d/topic/rubyonrails-security/8SA-M3as7A8/discussion" }, { :cve => "CVE-2012-2661", :versions => [%w[3.0.0 3.0.12 3.0.13], %w[3.1.0 3.1.4 3.1.5], %w[3.2.0 3.2.3 3.2.5]], :url => "https://groups.google.com/d/topic/rubyonrails-security/dUaiOOGWL1k/discussion" }, { :cve => "CVE-2012-2695", :versions => [%w[2.0.0 2.3.14 2.3.15], %w[3.0.0 3.0.13 3.0.14], %w[3.1.0 3.1.5 3.1.6], %w[3.2.0 3.2.5 3.2.6]], :url => "https://groups.google.com/d/topic/rubyonrails-security/l4L0TEVAz1k/discussion" }, { :cve => "CVE-2012-5664", :versions => [%w[2.0.0 2.3.14 2.3.15], %w[3.0.0 3.0.17 3.0.18], %w[3.1.0 3.1.8 3.1.9], %w[3.2.0 3.2.9 3.2.18]], :url => "https://groups.google.com/d/topic/rubyonrails-security/DCNTNp_qjFM/discussion" }, { :cve => "CVE-2013-0155", :versions => [%w[2.0.0 2.3.15 2.3.16], %w[3.0.0 3.0.18 3.0.19], %w[3.1.0 3.1.9 3.1.10], %w[3.2.0 3.2.10 3.2.11]], :url => "https://groups.google.com/d/topic/rubyonrails-security/c7jT-EeN9eI/discussion" }, { :cve => "CVE-2016-6317", :versions => [%w[4.2.0 4.2.7.0 4.2.7.1]], :url => "https://groups.google.com/d/msg/ruby-security-ann/WccgKSKiPZA/9DrsDVSoCgAJ" }, ] unless lts_version? '2.3.18.6' issues << { :cve => "CVE-2013-6417", :versions => [%w[2.0.0 3.2.15 3.2.16], %w[4.0.0 4.0.1 4.0.2]], :url => "https://groups.google.com/d/msg/ruby-security-ann/niK4drpSHT4/g8JW8ZsayRkJ" } end if tracker.config.has_gem? :pg issues << { :cve => "CVE-2014-3482", :versions => [%w[2.0.0 2.9.9 3.2.19], %w[3.0.0 3.2.18 3.2.19], %w[4.0.0 4.0.6 4.0.7], %w[4.1.0 4.1.2 4.1.3]], :url => "https://groups.google.com/d/msg/rubyonrails-security/wDxePLJGZdI/WP7EasCJTA4J" } << { :cve => "CVE-2014-3483", :versions => [%w[2.0.0 2.9.9 3.2.19], %w[3.0.0 3.2.18 3.2.19], %w[4.0.0 4.0.6 4.0.7], %w[4.1.0 4.1.2 4.1.3]], :url => "https://groups.google.com/d/msg/rubyonrails-security/wDxePLJGZdI/WP7EasCJTA4J" } end issues.each do |cve_issue| cve_warning_for cve_issue[:versions], cve_issue[:cve], cve_issue[:url] end end def cve_warning_for versions, cve, link upgrade_version = upgrade_version? versions return unless upgrade_version code = cve.tr('-', '_').to_sym warn :warning_type => 'SQL Injection', :warning_code => code, :message => msg(msg_version(rails_version), " contains a SQL injection vulnerability ", msg_cve(cve), ". Upgrade to ", msg_version(upgrade_version)), :confidence => :high, :gem_info => gemfile_or_environment, :link_path => link, :cwe_id => [89] end def upgrade_version? versions versions.each do |low, high, upgrade| return upgrade if version_between? low, high end false end def check_cve_2014_0080 return unless version_between? "4.0.0", "4.0.2" and @tracker.config.has_gem? :pg warn :warning_type => 'SQL Injection', :warning_code => :CVE_2014_0080, :message => msg(msg_version(rails_version), " contains a SQL injection vulnerability ", msg_cve("CVE-2014-0080"), " with PostgreSQL. Upgrade to ", msg_version("4.0.3")), :confidence => :high, :gem_info => gemfile_or_environment(:pg), :link_path => "https://groups.google.com/d/msg/rubyonrails-security/Wu96YkTUR6s/pPLBMZrlwvYJ", :cwe_id => [89] end end ================================================ FILE: lib/brakeman/checks/check_ssl_verify.rb ================================================ require 'brakeman/checks/base_check' # Checks if verify_mode= is called with OpenSSL::SSL::VERIFY_NONE class Brakeman::CheckSSLVerify < Brakeman::BaseCheck Brakeman::Checks.add self SSL_VERIFY_NONE = s(:colon2, s(:colon2, s(:const, :OpenSSL), :SSL), :VERIFY_NONE) @description = "Checks for OpenSSL::SSL::VERIFY_NONE" def run_check check_open_ssl_verify_none check_http_start end def check_open_ssl_verify_none tracker.find_call(:method => :verify_mode=).each {|call| process_verify_mode_result(call) } end def process_verify_mode_result result if result[:call].last_arg == SSL_VERIFY_NONE warn_about_ssl_verification_bypass result end end def check_http_start tracker.find_call(:target => :'Net::HTTP', :method => :start).each { |call| process_http_start_result call } end def process_http_start_result result arg = result[:call].last_arg if hash? arg and hash_access(arg, :verify_mode) == SSL_VERIFY_NONE warn_about_ssl_verification_bypass result end end def warn_about_ssl_verification_bypass result return unless original? result warn :result => result, :warning_type => "SSL Verification Bypass", :warning_code => :ssl_verification_bypass, :message => "SSL certificate verification was bypassed", :confidence => :high, :cwe_id => [295] end end ================================================ FILE: lib/brakeman/checks/check_strip_tags.rb ================================================ require 'brakeman/checks/base_check' #Check for uses of strip_tags in Rails versions before 3.0.17, 3.1.8, 3.2.8 (including 2.3.x): #https://groups.google.com/d/topic/rubyonrails-security/FgVEtBajcTY/discussion # #Check for uses of strip_tags in Rails versions before 2.3.13 and 3.0.10: #http://groups.google.com/group/rubyonrails-security/browse_thread/thread/2b9130749b74ea12 # #Check for user of strip_tags with rails-html-sanitizer 1.0.2: #https://groups.google.com/d/msg/rubyonrails-security/OU9ugTZcbjc/PjEP46pbFQAJ class Brakeman::CheckStripTags < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Report strip_tags vulnerabilities" def run_check if uses_strip_tags? cve_2011_2931 cve_2012_3465 end cve_2015_7579 end def cve_2011_2931 if version_between?('2.0.0', '2.3.12') or version_between?('3.0.0', '3.0.9') if rails_version =~ /^3/ message = msg("Versions before 3.0.10 have a vulnerability in ", msg_code("strip_tags"), " ", msg_cve("CVE-2011-2931")) else message = msg("Versions before 2.3.13 have a vulnerability in ", msg_code("strip_tags"), " ", msg_cve("CVE-2011-2931")) end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2011_2931, :message => message, :gem_info => gemfile_or_environment, :confidence => :high, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/K5EwdJt06hI/discussion", :cwe_id => [79] end end def cve_2012_3465 message = msg(msg_version(rails_version), " has a vulnerability in ", msg_code("strip_tags"), " ", msg_cve("CVE-2012-3465"), ". Upgrade to ") case when (version_between?('2.0.0', '2.3.14') and tracker.config.escape_html?) message = msg("All Rails 2.x versions have a vulnerability in ", msg_code("strip_tags"), " ", msg_cve("CVE-2012-3465")) when version_between?('3.0.10', '3.0.16') message << msg_version('3.0.17') when version_between?('3.1.0', '3.1.7') message << msg_version('3.1.8') when version_between?('3.2.0', '3.2.7') message << msg_version('3.2.8') else return end warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2012_3465, :message => message, :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/FgVEtBajcTY/discussion", :cwe_id => [79] end def cve_2015_7579 if tracker.config.gem_version(:'rails-html-sanitizer') == '1.0.2' if uses_strip_tags? confidence = :high else confidence = :medium end message = msg(msg_version("1.0.2", "rails-html-sanitizer"), " is vulnerable (CVE-2015-7579). Upgrade to ", msg_version("1.0.3", "rails-html-sanitizer")) warn :warning_type => "Cross-Site Scripting", :warning_code => :CVE_2015_7579, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment(:"rails-html-sanitizer"), :link_path => "https://groups.google.com/d/msg/rubyonrails-security/OU9ugTZcbjc/PjEP46pbFQAJ", :cwe_id => [79] end end def uses_strip_tags? Brakeman.debug "Finding calls to strip_tags()" not tracker.find_call(:target => false, :method => :strip_tags, :nested => true).empty? end end ================================================ FILE: lib/brakeman/checks/check_symbol_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSymbolDoS < Brakeman::BaseCheck Brakeman::Checks.add_optional self UNSAFE_METHODS = [:to_sym, :literal_to_sym, :intern, :symbolize_keys, :symbolize_keys!] @description = "Checks for symbol denial of service" def run_check return if rails_version and rails_version >= "5.0.0" return if tracker.config.ruby_version and tracker.config.ruby_version >= "2.2" tracker.find_call(:methods => UNSAFE_METHODS, :nested => true).each do |result| check_unsafe_symbol_creation(result) end end def check_unsafe_symbol_creation result return unless original? result call = result[:call] if result[:method] == :literal_to_sym args = call.select { |e| sexp? e } else args = [call.target] end if input = args.map{ |arg| has_immediate_user_input?(arg) }.compact.first confidence = :high elsif input = args.map{ |arg| include_user_input?(arg) }.compact.first confidence = :medium end if confidence return if safe_parameter? input.match return if symbolizing_attributes? input message = msg("Symbol conversion from unsafe string in ", msg_input(input)) warn :result => result, :warning_type => "Denial of Service", :warning_code => :unsafe_symbol_creation, :message => message, :user_input => input, :confidence => confidence, :cwe_id => [20] end end def safe_parameter? input if call? input if node_type? input.target, :params input.method == :[] and symbol? input.first_arg and [:controller, :action].include? input.first_arg.value else safe_parameter? input.target end else false end end def symbolizing_attributes? input input.type == :model and call? input.match and input.match.method == :attributes end end ================================================ FILE: lib/brakeman/checks/check_symbol_dos_cve.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckSymbolDoSCVE < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for versions with ActiveRecord symbol denial of service vulnerability" def run_check fix_version = case when version_between?('2.0.0', '2.3.17') '2.3.18' when version_between?('3.1.0', '3.1.11') '3.1.12' when version_between?('3.2.0', '3.2.12') '3.2.13' else nil end if fix_version && active_record_models.any? warn :warning_type => "Denial of Service", :warning_code => :CVE_2013_1854, :message => msg(msg_version(rails_version), " has a denial of service vulnerability in ActiveRecord. Upgrade to ", msg_version(fix_version), " or patch"), :confidence => :medium, :gem_info => gemfile_or_environment, :link => "https://groups.google.com/d/msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ", :cwe_id => [20] end end end ================================================ FILE: lib/brakeman/checks/check_template_injection.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckTemplateInjection < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Searches for evaluation of user input through template injection" #Process calls def run_check Brakeman.debug "Finding ERB.new calls" erb_calls = tracker.find_call :target => :ERB, :method => :new, :nested => true Brakeman.debug "Processing ERB.new calls" erb_calls.each do |call| process_result call end end #Warns if eval includes user input def process_result result return unless original? result if input = include_user_input?(result[:call].arglist) warn :result => result, :warning_type => "Template Injection", :warning_code => :erb_template_injection, :message => msg(msg_input(input), " used directly in ", msg_code("ERB"), " template, which might enable remote code execution"), :user_input => input, :confidence => :high, :cwe_id => [1336] end end end ================================================ FILE: lib/brakeman/checks/check_translate_bug.rb ================================================ require 'brakeman/checks/base_check' #Check for vulnerability in translate() helper that allows cross-site scripting class Brakeman::CheckTranslateBug < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Report XSS vulnerability in translate helper" def run_check return if lts_version? '2.3.18.6' if (version_between?('2.3.0', '2.3.99') and tracker.config.escape_html?) or version_between?('3.0.0', '3.0.10') or version_between?('3.1.0', '3.1.1') confidence = if uses_translate? :high else :medium end description = [" has a vulnerability in the translate helper with keys ending in ", msg_code("_html")] message = if rails_version =~ /^3\.1/ msg(msg_version(rails_version), *description, ". Upgrade to ", msg_version("3.1.2")) elsif rails_version =~ /^3\.0/ msg(msg_version(rails_version), *description, ". Upgrade to ", msg_version("3.0.11")) else msg("Rails 2.3.x using the rails_xss plugin", *description) end warn :warning_type => "Cross-Site Scripting", :warning_code => :translate_vuln, :message => message, :confidence => confidence, :gem_info => gemfile_or_environment, :link_path => "http://groups.google.com/group/rubyonrails-security/browse_thread/thread/2b61d70fb73c7cc5", :cwe_id => [79] end end def uses_translate? Brakeman.debug "Finding calls to translate() or t()" tracker.find_call(:target => nil, :methods => [:t, :translate]).any? end end ================================================ FILE: lib/brakeman/checks/check_unsafe_reflection.rb ================================================ require 'brakeman/checks/base_check' # Checks for string interpolation and parameters in calls to # String#constantize, String#safe_constantize, Module#const_get and Module#qualified_const_get. # # Exploit examples at: http://blog.conviso.com.br/exploiting-unsafe-reflection-in-rubyrails-applications/ class Brakeman::CheckUnsafeReflection < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for unsafe reflection" def run_check reflection_methods = [:constantize, :safe_constantize, :const_get, :qualified_const_get] tracker.find_call(:methods => reflection_methods, :nested => true).each do |result| check_unsafe_reflection result end end def check_unsafe_reflection result return unless original? result call = result[:call] method = call.method case method when :constantize, :safe_constantize arg = call.target else arg = call.first_arg end if input = has_immediate_user_input?(arg) confidence = :high elsif input = include_user_input?(arg) confidence = :medium end if confidence case method when :constantize, :safe_constantize message = msg("Unsafe reflection method ", msg_code(method), " called on ", msg_input(input)) else message = msg("Unsafe reflection method ", msg_code(method), " called with ", msg_input(input)) end warn :result => result, :warning_type => "Remote Code Execution", :warning_code => :unsafe_constantize, :message => message, :user_input => input, :confidence => confidence, :cwe_id => [470] end end end ================================================ FILE: lib/brakeman/checks/check_unsafe_reflection_methods.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckUnsafeReflectionMethods < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for unsafe reflection to access methods" def run_check check_method check_tap check_to_proc end def check_method tracker.find_call(method: :method, nested: true).each do |result| argument = result[:call].first_arg if user_input = include_user_input?(argument) warn_unsafe_reflection(result, user_input) end end end def check_tap tracker.find_call(method: :tap, nested: true).each do |result| argument = result[:call].first_arg # Argument is passed like a.tap(&argument) if node_type? argument, :block_pass argument = argument.value end if user_input = include_user_input?(argument) warn_unsafe_reflection(result, user_input) end end end def check_to_proc tracker.find_call(method: :to_proc, nested: true).each do |result| target = result[:call].target if user_input = include_user_input?(target) warn_unsafe_reflection(result, user_input) end end end def warn_unsafe_reflection result, input return unless original? result method = result[:call].method confidence = if input.type == :params :high else :medium end message = msg("Unsafe reflection method ", msg_code(method), " called with ", msg_input(input)) warn :result => result, :warning_type => "Remote Code Execution", :warning_code => :unsafe_method_reflection, :message => message, :user_input => input, :confidence => confidence, :cwe_id => [470] end end ================================================ FILE: lib/brakeman/checks/check_unscoped_find.rb ================================================ require 'brakeman/checks/base_check' # Checks for unscoped calls to models' #find and #find_by_id methods. class Brakeman::CheckUnscopedFind < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Check for unscoped ActiveRecord queries" def run_check Brakeman.debug("Finding instances of #find on models with associations") associated_model_names = active_record_models.keys.select do |name| if belongs_to = active_record_models[name].associations[:belongs_to] not optional_belongs_to? belongs_to else false end end calls = tracker.find_call :method => [:find, :find_by_id, :find_by_id!], :targets => associated_model_names calls.each do |call| process_result call end tracker.find_call(:methods => [:find_by, :find_by!], :targets => associated_model_names).each do |result| arg = result[:call].first_arg if hash? arg and hash_access(arg, :id) process_result result end end end def process_result result return if duplicate? result or result[:call].original_line # Not interested unless argument is user controlled. inputs = result[:call].args.map { |arg| include_user_input?(arg) } return unless input = inputs.compact.first add_result result warn :result => result, :warning_type => "Unscoped Find", :warning_code => :unscoped_find, :message => msg("Unscoped call to ", msg_code("#{result[:target]}##{result[:method]}")), :code => result[:call], :confidence => :weak, :user_input => input, :cwe_id => [285] end def optional_belongs_to? exp return false unless exp.is_a? Array exp.each do |e| if hash? e and true? hash_access(e, :optional) return true end end false end end ================================================ FILE: lib/brakeman/checks/check_validation_regex.rb ================================================ require 'brakeman/checks/base_check' #Reports any calls to +validates_format_of+ which do not use +\A+ and +\z+ #as anchors in the given regular expression. # #For example: # # #Allows anything after new line # validates_format_of :user_name, :with => /^\w+$/ class Brakeman::CheckValidationRegex < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Report uses of validates_format_of with improper anchors" WITH = Sexp.new(:lit, :with) FORMAT = Sexp.new(:lit, :format) def run_check active_record_models.each do |name, model| @current_model = model format_validations = model.options[:validates_format_of] if format_validations format_validations.each do |v| process_validates_format_of v end end validates = model.options[:validates] if validates validates.each do |v| process_validates v end end end end #Check validates_format_of def process_validates_format_of validator if value = hash_access(validator.last, WITH) check_regex value, validator end end #Check validates ..., :format => ... def process_validates validator hash_arg = validator.last return unless hash? hash_arg value = hash_access(hash_arg, FORMAT) if hash? value value = hash_access(value, WITH) end if value check_regex value, validator end end # Match secure regexp without extended option SECURE_REGEXP_PATTERN = %r{ \A \\A .* \\[zZ] \z }x # Match secure of regexp with extended option EXTENDED_SECURE_REGEXP_PATTERN = %r{ \A \s* \\A .* \\[zZ] \s* \z }mx #Issue warning if the regular expression does not use #+\A+ and +\z+ def check_regex value, validator return unless regexp? value regex = value.value unless secure_regex?(regex) warn :model => @current_model, :warning_type => "Format Validation", :warning_code => :validation_regex, :message => msg("Insufficient validation for ", msg_code(get_name validator), " using ", msg_code(regex.inspect), ". Use ", msg_code("\\A"), " and ", msg_code("\\z"), " as anchors"), :line => value.line, :confidence => :high, :cwe_id => [777] end end #Get the name of the attribute being validated. def get_name validator name = validator[1] if sexp? name name.value else name end end private def secure_regex?(regex) extended_regex = Regexp::EXTENDED == regex.options & Regexp::EXTENDED regex_pattern = extended_regex ? EXTENDED_SECURE_REGEXP_PATTERN : SECURE_REGEXP_PATTERN regex_pattern =~ regex.source end end ================================================ FILE: lib/brakeman/checks/check_verb_confusion.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckVerbConfusion < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for uses of `request.get?` that might have unintentional behavior" #Process calls def run_check calls = tracker.find_call(target: :request, methods: [:get?]) calls.each do |call| process_result call end end def process_result result @current_result = result @matched_call = result[:call] klass = tracker.find_class(result[:location][:class]) # TODO: abstract into tracker.find_location ? if klass.nil? Brakeman.debug "No class found: #{result[:location][:class]}" return end method = klass.get_method(result[:location][:method]) if method.nil? Brakeman.debug "No method found: #{result[:location][:method]}" return end process method.src end def process_if exp if exp.condition == @matched_call # Found `if request.get?` # Do not warn if there is an `elsif` clause if node_type? exp.else_clause, :if return exp end warn_about_result @current_result, exp end exp end def warn_about_result result, code return unless original? result confidence = :weak message = msg('Potential HTTP verb confusion. ', msg_code('HEAD'), ' is routed like ', msg_code('GET'), ' but ', msg_code('request.get?'), ' will return ', msg_code('false') ) warn :result => result, :warning_type => "HTTP Verb Confusion", :warning_code => :http_verb_confusion, :message => message, :code => code, :user_input => result[:call], :confidence => confidence, :cwe_id => [352] end end ================================================ FILE: lib/brakeman/checks/check_weak_hash.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckWeakHash < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "Checks for use of weak hashes like MD5" DIGEST_CALLS = [:base64digest, :digest, :hexdigest, :new] def run_check tracker.find_call(:targets => [:'Digest::MD5', :'Digest::SHA1', :'OpenSSL::Digest::MD5', :'OpenSSL::Digest::SHA1'], :nested => true).each do |result| process_hash_result result end tracker.find_call(:target => :'Digest::HMAC', :methods => [:new, :hexdigest], :nested => true).each do |result| process_hmac_result result end tracker.find_call(:targets => [:'OpenSSL::Digest::Digest', :'OpenSSL::Digest'], :method => :new).each do |result| process_openssl_result result end end def process_hash_result result return unless original? result input = nil call = result[:call] if DIGEST_CALLS.include? call.method if input = user_input_as_arg?(call) confidence = :high elsif input = hashing_password?(call) confidence = :high else confidence = :medium end else confidence = :medium end message = msg("Weak hashing algorithm used") case call.target.last when :MD5 message << ": " << msg_lit("MD5") when :SHA1 message << ": " << msg_lit("SHA1") end warn :result => result, :warning_type => "Weak Hash", :warning_code => :weak_hash_digest, :message => message, :confidence => confidence, :user_input => input, :cwe_id => [328] end def process_hmac_result result return unless original? result call = result[:call] message = msg("Weak hashing algorithm used in HMAC") case call.third_arg.last when :MD5 message << ": " << msg_lit("MD5") when :SHA1 message << ": " << msg_lit("SHA1") end warn :result => result, :warning_type => "Weak Hash", :warning_code => :weak_hash_hmac, :message => message, :confidence => :medium, :cwe_id => [328] end def process_openssl_result result return unless original? result arg = result[:call].first_arg if string? arg alg = arg.value.upcase if alg == 'MD5' or alg == 'SHA1' warn :result => result, :warning_type => "Weak Hash", :warning_code => :weak_hash_digest, :message => msg("Weak hashing algorithm used: ", msg_lit(alg)), :confidence => :medium, :cwe_id => [328] end end end def user_input_as_arg? call call.each_arg do |arg| if input = include_user_input?(arg) return input end end nil end def hashing_password? call call.each_arg do |arg| @has_password = false process arg if @has_password return @has_password end end nil end def process_call exp if exp.method == :password @has_password = exp else process_default exp end exp end def process_ivar exp if exp.value == :@password @has_password = exp end exp end def process_lvar exp if exp.value == :password @has_password = exp end exp end end ================================================ FILE: lib/brakeman/checks/check_weak_rsa_key.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckWeakRSAKey < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for weak uses RSA keys" def run_check check_rsa_key_creation check_rsa_operations end def check_rsa_key_creation tracker.find_call(targets: [:'OpenSSL::PKey::RSA'], method: [:new, :generate], nested: true).each do |result| key_size_arg = result[:call].first_arg check_key_size(result, key_size_arg) end tracker.find_call(targets: [:'OpenSSL::PKey'], method: [:generate_key], nested: true).each do |result| call = result[:call] key_type = call.first_arg options_arg = call.second_arg next unless options_arg and hash? options_arg if string? key_type and key_type.value.upcase == 'RSA' key_size_arg = (hash_access(options_arg, :rsa_keygen_bits) || hash_access(options_arg, s(:str, 'rsa_key_gen_bits'))) check_key_size(result, key_size_arg) end end end def check_rsa_operations tracker.find_call(targets: [:'OpenSSL::PKey::RSA.new'], methods: [:public_encrypt, :public_decrypt, :private_encrypt, :private_decrypt], nested: true).each do |result| padding_arg = result[:call].second_arg check_padding(result, padding_arg) end tracker.find_call(targets: [:'OpenSSL::PKey.generate_key'], methods: [:encrypt, :decrypt, :sign, :verify, :sign_raw, :verify_raw], nested: true).each do |result| call = result[:call] options_arg = call.last_arg if options_arg and hash? options_arg padding_arg = (hash_access(options_arg, :rsa_padding_mode) || hash_access(options_arg, s(:str, 'rsa_padding_mode'))) else padding_arg = nil end check_padding(result, padding_arg) end end def check_key_size result, key_size_arg return unless number? key_size_arg return unless original? result key_size = key_size_arg.value if key_size < 1024 confidence = :high message = msg("RSA key with size ", msg_code(key_size.to_s), " is considered very weak. Use at least 2048 bit key size") elsif key_size < 2048 confidence = :medium message = msg("RSA key with size ", msg_code(key_size.to_s), " is considered weak. Use at least 2048 bit key size") else return end warn result: result, warning_type: "Weak Cryptography", warning_code: :small_rsa_key_size, message: message, confidence: confidence, user_input: key_size_arg, cwe_id: [326] end PKCS1_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :PKCS1_PADDING).freeze PKCS1_PADDING_STR = s(:str, 'pkcs1').freeze SSLV23_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :SSLV23_PADDING).freeze SSLV23_PADDING_STR = s(:str, 'sslv23').freeze NO_PADDING = s(:colon2, s(:colon2, s(:colon2, s(:const, :OpenSSL), :PKey), :RSA), :NO_PADDING).freeze NO_PADDING_STR = s(:str, 'none').freeze def check_padding result, padding_arg return unless original? result if string? padding_arg padding_arg = padding_arg.deep_clone(padding_arg.line) padding_arg.value = padding_arg.value.downcase end case padding_arg when PKCS1_PADDING, PKCS1_PADDING_STR, nil message = "Use of padding mode PKCS1 (default if not specified), which is known to be insecure. Use OAEP instead" when SSLV23_PADDING, SSLV23_PADDING_STR message = "Use of padding mode SSLV23 for RSA key, which is only useful for outdated versions of SSL. Use OAEP instead" when NO_PADDING, NO_PADDING_STR message = "No padding mode used for RSA key. A safe padding mode (OAEP) should be specified for RSA keys" else return end warn result: result, warning_type: "Weak Cryptography", warning_code: :insecure_rsa_padding_mode, message: message, confidence: :high, user_input: padding_arg, cwe_id: [780] end end ================================================ FILE: lib/brakeman/checks/check_without_protection.rb ================================================ require 'brakeman/checks/base_check' #Check for bypassing mass assignment protection #with without_protection => true # #Only for Rails 3.1 class Brakeman::CheckWithoutProtection < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Check for mass assignment using without_protection" def run_check if version_between? "0.0.0", "3.0.99" return end return if active_record_models.empty? Brakeman.debug "Finding all mass assignments" calls = tracker.find_call :targets => active_record_models.keys, :methods => [:new, :attributes=, :update_attributes, :update_attributes!, :create, :create!] Brakeman.debug "Processing all mass assignments" calls.each do |result| process_result result end end #All results should be Model.new(...) or Model.attributes=() calls def process_result res call = res[:call] last_arg = call.last_arg if hash? last_arg and not call.original_line and not duplicate? res if value = hash_access(last_arg, :without_protection) if true? value add_result res if input = include_user_input?(call.arglist) confidence = :high elsif all_literals? call return else confidence = :medium end warn :result => res, :warning_type => "Mass Assignment", :warning_code => :mass_assign_without_protection, :message => "Unprotected mass assignment", :code => call, :user_input => input, :confidence => confidence, :cwe_id => [915] end end end end def all_literals? call call.each_arg do |arg| if hash? arg hash_iterate arg do |k, v| unless node_type? k, :str, :lit, :false, :true and node_type? v, :str, :lit, :false, :true return false end end else return false end end true end end ================================================ FILE: lib/brakeman/checks/check_xml_dos.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckXMLDoS < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for XML denial of service (CVE-2015-3227)" def run_check version = rails_version fix_version = case when version_between?("2.0.0", "3.2.21") "3.2.22" when version_between?("4.1.0", "4.1.10") "4.1.11" when version_between?("4.2.0", "4.2.1") "4.2.2" when version_between?("4.0.0", "4.0.99") "4.2.2" else return end return if has_workaround? message = msg(msg_version(version), " is vulnerable to denial of service via XML parsing ", msg_cve("CVE-2015-3227"), ". Upgrade to ", msg_version(fix_version)) warn :warning_type => "Denial of Service", :warning_code => :CVE_2015_3227, :message => message, :confidence => :medium, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/msg/rubyonrails-security/bahr2JLnxvk/x4EocXnHPp8J", :cwe_id => [125] end def has_workaround? tracker.find_call(target: :"ActiveSupport::XmlMini", method: :backend=).any? do |match| arg = match[:call].first_arg if string? arg value = arg.value value == 'Nokogiri' or value == 'LibXML' end end end end ================================================ FILE: lib/brakeman/checks/check_yaml_parsing.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckYAMLParsing < Brakeman::BaseCheck Brakeman::Checks.add self @description = "Checks for YAML parsing vulnerabilities (CVE-2013-0156)" def run_check return unless version_between? "0.0.0", "2.3.14" or version_between? "3.0.0", "3.0.18" or version_between? "3.1.0", "3.1.9" or version_between? "3.2.0", "3.2.10" unless disabled_xml_parser? or disabled_xml_dangerous_types? new_version = if version_between? "0.0.0", "2.3.14" "2.3.15" elsif version_between? "3.0.0", "3.0.18" "3.0.19" elsif version_between? "3.1.0", "3.1.9" "3.1.10" elsif version_between? "3.2.0", "3.2.10" "3.2.11" end message = msg(msg_version(rails_version), " has a remote code execution vulnerability. Upgrade to ", msg_version(new_version), " or disable XML parsing") warn :warning_type => "Remote Code Execution", :warning_code => :CVE_2013_0156, :message => message, :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/61bkgvnSGTQ/discussion", :cwe_id => [20] end #Warn if app accepts YAML if version_between?("0.0.0", "2.3.14") and enabled_yaml_parser? message = "Parsing YAML request parameters enables remote code execution: disable YAML parser" warn :warning_type => "Remote Code Execution", :warning_code => :CVE_2013_0156, :message => message, :confidence => :high, :gem_info => gemfile_or_environment, :link_path => "https://groups.google.com/d/topic/rubyonrails-security/61bkgvnSGTQ/discussion", :cwe_id => [20] end end def disabled_xml_parser? if version_between? "0.0.0", "2.3.14" #Look for ActionController::Base.param_parsers.delete(Mime::XML) matches = tracker.find_call(target: :"ActionController::Base.param_parsers", method: :delete) else #Look for ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML) matches = tracker.find_call(target: :"ActionDispatch::ParamsParser::DEFAULT_PARSERS", method: :delete) end unless matches.empty? mime_xml = s(:colon2, s(:const, :Mime), :XML) matches.each do |result| if result[:call].first_arg == mime_xml return true end end end false end #Look for ActionController::Base.param_parsers[Mime::YAML] = :yaml #in Rails 2.x apps def enabled_yaml_parser? matches = tracker.find_call(target: :'ActionController::Base.param_parsers', method: :[]=) mime_yaml = s(:colon2, s(:const, :Mime), :YAML) matches.each do |result| if result[:call].first_arg == mime_yaml and symbol? result[:call].second_arg and result[:call].second_arg.value == :yaml return true end end false end def disabled_xml_dangerous_types? if version_between? "0.0.0", "2.3.14" matches = tracker.find_call(target: :"ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING", method: :delete) else matches = tracker.find_call(target: :"ActiveSupport::XmlMini::PARSING", method: :delete) end symbols_off = false yaml_off = false matches.each do |result| arg = result[:call].first_arg if string? arg if arg.value == "yaml" yaml_off = true elsif arg.value == "symbol" symbols_off = true end end end symbols_off and yaml_off end end ================================================ FILE: lib/brakeman/checks/eol_check.rb ================================================ require 'date' require 'brakeman/checks/base_check' # Not used directly - base check for EOLRails and EOLRuby class Brakeman::EOLCheck < Brakeman::BaseCheck def check_eol_version library, eol_dates version = case library when :rails tracker.config.rails_version when :ruby tracker.config.ruby_version else raise 'Implement using tracker.config.gem_version' end eol_dates.each do |(start_version, end_version), eol_date| if version_between? start_version, end_version, version case when Date.today >= eol_date warn_about_unsupported_version library, eol_date, version when (Date.today + 30) >= eol_date warn_about_soon_unsupported_version library, eol_date, version, :medium when (Date.today + 60) >= eol_date warn_about_soon_unsupported_version library, eol_date, version, :low end break end end end def warn_about_soon_unsupported_version library, eol_date, version, confidence warn warning_type: 'Unmaintained Dependency', warning_code: :"pending_eol_#{library}", message: msg("Support for ", msg_version(version, library.capitalize), " ends on #{eol_date}"), confidence: confidence, gem_info: gemfile_or_environment(library), :cwe_id => [1104] end def warn_about_unsupported_version library, eol_date, version warn warning_type: 'Unmaintained Dependency', warning_code: :"eol_#{library}", message: msg("Support for ", msg_version(version, library.capitalize), " ended on #{eol_date}"), confidence: :high, gem_info: gemfile_or_environment(library), :cwe_id => [1104] end end ================================================ FILE: lib/brakeman/checks.rb ================================================ require 'thread' require 'brakeman/differ' #Collects up results from running different checks. # #Checks can be added with +Check.add(check_class)+ # #All .rb files in checks/ will be loaded. class Brakeman::Checks @checks = [] @optional_checks = [] attr_reader :warnings, :controller_warnings, :model_warnings, :template_warnings, :checks_run #Add a check. This will call +_klass_.new+ when running tests def self.add klass @checks << klass unless @checks.include? klass end #Add an optional check def self.add_optional klass @optional_checks << klass unless @checks.include? klass end def self.checks @checks + @optional_checks end def self.optional_checks @optional_checks end def self.initialize_checks check_directory = "" #Load all files in check_directory Dir.glob(File.join(check_directory, "*.rb")).sort.each do |f| require f end end def self.missing_checks check_args check_args = check_args.to_a.map(&:to_s).to_set if check_args == Set['CheckNone'] return [] else loaded = self.checks.map { |name| name.to_s.gsub('Brakeman::', '') }.to_set missing = check_args - loaded unless missing.empty? return missing end end [] end #No need to use this directly. def initialize options = { } if options[:min_confidence] @min_confidence = options[:min_confidence] else @min_confidence = Brakeman.get_defaults[:min_confidence] end @warnings = [] @template_warnings = [] @model_warnings = [] @controller_warnings = [] @checks_run = [] end #Add Warning to list of warnings to report. #Warnings are split into four different arrays #for template, controller, model, and generic warnings. # #Will not add warnings which are below the minimum confidence level. def add_warning warning unless warning.confidence > @min_confidence case warning.warning_set when :template @template_warnings << warning when :warning @warnings << warning when :controller @controller_warnings << warning when :model @model_warnings << warning else raise "Unknown warning: #{warning.warning_set}" end end end #Return a hash of arrays of new and fixed warnings # # diff = checks.diff old_checks # diff[:fixed] # [...] # diff[:new] # [...] def diff other_checks my_warnings = self.all_warnings other_warnings = other_checks.all_warnings Brakeman::Differ.new(my_warnings, other_warnings).diff end #Return an array of all warnings found. def all_warnings @warnings + @template_warnings + @controller_warnings + @model_warnings end #Run all the checks on the given Tracker. #Returns a new instance of Checks with the results. def self.run_checks(tracker) checks = self.checks_to_run(tracker) check_runner = self.new :min_confidence => tracker.options[:min_confidence] self.actually_run_checks(checks, check_runner, tracker) end def self.actually_run_checks(checks, check_runner, tracker) threads = [] # Results for parallel results = [] # Results for sequential parallel = tracker.options[:parallel_checks] error_mutex = Mutex.new message = if parallel "Running #{checks.length} checks in parallel" else "Running #{checks.length} checks" end Brakeman.process_step(message) do checks.each do |c| check_name = get_check_name c Brakeman.debug " - #{check_name}" if parallel threads << Thread.new do self.run_a_check(c, error_mutex, tracker) end else results << self.run_a_check(c, error_mutex, tracker) end #Maintain list of which checks were run #mainly for reporting purposes check_runner.checks_run << check_name[5..-1] end threads.each { |t| t.join } if parallel threads.each do |thread| thread.value.each do |warning| check_runner.add_warning warning end end else results.each do |warnings| warnings.each do |warning| check_runner.add_warning warning end end end end check_runner end private def self.get_check_name check_class check_class.to_s.split("::").last end def self.checks_to_run tracker to_run = if tracker.options[:run_all_checks] or tracker.options[:run_checks] @checks + @optional_checks else @checks.dup end.to_set if enabled_checks = tracker.options[:enable_checks] @optional_checks.each do |c| if enabled_checks.include? self.get_check_name(c) to_run << c end end end self.filter_checks to_run, tracker end def self.filter_checks checks, tracker skipped = tracker.options[:skip_checks] explicit = tracker.options[:run_checks] enabled = tracker.options[:enable_checks] || [] checks.reject do |c| check_name = self.get_check_name(c) skipped.include? check_name or (explicit and not explicit.include? check_name and not enabled.include? check_name) end end def self.run_a_check klass, mutex, tracker check = klass.new(tracker) begin check.run_check rescue => e mutex.synchronize do tracker.error e end end check.warnings end end #Load all files in checks/ directory Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/checks/*.rb").sort.each do |f| require f.match(/(brakeman\/checks\/.*)\.rb$/)[0] end ================================================ FILE: lib/brakeman/codeclimate/engine_configuration.rb ================================================ require 'pathname' module Brakeman module Codeclimate class EngineConfiguration def initialize(engine_config = {}) @engine_config = engine_config end def options default_options.merge(configured_options) end private attr_reader :engine_config def default_options @default_options = { :output_format => :codeclimate, :quiet => true, :pager => false, :app_path => Dir.pwd } if system("test -w /dev/stdout") @default_options[:output_files] = ["/dev/stdout"] end @default_options end def configured_options @configured_options = {} # ATM this gets parsed as a string instead of bool: "config":{ "debug":"true" } if brakeman_configuration["debug"] && brakeman_configuration["debug"].to_s.downcase == "true" @configured_options[:debug] = true @configured_options[:report_progress] = false end if active_include_paths @configured_options[:only_files] = active_include_paths end if brakeman_configuration["app_path"] @configured_options[:path_prefix] = brakeman_configuration["app_path"] path = Pathname.new(Dir.pwd) + brakeman_configuration["app_path"] @configured_options[:app_path] = path.to_s end @configured_options end def brakeman_configuration if engine_config["config"] engine_config["config"] else {} end end def active_include_paths @active_include_paths ||= if brakeman_configuration["app_path"] stripped_include_paths(brakeman_configuration["app_path"]) else engine_config["include_paths"] && engine_config["include_paths"].compact end end def stripped_include_paths(prefix) subprefixes = path_subprefixes(prefix) engine_config["include_paths"] && engine_config["include_paths"].map do |path| next unless path stripped_include_path(prefix, subprefixes, path) end.compact end def path_subprefixes(path) Pathname.new(path).each_filename.inject([]) do |memo, piece| memo << if memo.any? File.join(memo.last, piece) else File.join(piece) end end end def stripped_include_path(prefix, subprefixes, path) if path.start_with?(prefix) path.sub(%r{^#{prefix}/?}, "/") elsif subprefixes.any? { |subprefix| path =~ %r{^#{subprefix}/?$} } "/" end end end end end ================================================ FILE: lib/brakeman/commandline.rb ================================================ require 'brakeman/options' module Brakeman # Implements handling of running Brakeman from the command line. class Commandline class << self # Main method to run Brakeman from the command line. # # If no options are provided, ARGV will be parsed and used instead. # Otherwise, the options are expected to be a Hash like the one returned # after ARGV is parsed. def start options = nil, app_path = "." unless options options, app_path = parse_options ARGV end run options, app_path end # Runs everything: # # - `set_interrupt_handler` # - `early_exit_options` # - `set_options` # - `check_latest` # - `run_report` def run options, default_app_path = "." set_interrupt_handler options early_exit_options options set_options options, default_app_path check_latest(options[:ensure_latest]) if options[:ensure_latest] run_report options quit end # Check for the latest version. # # If the latest version is newer than the current version # and age, exit. def check_latest(days_old = 0) if days_old == true days_old = 0 end if error = Brakeman.ensure_latest(days_old:) quit Brakeman::Not_Latest_Version_Exit_Code, error end end # Runs a comparison report based on the options provided. def compare_results options require 'json' vulns = Brakeman.compare options.merge(:quiet => options[:quiet]) if options[:comparison_output_file] File.open options[:comparison_output_file], "w" do |f| f.puts JSON.pretty_generate(vulns) end Brakeman.announce "Comparison saved in '#{options[:comparison_output_file]}'" else puts JSON.pretty_generate(vulns) end Brakeman.cleanup(false) if options[:exit_on_warn] && vulns[:new].count > 0 quit Brakeman::Warnings_Found_Exit_Code end end # Handle options that exit without generating a report. def early_exit_options options if options[:list_checks] or options[:list_optional_checks] Brakeman.list_checks options quit elsif options[:create_config] Brakeman.dump_config options quit elsif options[:show_help] puts Brakeman::Options.create_option_parser({}) quit elsif options[:show_version] require 'brakeman/version' puts "brakeman #{Brakeman::Version}" quit end end # Parse ARGV-style array of options. # # Exits if options are invalid. # # Returns an option hash and the app_path. def parse_options argv begin options, _ = Brakeman::Options.parse! argv rescue OptionParser::ParseError => e $stderr.puts e.message $stderr.puts "Please see `brakeman --help` for valid options" quit(-1) end if argv[-1] app_path = argv[-1] else app_path = "." end if options[:ensure_ignore_notes] and options[:previous_results_json] warn '[Notice] --ensure-ignore-notes may not be used at the same ' \ 'time as --compare. Deactivating --ensure-ignore-notes. ' \ 'Please see `brakeman --help` for valid options' options[:ensure_ignore_notes] = false end return options, app_path end # Exits with the given exit code and prints out the message, if given. # # Override this method for different behavior. def quit exit_code = 0, message = nil warn message if message Brakeman.cleanup exit exit_code end # Runs a regular report based on the options provided. def regular_report options tracker = run_brakeman options ensure_ignore_notes_failed = false if tracker.options[:ensure_ignore_notes] fingerprints = Brakeman::ignore_file_entries_with_empty_notes tracker.ignored_filter&.file unless fingerprints.empty? ensure_ignore_notes_failed = true warn '[Error] Notes required for all ignored warnings when ' \ '--ensure-ignore-notes is set. No notes provided for these ' \ 'warnings: ' fingerprints.each { |f| warn f } end end if tracker.options[:exit_on_warn] and not tracker.filtered_warnings.empty? quit Brakeman::Warnings_Found_Exit_Code end if tracker.options[:exit_on_error] and tracker.errors.any? quit Brakeman::Errors_Found_Exit_Code end if tracker.options[:ensure_no_obsolete_ignore_entries] && tracker.unused_fingerprints.any? warn '[Error] Obsolete ignore entries were found, exiting with an error code.' quit Brakeman::Obsolete_Ignore_Entries_Exit_Code end if ensure_ignore_notes_failed quit Brakeman::Empty_Ignore_Note_Exit_Code end end # Actually run Brakeman. # # Returns a Tracker object. def run_brakeman options Brakeman.run options.merge(:print_report => true, :quiet => options[:quiet]) end # Run either a comparison or regular report based on options provided. def run_report options begin if options[:previous_results_json] compare_results options else regular_report options end rescue Brakeman::NoApplication => e quit Brakeman::No_App_Found_Exit_Code, e.message rescue Brakeman::MissingChecksError => e quit Brakeman::Missing_Checks_Exit_Code, e.message end end # Sets interrupt handler to gracefully handle Ctrl+C def set_interrupt_handler options trap("INT") do warn "\nInterrupted - exiting." if options[:debug] warn caller end Brakeman.cleanup exit! end end # Modifies options, including setting the app_path # if none is given in the options hash. def set_options options, default_app_path = "." unless options[:app_path] options[:app_path] = default_app_path end if options[:quiet].nil? options[:quiet] = :command_line end options end end end end ================================================ FILE: lib/brakeman/differ.rb ================================================ # extracting the diff logic to it's own class for consistency. Currently handles # an array of Brakeman::Warnings or plain hash representations. class Brakeman::Differ attr_reader :old_warnings, :new_warnings def initialize new_warnings, old_warnings @new_warnings = new_warnings @old_warnings = old_warnings end def diff warnings = {} warnings[:new] = @new_warnings - @old_warnings warnings[:fixed] = @old_warnings - @new_warnings second_pass(warnings) end # second pass to cleanup any vulns which have changed in line number only. # Given a list of new warnings, delete pairs of new/fixed vulns that differ # only by line number. def second_pass(warnings) new_fingerprints = Set.new(warnings[:new].map(&method(:fingerprint))) fixed_fingerprints = Set.new(warnings[:fixed].map(&method(:fingerprint))) # Remove warnings which fingerprints are both in :new and :fixed shared_fingerprints = new_fingerprints.intersection(fixed_fingerprints) unless shared_fingerprints.empty? warnings[:new].delete_if do |warning| shared_fingerprints.include?(fingerprint(warning)) end warnings[:fixed].delete_if do |warning| shared_fingerprints.include?(fingerprint(warning)) end end warnings end def fingerprint(warning) if warning.is_a?(Brakeman::Warning) warning.fingerprint else warning[:fingerprint] end end end ================================================ FILE: lib/brakeman/file_parser.rb ================================================ require 'parallel' module Brakeman ASTFile = Struct.new(:path, :ast) # This class handles reading and parsing files. class FileParser attr_reader :file_list, :errors def initialize app_tree, timeout, parallel = true, use_prism = false @use_prism = use_prism if @use_prism begin require 'prism' Brakeman.debug 'Using Prism parser' rescue LoadError => e Brakeman.debug "Asked to use Prism, but failed to load: #{e}" @use_prism = false end end @app_tree = app_tree @timeout = timeout @file_list = [] @errors = [] @parallel = parallel end def parse_files list if @parallel parallel_options = {} else # Disable parallelism parallel_options = { in_threads: 0 } end # Parse the files in parallel. # By default, the parsing will be in separate processes. # So we map the result to ASTFiles and/or Exceptions # then partition them into ASTFiles and Exceptions # and add the Exceptions to @errors # # Basically just a funky way to deal with two possible # return types that are returned from isolated processes. # # Note this method no longer uses read_files @file_list, new_errors = Parallel.map(list, parallel_options) do |file_name| Brakeman.logger.spin file_path = @app_tree.file_path(file_name) contents = file_path.read begin if ast = parse_ruby(contents, file_path.relative) ASTFile.new(file_name, ast) end rescue Exception => e e end end.compact.partition do |result| result.is_a? ASTFile end errors.concat new_errors end def read_files list list.each do |path| file = @app_tree.file_path(path) begin result = yield file, file.read if result @file_list << result end rescue Exception => e @errors << e end end end # _path_ can be a string or a Brakeman::FilePath def parse_ruby input, path if path.is_a? Brakeman::FilePath path = path.relative end Brakeman.debug "Parsing #{path}" if @use_prism begin parse_with_prism input, path rescue => e Brakeman.debug "Prism failed to parse #{path}: #{e}" parse_with_ruby_parser input, path end else parse_with_ruby_parser input, path end end private def parse_with_prism input, path Prism::Translation::RubyParser.parse(input, path) end def parse_with_ruby_parser input, path begin RubyParser.new.parse input, path, @timeout rescue Racc::ParseError => e raise e.exception(e.message + "\nCould not parse #{path}") rescue Timeout::Error => e raise Exception.new("Parsing #{path} took too long (> #{@timeout} seconds). Try increasing the limit with --parser-timeout") rescue => e raise e.exception(e.message + "\nWhile processing #{path}") end end end end ================================================ FILE: lib/brakeman/file_path.rb ================================================ require 'pathname' module Brakeman # Class to represent file paths within Brakeman. # FilePath objects track both the relative and absolute paths # to make it easier to manage paths. class FilePath attr_reader :absolute, :relative @cache = {} # Create a new FilePath using an AppTree object. # # Note that if the path is already a FilePath, that path will # be returned unaltered. # # Additionally, paths are cached. If the absolute path already has # a FilePath in the cache, that existing FilePath will be returned. def self.from_app_tree app_tree, path return path if path.is_a? Brakeman::FilePath absolute = app_tree.expand_path(path).freeze if fp = @cache[absolute] return fp end relative = app_tree.relative_path(path).freeze self.new(absolute, relative).tap { |fp| @cache[absolute] = fp } end # Create a new FilePath with the given absolute and relative paths. def initialize absolute_path, relative_path @absolute = absolute_path @relative = relative_path end # Just the file name, no path def basename @basename ||= File.basename(self.relative) end # Read file from absolute path. def read File.read self.absolute end # Check if absolute path exists. def exists? File.exist? self.absolute end # Compare FilePaths. Raises an ArgumentError unless both objects are FilePaths. def <=> rhs raise ArgumentError unless rhs.is_a? Brakeman::FilePath self.relative <=> rhs.relative end # Compare FilePaths. Raises an ArgumentError unless both objects are FilePaths. def == rhs return false unless rhs.is_a? Brakeman::FilePath self.absolute == rhs.absolute end # Returns a string with the absolute path. def to_str self.absolute end # Required for Pathname compatibility. # Ruby 3.5+ requires Pathname#initialize to receive a String or an object with to_path method. alias to_path to_str # Returns a string with the absolute path. def to_s self.to_str end def hash @hash ||= [@absolute, @relative].hash end def eql? rhs @absolute == rhs.absolute and @relative == rhs.relative end end end ================================================ FILE: lib/brakeman/format/style.css ================================================ /* CSS style used for HTML reports */ body { font-family: sans-serif; color: #161616; } a { color: #161616; } p { font-weight: bold; font-size: 11pt; color: #2D0200; } th { background-color: #980905; border-bottom: 5px solid #530200; color: white; font-size: 11pt; padding: 1px 8px 1px 8px; } td { border-bottom: 2px solid white; font-family: monospace; padding: 5px 8px 1px 8px; } table { background-color: #FCF4D4; border-collapse: collapse; } h1 { color: #2D0200; font-size: 14pt; } h2 { color: #2D0200; font-size: 12pt; } span.high-confidence { font-weight:bold; color: red; } span.med-confidence { } span.weak-confidence { color:gray; } div.warning_message { cursor: pointer; } div.warning_message:hover { background-color: white; } table caption { background-color: #FFE; padding: 2px; } table.context { margin-top: 5px; margin-bottom: 5px; border-left: 1px solid #90e960; color: #212121; } tr.context { background-color: white; } tr.first { border-top: 1px solid #7ecc54; padding-top: 2px; } tr.error { background-color: #f4c1c1 !important } tr.near_error { background-color: #f4d4d4 !important } tr.alt { background-color: #e8f4d4; } td.context { padding: 2px 10px 0px 6px; border-bottom: none; } td.context_line { padding: 2px 8px 0px 7px; border-right: 1px solid #b3bda4; border-bottom: none; color: #6e7465; } pre.context { margin-bottom: 1px; } .user_input { background-color: #fcecab; } div.render_path { display: none; background-color: #ffe; padding: 5px; margin: 2px 0px 2px 0px; } div.template_name { cursor: pointer; } div.template_name:hover { background-color: white; } span.code { font-family: monospace; } span.filename { font-family: monospace; } ================================================ FILE: lib/brakeman/logger.rb ================================================ module Brakeman module Logger def self.get_logger options, dest = $stderr case when options[:debug] Debug.new(options, dest) when options[:quiet] Quiet.new(options, dest) when options[:report_progress] == false Plain.new(options, dest) when dest.tty? Console.new(options, dest) else Plain.new(options, dest) end end class Base def initialize(options, log_destination = $stderr) @dest = log_destination @show_timing = options[:debug] || options[:show_timing] end # Output a message to the log. # If newline is `false`, does not output a newline after message. def log(message, newline: true) if newline @dest.puts message else @dest.write message end end # Notify about important information - use sparingly def announce(message); end # Notify regarding errors - use sparingly def alert(message); end # Output debug information def debug(message); end # Wraps a step in the scanning process def context(description, &) yield self end # Wraps a substep (e.g. processing one file) def single_context(description, &) yield end # Update progress towards a known total def update_progress(current, total, type = 'files'); end # Show a spinner def spin; end # Called on exit def cleanup(newline = true); end def show_timing? = @show_timing # Use ANSI codes to color a string def color(message, *) if @highline @highline.color(message, *) else message end end def color? @highline and @highline.use_color? end private def load_highline(output_color) if @dest.tty? or output_color == :force Brakeman.load_brakeman_dependency 'highline' @highline = HighLine.new @highline.use_color = !!output_color else @highline = nil end end end class Plain < Base def initialize(options, *) super load_highline(options[:output_color]) end def announce(message) log color(message, :bold, :green) end def alert(message) log color(message, :red) end def context(description, &) log "#{color(description, :green)}..." if show_timing? time_step(description, &) else yield end end def time_step(description, &) start_t = Time.now yield duration = Time.now - start_t log color(("Completed #{description.to_s.downcase} in %0.2fs" % duration), :gray) end end class Quiet < Base def initialize(*) super end end class Debug < Plain def debug(message) log color(message, :gray) end def context(description, &) log "#{description}..." time_step(description, &) end def single_context(description, &) debug "Processing #{description}" if show_timing? # Even in debug, only show timing for each file if asked time_step(description, &) else yield end end end class Console < Base attr_reader :prefix def initialize(options, *) super load_highline(options[:output_color]) require 'reline' require 'reline/io/ansi' @prefix = '' @post_fix_pos = 0 @reline = Reline::ANSI.new @reline.output = @dest @report_progress = options[:report_progress] @spinner = ["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"] @percenter = ["⣀", "⣤", "⣶", "⣿"] @spindex = 0 @last_spin = Time.now @reline.hide_cursor end def announce message clear_line log color(message, :bold, :green) rewrite_prefix end def alert message clear_line log color(message, :red) rewrite_prefix end def context(description, &) write_prefix description time_step(description, &) ensure clear_prefix end def time_step(description, &) if show_timing? start_t = Time.now yield duration = Time.now - start_t write_after color(('%0.2fs' % duration), :gray) log '' else yield end end def update_progress current, total, type = 'files' percent = ((current / total.to_f) * 100).to_i tenths = [(percent / 10), 0].max lead = color(@percenter[percent % 10 / 3], :bold, :red) done_blocks = color("⣿" * tenths, :red) remaining = color("⣀" * (9 - tenths), :gray) write_after "#{done_blocks}#{lead}#{remaining}" end def write_prefix pref set_prefix pref rewrite_prefix end # If an alert was written, redo prefix on next line def rewrite_prefix log(@prefix, newline: false) @reline.erase_after_cursor end def write_after message @reline.move_cursor_column(@post_fix_pos) log(message, newline: false) @reline.erase_after_cursor end def set_prefix message @prefix = "#{color('»', :bold, :cyan)} #{color(message, :green)}" @post_fix_pos = HighLine::Wrapper.actual_length(@prefix) + 1 end def clear_prefix @prefix = '' @post_fix_pos = 0 clear_line end def clear_line @reline.move_cursor_column(0) @reline.erase_after_cursor end def spin return unless (Time.now - @last_spin) > 0.2 write_after color(@spinner[@spindex], :bold, :red) @spindex = (@spindex + 1) % @spinner.length @last_spin = Time.now end def cleanup(newline = true) @reline.show_cursor log('') if newline end end end end ================================================ FILE: lib/brakeman/messages.rb ================================================ module Brakeman module Messages # Create a new message from a list of messages. # Strings are converted to Brakeman::Messages::Plain objects. def msg *args parts = args.map do |a| if a.is_a? String Plain.new(a) else a end end Message.new(*parts) end # Create a new code message fragment def msg_code code Code.new code end # Create a new message fragment with a CVE identifier def msg_cve cve CVE.new cve end # Create a new message fragment representing a file name def msg_file str Messages::FileName.new str end # Create a new message fragment from a user input type (e.g. `:params`). # The input type will be converted to a friendly version (e.g. "parameter value"). def msg_input input Input.new input end # Create a new message fragment which will not be modified during output def msg_lit str Literal.new str end # Create a new plain string message fragment def msg_plain str Plain.new str end # Create a message fragment representing the version of a library def msg_version version, lib = "Rails" Version.new version, lib end end end # Class to represent a list of message types class Brakeman::Messages::Message def initialize *args @parts = args.map do |a| case a when String, Symbol Brakeman::Messages::Plain.new(a.to_s) else a end end end def << msg if msg.is_a? String @parts << Brakeman::Messages::Plain.new(msg) else @parts << msg end end def to_s output = @parts.map(&:to_s).join case @parts.first when Brakeman::Messages::Code, Brakeman::Messages::Literal, Brakeman::Messages::Version else output[0] = output[0].capitalize end output end def to_html require 'cgi/escape' output = @parts.map(&:to_html).join case @parts.first when Brakeman::Messages::Code, Brakeman::Messages::Literal, Brakeman::Messages::Version else output[0] = output[0].capitalize end output end end class Brakeman::Messages::Code def initialize code @code = code.to_s end def to_s "`#{@code}`" end def to_html "#{CGI.escapeHTML(@code)}" end end class Brakeman::Messages::CVE def initialize cve @cve = cve end def to_s "(#{@cve})" end def to_html "(#{@cve})" end end class Brakeman::Messages::FileName def initialize file @file = file end def to_s "`#{@file}`" end def to_html "#{CGI.escapeHTML(@file)}" end end class Brakeman::Messages::Input def initialize input @input = input @value = friendly_type_of(@input) end def friendly_type_of input_type if input_type.is_a? Brakeman::BaseCheck::Match input_type = input_type.type end case input_type when :params "parameter value" when :cookies "cookie value" when :request "request value" when :model "model attribute" else "user input" end end def to_s @value end def to_html self.to_s end end class Brakeman::Messages::Literal def initialize value @value = value.to_s end def to_s @value end def to_html @value end end class Brakeman::Messages::Plain def initialize string @value = string end def to_s @value end def to_html CGI.escapeHTML(@value) end end class Brakeman::Messages::Version def initialize version, lib @version = version @library = lib end def to_s "#{@library} #{@version}" end def to_html CGI.escapeHTML(self.to_s) end end ================================================ FILE: lib/brakeman/options.rb ================================================ require 'optparse' require 'set' #Parses command line arguments for Brakeman module Brakeman::Options class << self #Parse argument array def parse args get_options args end #Parse arguments and remove them from the array as they are matched def parse! args get_options args, true end #Return hash of options and the parser def get_options args, destructive = false options = {} parser = create_option_parser options if destructive parser.parse! args else parser.parse args end if options[:previous_results_json] and options[:output_files] options[:comparison_output_file] = options[:output_files].shift end return options, parser end def create_option_parser options OptionParser.new do |opts| opts.banner = "Usage: brakeman [options] rails/root/path" opts.on "-n", "--no-threads", "Run checks and file parsing sequentially" do options[:parallel_checks] = false end opts.on "--[no-]progress", "Show progress reports" do |progress| options[:report_progress] = progress end opts.on "-p", "--path PATH", "Specify path to Rails application" do |path| options[:app_path] = path end opts.on "-q", "--[no-]quiet", "Suppress informational messages" do |quiet| options[:quiet] = quiet end opts.on( "-z", "--[no-]exit-on-warn", "Exit code is non-zero if warnings found (Default)") do |exit_on_warn| options[:exit_on_warn] = exit_on_warn end opts.on "--[no-]exit-on-error", "Exit code is non-zero if errors raised (Default)" do |exit_on_error| options[:exit_on_error] = exit_on_error end opts.on "--ensure-latest [DAYS]", Integer, "Fail when Brakeman is outdated. Optionally set minimum age in days." do |days| if days and not (1..15).include? days raise OptionParser::InvalidArgument end options[:ensure_latest] = days || true end opts.on "--ensure-ignore-notes", "Fail when an ignored warnings does not include a note" do options[:ensure_ignore_notes] = true end opts.on "--ensure-no-obsolete-ignore-entries", "Fail when an obsolete ignore entry is found" do options[:ensure_no_obsolete_ignore_entries] = true end opts.on "-3", "--rails3", "Force Rails 3 mode" do options[:rails3] = true end opts.on "-4", "--rails4", "Force Rails 4 mode" do options[:rails3] = true options[:rails4] = true end opts.on "-5", "--rails5", "Force Rails 5 mode" do options[:rails3] = true options[:rails4] = true options[:rails5] = true end opts.on "-6", "--rails6", "Force Rails 6 mode" do options[:rails3] = true options[:rails4] = true options[:rails5] = true options[:rails6] = true end opts.on "-7", "--rails7", "Force Rails 7 mode" do options[:rails3] = true options[:rails4] = true options[:rails5] = true options[:rails6] = true options[:rails7] = true end opts.on "-8", "--rails8", "Force Rails 8 mode" do options[:rails3] = true options[:rails4] = true options[:rails5] = true options[:rails6] = true options[:rails7] = true options[:rails8] = true end opts.separator "" opts.separator "Scanning options:" opts.on "-A", "--run-all-checks", "Run all default and optional checks" do options[:run_all_checks] = true end opts.on "-a", "--[no-]assume-routes", "Assume all controller methods are actions (Default)" do |assume| options[:assume_all_routes] = assume end opts.on "-e", "--escape-html", "Escape HTML by default" do options[:escape_html] = true end opts.on "--faster", "Faster, but less accurate scan" do options[:ignore_ifs] = true options[:disable_constant_tracking] = true end opts.on "--ignore-model-output", "Consider model attributes XSS-safe" do options[:ignore_model_output] = true end opts.on "--ignore-protected", "Consider models with attr_protected safe" do options[:ignore_attr_protected] = true end opts.on "--interprocedural", "Process method calls to known methods" do options[:interprocedural] = true end opts.on "--no-branching", "Disable flow sensitivity on conditionals" do options[:ignore_ifs] = true end opts.on "--branch-limit LIMIT", Integer, "Limit depth of values in branches (-1 for no limit)" do |limit| options[:branch_limit] = limit end opts.on "--parser-timeout SECONDS", Integer, "Set parse timeout (Default: 10)" do |timeout| options[:parser_timeout] = timeout end opts.on "--[no-]prism", "Use the Prism parser" do |use_prism| if use_prism min_prism_version = '1.0.0' begin gem 'prism', ">=#{min_prism_version}" require 'prism' rescue Gem::MissingSpecVersionError, Gem::MissingSpecError, Gem::LoadError => e $stderr.puts "Please install `prism` version #{min_prism_version} or newer:" raise e end end options[:use_prism] = use_prism end opts.on "-r", "--report-direct", "Only report direct use of untrusted data" do |option| options[:check_arguments] = !option end opts.on "-s", "--safe-methods meth1,meth2,etc", Array, "Set methods as safe for unescaped output in views" do |methods| options[:safe_methods] ||= Set.new options[:safe_methods].merge methods.map {|e| e.to_sym } end opts.on "--sql-safe-methods meth1,meth2,etc", Array, "Do not warn of SQL if the input is wrapped in a safe method" do |methods| options[:sql_safe_methods] ||= Set.new options[:sql_safe_methods].merge methods.map {|e| e.to_sym } end opts.on "--url-safe-methods method1,method2,etc", Array, "Do not warn of XSS if the link_to href parameter is wrapped in a safe method" do |methods| options[:url_safe_methods] ||= Set.new options[:url_safe_methods].merge methods.map {|e| e.to_sym } end opts.on "--skip-files file1,path2,etc", Array, "Skip processing of these files/directories. Directories are application relative and must end in \"#{File::SEPARATOR}\"" do |files| options[:skip_files] ||= Set.new options[:skip_files].merge files end opts.on "--only-files file1,path2,etc", Array, "Process only these files/directories. Directories are application relative and must end in \"#{File::SEPARATOR}\"" do |files| options[:only_files] ||= Set.new options[:only_files].merge files end opts.on "--[no-]skip-vendor", "Skip processing vendor directory (Default)" do |skip| options[:skip_vendor] = skip end opts.on "--add-libs-path path1,path2,etc", Array, "An application relative lib directory (ex. app/mailers) to process" do |paths| options[:additional_libs_path] ||= Set.new options[:additional_libs_path].merge paths end opts.on "--add-engines-path path1,path2,etc", Array, "Include these engines in the scan" do |paths| options[:engine_paths] ||= Set.new options[:engine_paths].merge paths end opts.on '--[no-]follow-symlinks', 'Follow symbolic links for directions' do |follow_symlinks| options[:follow_symlinks] = follow_symlinks end opts.on '--gemfile GEMFILE', 'Specify Gemfile to scan' do |gemfile| options[:gemfile] = gemfile end opts.on "-E", "--enable Check1,Check2,etc", Array, "Enable the specified checks" do |checks| checks.map! do |check| if check.start_with? "Check" check else "Check#{check}" end end options[:enable_checks] ||= Set.new options[:enable_checks].merge checks end opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks| checks.each_with_index do |s, index| if s[0,5] != "Check" checks[index] = "Check#{s}" end end options[:run_checks] ||= Set.new options[:run_checks].merge checks end opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip| skip.each do |s| if s[0,5] != "Check" s = "Check#{s}" end options[:skip_checks] ||= Set.new options[:skip_checks] << s end end opts.on "--add-checks-path path1,path2,etc", Array, "A directory containing additional out-of-tree checks to run" do |paths| options[:additional_checks_path] ||= Set.new options[:additional_checks_path].merge paths.map {|p| File.expand_path p} end opts.separator "" opts.separator "Output options:" opts.on "-d", "--debug", "Lots of output" do options[:debug] = true end opts.on "--timing", "Measure time for scan steps" do options[:show_timing] = true end opts.on "-f", "--format TYPE", [:pdf, :text, :html, :csv, :tabs, :json, :markdown, :codeclimate, :cc, :plain, :table, :junit, :sarif, :sonar, :github], "Specify output formats. Default is text" do |type| type = "s" if type == :text options[:output_format] = :"to_#{type}" end opts.on "--css-file CSSFile", "Specify CSS to use for HTML output" do |file| options[:html_style] = File.expand_path file end opts.on "-i IGNOREFILE", "--ignore-config IGNOREFILE", "Use configuration to ignore warnings" do |file| options[:ignore_file] = file end opts.on "-I", "--interactive-ignore", "Interactively ignore warnings" do options[:interactive_ignore] = true end opts.on "--show-ignored", "Show files that are usually ignored by the ignore configuration file" do options[:show_ignored] = true end opts.on "-l", "--[no-]combine-locations", "Combine warning locations (Default)" do |combine| options[:combine_locations] = combine end opts.on "--[no-]highlights", "Highlight user input in report" do |highlight| options[:highlight_user_input] = highlight end opts.on "--[no-]color", "Use ANSI colors in report (Default)" do |color| if color options[:output_color] = :force else options[:output_color] = color end end opts.on "-m", "--routes", "Report controller information" do options[:report_routes] = true end opts.on "--message-limit LENGTH", "Limit message length in HTML report" do |limit| options[:message_limit] = limit.to_i end opts.on "--[no-]pager", "Use pager for output to terminal (Default)" do |pager| options[:pager] = pager end opts.on "--table-width WIDTH", "Limit table width in text report" do |width| options[:table_width] = width.to_i end opts.on "-o", "--output FILE", "Specify files for output. Defaults to stdout. Multiple '-o's allowed" do |file| options[:output_files] ||= [] options[:output_files].push(file) end opts.on "--[no-]separate-models", "Warn on each model without attr_accessible (Default)" do |separate| options[:collapse_mass_assignment] = !separate end opts.on "--[no-]summary", "Only output summary of warnings" do |summary_only| if summary_only options[:summary_only] = :summary_only else options[:summary_only] = :no_summary end end opts.on "--absolute-paths", "Output absolute file paths in reports" do options[:absolute_paths] = true end opts.on "--github-repo USER/REPO[/PATH][@REF]", "Output links to GitHub in markdown and HTML reports using specified repo" do |repo| options[:github_repo] = repo end opts.on "--text-fields field1,field2,etc.", Array, "Specify fields for text report format" do |format| valid_options = [:category, :category_id, :check, :code, :confidence, :cwe, :file, :fingerprint, :line, :link, :message, :render_path] options[:text_fields] = format.map(&:to_sym) if options[:text_fields] == [:all] options[:text_fields] = valid_options else invalid_options = (options[:text_fields] - valid_options) unless invalid_options.empty? raise OptionParser::ParseError, "\nInvalid format options: #{invalid_options.inspect}" end end end opts.on "-w", "--confidence-level LEVEL", ["1", "2", "3"], "Set minimal confidence level (1 - 3)" do |level| options[:min_confidence] = 3 - level.to_i end opts.on "--compare FILE", "Compare the results of a previous Brakeman scan (only JSON is supported)" do |file| options[:previous_results_json] = File.expand_path(file) end opts.separator "" opts.separator "Configuration files:" opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file| options[:config_file] = File.expand_path(file) end opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file| if file options[:create_config] = file else options[:create_config] = true end end opts.on "--allow-check-paths-in-config", "Allow loading checks from configuration file (Unsafe)" do options[:allow_check_paths_in_config] = true end opts.separator "" opts.on "-k", "--checks", "List all available vulnerability checks" do options[:list_checks] = true end opts.on "--optional-checks", "List optional checks" do options[:list_optional_checks] = true end opts.on "-v", "--version", "Show Brakeman version" do options[:show_version] = true end opts.on "--force-scan", "Scan application even if rails is not detected" do options[:force_scan] = true end opts.on_tail "-h", "--help", "Display this message" do options[:show_help] = true end end end end end ================================================ FILE: lib/brakeman/parsers/haml6_embedded.rb ================================================ [:Coffee, :CoffeeScript, :Markdown, :Sass].each do |name| klass = Module.const_get("Haml::Filters::#{name}") klass.define_method(:compile) do |node| temple = [:multi] temple << [:static, ""] temple end klass.define_method(:compile_with_tilt) do |node| # From Haml text = ::Haml::Util.unescape_interpolation(node.value[:text]).gsub(/(\\+)n/) do |s| escapes = $1.size next s if escapes % 2 == 0 "#{'\\' * (escapes - 1)}\n" end text.prepend("\n").sub!(/\n"\z/, '"') [:dynamic, "BrakemanFilter.render(#{text})"] end end ================================================ FILE: lib/brakeman/parsers/haml_embedded.rb ================================================ module Brakeman module FakeHamlFilter # Copied from Haml 4 - force delayed compilation def compile(compiler, text) filter = self compiler.instance_eval do text = unescape_interpolation(text).gsub(/(\\+)n/) do |s| escapes = $1.size next s if escapes % 2 == 0 ("\\" * (escapes - 1)) + "\n" end # We need to add a newline at the beginning to get the # filter lines to line up (since the Haml filter contains # a line that doesn't show up in the source, namely the # filter name). Then we need to escape the trailing # newline so that the whole filter block doesn't take up # too many. text = "\n" + text.sub(/\n"\Z/, "\\n\"") push_script < false find_and_preserve(#{filter.inspect}.render_with_options(#{text}, _hamlout.options)) RUBY return end end end end # Fake CoffeeScript filter for Haml module Haml::Filters::Coffee include Haml::Filters::Base extend Brakeman::FakeHamlFilter end # Fake Markdown filter for Haml module Haml::Filters::Markdown include Haml::Filters::Base extend Brakeman::FakeHamlFilter end # Fake Sass filter for Haml module Haml::Filters::Sass include Haml::Filters::Base extend Brakeman::FakeHamlFilter end ================================================ FILE: lib/brakeman/parsers/rails_erubi.rb ================================================ # frozen_string_literal: true # Copied almost verbatim from Rails # https://github.com/rails/rails/blob/5359cf8a5b093b04170e884ee8da5a1e076b8a0d/actionview/lib/action_view/template/handlers/erb/erubi.rb#L9 Brakeman.load_brakeman_dependency "erubi" module Brakeman class Erubi < ::Erubi::Engine # :nodoc: all def initialize(input, properties = {}) @newline_pending = 0 # Dup properties so that we don't modify argument properties = Hash[properties] properties[:bufvar] ||= "@output_buffer" properties[:preamble] ||= "" properties[:postamble] ||= "#{properties[:bufvar]}" # Tell Erubi whether the template will be compiled with `frozen_string_literal: true` # properties[:freeze_template_literals] = !Template.frozen_string_literal properties[:freeze_template_literals] = false properties[:escapefunc] = "" super end private def add_text(text) return if text.empty? if text == "\n" @newline_pending += 1 else with_buffer do src << ".safe_append='" src << "\n" * @newline_pending if @newline_pending > 0 src << text.gsub(/['\\]/, '\\\\\&') << @text_end end @newline_pending = 0 end end BLOCK_EXPR = /((\s|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expression(indicator, code) flush_newline_if_pending(src) with_buffer do if (indicator == "==") || @escape src << ".safe_expr_append=" else src << ".append=" end if BLOCK_EXPR.match?(code) src << " " << code else src << "(" << code << ")" end end end def add_code(code) flush_newline_if_pending(src) super end def add_postamble(_) flush_newline_if_pending(src) super end def flush_newline_if_pending(src) if @newline_pending > 0 with_buffer { src << ".safe_append='#{"\n" * @newline_pending}" << @text_end } @newline_pending = 0 end end end end ================================================ FILE: lib/brakeman/parsers/slim_embedded.rb ================================================ # Fake filters for Slim module Slim class Embedded class TiltEngine alias_method :on_slim_embedded, :on_slim_embedded # silence redefined method warning def on_slim_embedded(engine, body, attrs) # Override this method to avoid Slim trying to load sass/scss and failing case engine when :sass, :scss, :coffee tilt_engine = nil # Doesn't really matter, ignored below else # Original Slim code tilt_engine = Tilt[engine] || raise(Temple::FilterError, "Tilt engine #{engine} is not available.") end tilt_options = options[engine.to_sym] || {} tilt_options[:default_encoding] ||= 'utf-8' [:multi, tilt_render(tilt_engine, tilt_options, collect_text(body)), collect_newlines(body)] end end class SassEngine protected alias_method :tilt_render, :tilt_render # silence redefined method warning def tilt_render(tilt_engine, tilt_options, text) [:dynamic, "BrakemanFilter.render(#{text.inspect}, #{self.class})"] end end class CoffeeEngine < TiltEngine protected def tilt_render(tilt_engine, tilt_options, text) [:dynamic, "BrakemanFilter.render(#{text.inspect}, #{self.class})"] end end # Override the engine for CoffeeScript, because Slim doesn't have # one, it just uses Tilt's register :coffee, JavaScriptEngine, engine: CoffeeEngine end end ================================================ FILE: lib/brakeman/parsers/template_parser.rb ================================================ module Brakeman class TemplateParser include Brakeman::Util attr_reader :tracker KNOWN_TEMPLATE_EXTENSIONS = /.*\.(erb|haml|rhtml|slim)$/ TemplateFile = Struct.new(:path, :ast, :name, :type) def initialize tracker, file_parser @tracker = tracker @file_parser = file_parser @slim_smart = nil # Load slim/smart ? end def parse_template path, text type = path.relative.match(KNOWN_TEMPLATE_EXTENSIONS)[1].to_sym type = :erb if type == :rhtml name = template_path_to_name path Brakeman.debug "Parsing #{path}" begin src = case type when :erb type = :erubi if erubi? parse_erb path, text when :haml type = :haml6 if haml6? parse_haml path, text when :slim parse_slim path, text else tracker.error "Unknown template type in #{path}" nil end if src and ast = @file_parser.parse_ruby(src, path) @file_parser.file_list << TemplateFile.new(path, ast, name, type) end rescue Racc::ParseError => e tracker.error e, "Could not parse #{path}" rescue StandardError, LoadError => e tracker.error e.exception(e.message + "\nWhile processing #{path}"), e.backtrace end nil end def parse_erb path, text if erubi? require 'brakeman/parsers/rails_erubi' Brakeman::Erubi.new(text, :filename => path).src else require 'erb' src = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ ERB.new(text, trim_mode: '-').src else ERB.new(text, nil, '-').src end src.sub!(/^#.*\n/, '') src end end def erubi? tracker.config.escape_html? or tracker.config.erubi? end def parse_haml path, text if haml6? require_relative 'haml6_embedded' Haml::Template.new(filename: path.relative, :escape_html => tracker.config.escape_html?, generator: Temple::Generators::RailsOutputBuffer, use_html_safe: true, buffer_class: 'ActionView::OutputBuffer', disable_capture: true, ) { text }.precompiled_template else require_relative 'haml_embedded' Haml::Engine.new(text, :filename => path, :escape_html => tracker.config.escape_html?, :escape_filter_interpolations => tracker.config.escape_filter_interpolations? ).precompiled.gsub(/([^\\])\\n/, '\1') end rescue Haml::Error => e tracker.error e, ["While compiling HAML in #{path}"] << e.backtrace nil end def haml6? return @haml6 unless @haml6.nil? Brakeman.load_brakeman_dependency 'haml' major_version = Haml::VERSION.split('.').first.to_i if major_version >= 6 @haml6 = true else @haml6 = false end end def parse_slim path, text Brakeman.load_brakeman_dependency 'slim' if @slim_smart.nil? and load_slim_smart? @slim_smart = true Brakeman.load_brakeman_dependency 'slim/smart' else @slim_smart = false end require_relative 'slim_embedded' Slim::Template.new(path, :disable_capture => true, :generator => Temple::Generators::RailsOutputBuffer) { text }.precompiled_template end def load_slim_smart? return !@slim_smart unless @slim_smart.nil? # Terrible hack to find # gem "slim", "~> 3.0.1", require: ["slim", "slim/smart"] if tracker.app_tree.exists? 'Gemfile' gemfile_contents = tracker.app_tree.file_path('Gemfile').read if gemfile_contents.include? 'slim/smart' return true end end false end def self.parse_inline_erb tracker, text fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout]) tp = self.new(tracker, fp) src = tp.parse_erb '_inline_', text type = tp.erubi? ? :erubi : :erb return type, fp.parse_ruby(src, "_inline_") end end end ================================================ FILE: lib/brakeman/processor.rb ================================================ #Load all files in processors/ Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/processors/*.rb").each { |f| require f.match(/brakeman\/processors.*/)[0] } require 'brakeman/tracker' require 'set' require 'pathname' module Brakeman #Makes calls to the appropriate processor. # #The ControllerProcessor, TemplateProcessor, and ModelProcessor will #update the Tracker with information about what is parsed. class Processor include Util def initialize(app_tree, options) @tracker = Tracker.new(app_tree, self, options) end def tracked_events @tracker end #Process configuration file source def process_config src, file_name ConfigProcessor.new(@tracker).process_config src, file_name end #Process Gemfile def process_gems gem_files GemProcessor.new(@tracker).process_gems gem_files end #Process route file source def process_routes src RoutesProcessor.new(@tracker).process_routes src end #Process controller source. +file_name+ is used for reporting def process_controller src, file_name if contains_class? src ControllerProcessor.new(@tracker).process_controller src, file_name else LibraryProcessor.new(@tracker).process_library src, file_name end end #Process variable aliasing in controller source and save it in the #tracker. def process_controller_alias name, src, only_method = nil, file = nil ControllerAliasProcessor.new(@tracker, only_method).process_controller name, src, file end #Process a model source def process_model src, file_name result = ModelProcessor.new(@tracker).process_model src, file_name AliasProcessor.new(@tracker, file_name).process result if result end #Process either an ERB or HAML template def process_template name, src, type, called_from = nil, file_name = nil case type when :erb result = ErbTemplateProcessor.new(@tracker, name, called_from, file_name).process src when :haml result = HamlTemplateProcessor.new(@tracker, name, called_from, file_name).process src when :haml6 result = Haml6TemplateProcessor.new(@tracker, name, called_from, file_name).process src when :erubi result = ErubiTemplateProcessor.new(@tracker, name, called_from, file_name).process src when :slim result = SlimTemplateProcessor.new(@tracker, name, called_from, file_name).process src else abort "Unknown template type: #{type} (#{name})" end #Each template which is rendered is stored separately #with a new name. if called_from name = ("#{name}.#{called_from}").to_sym end @tracker.templates[name].src = result @tracker.templates[name].type = type end #Process any calls to render() within a template def process_template_alias template TemplateAliasProcessor.new(@tracker, template).process_safely template.src end #Process source for initializing files def process_initializer file_name, src res = BaseProcessor.new(@tracker).process_file src, file_name res = AliasProcessor.new(@tracker).process_safely res, nil, file_name @tracker.initializers[file_name] = res end #Process source for a library file def process_lib src, file_name LibraryProcessor.new(@tracker).process_library src, file_name end end end ================================================ FILE: lib/brakeman/processors/alias_processor.rb ================================================ require 'brakeman/util' require 'ruby_parser/bm_sexp_processor' require 'brakeman/processors/lib/processor_helper' require 'brakeman/processors/lib/safe_call_helper' require 'brakeman/processors/lib/call_conversion_helper' #Returns an s-expression with aliases replaced with their value. #This does not preserve semantics (due to side effects, etc.), but it makes #processing easier when searching for various things. class Brakeman::AliasProcessor < Brakeman::SexpProcessor include Brakeman::ProcessorHelper include Brakeman::SafeCallHelper include Brakeman::Util include Brakeman::CallConversionHelper attr_reader :result, :tracker #Returns a new AliasProcessor with an empty environment. # #The recommended usage is: # # AliasProcessor.new.process_safely src def initialize tracker = nil, current_file = nil super() @env = SexpProcessor::Environment.new @inside_if = false @ignore_ifs = nil @exp_context = [] @tracker = tracker #set in subclass as necessary @helper_method_cache = {} @helper_method_info = Hash.new({}) @or_depth_limit = (tracker && tracker.options[:branch_limit]) || 5 #arbitrary default @meth_env = nil @current_file = current_file @mass_limit = (tracker && tracker.options[:mass_limit]) || 1000 # arbitrary default set_env_defaults end #This method processes the given Sexp, but copies it first so #the original argument will not be modified. # #_set_env_ should be an instance of SexpProcessor::Environment. If provided, #it will be used as the starting environment. # #This method returns a new Sexp with variables replaced with their values, #where possible. def process_safely src, set_env = nil, current_file = @current_file @current_file = current_file @env = set_env || SexpProcessor::Environment.new @result = src.deep_clone process @result @result end #Process a Sexp. If the Sexp has a value associated with it in the #environment, that value will be returned. def process_default exp @exp_context.push exp begin exp.map! do |e| if sexp? e and not e.empty? process e else e end end rescue => err if @tracker @tracker.error err else raise err end end result = replace(exp) @exp_context.pop result end def replace exp, int = 0 return exp if int > 3 if replacement = env[exp] if not duplicate? replacement and replacement.mass < @mass_limit replace(replacement.deep_clone(exp.line), int + 1) else exp end elsif tracker and replacement = tracker.constant_lookup(exp) and not duplicate? replacement replace(replacement.deep_clone(exp.line), int + 1) else exp end end def process_bracket_call exp # TODO: What is even happening in this method? r = replace(exp) if r != exp return r end exp.arglist = process_default(exp.arglist) r = replace(exp) if r != exp return r end t = process(exp.target.deep_clone) # sometimes t[blah] has a match in the env # but we don't want to actually set the target # in case the target is big...which is what this # whole method is trying to avoid if t != exp.target e = exp.deep_clone e.target = t r = replace(e) if r != e return r end else t = exp.target # put it back? end if hash? t if v = process_hash_access(t, exp.first_arg) v.deep_clone(exp.line) else case t.node_type when :params exp.target = PARAMS_SEXP.deep_clone(exp.target.line) when :session exp.target = SESSION_SEXP.deep_clone(exp.target.line) when :cookies exp.target = COOKIES_SEXP.deep_clone(exp.target.line) end exp end elsif array? t if v = process_array_access(t, exp.args) v.deep_clone(exp.line) else exp end elsif t exp.target = t exp else if exp.target # `self` target is reported as `nil` https://github.com/seattlerb/ruby_parser/issues/250 exp.target = process_default exp.target end exp end end ARRAY_CONST = s(:const, :Array) HASH_CONST = s(:const, :Hash) RAILS_TEST = s(:call, s(:call, s(:const, :Rails), :env), :test?) RAILS_DEV = s(:call, s(:call, s(:const, :Rails), :env), :development?) #Process a method call. def process_call exp return exp if process_call_defn? exp target_var = exp.target target_var &&= target_var.deep_clone if exp.node_type == :safe_call exp.node_type = :call end if exp.method == :[] return process_bracket_call exp else exp = process_default exp end #In case it is replaced with something else unless call? exp return exp end # If x(*[1,2,3]) change to x(1,2,3) # if that's the only argument if splat_array? exp.first_arg and exp.second_arg.nil? exp.arglist = exp.first_arg[1].sexp_body end target = exp.target method = exp.method first_arg = exp.first_arg if method == :send or method == :__send__ or method == :try collapse_send_call exp, first_arg end if node_type? target, :or and [:+, :-, :*, :/].include? method res = process_or_simple_operation(exp) return res if res elsif target == ARRAY_CONST and method == :new return Sexp.new(:array, *exp.args).line(exp.line) elsif target == HASH_CONST and method == :new and first_arg.nil? and !node_type?(@exp_context.last, :iter) return Sexp.new(:hash).line(exp.line) elsif exp == RAILS_TEST or exp == RAILS_DEV return Sexp.new(:false).line(exp.line) end # For the simplest case of `Foo.thing` if node_type? target, :const and first_arg.nil? if tracker and (klass = tracker.find_class(class_name(target.value))) if return_value = klass.get_simple_method_return_value(:class, method) return return_value.deep_clone(exp.line) end end end #See if it is possible to simplify some basic cases #of addition/concatenation. case method when :+ if array? target and array? first_arg exp = join_arrays(target, first_arg, exp) elsif string? first_arg exp = join_strings(target, first_arg, exp) elsif number? first_arg exp = math_op(:+, target, first_arg, exp) end when :-, :*, :/ if method == :* and array? target if string? first_arg exp = process_array_join(target, first_arg) end else exp = math_op(method, target, first_arg, exp) end when :[] # TODO: This might never be used because of process_bracket_call above if array? target exp = process_array_access(target, exp.args, exp) elsif hash? target exp = process_hash_access(target, first_arg, exp) end when :fetch if array? target # Not dealing with default value # so just pass in first argument, but process_array_access expects # an array of arguments. exp = process_array_access(target, [first_arg], exp) elsif hash? target exp = process_hash_access(target, first_arg, exp) end when :merge!, :update if hash? target and hash? first_arg target = process_hash_merge! target, first_arg env[target_var] = target return target end when :merge if hash? target and hash? first_arg return process_hash_merge(target, first_arg) end when :<< if string? target and string? first_arg target.value += first_arg.value env[target_var] = target return target elsif string? target and string_interp? first_arg exp = Sexp.new(:dstr, target.value + first_arg[1]).concat(first_arg.sexp_body(2)).line(exp.line) env[target_var] = exp elsif string? first_arg and string_interp? target if string? target.last target.last.value += first_arg.value elsif target.last.is_a? String # TODO Use target.last += ? target.last << first_arg.value else target << first_arg end env[target_var] = target return first_arg elsif new_string? target env[target_var] = first_arg return first_arg elsif array? target target << first_arg env[target_var] = target return target else target = find_push_target(target_var) env[target] = exp unless target.nil? # Happens in TemplateAliasProcessor end when :push if array? target target << first_arg env[target_var] = target return target end when :first if array? target and first_arg.nil? and sexp? target[1] exp = target[1] end when :freeze, :dup, :presence unless target.nil? exp = target end when :join if array? target and (string? first_arg or first_arg.nil?) exp = process_array_join(target, first_arg) end when :! # Convert `!!a` to boolean if call? target and target.method == :! exp = s(:or, s(:true).line(exp.line), s(:false).line(exp.line)).line(exp.line) end when :values # Hash literal if node_type? target, :hash exp = hash_values(target) end when :values_at if node_type? target, :hash res = hash_values_at target, exp.args # Only convert to array of values if _all_ keys # are present in the hash. unless res.any?(&:nil?) exp = res end end when :presence_in arg = exp.first_arg if node_type? arg, :array # 1.presence_in [1,2,3] if arg.include? target exp = target elsif all_literals? arg exp = safe_literal(exp.line) end end end exp end # Painful conversion of Array#join into string interpolation def process_array_join array, join_str # Empty array if array.length == 1 return s(:str, '').line(array.line) end result = s().line(array.line) join_value = if string? join_str join_str.value else nil end if array.length > 2 array[1..-2].each do |e| result << join_item(e, join_value) end end result << join_item(array.last, nil) # Combine the strings at the beginning because that's what RubyParser does combined_first = +"" result.each do |e| if string? e combined_first << e.value elsif e.is_a? String combined_first << e else break end end # Remove the strings at the beginning result.reject! do |e| if e.is_a? String or string? e true else break end end result.unshift combined_first # Have to fix up strings that follow interpolation string = result.reduce(s(:dstr).line(array.line)) do |memo, e| if string? e and node_type? memo.last, :evstr e.value = "#{join_value}#{e.value}" elsif join_value and node_type? memo.last, :evstr and node_type? e, :evstr memo << s(:str, join_value).line(e.line) end memo << e end # Convert (:dstr, "hello world") # to (:str, "hello world") if string.length == 2 and string.last.is_a? String string[0] = :str end string end def join_item item, join_value if item.nil? || item.is_a?(String) "#{item}#{join_value}" elsif string? item or symbol? item or number? item s(:str, "#{item.value}#{join_value}").line(item.line) else s(:evstr, item).line(item.line) end end TEMP_FILE_CLASS = s(:const, :Tempfile) def temp_file_open? exp call? exp and exp.target == TEMP_FILE_CLASS and exp.method == :open end def temp_file_create? exp call? exp and exp.target == TEMP_FILE_CLASS and exp.method == :create end def temp_file_new line s(:call, TEMP_FILE_CLASS, :new).line(line) end def splat_array? exp node_type? exp, :splat and node_type? exp[1], :array end def process_iter exp @exp_context.push exp exp[1] = process exp.block_call if array_detect_all_literals? exp[1] return safe_literal(exp.line) end @exp_context.pop env.scope do call = exp.block_call block_args = exp.block_args if call? call and [:each, :map].include? call.method and all_literals? call.target and block_args.length == 2 and block_args.last.is_a? Symbol # Iterating over an array of all literal values local = Sexp.new(:lvar, block_args.last) env.current[local] = safe_literal(exp.line) elsif temp_file_open? call local = Sexp.new(:lvar, block_args.last) env.current[local] = temp_file_new(exp.line) elsif temp_file_create? call local = Sexp.new(:lvar, block_args.last) env.current[local] = temp_file_new(exp.line) else block_args.each do |e| #Force block arg(s) to be local if node_type? e, :lasgn env.current[Sexp.new(:lvar, e.lhs)] = Sexp.new(:lvar, e.lhs) elsif node_type? e, :kwarg env.current[Sexp.new(:lvar, e[1])] = e[2] elsif node_type? e, :masgn, :shadow e[1..-1].each do |var| local = Sexp.new(:lvar, var) env.current[local] = local end elsif e.is_a? Symbol local = Sexp.new(:lvar, e) env.current[local] = local elsif e.nil? # trailing comma, argument destructuring next # Punt for now else raise "Unexpected value in block args: #{e.inspect}" end end end block = exp.block if block? block process_all! block else exp[3] = process block end end exp end #Process a new scope. def process_scope exp env.scope do process exp.block end exp end #Start new scope for block. def process_block exp env.scope do process_default exp end end #Process a method definition. def process_defn exp meth_env do exp.body = process_all! exp.body end exp end def meth_env begin env.scope do set_env_defaults @meth_env = env.current yield end ensure @meth_env = nil end end #Process a method definition on self. def process_defs exp meth_env do exp.body = process_all! exp.body end exp end # Handles x = y = z = 1 def get_rhs exp if node_type? exp, :lasgn, :iasgn, :gasgn, :attrasgn, :safe_attrasgn, :cvdecl, :cdecl get_rhs(exp.rhs) else exp end end #Local assignment # x = 1 def process_lasgn exp self_assign = self_assign?(exp.lhs, exp.rhs) exp.rhs = process exp.rhs if sexp? exp.rhs return exp if exp.rhs.nil? local = Sexp.new(:lvar, exp.lhs).line(exp.line || -2) if self_assign # Skip branching env[local] = get_rhs(exp) else set_value local, get_rhs(exp) end exp end #Instance variable assignment # @x = 1 def process_iasgn exp self_assign = self_assign?(exp.lhs, exp.rhs) exp.rhs = process exp.rhs ivar = Sexp.new(:ivar, exp.lhs).line(exp.line) if self_assign if env[ivar].nil? and @meth_env @meth_env[ivar] = get_rhs(exp) else env[ivar] = get_rhs(exp) end else set_value ivar, get_rhs(exp) end exp end #Global assignment # $x = 1 def process_gasgn exp match = Sexp.new(:gvar, exp.lhs) exp.rhs = process(exp.rhs) value = get_rhs(exp) if value value.line = exp.line set_value match, value end exp end #Class variable assignment # @@x = 1 def process_cvdecl exp match = Sexp.new(:cvar, exp.lhs) exp.rhs = process(exp.rhs) value = get_rhs(exp) set_value match, value exp end #'Attribute' assignment # x.y = 1 #or # x[:y] = 1 def process_attrasgn exp tar_variable = exp.target target = process(exp.target) method = exp.method index_arg = exp.first_arg value_arg = exp.second_arg if method == :[]= index = exp.first_arg = process(index_arg) value = exp.second_arg = process(value_arg) match = Sexp.new(:call, target, :[], index) set_value match, value if hash? target env[tar_variable] = hash_insert target.deep_clone, index, value end unless node_type? target, :hash exp.target = target end elsif method.to_s[-1,1] == "=" exp.first_arg = process(index_arg) value = get_rhs(exp) #This is what we'll replace with the value match = Sexp.new(:call, target, method.to_s[0..-2].to_sym) set_value match, value exp.target = target else raise "Unrecognized assignment: #{exp}" end exp end # Multiple/parallel assignment: # # x, y = z, w def process_masgn exp exp[2] = process exp[2] if sexp? exp[2] if node_type? exp[2], :to_ary and array? exp[2][1] exp[2] = exp[2][1] end unless array? exp[1] and array? exp[2] # Already processed RHS, don't do it again # https://github.com/presidentbeef/brakeman/issues/1877 return exp end vars = exp[1].dup vals = exp[2].dup vars.shift vals.shift # Call each assignment as if it is normal vars.each_with_index do |var, i| val = vals[i] next unless val # TODO: Break if there are no vals left? # This happens with nested destructuring like # x, (a, b) = blah if node_type? var, :masgn # Need to add value to masgn exp m = var.dup m[2] = s(:to_ary, val) process_masgn m elsif node_type? var, :splat # Assign the rest of the values to the variable: # # a, *b = 1, 2, 3 # # b == [2, 3] assign = var[1].dup # var is s(:splat, s(:lasgn, :b)) if i == vars.length - 1 # Last variable being assigned, slurp up the rest assign.rhs = s(:array, *vals[i..]) # val is the "rest" of the values else # Calculate how many values to assign based on how many variables # there are. # # If there are more values than variables, the splat gets an empty array. assign.rhs = s(:array, *vals[i, (vals.length - vars.length + 1)]).line(vals.line) end process assign else assign = var.dup assign.rhs = val process assign end end exp end def process_hash exp exp = process_default(exp) # Handle { **kw } if node_type? exp, :hash if exp.any? { |e| node_type? e, :kwsplat and node_type? e.value, :hash } kwsplats, rest = exp.partition { |e| node_type? e, :kwsplat and node_type? e.value, :hash } exp = Sexp.new.concat(rest).line(exp.line) kwsplats.each do |e| exp = process_hash_merge! exp, e.value end end end # Return early unless there might be short-hand syntax, # since handling it is kind of expensive. return exp unless exp.any? { |e| e.nil? } # Need to handle short-hand hash syntax new_hash = [:hash] hash_iterate(exp) do |key, value| # e.g. { a: } if value.nil? and symbol? key # Only handling local variables for now, not calls lvar = s(:lvar, key.value) if var_value = env[lvar] new_hash << key << var_value.deep_clone(key.line || 0) else # If the value is unknown, assume it was a call # and set the value to a call new_hash.concat << key << s(:call, nil, key.value).line(key.line || 0) end else new_hash.concat << key << value end end Sexp.from_array(new_hash).line(exp.line || 0) end #Merge values into hash when processing # # h.merge! :something => "value" def process_hash_merge! hash, args hash = hash.deep_clone hash_iterate args do |key, replacement| hash_insert hash, key, replacement match = Sexp.new(:call, hash, :[], key) env[match] = replacement end hash end #Return a new hash Sexp with the given values merged into it. # #+args+ should be a hash Sexp as well. def process_hash_merge hash, args hash = hash.deep_clone hash_iterate args do |key, replacement| hash_insert hash, key, replacement end hash end #Assignments like this # x[:y] ||= 1 def process_op_asgn1 exp target_var = exp[1] target_var &&= target_var.deep_clone target = exp[1] = process(exp[1]) index = exp[2][1] = process(exp[2][1]) value = exp[4] = process(exp[4]) match = Sexp.new(:call, target, :[], index) if exp[3] == :"||" unless env[match] if request_value? target env[match] = match.combine(value) else env[match] = value end end else new_value = process s(:call, s(:call, target_var, :[], index), exp[3], value).line(exp.line) env[match] = new_value end exp end #Assignments like this # x.y ||= 1 def process_op_asgn2 exp return process_default(exp) if exp[3] != :"||" target = exp[1] = process(exp[1]) value = exp[4] = process(exp[4]) method = exp[2] match = Sexp.new(:call, target, method.to_s[0..-2].to_sym) unless env[match] env[match] = value end exp end #This is the right hand side value of a multiple assignment, #like `x = y, z` def process_svalue exp exp.value end #Constant assignments like # BIG_CONSTANT = 234810983 def process_cdecl exp if sexp? exp.rhs exp.rhs = process exp.rhs end if @tracker @tracker.add_constant exp.lhs, exp.rhs, :file => @current_file, :module => @current_module, :class => @current_class, :method => @current_method end if exp.lhs.is_a? Symbol match = Sexp.new(:const, exp.lhs) else match = exp.lhs end env[match] = get_rhs(exp) exp end def hash_or_array_include_all_literals? exp return unless call? exp and sexp? exp.target target = exp.target case target.node_type when :hash hash_include_all_literals? exp else array_include_all_literals? exp end end # Check if exp is a call to Array#include? on an array literal # that contains all literal values. For example: # # [1, 2, "a"].include? x # def array_include_all_literals? exp call? exp and exp.method == :include? and (all_literals? exp.target or dir_glob? exp.target) end def array_detect_all_literals? exp call? exp and [:detect, :find].include? exp.method and exp.first_arg.nil? and (all_literals? exp.target or dir_glob? exp.target) end # Check if exp is a call to Array#include? on an array literal # that contains all literal values. For example: # # x.in? [1, 2, "a"] # def in_array_all_literals? exp call? exp and exp.method == :in? and all_literals? exp.first_arg end # Check if exp is a call to Hash#include? on a hash literal # that contains all literal values. For example: # # {x: 1}.include? x def hash_include_all_literals? exp call? exp and exp.method == :include? and all_literals? exp.target, :hash end #Sets @inside_if = true def process_if exp if @ignore_ifs.nil? @ignore_ifs = @tracker && @tracker.options[:ignore_ifs] end condition = exp.condition = process exp.condition #Check if a branch is obviously going to be taken if true? condition no_branch = true exps = [exp.then_clause, nil] elsif false? condition no_branch = true exps = [nil, exp.else_clause] elsif equality_check? condition and condition.target == condition.first_arg no_branch = true exps = [exp.then_clause, nil] else no_branch = false exps = [exp.then_clause, exp.else_clause] end if @ignore_ifs or no_branch exps.each_with_index do |branch, i| exp[2 + i] = process_if_branch branch end else # Translate `if !...` into `unless ...` # Technically they are different but that's only if someone overrides `!` if call? condition and condition.method == :! condition = condition.target exps.reverse! end was_inside = @inside_if @inside_if = true branch_scopes = [] exps.each_with_index do |branch, i| scope do @branch_env = env.current branch_index = 2 + i # s(:if, condition, then_branch, else_branch) exp[branch_index] = if i == 0 and hash_or_array_include_all_literals? condition # If the condition is ["a", "b"].include? x # set x to safe_literal inside the true branch var = condition.first_arg value = safe_literal(var.line) process_branch_with_value(var, value, branch, i) elsif i == 0 and in_array_all_literals? condition # If the condition is x.in? ["a", "b"] # set x to safe_literal inside the true branch var = condition.target value = safe_literal(var.line) process_branch_with_value(var, value, branch, i) elsif i == 0 and equality_check? condition # For conditions like a == b, # set a to b inside the true branch var = condition.target value = condition.first_arg process_branch_with_value(var, value, branch, i) elsif i == 1 and hash_or_array_include_all_literals? condition and early_return? branch var = condition.first_arg env.current[var] = safe_literal(var.line) process_if_branch branch else process_if_branch branch end branch_scopes << env.current @branch_env = nil end end @inside_if = was_inside branch_scopes.each do |s| merge_if_branch s end end exp end def process_branch_with_value var, value, branch, branch_index previous_value = env.current[var] env.current[var] = value result = process_if_branch branch env.current[var] = previous_value result end def early_return? exp return true if node_type? exp, :return return true if call? exp and [:fail, :raise].include? exp.method if node_type? exp, :block, :rlist node_type? exp.last, :return or (call? exp and [:fail, :raise].include? exp.method) else false end end def equality_check? exp call? exp and exp.method == :== end # Not a list of values # when :example def simple_when? exp node_type? exp[1], :array and exp[1].length == 2 and # only one element in the array not node_type? exp[1][1], :splat, :array end # A list of literal values # # when 1,2,3 # # or # # when *[:a, :b] def all_literals_when? exp if array? exp[1] # pretty sure this is always true all_literals? exp[1] or # simple list, not actually array (splat_array? exp[1][1] and all_literals? exp[1][1][1]) end end def process_case exp if @ignore_ifs.nil? @ignore_ifs = @tracker && @tracker.options[:ignore_ifs] end if @ignore_ifs process_default exp return exp end branch_scopes = [] was_inside = @inside_if @inside_if = true exp[1] = process exp[1] if exp[1] case_value = if node_type? exp[1], :lvar, :ivar, :call exp[1].deep_clone end exp.each_sexp do |e| if node_type? e, :when scope do # Process the when value for matching process_default e[1] # Moved here to avoid @branch_env being cleared out # in process_default # Maybe in the future don't set it to nil? @branch_env = env.current # set value of case var if possible if case_value if simple_when? e @branch_env[case_value] = e[1][1] elsif all_literals_when? e @branch_env[case_value] = safe_literal(e.line + 1) end end # when blocks aren't blocks, they are lists of expressions process_default e branch_scopes << env.current @branch_env = nil end end end # else clause if sexp? exp.last scope do @branch_env = env.current process_default exp[-1] branch_scopes << env.current @branch_env = nil end end @inside_if = was_inside branch_scopes.each do |s| merge_if_branch s end exp end def process_if_branch exp if sexp? exp if block? exp process_default exp else process exp end end end def merge_if_branch branch_env branch_env.each do |k, v| next if v.nil? current_val = env[k] if current_val unless same_value?(current_val, v) if too_deep? current_val # Give up branching, start over with latest value env[k] = v else env[k] = current_val.combine(v, k.line) end end else env[k] = v end end end def too_deep? exp @or_depth_limit >= 0 and node_type? exp, :or and exp.or_depth and exp.or_depth >= @or_depth_limit end # Change x.send(:y, 1) to x.y(1) def collapse_send_call exp, first_arg # Handle try(&:id) if node_type? first_arg, :block_pass first_arg = first_arg[1] end return unless symbol? first_arg or string? first_arg exp.method = first_arg.value.to_sym args = exp.args exp.pop # remove last arg if args.length > 1 exp.arglist = args.sexp_body end end #Returns a new SexpProcessor::Environment containing only instance variables. #This is useful, for example, when processing views. def only_ivars include_request_vars = false, lenv = nil lenv ||= env res = SexpProcessor::Environment.new if include_request_vars lenv.all.each do |k, v| #TODO Why would this have nil values? if (k.node_type == :ivar or request_value? k) and not v.nil? res[k] = v.dup end end else lenv.all.each do |k, v| #TODO Why would this have nil values? if k.node_type == :ivar and not v.nil? res[k] = v.dup end end end res end def only_request_vars res = SexpProcessor::Environment.new env.all.each do |k, v| if request_value? k and not v.nil? res[k] = v.dup end end res end def get_call_value call method_name = call.method #Look for helper methods and see if we can get a return value if found_method = tracker.find_method(method_name, @current_class) helper = found_method.src if sexp? helper value = process_helper_method helper, call.args value.line(call.line) return value else raise "Unexpected value for method: #{found_method}" end else call end end def process_helper_method method_exp, args method_name = method_exp.method_name Brakeman.debug "Processing method #{method_name}" info = @helper_method_info[method_name] #If method uses instance variables, then include those and request #variables (params, etc) in the method environment. Otherwise, #only include request variables. if info[:uses_ivars] meth_env = only_ivars(:include_request_vars) else meth_env = only_request_vars end #Add arguments to method environment assign_args method_exp, args, meth_env #Find return values if method does not depend on environment/args values = @helper_method_cache[method_name] unless values #Serialize environment for cache key meth_values = meth_env.instance_variable_get(:@env).to_a meth_values.sort! meth_values = meth_values.to_s digest = Digest::SHA1.new.update(meth_values << method_name.to_s).to_s.to_sym values = @helper_method_cache[digest] end if values #Use values from cache values[:ivar_values].each do |var, val| env[var] = val end values[:return_value] else #Find return value for method frv = Brakeman::FindReturnValue.new value = frv.get_return_value(method_exp.body_list, meth_env) ivars = {} only_ivars(false, meth_env).all.each do |var, val| env[var] = val ivars[var] = val end if not frv.uses_ivars? and args.length == 0 #Store return value without ivars and args if they are not used @helper_method_cache[method_exp.method_name] = { :return_value => value, :ivar_values => ivars } else @helper_method_cache[digest] = { :return_value => value, :ivar_values => ivars } end #Store information about method, just ivar usage for now @helper_method_info[method_name] = { :uses_ivars => frv.uses_ivars? } value end end def assign_args method_exp, args, meth_env = SexpProcessor::Environment.new formal_args = method_exp.formal_args formal_args.each_with_index do |arg, index| next if index == 0 if arg.is_a? Symbol and sexp? args[index - 1] meth_env[Sexp.new(:lvar, arg)] = args[index - 1] end end meth_env end #Finds the inner most call target which is not the target of a call to << def find_push_target exp if call? exp and exp.method == :<< find_push_target exp.target else exp end end def duplicate? exp @exp_context[0..-2].reverse_each do |e| return true if exp == e end false end def find_method *args nil end #Return true if lhs == rhs or lhs is an or expression and #rhs is one of its values def same_value? lhs, rhs if lhs == rhs true elsif node_type? lhs, :or lhs.rhs == rhs or lhs.lhs == rhs else false end end def self_assign? var, value self_assign_var?(var, value) or self_assign_target?(var, value) end #Return true if for x += blah or @x += blah def self_assign_var? var, value call? value and value.method == :+ and node_type? value.target, :lvar, :ivar and value.target.value == var end #Return true for x = x.blah def self_assign_target? var, value target = top_target(value) if node_type? target, :lvar, :ivar target = target.value end var == target end #Returns last non-nil target in a call chain def top_target exp, last = nil if call? exp top_target exp.target, exp elsif node_type? exp, :iter top_target exp.block_call, last else exp || last end end def value_from_if exp if block? exp.else_clause or block? exp.then_clause #If either clause is more than a single expression, just use entire #if expression for now exp elsif exp.else_clause.nil? exp.then_clause elsif exp.then_clause.nil? exp.else_clause else condition = exp.condition if true? condition exp.then_clause elsif false? condition exp.else_clause else exp.then_clause.combine(exp.else_clause, exp.line) end end end def value_from_case exp result = [] exp.each do |e| if node_type? e, :when result << e.last end end result << exp.last if exp.last # else result.reduce do |c, e| if c.nil? e elsif node_type? e, :if c.combine(value_from_if e) elsif raise? e c # ignore exceptions elsif e c.combine e else # when e is nil c end end end def raise? exp call? exp and exp.method == :raise end STRING_NEW = s(:call, s(:const, :String), :new) # String.new ? def new_string? exp exp == STRING_NEW end #Set variable to given value. #Creates "branched" versions of values when appropriate. #Avoids creating multiple branched versions inside same #if branch. def set_value var, value if node_type? value, :if value = value_from_if(value) elsif node_type? value, :case value = value_from_case(value) end if @ignore_ifs or not @inside_if if @meth_env and node_type? var, :ivar and env[var].nil? @meth_env[var] = value else env[var] = value end elsif env.current[var] env.current[var] = value elsif @branch_env and @branch_env[var] @branch_env[var] = value elsif @branch_env and @meth_env and node_type? var, :ivar @branch_env[var] = value else env.current[var] = value end end #If possible, distribute operation over both sides of an or. #For example, # # (1 or 2) * 5 # #Becomes # # (5 or 10) # #Only works for strings and numbers right now. def process_or_simple_operation exp arg = exp.first_arg return nil unless string? arg or number? arg target = exp.target lhs = process_or_target(target.lhs, exp.dup) rhs = process_or_target(target.rhs, exp.dup) if lhs and rhs if same_value? lhs, rhs lhs else exp.target.lhs = lhs exp.target.rhs = rhs exp.target end else nil end end def process_or_target value, copy if string? value or number? value copy.target = value process copy else false end end end ================================================ FILE: lib/brakeman/processors/base_processor.rb ================================================ require 'brakeman/processors/lib/processor_helper' require 'brakeman/processors/lib/safe_call_helper' require 'brakeman/util' #Base processor for most processors. class Brakeman::BaseProcessor < Brakeman::SexpProcessor include Brakeman::ProcessorHelper include Brakeman::SafeCallHelper include Brakeman::Util IGNORE = Sexp.new(:ignore).line(0) #Return a new Processor. def initialize tracker super() @last = nil @tracker = tracker @app_tree = tracker.app_tree if tracker @current_template = @current_module = @current_class = @current_method = @current_file = nil end def process_file exp, current_file @current_file = current_file process exp end def ignore IGNORE end #Process a new scope. Removes expressions that are set to nil. def process_scope exp #NOPE? end #Default processing. def process_default exp exp = exp.dup exp.each_with_index do |e, i| exp[i] = process e if sexp? e and not e.empty? end exp end #Process an if statement. def process_if exp exp = exp.dup condition = exp[1] = process exp.condition if true? condition exp[2] = process exp.then_clause if exp.then_clause exp[3] = nil elsif false? condition exp[2] = nil exp[3] = process exp.else_clause if exp.else_clause else exp[2] = process exp.then_clause if exp.then_clause exp[3] = process exp.else_clause if exp.else_clause end exp end #Processes calls with blocks. # #s(:iter, CALL, {:lasgn|:masgn}, BLOCK) def process_iter exp exp = exp.dup call = process exp.block_call #deal with assignments somehow if exp.block block = process exp.block block = nil if block.empty? else block = nil end call = Sexp.new(:iter, call, exp.block_args, block).compact call.line(exp.line) call end #String with interpolation. def process_dstr exp exp = exp.dup exp.shift exp.map! do |e| if e.is_a? String e else res = process e if res.empty? nil else res end end end.compact! exp.unshift :dstr end #Processes a block. Changes Sexp node type to :rlist def process_block exp exp = exp.dup exp.shift exp.map! do |e| process e end exp.unshift :rlist end alias process_rlist process_block #Processes the inside of an interpolated String. def process_evstr exp exp = exp.dup if exp[1] exp[1] = process exp[1] end exp end #Processes a hash def process_hash exp exp = exp.dup exp.shift exp.map! do |e| if sexp? e process e else e end end exp.unshift :hash end #Processes the values in an argument list def process_arglist exp exp = exp.dup exp.shift exp.map! do |e| process e end exp.unshift :arglist end #Processes a local assignment def process_lasgn exp exp = exp.dup exp.rhs = process exp.rhs exp end alias :process_iasgn :process_lasgn #Processes an instance variable assignment def process_iasgn exp exp = exp.dup exp.rhs = process exp.rhs exp end #Processes an attribute assignment, which can be either x.y = 1 or x[:y] = 1 def process_attrasgn exp exp = exp.dup exp.target = process exp.target exp.arglist = process exp.arglist exp end #Ignore ignore Sexps def process_ignore exp exp end def process_cdecl exp if @tracker @tracker.add_constant exp.lhs, exp.rhs, :file => current_file, :module => @current_module, :class => @current_class, :method => @current_method end exp end #Convenience method for `make_render exp, true` def make_render_in_view exp make_render exp, true end #Generates :render node from call to render. def make_render exp, in_view = false render_type, value, rest = find_render_type exp, in_view rest = process rest result = Sexp.new(:render, render_type, value, rest) result.line(exp.line) result end #Determines the type of a call to render. # #Possible types are: #:action, :default, :file, :inline, :js, :json, :nothing, :partial, #:template, :text, :update, :xml # #And also :layout for inside templates def find_render_type call, in_view = false rest = Sexp.new(:hash).line(call.line) type = nil value = nil first_arg = call.first_arg if call.second_arg.nil? and first_arg == Sexp.new(:lit, :update) return :update, nil, Sexp.new(:arglist, *call.args[0..-2]) #TODO HUH? end #Look for render :action, ... or render "action", ... if string? first_arg or symbol? first_arg if @current_template and @tracker.options[:rails3] type = :partial value = first_arg else type = :action value = first_arg end elsif first_arg.is_a? Symbol or first_arg.is_a? String type = :action value = Sexp.new(:lit, first_arg.to_sym).line(call.line) elsif first_arg.nil? type = :default elsif not hash? first_arg # Maybe do partial if in view? type = :action value = first_arg end types_in_hash = Set[:action, :file, :inline, :js, :json, :nothing, :partial, :template, :text, :update, :xml] #render :layout => "blah" means something else when in a template if in_view types_in_hash << :layout end last_arg = call.last_arg #Look for "type" of render in options hash #For example, render :file => "blah" if hash? last_arg hash_iterate(last_arg) do |key, val| if symbol? key and types_in_hash.include? key.value type = key.value value = val else rest << key << val end end end type ||= :default value ||= :default if type == :inline and string? value and not hash_access(rest, :type) value, rest = make_inline_render(value, rest) end return type, value, rest end def make_inline_render value, options require 'brakeman/parsers/template_parser' class_or_module = (@current_class || @current_module) class_or_module = if class_or_module.nil? "Unknown" else class_or_module.name end template_name = "#@current_method/inline@#{value.line}:#{class_or_module}".to_sym type, ast = Brakeman::TemplateParser.parse_inline_erb(@tracker, value.value) ast = ast.deep_clone(value.line) @tracker.processor.process_template(template_name, ast, type, nil, @current_file) @tracker.processor.process_template_alias(@tracker.templates[template_name]) return s(:lit, template_name).line(value.line), options end end ================================================ FILE: lib/brakeman/processors/config_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/processors/alias_processor' require 'brakeman/processors/lib/rails4_config_processor.rb' require 'brakeman/processors/lib/rails3_config_processor.rb' require 'brakeman/processors/lib/rails2_config_processor.rb' class Brakeman::ConfigProcessor def self.new tracker if tracker.options[:rails4] Brakeman::Rails4ConfigProcessor.new tracker elsif tracker.options[:rails3] Brakeman::Rails3ConfigProcessor.new tracker else Brakeman::Rails2ConfigProcessor.new tracker end end end ================================================ FILE: lib/brakeman/processors/controller_alias_processor.rb ================================================ require 'brakeman/processors/alias_processor' require 'brakeman/processors/lib/render_helper' require 'brakeman/processors/lib/render_path' require 'brakeman/processors/lib/find_return_value' #Processes aliasing in controllers, but includes following #renders in routes and putting variables into templates class Brakeman::ControllerAliasProcessor < Brakeman::AliasProcessor include Brakeman::RenderHelper #If only_method is specified, only that method will be processed, #other methods will be skipped. #This is for rescanning just a single action. def initialize tracker, only_method = nil super tracker @app_tree = tracker.app_tree @only_method = only_method @rendered = false @current_class = @current_module = @current_method = nil @method_cache = {} #Cache method lookups end def process_controller name, src, current_file if not node_type? src, :class Brakeman.debug "#{name} is not a class, it's a #{src.node_type}" return else @current_class = name @current_file = @app_tree.file_path(current_file) process_default src process_mixins end end #Process modules mixed into the controller, in case they contain actions. def process_mixins controller = @tracker.controllers[@current_class] original_file = @current_file controller.includes.each do |i| mixin = @tracker.libs[i] next unless mixin #Process methods in alphabetical order for consistency methods = mixin.methods_public.keys.map { |n| n.to_s }.sort.map { |n| n.to_sym } methods.each do |name| #Need to process the method like it was in a controller in order #to get the renders set processor = Brakeman::ControllerProcessor.new(@tracker, mixin.file) method = mixin.get_method(name).src.deep_clone if node_type? method, :defn method = processor.process_defn method else #Should be a defn, but this will catch other cases method = processor.process method end @current_file = mixin.file #Then process it like any other method in the controller process method end end ensure @current_file = original_file end #Skip it, must be an inner class def process_class exp exp end #Processes a method definition, which may include #processing any rendered templates. def process_defn exp meth_name = exp.method_name Brakeman.debug "Processing #{@current_class}##{meth_name}" #Skip if instructed to only process a specific method #(but don't skip if this method was called from elsewhere) return exp if @current_method.nil? and @only_method and @only_method != meth_name is_route = route? meth_name other_method = @current_method @current_method = meth_name @rendered = false if is_route meth_env do if is_route before_filter_list(@current_method, @current_class).each do |f| process_before_filter f end end process_all exp.body if is_route and not @rendered process_default_render exp end end @current_method = other_method exp end #Look for calls to head() def process_call exp exp = super return exp unless call? exp method = exp.method if method == :head @rendered = true elsif exp.target.nil? and method == :template_exists? env[exp.first_arg] = Sexp.new(:lit, :"brakeman:existing_template") elsif @tracker.options[:interprocedural] and @current_method and (exp.target.nil? or exp.target.node_type == :self) exp = get_call_value(exp) end exp end #Check for +respond_to+ def process_iter exp super if call? exp.block_call and exp.block_call.method == :respond_to @rendered = true end exp end #Processes a call to a before filter. #Basically, adds any instance variable assignments to the environment. #TODO: method arguments? def process_before_filter name filter = tracker.find_method name, @current_class if filter.nil? Brakeman.debug "Could not find filter #{name}" return end method = filter.src if ivars = @tracker.filter_cache[[filter.owner, name]] ivars.each do |variable, value| env[variable] = value end else processor = Brakeman::AliasProcessor.new @tracker processor.process_safely(method.body_list, only_ivars(:include_request_vars)) ivars = processor.only_ivars(:include_request_vars).all @tracker.filter_cache[[filter.owner, name]] = ivars ivars.each do |variable, value| env[variable] = value end end end #Processes the default template for the current action def process_default_render exp process_layout process_template template_name, nil, nil, nil end #Process template and add the current class and method name as called_from info def process_template name, args, _, line # If line is null, assume implicit render and set the end of the action # method as the line number if line.nil? and controller = @tracker.controllers[@current_class] if meth = controller.get_method(@current_method) if line = meth.src && meth.src.last && meth.src.last.line line += 1 else line = 1 end end end render_path = Brakeman::RenderPath.new.add_controller_render(@current_class, @current_method, line, @current_file) super name, args, render_path, line end #Turns a method name into a template name def template_name name = nil name ||= @current_method name = name.to_s if name.include? "/" name else controller = @current_class.to_s.gsub("Controller", "") controller.gsub!("::", "/") underscore(controller + "/" + name.to_s) end end #Determines default layout name def layout_name controller = @tracker.controllers[@current_class] return controller.layout if controller.layout return false if controller.layout == false app_controller = @tracker.controllers[:ApplicationController] return app_controller.layout if app_controller and app_controller.layout nil end #Returns true if the given method name is also a route def route? method if @tracker.routes[:allow_all_actions] or @tracker.options[:assume_all_routes] true else routes = @tracker.routes[@current_class] routes and (routes.include? :allow_all_actions or routes.include? method) end end #Get list of filters, including those that are inherited def before_filter_list method, klass controller = @tracker.controllers[klass] if controller controller.before_filter_list self, method else [] end end end ================================================ FILE: lib/brakeman/processors/controller_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/processors/lib/module_helper' require 'brakeman/tracker/controller' #Processes controller. Results are put in tracker.controllers class Brakeman::ControllerProcessor < Brakeman::BaseProcessor include Brakeman::ModuleHelper FORMAT_HTML = Sexp.new(:call, Sexp.new(:lvar, :format), :html) def initialize tracker, current_file = nil super(tracker) @visibility = :public @current_file = current_file @concerns = Set.new end #Use this method to process a Controller def process_controller src, current_file = @current_file @current_file = current_file process src end #s(:class, NAME, PARENT, s(:scope ...)) def process_class exp name = class_name(exp.class_name) parent = class_name(exp.parent_name) #If inside a real controller, treat any other classes as libraries. #But if not inside a controller already, then the class may include #a real controller, so we can't take this shortcut. if @current_class and @current_class.name.to_s.end_with? "Controller" Brakeman.debug "Treating inner class as library: #{name}" Brakeman::LibraryProcessor.new(@tracker).process_library exp, @current_file return exp end if not name.to_s.end_with? "Controller" Brakeman.debug "Adding noncontroller as library: #{name}" #Set the class to be a module in order to get the right namespacing. #Add class to libraries, in case it is needed later (e.g. it's used #as a parent class for a controller.) #However, still want to process it in this class, so have to set #@current_class to this not-really-a-controller thing. process_module exp, parent return exp end handle_class(exp, @tracker.controllers, Brakeman::Controller) do set_layout_name end exp end def process_module exp, parent = nil handle_module exp, Brakeman::Controller, parent end def process_concern concern_name return unless @current_class if mod = @tracker.find_class(concern_name) if mod.options[:included] and not @concerns.include? concern_name @concerns << concern_name process mod.options[:included].deep_clone end end end #Look for specific calls inside the controller def process_call exp return exp if process_call_defn? exp target = exp.target if sexp? target target = process target end method = exp.method first_arg = exp.first_arg last_arg = exp.last_arg #Methods called inside class definition #like attr_* and other settings if @current_method.nil? and target.nil? and @current_class if first_arg.nil? #No args case method when :private, :protected, :public @visibility = method when :protect_from_forgery @current_class.options[:protect_from_forgery] = true else #?? end else case method when :include if @current_class concern = class_name(first_arg) @current_class.add_include concern process_concern concern end when :before_filter, :append_before_filter, :before_action, :append_before_action if node_type? exp.first_arg, :iter add_lambda_filter exp else @current_class.add_before_filter exp end when :prepend_before_filter, :prepend_before_action if node_type? exp.first_arg, :iter add_lambda_filter exp else @current_class.prepend_before_filter exp end when :skip_before_filter, :skip_filter, :skip_before_action, :skip_action_callback @current_class.skip_filter exp when :layout if string? last_arg #layout "some_layout" name = last_arg.value.to_s if @app_tree.layout_exists?(name) @current_class.layout = "layouts/#{name}" else Brakeman.debug "Layout not found: #{name}" end elsif node_type? last_arg, :nil, :false #layout :false or layout nil @current_class.layout = false end else @current_class.add_option method, exp end end exp elsif target == nil and method == :render make_render exp elsif exp == FORMAT_HTML and context[1] != :iter #This is an empty call to # format.html #Which renders the default template if no arguments #Need to make more generic, though. call = Sexp.new :render, :default, @current_method call.line(exp.line) call else call = make_call target, method, process_all!(exp.args) call.line(exp.line) call end end #Look for before_filters and add fake ones if necessary def process_iter exp if @current_method.nil? and call? exp.block_call block_call_name = exp.block_call.method if block_call_name == :before_filter or block_call_name == :before_action add_fake_filter exp else super end else super end end #Sets default layout for renders inside Controller def set_layout_name return if @current_class.layout name = underscore(@current_class.name.to_s.split("::")[-1].gsub("Controller", '')) #There is a layout for this Controller if @app_tree.layout_exists?(name) @current_class.layout = "layouts/#{name}" end end #This is to handle before_filter do |controller| ... end # #We build a new method and process that the same way as usual #methods and filters. def add_fake_filter exp unless @current_class Brakeman.debug "Skipping before_filter outside controller: #{exp}" return exp end filter_name = ("fake_filter" + rand.to_s[/\d+$/]).to_sym args = exp.block_call.arglist args.insert(1, Sexp.new(:lit, filter_name).line(exp.line)) before_filter_call = make_call(nil, :before_filter, args).line(exp.line) if exp.block_args.length > 1 block_variable = exp.block_args[1] else block_variable = :temp end if node_type? exp.block, :block block_inner = exp.block.sexp_body else block_inner = [exp.block] end #Build Sexp for filter method body = Sexp.new(:lasgn, block_variable, Sexp.new(:call, Sexp.new(:const, @current_class.name).line(exp.line), :new).line(exp.line)).line(exp.line) filter_method = Sexp.new(:defn, filter_name, Sexp.new(:args).line(exp.line), body).concat(block_inner).line(exp.line) vis = @visibility @visibility = :private process_defn filter_method @visibility = vis process before_filter_call exp end def add_lambda_filter exp # Convert into regular block call e = exp.dup lambda_node = e.delete_at(3) result = Sexp.new(:iter, e).line(e.line) # Add block arguments if node_type? lambda_node[2], :args result << lambda_node[2].last else result << s(:args) end # Add block contents if sexp? lambda_node[3] result << lambda_node[3] end add_fake_filter result end end ================================================ FILE: lib/brakeman/processors/erb_template_processor.rb ================================================ require 'brakeman/processors/template_processor' #Processes ERB templates #(those ending in .html.erb or .rthml). class Brakeman::ErbTemplateProcessor < Brakeman::TemplateProcessor #s(:call, TARGET, :method, ARGS) def process_call exp target = exp.target if sexp? target target = process target end method = exp.method #_erbout is the default output variable for erb if node_type? target, :lvar and target.value == :_erbout if method == :concat or method == :<< @inside_concat = true exp.arglist = process(exp.arglist) @inside_concat = false if exp.second_arg raise "Did not expect more than a single argument to _erbout.concat" end arg = normalize_output(exp.first_arg) if arg.node_type == :str #ignore plain strings ignore else add_output arg end elsif method == :force_encoding ignore else abort "Unrecognized action on _erbout: #{method}" end elsif target == nil and method == :render exp.arglist = process(exp.arglist) make_render_in_view exp else exp.target = target exp.arglist = process(exp.arglist) exp end end #Process block, removing irrelevant expressions def process_block exp exp = exp.dup exp.shift if @inside_concat @inside_concat = false exp[0..-2].each do |e| process e end @inside_concat = true process exp.last else exp.map! do |e| res = process e if res.empty? or res == ignore nil elsif node_type?(res, :lvar) and res.value == :_erbout nil else res end end block = Sexp.new(:rlist).concat(exp).compact block.line(exp.line) block end end end ================================================ FILE: lib/brakeman/processors/erubi_template_procesor.rb ================================================ require 'brakeman/processors/template_processor' #Processes ERB templates using Erubi instead of erb. class Brakeman::ErubiTemplateProcessor < Brakeman::TemplateProcessor #s(:call, TARGET, :method, ARGS) def process_call exp target = exp.target if sexp? target target = process target end exp.target = target exp.arglist = process exp.arglist method = exp.method #_buf is the default output variable for Erubi if node_type?(target, :lvar, :ivar) and (target.value == :_buf or target.value == :@output_buffer) if method == :<< or method == :safe_concat arg = normalize_output(exp.first_arg) if arg.node_type == :str #ignore plain strings ignore elsif node_type? target, :ivar and target.value == :@output_buffer add_escaped_output arg else add_output arg end elsif method == :to_s ignore else abort "Unrecognized action on buffer: #{method}" end elsif target == nil and method == :render make_render_in_view exp else exp end end #Process blocks, ignoring :ignore exps def process_block exp exp = exp.dup exp.shift exp.map! do |e| res = process e if res.empty? or res == ignore nil else res end end block = Sexp.new(:rlist).concat(exp).compact block.line(exp.line) block end #Look for assignments to output buffer that look like this: # @output_buffer.append = some_output # @output_buffer.safe_append = some_output # @output_buffer.safe_expr_append = some_output def process_attrasgn exp if exp.target.node_type == :ivar and exp.target.value == :@output_buffer if append_method?(exp.method) exp.first_arg = process(exp.first_arg) arg = normalize_output(exp.first_arg) if arg.node_type == :str ignore elsif safe_append_method?(exp.method) add_output arg else add_escaped_output arg end else super end else super end end private def append_method?(method) method == :append= || safe_append_method?(method) end def safe_append_method?(method) method == :safe_append= || method == :safe_expr_append= end end ================================================ FILE: lib/brakeman/processors/gem_processor.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Processes Gemfile and Gemfile.lock class Brakeman::GemProcessor < Brakeman::BasicProcessor def initialize *args super @gem_name_version = /^\s*([-_+.A-Za-z0-9]+) \((\w(\.\w+)*)\)/ @ruby_version = /^\s+ruby (\d\.\d.\d+)/ end def process_gems gem_files @gem_files = gem_files @gemfile = gem_files[:gemfile] && gem_files[:gemfile][:file] @gemspec = gem_files[:gemspec] && gem_files[:gemspec][:file] if @gemspec process gem_files[:gemspec][:src] end if @gemfile process gem_files[:gemfile][:src] end if gem_files[:gemlock] process_gem_lock end @tracker.config.set_rails_version end # Known issue: Brakeman does not yet support `gem` calls with multiple # "version requirements". Consider the following example from the ruby docs: # # gem 'rake', '>= 1.1.a', '< 2' # # We are assuming that `second_arg` (eg. '>= 1.1.a') is the only requirement. # Perhaps we should instantiate an array of `::Gem::Requirement`s or even a # `::Gem::Dependency` and pass that to `Tracker::Config#add_gem`? def process_call exp if exp.target == nil if exp.method == :gem gem_name = exp.first_arg return exp unless string? gem_name gem_version = exp.second_arg version = if string? gem_version gem_version.value else nil end @tracker.config.add_gem gem_name.value, version, @gemfile, exp.line elsif exp.method == :ruby version = exp.first_arg if string? version @tracker.config.set_ruby_version version.value, @gemfile, exp.line end end elsif @inside_gemspec and exp.method == :add_dependency if string? exp.first_arg and string? exp.second_arg @tracker.config.add_gem exp.first_arg.value, exp.second_arg.value, @gemspec, exp.line end end exp end GEM_SPEC = s(:colon2, s(:const, :Gem), :Specification) def process_iter exp if exp.block_call.target == GEM_SPEC and exp.block_call.method == :new @inside_gemspec = true process exp.block if sexp? exp.block exp else process_default exp end ensure @inside_gemspec = false end def process_gem_lock line_num = 1 file = @gem_files[:gemlock][:file] @gem_files[:gemlock][:src].each_line do |line| set_gem_version_and_file line, file, line_num line_num += 1 end end # Supports .rc2 but not ~>, >=, or <= def set_gem_version_and_file line, file, line_num if line =~ @gem_name_version @tracker.config.add_gem $1, $2, file, line_num elsif line =~ @ruby_version @tracker.config.set_ruby_version $1, file, line_num end end end ================================================ FILE: lib/brakeman/processors/haml6_template_processor.rb ================================================ require 'brakeman/processors/haml_template_processor' class Brakeman::Haml6TemplateProcessor < Brakeman::HamlTemplateProcessor OUTPUT_BUFFER = s(:ivar, :@output_buffer) HAML_UTILS = s(:colon2, s(:colon3, :Haml), :Util) HAML_UTILS2 = s(:colon2, s(:const, :Haml), :Util) # @output_buffer = output_buffer || ActionView::OutputBuffer.new AV_SAFE_BUFFER = s(:or, s(:call, nil, :output_buffer), s(:call, s(:colon2, s(:const, :ActionView), :OutputBuffer), :new)) EMBEDDED_FILTER = s(:const, :BrakemanFilter) def initialize(*) super # Because of how Haml 6 handles line breaks - # we have to track where _haml_compiler variables are assigned. # then change the line number of where they are output to where # they are assigned. # # Like this: # # ; _haml_compiler1 = (params[:x]; # ; ); @output_buffer.safe_concat((((::Haml::Util.escape_html_safe((_haml_compiler1))).to_s).to_s)); # # `_haml_compiler1` is output a line after it's assigned, # but the assignment matches the "real" line where it is output in the template. @compiler_assigns = {} end # @output_buffer.safe_concat def buffer_append? exp call? exp and output_buffer? exp.target and exp.method == :safe_concat end def process_lasgn exp if exp.lhs.match?(/_haml_compiler\d+/) @compiler_assigns[exp.lhs] = exp.rhs ignore else exp end end def process_lvar exp if exp.value.match?(/_haml_compiler\d+/) exp = @compiler_assigns[exp.value] || exp end exp end def is_escaped? exp return unless call? exp html_escaped? exp or javascript_escaped? exp end def javascript_escaped? call # TODO: Adding here to match existing behavior for HAML, # but really this is not safe and needs to be revisited call.method == :j or call.method == :escape_javascript end def html_escaped? call (call.target == HAML_UTILS or call.target == HAML_UTILS2) and (call.method == :escape_html or call.method == :escape_html_safe) end def output_buffer? exp exp == OUTPUT_BUFFER or exp == AV_SAFE_BUFFER end def normalize_output arg arg = super(arg) if embedded_filter? arg super(arg.first_arg) else arg end end # Handle our "fake" embedded filters def embedded_filter? arg call? arg and arg.method == :render and arg.target == EMBEDDED_FILTER end end ================================================ FILE: lib/brakeman/processors/haml_template_processor.rb ================================================ require 'brakeman/processors/template_processor' #Processes HAML templates. class Brakeman::HamlTemplateProcessor < Brakeman::TemplateProcessor HAMLOUT = s(:call, nil, :_hamlout) HAML_BUFFER = s(:call, HAMLOUT, :buffer) HAML_HELPERS = s(:colon2, s(:const, :Haml), :Helpers) HAML_HELPERS2 = s(:colon2, s(:colon3, :Haml), :Helpers) JAVASCRIPT_FILTER = s(:colon2, s(:colon2, s(:const, :Haml), :Filters), :Javascript) COFFEE_FILTER = s(:colon2, s(:colon2, s(:const, :Haml), :Filters), :Coffee) ATTRIBUTE_BUILDER = s(:colon2, s(:colon3, :Haml), :AttributeBuilder) def initialize *args super @javascript = false end #Processes call, looking for template output def process_call exp exp = process_default exp if buffer_append? exp output = normalize_output(exp.first_arg) res = get_pushed_value(output) end res or exp end # _haml_out.buffer << ... def buffer_append? exp call? exp and exp.target == HAML_BUFFER and exp.method == :<< end PRESERVE_METHODS = [:find_and_preserve, :preserve] def find_and_preserve? exp call? exp and PRESERVE_METHODS.include?(exp.method) and exp.first_arg end #If inside an output stream, only return the final expression def process_block exp exp = exp.dup exp.shift exp.map! do |e| res = process e if res.empty? nil else res end end Sexp.new(:rlist).concat(exp).compact end #HAML likes to put interpolated values into _hamlout.push_text #but we want to handle those individually def build_output_from_push_text exp, default = :output if string_interp? exp exp.map! do |e| if sexp? e if node_type? e, :evstr and e[1] e = e.value end get_pushed_value e, default else e end end end end ESCAPE_METHODS = [ :html_escape, :html_escape_without_haml_xss, :escape_once, :escape_once_without_haml_xss ] def is_escaped? exp return unless call? exp haml_helpers? exp.target and ESCAPE_METHODS.include? exp.method end def get_pushed_value exp, default = :output return exp unless sexp? exp case exp.node_type when :format exp.node_type = :output @current_template.add_output exp exp when :format_escaped exp.node_type = :escaped_output @current_template.add_output exp exp when :str, :ignore, :output, :escaped_output exp when :block, :rlist exp.map! { |e| get_pushed_value(e, default) } when :dstr build_output_from_push_text(exp, default) when :if clauses = [get_pushed_value(exp.then_clause, default), get_pushed_value(exp.else_clause, default)].compact if clauses.length > 1 s(:or, *clauses).line(exp.line) else clauses.first end when :call if exp.method == :to_s or exp.method == :strip get_pushed_value(exp.target, default) elsif is_escaped? exp get_pushed_value(exp.first_arg, :escaped_output) elsif @javascript and (exp.method == :j or exp.method == :escape_javascript) # TODO: Remove - this is not safe get_pushed_value(exp.first_arg, :escaped_output) elsif find_and_preserve? exp or fix_textareas? exp get_pushed_value(exp.first_arg, default) elsif raw? exp get_pushed_value(exp.first_arg, :output) elsif hamlout_attributes? exp ignore # ignore _hamlout.attributes calls elsif exp.target.nil? and exp.method == :render #Process call to render() exp.arglist = process exp.arglist make_render_in_view exp elsif exp.method == :render_with_options if exp.target == JAVASCRIPT_FILTER or exp.target == COFFEE_FILTER @javascript = true end get_pushed_value(exp.first_arg, default) @javascript = false elsif haml_attribute_builder? exp ignore # probably safe... seems escaped by default? else add_output exp, default end else add_output exp, default end end def haml_helpers? exp # Sometimes its Haml::Helpers and # sometimes its ::Haml::Helpers exp == HAML_HELPERS or exp == HAML_HELPERS2 end def hamlout_attributes? exp call? exp and exp.target == HAMLOUT and exp.method == :attributes end def haml_attribute_builder? exp call? exp and exp.target == ATTRIBUTE_BUILDER and escaped_builder_method? exp end def escaped_builder_method? exp case exp.method when :build, :build_aria, :build_boolean, :build_data, :build_id, :escape_html true? exp.first_arg else false end end def fix_textareas? exp call? exp and exp.target == HAMLOUT and exp.method == :fix_textareas! end def raw? exp call? exp and exp.method == :raw end end ================================================ FILE: lib/brakeman/processors/lib/basic_processor.rb ================================================ require 'brakeman/processors/lib/processor_helper' require 'brakeman/processors/lib/safe_call_helper' require 'brakeman/util' class Brakeman::BasicProcessor < Brakeman::SexpProcessor include Brakeman::ProcessorHelper include Brakeman::SafeCallHelper include Brakeman::Util def initialize tracker super() @tracker = tracker @current_template = @current_module = @current_class = @current_method = nil end def process_default exp process_all exp end def process_if exp condition = exp.condition process condition if true? condition process exp.then_clause elsif false? condition process exp.else_clause else [exp.then_clause, exp.else_clause].compact.map do |e| process e end end exp end end ================================================ FILE: lib/brakeman/processors/lib/call_conversion_helper.rb ================================================ module Brakeman module CallConversionHelper # Join two array literals into one. def join_arrays lhs, rhs, original_exp = nil if array? lhs and array? rhs result = Sexp.new(:array) result.line(lhs.line || rhs.line || 1) result.concat lhs[1..-1] result.concat rhs[1..-1] result else original_exp end end STRING_LENGTH_LIMIT = 50 # Join two string literals into one. def join_strings lhs, rhs, original_exp = nil if string? lhs and string? rhs if (lhs.value.length + rhs.value.length > STRING_LENGTH_LIMIT) # Avoid gigantic strings lhs else result = Sexp.new(:str).line(lhs.line) result.value = lhs.value + rhs.value result end elsif call? lhs and lhs.method == :+ and string? lhs.first_arg and string? rhs joined = join_strings lhs.first_arg, rhs lhs.first_arg = joined lhs elsif safe_literal? lhs or safe_literal? rhs safe_literal(lhs.line) else original_exp end rescue Encoding::CompatibilityError => e # If the two strings are different encodings, we can't join them. Brakeman.debug e.inspect original_exp end def math_op op, lhs, rhs, original_exp = nil if number? lhs and number? rhs if op == :/ and rhs.value == 0 and not lhs.value.is_a? Float # Avoid division by zero return original_exp else value = lhs.value.send(op, rhs.value) Sexp.new(:lit, value).line(lhs.line) end elsif call? lhs and lhs.method == :+ and number? lhs.first_arg and number? rhs # (x + 1) + 2 -> (x + 3) lhs.first_arg = Sexp.new(:lit, lhs.first_arg.value + rhs.value).line(lhs.first_arg.line) lhs elsif safe_literal? lhs or safe_literal? rhs safe_literal(lhs.line) else original_exp end end # Process single integer access to an array. # # Returns the value inside the array, if possible. def process_array_access array, args, original_exp = nil if args.length == 1 and integer? args.first index = args.first.value #Have to do this because first element is :array and we have to skip it array[1..-1][index] or original_exp elsif all_literals? array safe_literal(array.line) else original_exp end end # Process hash access by returning the value associated # with the given argument. def process_hash_access hash, index, original_exp = nil if value = hash_access(hash, index) value # deep_clone? elsif all_literals? hash, :hash safe_literal(hash.line) else original_exp end end # You must check the return value for `nil`s - # which indicate a key could not be found. def hash_values_at hash, keys values = keys.map do |key| process_hash_access hash, key end Sexp.new(:array).concat(values).line(hash.line) end end end ================================================ FILE: lib/brakeman/processors/lib/file_type_detector.rb ================================================ module Brakeman class FileTypeDetector < BaseProcessor def initialize super(nil) reset end def detect_type(file) reset process(file.ast) if @file_type.nil? @file_type = guess_from_path(file.path.relative) end @file_type || :lib end MODEL_CLASSES = [ :'ActiveRecord::Base', :ApplicationRecord ] def process_class exp name = class_name(exp.class_name) parent = class_name(exp.parent_name) if name.match(/Controller$/) @file_type = :controller return exp elsif MODEL_CLASSES.include? parent @file_type = :model return exp end super end def guess_from_path path case when path.include?('app/models') :model when path.include?('app/controllers') :controller when path.include?('config/initializers') :initializer when path.include?('lib/') :lib when path.match?(%r{config/environments/(?!production\.rb)$}) :skip when path.match?(%r{environments/production\.rb$}) :skip when path.match?(%r{application\.rb$}) :skip when path.match?(%r{config/routes\.rb$}) :skip end end private def reset @file_type = nil end end end ================================================ FILE: lib/brakeman/processors/lib/find_all_calls.rb ================================================ require 'brakeman/processors/lib/basic_processor' class Brakeman::FindAllCalls < Brakeman::BasicProcessor attr_reader :calls def initialize tracker super @in_target = false @processing_class = false @calls = [] @cache = {} end #Process the given source. Provide either class and method being searched #or the template. These names are used when reporting results. def process_source exp, opts @current_class = opts[:class] @current_method = opts[:method] @current_template = opts[:template] @current_file = opts[:file] @current_call = nil @full_call = nil process exp end #For whatever reason, originally the indexing of calls #was performed on individual method bodies (see process_defn). #This method explicitly indexes all calls everywhere given any #source. def process_all_source exp, opts @processing_class = true process_source exp, opts ensure @processing_class = false end #Process body of method def process_defn exp return exp unless @current_method or @processing_class # 'Normal' processing assumes the method name was given # as an option to `process_source` but for `process_all_source` # we don't want to do that. if @current_method.nil? @current_method = exp.method_name process_all exp.body @current_method = nil else process_all exp.body end exp end alias process_defs process_defn #Process body of block def process_rlist exp process_all exp end def process_call exp @calls << create_call_hash(exp).freeze exp end def process_iter exp call = exp.block_call if call.node_type == :call call_hash = create_call_hash(call) call_hash[:block] = exp.block call_hash[:block_args] = exp.block_args call_hash.freeze @calls << call_hash process exp.block else #Probably a :render call with block process call process exp.block end exp end #Calls to render() are converted to s(:render, ...) but we would #like them in the call cache still for speed def process_render exp process_all exp add_simple_call :render, exp exp end #Technically, `` is call to Kernel#` #But we just need them in the call cache for speed def process_dxstr exp process exp.last if sexp? exp.last add_simple_call :`, exp exp end #:"string" is equivalent to "string".to_sym def process_dsym exp exp.each { |arg| process arg if sexp? arg } add_simple_call :literal_to_sym, exp exp end # Process a dynamic regex like a call def process_dregx exp exp.each { |arg| process arg if sexp? arg } add_simple_call :brakeman_regex_interp, exp exp end #Process an assignment like a call def process_attrasgn exp process_call exp end private def add_simple_call method_name, exp @calls << { :target => nil, :method => method_name, :call => exp, :nested => false, :location => make_location, :parent => @current_call, :full_call => @full_call }.freeze end #Gets the target of a call as a Symbol #if possible def get_target exp, include_calls = false if sexp? exp case exp.node_type when :ivar, :lvar, :const, :lit exp.value when :true, :false exp[0] when :colon2 class_name exp when :self @current_class || @current_module || nil when :params, :session, :cookies exp.node_type when :call, :safe_call if include_calls if exp.target.nil? exp.method else t = get_target(exp.target, :include_calls) if t.is_a? Symbol :"#{t}.#{exp.method}" else exp end end else exp end else exp end else exp end end #Returns method chain as an array #For example, User.human.alive.all would return [:User, :human, :alive, :all] def get_chain call if node_type? call, :call, :attrasgn, :safe_call, :safe_attrasgn get_chain(call.target) + [call.method] elsif call.nil? [] else [get_target(call)] end end def make_location if @current_template key = [@current_template, @current_file] cached = @cache[key] return cached if cached @cache[key] = { :type => :template, :template => @current_template, :file => @current_file } else key = [@current_class, @current_method, @current_file] cached = @cache[key] return cached if cached @cache[key] = { :type => :class, :class => @current_class, :method => @current_method, :file => @current_file } end end #Return info hash for a call Sexp def create_call_hash exp target = get_target exp.target target_symbol = get_target(target, :include_calls) method = exp.method call_hash = { :target => target_symbol, :method => method, :call => exp, :nested => @in_target, :chain => get_chain(exp), :location => make_location, :parent => @current_call, :full_call => @full_call } unless @in_target @full_call = call_hash call_hash[:full_call] = call_hash end # Process up the call chain if call? target or node_type? target, :dxstr # need to index `` even if target of a call already_in_target = @in_target @in_target = true process target @in_target = already_in_target end # Process call arguments # but add the current call as the 'parent' # to any calls in the arguments old_parent = @current_call @current_call = call_hash # Do not set @full_call when processing arguments old_full_call = @full_call @full_call = nil process_call_args exp @current_call = old_parent @full_call = old_full_call call_hash end end ================================================ FILE: lib/brakeman/processors/lib/find_call.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Finds method calls matching the given target(s). # #-- This should be deprecated --# # #-- Do not use for new code --# # #Targets/methods can be: # # - nil: matches anything, including nothing # - Empty array: matches nothing # - Symbol: matches single target/method exactly # - Array of symbols: matches against any of the symbols # - Regular expression: matches the expression # - Array of regular expressions: matches any of the expressions # #If a target is also the name of a class, methods called on instances #of that class will also be matched, in a very limited way. #(Any methods called on Klass.new, basically. More useful when used #in conjunction with AliasProcessor.) # #Examples: # # #To find any uses of this class: # FindCall.new :FindCall, nil # # #Find system calls without a target # FindCall.new [], [:system, :exec, :syscall] # # #Find all calls to length(), no matter the target # FindCall.new nil, :length # # #Find all calls to sub, sub!, gsub, or gsub! # FindCall.new nil, /^g?sub!?$/ class Brakeman::FindCall < Brakeman::BasicProcessor def initialize targets, methods, tracker super tracker @calls = [] @find_targets = targets @find_methods = methods @current_class = nil @current_method = nil end #Returns a list of results. # #A result looks like: # # s(:result, :ClassName, :method_name, s(:call, ...)) def matches @calls end #Process the given source. Provide either class and method being searched #or the template. These names are used when reporting results. # #Use FindCall#matches to retrieve results. def process_source exp process exp end #Process body of method def process_defn exp process_all exp.body end alias :process_defs :process_defn #Look for matching calls and add them to results def process_call exp target = get_target exp.target method = exp.method process_call_args exp if match(@find_targets, target) and match(@find_methods, method) @calls << Sexp.new(:result, @current_module, @current_class, @current_method, exp).line(exp.line) end exp end #Process an assignment like a call def process_attrasgn exp process_call exp end private #Gets the target of a call as a Symbol #if possible def get_target exp if sexp? exp case exp.node_type when :ivar, :lvar, :const, :lit exp.value when :colon2 class_name exp else exp end else exp end end #Checks if the search terms match the given item def match search_terms, item case search_terms when Symbol if search_terms == item true else false end when Enumerable if search_terms.empty? item == nil end end end end ================================================ FILE: lib/brakeman/processors/lib/find_return_value.rb ================================================ require 'brakeman/processors/alias_processor' #Attempts to determine the return value of a method. # #Preferred usage: # # Brakeman::FindReturnValue.return_value exp class Brakeman::FindReturnValue include Brakeman::Util #Returns a guess at the return value of a given method or other block of code. # #If multiple return values are possible, returns all values in an :or Sexp. def self.return_value exp, env = nil self.new.get_return_value exp, env end def initialize @uses_ivars = false @return_values = [] end def uses_ivars? @uses_ivars end #Find return value of Sexp. Takes an optional starting environment. def get_return_value exp, env = nil process_method exp, env value = make_return_value value.original_line = exp.line value end #Process method (or, actually, any Sexp) for return value. def process_method exp, env = nil exp = Brakeman::AliasProcessor.new.process_safely exp, env find_explicit_return_values exp if node_type? exp, :defn, :defs body = exp.body unless body.empty? @return_values << last_value(body) else Brakeman.debug "FindReturnValue: Empty method? #{exp.inspect}" end elsif exp @return_values << last_value(exp) else Brakeman.debug "FindReturnValue: Given something strange? #{exp.inspect}" end exp end #Searches expression for return statements. def find_explicit_return_values exp todo = [exp] until todo.empty? current = todo.shift @uses_ivars = true if node_type? current, :ivar if node_type? current, :return @return_values << last_value(current.value) if current.value elsif sexp? current todo = current[1..-1].concat todo end end end #Determines the "last value" of an expression. def last_value exp case exp.node_type when :rlist, :block, :scope, Sexp last_value exp.last when :if then_clause = exp.then_clause else_clause = exp.else_clause if then_clause.nil? and else_clause.nil? nil elsif then_clause.nil? last_value else_clause elsif else_clause.nil? last_value then_clause else true_branch = last_value then_clause false_branch = last_value else_clause if true_branch and false_branch value = make_or(true_branch, false_branch) value.original_line = value.rhs.line value else #Unlikely? true_branch or false_branch end end when :lasgn, :iasgn, :op_asgn_or, :attrasgn last_value exp.rhs when :rescue values = [] exp.each_sexp do |e| if node_type? e, :resbody if e.last values << last_value(e.last) end elsif sexp? e values << last_value(e) end end values.reject! do |v| v.nil? or node_type? v, :nil end if values.length > 1 values.inject do |m, v| make_or(m, v) end else values.first end when :return if exp.value last_value exp.value else nil end when :nil nil else exp.original_line = exp.line unless exp.original_line exp end end def make_or lhs, rhs #Better checks in future if lhs == rhs lhs else Sexp.new(:or, lhs, rhs) end end #Turns the array of return values into an :or Sexp def make_return_value @return_values.compact! @return_values.uniq! if @return_values.empty? Sexp.new(:nil) elsif @return_values.length == 1 @return_values.first else @return_values.reduce do |value, sexp| make_or value, sexp end end end end ================================================ FILE: lib/brakeman/processors/lib/module_helper.rb ================================================ module Brakeman::ModuleHelper def handle_module exp, tracker_class, parent = nil name = class_name(exp.module_name) if @current_module outer_module = @current_module name = (outer_module.name.to_s + "::" + name.to_s).to_sym end if @current_class name = (@current_class.name.to_s + "::" + name.to_s).to_sym end if @tracker.libs[name] @current_module = @tracker.libs[name] @current_module.add_file @current_file, exp else @current_module = tracker_class.new name, parent, @current_file, exp, @tracker @tracker.libs[name] = @current_module end exp.body = process_all! exp.body if outer_module @current_module = outer_module else @current_module = nil end exp end def handle_class exp, collection, tracker_class name = class_name(exp.class_name) parent = class_name exp.parent_name if @current_class outer_class = @current_class name = (outer_class.name.to_s + "::" + name.to_s).to_sym end if @current_module name = (@current_module.name.to_s + "::" + name.to_s).to_sym end if collection[name] @current_class = collection[name] @current_class.add_file @current_file, exp else @current_class = tracker_class.new name, parent, @current_file, exp, @tracker collection[name] = @current_class end exp.body = process_all! exp.body yield if block_given? if outer_class @current_class = outer_class else @current_class = nil end exp end def process_defs exp name = exp.method_name if node_type? exp[1], :self if @current_class target = @current_class.name elsif @current_module target = @current_module.name else target = nil end else target = class_name exp[1] end @current_method = name res = Sexp.new :defs, target, name, exp.formal_args, *process_all!(exp.body) res.line(exp.line) @current_method = nil # TODO: if target is not self/nil, then # the method should be added to `target`, not current class if @current_class @current_class.add_method @visibility, name, res, @current_file elsif @current_module @current_module.add_method @visibility, name, res, @current_file end res end def process_defn exp name = exp.method_name @current_method = name if @inside_sclass res = Sexp.new :defs, s(:self), name, exp.formal_args, *process_all!(exp.body) else res = Sexp.new :defn, name, exp.formal_args, *process_all!(exp.body) end res.line(exp.line) @current_method = nil if @current_class @current_class.add_method @visibility, name, res, @current_file elsif @current_module @current_module.add_method @visibility, name, res, @current_file end res end # class << self def process_sclass exp @inside_sclass = true process_all! exp exp ensure @inside_sclass = false end def make_defs exp # 'What if' there was some crazy code that had a # defs inside a def inside an sclass? :| return exp if node_type? exp, :defs raise "Unexpected node type: #{exp.node_type}" unless node_type? exp, :defn Sexp.new(:defs, s(:self), exp.method_name, exp.formal_args, *exp.body).line(exp.line) end end ================================================ FILE: lib/brakeman/processors/lib/processor_helper.rb ================================================ #Contains a couple shared methods for Processors. module Brakeman::ProcessorHelper def process_all exp exp.each_sexp do |e| process e end exp end def process_all! exp exp.map! do |e| if sexp? e process e else e end end exp end #Process the arguments of a method call. Does not store results. # #This method is used because Sexp#args and Sexp#arglist create new objects. def process_call_args exp exp.each_arg do |a| process a if sexp? a end exp end def process_class exp current_class = @current_class @current_class = class_name exp[1] process_all exp.body @current_class = current_class exp end #Sets the current module. def process_module exp module_name = class_name(exp.class_name).to_s prev_module = @current_module if prev_module @current_module = "#{prev_module}::#{module_name}" else @current_module = module_name end if block_given? yield else process_all exp.body end @current_module = prev_module exp end # e.g. private defn def process_call_defn? exp if call? exp and exp.target.nil? and node_type? exp.first_arg, :defn, :defs and [:private, :public, :protected].include? exp.method prev_visibility = @visibility @visibility = exp.method process exp.first_arg @visibility = prev_visibility exp else false end end def current_file case when @current_file @current_file when @current_class.is_a?(Brakeman::Collection) @current_class.file when @current_module.is_a?(Brakeman::Collection) @current_module.file else nil end end end ================================================ FILE: lib/brakeman/processors/lib/rails2_config_processor.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Processes configuration. Results are put in tracker.config. # #Configuration of Rails via Rails::Initializer are stored in tracker.config.rails. #For example: # # Rails::Initializer.run |config| # config.action_controller.session_store = :cookie_store # end # #will be stored in # # tracker.config[:rails][:action_controller][:session_store] # #Values for tracker.config.rails will still be Sexps. class Brakeman::Rails2ConfigProcessor < Brakeman::BasicProcessor #Replace block variable in # # Rails::Initializer.run |config| # #with this value so we can keep track of it. RAILS_CONFIG = Sexp.new(:const, :"!BRAKEMAN_RAILS_CONFIG") def initialize *args super end #Use this method to process configuration file def process_config src, current_file @current_file = current_file res = Brakeman::ConfigAliasProcessor.new.process_safely(src, nil, current_file) process res end # Check if config is set to use Erubis # but because it's 2026 we're going to use Erubi def process_call exp target = exp.target target = process target if sexp? target if exp.method == :gem and exp.first_arg.value == "erubis" Brakeman.debug "[Notice] Using Erubi for ERB templates" @tracker.config.erubi = true end exp end #Look for configuration settings def process_attrasgn exp if exp.target == RAILS_CONFIG #Get rid of '=' at end attribute = exp.method.to_s[0..-2].to_sym if exp.num_args > 1 #Multiple arguments?...not sure if this will ever happen @tracker.config.rails[attribute] = exp.args else @tracker.config.rails[attribute] = exp.first_arg end elsif include_rails_config? exp options = get_rails_config exp level = @tracker.config.rails options[0..-2].each do |o| level[o] ||= {} level = level[o] end level[options.last] = exp.first_arg end exp end #Check for Rails version def process_cdecl exp #Set Rails version required if exp.lhs == :RAILS_GEM_VERSION @tracker.config.set_rails_version exp.rhs.value end exp end #Check if an expression includes a call to set Rails config def include_rails_config? exp target = exp.target if call? target if target.target == RAILS_CONFIG true else include_rails_config? target end elsif target == RAILS_CONFIG true else false end end #Returns an array of symbols for each 'level' in the config # # config.action_controller.session_store = :cookie # #becomes # # [:action_controller, :session_store] def get_rails_config exp if node_type? exp, :attrasgn attribute = exp.method.to_s[0..-2].to_sym get_rails_config(exp.target) << attribute elsif call? exp if exp.target == RAILS_CONFIG [exp.method] else get_rails_config(exp.target) << exp.method end else raise "WHAT" end end end #This is necessary to replace block variable so we can track config settings class Brakeman::ConfigAliasProcessor < Brakeman::AliasProcessor RAILS_INIT = Sexp.new(:colon2, Sexp.new(:const, :Rails), :Initializer) #Look for a call to # # Rails::Initializer.run do |config| # ... # end # #and replace config with RAILS_CONFIG def process_iter exp target = exp.block_call.target method = exp.block_call.method if sexp? target and target == RAILS_INIT and method == :run env[Sexp.new(:lvar, exp.block_args.value)] = Brakeman::Rails2ConfigProcessor::RAILS_CONFIG end process_default exp end end ================================================ FILE: lib/brakeman/processors/lib/rails2_route_processor.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Processes the Sexp from routes.rb. Stores results in tracker.routes. # #Note that it is only interested in determining what methods on which #controllers are used as routes, not the generated URLs for routes. class Brakeman::Rails2RoutesProcessor < Brakeman::BasicProcessor include Brakeman::RouteHelper attr_reader :map, :nested, :current_controller def initialize tracker super @map = Sexp.new(:lvar, :map) @nested = nil #used for identifying nested targets @prefix = [] #Controller name prefix (a module name, usually) @current_controller = nil @with_options = nil #For use inside map.with_options @current_file = "config/routes.rb" end #Call this with parsed route file information. # #This method first calls RouteAliasProcessor#process_safely on the +exp+, #so it does not modify the +exp+. def process_routes exp process Brakeman::RouteAliasProcessor.new.process_safely(exp, nil, @current_file) end #Looking for mapping of routes def process_call exp target = exp.target if target == map or (not target.nil? and target == nested) process_map exp else process_default exp end exp end #Process a map.something call #based on the method used def process_map exp args = exp.args case exp.method when :resource process_resource args when :resources process_resources args when :connect, :root process_connect args else process_named_route args end exp end #Look for map calls that take a block. #Otherwise, just do the default processing. def process_iter exp target = exp.block_call.target if target == map or target == nested method = exp.block_call.method case method when :namespace process_namespace exp when :resources, :resource process_resources exp.block_call.args process_default exp.block if exp.block when :with_options process_with_options exp end exp else process_default exp end end #Process # map.resources :x, :controller => :y, :member => ... #etc. def process_resources exp controller = check_for_controller_name exp if controller self.current_controller = controller process_resource_options exp[-1] else exp.each do |argument| if node_type? argument, :lit self.current_controller = exp.first.value add_resources_routes process_resource_options exp.last end end end end #Process all the options that might be in the hash passed to #map.resource, et al. def process_resource_options exp if exp.nil? and @with_options exp = @with_options elsif @with_options exp = exp.concat @with_options[1..-1] end return unless exp.node_type == :hash hash_iterate(exp) do |option, value| case option[1] when :controller, :requirements, :singular, :path_prefix, :as, :path_names, :shallow, :name_prefix, :member_path, :nested_member_path, :belongs_to, :conditions, :active_scaffold #should be able to skip when :collection, :member, :new process_collection value when :has_one save_controller = current_controller process_resource value[1..-1] #Verify this is proper behavior self.current_controller = save_controller when :has_many save_controller = current_controller process_resources value[1..-1] self.current_controller = save_controller when :only process_option_only value when :except process_option_except value else Brakeman.alert "Unhandled resource option, please report: #{option}" end end end #Process route option :only => ... def process_option_only exp routes = @tracker.routes[@current_controller] [:index, :new, :create, :show, :edit, :update, :destroy].each do |r| routes.delete r end if exp.node_type == :array exp[1..-1].each do |e| routes << e.value end end end #Process route option :except => ... def process_option_except exp return unless exp.node_type == :array routes = @tracker.routes[@current_controller] exp[1..-1].each do |e| routes.delete e.value end end # map.resource :x, .. def process_resource exp controller = check_for_controller_name exp if controller self.current_controller = controller process_resource_options exp.last else exp.each do |argument| if node_type? argument, :lit self.current_controller = pluralize(exp.first.value.to_s) add_resource_routes process_resource_options exp.last end end end end #Process # map.connect '/something', :controller => 'blah', :action => 'whatever' def process_connect exp return if exp.empty? controller = check_for_controller_name exp self.current_controller = controller if controller #Check for default route if string? exp.first if exp.first.value == ":controller/:action/:id" @tracker.routes[:allow_all_actions] = exp.first elsif exp.first.value.include? ":action" @tracker.routes[@current_controller] = [:allow_all_actions, exp.line] return end end #This -seems- redundant, but people might connect actions #to a controller which already allows them all return if @tracker.routes[@current_controller].is_a? Array and @tracker.routes[@current_controller][0] == :allow_all_actions exp.last.each_with_index do |e,i| if symbol? e and e.value == :action action = exp.last[i + 1] if node_type? action, :lit @tracker.routes[@current_controller] << action.value.to_sym end return end end end # map.with_options :controller => 'something' do |something| # something.resources :blah # end def process_with_options exp @with_options = exp.block_call.last_arg @nested = Sexp.new(:lvar, exp.block_args.value) self.current_controller = check_for_controller_name exp.block_call.args #process block process exp.block @with_options = nil @nested = nil end # map.namespace :something do |something| # something.resources :blah # end def process_namespace exp call = exp.block_call formal_args = exp.block_args block = exp.block @prefix << camelize(call.first_arg.value) if formal_args @nested = Sexp.new(:lvar, formal_args.value) end process block @prefix.pop end # map.something_abnormal '/blah', :controller => 'something', :action => 'wohoo' def process_named_route exp process_connect exp end #Process collection option # :collection => { :some_action => :http_actions } def process_collection exp return unless exp.node_type == :hash routes = @tracker.routes[@current_controller] hash_iterate(exp) do |action, _type| routes << action.value end end private #Checks an argument list for a hash that has a key :controller. #If it does, returns the value. # #Otherwise, returns nil. def check_for_controller_name args args.each do |a| if hash? a and value = hash_access(a, :controller) return value.value if string? value or symbol? value end end nil end end #This is for a really specific case where a hash is used as arguments #to one of the map methods. class Brakeman::RouteAliasProcessor < Brakeman::AliasProcessor #This replaces # { :some => :hash }.keys #with # [:some] def process_call exp process_default exp if hash? exp.target and exp.method == :keys keys = get_keys exp.target exp.clear keys.each_with_index do |e,i| exp[i] = e end end exp end #Returns an array Sexp containing the keys from the hash def get_keys hash keys = Sexp.new(:array) hash_iterate(hash) do |key, _value| keys << key end keys end end ================================================ FILE: lib/brakeman/processors/lib/rails3_config_processor.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Processes configuration. Results are put in tracker.config. # #Configuration of Rails via Rails::Initializer are stored in tracker.config.rails. #For example: # # MyApp::Application.configure do # config.active_record.whitelist_attributes = true # end # #will be stored in # # tracker.config.rails[:active_record][:whitelist_attributes] # #Values for tracker.config.rails will still be Sexps. class Brakeman::Rails3ConfigProcessor < Brakeman::BasicProcessor RAILS_CONFIG = Sexp.new(:call, nil, :config) RAILS_APPLICATION = Sexp.new(:colon2, s(:const, :Rails), :Application) def initialize *args super @inside_config = false end #Use this method to process configuration file def process_config src, current_file @current_file = current_file res = Brakeman::AliasProcessor.new(@tracker).process_safely(src, nil, @current_file) process res end #Look for MyApp::Application.configure do ... end def process_iter exp call = exp.block_call if node_type?(call.target, :colon2) and call.target.rhs == :Application and call.method == :configure @inside_config = true process exp.block if sexp? exp.block @inside_config = false end exp end #Look for class Application < Rails::Application def process_class exp if application_class? exp @inside_config = true process_all exp.body if sexp? exp.body @inside_config = false end exp end def application_class? exp return unless node_type? exp, :class exp.class_name == :Application or (node_type? exp.class_name, :colon2 and exp.class_name.rhs == :Application) or (exp.parent_name == RAILS_APPLICATION) end #Look for configuration settings that #are just a call like # # config.load_defaults 5.2 def process_call exp return exp unless @inside_config if exp.target == RAILS_CONFIG and exp.first_arg @tracker.config.rails[exp.method] = exp.first_arg end exp end #Look for configuration settings def process_attrasgn exp return exp unless @inside_config if exp.target == RAILS_CONFIG #Get rid of '=' at end attribute = exp.method.to_s[0..-2].to_sym if exp.num_args > 1 #Multiple arguments?...not sure if this will ever happen @tracker.config.rails[attribute] = exp.args else @tracker.config.rails[attribute] = exp.first_arg end elsif include_rails_config? exp options_path = get_rails_config exp @tracker.config.set_rails_config(value: exp.first_arg, path: options_path, overwrite: true) end exp end #Check if an expression includes a call to set Rails config def include_rails_config? exp target = exp.target if call? target if target.target == RAILS_CONFIG true else include_rails_config? target end elsif target == RAILS_CONFIG true else false end end #Returns an array of symbols for each 'level' in the config # # config.action_controller.session_store = :cookie # #becomes # # [:action_controller, :session_store] def get_rails_config exp if node_type? exp, :attrasgn attribute = exp.method.to_s[0..-2].to_sym get_rails_config(exp.target) << attribute elsif call? exp if exp.target == RAILS_CONFIG [exp.method] else get_rails_config(exp.target) << exp.method end else raise "WHAT" end end end ================================================ FILE: lib/brakeman/processors/lib/rails3_route_processor.rb ================================================ require 'brakeman/processors/lib/basic_processor' #Processes the Sexp from routes.rb. Stores results in tracker.routes. # #Note that it is only interested in determining what methods on which #controllers are used as routes, not the generated URLs for routes. class Brakeman::Rails3RoutesProcessor < Brakeman::BasicProcessor include Brakeman::RouteHelper attr_reader :map, :nested, :current_controller def initialize tracker super @map = Sexp.new(:lvar, :map) @nested = nil #used for identifying nested targets @prefix = [] #Controller name prefix (a module name, usually) @current_controller = nil @with_options = nil #For use inside map.with_options @controller_block = false @current_file = "config/routes.rb" end def process_routes exp process Brakeman::AliasProcessor.new.process_safely(exp, nil, @current_file) end def process_call exp case exp.method when :resources process_resources exp when :resource process_resource exp when :root process_root exp when :member process_default exp when :get, :put, :post, :delete process_verb exp when :match process_match exp else exp end end def process_iter exp case exp.block_call.method when :namespace process_namespace exp when :resource process_resource_block exp when :resources process_resources_block exp when :scope process_scope_block exp when :controller process_controller_block exp else process_default exp end end def process_namespace exp arg = exp.block_call.first_arg return exp unless symbol? arg or string? arg name = arg.value block = exp.block @prefix << camelize(name) process block @prefix.pop exp end #TODO: Need test for this def process_root exp return exp unless hash? exp.first_arg if value = hash_access(exp.first_arg, :to) if string? value add_route_from_string value end end exp end def process_match exp first_arg = exp.first_arg second_arg = exp.second_arg last_arg = exp.last_arg if string? first_arg matcher = first_arg.value if matcher == ':controller(/:action(/:id(.:format)))' or matcher.include? ':controller' and action_route?(matcher) #Default routes @tracker.routes[:allow_all_actions] = first_arg return exp elsif action_route?(first_arg) if hash? second_arg and controller_name = hash_access(second_arg, :controller) loose_action(controller_name, "matched") #TODO: Parse verbs end elsif second_arg.nil? and in_controller_block? and not matcher.include? ":" add_route matcher end end if hash? last_arg hash_iterate last_arg do |k, v| if string? k if string? v add_route_from_string v elsif in_controller_block? and symbol? v add_route v end elsif symbol? k case k.value when :action if string? v add_route_from_string v else add_route v end when :to if string? v add_route_from_string v[1] elsif in_controller_block? and symbol? v add_route v end end end end end @current_controller = nil unless in_controller_block? exp end def add_route_from_string value value = value[1] if string? value controller, action = extract_action value if action add_route action, controller elsif in_controller_block? add_route value end end def process_verb exp first_arg = exp.first_arg second_arg = exp.second_arg if symbol? first_arg and not hash? second_arg add_route first_arg elsif hash? second_arg hash_iterate second_arg do |k, v| if symbol? k and k.value == :to if string? v add_route_from_string v elsif in_controller_block? and symbol? v add_route v end elsif action_route?(first_arg) if hash? second_arg and controller_name = hash_access(second_arg, :controller) loose_action(controller_name, exp.method) end end end elsif string? first_arg if first_arg.value.include? ':controller' and action_route?(first_arg) #Default routes @tracker.routes[:allow_all_actions] = first_arg end route = first_arg.value.split "/" if route.length != 2 add_route route[0] else add_route route[1], route[0] end else hash? first_arg hash_iterate first_arg do |k, v| if string? k if string? v add_route_from_string v elsif in_controller_block? add_route v end end end end @current_controller = nil unless in_controller_block? exp end def process_resources exp first_arg = exp.first_arg second_arg = exp.second_arg return exp unless symbol? first_arg or string? first_arg if second_arg and second_arg.node_type == :hash self.current_controller = first_arg.value #handle hash add_resources_routes elsif exp.args.all? { |s| symbol? s } exp.each_arg do |s| self.current_controller = s.value add_resources_routes end end @current_controller = nil unless in_controller_block? exp end def process_resource exp #Does resource even take more than one controller name? exp.each_arg do |s| if symbol? s self.current_controller = pluralize(s.value.to_s) add_resource_routes else #handle something else, like options #or something? end end @current_controller = nil unless in_controller_block? exp end def process_resources_block exp in_controller_block do process_resources exp.block_call process exp.block end @current_controller = nil exp end def process_resource_block exp in_controller_block do process_resource exp.block_call process exp.block end @current_controller = nil exp end def process_scope_block exp #How to deal with options? process exp.block exp end def process_controller_block exp if string? exp or symbol? exp self.current_controller = exp.block_call.first_arg.value in_controller_block do process exp[-1] if exp[-1] end @current_controller = nil end exp end def extract_action str str.split "#" end def in_controller_block? @controller_block end def in_controller_block prev_block = @controller_block @controller_block = true yield @controller_block = prev_block end def action_route? arg if string? arg arg = arg.value end arg.is_a? String and (arg.include? ":action" or arg.include? "*action") end def loose_action controller_name, verb = "any" self.current_controller = controller_name.value @tracker.routes[@current_controller] = [:allow_all_actions, {:allow_verb => verb}] end end ================================================ FILE: lib/brakeman/processors/lib/rails4_config_processor.rb ================================================ require 'brakeman/processors/lib/rails3_config_processor' class Brakeman::Rails4ConfigProcessor < Brakeman::Rails3ConfigProcessor APPLICATION_CONFIG = s(:call, s(:call, s(:const, :Rails), :application), :configure) ALT_APPLICATION_CONFIG = s(:call, s(:call, s(:colon3, :Rails), :application), :configure) # Look for Rails.application.configure do ... end def process_iter exp if exp.block_call == APPLICATION_CONFIG or exp.block_call == ALT_APPLICATION_CONFIG @inside_config = true process exp.block if sexp? exp.block @inside_config = false else super end exp end end ================================================ FILE: lib/brakeman/processors/lib/render_helper.rb ================================================ require 'digest/sha1' #Processes a call to render() in a controller or template module Brakeman::RenderHelper #Process s(:render, TYPE, OPTION?, OPTIONS) def process_render exp process_default exp @rendered = true case exp.render_type when :action, :template, :inline action = exp[2] args = exp[3] if string? action or symbol? action process_action action.value, args, exp.line else process_model_action action, args end when :default begin process_template template_name, exp[3], nil, exp.line rescue ArgumentError Brakeman.debug "Problem processing render: #{exp}" end when :partial, :layout process_partial exp[2], exp[3], exp.line when :nothing end exp end #Processes layout def process_layout name = nil if name.nil? and defined? layout_name name = layout_name end return unless name process_template name, nil, nil, nil end #Determines file name for partial and then processes it def process_partial name, args, line if !(string? name or symbol? name) or name.value == "" return end names = name.value.to_s.split("/") names[-1] = "_" + names[-1] process_template template_name(names.join("/")), args, nil, line end #Processes a given action def process_action name, args, line if name.is_a? String or name.is_a? Symbol process_template template_name(name), args, nil, line else Brakeman.debug "Not processing render #{name.inspect}" end end SINGLE_RECORD = [:first, :find, :last, :new] COLLECTION = [:all, :where] def process_model_action action, args return unless call? action method = action.method klass = get_class_target(action) || Brakeman::Tracker::UNKNOWN_MODEL name = Sexp.new(:lit, klass.downcase) if SINGLE_RECORD.include? method # Set a local variable with name based on class of model # and value of the value passed to render local_key = Sexp.new(:lit, :locals) locals = hash_access(args, local_key) || Sexp.new(:hash) hash_insert(locals, name, action) hash_insert(args, local_key, locals) process_partial name, args, action.line elsif COLLECTION.include? method collection_key = Sexp.new(:lit, :collection) hash_insert(args, collection_key, action) process_partial name, args, action.line end end #Processes a template, adding any instance variables #to its environment. def process_template name, args, called_from = nil, *_ Brakeman.debug "Rendering #{name} (#{called_from})" #Get scanned source for this template name = name.to_s.gsub(/^\//, "") template = @tracker.templates[name.to_sym] unless template Brakeman.debug "No such template: #{name}" return end if called_from # Track actual template that was rendered called_from.last_template = template end template_env = only_ivars(:include_request_vars) #Hash the environment and the source of the template to avoid #pointlessly processing templates, which can become prohibitively #expensive in terms of time and memory. digest = Digest::SHA1.new.update(template_env.instance_variable_get(:@env).to_a.sort.to_s << name).to_s.to_sym if @tracker.template_cache.include? digest #Already processed this template with identical environment return else @tracker.template_cache << digest options = get_options args #Process layout if string? options[:layout] process_template "layouts/#{options[:layout][1]}", nil, nil, nil elsif node_type? options[:layout], :false #nothing elsif not template.name.to_s.match(/[^\/_][^\/]+$/) #Don't do this for partials process_layout end if hash? options[:locals] hash_iterate options[:locals] do |key, value| if symbol? key template_env[Sexp.new(:call, nil, key.value)] = value end end end if options[:collection] #The collection name is the name of the partial without the leading #underscore. variable = template.name.to_s.match(/[^\/_][^\/]+$/)[0].to_sym #Unless the :as => :variable_name option is used if options[:as] if string? options[:as] or symbol? options[:as] variable = options[:as].value.to_sym end end collection = get_class_target(options[:collection]) || Brakeman::Tracker::UNKNOWN_MODEL template_env[Sexp.new(:call, nil, variable)] = Sexp.new(:call, Sexp.new(:const, collection), :new) end #Set original_line for values so it is clear #that values came from another file template_env.all.each do |_var, value| unless value.original_line #TODO: This has been broken for a while now and no one noticed #so maybe we can skip it value.original_line = value.line end end #Run source through AliasProcessor with instance variables from the #current environment. #TODO: Add in :locals => { ... } to environment src = Brakeman::TemplateAliasProcessor.new(@tracker, template, called_from).process_safely(template.src, template_env) digest = Digest::SHA1.new.update(name + src.to_s).to_s.to_sym if @tracker.template_cache.include? digest return else @tracker.template_cache << digest end #Run alias-processed src through the template processor to pull out #information and outputs. #This information will be stored in tracker.templates, but with a name #specifying this particular route. The original source should remain #pristine (so it can be processed within other environments). @tracker.processor.process_template name, src, template.type, called_from, template.file end end #Override to process name, such as adding the controller name. def template_name name raise "RenderHelper#template_name should be overridden." end #Turn options Sexp into hash def get_options args options = {} return options unless hash? args hash_iterate args do |key, value| if symbol? key options[key.value] = value end end options end def get_class_target sexp if call? sexp get_class_target sexp.target else klass = class_name sexp if klass.is_a? Symbol klass else nil end end end end ================================================ FILE: lib/brakeman/processors/lib/render_path.rb ================================================ module Brakeman class RenderPath attr_reader :path def initialize @path = [] end def add_controller_render controller_name, method_name, line, file method_name ||= "" @path << { :type => :controller, :class => controller_name.to_sym, :method => method_name.to_sym, :line => line, :file => file } self end def add_template_render template_name, line, file @path << { :type => :template, :name => template_name.to_sym, :line => line, :file => file } self end def last_template= template if @path.last @path.last[:rendered] = { name: template.name, file: template.file, } else Brakeman.debug "No render path to add template information" end end def include_template? name name = name.to_sym @path.any? do |loc| loc[:type] == :template and loc[:name] == name end end def include_controller? klass klass = klass.to_sym @path.any? do |loc| loc[:type] == :controller and loc[:class] == klass end end def include_any_method? method_names names = method_names.map(&:to_sym) @path.any? do |loc| loc[:type] == :controller and names.include? loc[:method] end end def rendered_from_controller? @path.any? do |loc| loc[:type] == :controller end end def each &block @path.each(&block) end def join *args self.to_a.join(*args) end def length @path.length end def map &block @path.map(&block) end def to_a @path.map do |loc| case loc[:type] when :template "Template:#{loc[:name]}" when :controller "#{loc[:class]}##{loc[:method]}" end end end def last self.to_a.last end def to_s self.to_a.to_s end def to_sym self.to_s.to_sym end def to_json *args require 'json' JSON.generate(@path) end def with_relative_paths @path.map do |loc| r = loc.dup if r[:file] r[:file] = r[:file].relative end if r[:rendered] and r[:rendered][:file] r[:rendered] = r[:rendered].dup r[:rendered][:file] = r[:rendered][:file].relative end r end end def initialize_copy original @path = original.path.dup self end end end ================================================ FILE: lib/brakeman/processors/lib/route_helper.rb ================================================ module Brakeman::RouteHelper #Manage Controller prefixes #@prefix is an Array, but this method returns a string #suitable for prefixing onto a controller name. def prefix if @prefix.length > 0 @prefix.join("::") << "::" else '' end end #Sets the controller name to a proper class name. #For example # self.current_controller = :session # @controller == :SessionController #true # #Also prepends the prefix if there is one set. def current_controller= name @current_controller = (prefix + camelize(name) + "Controller").to_sym @tracker.routes[@current_controller] ||= Set.new end #Add route to controller. If a controller is specified, #the current controller will be set to that controller. #If no controller is specified, uses current controller value. def add_route route, controller = nil if node_type? route, :str, :lit route = route.value end return unless route.is_a? String or route.is_a? Symbol if route.is_a? String and controller.nil? and route.include? ":controller" controller = ":controller" end route = route.to_sym if controller self.current_controller = controller end routes = @tracker.routes[@current_controller] if routes and not routes.include? :allow_all_actions routes << route end end #Add default routes def add_resources_routes existing_routes = @tracker.routes[@current_controller] unless existing_routes.is_a? Array and existing_routes.first == :allow_all_actions existing_routes.merge [:index, :new, :create, :show, :edit, :update, :destroy] end end #Add default routes minus :index def add_resource_routes existing_routes = @tracker.routes[@current_controller] unless existing_routes.is_a? Array and existing_routes.first == :allow_all_actions existing_routes.merge [:new, :create, :show, :edit, :update, :destroy] end end end ================================================ FILE: lib/brakeman/processors/lib/safe_call_helper.rb ================================================ module Brakeman module SafeCallHelper [[:process_safe_call, :process_call], [:process_safe_attrasgn, :process_attrasgn], [:process_safe_op_asgn, :process_op_asgn], [:process_safe_op_asgn2, :process_op_asgn2]].each do |call, replacement| define_method(call) do |exp| if self.respond_to? replacement self.send(replacement, exp) else process_default exp end end end end end ================================================ FILE: lib/brakeman/processors/library_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/processors/alias_processor' require 'brakeman/processors/lib/module_helper' require 'brakeman/tracker/library' #Process generic library and stores it in Tracker.libs class Brakeman::LibraryProcessor < Brakeman::BaseProcessor include Brakeman::ModuleHelper def initialize tracker super @current_file = nil @alias_processor = Brakeman::AliasProcessor.new tracker @current_module = nil @current_class = nil @initializer_env = nil end def process_library src, current_file = @current_file @current_file = current_file process src end def process_class exp handle_class exp, @tracker.libs, Brakeman::Library end def process_module exp handle_module exp, Brakeman::Library end def process_defn exp # TODO: Why is this different from ModuleHelper? if @inside_sclass exp = make_defs(exp) end if exp.method_name == :initialize @alias_processor.process_safely exp.body_list @initializer_env = @alias_processor.only_ivars elsif node_type? exp, :defn exp = @alias_processor.process_safely exp, @initializer_env else exp = @alias_processor.process exp end if @current_class exp.body = process_all! exp.body @current_class.add_method :public, exp.method_name, exp, @current_file elsif @current_module exp.body = process_all! exp.body @current_module.add_method :public, exp.method_name, exp, @current_file end exp end alias process_defs process_defn def process_call exp if process_call_defn? exp exp elsif @current_method.nil? and exp.target.nil? and (@current_class or @current_module) # Methods called inside class / module case exp.method when :include module_name = class_name(exp.first_arg) (@current_class || @current_module).add_include module_name end exp else process_default exp end end def process_iter exp res = process_default exp if node_type? res, :iter and call? exp.block_call # sometimes this changes after processing if exp.block_call.method == :included and (@current_module or @current_class) (@current_module || @current_class).options[:included] = res.block end end res end end ================================================ FILE: lib/brakeman/processors/model_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/processors/lib/module_helper' require 'brakeman/tracker/model' #Processes models. Puts results in tracker.models class Brakeman::ModelProcessor < Brakeman::BaseProcessor include Brakeman::ModuleHelper def initialize tracker super @current_class = nil @current_method = nil @current_module = nil @visibility = :public @current_file = nil end #Process model source def process_model src, current_file = @current_file @current_file = current_file process src end #s(:class, NAME, PARENT, BODY) def process_class exp name = class_name(exp.class_name) #If inside an inner class we treat it as a library. if @current_class Brakeman.debug "Treating inner class as library: #{name}" Brakeman::LibraryProcessor.new(@tracker).process_library exp, @current_file return exp end handle_class exp, @tracker.models, Brakeman::Model end def process_module exp handle_module exp, Brakeman::Model end #Handle calls outside of methods, #such as include, attr_accessible, private, etc. def process_call exp return exp unless @current_class return exp if process_call_defn? exp target = exp.target if sexp? target target = process target end method = exp.method first_arg = exp.first_arg #Methods called inside class definition #like attr_* and other settings if @current_method.nil? and target.nil? if first_arg.nil? case method when :private, :protected, :public @visibility = method when :attr_accessible @current_class.set_attr_accessible else #?? end else case method when :include @current_class.add_include class_name(first_arg) if @current_class when :attr_accessible @current_class.set_attr_accessible exp when :attr_protected @current_class.set_attr_protected exp when :enum add_enum_method exp else if @current_class @current_class.add_option method, exp end end end exp else call = make_call target, method, process_all!(exp.args) call.line(exp.line) call end end def add_enum_method call arg = call.first_arg return unless hash? arg return unless symbol? arg[1] enum_name = arg[1].value # first key enums = arg[2] # first value enums_name = pluralize(enum_name.to_s).to_sym call_line = call.line if hash? enums enum_values = enums elsif array? enums # Build hash for enum values like Rails does enum_values = s(:hash).line(call_line) enums.each_sexp.with_index do |v, index| enum_values << v enum_values << s(:lit, index).line(call_line) end end enum_method = s(:defn, enum_name, s(:args), safe_literal(call_line)) enums_method = s(:defs, s(:self), enums_name, s(:args), enum_values) @current_class.add_method :public, enum_name, enum_method, @current_file @current_class.add_method :public, enums_name, enums_method, @current_file end end ================================================ FILE: lib/brakeman/processors/output_processor.rb ================================================ Brakeman.load_brakeman_dependency 'ruby2ruby' require 'brakeman/util' #Produces formatted output strings from Sexps. #Recommended usage is # # OutputProcessor.new.format(Sexp.new(:str, "hello")) class Brakeman::OutputProcessor < Ruby2Ruby include Brakeman::Util def initialize *args super @user_input = nil end #Copies +exp+ and then formats it. def format exp, user_input = nil, &block @user_input = user_input @user_input_block = block process(exp.deep_clone) || "[Format Error]" end alias process_safely format def process exp begin if @user_input and @user_input == exp @user_input_block.call(exp, super(exp)) else super exp if sexp? exp and not exp.empty? end rescue => e Brakeman.debug "While formatting #{exp}: #{e}\n#{e.backtrace.join("\n")}" end end def process_ignore exp "[ignored]" end def process_params exp "params" end def process_session exp "session" end def process_cookies exp "cookies" end def process_rlist exp out = exp.map do |e| res = process e if res == "" nil else res end end.compact.join("\n") out end def process_defn exp # Copied from Ruby2Ruby except without the whole # "convert methods to attr_*" stuff exp = exp.deep_clone exp.shift name = exp.shift args = process exp.shift args = "" if args == "()" exp.shift if exp == s(s(:nil)) # empty it out of a default nil expression body = [] until exp.empty? do body << indent(process(exp.shift)) end body << indent("# do nothing") if body.empty? body = body.join("\n") return "def #{name}#{args}\n#{body}\nend".gsub(/\n\s*\n+/, "\n") end def process_iter exp call = process exp[1] block = process_rlist exp.sexp_body(3) out = "#{call} do\n #{block}\n end" out end def process_output exp output_format exp, "Output" end def process_escaped_output exp output_format exp, "Escaped Output" end def process_format exp output_format exp, "Format" end def process_format_escaped exp output_format exp, "Escaped" end def output_format exp, tag out = if exp[1].node_type == :str or exp[1].node_type == :ignore "" else res = process exp[1] if res == "" "" else "[#{tag}] #{res}" end end out end def process_const exp if exp[1] == Brakeman::Tracker::UNKNOWN_MODEL "(Unresolved Model)" else out = exp[1].to_s out end end def process_render exp exp = exp.deep_clone exp.shift exp[1] = process exp[1] if sexp? exp[1] exp[2] = process exp[2] if sexp? exp[2] out = "render(#{exp[0]} => #{exp[1]}, #{exp[2]})" out end end ================================================ FILE: lib/brakeman/processors/route_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/processors/alias_processor' require 'brakeman/processors/lib/route_helper' require 'brakeman/util' require 'brakeman/processors/lib/rails3_route_processor.rb' require 'brakeman/processors/lib/rails2_route_processor.rb' require 'set' class Brakeman::RoutesProcessor def self.new tracker if tracker.options[:rails3] Brakeman::Rails3RoutesProcessor.new tracker else Brakeman::Rails2RoutesProcessor.new tracker end end end ================================================ FILE: lib/brakeman/processors/slim_template_processor.rb ================================================ require 'brakeman/processors/template_processor' require 'brakeman/processors/lib/render_helper' class Brakeman::SlimTemplateProcessor < Brakeman::TemplateProcessor include Brakeman::RenderHelper SAFE_BUFFER = s(:call, s(:colon2, s(:const, :ActiveSupport), :SafeBuffer), :new) OUTPUT_BUFFER = s(:ivar, :@output_buffer) TEMPLE_UTILS = s(:colon2, s(:colon3, :Temple), :Utils) ATTR_MERGE = s(:call, s(:call, s(:array), :reject, s(:block_pass, s(:lit, :empty?))), :join, s(:str, " ")) EMBEDDED_FILTER = s(:const, :BrakemanFilter) def process_call exp target = exp.target method = exp.method if method == :safe_concat and (target == SAFE_BUFFER or target == OUTPUT_BUFFER) arg = normalize_output(exp.first_arg) if is_escaped? arg add_escaped_output arg.first_arg elsif string? arg ignore elsif render? arg add_output make_render_in_view arg elsif string_interp? arg process_inside_interp arg elsif node_type? arg, :ignore ignore elsif internal_variable? arg ignore elsif arg == ATTR_MERGE ignore else add_output arg end elsif is_escaped? exp add_escaped_output arg elsif target == nil and method == :render exp.arglist = process exp.arglist make_render_in_view exp else exp.arglist = process exp.arglist exp end end def normalize_output arg arg = super(arg) if embedded_filter? arg super(arg.first_arg) else arg end end # Handle our "fake" embedded filters def embedded_filter? arg call? arg and arg.method == :render and arg.target == EMBEDDED_FILTER end #Slim likes to interpolate output into strings then pass them to safe_concat. #Better to pull those values out directly. def process_inside_interp exp exp.map! do |e| if node_type? e, :evstr e.value = process_interp_output e.value e else e end end exp end def process_interp_output exp if sexp? exp if node_type? exp, :if process_interp_output exp.then_clause process_interp_output exp.else_clause elsif exp == SAFE_BUFFER ignore elsif render? exp add_output make_render_in_view exp elsif node_type? :output, :escaped_output exp elsif is_escaped? exp add_escaped_output exp else add_output exp end end end def add_escaped_output exp exp = normalize_output(exp) return exp if string? exp or internal_variable? exp super exp end def is_escaped? exp call? exp and exp.target == TEMPLE_UTILS and (exp.method == :escape_html or exp.method == :escape_html_safe) end def internal_variable? exp node_type? exp, :lvar and exp.value =~ /^_(temple_|slim_)/ end def render? exp call? exp and exp.target.nil? and exp.method == :render end def process_render exp #Still confused as to why this is not needed in other template processors #but is needed here exp end end ================================================ FILE: lib/brakeman/processors/template_alias_processor.rb ================================================ require 'set' require 'brakeman/processors/alias_processor' require 'brakeman/processors/lib/render_helper' require 'brakeman/processors/lib/render_path' require 'brakeman/tracker' #Processes aliasing in templates. #Handles calls to +render+. class Brakeman::TemplateAliasProcessor < Brakeman::AliasProcessor include Brakeman::RenderHelper FORM_METHODS = Set[:form_for, :remote_form_for, :form_remote_for] def initialize tracker, template, called_from = nil super tracker @template = template @current_file = template.file @called_from = called_from end #Process template def process_template name, args, _, line = nil # Strip forward slash from beginning of template path. # This also happens in RenderHelper#process_template but # we need it here too to accurately avoid circular renders below. name = name.to_s.gsub(/^\//, "") if @called_from if @called_from.include_template? name Brakeman.debug "Skipping circular render from #{@template.name} to #{name}" return end super name, args, @called_from.dup.add_template_render(@template.name, line, @current_file), line else super name, args, Brakeman::RenderPath.new.add_template_render(@template.name, line, @current_file), line end end def process_lasgn exp if exp.lhs == :haml_temp or haml_capture? exp.rhs exp.rhs = process exp.rhs # Avoid propagating contents of block if node_type? exp.rhs, :iter new_exp = exp.dup new_exp.rhs = exp.rhs.block_call super new_exp exp # Still save the original, though else super exp end else super exp end end HAML_CAPTURE = [:capture, :capture_haml] def haml_capture? exp node_type? exp, :iter and call? exp.block_call and HAML_CAPTURE.include? exp.block_call.method end #Determine template name def template_name name if !name.to_s.include?('/') && @template.name.to_s.include?('/') name = "#{@template.name.to_s.match(/^(.*\/).*$/)[1]}#{name}" end name end UNKNOWN_MODEL_CALL = Sexp.new(:call, Sexp.new(:const, Brakeman::Tracker::UNKNOWN_MODEL), :new) FORM_BUILDER_CALL = Sexp.new(:call, Sexp.new(:const, :FormBuilder), :new) #Looks for form methods and iterating over collections of Models def process_iter exp process_default exp call = exp.block_call if call? call target = call.target method = call.method arg = exp.block_args.first_param block = exp.block #Check for e.g. Model.find.each do ... end if method == :each and arg and block and model = get_model_target(target) if arg.is_a? Symbol if model == target.target env[Sexp.new(:lvar, arg)] = Sexp.new(:call, model, :new) else env[Sexp.new(:lvar, arg)] = UNKNOWN_MODEL_CALL end process block if sexp? block end elsif FORM_METHODS.include? method if arg.is_a? Symbol env[Sexp.new(:lvar, arg)] = FORM_BUILDER_CALL process block if sexp? block end end end exp end COLLECTION_METHODS = [:all, :find, :select, :where] #Checks if +exp+ is a call to Model.all or Model.find* def get_model_target exp if call? exp target = exp.target if COLLECTION_METHODS.include? exp.method or exp.method.to_s[0,4] == "find" models = Set.new @tracker.models.keys name = class_name target return target if models.include?(name) end return get_model_target(target) end false end #Ignore `<<` calls on template variables which are used by the templating #library (HAML, ERB, etc.) def find_push_target exp if sexp? exp if exp.node_type == :lvar and (exp.value == :_buf or exp.value == :_erbout) return nil elsif exp.node_type == :ivar and exp.value == :@output_buffer return nil elsif exp.node_type == :call and call? exp.target and exp.target.method == :_hamlout and exp.method == :buffer return nil end end super end end ================================================ FILE: lib/brakeman/processors/template_processor.rb ================================================ require 'brakeman/processors/base_processor' require 'brakeman/tracker/template' #Base Processor for templates/views class Brakeman::TemplateProcessor < Brakeman::BaseProcessor #Initializes template information. def initialize tracker, template_name, called_from = nil, current_file = nil super(tracker) @current_template = Brakeman::Template.new template_name, called_from, current_file, tracker @current_file = @current_template.file if called_from template_name = (template_name.to_s + "." + called_from.to_s).to_sym end tracker.templates[template_name] = @current_template @inside_concat = false end #Process the template Sexp. def process exp begin super rescue => e except = e.exception("Error when processing #{@current_template.name}: #{e.message}") except.set_backtrace(e.backtrace) raise except end end #Ignore initial variable assignment def process_lasgn exp if exp.lhs == :_erbout and exp.rhs.node_type == :str #ignore ignore elsif exp.lhs == :_buf and exp.rhs.node_type == :str ignore else exp.rhs = process exp.rhs exp end end #Adds output to the list of outputs. def process_output exp exp.value = process exp.value @current_template.add_output exp unless exp.original_line exp end def process_escaped_output exp process_output exp end # Pull out actual output value from template def normalize_output arg if call? arg and [:to_s, :html_safe!, :freeze].include? arg.method normalize_output(arg.target) # sometimes it's foo.to_s.to_s elsif node_type? arg, :if branches = [arg.then_clause, arg.else_clause].compact if branches.empty? s(:nil).line(arg.line) elsif branches.length == 2 Sexp.new(:or, *branches).line(arg.line) else branches.first end else arg end end def add_escaped_output output add_output output, :escaped_output end def add_output output, type = :output if node_type? output, :or Sexp.new(:or, add_output(output.lhs, type), add_output(output.rhs, type)).line(output.line) else s = Sexp.new(type, output) s.line(output.line) @current_template.add_output s s end end end ================================================ FILE: lib/brakeman/report/config/remediation.yml ================================================ --- basic_auth_password: 300000 cross_site_scripting: 300000 xss_content_tag: 300000 CVE_2014_3514_call: 600000 all_default_routes: 2000000 unsafe_deserialize: 2000000 local_request_config: 100000 CVE_2012_3424: 4000000 CVE_2011_2932: 8000000 code_eval: 2000000 command_injection: 2000000 file_access: 2000000 CVE_2014_7829: 4000000 CVE_2011_2929: 4000000 csrf_protection_disabled: 4000000 CVE_2013_6414: 4000000 CVE_2013_4491: 4000000 CVE_2013_1856: 4000000 CVE_2015_3226: 4000000 CVE_2013_0333: 4000000 xss_link_to: 300000 xss_link_to_href: 300000 CVE_2011_0446: 300000 mass_assign_call: 2000000 dangerous_attr_accessible: 2000000 no_attr_accessible: 2000000 CVE_2013_0277: 2000000 CVE_2010_3933: 4000000 CVE_2014_0081: 300000 CVE_2011_2930: 600000 open_redirect: 300000 regex_dos: 600000 dynamic_render_path: 4000000 CVE_2014_0082: 4000000 cross_site_scripting_inline: 600000 CVE_2011_3186: 2000000 safe_buffer_vuln: 4000000 CVE_2013_1855: 4000000 CVE_2013_1857: 4000000 CVE_2012_3463: 600000 select_options_vuln: 4000000 dangerous_send: 600000 session_key_manipulation: 600000 http_cookies: 600000 session_secret: 600000 secure_cookies: 600000 CVE_2013_6416: 600000 CVE_2012_3464: 4000000 csrf_blacklist: 300000 auth_blacklist: 300000 sql_injection: 1200000 CVE-2012-2660: 4000000 CVE-2012-2661: 4000000 CVE-2012-2695: 4000000 CVE-2012-5664: 4000000 CVE-2013-0155: 4000000 CVE-2013-6417: 4000000 CVE-2014-3482: 4000000 CVE-2014-3483: 4000000 ssl_verification_bypass: 2500000 CVE_2011_2931: 4000000 unsafe_symbol_creation: 300000 translate_vuln: 300000 unsafe_constantize: 600000 unscoped_find: 300000 validation_regex: 300000 mass_assign_without_protection: 600000 CVE_2015_3227: 4000000 CVE_2013_0156: 4000000 weak_hash_digest: 800000 ================================================ FILE: lib/brakeman/report/ignore/config.rb ================================================ require 'set' require 'json' module Brakeman class IgnoreConfig attr_reader :shown_warnings, :ignored_warnings attr_accessor :file def initialize file, new_warnings @file = file @new_warnings = new_warnings @already_ignored = [] @ignored_fingerprints = Set.new @used_fingerprints = Set.new @notes = {} @shown_warnings = @ignored_warnings = nil @changed = false end # Populate ignored_warnings and shown_warnings based on ignore # configuration def filter_ignored @shown_warnings = [] @ignored_warnings = [] @used_fingerprints = Set.new @new_warnings.each do |w| if ignored? w @ignored_warnings << w else @shown_warnings << w end end @shown_warnings end # Remove warning from ignored list def unignore warning @ignored_fingerprints.delete warning.fingerprint if @already_ignored.reject! { |w|w[:fingerprint] == warning.fingerprint } @changed = true end end # Determine if warning should be ignored def ignored? warning @used_fingerprints << warning.fingerprint @ignored_fingerprints.include? warning.fingerprint end def ignore warning @changed = true unless ignored? warning @ignored_fingerprints << warning.fingerprint end # Add note for warning def add_note warning, note @changed = true @notes[warning.fingerprint] = note end # Retrieve note for warning if it exists. Returns nil if no # note is found def note_for warning if warning.is_a? Warning fingerprint = warning.fingerprint else fingerprint = warning[:fingerprint] end @already_ignored.each do |w| if fingerprint == w[:fingerprint] return w[:note] end end nil end # The set of unused ignore entries def obsolete_fingerprints (@ignored_fingerprints - @used_fingerprints).to_a end def prune_obsolete obsolete = obsolete_fingerprints.to_set @ignored_fingerprints -= obsolete @already_ignored.reject! do |w| if obsolete.include? w[:fingerprint] @changed = true end end end def already_ignored_entries_with_empty_notes @already_ignored.select { |i| i if i[:note].strip.empty? } end # Read configuration to file def read_from_file file = @file if File.exist? file begin @already_ignored = JSON.parse(File.read(file), :symbolize_names => true)[:ignored_warnings] rescue => e raise e, "\nError[#{e.class}] while reading brakeman ignore file: #{file}\n" end else Brakeman.alert "Could not find ignore configuration in #{file} (no file)" @already_ignored = [] end @already_ignored.each do |w| @ignored_fingerprints << w[:fingerprint] @notes[w[:fingerprint]] = w[:note] end end # Save configuration to file def save_to_file warnings, file = @file warnings = warnings.map do |w| if w.is_a? Warning w = w.to_hash(absolute_paths: false) end w[:note] = @notes[w[:fingerprint]] || "" w end.sort_by { |w| [w[:fingerprint], w[:line] || 0] } output = { :ignored_warnings => warnings, :brakeman_version => Brakeman::Version } File.open file, "w" do |f| f.puts JSON.pretty_generate(output) end end # Save old ignored warnings and newly ignored ones def save_with_old warnings = @ignored_warnings.dup # Only add ignored warnings not already ignored @already_ignored.each do |w| fingerprint = w[:fingerprint] unless @ignored_warnings.find { |ignored_warning| ignored_warning.fingerprint == fingerprint } warnings << w end end if @changed save_to_file warnings end end end end ================================================ FILE: lib/brakeman/report/ignore/interactive.rb ================================================ Brakeman.load_brakeman_dependency 'highline' module Brakeman class InteractiveIgnorer def initialize file, warnings @ignore_config = Brakeman::IgnoreConfig.new(file, warnings) @new_warnings = warnings @skip_ignored = false @skip_rest = false @ignore_rest = false @quit = false @restart = false end def start file_menu initial_menu @ignore_config.filter_ignored unless @quit penultimate_menu final_menu end if @restart @restart = false start end @ignore_config end private def file_menu loop do @ignore_config.file = HighLine.new.ask "Input file: " do |q| if @ignore_config.file and not @ignore_config.file.empty? q.default = @ignore_config.file else q.default = "config/brakeman.ignore" end end if File.exist? @ignore_config.file @ignore_config.read_from_file return else if yes_or_no "No such file. Continue with empty config? " return end end end end def initial_menu HighLine.new.choose do |m| m.choice "Inspect all warnings" do @skip_ignored = false pre_show_help process_warnings end m.choice "Inspect new warnings" do @skip_ignored = true pre_show_help process_warnings end m.choice "Prune obsolete ignored warnings" do prune_obsolete end m.choice "Skip - use current ignore configuration" do @quit = true @ignore_config.filter_ignored end end end def warning_menu HighLine.new.choose do |m| m.prompt = "Action: " m.layout = :one_line m.list_option = ", " m.select_by = :name m.choice "i" m.choice "n" m.choice "s" m.choice "u" m.choice "a" m.choice "k" m.choice "q" m.choice "?" do show_help "?" end end end def pre_show_help say "-" * 30 say "Actions:", :cyan show_help end def show_help say <<-HELP i - Add warning to ignore list n - Add warning to ignore list and add note s - Skip this warning (will remain ignored or shown) u - Remove this warning from ignore list a - Ignore this warning and all remaining warnings k - Skip this warning and all remaining warnings q - Quit, do not update ignored warnings ? - Display this help HELP end def penultimate_menu obsolete = @ignore_config.obsolete_fingerprints return unless obsolete.any? if obsolete.length > 1 plural = 's' verb = 'are' else plural = '' verb = 'is' end say "\n#{obsolete.length} fingerprint#{plural} #{verb} unused:", :green obsolete.each do |obs| say obs end if yes_or_no "\nRemove fingerprint#{plural}?" @ignore_config.prune_obsolete end end def prune_obsolete @ignore_config.filter_ignored obsolete = @ignore_config.obsolete_fingerprints @ignore_config.prune_obsolete say "Removed #{obsolete.length} obsolete fingerprint#{'s' if obsolete.length > 1} from ignore config.", :yellow end def final_menu summarize_changes HighLine.new.choose do |m| m.choice "Save changes" do save end m.choice "Start over" do start_over end m.choice "Quit, do not save changes" do quit end end end def save @ignore_config.file = HighLine.new.ask "Output file: " do |q| if @ignore_config.file and not @ignore_config.file.empty? q.default = @ignore_config.file else q.default = "config/brakeman.ignore" end end @ignore_config.save_with_old end def start_over reset_config @restart = true end def reset_config @ignore_config = Brakeman::IgnoreConfig.new(@ignore_config.file, @new_warnings) end def process_warnings @warning_count = @new_warnings.length @new_warnings.each_with_index do |w, index| @current_index = index if skip_ignored? w or @skip_rest next elsif @ignore_rest ignore w elsif @quit or @restart return else ask_about w end end end def ask_about warning pretty_display warning warning_action warning_menu, warning end def warning_action action, warning case action when "i" ignore warning when "n" ignore_and_note warning when "s" # do nothing when "u" unignore warning when "a" ignore_rest warning when "k" skip_rest warning when "q" quit when "?" ask_about warning else raise "Unexpected action" end end def ignore warning @ignore_config.ignore warning end def ignore_and_note warning note = HighLine.new.ask("Note: ") @ignore_config.ignore warning @ignore_config.add_note warning, note end def unignore warning @ignore_config.unignore warning end def skip_rest warning @skip_rest = true end def ignore_rest warning ignore warning @ignore_rest = true end def quit reset_config @ignore_config.read_from_file @ignore_config.filter_ignored @quit = true end def pretty_display warning progress = "#{@current_index + 1}/#{@warning_count}" say "-------- #{progress} #{"-" * (20 - progress.length)}", :cyan show_confidence warning label "Category" say warning.warning_type label "Message" say warning.message if warning.code label "Code" say warning.format_code end if warning.file label "File" say warning.file.relative end if warning.line label "Line" say warning.line end if already_ignored? warning show_note warning say "Already ignored", :red end say "" end def already_ignored? warning @ignore_config.ignored? warning end def skip_ignored? warning @skip_ignored and already_ignored? warning end def summarize_changes say "-" * 30 say "Ignoring #{@ignore_config.ignored_warnings.length} warnings", :yellow say "Showing #{@ignore_config.shown_warnings.length} warnings", :green end def label name say "#{name}: ", :green end def show_confidence warning label "Confidence" case warning.confidence when 0 say "High", :red when 1 say "Medium", :yellow when 2 say "Weak", :cyan else say "Unknown" end end def show_note warning note = @ignore_config.note_for warning if note label "Note" say note end end def say text, color = nil text = text.to_s if color HighLine.new.say HighLine.new.color(text, color) else HighLine.new.say text end end def yes_or_no message answer = HighLine.new.ask message do |q| q.in = ["y", "n", "yes", "no"] end answer.match /^y/i end end end ================================================ FILE: lib/brakeman/report/pager.rb ================================================ module Brakeman class Pager def initialize tracker, pager = :less, output = $stdout @tracker = tracker @pager = pager @output = output @less_available = @less_options = nil end def page_report report, format if @pager == :less set_color end text = report.format(format) if in_ci? no_pager text else page_output text end end def page_output text case @pager when :none no_pager text when :highline page_via_highline text when :less if less_available? page_via_less text else page_via_highline text end else no_pager text end end def no_pager text @output.puts text end def page_via_highline text Brakeman.load_brakeman_dependency 'highline' h = ::HighLine.new($stdin, @output) h.page_at = :auto h.say text end def page_via_less text # Adapted from https://github.com/piotrmurach/tty-pager/ write_io = IO.popen("less #{less_options.join}", 'w') pid = write_io.pid write_io.write(text) write_io.close Process.waitpid2(pid, Process::WNOHANG) rescue Errno::ECHILD # on jruby 9x waiting on pid raises (per tty-pager) true rescue => e warn "[Error] #{e}" warn "[Error] Could not use pager. Set --no-pager to avoid this issue." no_pager text end def in_ci? ci = ENV["CI"] ci.is_a? String and ci.downcase == "true" end def less_available? return @less_available unless @less_available.nil? @less_available = system("which less > /dev/null") end def less_options # -R show colors # -F exit if output fits on one screen # -X do not clear screen after less exits return @less_options if @less_options @less_options = [] if system("which less > /dev/null") less_help = `less -?` ["-R ", "-F ", "-X ", " --wordwrap"].each do |opt| if less_help.include? opt @less_options << opt end end end @less_options end def set_color return unless @tracker unless less_options.include? "-R " or @tracker.options[:output_color] == :force @tracker.options[:output_color] = false end end end end ================================================ FILE: lib/brakeman/report/renderer.rb ================================================ require 'erb' class Brakeman::Report class Renderer def initialize(template_file, hash = {}) hash[:locals] ||= {} singleton = class << self; self end hash[:locals].each do |attribute_name, attribute_value| singleton.send(:define_method, attribute_name) { attribute_value } end # There are last, so as to make overwriting these using locals impossible. singleton.send(:define_method, 'template_file') { template_file } singleton.send(:define_method, 'template') { File.read(File.expand_path("templates/#{template_file}.html.erb", File.dirname(__FILE__))) } end def render ERB.new(template).result(binding) end end end ================================================ FILE: lib/brakeman/report/report_base.rb ================================================ require 'set' require 'brakeman/util' require 'brakeman/version' require 'brakeman/report/renderer' require 'brakeman/processors/output_processor' require 'brakeman/warning' # Base class for report formats class Brakeman::Report::Base include Brakeman::Util attr_reader :tracker, :checks def initialize tracker @app_tree = tracker.app_tree @tracker = tracker @checks = tracker.checks @ignore_filter = tracker.ignored_filter @highlight_user_input = tracker.options[:highlight_user_input] @warnings_summary = nil end #Return summary of warnings in hash and store in @warnings_summary def warnings_summary return @warnings_summary if @warnings_summary summary = Hash.new(0) high_confidence_warnings = 0 [all_warnings].each do |warnings| warnings.each do |warning| summary[warning.warning_type.to_s] += 1 high_confidence_warnings += 1 if warning.confidence == 0 end end summary[:high_confidence] = high_confidence_warnings @warnings_summary = summary end def controller_information controller_rows = [] tracker.controllers.keys.map{|k| k.to_s}.sort.each do |name| name = name.to_sym c = tracker.controllers[name] if tracker.routes.include? :allow_all_actions or (tracker.routes[name] and tracker.routes[name].include? :allow_all_actions) routes = c.methods_public.keys.map{|e| e.to_s}.sort.join(", ") elsif tracker.routes[name].nil? #No routes defined for this controller. #This can happen when it is only a parent class #for other controllers, for example. routes = "[None]" else routes = (Set.new(c.methods_public.keys) & tracker.routes[name.to_sym]). to_a. map {|e| e.to_s}. sort. join(", ") end if routes == "" routes = "[None]" end controller_rows << { "Name" => name.to_s, "Parent" => c.parent.to_s, "Includes" => c.includes.join(", "), "Routes" => routes } end controller_rows end def all_warnings if @ignore_filter @all_warnings ||= @ignore_filter.shown_warnings else @all_warnings ||= tracker.checks.all_warnings end end def filter_warnings warnings if @ignore_filter warnings.reject do |w| @ignore_filter.ignored? w end else warnings end end def generic_warnings filter_warnings tracker.checks.warnings end def template_warnings filter_warnings tracker.checks.template_warnings end def model_warnings filter_warnings tracker.checks.model_warnings end def controller_warnings filter_warnings tracker.checks.controller_warnings end def ignored_warnings if @ignore_filter @ignore_filter.ignored_warnings else [] end end def number_of_templates tracker Set.new(tracker.templates.map {|k,v| v.name.to_s[/[^.]+/]}).length end def absolute_paths? @tracker.options[:absolute_paths] end def warning_file warning return nil if warning.file.nil? if absolute_paths? warning.file.absolute else warning.file.relative end end #Return array of lines surrounding the warning location from the original #file. def context_for warning file = warning.file context = [] return context unless warning.line and file and file.exists? current_line = 0 start_line = warning.line - 5 end_line = warning.line + 5 start_line = 1 if start_line < 0 File.open file do |f| f.each_line do |line| current_line += 1 next if line.strip == "" if current_line > end_line break end if current_line >= start_line context << [current_line, line] end end end context end def rails_version case when tracker.config.rails_version tracker.config.rails_version when tracker.options[:rails4] "4.x" when tracker.options[:rails3] "3.x" else "Unknown" end end def github_url file, line=nil if repo_url = @tracker.options[:github_url] and file url = "#{repo_url}/#{file.relative}" url << "#L#{line}" if line else nil end end end ================================================ FILE: lib/brakeman/report/report_codeclimate.rb ================================================ require "json" require "yaml" require "pathname" class Brakeman::Report::CodeClimate < Brakeman::Report::Base DOCUMENTATION_PATH = File.expand_path("../../../../docs/warning_types", __FILE__) REMEDIATION_POINTS_CONFIG_PATH = File.expand_path("../config/remediation.yml", __FILE__) REMEDIATION_POINTS_DEFAULT = 300_000 def generate_report all_warnings.map { |warning| issue_json(warning) }.join("\0") end private def issue_json(warning) warning_code_name = name_for(warning.warning_code) { type: "Issue", check_name: warning_code_name, description: warning.message, fingerprint: warning.fingerprint, categories: ["Security"], severity: severity_level_for(warning.confidence), remediation_points: remediation_points_for(warning_code_name), location: { path: file_path(warning), lines: { begin: warning.line || 1, end: warning.line || 1, } }, content: { body: content_for(warning.warning_code, warning.link) } }.to_json end def severity_level_for(confidence) if confidence == 0 "critical" else "normal" end end def remediation_points_for(warning_code) @remediation_points ||= YAML.load_file(REMEDIATION_POINTS_CONFIG_PATH) @remediation_points.fetch(name_for(warning_code), REMEDIATION_POINTS_DEFAULT) end def name_for(warning_code) @warning_codes ||= Brakeman::WarningCodes::Codes.invert @warning_codes[warning_code].to_s end def content_for(warning_code, link) @contents ||= {} unless link.nil? @contents[warning_code] ||= local_content_for(link) || "Read more: #{link}" end end def local_content_for(link) directory = link.split("/").last filename = File.join(DOCUMENTATION_PATH, directory, "index.markdown") File.read(filename) if File.exist?(filename) end def file_path(warning) if tracker.options[:path_prefix] (Pathname.new(tracker.options[:path_prefix]) + Pathname.new(warning.file.relative)).to_s else warning.relative_path end end end ================================================ FILE: lib/brakeman/report/report_csv.rb ================================================ require 'csv' class Brakeman::Report::CSV < Brakeman::Report::Base def generate_report headers = [ "Confidence", "Warning Type", "CWE", "File", "Line", "Message", "Code", "User Input", "Check Name", "Warning Code", "Fingerprint", "Link" ] rows = tracker.filtered_warnings.sort_by do |w| [w.confidence, w.warning_type, w.file, w.line || 0, w.fingerprint] end.map do |warning| generate_row(headers, warning) end table = CSV::Table.new(rows) table.to_csv end def generate_row headers, warning CSV::Row.new headers, warning_row(warning) end def warning_row warning [ warning.confidence_name, warning.warning_type, warning.cwe_id.first, warning_file(warning), warning.line, warning.message, warning.code && warning.format_code(false), warning.user_input && warning.format_user_input(false), warning.check_name, warning.warning_code, warning.fingerprint, warning.link, ] end end ================================================ FILE: lib/brakeman/report/report_github.rb ================================================ # GitHub Actions Formatter # Formats warnings as workflow commands to create annotations in GitHub UI class Brakeman::Report::Github < Brakeman::Report::Base def generate_report # @see https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message errors.concat(warnings).join("\n") end def warnings all_warnings .map { |warning| "::warning file=#{warning_file(warning)},line=#{warning.line}::#{warning.message}" } end def errors tracker.errors.map do |error| if error[:exception].is_a?(Racc::ParseError) # app/services/balance.rb:4 :: parse error on value "..." (tDOT3) file, line = error[:exception].message.split(':').map(&:strip)[0,2] "::error file=#{file},line=#{line}::#{clean_message(error[:error])}" else "::error ::#{clean_message(error[:error])}" end end end private def clean_message(msg) msg.gsub('::','').squeeze(' ') end end ================================================ FILE: lib/brakeman/report/report_hash.rb ================================================ # Generates a hash table for use in Brakeman tests class Brakeman::Report::Hash < Brakeman::Report::Base def generate_report report = { :errors => tracker.errors, :controllers => tracker.controllers, :models => tracker.models, :templates => tracker.templates } [:generic_warnings, :controller_warnings, :model_warnings, :template_warnings].each do |meth| report[meth] = self.send(meth) report[meth].each do |w| w.message = w.format_message w.context = context_for(w).join("\n") end end report[:config] = tracker.config report[:checks_run] = tracker.checks.checks_run report end end ================================================ FILE: lib/brakeman/report/report_html.rb ================================================ require 'cgi/escape' require 'brakeman/report/report_table.rb' class Brakeman::Report::HTML < Brakeman::Report::Table HTML_CONFIDENCE = [ "High", "Medium", "Weak" ] def initialize *args super @element_id = 0 #Used for HTML ids end def generate_report out = html_header << generate_overview << generate_warning_overview.to_s # Return early if only summarizing return out if tracker.options[:summary_only] out << generate_controllers.to_s if tracker.options[:report_routes] or tracker.options[:debug] out << generate_templates.to_s if tracker.options[:debug] out << generate_errors.to_s out << generate_warnings.to_s out << generate_controller_warnings.to_s out << generate_model_warnings.to_s out << generate_template_warnings.to_s out << generate_ignored_warnings.to_s out << "" end def generate_overview locals = { :tracker => tracker, :warnings => all_warnings.length, :warnings_summary => warnings_summary, :number_of_templates => number_of_templates(@tracker), :ignored_warnings => ignored_warnings.length } Brakeman::Report::Renderer.new('overview', :locals => locals).render end #Generate listings of templates and their output def generate_templates out_processor = Brakeman::OutputProcessor.new template_rows = {} tracker.templates.each do |name, template| template.each_output do |out| out = CGI.escapeHTML(out_processor.format(out)) template_rows[name] ||= [] template_rows[name] << out.gsub("\n", ";").gsub(/\s+/, " ") end end template_rows = template_rows.sort_by{|name, value| name.to_s} Brakeman::Report::Renderer.new('template_overview', :locals => {:template_rows => template_rows}).render end def render_array template, headings, value_array, locals return if value_array.empty? Brakeman::Report::Renderer.new(template, :locals => locals).render end def convert_warning warning, original warning["Confidence"] = HTML_CONFIDENCE[original.confidence] warning["Message"] = with_context original, warning["Message"] warning["Warning Type"] = with_link original, warning["Warning Type"] warning end def with_link warning, message "#{message}" end def convert_template_warning warning, original warning = convert_warning warning, original warning["Called From"] = original.called_from warning["Template Name"] = original.template.name warning end def convert_ignored_warning warning, original warning = convert_warning(warning, original) warning['File'] = original.file.relative warning['Note'] = CGI.escapeHTML(@ignore_filter.note_for(original) || "") warning end #Return header for HTML output. Uses CSS from tracker.options[:html_style] def html_header if File.exist? tracker.options[:html_style] css = File.read tracker.options[:html_style] else raise "Cannot find CSS stylesheet for HTML: #{tracker.options[:html_style]}" end locals = { :css => css, :tracker => tracker, :checks => checks, :rails_version => rails_version, :brakeman_version => Brakeman::Version } Brakeman::Report::Renderer.new('header', :locals => locals).render end #Generate HTML for warnings, including context show/hidden via Javascript def with_context warning, message @element_id += 1 context = context_for(warning) message = html_message(warning, message) code_id = "context#@element_id" message_id = "message#@element_id" full_message_id = "full_message#@element_id" alt = false output = "
" << message << "" << "" output << <<-HTML HTML unless context.empty? if warning.line - 1 == 1 or warning.line + 1 == 1 error = " near_error" elsif 1 == warning.line error = " error" else error = "" end output << <<-HTML HTML if context.length > 1 output << context[1..-1].map do |code| alt = !alt if code[0] == warning.line - 1 or code[0] == warning.line + 1 error = " near_error" elsif code[0] == warning.line error = " error" else error = "" end <<-HTML HTML end.join end end output << "
" end #Escape warning message and highlight user input in HTML output def html_message warning, message message = message.to_html if warning.file if github_url = github_url(warning.file, warning.line) message << " near line #{warning.line}" elsif warning.line message << " near line #{warning.line}" end end if warning.code code = warning.format_with_user_input do |_, user_input| "[BMP_UI]#{user_input}[/BMP_UI]" end code = "#{CGI.escapeHTML(code).gsub("[BMP_UI]", "").gsub("[/BMP_UI]", "")}" full_message = "#{message}: #{code}" if warning.code.mass > 20 message_id = "message#@element_id" full_message_id = "full_message#@element_id" "#{message}: ..." << "" else full_message end else message end end end ================================================ FILE: lib/brakeman/report/report_json.rb ================================================ class Brakeman::Report::JSON < Brakeman::Report::Base def generate_report errors = tracker.errors.map{|e| { :error => e[:error], :location => e[:backtrace][0] }} obsolete = tracker.unused_fingerprints warnings = convert_to_hashes all_warnings ignored = convert_to_hashes ignored_warnings scan_info = { :app_path => tracker.app_path, :rails_version => rails_version, :security_warnings => all_warnings.length, :start_time => tracker.start_time.to_s, :end_time => tracker.end_time.to_s, :duration => tracker.duration, :checks_performed => checks.checks_run.sort, :number_of_controllers => tracker.controllers.length, # ignore the "fake" model :number_of_models => tracker.models.length - 1, :number_of_templates => number_of_templates(@tracker), :ruby_version => RUBY_VERSION, :brakeman_version => Brakeman::Version } report_info = { :scan_info => scan_info, :warnings => warnings, :ignored_warnings => ignored, :errors => errors, :obsolete => obsolete } JSON.pretty_generate report_info end def convert_to_hashes warnings warnings.map do |w| w.to_hash(absolute_paths: false) end.sort_by { |w| "#{w[:fingerprint]}#{w[:line]}" } end end ================================================ FILE: lib/brakeman/report/report_junit.rb ================================================ require 'time' require 'stringio' Brakeman.load_brakeman_dependency 'rexml/document' class Brakeman::Report::JUnit < Brakeman::Report::Base def generate_report io = StringIO.new doc = REXML::Document.new doc.add REXML::XMLDecl.new '1.0', 'UTF-8' test_suites = REXML::Element.new 'testsuites' i = 0 all_warnings .map { |warning| [warning.file, [warning]] } .reduce({}) { |entries, entry| key, value = entry entries[key] = entries[key] ? entries[key].concat(value) : value entries } .each { |file, warnings| i += 1 test_suite = test_suites.add_element 'testsuite' test_suite.add_attribute 'id', i test_suite.add_attribute 'package', 'brakeman' test_suite.add_attribute 'file', file.relative test_suite.add_attribute 'timestamp', tracker.start_time.strftime('%FT%T') test_suite.add_attribute 'tests', checks.checks_run.length test_suite.add_attribute 'failures', warnings.length test_suite.add_attribute 'errors', '0' test_suite.add_attribute 'time', '0' warnings.each { |warning| test_case = test_suite.add_element 'testcase' test_case.add_attribute 'name', warning.check.sub(/^Brakeman::/, '') test_case.add_attribute 'file', file.relative test_case.add_attribute 'line', warning.line if warning.line test_case.add_attribute 'time', '0' failure = test_case.add_element 'failure' failure.add_attribute 'message', warning.message failure.add_attribute 'type', warning.warning_type failure.add_text warning.to_s } } doc.add test_suites doc.write io io.string end end ================================================ FILE: lib/brakeman/report/report_markdown.rb ================================================ require 'brakeman/report/report_table' class Brakeman::Report::Markdown < Brakeman::Report::Table class MarkdownTable < Terminal::Table def initialize options = {}, &block options[:style] ||= {} options[:style].merge!({ :border_x => '-', :border_y => '|', :border_i => '|' }) super options, &block end def render super.split("\n")[1...-1].join("\n") end alias :to_s :render end def initialize *args super @table = MarkdownTable end def generate_report out = +"# BRAKEMAN REPORT\n\n" << generate_metadata.to_s << "\n\n" << generate_checks.to_s << "\n\n" << "### SUMMARY\n\n" << generate_overview.to_s << "\n\n" << generate_warning_overview.to_s << "\n\n" #Return output early if only summarizing return out if tracker.options[:summary_only] if tracker.options[:report_routes] or tracker.options[:debug] out << "### CONTROLLERS" << "\n\n" << generate_controllers.to_s << "\n\n" end if tracker.options[:debug] out << "### TEMPLATES\n\n" << generate_templates.to_s << "\n\n" end output_table("Errors", generate_errors, out) output_table("SECURITY WARNINGS", generate_warnings, out) output_table("Controller Warnings:", generate_controller_warnings, out) output_table("Model Warnings:", generate_model_warnings, out) output_table("View Warnings:", generate_template_warnings, out) out end def output_table title, result, output return unless result output << "### #{title}\n\n#{result.to_s}\n\n" end def generate_metadata MarkdownTable.new( :headings => ['Application path', 'Rails version', 'Brakeman version', 'Started at', 'Duration'] ) do |t| t.add_row([ tracker.app_path, rails_version, Brakeman::Version, tracker.start_time, "#{tracker.duration} seconds", ]) end end def generate_checks MarkdownTable.new(:headings => ['Checks performed']) do |t| t.add_row([checks.checks_run.sort.join(", ")]) end end def convert_warning warning, original warning["Message"] = markdown_message original, warning["Message"] warning["Warning Type"] = "[#{warning['Warning Type']}](#{original.link})" if original.link warning end # Escape and code format warning message def markdown_message warning, message message = message.to_s if warning.file github_url = github_url warning.file, warning.line if github_url message << " near line [#{warning.line}](#{github_url})" elsif warning.line message << " near line #{warning.line}" end end if warning.code code = warning.format_code.gsub('`','``').gsub(/\A``|``\z/, '` `') message << ": `#{code}`" end message end end ================================================ FILE: lib/brakeman/report/report_sarif.rb ================================================ require 'uri' class Brakeman::Report::SARIF < Brakeman::Report::Base def generate_report sarif_log = { :version => '2.1.0', :$schema => 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json', :runs => runs, } JSON.pretty_generate sarif_log end def runs [ { :tool => { :driver => { :name => 'Brakeman', :informationUri => 'https://brakemanscanner.org', :semanticVersion => Brakeman::Version, :rules => rules, }, }, :results => results, }.merge(original_uri_base_ids) ] end # Output base URIs # based on what the user specified for the application path # and whether or not --absolute-paths was set. def original_uri_base_ids if tracker.options[:app_path] == '.' # Probably no app_path was specified, as that's the default if absolute_paths? # Set %SRCROOT% to absolute path { originalUriBaseIds: { '%SRCROOT%' => { uri: file_uri(tracker.app_tree.root), description: { text: 'Base path for application' } } } } else # Empty %SRCROOT% # This avoids any paths appearing in the report # that are not part of the application directory. # Seems fine! { originalUriBaseIds: { '%SRCROOT%' => { description: { text: 'Base path for application' } }, } } end elsif tracker.options[:app_path] != tracker.app_tree.root # Path was specified and it was relative if absolute_paths? # Include absolute root and relative application path { originalUriBaseIds: { PROJECTROOT: { uri: file_uri(tracker.app_tree.root), description: { text: 'Base path for all project files' } }, '%SRCROOT%' => { # Technically should ensure this doesn't have any '..' # but... TODO uri: File.join(tracker.options[:app_path], '/'), uriBaseId: 'PROJECTROOT', description: { text: 'Base path for application' } } } } else # Just include relative application path. # Not clear this is 100% valid, but there is one example in the spec like this { originalUriBaseIds: { PROJECTROOT: { description: { text: 'Base path for all project files' } }, '%SRCROOT%' => { # Technically should ensure this doesn't have any '..' # but... TODO uri: File.join(tracker.options[:app_path], '/'), uriBaseId: 'PROJECTROOT', description: { text: 'Base path for application' } } } } end else # app_path was absolute if absolute_paths? # Set %SRCROOT% to absolute path { originalUriBaseIds: { '%SRCROOT%' => { uri: file_uri(tracker.app_tree.root), description: { text: 'Base path for application' } } } } else # Empty %SRCROOT% # Seems fine! { originalUriBaseIds: { '%SRCROOT%' => { description: { text: 'Base path for application' } }, } } end end end def rules @rules ||= unique_warnings_by_warning_code.map do |warning| rule_id = render_id warning check_name = warning.check_name check_description = render_message check_descriptions[check_name] { :id => rule_id, :name => "#{check_name}/#{warning.warning_type}", :fullDescription => { :text => check_description, }, :helpUri => warning.link, :help => { :text => "More info: #{warning.link}.", :markdown => "[More info](#{warning.link}).", }, :properties => { :tags => [check_name], }, } end end def results @results ||= tracker.checks.all_warnings.map do |warning| rule_id = render_id warning result_level = infer_level warning message_text = render_message warning.message.to_s result = { :ruleId => rule_id, :ruleIndex => rules.index { |r| r[:id] == rule_id }, :level => result_level, :message => { :text => message_text, }, :locations => [ :physicalLocation => { :artifactLocation => { :uri => warning.file.relative, :uriBaseId => '%SRCROOT%', }, :region => { :startLine => warning.line.is_a?(Integer) ? warning.line : 1, }, }, ], } if @ignore_filter && @ignore_filter.ignored?(warning) result[:suppressions] = [ { :kind => 'external', :justification => @ignore_filter.note_for(warning), :location => { :physicalLocation => { :artifactLocation => { :uri => Brakeman::FilePath.from_app_tree(@app_tree, @ignore_filter.file).relative, :uriBaseId => '%SRCROOT%', }, }, }, } ] end result end end # Returns a hash of all check descriptions, keyed by check name def check_descriptions @check_descriptions ||= Brakeman::Checks.checks.map do |check| [check.name.gsub(/^Check/, ''), check.description] end.to_h end # Returns a de-duplicated set of warnings, used to generate rules def unique_warnings_by_warning_code @unique_warnings_by_warning_code ||= tracker.checks.all_warnings.uniq { |w| w.warning_code } end def render_id warning # Include alpha prefix to provide 'compiler error' appearance "BRAKE#{'%04d' % warning.warning_code}" # 46 becomes BRAKE0046, for example end def render_message message return message if message.nil? # Ensure message ends with a period if message.end_with? "." message else "#{message}." end end def infer_level warning # Infer result level from warning confidence @@levels_from_confidence ||= Hash.new('warning').update({ 0 => 'error', # 0 represents 'high confidence', which we infer as 'error' 1 => 'warning', # 1 represents 'medium confidence' which we infer as 'warning' 2 => 'note', # 2 represents 'weak, or low, confidence', which we infer as 'note' }) @@levels_from_confidence[warning.confidence] end # File URI as a string with trailing forward-slash # as required by SARIF standard def file_uri(path) URI::File.build(path: File.join(path, '/')).to_s end end ================================================ FILE: lib/brakeman/report/report_sonar.rb ================================================ class Brakeman::Report::Sonar < Brakeman::Report::Base def generate_report report_object = { issues: all_warnings.map { |warning| issue_json(warning) } } return JSON.pretty_generate report_object end private def issue_json(warning) { engineId: "Brakeman", ruleId: warning.warning_code, type: "VULNERABILITY", severity: severity_level_for(warning.confidence), primaryLocation: { message: warning.message, filePath: warning.file.relative, textRange: { "startLine": warning.line || 1, "endLine": warning.line || 1, } }, effortMinutes: (4 - warning.confidence) * 15 } end def severity_level_for(confidence) if confidence == 0 "CRITICAL" elsif confidence == 1 "MAJOR" else "MINOR" end end end ================================================ FILE: lib/brakeman/report/report_table.rb ================================================ Brakeman.load_brakeman_dependency 'terminal-table' class Brakeman::Report::Table < Brakeman::Report::Base def initialize *args super @table = Terminal::Table end def generate_report summary_option = tracker.options[:summary_only] out = +"" unless summary_option == :no_summary out << text_header << "\n\n+SUMMARY+\n\n" << truncate_table(generate_overview.to_s) << "\n\n" << truncate_table(generate_warning_overview.to_s) << "\n" end #Return output early if only summarizing if summary_option == :summary_only or summary_option == true return out end if tracker.options[:report_routes] or tracker.options[:debug] out << "\n+CONTROLLERS+\n" << truncate_table(generate_controllers.to_s) << "\n" end if tracker.options[:debug] out << "\n+TEMPLATES+\n\n" << truncate_table(generate_templates.to_s) << "\n" end output_table("+Obsolete Ignore Entries+", generate_obsolete, out) output_table("+Errors+", generate_errors, out) output_table("+SECURITY WARNINGS+", generate_warnings, out) output_table("Controller Warnings:", generate_controller_warnings, out) output_table("Model Warnings:", generate_model_warnings, out) output_table("View Warnings:", generate_template_warnings, out) out << "\n" out end def output_table title, result, output return unless result output << "\n\n#{title}\n\n#{truncate_table(result.to_s)}" end def generate_overview num_warnings = all_warnings.length @table.new(:headings => ['Scanned/Reported', 'Total']) do |t| t.add_row ['Controllers', tracker.controllers.length] t.add_row ['Models', tracker.models.length - 1] t.add_row ['Templates', number_of_templates(@tracker)] t.add_row ['Errors', tracker.errors.length] t.add_row ['Security Warnings', "#{num_warnings} (#{warnings_summary[:high_confidence]})"] t.add_row ['Ignored Warnings', ignored_warnings.length] unless ignored_warnings.empty? end end #Generate table of how many warnings of each warning type were reported def generate_warning_overview types = warnings_summary.keys types.delete :high_confidence values = types.sort.collect{|warning_type| [warning_type, warnings_summary[warning_type]] } locals = {:types => types, :warnings_summary => warnings_summary} render_array('warning_overview', ['Warning Type', 'Total'], values, locals) end #Generate table of controllers and routes found for those controllers def generate_controllers controller_rows = controller_information cols = ['Name', 'Parent', 'Includes', 'Routes'] locals = {:controller_rows => controller_rows} values = controller_rows.collect{|row| row.values_at(*cols) } render_array('controller_overview', cols, values, locals) end #Generate table of errors or return nil if no errors def generate_errors values = tracker.errors.collect{|error| [error[:error], error[:backtrace][0]]} render_array('error_overview', ['Error', 'Location'], values, {:tracker => tracker}) end def generate_obsolete values = tracker.unused_fingerprints.collect{|fingerprint| [fingerprint] } render_array('obsolete_ignore_entries', ['fingerprint'], values, {:tracker => tracker}) end def generate_warnings render_warnings generic_warnings, :warning, 'security_warnings', ["Confidence", "Class", "Method", "Warning Type", "CWE ID", "Message"], 'Class' end #Generate table of template warnings or return nil if no warnings def generate_template_warnings render_warnings template_warnings, :template, 'view_warnings', ['Confidence', 'Template', 'Warning Type', "CWE ID", 'Message'], 'Template' end #Generate table of model warnings or return nil if no warnings def generate_model_warnings render_warnings model_warnings, :model, 'model_warnings', ['Confidence', 'Model', 'Warning Type', "CWE ID", 'Message'], 'Model' end #Generate table of controller warnings or nil if no warnings def generate_controller_warnings render_warnings controller_warnings, :controller, 'controller_warnings', ['Confidence', 'Controller', 'Warning Type', "CWE ID", 'Message'], 'Controller' end def generate_ignored_warnings render_warnings ignored_warnings, :ignored, 'ignored_warnings', ['Confidence', 'Warning Type', "CWE ID", 'File', 'Message'], 'Warning Type' end def render_warnings warnings, type, template, cols, sort_col unless warnings.empty? rows = sort(convert_to_rows(warnings, type), sort_col) values = rows.collect { |row| row.values_at(*cols) } locals = { :warnings => rows } render_array(template, cols, values, locals) else nil end end #Generate listings of templates and their output def generate_templates out_processor = Brakeman::OutputProcessor.new template_rows = {} tracker.templates.each do |name, template| template.each_output do |out| out = out_processor.format out template_rows[name] ||= [] template_rows[name] << out.gsub("\n", ";").gsub(/\s+/, " ") end end template_rows = template_rows.sort_by{|name, value| name.to_s} output = +'' template_rows.each do |template| output << template.first.to_s << "\n\n" table = @table.new(:headings => ['Output']) do |t| # template[1] is an array of calls template[1].each do |v| t.add_row [v] end end output << table.to_s << "\n\n" end output end def convert_to_rows warnings, type = :warning warnings.map do |warning| w = warning.to_row type case type when :warning convert_warning w, warning when :ignored convert_ignored_warning w, warning when :template convert_template_warning w, warning else convert_warning w, warning end end end def convert_ignored_warning warning, original convert_warning warning, original end def convert_template_warning warning, original convert_warning warning, original end def sort rows, sort_col stabilizer = 0 rows.sort_by do |row| stabilizer += 1 row.values_at("Confidence", "Warning Type", sort_col) << stabilizer end end def render_array template, headings, value_array, locals return if value_array.empty? @table.new(:headings => headings) do |t| value_array.each { |value_row| t.add_row value_row } end end def convert_warning warning, original warning["Message"] = text_message original, warning["Message"] warning end #Escape warning message and highlight user input in text output def text_message warning, message message = message.to_s if warning.line message << " near line #{warning.line}" end if warning.code if @highlight_user_input and warning.user_input code = warning.format_with_user_input do |user_input, user_input_string| "+#{user_input_string}+" end else code = warning.format_code end message << ": #{code}" end message end #Generate header for text output def text_header <<-HEADER +BRAKEMAN REPORT+ Application path: #{tracker.app_path} Rails version: #{rails_version} Brakeman version: #{Brakeman::Version} Started at #{tracker.start_time} Duration: #{tracker.duration} seconds Checks run: #{checks.checks_run.sort.join(", ")} HEADER end def truncate_table str @terminal_width ||= if @tracker.options[:table_width] @tracker.options[:table_width] elsif $stdin && $stdin.tty? Brakeman.load_brakeman_dependency 'highline' ::HighLine.default_instance.terminal.terminal_size[0] else 80 end lines = str.lines lines.map do |line| if line.chomp.length > @terminal_width line[0..(@terminal_width - 3)] + ">>\n" else line end end.join end end ================================================ FILE: lib/brakeman/report/report_tabs.rb ================================================ require 'brakeman/report/report_table' #Generated tab-separated output suitable for the Jenkins Brakeman Plugin: #https://github.com/presidentbeef/brakeman-jenkins-plugin class Brakeman::Report::Tabs < Brakeman::Report::Table def generate_report [[:generic_warnings, "General"], [:controller_warnings, "Controller"], [:model_warnings, "Model"], [:template_warnings, "Template"]].map do |meth, category| self.send(meth).map do |w| line = w.line || 0 "#{(w.file.absolute)}\t#{line}\t#{w.warning_type}\t#{category}\t#{w.format_message}\t#{w.confidence_name}" end.join "\n" end.join "\n" end end ================================================ FILE: lib/brakeman/report/report_text.rb ================================================ Brakeman.load_brakeman_dependency 'highline' class Brakeman::Report::Text < Brakeman::Report::Base def generate_report HighLine.use_color = !!tracker.options[:output_color] summary_option = tracker.options[:summary_only] @output_string = +"\n" unless summary_option == :no_summary add_chunk generate_header add_chunk generate_overview add_chunk generate_warning_overview end if summary_option == :summary_only or summary_option == true return @output_string end add_chunk generate_controllers if tracker.options[:debug] or tracker.options[:report_routes] add_chunk generate_templates if tracker.options[:debug] add_chunk generate_obsolete add_chunk generate_errors add_chunk generate_warnings add_chunk generate_show_ignored_overview if tracker.options[:show_ignored] && ignored_warnings.any? @output_string end def add_chunk chunk, out = @output_string if chunk and not chunk.empty? if chunk.is_a? Array chunk = chunk.join("\n") end out << chunk << "\n\n" end end def generate_controllers double_space "Controller Overview", controller_information.map { |ci| controller = [ label("Controller", ci["Name"]), label("Parent", ci["Parent"]), label("Routes", ci["Routes"]) ] if ci["Includes"] and not ci["Includes"].empty? controller.insert(2, label("Includes", ci["Includes"])) end controller } end def generate_header [ header("Brakeman Report"), label("Application Path", tracker.app_path), label("Rails Version", rails_version), label("Brakeman Version", Brakeman::Version), label("Scan Date", tracker.start_time), label("Duration", "#{tracker.duration} seconds"), label("Checks Run", checks.checks_run.sort.join(", ")) ] end def generate_overview overview = [ header("Overview"), label('Controllers', tracker.controllers.length), label('Models', tracker.models.length - 1), label('Templates', number_of_templates(@tracker)), label('Errors', tracker.errors.length), label('Security Warnings', all_warnings.length) ] unless ignored_warnings.empty? overview << label('Ignored Warnings', ignored_warnings.length) end overview end def generate_warning_overview warning_types = warnings_summary warning_types.delete :high_confidence warning_types.sort_by { |t, c| t }.map do |type, count| label(type, count) end.unshift(header('Warning Types')) end def generate_warnings if tracker.filtered_warnings.empty? HighLine.color("No warnings found", :bold, :green) else warnings = tracker.filtered_warnings.sort_by do |w| [w.confidence, w.warning_type, w.file, w.line || 0, w.fingerprint] end.map do |w| output_warning w end double_space "Warnings", warnings end end def generate_show_ignored_overview double_space("Ignored Warnings", ignored_warnings.map {|w| output_warning w}) end def generate_errors return if tracker.errors.empty? full_trace = tracker.options[:debug] errors = tracker.errors.map do |e| trace = if full_trace e[:backtrace].join("\n") else e[:backtrace][0] end [ label("Error", e[:error]), label("Location", trace) ] end double_space "Errors", errors end def generate_obsolete return if tracker.unused_fingerprints.empty? [header("Obsolete Ignore Entries")] + tracker.unused_fingerprints end def generate_templates out_processor = Brakeman::OutputProcessor.new template_rows = {} tracker.templates.each do |name, template| template.each_output do |out| out = out_processor.format out template_rows[name] ||= [] template_rows[name] << out.gsub("\n", ";").gsub(/\s+/, " ") end end double_space "Template Output", template_rows.sort_by { |name, value| name.to_s }.map { |template| [HighLine.new.color("#{template.first}\n", :cyan)] + template[1] }.compact end def output_warning w text_format = tracker.options[:text_fields] || [:confidence, :category, :check, :message, :code, :file, :line] text_format.map do |option| format_line(w, option) end.compact end def format_line w, option case option when :confidence label('Confidence', confidence(w.confidence)) when :category label('Category', w.warning_type.to_s) when :cwe label('CWE', w.cwe_id.join(', ')) when :check label('Check', w.check_name) when :message label('Message', w.message) when :code if w.code label('Code', format_code(w)) end when :file label('File', warning_file(w)) when :line if w.line label('Line', w.line) end when :link label('Link', w.link) when :fingerprint label('Fingerprint', w.fingerprint) when :category_id label('Category ID', w.warning_code) when :render_path if w.called_from label('Render Path', w.called_from.join(" > ")) end end end def double_space title, values values = values.map { |v| v.join("\n") }.join("\n\n") [header(title), values] end def format_code w if @highlight_user_input and w.user_input w.format_with_user_input do |exp, text| HighLine.new.color(text, :yellow) end else w.format_code end end def confidence c case c when 0 HighLine.new.color("High", :red) when 1 HighLine.new.color("Medium", :yellow) when 2 HighLine.new.color("Weak", :none) end end def label l, value, color = :green "#{HighLine.new.color(l, color)}: #{value}" end def header text HighLine.new.color("== #{text} ==\n", :bold, :magenta) end # ONLY used for generate_controllers to avoid duplication def render_array name, cols, values, locals controllers = values.map do |controller_name, parent, includes, routes| c = [ label("Controller", controller_name) ] c << label("Parent", parent) unless parent.empty? c << label("Includes", includes) unless includes.empty? c << label("Routes", routes) end double_space "Controller Overview", controllers end end ================================================ FILE: lib/brakeman/report/templates/controller_overview.html.erb ================================================

Controllers

<% controller_rows.each do |row| %> <% end %>
Name Parent Includes Routes
<%= row['Name'] %> <%= row['Parent'] %> <%= row['Includes'] %> <%= row['Routes'] %>
================================================ FILE: lib/brakeman/report/templates/controller_warnings.html.erb ================================================

Controller Warnings

<% warnings.each do |warning| %> <% end %>
Confidence Controller Warning Type CWE ID Message
<%= warning['Confidence']%> <%= warning['Controller']%> <%= warning['Warning Type']%> <%= warning['CWE ID']%> <%= warning['Message']%>
================================================ FILE: lib/brakeman/report/templates/error_overview.html.erb ================================================

Exceptions raised during the analysis (click to see them)

================================================ FILE: lib/brakeman/report/templates/header.html.erb ================================================ Brakeman Report

Brakeman Report

Application Path Rails Version Brakeman Version Report Time Checks Performed
<%= tracker.app_path %> <%= rails_version %> <%= brakeman_version %> <%= tracker.start_time %>

<%= tracker.duration %> seconds
<%= checks.checks_run.sort.join(", ") %>

================================================ FILE: lib/brakeman/report/templates/ignored_warnings.html.erb ================================================

<%= warnings.length %> Ignored Warnings (click to see them)

================================================ FILE: lib/brakeman/report/templates/model_warnings.html.erb ================================================

Model Warnings

<% warnings.each do |warning| %> <% end %>
Confidence Model Warning Type CWE ID Message
<%= warning['Confidence']%> <%= warning['Model']%> <%= warning['Warning Type']%> <%= warning['CWE ID']%> <%= warning['Message']%>
================================================ FILE: lib/brakeman/report/templates/overview.html.erb ================================================

Summary

<% if warnings_summary['Ignored Warnings'] %> <% end %>
Scanned/Reported Total
Controllers <%= tracker.controllers.length %>
Models <%= tracker.models.length - 1 %>
Templates <%= number_of_templates %>
Errors <%= tracker.errors.length %>
Security Warnings <%= warnings %> (<%= warnings_summary[:high_confidence] %>)
Ignored Warnings <%= ignored_warnings %>

================================================ FILE: lib/brakeman/report/templates/security_warnings.html.erb ================================================

Security Warnings

<% warnings.each do |warning| %> <% end %>
Confidence Class Method Warning Type CWE ID Message
<%= warning['Confidence']%> <%= warning['Class']%> <%= warning['Method']%> <%= warning['Warning Type']%> <%= warning['CWE ID']%> <%= warning['Message']%>
================================================ FILE: lib/brakeman/report/templates/template_overview.html.erb ================================================

Templates

<% template_rows.each do |template| %>

<%= template[0] %>

<% template[1].each do |call| %> <% end %>
Output
<%= call %>
<% end %> ================================================ FILE: lib/brakeman/report/templates/view_warnings.html.erb ================================================

View Warnings

<% warnings.each_with_index do |warning, i| %> <% end %>
Confidence Template Warning Type CWE ID Message
<%= warning['Confidence']%> <% if warning['Called From'] and warning['Called From'].length > 1 %>
<%= warning['Template'] %>
<%= warning['Called From'].join(' → ') %> → <%= warning['Template Name'] %>
<% else %> <%= warning['Template']%> <% end %>
<%= warning['Warning Type']%> <%= warning['CWE ID']%> <%= warning['Message']%>
================================================ FILE: lib/brakeman/report/templates/warning_overview.html.erb ================================================ <% types.sort.each do |warning_type| %> <% end %>
Warning Type Total
<%= warning_type %> <%= warnings_summary[warning_type] %>

================================================ FILE: lib/brakeman/report.rb ================================================ require 'brakeman/report/report_base' #Generates a report based on the Tracker and the results of #Tracker#run_checks. Be sure to +run_checks+ before generating #a report. class Brakeman::Report attr_reader :tracker VALID_FORMATS = [:to_html, :to_pdf, :to_csv, :to_json, :to_tabs, :to_hash, :to_s, :to_markdown, :to_codeclimate, :to_plain, :to_text, :to_junit, :to_github] def initialize tracker @app_tree = tracker.app_tree @tracker = tracker end def format format reporter = case format when :to_codeclimate require_report 'codeclimate' Brakeman::Report::CodeClimate when :to_csv require_report 'csv' Brakeman::Report::CSV when :to_html require_report 'html' Brakeman::Report::HTML when :to_json return self.to_json when :to_tabs require_report 'tabs' Brakeman::Report::Tabs when :to_hash require_report 'hash' Brakeman::Report::Hash when :to_markdown return self.to_markdown when :to_plain, :to_text, :to_s return self.to_plain when :to_table return self.to_table when :to_pdf raise "PDF output is not yet supported." when :to_junit require_report 'junit' Brakeman::Report::JUnit when :to_sarif return self.to_sarif when :to_sonar require_report 'sonar' Brakeman::Report::Sonar when :to_github require_report 'github' Brakeman::Report::Github else raise "Invalid format: #{format}. Should be one of #{VALID_FORMATS.inspect}" end generate(reporter) end def method_missing method, *args if VALID_FORMATS.include? method format method else super end end def require_report type require "brakeman/report/report_#{type}" end def to_json require_report 'json' generate Brakeman::Report::JSON end def to_sonar require_report 'sonar' generate Brakeman::Report::Sonar end def to_table require_report 'table' generate Brakeman::Report::Table end def to_markdown require_report 'markdown' generate Brakeman::Report::Markdown end def to_text require_report 'text' generate Brakeman::Report::Text end alias to_plain to_text alias to_s to_text def to_sarif require_report 'sarif' generate Brakeman::Report::SARIF end def generate reporter reporter.new(@tracker).generate_report end end ================================================ FILE: lib/brakeman/rescanner.rb ================================================ require 'brakeman/scanner' require 'brakeman/util' require 'brakeman/differ' #Class for rescanning changed files after an initial scan class Brakeman::Rescanner < Brakeman::Scanner include Brakeman::Util KNOWN_TEMPLATE_EXTENSIONS = Brakeman::TemplateParser::KNOWN_TEMPLATE_EXTENSIONS #Create new Rescanner to scan changed files def initialize options, processor, changed_files super(options) @old_tracker = processor.tracked_events @paths = changed_files.map {|f| tracker.app_tree.file_path(f) } @old_results = @old_tracker.filtered_warnings.dup #Old warnings from previous scan @changes = nil #True if files had to be rescanned @reindex = Set.new end #Runs checks. #Will rescan files if they have not already been scanned def recheck rescan if @changes.nil? if @changes tracker.run_checks Brakeman.filter_warnings(tracker, options) # Actually sets ignored_filter Brakeman::RescanReport.new @old_results, tracker else # No changes, fake no new results Brakeman::RescanReport.new @old_results, @old_tracker end end #Rescans changed files def rescan raise "Cannot rescan: set `support_rescanning: true`" unless @old_tracker.options[:support_rescanning] tracker.file_cache = @old_tracker.pristine_file_cache template_paths = [] ruby_paths = [] # Remove changed files from the cache. # Collect files to re-parse. @paths.each do |path| file_cache.delete path if path.exists? if path.relative.match? KNOWN_TEMPLATE_EXTENSIONS template_paths << path elsif path.relative.end_with? '.rb' ruby_paths << path end end end # Try to skip rescanning files that do not impact # Brakeman results if @paths.all? { |path| ignorable? path } @changes = false else @changes = true process(ruby_paths:, template_paths:) end self end IGNORE_PATTERN = /\.(md|txt|js|ts|tsx|json|scss|css|xml|ru|png|jpg|pdf|gif|svg|webm|ttf|sql)$/ def ignorable? path path.relative.match? IGNORE_PATTERN end end #Class to make reporting of rescan results simpler to deal with class Brakeman::RescanReport include Brakeman::Util attr_reader :old_results, :new_results def initialize old_results, tracker @tracker = tracker @old_results = old_results @all_warnings = nil @diff = nil end #Returns true if any warnings were found (new or old) def any_warnings? not all_warnings.empty? end #Returns an array of all warnings found def all_warnings @all_warnings ||= @tracker.filtered_warnings end #Returns an array of warnings which were in the old report but are not in the #new report after rescanning def fixed_warnings diff[:fixed] end #Returns an array of warnings which were in the new report but were not in #the old report def new_warnings diff[:new] end #Returns true if there are any new or fixed warnings def warnings_changed? not (diff[:new].empty? and diff[:fixed].empty?) end #Returns a hash of arrays for :new and :fixed warnings def diff @diff ||= Brakeman::Differ.new(all_warnings, @old_results).diff end #Returns an array of warnings which were in the old report and the new report def existing_warnings @old ||= all_warnings.select do |w| not new_warnings.include? w end end #Output total, fixed, and new warnings def to_s <<~OUTPUT Total warnings: #{all_warnings.length} Fixed warnings: #{fixed_warnings.length} New warnings: #{new_warnings.length} OUTPUT end end ================================================ FILE: lib/brakeman/scanner.rb ================================================ begin Brakeman.load_brakeman_dependency 'ruby_parser' require 'ruby_parser/bm_sexp.rb' require 'ruby_parser/bm_sexp_processor.rb' require 'brakeman/processor' require 'brakeman/app_tree' require 'brakeman/file_parser' require 'brakeman/parsers/template_parser' require 'brakeman/processors/lib/file_type_detector' require 'brakeman/tracker/file_cache' rescue LoadError => e $stderr.puts e.message $stderr.puts "Please install the appropriate dependency." exit(-1) end #Scans the Rails application. class Brakeman::Scanner attr_reader :options #Pass in path to the root of the Rails application def initialize options, processor = nil @options = options @app_tree = Brakeman::AppTree.from_options(options) if (!@app_tree.root || !@app_tree.exists?("app")) && !options[:force_scan] message = "Please supply the path to a Rails application (looking in #{@app_tree.root}).\n" << " Use `--force` to run a scan anyway." raise Brakeman::NoApplication, message end @processor = processor || Brakeman::Processor.new(@app_tree, options) end #Returns the Tracker generated from the scan def tracker @processor.tracked_events end def file_cache tracker.file_cache end def process_step(description, &) Brakeman.process_step(description, &) end def process_step_file(description, &) Brakeman.logger.single_context(description, &) end #Process everything in the Rails application def process(ruby_paths: nil, template_paths: nil) process_step 'Processing gems' do process_gems end process_step 'Processing configuration' do guess_rails_version process_config end # - # If ruby_paths or template_paths are set, # only parse those files. The rest will be fetched # from the file cache. # # Otherwise, parse everything normally. # astfiles = nil process_step 'Finding files' do ruby_paths ||= tracker.app_tree.ruby_file_paths template_paths ||= tracker.app_tree.template_paths end process_step 'Parsing files' do astfiles = parse_files(ruby_paths: ruby_paths, template_paths: template_paths) end process_step 'Detecting file types' do detect_file_types(astfiles) end tracker.save_file_cache! if support_rescanning? # - process_step 'Processing initializers' do process_initializers end process_step 'Processing libraries' do process_libs end process_step 'Processing routes' do process_routes end process_step 'Processing templates' do process_templates end process_step 'Processing data flow' do process_template_data_flows end process_step 'Processing models' do process_models end process_step 'Processing controllers' do process_controllers end process_step 'Processing data flow' do process_controller_data_flows end process_step 'Indexing method calls' do index_call_sites end tracker end def parse_files(ruby_paths:, template_paths:) fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], tracker.options[:parallel_checks], tracker.options[:use_prism]) fp.parse_files ruby_paths template_parser = Brakeman::TemplateParser.new(tracker, fp) fp.read_files(template_paths) do |path, contents| Brakeman.logger.spin template_parser.parse_template(path, contents) end # Collect errors raised during parsing tracker.add_errors(fp.errors) fp.file_list end def detect_file_types(astfiles) detector = Brakeman::FileTypeDetector.new astfiles.each do |file| Brakeman.logger.spin if file.is_a? Brakeman::TemplateParser::TemplateFile file_cache.add_file file, :template else type = detector.detect_type(file) unless type == :skip if file_cache.valid_type? type file_cache.add_file(file, type) else raise "Unexpected file type: #{type.inspect}" end end end end end #Process config/environment.rb and config/gems.rb # #Stores parsed information in tracker.config def process_config # Sometimes folks like to put constants in environment.rb # so let's always process it even for newer Rails versions process_config_file "environment.rb" if options[:rails3] or options[:rails4] or options[:rails5] or options[:rails6] process_config_file "application.rb" process_config_file "environments/production.rb" else process_config_file "gems.rb" end if @app_tree.exists?("vendor/plugins/rails_xss") or options[:rails3] or options[:escape_html] tracker.config.escape_html = true Brakeman.debug 'Escaping HTML by default' end if @app_tree.exists? ".ruby-version" if version = @app_tree.file_path(".ruby-version").read[/(\d\.\d.\d+)/] tracker.config.set_ruby_version version, @app_tree.file_path(".ruby-version"), 1 end end tracker.config.load_rails_defaults end def process_config_file file path = @app_tree.file_path("config/#{file}") if path.exists? @processor.process_config(parse_ruby_file(path), path) end rescue => e Brakeman.alert "Error while processing #{path}" tracker.error e.exception(e.message + "\nwhile processing #{path}"), e.backtrace end private :process_config_file #Process Gemfile def process_gems gem_files = {} gem_file_names = ['Gemfile', 'gems.rb'] lock_file_names = ['Gemfile.lock', 'gems.locked'] if tracker.options[:gemfile] name = tracker.options[:gemfile] gem_file_names.unshift name lock_file_names.unshift "#{name}.lock" end gem_file_names.each do |name| if @app_tree.exists? name file = @app_tree.file_path(name) gem_files[:gemfile] = { :src => parse_ruby_file(file), :file => file } break end end lock_file_names.each do |name| if @app_tree.exists? name file = @app_tree.file_path(name) gem_files[:gemlock] = { :src => file.read, :file => file } break end end if @app_tree.gemspec gem_files[:gemspec] = { :src => parse_ruby_file(@app_tree.gemspec), :file => @app_tree.gemspec } end if not gem_files.empty? @processor.process_gems gem_files end rescue => e Brakeman.alert 'Error while processing Gemfile' tracker.error e.exception(e.message + "\nWhile processing Gemfile"), e.backtrace end #Set :rails3/:rails4 option if version was not determined from Gemfile def guess_rails_version unless tracker.options[:rails3] or tracker.options[:rails4] if @app_tree.exists?("script/rails") tracker.options[:rails3] = true Brakeman.debug 'Detected Rails 3 application' elsif @app_tree.exists?("app/channels") tracker.options[:rails3] = true tracker.options[:rails4] = true tracker.options[:rails5] = true Brakeman.debug 'Detected Rails 5 application' elsif not @app_tree.exists?("script") tracker.options[:rails3] = true tracker.options[:rails4] = true Brakeman.debug 'Detected Rails 4 application' end end end #Process all the .rb files in config/initializers/ # #Adds parsed information to tracker.initializers def process_initializers track_progress file_cache.initializers do |path, init| process_step_file path do process_initializer init end end end #Process an initializer def process_initializer init @processor.process_initializer(init.path, init.ast) end # Adds parsed information to tracker.libs. # This is a catch-all for any Ruby files that weren't determined # to be a specific type of file (like a controller). def process_libs libs = file_cache.libs.sort_by { |path, _| path } track_progress libs do |path, lib| process_step_file path do process_lib lib end end end #Process a library def process_lib lib @processor.process_lib lib.ast, lib.path end #Process config/routes.rb # #Adds parsed information to tracker.routes def process_routes if @app_tree.exists?("config/routes.rb") file = @app_tree.file_path("config/routes.rb") if routes_sexp = parse_ruby_file(file) @processor.process_routes routes_sexp else Brakeman.alert 'Error while processing routes - assuming all public controller methods are actions.' options[:assume_all_routes] = true end else Brakeman.alert 'No route information found' end end #Process all .rb files in controllers/ # #Adds processed controllers to tracker.controllers def process_controllers controllers = file_cache.controllers.sort_by { |path, _| path } track_progress controllers do |path, controller| process_step_file path do process_controller controller end end end def process_controller_data_flows controllers = tracker.controllers.sort_by { |name, _| name } track_progress controllers, "controllers" do |name, controller| process_step_file name do controller.src.each do |file, src| @processor.process_controller_alias name, src, nil, file end end end #No longer need these processed filter methods tracker.filter_cache.clear end def process_controller astfile begin @processor.process_controller(astfile.ast, astfile.path) rescue => e tracker.error e.exception(e.message + "\nWhile processing #{astfile.path}"), e.backtrace end end #Process all views and partials in views/ # #Adds processed views to tracker.views def process_templates templates = file_cache.templates.sort_by { |path, _| path } track_progress templates, "templates" do |path, template| process_step_file path do process_template template end end end def process_template template @processor.process_template(template.name, template.ast, template.type, nil, template.path) end def process_template_data_flows templates = tracker.templates.sort_by { |name, _| name } track_progress templates, "templates" do |name, template| process_step_file name do @processor.process_template_alias template end end end #Process all the .rb files in models/ # #Adds the processed models to tracker.models def process_models models = file_cache.models.sort_by { |path, _| path } track_progress models do |path, model| process_step_file path do process_model model end end end def process_model astfile @processor.process_model(astfile.ast, astfile.path) end def track_progress list, type = "files" total = list.length current = 0 list.each do |item| report_progress current, total current += 1 yield item end end def report_progress(current, total) return unless @options[:report_progress] Brakeman.logger.update_progress(current, total) end def index_call_sites tracker.index_call_sites end def parse_ruby_file file fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], false, tracker.options[:use_prism]) fp.parse_ruby(file.read, file) rescue Exception => e tracker.error(e) nil end def support_rescanning? tracker.options[:support_rescanning] end end # This is to allow operation without loading the Haml library module Haml; class Error < StandardError; end; end ================================================ FILE: lib/brakeman/tracker/collection.rb ================================================ require 'brakeman/util' require 'brakeman/tracker/method_info' module Brakeman class Collection include Brakeman::Util attr_reader :collection, :files, :includes, :name, :options, :parent, :src, :tracker def initialize name, parent, file_name, src, tracker @name = name @parent = parent @files = [] @src = {} @includes = [] @methods = { :public => {}, :private => {}, :protected => {} } @class_methods = {} @simple_methods = { :class => {}, instance: {} } @options = {} @tracker = tracker add_file file_name, src end def ancestor? parent, seen={} seen[self.name] = true if self.parent == parent or self.name == parent or seen[self.parent] true elsif parent_model = collection[self.parent] parent_model.ancestor? parent, seen else false end end def add_file file_name, src @files << file_name unless @files.include? file_name @src[file_name] = src end def add_include class_name @includes << class_name unless ancestor?(class_name) end def add_option name, exp @options[name] ||= [] @options[name] << exp end def add_method visibility, name, src, file_name meth_info = Brakeman::MethodInfo.new(name, src, self, file_name) add_simple_method_maybe meth_info if src.node_type == :defs @class_methods[name] = meth_info name = :"#{method_definition_receiver(src[1])}.#{name}" end @methods[visibility][name] = meth_info end def method_definition_receiver(receiver) return receiver if receiver.is_a?(Symbol) case receiver.sexp_type when :self "self" else receiver[1].to_s end end def each_method @methods.each do |_vis, meths| meths.each do |name, info| yield name, info end end end def get_method name, type = :instance case type when :class get_class_method name when :instance get_instance_method name else raise "Unexpected method type: #{type.inspect}" end end def get_instance_method name @methods.each do |_vis, meths| if meths[name] return meths[name] end end nil end def get_class_method name @class_methods[name] end def file @files.first end def top_line if sexp? @src[file] @src[file].line else @src.each_value do |source| if sexp? source return source.line end end end end def methods_public @methods[:public] end def get_simple_method_return_value type, name @simple_methods[type][name] end private def add_simple_method_maybe meth_info if meth_info.very_simple_method? add_simple_method meth_info end end def add_simple_method meth_info name = meth_info.name value = meth_info.return_value case meth_info.src.node_type when :defn @simple_methods[:instance][name] = value when :defs @simple_methods[:class][name] = value else raise "Expected sexp type: #{src.node_type}" end end end end ================================================ FILE: lib/brakeman/tracker/config.rb ================================================ require 'brakeman/util' module Brakeman class Config include Util attr_reader :gems, :rails, :ruby_version, :tracker attr_writer :erubi, :escape_html def initialize tracker @tracker = tracker @rails = {} @gems = {} @settings = {} @escape_html = nil @erubi = nil @ruby_version = nil @rails_version = nil end def default_protect_from_forgery? if version_between? "5.2.0.beta1", "9.9.9" if @rails.dig(:action_controller, :default_protect_from_forgery) == Sexp.new(:true) return true end end false end def erubi? @erubi end def escape_html? @escape_html end def escape_html_entities_in_json? #TODO add version-specific information here true? @rails.dig(:active_support, :escape_html_entities_in_json) end def escape_filter_interpolations? # TODO see if app is actually turning this off itself has_gem?(:haml) and version_between? "5.0.0", "5.99", gem_version(:haml) end def whitelist_attributes? @rails.dig(:active_record, :whitelist_attributes) == Sexp.new(:true) end def gem_version name extract_version @gems.dig(name.to_sym, :version) end def add_gem name, version, file, line name = name.to_sym @gems[name] = { :version => version, :file => file, :line => line } end def has_gem? name !!@gems[name.to_sym] end def get_gem name @gems[name.to_sym] end def set_rails_version version = nil version = if version # Only used by Rails2ConfigProcessor right now extract_version(version) else gem_version(:rails) || gem_version(:railties) || gem_version(:activerecord) end if version @rails_version = version if tracker.options[:rails3].nil? and tracker.options[:rails4].nil? if @rails_version.start_with? "3" tracker.options[:rails3] = true notify_version 3 elsif @rails_version.start_with? "4" tracker.options[:rails3] = true tracker.options[:rails4] = true notify_version 4 elsif @rails_version.start_with? "5" tracker.options[:rails3] = true tracker.options[:rails4] = true tracker.options[:rails5] = true notify_version 5 elsif @rails_version.start_with? "6" tracker.options[:rails3] = true tracker.options[:rails4] = true tracker.options[:rails5] = true tracker.options[:rails6] = true notify_version 6 elsif @rails_version.start_with? "7" tracker.options[:rails3] = true tracker.options[:rails4] = true tracker.options[:rails5] = true tracker.options[:rails6] = true tracker.options[:rails7] = true notify_version 7 elsif @rails_version.start_with? "8" tracker.options[:rails3] = true tracker.options[:rails4] = true tracker.options[:rails5] = true tracker.options[:rails6] = true tracker.options[:rails7] = true tracker.options[:rails8] = true notify_version 8 end end end if get_gem :rails_xss @escape_html = true Brakeman.debug "Escaping HTML by default" end end def rails_version # This needs to be here because Util#rails_version calls Tracker::Config#rails_version # but Tracker::Config includes Util... @rails_version end def set_ruby_version version, file, line @ruby_version = extract_version(version) add_gem :ruby, @ruby_version, file, line end def extract_version version return unless version.is_a? String version[/\d+\.\d+(\.\d+.*)?/] end #Returns true if low_version <= RAILS_VERSION <= high_version # #If the Rails version is unknown, returns false. def version_between? low_version, high_version, current_version = nil current_version ||= rails_version return false unless current_version low = Gem::Version.new(low_version) high = Gem::Version.new(high_version) current = Gem::Version.new(current_version) current.between?(low, high) end def session_settings @rails.dig(:action_controller, :session) end # Set Rails config option value # where path is an array of attributes, e.g. # # :action_controller, :perform_caching # # then this will set # # rails[:action_controller][:perform_caching] = value def set_rails_config value:, path:, overwrite: false config = self.rails path[0..-2].each do |o| config[o] ||= {} option = config[o] if not option.is_a? Hash Brakeman.debug "Skipping config setting: #{path.map(&:to_s).join(".")}" return end config = option end if overwrite || config[path.last].nil? config[path.last] = value end end # Load defaults based on config.load_defaults value # as documented here: https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults def load_rails_defaults return unless node_type? tracker.config.rails[:load_defaults], :lit, :str version = tracker.config.rails[:load_defaults].value.to_s unless version.match?(/^\d+\.\d+$/) Brakeman.alert "Unknown version: #{tracker.config.rails[:load_defaults]}" return end true_value = Sexp.new(:true) false_value = Sexp.new(:false) if version >= '5.0' set_rails_config(value: true_value, path: [:action_controller, :per_form_csrf_tokens]) set_rails_config(value: true_value, path: [:action_controller, :forgery_protection_origin_check]) set_rails_config(value: true_value, path: [:active_record, :belongs_to_required_by_default]) # Note: this may need to be changed, because ssl_options is a Hash set_rails_config(value: true_value, path: [:ssl_options, :hsts, :subdomains]) end if version >= '5.1' set_rails_config(value: false_value, path: [:assets, :unknown_asset_fallback]) set_rails_config(value: true_value, path: [:action_view, :form_with_generates_remote_forms]) end if version >= '5.2' set_rails_config(value: true_value, path: [:active_record, :cache_versioning]) set_rails_config(value: true_value, path: [:action_dispatch, :use_authenticated_cookie_encryption]) set_rails_config(value: true_value, path: [:active_support, :use_authenticated_message_encryption]) set_rails_config(value: true_value, path: [:active_support, :use_sha1_digests]) set_rails_config(value: true_value, path: [:action_controller, :default_protect_from_forgery]) set_rails_config(value: true_value, path: [:action_view, :form_with_generates_ids]) end if version >= '6.0' set_rails_config(value: Sexp.new(:lit, :zeitwerk), path: [:autoloader]) set_rails_config(value: false_value, path: [:action_view, :default_enforce_utf8]) set_rails_config(value: true_value, path: [:action_dispatch, :use_cookies_with_metadata]) set_rails_config(value: false_value, path: [:action_dispatch, :return_only_media_type_on_content_type]) set_rails_config(value: Sexp.new(:str, 'ActionMailer::MailDeliveryJob'), path: [:action_mailer, :delivery_job]) set_rails_config(value: true_value, path: [:active_job, :return_false_on_aborted_enqueue]) set_rails_config(value: Sexp.new(:lit, :active_storage_analysis), path: [:active_storage, :queues, :analysis]) set_rails_config(value: Sexp.new(:lit, :active_storage_purge), path: [:active_storage, :queues, :purge]) set_rails_config(value: true_value, path: [:active_storage, :replace_on_assign_to_many]) set_rails_config(value: true_value, path: [:active_record, :collection_cache_versioning]) end if version >= '6.1' set_rails_config(value: true_value, path: [:action_controller, :urlsafe_csrf_tokens]) set_rails_config(value: Sexp.new(:lit, :lax), path: [:action_dispatch, :cookies_same_site_protection]) set_rails_config(value: Sexp.new(:lit, 308), path: [:action_dispatch, :ssl_default_redirect_status]) set_rails_config(value: false_value, path: [:action_view, :form_with_generates_remote_forms]) set_rails_config(value: true_value, path: [:action_view, :preload_links_header]) set_rails_config(value: Sexp.new(:lit, 0.15), path: [:active_job, :retry_jitter]) set_rails_config(value: true_value, path: [:active_record, :has_many_inversing]) set_rails_config(value: false_value, path: [:active_record, :legacy_connection_handling]) set_rails_config(value: true_value, path: [:active_storage, :track_variants]) end if version >= '7.0' video_args = Sexp.new(:str, "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2") hash_class = s(:colon2, s(:colon2, s(:const, :OpenSSL), :Digest), :SHA256) set_rails_config(value: true_value, path: [:action_controller, :raise_on_open_redirects]) set_rails_config(value: true_value, path: [:action_controller, :wrap_parameters_by_default]) set_rails_config(value: Sexp.new(:lit, :json), path: [:action_dispatch, :cookies_serializer]) set_rails_config(value: false_value, path: [:action_dispatch, :return_only_request_media_type_on_content_type]) set_rails_config(value: Sexp.new(:lit, 5), path: [:action_mailer, :smtp_timeout]) set_rails_config(value: false_value, path: [:action_view, :apply_stylesheet_media_default]) set_rails_config(value: true_value, path: [:ction_view, :button_to_generates_button_tag]) set_rails_config(value: true_value, path: [:active_record, :automatic_scope_inversing]) set_rails_config(value: false_value, path: [:active_record, :partial_inserts]) set_rails_config(value: true_value, path: [:active_record, :verify_foreign_keys_for_fixtures]) set_rails_config(value: true_value, path: [:active_storage, :multiple_file_field_include_hidden]) set_rails_config(value: Sexp.new(:lit, :vips), path: [:active_storage, :variant_processor]) set_rails_config(value: video_args, path: [:active_storage, :video_preview_arguments]) set_rails_config(value: Sexp.new(:lit, 7.0), path: [:active_support, :cache_format_version]) set_rails_config(value: true_value, path: [:active_support, :disable_to_s_conversion]) set_rails_config(value: true_value, path: [:active_support, :executor_around_test_case]) set_rails_config(value: hash_class, path: [:active_support, :hash_digest_class]) set_rails_config(value: Sexp.new(:lit, :thread), path: [:active_support, :isolation_level]) set_rails_config(value: hash_class, path: [:active_support, :key_generator_hash_digest_class]) set_rails_config(value: true_value, path: [:active_support, :remove_deprecated_time_with_zone_name]) set_rails_config(value: true_value, path: [:active_support, :use_rfc4122_namespaced_uuids]) end end private def notify_version version Brakeman.debug "Detected Rails #{version} application" end end end ================================================ FILE: lib/brakeman/tracker/constants.rb ================================================ require 'brakeman/processors/output_processor' require 'brakeman/util' module Brakeman class Constant include Brakeman::Util attr_reader :name, :name_array, :file, :value, :context def initialize name, value, context = {} set_name name, context @value = value @context = context if @context if @context[:class].is_a? Brakeman::Controller @context[:class] = @context[:class].name end @file = @context[:file] end end def line if @value.is_a? Sexp @value.line end end def set_name name, context @name = name @name_array = Constants.constant_as_array(name, context) end def match? name if name == @name return true elsif name.is_a? Sexp and name.node_type == :const and name.value == @name return true elsif name.is_a? Symbol and name.value == @name return true elsif name.class == Array name == @name_array or @name_array.reverse.zip(name.reverse).reduce(true) { |m, a| a[1] ? a[0] == a[1] && m : m } else false end end end class Constants include Brakeman::Util def initialize @constants = {} end def size @constants.length end def [] exp return unless constant? exp match = find_constant exp if match match.value else nil end end def find_constant exp base_name = Constants.get_constant_base_name(exp) if @constants.key? base_name @constants[base_name].find do |c| if c.match? exp return c end end name_array = Constants.constant_as_array(exp) # Avoid losing info about dynamic constant values return unless name_array.all? { |n| constant? n or n.is_a? Symbol } @constants[base_name].find do |c| if c.match? name_array return c end end end nil end def find_all exp base_name = Constants.get_constant_base_name(exp) @constants[base_name] end def add name, value, context = nil if call? value and value.method == :freeze value = value.target end base_name = Constants.get_constant_base_name(name) @constants[base_name] ||= [] @constants[base_name] << Constant.new(name, value, context) end # Returns constant values that are not too complicated. # Right now that means literal values (string, array, etc.) # or calls on Dir.glob(..).whatever. def get_simple_value name if x = self[name] and (literal? x or dir_glob? x) x else nil end end def each @constants.each do |name, values| values.each do |constant| yield constant end end end def self.constant_as_array exp, context = nil # Only prepend context for simple (unqualified) constants if context && (exp.is_a?(Symbol) || (exp.is_a?(Sexp) && exp.node_type == :const)) context_name = context[:module] || context[:class] context_name = context_name.name if context_name.respond_to?(:name) if context_name # Build colon2 chain: A::B becomes s(:colon2, s(:const, :A), :B) parts = context_name.to_s.split("::") base = Sexp.new(:const, parts.first.to_sym) parts[1..].each do |part| base = Sexp.new(:colon2, base, part.to_sym) end exp = Sexp.new(:colon2, base, exp) end end res = [] while exp if exp.is_a? Sexp case exp.node_type when :const res << exp.value exp = nil when :colon3 res << exp.value << :"" exp = nil when :colon2 res << exp.last exp = exp[1] else res << exp exp = nil end else res << exp exp = nil end end res.reverse! res end def self.get_constant_base_name exp return exp unless exp.is_a? Sexp case exp.node_type when :const, :colon3 exp.value when :colon2 exp.last else exp end end end end ================================================ FILE: lib/brakeman/tracker/controller.rb ================================================ require 'brakeman/tracker/collection' module Brakeman module ControllerMethods attr_accessor :layout def initialize_controller @options[:before_filters] = [] @options[:skip_filters] = [] @layout = nil @skip_filter_cache = nil @before_filter_cache = nil end def protect_from_forgery? @options[:protect_from_forgery] end def add_before_filter exp @options[:before_filters] << exp end def prepend_before_filter exp @options[:before_filters].unshift exp end def before_filters @options[:before_filters] end def skip_filter exp @options[:skip_filters] << exp end def skip_filters @options[:skip_filters] end def before_filter_list processor, method controller = self filters = [] while controller filters = controller.get_before_filters(processor, method) + filters controller = tracker.controllers[controller.parent] || tracker.libs[controller.parent] end remove_skipped_filters processor, filters, method end def get_skipped_filters processor, method filters = [] if @skip_filter_cache.nil? @skip_filter_cache = skip_filters.map do |filter| before_filter_to_hash(processor, filter.args) end end @skip_filter_cache.each do |f| if filter_includes_method? f, method filters.concat f[:methods] else end end filters end def remove_skipped_filters processor, filters, method controller = self while controller filters = filters - controller.get_skipped_filters(processor, method) controller = tracker.controllers[controller.parent] || tracker.libs[controller.parent] end filters end def get_before_filters processor, method filters = [] if @before_filter_cache.nil? @before_filter_cache = [] before_filters.each do |filter| @before_filter_cache << before_filter_to_hash(processor, filter.args) end end @before_filter_cache.each do |f| if filter_includes_method? f, method filters.concat f[:methods] end end filters end def before_filter_to_hash processor, args filter = {} #Process args for the uncommon but possible situation #in which some variables are used in the filter. args.each do |a| if sexp? a a = processor.process_default a end end filter[:methods] = [] args.each do |a| filter[:methods] << a[1] if a.node_type == :lit end options = args.last if hash? options # Probably only one option, # but this also avoids issues with kwsplats hash_iterate(options) do |option, value| case value.node_type when :array filter[option.value] = value.sexp_body.map {|v| v[1] } when :lit, :str filter[option.value] = value[1] else Brakeman.debug "Unknown before_filter value: #{option} => #{value}" end end else filter[:all] = true end filter end private def filter_includes_method? filter_rule, method_name filter_rule[:all] or (filter_rule[:only] == method_name) or (filter_rule[:only].is_a? Array and filter_rule[:only].include? method_name) or (filter_rule[:except].is_a? Symbol and filter_rule[:except] != method_name) or (filter_rule[:except].is_a? Array and not filter_rule[:except].include? method_name) end end class Controller < Brakeman::Collection include ControllerMethods def initialize name, parent, file_name, src, tracker super initialize_controller @collection = tracker.controllers end end end ================================================ FILE: lib/brakeman/tracker/file_cache.rb ================================================ module Brakeman class FileCache def initialize(file_list = nil) @file_list = file_list || { controller: {}, initializer: {}, lib: {}, model: {}, template: {}, } end def controllers @file_list[:controller] end def initializers @file_list[:initializer] end def libs @file_list[:lib] end def models @file_list[:model] end def templates @file_list[:template] end def add_file(astfile, type) raise "Unknown type: #{type}" unless valid_type? type @file_list[type][astfile.path] = astfile end def valid_type?(type) @file_list.key? type end def cached? path @file_list.any? do |name, list| list[path] end end def delete path @file_list.each do |name, list| list.delete path end end def diff other @file_list.each do |name, list| other_list = other.send(:"#{name}s") if list == other_list next else puts "-- #{name} --" puts "Old: #{other_list.keys - list.keys}" puts "New: #{list.keys - other_list.keys}" end end end def dup copy_file_list = @file_list.map do |name, list| copy_list = list.map do |path, astfile| copy_astfile = astfile.dup copy_astfile.ast = copy_astfile.ast.deep_clone [path, copy_astfile] end.to_h [name, copy_list] end.to_h FileCache.new(copy_file_list) end end end ================================================ FILE: lib/brakeman/tracker/library.rb ================================================ require 'brakeman/tracker/collection' require 'brakeman/tracker/controller' require 'brakeman/tracker/model' module Brakeman class Library < Brakeman::Collection include ControllerMethods include ModelMethods def initialize name, parent, file_name, src, tracker super initialize_controller initialize_model @collection = tracker.libs end end end ================================================ FILE: lib/brakeman/tracker/method_info.rb ================================================ require 'brakeman/util' module Brakeman class MethodInfo include Brakeman::Util attr_reader :name, :src, :owner, :file, :type def initialize name, src, owner, file @name = name @src = src @owner = owner @file = file @type = case src.node_type when :defn :instance when :defs :class else raise "Expected sexp type: #{src.node_type}" end @simple_method = nil end # To support legacy code that expected a Hash def [] attr self.send(attr) end def very_simple_method? return @simple_method == :very unless @simple_method.nil? # Very simple methods have one (simple) expression in the body and # no arguments if src.formal_args.length == 1 # no args if src.method_length == 1 # Single expression in body value = first_body # First expression in body if simple_literal? value or (array? value and all_literals? value) or (hash? value and all_literals? value, :hash) @return_value = value @simple_method = :very end end end @simple_method ||= false end def return_value env = nil if very_simple_method? return @return_value else nil end end def first_body case @type when :class src[4] when :instance src[3] end end end end ================================================ FILE: lib/brakeman/tracker/model.rb ================================================ require 'brakeman/tracker/collection' module Brakeman module ModelMethods attr_reader :associations, :attr_accessible, :role_accessible def initialize_model @associations = {} @role_accessible = [] @attr_accessible = nil end def association? method_name @associations.each do |name, args| args.each do |arg| if symbol? arg and arg.value == method_name return true end end end false end def unprotected_model? @attr_accessible.nil? and !parent_classes_protected? and ancestor?(:"ActiveRecord::Base") end # go up the chain of parent classes to see if any have attr_accessible def parent_classes_protected? seen={} seen[self.name] = true if @attr_accessible or self.includes.include? :"ActiveModel::ForbiddenAttributesProtection" true elsif parent = tracker.models[self.parent] and !seen[self.parent] parent.parent_classes_protected? seen else false end end def set_attr_accessible exp = nil if exp args = [] exp.each_arg do |e| if node_type? e, :lit args << e.value elsif hash? e @role_accessible.concat args end end @attr_accessible ||= [] @attr_accessible.concat args else @attr_accessible ||= [] end end def set_attr_protected exp add_option :attr_protected, exp end def attr_protected @options[:attr_protected] end end class Model < Brakeman::Collection include ModelMethods ASSOCIATIONS = Set[:belongs_to, :has_one, :has_many, :has_and_belongs_to_many] def initialize name, parent, file_name, src, tracker super initialize_model @collection = tracker.models end def add_option name, exp if ASSOCIATIONS.include? name @associations[name] ||= [] @associations[name].concat exp.args else super name, exp.arglist.line(exp.line) end end end end ================================================ FILE: lib/brakeman/tracker/template.rb ================================================ require 'brakeman/tracker/collection' module Brakeman class Template < Brakeman::Collection attr_accessor :type attr_reader :render_path attr_writer :src def initialize name, called_from, file_name, tracker super name, nil, file_name, nil, tracker @render_path = called_from @outputs = [] end def add_output exp @outputs << exp end def each_output @outputs.each do |o| yield o end end def rendered_from_controller? if @render_path @render_path.rendered_from_controller? else false end end end end ================================================ FILE: lib/brakeman/tracker.rb ================================================ require 'set' require 'brakeman/call_index' require 'brakeman/checks' require 'brakeman/report' require 'brakeman/processors/lib/find_call' require 'brakeman/processors/lib/find_all_calls' require 'brakeman/tracker/config' require 'brakeman/tracker/constants' #The Tracker keeps track of all the processed information. class Brakeman::Tracker attr_accessor :controllers, :constants, :templates, :models, :errors, :checks, :initializers, :config, :routes, :processor, :libs, :template_cache, :options, :filter_cache, :start_time, :end_time, :duration, :ignored_filter, :app_tree, :file_cache, :pristine_file_cache #Place holder when there should be a model, but it is not #clear what model it will be. UNKNOWN_MODEL = :BrakemanUnresolvedModel #Creates a new Tracker. # #The Processor argument is only used by other Processors #that might need to access it. def initialize(app_tree, processor = nil, options = {}) @app_tree = app_tree @processor = processor @options = options @file_cache = Brakeman::FileCache.new @pristine_file_cache = nil reset_all end def reset_all @templates = {} @controllers = {} #Initialize models with the unknown model so #we can match models later without knowing precisely what #class they are. @models = {} @models[UNKNOWN_MODEL] = Brakeman::Model.new(UNKNOWN_MODEL, nil, @app_tree.file_path("NOT_REAL.rb"), nil, self) @method_cache = {} @routes = {} @initializers = {} @errors = [] @libs = {} @constants = Brakeman::Constants.new @checks = nil @processed = nil @template_cache = Set.new @filter_cache = {} @call_index = nil @config = Brakeman::Config.new(self) @start_time = Time.now @end_time = nil @duration = nil end def save_file_cache! @pristine_file_cache = @file_cache.dup end #Add an error to the list. If no backtrace is given, #the one from the exception will be used. def error exception, backtrace = nil backtrace ||= exception.backtrace unless backtrace.is_a? Array backtrace = [ backtrace ] end Brakeman.debug exception Brakeman.debug backtrace @errors << { :exception => exception, :error => exception.to_s.gsub("\n", " "), :backtrace => backtrace } end def add_errors exceptions exceptions.each do |e| error(e) end end #Run a set of checks on the current information. Results will be stored #in Tracker#checks. def run_checks @checks = Brakeman::Checks.run_checks(self) @end_time = Time.now @duration = @end_time - @start_time @checks end def app_path @app_path ||= File.expand_path @options[:app_path] end #Iterate over all methods def each_method [self.controllers, self.models, self.libs].each do |set| set.each do |set_name, collection| collection.each_method do |method_name, definition| src = definition.src yield src, set_name, method_name, definition.file end end end end #Iterates over each template, yielding the name and the template. #Prioritizes templates which have been rendered. def each_template if @processed.nil? @processed, @rest = templates.keys.sort_by{|template| template.to_s}.partition { |k| k.to_s.include? "." } end @processed.each do |k| yield k, templates[k] end @rest.each do |k| yield k, templates[k] end end def each_class [self.controllers, self.models, self.libs].each do |set| set.each do |set_name, collection| collection.src.each do |file, src| yield src, set_name, file end end end end #Find a method call. # #Options: # * :target => target name(s) # * :method => method name(s) # * :chained => search in method chains # #If :target => false or :target => nil, searches for methods without a target. #Targets and methods can be specified as a symbol, an array of symbols, #or a regular expression. # #If :chained => true, matches target at head of method chain and method at end. # #For example: # # find_call :target => User, :method => :all, :chained => true # #could match # # User.human.active.all(...) # def find_call options index_call_sites unless @call_index @call_index.find_calls options end #Searches the initializers for a method call def check_initializers target, method finder = Brakeman::FindCall.new target, method, self initializers.sort.each do |name, initializer| finder.process_source initializer end finder.matches end #Returns a Report with this Tracker's information def report Brakeman::Report.new(self) end def warnings self.checks.all_warnings end def filtered_warnings if self.ignored_filter self.warnings.reject do |w| self.ignored_filter.ignored? w end else self.warnings end end def unused_fingerprints return [] unless self.ignored_filter self.ignored_filter.obsolete_fingerprints end def add_constant name, value, context = nil @constants.add name, value, context unless @options[:disable_constant_tracking] end # This method does not return all constants at this time, # just ones with "simple" values. def constant_lookup name @constants.get_simple_value name unless @options[:disable_constant_tracking] end def find_class name [@controllers, @models, @libs].each do |collection| if c = collection[name] return c end end nil end def find_method method_name, class_name, method_type = :instance return nil unless method_name.is_a? Symbol klass = find_class(class_name) return nil unless klass cache_key = [klass, method_name, method_type] if method = @method_cache[cache_key] return method end if method = klass.get_method(method_name, method_type) return method else # Check modules included for method definition # TODO: only for instance methods, otherwise check extends! klass.includes.each do |included_name| if method = find_method(method_name, included_name, method_type) return (@method_cache[cache_key] = method) end end # Not in any included modules, check the parent @method_cache[cache_key] = find_method(method_name, klass.parent, method_type) end end def index_call_sites finder = Brakeman::FindAllCalls.new self self.each_method do |definition, set_name, method_name, file| finder.process_source definition, :class => set_name, :method => method_name, :file => file end self.each_class do |definition, set_name, file| finder.process_source definition, :class => set_name, :file => file end self.each_template do |_name, template| finder.process_source template.src, :template => template, :file => template.file end self.initializers.each do |file_name, src| finder.process_all_source src, :file => file_name end @call_index = Brakeman::CallIndex.new finder.calls end #Reindex call sites # #Takes a set of symbols which can include :templates, :models, #or :controllers # #This will limit reindexing to the given sets def reindex_call_sites locations #If reindexing templates, models, controllers, #just redo everything. if locations.length == 3 return index_call_sites end if locations.include? :templates @call_index.remove_template_indexes end classes_to_reindex = Set.new method_sets = [] if locations.include? :models classes_to_reindex.merge self.models.keys method_sets << self.models end if locations.include? :controllers classes_to_reindex.merge self.controllers.keys method_sets << self.controllers end if locations.include? :libs classes_to_reindex.merge self.libs.keys method_sets << self.libs end if locations.include? :initializers self.initializers.each do |file_name, src| @call_index.remove_indexes_by_file file_name end end @call_index.remove_indexes_by_class classes_to_reindex finder = Brakeman::FindAllCalls.new self method_sets.each do |set| Brakeman.logger.spin set.each do |set_name, info| info.each_method do |method_name, definition| src = definition.src finder.process_source src, :class => set_name, :method => method_name, :file => definition.file end end end if locations.include? :templates self.each_template do |_name, template| Brakeman.logger.spin finder.process_source template.src, :template => template, :file => template.file end end if locations.include? :initializers self.initializers.each do |file_name, src| Brakeman.logger.spin finder.process_all_source src, :file => file_name end end @call_index.index_calls finder.calls end #Clear information related to templates. #If :only_rendered => true, will delete templates rendered from #controllers (but not those rendered from other templates) def reset_templates options = { :only_rendered => false } if options[:only_rendered] @templates.delete_if do |_name, template| template.rendered_from_controller? end else @templates = {} end @processed = nil @rest = nil @template_cache.clear end #Clear information related to template def reset_template name name = name.to_sym @templates.delete name @processed = nil @rest = nil @template_cache.clear end #Clear information related to model def reset_model path model_name = nil @models.each do |name, model| if model.files.include?(path) model_name = name break end end @models.delete(model_name) end #Clear information related to model def reset_lib path lib_name = nil @libs.each do |name, lib| if lib.files.include?(path) lib_name = name break end end @libs.delete lib_name end def reset_controller path controller_name = nil #Remove from controller @controllers.each do |name, controller| if controller.files.include?(path) controller_name = name #Remove templates rendered from this controller @templates.each do |template_name, template| if template.render_path and template.render_path.include_controller? name reset_template template_name @call_index.remove_template_indexes template_name end end #Remove calls indexed from this controller @call_index.remove_indexes_by_class [name] break end end @controllers.delete controller_name end #Clear information about routes def reset_routes @routes = {} end def reset_initializer path @initializers.delete_if do |file, src| path.relative.include? file end @call_index.remove_indexes_by_file path end # Call this to be able to marshal the Tracker def marshallable @app_tree.marshallable self end end ================================================ FILE: lib/brakeman/util.rb ================================================ require 'set' require 'pathname' #This is a mixin containing utility methods. module Brakeman::Util QUERY_PARAMETERS = Sexp.new(:call, Sexp.new(:call, nil, :request), :query_parameters) PATH_PARAMETERS = Sexp.new(:call, Sexp.new(:call, nil, :request), :path_parameters) REQUEST_REQUEST_PARAMETERS = Sexp.new(:call, Sexp.new(:call, nil, :request), :request_parameters) REQUEST_PARAMETERS = Sexp.new(:call, Sexp.new(:call, nil, :request), :parameters) REQUEST_PARAMS = Sexp.new(:call, Sexp.new(:call, nil, :request), :params) REQUEST_ENV = Sexp.new(:call, Sexp.new(:call, nil, :request), :env) PARAMETERS = Sexp.new(:call, nil, :params) COOKIES = Sexp.new(:call, nil, :cookies) REQUEST_COOKIES = s(:call, s(:call, nil, :request), :cookies) SESSION = Sexp.new(:call, nil, :session) ALL_PARAMETERS = Set[PARAMETERS, QUERY_PARAMETERS, PATH_PARAMETERS, REQUEST_REQUEST_PARAMETERS, REQUEST_PARAMETERS, REQUEST_PARAMS] ALL_COOKIES = Set[COOKIES, REQUEST_COOKIES] SAFE_LITERAL = s(:lit, :BRAKEMAN_SAFE_LITERAL) #Convert a string from "something_like_this" to "SomethingLikeThis" # #Taken from ActiveSupport. def camelize lower_case_and_underscored_word lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } end #Convert a string from "Something::LikeThis" to "something/like_this" # #Taken from ActiveSupport. def underscore camel_cased_word camel_cased_word.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end # stupid simple, used to delegate to ActiveSupport def pluralize word if word.end_with? 's' word + 'es' else word + 's' end end #Returns a class name as a Symbol. #If class name cannot be determined, returns _exp_. def class_name exp case exp when Sexp case exp.node_type when :const, :colon3 exp.value when :lvar exp.value.to_sym when :colon2 "#{class_name(exp.lhs)}::#{exp.rhs}".to_sym when :self @current_class || @current_module || nil else exp end when Symbol exp when nil nil else exp end end #Takes an Sexp like # (:hash, (:lit, :key), (:str, "value")) #and yields the key and value pairs to the given block. # #For example: # # h = Sexp.new(:hash, (:lit, :name), (:str, "bob"), (:lit, :name), (:str, "jane")) # names = [] # hash_iterate(h) do |key, value| # if symbol? key and key[1] == :name # names << value[1] # end # end # names #["bob"] def hash_iterate hash hash = remove_kwsplat(hash) 1.step(hash.length - 1, 2) do |i| yield hash[i], hash[i + 1] end end def remove_kwsplat exp if exp.any? { |e| node_type? e, :kwsplat } exp.reject { |e| node_type? e, :kwsplat } else exp end end #Insert value into Hash Sexp def hash_insert hash, key, value index = 1 hash_iterate hash.dup do |k,v| if k == key hash[index + 1] = value return hash end index += 2 end hash << key << value hash end #Get value from hash using key. # #If _key_ is a Symbol, it will be converted to a Sexp(:lit, key). def hash_access hash, key if key.is_a? Symbol key = Sexp.new(:lit, key) end if index = hash.find_index(key) and index > 0 return hash[index + 1] end nil end def hash_values hash values = hash.each_sexp.each_slice(2).map do |_, value| value end Sexp.new(:array).concat(values).line(hash.line) end #These are never modified PARAMS_SEXP = Sexp.new(:params) SESSION_SEXP = Sexp.new(:session) COOKIES_SEXP = Sexp.new(:cookies) #Adds params, session, and cookies to environment #so they can be replaced by their respective Sexps. def set_env_defaults @env[PARAMETERS] = PARAMS_SEXP @env[SESSION] = SESSION_SEXP @env[COOKIES] = COOKIES_SEXP end #Check if _exp_ represents a hash: s(:hash, {...}) #This also includes pseudo hashes params, session, and cookies. def hash? exp exp.is_a? Sexp and (exp.node_type == :hash or exp.node_type == :params or exp.node_type == :session or exp.node_type == :cookies) end #Check if _exp_ represents an array: s(:array, [...]) def array? exp exp.is_a? Sexp and exp.node_type == :array end #Check if _exp_ represents a String: s(:str, "...") def string? exp exp.is_a? Sexp and exp.node_type == :str end def string_interp? exp exp.is_a? Sexp and exp.node_type == :dstr end #Check if _exp_ represents a Symbol: s(:lit, :...) def symbol? exp exp.is_a? Sexp and exp.node_type == :lit and exp[1].is_a? Symbol end #Check if _exp_ represents a method call: s(:call, ...) def call? exp exp.is_a? Sexp and (exp.node_type == :call or exp.node_type == :safe_call) end #Check if _exp_ represents a Regexp: s(:lit, /.../) def regexp? exp exp.is_a? Sexp and exp.node_type == :lit and exp[1].is_a? Regexp end #Check if _exp_ represents an Integer: s(:lit, ...) def integer? exp exp.is_a? Sexp and exp.node_type == :lit and exp[1].is_a? Integer end #Check if _exp_ represents a number: s(:lit, ...) def number? exp exp.is_a? Sexp and exp.node_type == :lit and exp[1].is_a? Numeric end #Check if _exp_ represents a result: s(:result, ...) def result? exp exp.is_a? Sexp and exp.node_type == :result end #Check if _exp_ represents a :true, :lit, or :string node def true? exp exp.is_a? Sexp and (exp.node_type == :true or exp.node_type == :lit or exp.node_type == :string) end #Check if _exp_ represents a :false or :nil node def false? exp exp.is_a? Sexp and (exp.node_type == :false or exp.node_type == :nil) end #Check if _exp_ represents a block of code def block? exp exp.is_a? Sexp and (exp.node_type == :block or exp.node_type == :rlist) end #Check if _exp_ is a params hash def params? exp recurse_check?(exp) { |child| child.node_type == :params or ALL_PARAMETERS.include? child } end def cookies? exp recurse_check?(exp) { |child| child.node_type == :cookies or ALL_COOKIES.include? child } end def recurse_check? exp, &check if exp.is_a? Sexp return true if yield(exp) if call? exp if recurse_check? exp[1], &check return true elsif exp[2] == :[] return recurse_check? exp[1], &check end end end false end # Only return true when accessing request headers via request.env[...] def request_headers? exp return unless sexp? exp if exp[1] == REQUEST_ENV if exp.method == :[] if string? exp.first_arg # Only care about HTTP headers, which are prefixed by 'HTTP_' exp.first_arg.value.start_with?('HTTP_'.freeze) else true # request.env[something] end else false # request.env.something end else false end end #Check if exp is params, cookies, or request_headers def request_value? exp params? exp or cookies? exp or request_headers? exp end def constant? exp node_type? exp, :const, :colon2, :colon3 end def kwsplat? exp exp.is_a? Sexp and exp.node_type == :hash and exp[1].is_a? Sexp and exp[1].node_type == :kwsplat end #Check if _exp_ is a Sexp. def sexp? exp exp.is_a? Sexp end #Check if _exp_ is a Sexp and the node type matches one of the given types. def node_type? exp, *types exp.is_a? Sexp and types.include? exp.node_type end SIMPLE_LITERALS = [:lit, :false, :str, :true] def simple_literal? exp exp.is_a? Sexp and SIMPLE_LITERALS.include? exp.node_type end LITERALS = [*SIMPLE_LITERALS, :array, :hash] def literal? exp exp.is_a? Sexp and LITERALS.include? exp.node_type end def all_literals? exp, expected_type = :array node_type? exp, expected_type and exp.length > 1 and exp.all? { |e| e.is_a? Symbol or node_type? e, :lit, :str } end DIR_CONST = s(:const, :Dir) # Dir.glob(...).whatever def dir_glob? exp exp = exp.block_call if node_type? exp, :iter return unless call? exp (exp.target == DIR_CONST and exp.method == :glob) or dir_glob? exp.target end #Returns true if the given _exp_ contains a :class node. # #Useful for checking if a module is just a module or if it is a namespace. def contains_class? exp todo = [exp] until todo.empty? current = todo.shift if node_type? current, :class return true elsif sexp? current todo = current.sexp_body.concat todo end end false end def make_call target, method, *args call = Sexp.new(:call, target, method) if args.empty? or args.first.empty? #nothing to do elsif node_type? args.first, :arglist call.concat args.first.sexp_body elsif args.first.node_type.is_a? Sexp #just a list of args call.concat args.first else call.concat args end call end def safe_literal line = nil s(:lit, :BRAKEMAN_SAFE_LITERAL).line(line || 0) end def safe_literal? exp exp == SAFE_LITERAL end def safe_literal_target? exp if call? exp safe_literal_target? exp.target else safe_literal? exp end end def rails_version @tracker.config.rails_version end #Convert path/filename to view name # # views/test/something.html.erb -> test/something def template_path_to_name path names = path.relative.split('/') names.last.gsub!(/(\.(html|js)\..*|\.(rhtml|haml|erb|slim))$/, '') if names.include? 'views' names[(names.index('views') + 1)..-1] else names end.join('/').to_sym end end ================================================ FILE: lib/brakeman/version.rb ================================================ module Brakeman Version = "8.0.4" end ================================================ FILE: lib/brakeman/warning.rb ================================================ require 'json' require 'digest/sha2' require 'brakeman/warning_codes' require 'brakeman/messages' #The Warning class stores information about warnings class Brakeman::Warning attr_reader :called_from, :check, :class, :confidence, :controller, :cwe_id, :line, :method, :model, :template, :user_input, :user_input_type, :warning_code, :warning_set, :warning_type attr_accessor :code, :context, :file, :message TEXT_CONFIDENCE = { 0 => "High", 1 => "Medium", 2 => "Weak", } CONFIDENCE = { :high => 0, :med => 1, :medium => 1, :low => 2, :weak => 2, } OPTIONS = { :called_from => :@called_from, :check => :@check, :class => :@class, :code => :@code, :controller => :@controller, :cwe_id => :@cwe_id, :file => :@file, :gem_info => :@gem_info, :line => :@line, :link => :@link, :link_path => :@link_path, :message => :@message, :method => :@method, :model => :@model, :template => :@template, :user_input => :@user_input, :warning_set => :@warning_set, :warning_type => :@warning_type, } #+options[:result]+ can be a result from Tracker#find_call. Otherwise, it can be +nil+. def initialize options = {} @view_name = nil OPTIONS.each do |key, var| self.instance_variable_set(var, options[key]) end self.confidence = options[:confidence] result = options[:result] if result @code ||= result[:call] @file ||= result[:location][:file] if result[:location][:type] == :template #template result @template ||= result[:location][:template] else @class ||= result[:location][:class] @method ||= result[:location][:method] end end if @method.to_s =~ /^fake_filter\d+/ @method = :before_filter end if @user_input.is_a? Brakeman::BaseCheck::Match @user_input_type = @user_input.type @user_input = @user_input.match elsif @user_input == false @user_input = nil end if not @line if @user_input and @user_input.respond_to? :line @line = @user_input.line elsif @code and @code.respond_to? :line @line = @code.line end end if @gem_info if @gem_info.is_a? Hash @line ||= @gem_info[:line] @file ||= @gem_info[:file] else # Fallback behavior returns just a string for the file name @file ||= @gem_info end end unless @warning_set if self.model @warning_set = :model @file ||= self.model.file elsif self.template @warning_set = :template @called_from = self.template.render_path @file ||= self.template.file elsif self.controller @warning_set = :controller else @warning_set = :warning end end if options[:warning_code] @warning_code = Brakeman::WarningCodes.code options[:warning_code] else @warning_code = nil end Brakeman.debug("Warning created without warning code: #{options[:warning_code]}") unless @warning_code if options[:message].is_a? String @message = Brakeman::Messages::Message.new(options[:message]) end @format_message = nil @row = nil end def hash self.to_s.hash end def eql? other_warning self.hash == other_warning.hash end def confidence= conf @confidence = case conf when Integer conf when Symbol CONFIDENCE[conf] else raise "Could not set confidence to `#{conf}`" end raise "Could not set confidence to `#{conf}`" unless @confidence raise "Invalid confidence: `#{@confidence}`" unless TEXT_CONFIDENCE[@confidence] end #Returns name of a view, including where it was rendered from def view_name(include_renderer = true) if called_from and include_renderer @view_name = "#{template.name} (#{called_from.last})" else @view_name = template.name end end #Return String of the code output from the OutputProcessor and #stripped of newlines and tabs. def format_code strip = true format_ruby self.code, strip end #Return String of the user input formatted and #stripped of newlines and tabs. def format_user_input strip = true format_ruby self.user_input, strip end def format_with_user_input strip = true, &block if self.user_input formatted = Brakeman::OutputProcessor.new.format(code, self.user_input, &block) formatted.gsub!(/(\t|\r|\n)+/, " ") if strip formatted else format_code end end #Return formatted warning message def format_message return @format_message if @format_message @format_message = self.message.to_s.dup if self.line @format_message << " near line #{self.line}" end if self.code @format_message << ": #{format_code}" end @format_message end def link return @link if @link if @link_path if @link_path.start_with? "http" @link = @link_path else @link = "https://brakemanscanner.org/docs/warning_types/#{@link_path}" end else warning_path = self.warning_type.to_s.downcase.gsub(/\s+/, '_') + "/" @link = "https://brakemanscanner.org/docs/warning_types/#{warning_path}" end @link end #Generates a hash suitable for inserting into a table def to_row type = :warning @row = { "Confidence" => TEXT_CONFIDENCE[self.confidence], "Warning Type" => self.warning_type.to_s, "CWE ID" => self.cwe_id, "Message" => self.message } case type when :template @row["Template"] = self.view_name.to_s when :model @row["Model"] = self.model.name.to_s when :controller @row["Controller"] = self.controller.to_s when :warning @row["Class"] = self.class.to_s @row["Method"] = self.method.to_s end @row end def to_s output = "(#{TEXT_CONFIDENCE[self.confidence]}) #{self.warning_type} - #{self.message}" output << " near line #{self.line}" if self.line output << " in #{self.file.relative}" if self.file output << ": #{self.format_code}" if self.code output end def fingerprint loc = self.location location_string = loc && loc.sort_by { |k, v| k.to_s }.inspect warning_code_string = sprintf("%03d", @warning_code) code_string = @code.inspect Digest::SHA2.new(256).update("#{warning_code_string}#{code_string}#{location_string}#{self.file.relative}#{self.confidence}").to_s end def location include_renderer = true case @warning_set when :template { :type => :template, :template => self.view_name(include_renderer) } when :model { :type => :model, :model => self.model.name } when :controller { :type => :controller, :controller => self.controller } when :warning if self.class { :type => :method, :class => self.class, :method => self.method } else nil end end end def relative_path self.file.relative end def check_name @check_name ||= self.check.sub(/^Brakeman::Check/, '') end def confidence_name TEXT_CONFIDENCE[self.confidence] end def to_hash absolute_paths: true if self.called_from and not absolute_paths render_path = self.called_from.with_relative_paths else render_path = self.called_from end { :warning_type => self.warning_type, :warning_code => @warning_code, :fingerprint => self.fingerprint, :check_name => self.check_name, :message => self.message.to_s, :file => (absolute_paths ? self.file.absolute : self.file.relative), :line => self.line, :link => self.link, :code => (@code && self.format_code(false)), :render_path => render_path, :location => self.location(false), :user_input => (@user_input && self.format_user_input(false)), :confidence => self.confidence_name, :cwe_id => cwe_id } end def to_json JSON.generate self.to_hash end private def format_ruby code, strip formatted = Brakeman::OutputProcessor.new.format(code) formatted = formatted.gsub(/(\t|\r|\n)+/, " ") if strip formatted end end ================================================ FILE: lib/brakeman/warning_codes.rb ================================================ module Brakeman::WarningCodes Codes = { :sql_injection => 0, :sql_injection_limit_offset => 1, :cross_site_scripting => 2, :xss_link_to => 3, :xss_link_to_href => 4, :xss_to_json => 5, :csrf_protection_disabled => 6, :csrf_protection_missing => 7, :csrf_blacklist => 8, :basic_auth_password => 9, :auth_blacklist => 10, :all_default_routes => 11, :controller_default_routes => 12, :code_eval => 13, :command_injection => 14, :dynamic_render_path => 15, :file_access => 16, :mass_assign_call => 17, :open_redirect => 18, :no_attr_accessible => 19, :attr_protected_used => 20, :safe_buffer_vuln => 21, :select_options_vuln => 22, :dangerous_send => 23, :unsafe_constantize => 24, :unsafe_deserialize => 25, :http_cookies => 26, :secure_cookies => 27, :translate_vuln => 28, :session_secret => 29, :validation_regex => 30, :CVE_2010_3933 => 31, :CVE_2011_0446 => 32, :CVE_2011_0447 => 33, :CVE_2011_2929 => 34, :CVE_2011_2930 => 35, :CVE_2011_2931 => 36, :CVE_2011_3186 => 37, :CVE_2012_2660 => 38, :CVE_2012_2661 => 39, :CVE_2012_2695 => 40, #:CVE_2012_2931 => 41, :CVE_2012_3424 => 42, :CVE_2012_3463 => 43, :CVE_2012_3464 => 44, :CVE_2012_3465 => 45, :CVE_2012_5664 => 46, :CVE_2013_0155 => 47, :CVE_2013_0156 => 48, :CVE_2013_0269 => 49, :CVE_2013_0277 => 50, :CVE_2013_0276 => 51, :CVE_2013_0333 => 52, :xss_content_tag => 53, :mass_assign_without_protection => 54, :CVE_2013_1854 => 55, :CVE_2013_1855 => 56, :CVE_2013_1856 => 57, :CVE_2013_1857 => 58, :unsafe_symbol_creation => 59, :dangerous_attr_accessible => 60, :local_request_config => 61, :detailed_exceptions => 62, :CVE_2013_4491 => 63, :CVE_2013_6414 => 64, # Replaced by CVE_2014_0081 #:CVE_2013_6415 => 65, #:CVE_2013_6415_call => 66, :CVE_2013_6416 => 67, :CVE_2013_6416_call => 68, :CVE_2013_6417 => 69, :mass_assign_permit! => 70, :ssl_verification_bypass => 71, :CVE_2014_0080 => 72, :CVE_2014_0081 => 73, :CVE_2014_0081_call => 74, :CVE_2014_0082 => 75, :regex_dos => 76, :CVE_2014_0130 => 77, :CVE_2014_3482 => 78, :CVE_2014_3483 => 79, :CVE_2014_3514 => 80, :CVE_2014_3514_call => 81, :unscoped_find => 82, :CVE_2011_2932 => 83, :cross_site_scripting_inline => 84, :CVE_2014_7829 => 85, :csrf_not_protected_by_raising_exception => 86, :CVE_2015_3226 => 87, :CVE_2015_3227 => 88, :session_key_manipulation => 89, :weak_hash_digest => 90, :weak_hash_hmac => 91, :sql_injection_dynamic_finder => 92, :CVE_2015_7576 => 93, :CVE_2016_0751 => 94, :CVE_2015_7577 => 95, :CVE_2015_7578 => 96, :CVE_2015_7580 => 97, :CVE_2015_7579 => 98, :dynamic_render_path_rce => 99, :CVE_2015_7581 => 100, :secret_in_source => 101, :CVE_2016_6316 => 102, :CVE_2016_6317 => 103, :divide_by_zero => 104, :dangerous_permit_key => 105, :CVE_2018_8048 => 106, :CVE_2018_3741 => 107, :CVE_2018_3760 => 108, :force_ssl_disabled => 109, :unsafe_cookie_serialization => 110, :reverse_tabnabbing => 111, :mass_assign_permit_all => 112, :json_html_escape_config => 113, :json_html_escape_module => 114, :CVE_2020_8159 => 115, :CVE_2020_8166 => 116, :erb_template_injection => 117, :http_verb_confusion => 118, :unsafe_method_reflection => 119, :eol_rails => 120, :eol_ruby => 121, :pending_eol_rails => 122, :pending_eol_ruby => 123, :CVE_2022_32209 => 124, :pathname_traversal => 125, :insecure_rsa_padding_mode => 126, :missing_rsa_padding_mode => 127, :small_rsa_key_size => 128, :ransack_search => 129, :custom_check => 9090, } def self.code name Codes[name] end end ================================================ FILE: lib/brakeman.rb ================================================ require 'set' require 'brakeman/logger' require 'brakeman/version' module Brakeman #This exit code is used when warnings are found and the --exit-on-warn #option is set Warnings_Found_Exit_Code = 3 #Exit code returned when no Rails application is detected No_App_Found_Exit_Code = 4 #Exit code returned when brakeman was outdated Not_Latest_Version_Exit_Code = 5 #Exit code returned when user requests non-existent checks Missing_Checks_Exit_Code = 6 #Exit code returned when errors were found and the --exit-on-error #option is set Errors_Found_Exit_Code = 7 #Exit code returned when an ignored warning has no note and #--ensure-ignore-notes is set Empty_Ignore_Note_Exit_Code = 8 # Exit code returned when at least one obsolete ignore entry is present # and `--ensure-no-obsolete-ignore-entries` is set. Obsolete_Ignore_Entries_Exit_Code = 9 @debug = false @quiet = false @loaded_dependencies = [] @vendored_paths = false @logger = nil #Run Brakeman scan. Returns Tracker object. # #Options: # # * :app_path - path to root of Rails app (required) # * :additional_checks_path - array of additional directories containing additional out-of-tree checks to run # * :additional_libs_path - array of additional application relative lib directories (ex. app/mailers) to process # * :assume_all_routes - assume all methods are routes (default: true) # * :check_arguments - check arguments of methods (default: true) # * :collapse_mass_assignment - report unprotected models in single warning (default: false) # * :combine_locations - combine warning locations (default: true) # * :config_file - configuration file # * :escape_html - escape HTML by default (automatic) # * :exit_on_error - only affects Commandline module (default: true) # * :exit_on_warn - only affects Commandline module (default: true) # * :github_repo - github repo to use for file links (user/repo[/path][@ref]) # * :highlight_user_input - highlight user input in reported warnings (default: true) # * :html_style - path to CSS file # * :ignore_model_output - consider models safe (default: false) # * :interprocedural - limited interprocedural processing of method calls (default: false) # * :message_limit - limit length of messages # * :min_confidence - minimum confidence (0-2, 0 is highest) # * :output_files - files for output # * :output_formats - formats for output (:to_s, :to_tabs, :to_csv, :to_html) # * :parallel_checks - run checks in parallel (default: true) # * :parser_timeout - set timeout for parsing an individual file (default: 10 seconds) # * :print_report - if no output file specified, print to stdout (default: false) # * :quiet - suppress most messages (default: true) # * :rails3 - force Rails 3 mode (automatic) # * :rails4 - force Rails 4 mode (automatic) # * :rails5 - force Rails 5 mode (automatic) # * :rails6 - force Rails 6 mode (automatic) # * :report_routes - show found routes on controllers (default: false) # * :run_checks - array of checks to run (run all if not specified) # * :safe_methods - array of methods to consider safe # * :show_ignored - Display warnings that are usually ignored # * :sql_safe_methods - array of sql sanitization methods to consider safe # * :skip_vendor - do not process vendor/ directory (default: true) # * :skip_checks - checks not to run (run all if not specified) # * :absolute_paths - show absolute path of each file (default: false) # * :summary_only - only output summary section of report for plain/table (:summary_only, :no_summary, true) # #Alternatively, just supply a path as a string. def self.run options if not $stderr.tty? and options[:report_progress].nil? options[:report_progress] = false end options = set_options options @quiet = !!options[:quiet] @debug = !!options[:debug] if @quiet options[:report_progress] = false end @logger = options[:logger] || set_default_logger(options) if options[:use_prism] begin require 'prism' rescue LoadError => e Brakeman.alert "Asked to use Prism, but failed to load: #{e}" end end Brakeman.announce "Brakeman v#{Brakeman::Version}" scan options end def self.logger @logger end def self.logger= log @logger = log end def self.set_default_logger(options = {}) @logger = Brakeman::Logger.get_logger(options) end def self.cleanup(newline = true) @logger.cleanup(newline) if @logger end #Sets up options for run, checks given application path def self.set_options options if options.is_a? String options = { :app_path => options } end if options[:quiet] == :command_line command_line = true options.delete :quiet end options = default_options.merge(load_options(options)).merge(options) if options[:quiet].nil? and not command_line options[:quiet] = true end if options[:rails4] options[:rails3] = true elsif options[:rails5] options[:rails3] = true options[:rails4] = true elsif options[:rails6] options[:rails3] = true options[:rails4] = true options[:rails5] = true end options[:output_formats] = get_output_formats options options[:github_url] = get_github_url options # Use ENV value only if option was not already explicitly set # (i.e. prefer commandline option over environment variable). if options[:gemfile].nil? and ENV['BUNDLE_GEMFILE'] and not ENV['BUNDLE_GEMFILE'].empty? options[:gemfile] = ENV['BUNDLE_GEMFILE'] end options end #Load options from YAML file def self.load_options line_options custom_location = line_options[:config_file] app_path = line_options[:app_path] #Load configuration file if config = config_file(custom_location, app_path) require 'yaml' options = YAML.safe_load_file config, permitted_classes: [Symbol], symbolize_names: true if options options.each { |k, v| options[k] = Set.new v if v.is_a? Array } # After parsing the yaml config file for options, convert any string keys into symbols. options.keys.select {|k| k.is_a? String}.map {|k| k.to_sym }.each {|k| options[k] = options[k.to_s]; options.delete(k.to_s) } # Brakeman.logger is probably not set yet logger = Brakeman::Logger.get_logger(options.merge(line_options)) unless line_options[:allow_check_paths_in_config] if options.include? :additional_checks_path options.delete :additional_checks_path logger.alert 'Ignoring additional check paths in config file. Use --allow-check-paths-in-config to allow' end end logger.alert "Using configuration in #{config}" options else logger = Brakeman::Logger.get_logger(line_options) logger.alert "Empty configuration file: #{config}" {} end else {} end end CONFIG_FILES = begin [ File.expand_path("~/.brakeman/config.yml"), File.expand_path("/etc/brakeman/config.yml") ] rescue ArgumentError # In case $HOME or $USER aren't defined for use of `~` [ File.expand_path("/etc/brakeman/config.yml") ] end def self.config_file custom_location, app_path app_config = File.expand_path(File.join(app_path, "config", "brakeman.yml")) supported_locations = [File.expand_path(custom_location || ""), app_config] + CONFIG_FILES supported_locations.detect {|f| File.file?(f) } end #Default set of options def self.default_options { :assume_all_routes => true, :check_arguments => true, :collapse_mass_assignment => false, :combine_locations => true, :engine_paths => ["engines/*"], :exit_on_error => true, :exit_on_warn => true, :highlight_user_input => true, :html_style => "#{File.expand_path(File.dirname(__FILE__))}/brakeman/format/style.css", :ignore_model_output => false, :ignore_redirect_to_model => true, :message_limit => 100, :min_confidence => 2, :output_color => true, :pager => true, :parallel_checks => true, :parser_timeout => 10, :use_prism => true, :relative_path => false, :report_progress => true, :safe_methods => Set.new, :show_ignored => false, :sql_safe_methods => Set.new, :skip_checks => Set.new, :skip_vendor => true, } end #Determine output formats based on options[:output_formats] #or options[:output_files] def self.get_output_formats options #Set output format if options[:output_format] && options[:output_files] && options[:output_files].size > 1 raise ArgumentError, "Cannot specify output format if multiple output files specified" end if options[:output_format] get_formats_from_output_format options[:output_format] elsif options[:output_files] get_formats_from_output_files options[:output_files] else begin self.load_brakeman_dependency 'terminal-table', :allow_fail return [:to_s] rescue LoadError return [:to_json] end end end def self.get_formats_from_output_format output_format case output_format when :html, :to_html [:to_html] when :csv, :to_csv [:to_csv] when :pdf, :to_pdf [:to_pdf] when :tabs, :to_tabs [:to_tabs] when :json, :to_json [:to_json] when :markdown, :to_markdown [:to_markdown] when :cc, :to_cc, :codeclimate, :to_codeclimate [:to_codeclimate] when :plain ,:to_plain, :text, :to_text, :to_s [:to_text] when :table, :to_table [:to_table] when :junit, :to_junit [:to_junit] when :sarif, :to_sarif [:to_sarif] when :sonar, :to_sonar [:to_sonar] when :github, :to_github [:to_github] else [:to_text] end end private_class_method :get_formats_from_output_format def self.get_formats_from_output_files output_files output_files.map do |output_file| case output_file when /\.html$/i :to_html when /\.csv$/i :to_csv when /\.pdf$/i :to_pdf when /\.tabs$/i :to_tabs when /\.json$/i :to_json when /\.md$/i :to_markdown when /(\.cc|\.codeclimate)$/i :to_codeclimate when /\.plain$/i :to_text when /\.table$/i :to_table when /\.junit$/i :to_junit when /\.sarif$/i :to_sarif when /\.sonar$/i :to_sonar when /\.github$/i :to_github else :to_text end end end private_class_method :get_formats_from_output_files def self.get_github_url options if github_repo = options[:github_repo] full_repo, ref = github_repo.split '@', 2 name, repo, path = full_repo.split '/', 3 unless name && repo && !(name.empty? || repo.empty?) raise ArgumentError, "Invalid GitHub repository format" end path.chomp '/' if path ref ||= 'master' ['https://github.com', name, repo, 'blob', ref, path].compact.join '/' else nil end end private_class_method :get_github_url #Output list of checks (for `-k` option) def self.list_checks options require 'brakeman/scanner' add_external_checks options if options[:list_optional_checks] $stderr.puts "Optional Checks:" checks = Checks.optional_checks else $stderr.puts "Available Checks:" checks = Checks.checks end format_length = 30 $stderr.puts "-" * format_length checks.each do |check| $stderr.printf("%-#{format_length}s%s\n", check.name, check.description) end end #Output configuration to YAML def self.dump_config options require 'yaml' if options[:create_config].is_a? String file = options[:create_config] else file = nil end options.delete :create_config if options[:logger] @logger = options.delete(:logger) else set_default_logger(options) end options.each do |k,v| if v.is_a? Set options[k] = v.to_a end end if file File.open file, "w" do |f| YAML.dump options, f end announce "Output configuration to #{file}" else $stdout.puts YAML.dump(options) end end # Returns quit message unless the latest version # of Brakeman matches the current version. # # Optionally checks that the latest version is at least # the specified number of days old. def self.ensure_latest(days_old: 0) require 'date' current = Brakeman::Version latest = Gem.latest_spec_for('brakeman') release_date = latest.date.to_date latest_version = latest.version.to_s if (Date.today - latest.date.to_date) >= days_old if current != latest_version return "Brakeman #{current} is not the latest version #{latest_version}" else false end else false end end #Run a scan. Generally called from Brakeman.run instead of directly. def self.scan options #Load scanner scanner, tracker = nil process_step 'Loading scanner' do begin require 'brakeman/scanner' rescue LoadError raise NoBrakemanError, 'Cannot find lib/ directory.' end add_external_checks options #Start scanning scanner = Scanner.new options tracker = scanner.tracker check_for_missing_checks options[:run_checks], options[:skip_checks], options[:enable_checks] end logger.announce "Scanning #{tracker.app_path}" scanner.process tracker.run_checks self.filter_warnings tracker, options if options[:output_files] process_step 'Generating report' do write_report_to_files tracker, options[:output_files] end elsif options[:print_report] process_step 'Generating report' do write_report_to_formats tracker, options[:output_formats] end end tracker end def self.write_report_to_files tracker, output_files require 'fileutils' tracker.options[:output_color] = false unless tracker.options[:output_color] == :force output_files.each_with_index do |output_file, idx| dir = File.dirname(output_file) unless Dir.exist? dir FileUtils.mkdir_p(dir) end File.open output_file, "w" do |f| f.write tracker.report.format(tracker.options[:output_formats][idx]) end logger.announce "Report saved in '#{output_file}'" end end private_class_method :write_report_to_files def self.write_report_to_formats tracker, output_formats unless $stdout.tty? or tracker.options[:output_color] == :force tracker.options[:output_color] = false end if not $stdout.tty? or not tracker.options[:pager] or output_formats.length > 1 # does this ever happen?? output_formats.each do |output_format| puts tracker.report.format(output_format) end else require "brakeman/report/pager" Brakeman::Pager.new(tracker).page_report(tracker.report, output_formats.first) end end private_class_method :write_report_to_formats #Rescan a subset of files in a Rails application. # #A full scan must have been run already to use this method. #The returned Tracker object from Brakeman.run is used as a starting point #for the rescan. # #Options may be given as a hash with the same values as Brakeman.run. #Note that these options will be merged into the Tracker. # #This method returns a RescanReport object with information about the scan. #However, the Tracker object will also be modified as the scan is run. def self.rescan tracker, files, options = {} require 'brakeman/rescanner' options = tracker.options.merge options @quiet = !!tracker.options[:quiet] @debug = !!tracker.options[:debug] Rescanner.new(options, tracker.processor, files).recheck end def self.announce message logger.announce message end def self.alert message logger.alert message end def self.debug message logger.debug message end # Compare JSON output from a previous scan and return the diff of the two scans def self.compare options require 'json' require 'brakeman/differ' raise ArgumentError.new("Comparison file doesn't exist") unless File.exist? options[:previous_results_json] begin previous_results = JSON.parse(File.read(options[:previous_results_json]), :symbolize_names => true)[:warnings] rescue JSON::ParserError self.alert "Error parsing comparison file: #{options[:previous_results_json]}" exit! end tracker = run(options) new_report = JSON.parse(tracker.report.to_json, symbolize_names: true) new_results = new_report[:warnings] obsolete_ignored = tracker.unused_fingerprints Brakeman::Differ.new(new_results, previous_results).diff.tap do |diff| diff[:obsolete] = obsolete_ignored end end def self.load_brakeman_dependency name, allow_fail = false return if @loaded_dependencies.include? name unless @vendored_paths path_load = "#{File.expand_path(File.dirname(__FILE__))}/../bundle/load.rb" if File.exist? path_load require path_load end @vendored_paths = true end begin require name rescue LoadError => e if allow_fail raise e else $stderr.puts e.message $stderr.puts "Please install the appropriate dependency: #{name}." exit!(-1) end end end # Returns an array of alert fingerprints for any ignored warnings without # notes found in the specified ignore file (if it exists). def self.ignore_file_entries_with_empty_notes file return [] unless file require 'brakeman/report/ignore/config' config = IgnoreConfig.new(file, nil) config.read_from_file config.already_ignored_entries_with_empty_notes.map { |i| i[:fingerprint] } end def self.filter_warnings tracker, options require 'brakeman/report/ignore/config' config = nil app_tree = Brakeman::AppTree.from_options(options) if options[:ignore_file] file = options[:ignore_file] elsif app_tree.exists? "config/brakeman.ignore" file = app_tree.expand_path("config/brakeman.ignore") elsif not options[:interactive_ignore] return end process_step "Filtering warnings..." do if options[:interactive_ignore] require 'brakeman/report/ignore/interactive' logger.cleanup config = InteractiveIgnorer.new(file, tracker.warnings).start else logger.announce "Using '#{file}' to filter warnings" config = IgnoreConfig.new(file, tracker.warnings) config.read_from_file config.filter_ignored end end tracker.ignored_filter = config end def self.add_external_checks options options[:additional_checks_path].each do |path| Brakeman::Checks.initialize_checks path end if options[:additional_checks_path] end def self.check_for_missing_checks included_checks, excluded_checks, enabled_checks checks = included_checks.to_a + excluded_checks.to_a + enabled_checks.to_a missing = Brakeman::Checks.missing_checks(checks) unless missing.empty? raise MissingChecksError, "Could not find specified check#{missing.length > 1 ? 's' : ''}: #{missing.map {|c| "`#{c}`"}.join(', ')}" end end def self.debug= val @debug = val end def self.quiet= val @quiet = val end def self.process_step(description, &) logger.context(description, &) end class DependencyError < RuntimeError; end class NoBrakemanError < RuntimeError; end class NoApplication < RuntimeError; end class MissingChecksError < RuntimeError; end end ================================================ FILE: lib/ruby_parser/bm_sexp.rb ================================================ #Sexp changes from ruby_parser #and some changes for caching hash value and tracking 'original' line number #of a Sexp. class Sexp attr_accessor :original_line, :or_depth ASSIGNMENT_BOOL = [:gasgn, :iasgn, :lasgn, :cvdecl, :cvasgn, :cdecl, :or, :and, :colon2, :op_asgn_or] CALLS = [:call, :attrasgn, :safe_call, :safe_attrasgn] alias_method :method_missing, :method_missing # silence redefined method warning def method_missing name, *args #Brakeman does not use this functionality, #so overriding it to raise a NoMethodError. # #The original functionality calls find_node and optionally #deletes the node if found. # #Defining a method named "return" seems like a bad idea, so we have to #check for it here instead if name == :return find_node name, *args else raise NoMethodError.new("No method '#{name}' for Sexp", name, args) end end #Create clone of Sexp and nested Sexps but not their non-Sexp contents. #If a line number is provided, also sets line/original_line on all Sexps. def deep_clone line = nil s = Sexp.new self.each do |e| if e.is_a? Sexp s << e.deep_clone(line) else s << e end end if line s.original_line = self.original_line || self.line s.line(line) else s.original_line = self.original_line s.line(self.line) if self.line end s end alias_method :paren, :paren # silence redefined method warning def paren @paren ||= false end alias_method :value, :value # silence redefined method warning def value raise WrongSexpError, "Sexp#value called on multi-item Sexp: `#{self.inspect}`" if size > 2 self[1] end def value= exp raise WrongSexpError, "Sexp#value= called on multi-item Sexp: `#{self.inspect}`" if size > 2 @my_hash_value = nil self[1] = exp end def second self[1] end def to_sym self.value.to_sym end def node_type= type @my_hash_value = nil self[0] = type end #Join self and exp into an :or Sexp. #Sets or_depth. #Used for combining "branched" values in AliasProcessor. def combine exp, line = nil combined = Sexp.new(:or, self, exp).line(line || -2) combined.or_depth = [self.or_depth, exp.or_depth].compact.reduce(0, :+) + 1 combined end alias :node_type :sexp_type alias :values :sexp_body # TODO: retire alias :old_push :<< alias :old_compact :compact alias :old_fara :find_and_replace_all alias :old_find_node :find_node def << arg @my_hash_value = nil old_push arg end alias_method :hash, :hash # silence redefined method warning def hash #There still seems to be some instances in which the hash of the #Sexp changes, but I have not found what method call is doing it. #Of course, Sexp is subclasses from Array, so who knows what might #be going on. @my_hash_value ||= super end def compact @my_hash_value = nil old_compact end def find_and_replace_all *args @my_hash_value = nil old_fara(*args) end def find_node *args @my_hash_value = nil old_find_node(*args) end #Raise a WrongSexpError if the nodes type does not match one of the expected #types. def expect *types unless types.include? self.node_type raise WrongSexpError, "Expected #{types.join ' or '} but given #{self.inspect}", caller[1..-1] end end #Returns target of a method call: # #s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1))) # ^-----------target-----------^ def target expect :call, :attrasgn, :safe_call, :safe_attrasgn self[1] end #Sets the target of a method call: def target= exp expect :call, :attrasgn, :safe_call, :safe_attrasgn @my_hash_value = nil self[1] = exp end #Returns method of a method call: # #s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1))) # ^- method def method expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper, :result case self.node_type when :call, :attrasgn, :safe_call, :safe_attrasgn self[2] when :super, :zsuper :super when :result self.last end end def method= name expect :call, :safe_call self[2] = name end # Number of arguments in a method call. def num_args expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper case self.node_type when :call, :attrasgn, :safe_call, :safe_attrasgn self.length - 3 when :super self.length - 1 when :zsuper 0 end end #Sets the arglist in a method call. def arglist= exp expect :call, :attrasgn, :safe_call, :safe_attrasgn @my_hash_value = nil start_index = 3 if exp.is_a? Sexp and exp.node_type == :arglist exp = exp.sexp_body end exp.each_with_index do |e, i| self[start_index + i] = e end end def set_args *exp self.arglist = exp end #Returns arglist for method call. This differs from Sexp#args, as Sexp#args #does not return a 'real' Sexp (it does not have a node type) but #Sexp#arglist returns a s(:arglist, ...) # # s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1), s(:lit, 2))) # ^------------ arglist ------------^ def arglist expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper case self.node_type when :call, :attrasgn, :safe_call, :safe_attrasgn self.sexp_body(3).unshift :arglist when :super, :zsuper if self[1] self.sexp_body.unshift :arglist else Sexp.new(:arglist) end end end #Returns arguments of a method call. This will be an 'untyped' Sexp. # # s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1), s(:lit, 2))) # ^--------args--------^ def args expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper case self.node_type when :call, :attrasgn, :safe_call, :safe_attrasgn if self[3] self.sexp_body(3) else Sexp.new end when :super, :zsuper if self[1] self.sexp_body else Sexp.new end end end def each_arg replace = false expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper range = nil case self.node_type when :call, :attrasgn, :safe_call, :safe_attrasgn if self[3] range = (3...self.length) end when :super, :zsuper if self[1] range = (1...self.length) end end if range range.each do |i| res = yield self[i] self[i] = res if replace end end self end def each_arg! &block @my_hash_value = nil self.each_arg true, &block end #Returns first argument of a method call. def first_arg expect :call, :attrasgn, :safe_call, :safe_attrasgn self[3] end #Sets first argument of a method call. def first_arg= exp expect :call, :attrasgn, :safe_call, :safe_attrasgn @my_hash_value = nil self[3] = exp end #Returns second argument of a method call. def second_arg expect :call, :attrasgn, :safe_call, :safe_attrasgn self[4] end #Sets second argument of a method call. def second_arg= exp expect :call, :attrasgn, :safe_call, :safe_attrasgn @my_hash_value = nil self[4] = exp end def third_arg expect :call, :attrasgn, :safe_call, :safe_attrasgn self[5] end def third_arg= exp expect :call, :attrasgn, :safe_call, :safe_attrasgn @my_hash_value = nil self[5] = exp end def last_arg expect :call, :attrasgn, :safe_call, :safe_attrasgn if self[3] self[-1] else nil end end def call_chain expect :call, :attrasgn, :safe_call, :safe_attrasgn chain = [] call = self while call.class == Sexp and CALLS.include? call.first chain << call.method call = call.target end chain.reverse! chain end #Returns condition of an if expression: # # s(:if, # s(:lvar, :condition), <-- condition # s(:lvar, :then_val), # s(:lvar, :else_val))) def condition expect :if self[1] end def condition= exp expect :if self[1] = exp end #Returns 'then' clause of an if expression: # # s(:if, # s(:lvar, :condition), # s(:lvar, :then_val), <-- then clause # s(:lvar, :else_val))) def then_clause expect :if self[2] end #Returns 'else' clause of an if expression: # # s(:if, # s(:lvar, :condition), # s(:lvar, :then_val), # s(:lvar, :else_val))) # ^---else caluse---^ def else_clause expect :if self[3] end #Method call associated with a block: # # s(:iter, # s(:call, nil, :x, s(:arglist)), <- block_call # s(:lasgn, :y), # s(:block, s(:lvar, :y), s(:call, nil, :z, s(:arglist)))) def block_call expect :iter if self[1].node_type == :lambda s(:call, nil, :lambda).line(self.line) else self[1] end end #Returns block of a call with a block. #Could be a single expression or a block: # # s(:iter, # s(:call, nil, :x, s(:arglist)), # s(:lasgn, :y), # s(:block, s(:lvar, :y), s(:call, nil, :z, s(:arglist)))) # ^-------------------- block --------------------------^ def block delete = nil unless delete.nil? #this is from RubyParser return find_node :block, delete end expect :iter, :scope, :resbody case self.node_type when :iter self[3] when :scope self[1] when :resbody #This is for Ruby2Ruby ONLY find_node :block end end #Returns parameters for a block # # s(:iter, # s(:call, nil, :x, s(:arglist)), # s(:lasgn, :y), <- block_args # s(:call, nil, :p, s(:arglist, s(:lvar, :y)))) def block_args expect :iter if self[2] == 0 # ?! See https://github.com/presidentbeef/brakeman/issues/331 return Sexp.new(:args) else self[2] end end def first_param expect :args self[1] end #Returns the left hand side of assignment or boolean: # # s(:lasgn, :x, s(:lit, 1)) # ^--lhs def lhs expect(*ASSIGNMENT_BOOL) self[1] end #Sets the left hand side of assignment or boolean. def lhs= exp expect(*ASSIGNMENT_BOOL) @my_hash_value = nil self[1] = exp end #Returns right side (value) of assignment or boolean: # # s(:lasgn, :x, s(:lit, 1)) # ^--rhs---^ def rhs expect :attrasgn, :safe_attrasgn, *ASSIGNMENT_BOOL if self.node_type == :attrasgn or self.node_type == :safe_attrasgn if self[2] == :[]= self[4] else self[3] end else self[2] end end #Sets the right hand side of assignment or boolean. def rhs= exp expect :attrasgn, :safe_attrasgn, *ASSIGNMENT_BOOL @my_hash_value = nil if self.node_type == :attrasgn or self.node_type == :safe_attrasgn self[3] = exp else self[2] = exp end end #Returns name of method being defined in a method definition. def method_name expect :defn, :defs case self.node_type when :defn self[1] when :defs self[2] end end def formal_args expect :defn, :defs case self.node_type when :defn self[2] when :defs self[3] end end #Sets body, which is now a complicated process because the body is no longer #a separate Sexp, but just a list of Sexps. def body= exp expect :defn, :defs, :class, :module @my_hash_value = nil case self.node_type when :defn, :class index = 3 when :defs index = 4 when :module index = 2 end self.slice!(index..-1) #Remove old body if exp.first == :rlist exp = exp.sexp_body end #Insert new body exp.each do |e| self[index] = e index += 1 end end #Returns body of a method definition, class, or module. #This will be an untyped Sexp containing a list of Sexps from the body. def body expect :defn, :defs, :class, :module case self.node_type when :defn, :class self.sexp_body(3) when :defs self.sexp_body(4) when :module self.sexp_body(2) end end #Like Sexp#body, except the returned Sexp is of type :rlist #instead of untyped. def body_list self.body.unshift :rlist end # Number of "statements" in a method. # This is more efficient than `Sexp#body.length` # because `Sexp#body` creates a new Sexp. def method_length expect :defn, :defs case self.node_type when :defn self.length - 3 when :defs self.length - 4 end end def render_type expect :render self[1] end def class_name expect :class, :module self[1] end alias module_name class_name def parent_name expect :class self[2] end #Returns the call Sexp in a result returned from FindCall def call expect :result self.last end #Returns the module the call is inside def module expect :result self[1] end #Return the class the call is inside def result_class expect :result self[2] end require 'set' def inspect seen = Set.new if seen.include? self.object_id 's(...)' else seen << self.object_id sexp_str = self.map do |x| if x.is_a? Sexp x.inspect seen else x.inspect end end.join(', ') "s(#{sexp_str})" end end end #Invalidate hash cache if the Sexp changes [:[]=, :clear, :collect!, :compact!, :concat, :delete, :delete_at, :delete_if, :drop, :drop_while, :fill, :flatten!, :insert, :keep_if, :map!, :pop, :push, :reject!, :replace, :reverse!, :rotate!, :select!, :shift, :shuffle!, :slice!, :sort!, :sort_by!, :transpose, :uniq!, :unshift].each do |method| Sexp.class_eval <<-RUBY def #{method} *args @my_hash_value = nil super end RUBY end #Methods used by RubyParser which would normally go through method_missing but #we don't want that to happen because it hides Brakeman errors [:resbody, :lasgn, :iasgn, :splat].each do |method| Sexp.class_eval <<-RUBY def #{method} delete = false if delete @my_hash_value = false end find_node :#{method}, delete end RUBY end class String ## # This is a hack used by the lexer to sneak in line numbers at the # identifier level. This should be MUCH smaller than making # process_token return [value, lineno] and modifying EVERYTHING that # reduces tIDENTIFIER. attr_accessor :lineno end class WrongSexpError < RuntimeError; end ================================================ FILE: lib/ruby_parser/bm_sexp_processor.rb ================================================ ## # SexpProcessor provides a uniform interface to process Sexps. # # In order to create your own SexpProcessor subclass you'll need # to call super in the initialize method, then set any of the # Sexp flags you want to be different from the defaults. # # SexpProcessor uses a Sexp's type to determine which process method # to call in the subclass. For Sexp s(:lit, 1) # SexpProcessor will call #process_lit, if it is defined. # class Brakeman::SexpProcessor VERSION = 'CUSTOM' ## # Return a stack of contexts. Most recent node is first. attr_reader :context ## # Expected result class attr_accessor :expected ## # A scoped environment to make you happy. attr_reader :env # Cache process methods per class def self.processors @processors ||= {} end ## # Creates a new SexpProcessor. Use super to invoke this # initializer from SexpProcessor subclasses, then use the # attributes above to customize the functionality of the # SexpProcessor def initialize @expected = Sexp @processors = self.class.processors @context = [] @current_class = @current_module = @current_method = @visibility = nil if @processors.empty? public_methods.each do |name| if name.to_s.start_with? "process_" then @processors[name[8..-1].to_sym] = name.to_sym end end end end ## # Default Sexp processor. Invokes process_ methods matching # the Sexp type given. Performs additional checks as specified by # the initializer. def process(exp) return nil if exp.nil? result = nil type = exp.first raise "Type should be a Symbol, not: #{exp.first.inspect} in #{exp.inspect}" unless Symbol === type in_context type do # now do a pass with the real processor (or generic) meth = @processors[type] if meth then result = self.send(meth, exp) else result = self.process_default(exp) end end raise SexpTypeError, "Result must be a #{@expected}, was #{result.class}:#{result.inspect}" unless @expected === result result end ## # Add a scope level to the current env. Eg: # # def process_defn exp # name = exp.shift # args = process(exp.shift) # scope do # body = process(exp.shift) # # ... # end # end # # env[:x] = 42 # scope do # env[:x] # => 42 # env[:y] = 24 # end # env[:y] # => nil def scope &block env.scope(&block) end def in_context type self.context.unshift type yield self.context.shift end end ================================================ FILE: test/README.md ================================================ ## Testing Run `bundle` then `rake` or if you want to avoid bundler `ruby test/test.rb`. This runs Brakeman against full apps in the `apps` directory and checks the results against what is expected. ## Test Generation Run `cd test && ruby to_test.rb apps/some_app > tests/some_app.rb` to generate a test suite with tests for each warning reported. ## Single File Run `ruby test/tests/some_file.rb` to run a single file of tests. ## Single Test Ruby `ruby test/test.rb --name test_something` to run a single test. ================================================ FILE: test/apps/active_record_only/Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gem "activerecord", "~> 5.2.4.3" ================================================ FILE: test/apps/active_record_only/app/models/book.rb ================================================ # frozen_string_literal: true require "active_record" class Book < ActiveRecord::Base end ================================================ FILE: test/apps/active_record_only/script/.gitkeep ================================================ Kept only to help guessing this is a rails 2 unless activerecord version is detected ================================================ FILE: test/apps/rails2/README ================================================ == Welcome to Rails Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Control pattern. This pattern splits the view (also called the presentation) into "dumb" templates that are primarily responsible for inserting pre-built data in between HTML tags. The model contains the "smart" domain objects (such as Account, Product, Person, Post) that holds all the business logic and knows how to persist themselves to a database. The controller handles the incoming requests (such as Save New Account, Update Product, Show Post) by manipulating the model and directing data to the view. In Rails, the model is handled by what's called an object-relational mapping layer entitled Active Record. This layer allows you to present the data from database rows as objects and embellish these data objects with business logic methods. You can read more about Active Record in link:files/vendor/rails/activerecord/README.html. The controller and view are handled by the Action Pack, which handles both layers by its two parts: Action View and Action Controller. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between the Active Record and Action Pack that is much more separate. Each of these packages can be used independently outside of Rails. You can read more about Action Pack in link:files/vendor/rails/actionpack/README.html. == Getting Started 1. At the command prompt, start a new Rails application using the rails command and your application name. Ex: rails myapp 2. Change directory into myapp and start the web server: script/server (run with --help for options) 3. Go to http://localhost:3000/ and get "Welcome aboard: You're riding the Rails!" 4. Follow the guidelines to start developing your application == Web Servers By default, Rails will try to use Mongrel if it's are installed when started with script/server, otherwise Rails will use WEBrick, the webserver that ships with Ruby. But you can also use Rails with a variety of other web servers. Mongrel is a Ruby-based webserver with a C component (which requires compilation) that is suitable for development and deployment of Rails applications. If you have Ruby Gems installed, getting up and running with mongrel is as easy as: gem install mongrel. More info at: http://mongrel.rubyforge.org Say other Ruby web servers like Thin and Ebb or regular web servers like Apache or LiteSpeed or Lighttpd or IIS. The Ruby web servers are run through Rack and the latter can either be setup to use FCGI or proxy to a pack of Mongrels/Thin/Ebb servers. == Apache .htaccess example for FCGI/CGI # General Apache options AddHandler fastcgi-script .fcgi AddHandler cgi-script .cgi Options +FollowSymLinks +ExecCGI # If you don't want Rails to look in certain directories, # use the following rewrite rules so that Apache won't rewrite certain requests # # Example: # RewriteCond %{REQUEST_URI} ^/notrails.* # RewriteRule .* - [L] # Redirect all requests not available on the filesystem to Rails # By default the cgi dispatcher is used which is very slow # # For better performance replace the dispatcher with the fastcgi one # # Example: # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] RewriteEngine On # If your Rails application is accessed via an Alias directive, # then you MUST also set the RewriteBase in this htaccess file. # # Example: # Alias /myrailsapp /path/to/myrailsapp/public # RewriteBase /myrailsapp RewriteRule ^$ index.html [QSA] RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ dispatch.cgi [QSA,L] # In case Rails experiences terminal errors # Instead of displaying this message you can supply a file here which will be rendered instead # # Example: # ErrorDocument 500 /500.html ErrorDocument 500 "

Application error

Rails application failed to start properly" == Debugging Rails Sometimes your application goes wrong. Fortunately there are a lot of tools that will help you debug it and get it back on the rails. First area to check is the application log files. Have "tail -f" commands running on the server.log and development.log. Rails will automatically display debugging and runtime information to these files. Debugging info will also be shown in the browser on requests from 127.0.0.1. You can also log your own messages directly into the log file from your code using the Ruby logger class from inside your controllers. Example: class WeblogController < ActionController::Base def destroy @weblog = Weblog.find(params[:id]) @weblog.destroy logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") end end The result will be a message in your log file along the lines of: Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1 More information on how to use the logger is at http://www.ruby-doc.org/core/ Also, Ruby documentation can be found at http://www.ruby-lang.org/ including: * The Learning Ruby (Pickaxe) Book: http://www.ruby-doc.org/docs/ProgrammingRuby/ * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) These two online (and free) books will bring you up to speed on the Ruby language and also on programming in general. == Debugger Debugger support is available through the debugger command when you start your Mongrel or Webrick server with --debugger. This means that you can break out of execution at any point in the code, investigate and change the model, AND then resume execution! You need to install ruby-debug to run the server in debugging mode. With gems, use 'gem install ruby-debug' Example: class WeblogController < ActionController::Base def index @posts = Post.find(:all) debugger end end So the controller will accept the action, run the first line, then present you with a IRB prompt in the server window. Here you can do things like: >> @posts.inspect => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>, #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]" >> @posts.first.title = "hello from a debugger" => "hello from a debugger" ...and even better is that you can examine how your runtime objects actually work: >> f = @posts.first => #nil, "body"=>nil, "id"=>"1"}> >> f. Display all 152 possibilities? (y or n) Finally, when you're ready to resume execution, you enter "cont" == Console You can interact with the domain model by starting the console through script/console. Here you'll have all parts of the application configured, just like it is when the application is running. You can inspect domain models, change values, and save to the database. Starting the script without arguments will launch it in the development environment. Passing an argument will specify a different environment, like script/console production. To reload your controllers and models after launching the console run reload! == dbconsole You can go to the command line of your database directly through script/dbconsole. You would be connected to the database with the credentials defined in database.yml. Starting the script without arguments will connect you to the development database. Passing an argument will connect you to a different database, like script/dbconsole production. Currently works for mysql, postgresql and sqlite. == Description of Contents app Holds all the code that's specific to this particular application. app/controllers Holds controllers that should be named like weblogs_controller.rb for automated URL mapping. All controllers should descend from ApplicationController which itself descends from ActionController::Base. app/models Holds models that should be named like post.rb. Most models will descend from ActiveRecord::Base. app/views Holds the template files for the view that should be named like weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby syntax. app/views/layouts Holds the template files for layouts to be used with views. This models the common header/footer method of wrapping views. In your views, define a layout using the layout :default and create a file named default.html.erb. Inside default.html.erb, call <% yield %> to render the view using this layout. app/helpers Holds view helpers that should be named like weblogs_helper.rb. These are generated for you automatically when using script/generate for controllers. Helpers can be used to wrap functionality for your views into methods. config Configuration files for the Rails environment, the routing map, the database, and other dependencies. db Contains the database schema in schema.rb. db/migrate contains all the sequence of Migrations for your schema. doc This directory is where your application documentation will be stored when generated using rake doc:app lib Application specific libraries. Basically, any kind of custom code that doesn't belong under controllers, models, or helpers. This directory is in the load path. public The directory available for the web server. Contains subdirectories for images, stylesheets, and javascripts. Also contains the dispatchers and the default HTML files. This should be set as the DOCUMENT_ROOT of your web server. script Helper scripts for automation and generation. test Unit and functional tests along with fixtures. When using the script/generate scripts, template test files will be generated for you and placed in this directory. vendor External libraries that the application depends on. Also includes the plugins subdirectory. If the app has frozen rails, those gems also go here, under vendor/rails/. This directory is in the load path. ================================================ FILE: test/apps/rails2/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require(File.join(File.dirname(__FILE__), 'config', 'boot')) require 'rake' require 'rake/testtask' require 'rake/rdoctask' require 'tasks/rails' ================================================ FILE: test/apps/rails2/app/controllers/application_controller.rb ================================================ # Filters added to this controller apply to all controllers in the application. # Likewise, all the methods added will be available for all controllers. class ApplicationController < ActionController::Base helper :all # include all helpers, all the time #protect_from_forgery # See ActionController::RequestForgeryProtection for details # Scrub sensitive parameters from your log # filter_parameter_logging :password before_filter :awesome def funky_panda end def awesome something = if params[:thang] params[:thang] elsif somevar = "monkeypanda" somevar = somevar.split(",").map { |s| s += 'stuff' unless s =~ /regex/ s.split('things') }.first somevar.first.downcase end if (some_var = SomeClass.things, something) AnotherClass.thang = @thang = some_var.to_sym elsif (some_var = find_thang(AppConfig.stuff, something)) AnotherClass.thang = @thang = some_var.to_sym end if beta_override && cookies['yummy'] != @thang.to_s cookies['yummy'] = { :value => @thang.to_s } end return true end def decent p = params if params[:thang] && self.respond_to?(params[:thang].to_sym) :"really_#{params[:thang]}" end p.symbolize_keys[:custom] end end ================================================ FILE: test/apps/rails2/app/controllers/emails_controller.rb ================================================ class EmailsController < ApplicationController def show @email = Email.find params[:email_id] end def show_email_1 @email = Email.find 1 end end ================================================ FILE: test/apps/rails2/app/controllers/home_controller.rb ================================================ class HomeController < ApplicationController before_filter :filter_it, :only => :test_filter before_filter :or_equals, :only => :test_mass_assign_with_or_equals def index; end def test_params @name = params[:name] @indirect = indirect_method(params[:input]) end def test_model @name = User.first.name end def test_cookie @name = cookies[:name] end def test_filter end def test_file_access File.open RAILS_ROOT + "/" + params[:file] end def test_sql some_var = "hello" User.find_by_sql "select * from users where something = '#{some_var}'" User.all(:conditions => "status => '#{happy}'") @user = User.first(:conditions => "name = '#{params[:name]}'") end def test_command `ls #{params[:file_name]}` system params[:user_input] end def test_eval eval params[:dangerous_input] end def test_redirect params[:action] = :index redirect_to params end def test_render @some_variable = params[:unsafe_input] render :index end def test_mass_assignment User.new(params[:user]) end def test_dynamic_render page = params[:page] render :file => "/some/path/#{page}" end def test_load_params load params[:file] RandomClass.load params[:file] end def test_redirect_with_url_for url = url_for(params) redirect_to url end def test_sql_nested User.humans.alive.find(:all, :conditions => "age > #{params[:age]}") end def test_another_dynamic_render render :action => params[:my_action] end # not safe def test_send_first_param method = params["method"] @result = User.send(method.to_sym) do_something_with @result # don't warn on this line end def test_send_target # not that safe table = params["table"] model = table.classify.constantize @result = model.send(:method) end # safe def test_send_second_param args = params["args"] || [] @result = User.send(:method, *args) end # safe def test_send_second_param method = params["method"] == 1 ? :method_a : :method_b @result = User.send(method, *args) end # safe def test_send_second_param target = params["target"] == 1 ? Account : User @result = target.send(:method, *args) end def test_sanitized_param params["something"] = h(params["something"]) end def test_safe_find_by User.find_or_create_by_name(params[:name], :code => (params[:x] + "code")) end def test_user_input_on_multiline User.find_by_sql "select * from users where something = 'something safe' AND " + "something_not_safe = #{params[:unsafe]} AND " + "something_else_that_is_safe = 'something else safe'" SQL end def test_mass_assign_with_or_equals User.new(params[:still_bad]) end def test_xss_with_or @params_or_something = params[:x] || something if some_condition @user_input = true else @user_input = params[:y] end @more_user_input = x || params[:z] || z @user = User.find(current_user) end def test_to_json @model_json = User.find(current_user).to_json @not_json = {:thing => params[:thing]} @json = {:json_thing => params[:json_thing]}.to_json end def test_content_tag @user = User.find(current_user) end def test_more_send_methods User.try(params[:meth]) self.__send__(params[:meth]) Account.public_send(params[:meth]) table = params["table"] table.classify.constantize.try(:meth) end private def filter_it @filtered = params[:evil_input] end def or_equals params[:still_bad] ||= {} end def test_safe_model_redirect redirect_to User.find(1) end def test_safe_mode_array_redirect redirect_to [User.find(1), User.find(2)] end def test_model_attributes_badness redirect_to User.new.donkey end end ================================================ FILE: test/apps/rails2/app/controllers/other_controller.rb ================================================ class OtherController < ApplicationController def test_locals render :locals => { :input => params[:user_input] } end def test_object render :partial => "account", :object => Account.first end def test_collection users = User.all partial = "user" render :partial => partial, :collection => users end def test_iteration @users = User.all end def test_send_file send_file params[:file] end def test_update_attribute @user = User.first @user.update_attribute(:attr, params[:attr]) end def test_render_template @something_bad = params[:bad] render :template => 'home/test_render_template' end def test_render_update render :update do |page| do_something end end def test_to_i @x = params[:x].to_i @id = cookies[:id].to_i end def test_to_sym :"#{hello!}" x = params[:x].to_sym #Checking that the code below does not warn about to_sym again call_something_with x x.cool_thing? end def test_xss_duplicates1 @thing = params[:thing] render :xss_dupes, :layout => 'thing' end def test_xss_duplicates2 @thing = blah(params[:other_thing]) render :xss_dupes, :layout => 'thing' end def test_haml_stuff render :locals => { :user => User.first } end def test_regex_dos /#{params[:regex]}/ end def test_escaped_regex /#{Regexp.escape(params[:regex])}/ end def test_unescaped_regex /#{Rack::Utils.escape_html(params[:regex])}/ end def test_intern x = params[:x].intern params.symbolize_keys! #Checking that the code below does not warn about to_sym again call_something_with x x.cool_thing? end end ================================================ FILE: test/apps/rails2/app/helpers/application_helper.rb ================================================ # Methods added to this helper will be available to all templates in the application. module ApplicationHelper end ================================================ FILE: test/apps/rails2/app/helpers/home_helper.rb ================================================ module HomeHelper end ================================================ FILE: test/apps/rails2/app/helpers/other_helper.rb ================================================ module OtherHelper end ================================================ FILE: test/apps/rails2/app/models/account.rb ================================================ class Account < ActiveRecord::Base validates_format_of :name, :with => /^[a-zA-Z]+$/ named_scope :all end ================================================ FILE: test/apps/rails2/app/models/email.rb ================================================ class Email < ActiveRecord::Base attr_accessible :email belongs_to :user end ================================================ FILE: test/apps/rails2/app/models/protected.rb ================================================ class Protected < ActiveRecord::Base attr_accessible nil end ================================================ FILE: test/apps/rails2/app/models/unprotected.rb ================================================ class Unprotected < Protected serialize :unsafe_stuff end ================================================ FILE: test/apps/rails2/app/models/user.rb ================================================ class User < ActiveRecord::Base named_scope :dah, lambda {|*args| { :conditions => "dah = '#{args[1]}'"}} named_scope :phooey, :conditions => "phoeey = '#{User.phooey}'" named_scope :with_state, lambda {|state| state.present? ? {:conditions => "state_name = '#{state}'"} : {}} named_scope :safe_phooey, :conditions => ["phoeey = ?", "#{User.phooey}"] named_scope :safe_dah, lambda {|*args| { :conditions => ["dah = ?", "#{args[1]}"]}} named_scope :with_state, lambda {|state| state.present? ? {:conditions => ["state_name = ?", "#{state}"]} : {}} def get_something x self.find(:all, :conditions => "where blah = #{x}") end def test_merge_conditions #Should not warn User.find(:all, :conditions => merge_conditions(some_conditions)) User.find(:all, :conditions => self.merge_conditions(some_conditions)) find(:all, :conditions => User.merge_conditions(some_conditions)) end def self.some_method(value) results = ActiveRecord::Base.connection.execute(%Q(SELECT id FROM table t WHERE t.something = '#{value}')) end def self.test_sanitized_sql input self.connection.select_all("select * from cool_table where stuff = " + self.sanitize_sql(input)) end def more_sanitized_sql self.connection.execute("DELETE FROM cool_table WHERE cool_id=" + quote_value(self.cool_id) + " AND my_id=" + quote_value(self.id)) end has_many :emails end ================================================ FILE: test/apps/rails2/app/views/home/_models.html.erb ================================================ <%= model.id %> ================================================ FILE: test/apps/rails2/app/views/home/index.html.erb ================================================

Home#index

Find me in app/views/home/index.html.erb

<%= params[:user_input] %> <%= @some_variable %> <%= escape_once params[:some_cookie] %> <%= x = []; x << 1 %> ================================================ FILE: test/apps/rails2/app/views/home/test_command.html.erb ================================================

Home#test_command

Find me in app/views/home/test_command.html.erb

================================================ FILE: test/apps/rails2/app/views/home/test_content_tag.html.erb ================================================ Should not warn <%= content_tag :p, h(params[:something]) %> Should warn <%= content_tag :span, @user.name %> Should not warn <%= content_tag :div, "Blah!", { :class => params[:class] }, true %> Should warn <%= content_tag :div, "Blah!", { cookies[:weird] => "bad idea" } %> Should not warn <%= content_tag :h1, params[:x] == 1 ? "totally" : "safe" %> Should still warn <%= content_tag :div, "Blah!", { @user.something => "bad idea"}, true %> Should not warn <%= content_tag :div, "Blah!", { :class => params[:class] } %> Should warn <%= content_tag :div, "Blah!", { :id => @user.name }, false %> Should warn, medium confidence <%= content_tag :div, x(params[:maybe_bad]) %> Should not warn <%= content_tag :span, u(params[:url]) %> ================================================ FILE: test/apps/rails2/app/views/home/test_cookie.html.erb ================================================

Home#test_cookie

Find me in app/views/home/test_cookie.html.erb

Hello, cookie named <%= @name %>! <%= indirect cookies[:oreo] %> And: <%= cookies[:user][:name] %> ================================================ FILE: test/apps/rails2/app/views/home/test_dynamic_render.html.erb ================================================

Home#test_dynamic_render

Find me in app/views/home/test_dynamic_render.html.erb

This is not a problem, because this page is not rendered: <%= @page %> ================================================ FILE: test/apps/rails2/app/views/home/test_eval.html.erb ================================================

Home#test_eval

Find me in app/views/home/test_eval.html.erb

================================================ FILE: test/apps/rails2/app/views/home/test_filter.html.erb ================================================

Home#test_filter

Find me in app/views/home/test_filter.html.erb

Value from filter: <%= @filtered %> ================================================ FILE: test/apps/rails2/app/views/home/test_link_to.html.erb ================================================ <%= link_to :action => "should_not_warn", :q => params[:q] %> <% link_to(params[:evil_url]) do %> Something! <% end %> <%= link_to params[:evil], "http://brakemanscanner.org" %> <%= link_to make_awesome(User.find(1).name), "http://google.com" %> ================================================ FILE: test/apps/rails2/app/views/home/test_mass_assignment.html.erb ================================================

Home#test_mass_assignment

Find me in app/views/home/test_mass_assignment.html.erb

================================================ FILE: test/apps/rails2/app/views/home/test_model.html.erb ================================================

Home#test_model

Find me in app/views/home/test_model.html.erb

Hello, <%= @name %>! Very likely bad: <%= truncate User.profile %> Bad for 2.x: <%= link_to User.first.name, "some_url" %> It's just a model <%= link_to "Hipster ipsum", User.first %> It's just a couple of models <%= link_to "Hipster ipsum", [Account.first, User.last] %> <%= render :partial => 'models', :collection => all_the_things, :as => :model %> Safe link_to herf fun: <%= link_to "test", u(params[:user_id]) %> ================================================ FILE: test/apps/rails2/app/views/home/test_params.html.erb ================================================

Home#test_params

Find me in app/views/home/test_params.html.erb

Jello, <%= @name %> More: <%= @indirect %> And: <%= params[:x][:y] %> Should not warn: <%= link_to h(params[:blah]), "some_url" %> Not-so-dangerous href: <%= link_to "some text", ensure_valid_proto!(params[:not_so_bad], :js) %> Dangerous href: <%= link_to "more text", params[:dangerous] %> Not going to warn: <%= link_to "donkey", not_safe(params[:bad_robot]) %> Not completely safe: <%= link_to "Helvetica hoodie bushwick", h(params[:js_xss]) %> Should not warn <%= u(params[:w00t]) %> Should not warn <%= link_to u(params[:w00t]), "some_url" %> ================================================ FILE: test/apps/rails2/app/views/home/test_redirect.html.erb ================================================

Home#test_redirect

Find me in app/views/home/test_redirect.html.erb

================================================ FILE: test/apps/rails2/app/views/home/test_render.html.erb ================================================ Should not raise a warning: <%= render :partial => (params[:awesome] ? 'awesome' : 'not_awesome') %> Also should not raise a warning: <%= render :partial => User.find(params[:user][:id]) %> Should raise a warning: <%= render :file => "/tmp/#{params[:file]}" %> ================================================ FILE: test/apps/rails2/app/views/home/test_render_template.html.haml ================================================ = @something_bad ================================================ FILE: test/apps/rails2/app/views/home/test_sanitized_param.html.erb ================================================ <%= params["something"] %> <% x = params["something"] %> <%= x %> ================================================ FILE: test/apps/rails2/app/views/home/test_send_target.html.erb ================================================ <%= h @result %> should not warn about send() because it warns in controller where it happens. ================================================ FILE: test/apps/rails2/app/views/home/test_sql.html.erb ================================================

Home#test_sql

Find me in app/views/home/test_sql.html.erb

<%= @user %> ================================================ FILE: test/apps/rails2/app/views/home/test_strip_tags.html.erb ================================================ <%= h strip_tags(params[:name]) %> <%= strip_tags(params[:body]) %> ================================================ FILE: test/apps/rails2/app/views/home/test_to_json.html.erb ================================================ Detection of to_json <%= @model_json %> In the view <%= @not_json.to_json %> In the controller <%= @json %> You would break the json formatting by doing this, but it's technically safe... <%= h(@json) %> ================================================ FILE: test/apps/rails2/app/views/home/test_xss_with_or.html.erb ================================================ <%= params[:x] || nil %> <%= @params_or_something %> <%= @user_input %> <%= @more_user_input %> <%= @user.name || 'nothing dangerous' %> ================================================ FILE: test/apps/rails2/app/views/layouts/thing.html.erb ================================================ <%= @thing %> ================================================ FILE: test/apps/rails2/app/views/other/_account.html.haml ================================================ %p Name: = account.name = account.type ================================================ FILE: test/apps/rails2/app/views/other/_user.html.erb ================================================ Name: <%= user.first_name %> <%= user.last_name %> ================================================ FILE: test/apps/rails2/app/views/other/ignore_me.html.erb ================================================ Going to ignore the warning below <%= User.first(:conditions => "x = #{params[:x]}").bio %> ================================================ FILE: test/apps/rails2/app/views/other/not_used.html.erb ================================================ <%= params[:blah] %> <%= select('post', 'author_id', "") %> <%= sanitize params[:id] %> ================================================ FILE: test/apps/rails2/app/views/other/test_collection.html.erb ================================================

Other#test_collection

Find me in app/views/other/test_collection.html.erb

================================================ FILE: test/apps/rails2/app/views/other/test_env.html.erb ================================================ <%= request.env["HTTP_USER_AGENT"] %> ================================================ FILE: test/apps/rails2/app/views/other/test_haml_stuff.html.haml ================================================ %tr %td= user.age.to_i %td= user.stuff %td= user.status ================================================ FILE: test/apps/rails2/app/views/other/test_iteration.html.erb ================================================ <% @users.each do |user| %> <%= user.name %> <%= user.email %> <% end %> ================================================ FILE: test/apps/rails2/app/views/other/test_locals.html.erb ================================================

Other#test_locals

Find me in app/views/other/test_locals.html.erb

This is user input: <%= input %> ================================================ FILE: test/apps/rails2/app/views/other/test_object.html.erb ================================================

Other#test_object

Find me in app/views/other/test_object.html.erb

================================================ FILE: test/apps/rails2/app/views/other/test_to_i.html.erb ================================================ <%= @x %> <%= request.env[:QUERY_STRING].to_i %> <%= out @id %> <%= User.current.age.to_i %> <%= out Account.current.number.to_i %> ================================================ FILE: test/apps/rails2/app/views/other/test_trim_mode.html.erb ================================================ <%= blah -%> ================================================ FILE: test/apps/rails2/app/views/other/xss_dupes.html.erb ================================================ ================================================ FILE: test/apps/rails2/config/boot.rb ================================================ # Don't change this file! # Configure your app in config/environment.rb and config/environments/*.rb RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) module Rails class << self def boot! unless booted? preinitialize pick_boot.run end end def booted? defined? Rails::Initializer end def pick_boot (vendor_rails? ? VendorBoot : GemBoot).new end def vendor_rails? File.exist?("#{RAILS_ROOT}/vendor/rails") end def preinitialize load(preinitializer_path) if File.exist?(preinitializer_path) end def preinitializer_path "#{RAILS_ROOT}/config/preinitializer.rb" end end class Boot def run load_initializer Rails::Initializer.run(:set_load_path) end end class VendorBoot < Boot def load_initializer require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" Rails::Initializer.run(:install_gem_spec_stubs) Rails::GemDependency.add_frozen_gem_path end end class GemBoot < Boot def load_initializer self.class.load_rubygems load_rails_gem require 'initializer' end def load_rails_gem if version = self.class.gem_version gem 'rails', version else gem 'rails' end rescue Gem::LoadError => load_error if load_error.message =~ /Could not find RubyGem rails/ STDERR.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) exit 1 else raise end end class << self def rubygems_version Gem::RubyGemsVersion rescue nil end def gem_version if defined? RAILS_GEM_VERSION RAILS_GEM_VERSION elsif ENV.include?('RAILS_GEM_VERSION') ENV['RAILS_GEM_VERSION'] else parse_gem_version(read_environment_rb) end end def load_rubygems min_version = '1.3.2' require 'rubygems' unless rubygems_version >= min_version $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) exit 1 end rescue LoadError $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) exit 1 end def parse_gem_version(text) $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ end private def read_environment_rb File.read("#{RAILS_ROOT}/config/environment.rb") end end end end # All that for this: Rails.boot! ================================================ FILE: test/apps/rails2/config/brakeman.ignore ================================================ { "ignored_warnings": [ { "warning_type": "Cross-Site Scripting", "warning_code": 2, "fingerprint": "e53a25ddc8ca61f7c4b81602bae04cd6746cf6a7432e89407fbeb362a66f4e8f", "message": "Unescaped model attribute", "file": "app/views/other/ignore_me.html.erb", "line": 2, "link": "http://brakemanscanner.org/docs/warning_types/cross_site_scripting", "code": "User.first(:conditions => (\"x = #{params[:x]}\")).bio", "render_path": null, "location": { "type": "template", "template": "other/ignore_me" }, "user_input": null, "confidence": "High", "note": "" }, { "warning_type": "SQL Injection", "warning_code": 0, "fingerprint": "5313b025804b1cbf885f2862078391086c6d5cba892f47ed31506cb5b23df24a", "message": "Possible SQL injection", "file": "app/views/other/ignore_me.html.erb", "line": 2, "link": "http://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "User.first(:conditions => (\"x = #{params[:x]}\"))", "render_path": null, "location": { "type": "template", "template": "other/ignore_me" }, "user_input": "params[:x]", "confidence": "High", "note": "" } ], "brakeman_version": "3.0.5" } ================================================ FILE: test/apps/rails2/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3-ruby (not necessary on OS X Leopard) development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails2/config/environment.rb ================================================ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present RAILS_GEM_VERSION = '2.3.11' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') Rails::Initializer.run do |config| # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Add additional load paths for your own custom dirs # config.autoload_paths += %W( #{RAILS_ROOT}/extras ) # Specify gems that this application depends on and have them installed with rake gems:install # config.gem "bj" # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" # config.gem "sqlite3-ruby", :lib => "sqlite3" # config.gem "aws-s3", :lib => "aws/s3" # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Skip frameworks you're not going to use. To use Rails without a database, # you must remove the Active Record framework. # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] # Activate observers that should always be running # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. config.time_zone = 'UTC' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] # config.i18n.default_locale = :de end ================================================ FILE: test/apps/rails2/config/environments/development.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the webserver when you make code changes. config.cache_classes = false # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_view.debug_rjs = true config.action_controller.perform_caching = false # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false ================================================ FILE: test/apps/rails2/config/environments/production.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # The production environment is meant for finished, "live" apps. # Code is not reloaded between requests config.cache_classes = true # Full error reports are disabled and caching is turned on config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true config.action_view.cache_template_loading = true # See everything in the log (default is :info) # config.log_level = :debug # Use a different logger for distributed setups # config.logger = SyslogLogger.new # Use a different cache store in production # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and javascripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false # Enable threaded mode # config.threadsafe! ================================================ FILE: test/apps/rails2/config/environments/test.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false config.action_view.cache_template_loading = true # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Use SQL instead of Active Record's schema dumper when creating the test database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql ================================================ FILE: test/apps/rails2/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying do debug a problem that might steem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails2/config/initializers/cookie_verification_secret.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. ActionController::Base.cookie_verifier_secret = '4cdaca76a832bfacec3a57ef202842febb9c973facbbd472756d14eba6653e9ed5db8532ed4c089b16ed7ce44e8865cf676054fc2a7a8f5f42816894ddb2f3a4'; ================================================ FILE: test/apps/rails2/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end ================================================ FILE: test/apps/rails2/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails2/config/initializers/new_rails_defaults.rb ================================================ # Be sure to restart your server when you modify this file. # These settings change the behavior of Rails 2 apps and will be defaults # for Rails 3. You can remove this initializer when Rails 3 is released. if defined?(ActiveRecord) # Include Active Record class name as root for JSON serialized output. ActiveRecord::Base.include_root_in_json = true # Store the full class name (including module namespace) in STI type column. ActiveRecord::Base.store_full_sti_class = true end ActionController::Routing.generate_best_match = false # Use ISO 8601 format for JSON serialized times and dates. ActiveSupport.use_standard_json_time_format = true # Don't escape HTML entities in JSON, leave that for the #json_escape helper. # if you're including raw json in an HTML page. ActiveSupport.escape_html_entities_in_json = false ================================================ FILE: test/apps/rails2/config/initializers/security_defaults.rb ================================================ #ActiveRecord::Base.send(:attr_accessible, nil) ================================================ FILE: test/apps/rails2/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying cookie session data integrity. # If you change this key, all old sessions will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. ActionController::Base.session = { :key => '_rails2_session', :secret => 'secret!', :session_http_only => false } # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rake db:sessions:create") ActionController::Base.session_store = :active_record_store ================================================ FILE: test/apps/rails2/config/locales/en.yml ================================================ # Sample localization file for English. Add more files in this directory for other locales. # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: hello: "Hello world" ================================================ FILE: test/apps/rails2/config/routes.rb ================================================ ActionController::Routing::Routes.draw do |map| #Default routes are set for everything, #so these routes are testing for specific problems map.with_options :controller => 'other', :action => 'nothing' do |r| r.connect 'blah' end map.connect 'something', :controller => "something#{dynamic}" map.resource :users map.resources :stuff do |r|; end namespace :without_block_arg do;end # The priority is based upon order of creation: first created -> highest priority. # Sample of regular route: # map.connect 'products/:id', :controller => 'catalog', :action => 'view' # Keep in mind you can assign values other than :controller and :action # Sample of named route: # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' # This route can be invoked with purchase_url(:id => product.id) # Sample resource route (maps HTTP verbs to controller actions automatically): # map.resources :products # Sample resource route with options: # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } # Sample resource route with sub-resources: # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller # Sample resource route with more complex sub-resources # map.resources :products do |products| # products.resources :comments # products.resources :sales, :collection => { :recent => :get } # end # Sample resource route within a namespace: # map.namespace :admin do |admin| # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) # admin.resources :products # end # You can have the root of your site routed with map.root -- just remember to delete public/index.html. # map.root :controller => "welcome" # See how all your routes lay out with "rake routes" # Install the default routes as the lowest priority. # Note: These default routes make all actions in every controller accessible via GET requests. You should # consider removing or commenting them out if you're using named routes and resources. map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' map.some_routes map.things "/things", :controller => "home", :action => "index_#{random_dynamic_thing}" end ================================================ FILE: test/apps/rails2/db/migrate/20110520193611_create_users.rb ================================================ class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.timestamps end end def self.down drop_table :users end end ================================================ FILE: test/apps/rails2/db/migrate/20110523184125_create_accounts.rb ================================================ class CreateAccounts < ActiveRecord::Migration def self.up create_table :accounts do |t| t.timestamps end end def self.down drop_table :accounts end end ================================================ FILE: test/apps/rails2/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) # Major.create(:name => 'Daley', :city => cities.first) ================================================ FILE: test/apps/rails2/doc/README_FOR_APP ================================================ Use this README file to introduce your application and point to useful places in the API for learning more. Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. ================================================ FILE: test/apps/rails2/lib/generators/test_generator/templates/model.rb ================================================ class <%= file_name.camelize %> < ActiveRecord::Base end ================================================ FILE: test/apps/rails2/log/development.log ================================================ ================================================ FILE: test/apps/rails2/log/production.log ================================================ ================================================ FILE: test/apps/rails2/log/server.log ================================================ ================================================ FILE: test/apps/rails2/log/test.log ================================================ ================================================ FILE: test/apps/rails2/public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

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

================================================ FILE: test/apps/rails2/public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

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

================================================ FILE: test/apps/rails2/public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

We've been notified about this issue and we'll take a look at it shortly.

================================================ FILE: test/apps/rails2/public/index.html ================================================ Ruby on Rails: Welcome aboard

Getting started

Here’s how to get rolling:

  1. Use script/generate to create your models and controllers

    To see all available options, run it without parameters.

  2. Set up a default route and remove or rename this file

    Routes are set up in config/routes.rb.

  3. Create your database

    Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

================================================ FILE: test/apps/rails2/public/javascripts/application.js ================================================ // Place your application-specific JavaScript functions and classes here // This file is automatically included by javascript_include_tag :defaults ================================================ FILE: test/apps/rails2/public/javascripts/controls.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava // Rob Wills // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This // includes drawing the autocompletion menu, observing keyboard // and mouse events, and similar. // // Specific autocompleters need to provide, at the very least, // a getUpdatedChoices function that will be invoked every time // the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by // invoking this.getToken(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized // autocompletion. Specific auto-completion logic (AJAX, etc) // belongs in getUpdatedChoices. // // Tokenized incremental autocompletion is enabled automatically // when an autocompleter is instantiated with the 'tokens' option // in the options parameter, e.g.: // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with // a token array, e.g. { tokens: [',', '\n'] } which // enables autocompletion on multiple tokens. This is most // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. if(typeof Effect == 'undefined') throw("controls.js requires including script.aculo.us' effects.js library"); var Autocompleter = { }; Autocompleter.Base = Class.create({ baseInitialize: function(element, update, options) { element = $(element); this.element = element; this.update = $(update); this.hasFocus = false; this.changed = false; this.active = false; this.index = 0; this.entryCount = 0; this.oldElementValue = this.element.value; if(this.setOptions) this.setOptions(options); else this.options = options || { }; this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; this.options.minChars = this.options.minChars || 1; this.options.onShow = this.options.onShow || function(element, update){ if(!update.style.position || update.style.position=='absolute') { update.style.position = 'absolute'; Position.clone(element, update, { setHeight: false, offsetTop: element.offsetHeight }); } Effect.Appear(update,{duration:0.15}); }; this.options.onHide = this.options.onHide || function(element, update){ new Effect.Fade(update,{duration:0.15}) }; if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); // Force carriage returns as token delimiters anyway if (!this.options.tokens.include('\n')) this.options.tokens.push('\n'); this.observer = null; this.element.setAttribute('autocomplete','off'); Element.hide(this.update); Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); if(!this.iefix && (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, ''); this.iefix = $(this.update.id+'_iefix'); } if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); }, fixIEOverlapping: function() { Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); this.iefix.style.zIndex = 1; this.update.style.zIndex = 2; Element.show(this.iefix); }, hide: function() { this.stopIndicator(); if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); if(this.iefix) Element.hide(this.iefix); }, startIndicator: function() { if(this.options.indicator) Element.show(this.options.indicator); }, stopIndicator: function() { if(this.options.indicator) Element.hide(this.options.indicator); }, onKeyPress: function(event) { if(this.active) switch(event.keyCode) { case Event.KEY_TAB: case Event.KEY_RETURN: this.selectEntry(); Event.stop(event); case Event.KEY_ESC: this.hide(); this.active = false; Event.stop(event); return; case Event.KEY_LEFT: case Event.KEY_RIGHT: return; case Event.KEY_UP: this.markPrevious(); this.render(); Event.stop(event); return; case Event.KEY_DOWN: this.markNext(); this.render(); Event.stop(event); return; } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; this.changed = true; this.hasFocus = true; if(this.observer) clearTimeout(this.observer); this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, activate: function() { this.changed = false; this.hasFocus = true; this.getUpdatedChoices(); }, onHover: function(event) { var element = Event.findElement(event, 'LI'); if(this.index != element.autocompleteIndex) { this.index = element.autocompleteIndex; this.render(); } Event.stop(event); }, onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; this.selectEntry(); this.hide(); }, onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); this.hasFocus = false; this.active = false; }, render: function() { if(this.entryCount > 0) { for (var i = 0; i < this.entryCount; i++) this.index==i ? Element.addClassName(this.getEntry(i),"selected") : Element.removeClassName(this.getEntry(i),"selected"); if(this.hasFocus) { this.show(); this.active = true; } } else { this.active = false; this.hide(); } }, markPrevious: function() { if(this.index > 0) this.index--; else this.index = this.entryCount-1; this.getEntry(this.index).scrollIntoView(true); }, markNext: function() { if(this.index < this.entryCount-1) this.index++; else this.index = 0; this.getEntry(this.index).scrollIntoView(false); }, getEntry: function(index) { return this.update.firstChild.childNodes[index]; }, getCurrentEntry: function() { return this.getEntry(this.index); }, selectEntry: function() { this.active = false; this.updateElement(this.getCurrentEntry()); }, updateElement: function(selectedElement) { if (this.options.updateElement) { this.options.updateElement(selectedElement); return; } var value = ''; if (this.options.select) { var nodes = $(selectedElement).select('.' + this.options.select) || []; if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); } else value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); var bounds = this.getTokenBounds(); if (bounds[0] != -1) { var newValue = this.element.value.substr(0, bounds[0]); var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); if (whitespace) newValue += whitespace[0]; this.element.value = newValue + value + this.element.value.substr(bounds[1]); } else { this.element.value = value; } this.oldElementValue = this.element.value; this.element.focus(); if (this.options.afterUpdateElement) this.options.afterUpdateElement(this.element, selectedElement); }, updateChoices: function(choices) { if(!this.changed && this.hasFocus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update.down()); if(this.update.firstChild && this.update.down().childNodes) { this.entryCount = this.update.down().childNodes.length; for (var i = 0; i < this.entryCount; i++) { var entry = this.getEntry(i); entry.autocompleteIndex = i; this.addObservers(entry); } } else { this.entryCount = 0; } this.stopIndicator(); this.index = 0; if(this.entryCount==1 && this.options.autoSelect) { this.selectEntry(); this.hide(); } else { this.render(); } } }, addObservers: function(element) { Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); Event.observe(element, "click", this.onClick.bindAsEventListener(this)); }, onObserverEvent: function() { this.changed = false; this.tokenBounds = null; if(this.getToken().length>=this.options.minChars) { this.getUpdatedChoices(); } else { this.active = false; this.hide(); } this.oldElementValue = this.element.value; }, getToken: function() { var bounds = this.getTokenBounds(); return this.element.value.substring(bounds[0], bounds[1]).strip(); }, getTokenBounds: function() { if (null != this.tokenBounds) return this.tokenBounds; var value = this.element.value; if (value.strip().empty()) return [-1, 0]; var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); var offset = (diff == this.oldElementValue.length ? 1 : 0); var prevTokenPos = -1, nextTokenPos = value.length; var tp; for (var index = 0, l = this.options.tokens.length; index < l; ++index) { tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); if (tp > prevTokenPos) prevTokenPos = tp; tp = value.indexOf(this.options.tokens[index], diff + offset); if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; } return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); } }); Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { var boundary = Math.min(newS.length, oldS.length); for (var index = 0; index < boundary; ++index) if (newS[index] != oldS[index]) return index; return boundary; }; Ajax.Autocompleter = Class.create(Autocompleter.Base, { initialize: function(element, update, url, options) { this.baseInitialize(element, update, options); this.options.asynchronous = true; this.options.onComplete = this.onComplete.bind(this); this.options.defaultParams = this.options.parameters || null; this.url = url; }, getUpdatedChoices: function() { this.startIndicator(); var entry = encodeURIComponent(this.options.paramName) + '=' + encodeURIComponent(this.getToken()); this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; new Ajax.Request(this.url, this.options); }, onComplete: function(request) { this.updateChoices(request.responseText); } }); // The local array autocompleter. Used when you'd prefer to // inject an array of autocompletion options into the page, rather // than sending out Ajax queries, which can be quite slow sometimes. // // The constructor takes four parameters. The first two are, as usual, // the id of the monitored textbox, and id of the autocompletion menu. // The third is the array you want to autocomplete from, and the fourth // is the options block. // // Extra local autocompletion options: // - choices - How many autocompletion choices to offer // // - partialSearch - If false, the autocompleter will match entered // text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to // search anywhere in the string, additionally set // the option fullSearch to true (default: off). // // - fullSsearch - Search anywhere in autocomplete array strings. // // - partialChars - How many characters to enter before triggering // a partial match (unlike minChars, which defines // how many characters are required to do any match // at all). Defaults to 2. // // - ignoreCase - Whether to ignore case when autocompleting. // Defaults to true. // // It's possible to pass in a custom function as the 'selector' // option, if you prefer to write your own autocompletion logic. // In that case, the other options above will not apply unless // you support them. Autocompleter.Local = Class.create(Autocompleter.Base, { initialize: function(element, update, array, options) { this.baseInitialize(element, update, options); this.options.array = array; }, getUpdatedChoices: function() { this.updateChoices(this.options.selector(this)); }, setOptions: function(options) { this.options = Object.extend({ choices: 10, partialSearch: true, partialChars: 2, ignoreCase: true, fullSearch: false, selector: function(instance) { var ret = []; // Beginning matches var partial = []; // Inside matches var entry = instance.getToken(); var count = 0; for (var i = 0; i < instance.options.array.length && ret.length < instance.options.choices ; i++) { var elem = instance.options.array[i]; var foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); while (foundPos != -1) { if (foundPos == 0 && elem.length != entry.length) { ret.push("
  • " + elem.substr(0, entry.length) + "" + elem.substr(entry.length) + "
  • "); break; } else if (entry.length >= instance.options.partialChars && instance.options.partialSearch && foundPos != -1) { if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { partial.push("
  • " + elem.substr(0, foundPos) + "" + elem.substr(foundPos, entry.length) + "" + elem.substr( foundPos + entry.length) + "
  • "); break; } } foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : elem.indexOf(entry, foundPos + 1); } } if (partial.length) ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); return "
      " + ret.join('') + "
    "; } }, options || { }); } }); // AJAX in-place editor and collection editor // Full rewrite by Christophe Porteneuve (April 2007). // Use this if you notice weird scrolling problems on some browsers, // the DOM might be a bit confused when this gets called so do this // waits 1 ms (with setTimeout) until it does the activation Field.scrollFreeActivate = function(field) { setTimeout(function() { Field.activate(field); }, 1); }; Ajax.InPlaceEditor = Class.create({ initialize: function(element, url, options) { this.url = url; this.element = element = $(element); this.prepareOptions(); this._controls = { }; arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! Object.extend(this.options, options || { }); if (!this.options.formId && this.element.id) { this.options.formId = this.element.id + '-inplaceeditor'; if ($(this.options.formId)) this.options.formId = ''; } if (this.options.externalControl) this.options.externalControl = $(this.options.externalControl); if (!this.options.externalControl) this.options.externalControlOnly = false; this._originalBackground = this.element.getStyle('background-color') || 'transparent'; this.element.title = this.options.clickToEditText; this._boundCancelHandler = this.handleFormCancellation.bind(this); this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); this._boundFailureHandler = this.handleAJAXFailure.bind(this); this._boundSubmitHandler = this.handleFormSubmission.bind(this); this._boundWrapperHandler = this.wrapUp.bind(this); this.registerListeners(); }, checkForEscapeOrReturn: function(e) { if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; if (Event.KEY_ESC == e.keyCode) this.handleFormCancellation(e); else if (Event.KEY_RETURN == e.keyCode) this.handleFormSubmission(e); }, createControl: function(mode, handler, extraClasses) { var control = this.options[mode + 'Control']; var text = this.options[mode + 'Text']; if ('button' == control) { var btn = document.createElement('input'); btn.type = 'submit'; btn.value = text; btn.className = 'editor_' + mode + '_button'; if ('cancel' == mode) btn.onclick = this._boundCancelHandler; this._form.appendChild(btn); this._controls[mode] = btn; } else if ('link' == control) { var link = document.createElement('a'); link.href = '#'; link.appendChild(document.createTextNode(text)); link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; link.className = 'editor_' + mode + '_link'; if (extraClasses) link.className += ' ' + extraClasses; this._form.appendChild(link); this._controls[mode] = link; } }, createEditField: function() { var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); var fld; if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { fld = document.createElement('input'); fld.type = 'text'; var size = this.options.size || this.options.cols || 0; if (0 < size) fld.size = size; } else { fld = document.createElement('textarea'); fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); fld.cols = this.options.cols || 40; } fld.name = this.options.paramName; fld.value = text; // No HTML breaks conversion anymore fld.className = 'editor_field'; if (this.options.submitOnBlur) fld.onblur = this._boundSubmitHandler; this._controls.editor = fld; if (this.options.loadTextURL) this.loadExternalText(); this._form.appendChild(this._controls.editor); }, createForm: function() { var ipe = this; function addText(mode, condition) { var text = ipe.options['text' + mode + 'Controls']; if (!text || condition === false) return; ipe._form.appendChild(document.createTextNode(text)); }; this._form = $(document.createElement('form')); this._form.id = this.options.formId; this._form.addClassName(this.options.formClassName); this._form.onsubmit = this._boundSubmitHandler; this.createEditField(); if ('textarea' == this._controls.editor.tagName.toLowerCase()) this._form.appendChild(document.createElement('br')); if (this.options.onFormCustomization) this.options.onFormCustomization(this, this._form); addText('Before', this.options.okControl || this.options.cancelControl); this.createControl('ok', this._boundSubmitHandler); addText('Between', this.options.okControl && this.options.cancelControl); this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); addText('After', this.options.okControl || this.options.cancelControl); }, destroy: function() { if (this._oldInnerHTML) this.element.innerHTML = this._oldInnerHTML; this.leaveEditMode(); this.unregisterListeners(); }, enterEditMode: function(e) { if (this._saving || this._editing) return; this._editing = true; this.triggerCallback('onEnterEditMode'); if (this.options.externalControl) this.options.externalControl.hide(); this.element.hide(); this.createForm(); this.element.parentNode.insertBefore(this._form, this.element); if (!this.options.loadTextURL) this.postProcessEditField(); if (e) Event.stop(e); }, enterHover: function(e) { if (this.options.hoverClassName) this.element.addClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onEnterHover'); }, getText: function() { return this.element.innerHTML.unescapeHTML(); }, handleAJAXFailure: function(transport) { this.triggerCallback('onFailure', transport); if (this._oldInnerHTML) { this.element.innerHTML = this._oldInnerHTML; this._oldInnerHTML = null; } }, handleFormCancellation: function(e) { this.wrapUp(); if (e) Event.stop(e); }, handleFormSubmission: function(e) { var form = this._form; var value = $F(this._controls.editor); this.prepareSubmission(); var params = this.options.callback(form, value) || ''; if (Object.isString(params)) params = params.toQueryParams(); params.editorId = this.element.id; if (this.options.htmlResponse) { var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Updater({ success: this.element }, this.url, options); } else { var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Request(this.url, options); } if (e) Event.stop(e); }, leaveEditMode: function() { this.element.removeClassName(this.options.savingClassName); this.removeForm(); this.leaveHover(); this.element.style.backgroundColor = this._originalBackground; this.element.show(); if (this.options.externalControl) this.options.externalControl.show(); this._saving = false; this._editing = false; this._oldInnerHTML = null; this.triggerCallback('onLeaveEditMode'); }, leaveHover: function(e) { if (this.options.hoverClassName) this.element.removeClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onLeaveHover'); }, loadExternalText: function() { this._form.addClassName(this.options.loadingClassName); this._controls.editor.disabled = true; var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._form.removeClassName(this.options.loadingClassName); var text = transport.responseText; if (this.options.stripLoadedTextTags) text = text.stripTags(); this._controls.editor.value = text; this._controls.editor.disabled = false; this.postProcessEditField(); }.bind(this), onFailure: this._boundFailureHandler }); new Ajax.Request(this.options.loadTextURL, options); }, postProcessEditField: function() { var fpc = this.options.fieldPostCreation; if (fpc) $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); }, prepareOptions: function() { this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); [this._extraDefaultOptions].flatten().compact().each(function(defs) { Object.extend(this.options, defs); }.bind(this)); }, prepareSubmission: function() { this._saving = true; this.removeForm(); this.leaveHover(); this.showSaving(); }, registerListeners: function() { this._listeners = { }; var listener; $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { listener = this[pair.value].bind(this); this._listeners[pair.key] = listener; if (!this.options.externalControlOnly) this.element.observe(pair.key, listener); if (this.options.externalControl) this.options.externalControl.observe(pair.key, listener); }.bind(this)); }, removeForm: function() { if (!this._form) return; this._form.remove(); this._form = null; this._controls = { }; }, showSaving: function() { this._oldInnerHTML = this.element.innerHTML; this.element.innerHTML = this.options.savingText; this.element.addClassName(this.options.savingClassName); this.element.style.backgroundColor = this._originalBackground; this.element.show(); }, triggerCallback: function(cbName, arg) { if ('function' == typeof this.options[cbName]) { this.options[cbName](this, arg); } }, unregisterListeners: function() { $H(this._listeners).each(function(pair) { if (!this.options.externalControlOnly) this.element.stopObserving(pair.key, pair.value); if (this.options.externalControl) this.options.externalControl.stopObserving(pair.key, pair.value); }.bind(this)); }, wrapUp: function(transport) { this.leaveEditMode(); // Can't use triggerCallback due to backward compatibility: requires // binding + direct element this._boundComplete(transport, this.element); } }); Object.extend(Ajax.InPlaceEditor.prototype, { dispose: Ajax.InPlaceEditor.prototype.destroy }); Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { initialize: function($super, element, url, options) { this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; $super(element, url, options); }, createEditField: function() { var list = document.createElement('select'); list.name = this.options.paramName; list.size = 1; this._controls.editor = list; this._collection = this.options.collection || []; if (this.options.loadCollectionURL) this.loadCollection(); else this.checkForExternalText(); this._form.appendChild(this._controls.editor); }, loadCollection: function() { this._form.addClassName(this.options.loadingClassName); this.showLoadingText(this.options.loadingCollectionText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { var js = transport.responseText.strip(); if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check throw('Server returned an invalid collection representation.'); this._collection = eval(js); this.checkForExternalText(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadCollectionURL, options); }, showLoadingText: function(text) { this._controls.editor.disabled = true; var tempOption = this._controls.editor.firstChild; if (!tempOption) { tempOption = document.createElement('option'); tempOption.value = ''; this._controls.editor.appendChild(tempOption); tempOption.selected = true; } tempOption.update((text || '').stripScripts().stripTags()); }, checkForExternalText: function() { this._text = this.getText(); if (this.options.loadTextURL) this.loadExternalText(); else this.buildOptionList(); }, loadExternalText: function() { this.showLoadingText(this.options.loadingText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._text = transport.responseText.strip(); this.buildOptionList(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadTextURL, options); }, buildOptionList: function() { this._form.removeClassName(this.options.loadingClassName); this._collection = this._collection.map(function(entry) { return 2 === entry.length ? entry : [entry, entry].flatten(); }); var marker = ('value' in this.options) ? this.options.value : this._text; var textFound = this._collection.any(function(entry) { return entry[0] == marker; }.bind(this)); this._controls.editor.update(''); var option; this._collection.each(function(entry, index) { option = document.createElement('option'); option.value = entry[0]; option.selected = textFound ? entry[0] == marker : 0 == index; option.appendChild(document.createTextNode(entry[1])); this._controls.editor.appendChild(option); }.bind(this)); this._controls.editor.disabled = false; Field.scrollFreeActivate(this._controls.editor); } }); //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** //**** This only exists for a while, in order to let **** //**** users adapt to the new API. Read up on the new **** //**** API and convert your code to it ASAP! **** Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { if (!options) return; function fallback(name, expr) { if (name in options || expr === undefined) return; options[name] = expr; }; fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : options.cancelLink == options.cancelButton == false ? false : undefined))); fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : options.okLink == options.okButton == false ? false : undefined))); fallback('highlightColor', options.highlightcolor); fallback('highlightEndColor', options.highlightendcolor); }; Object.extend(Ajax.InPlaceEditor, { DefaultOptions: { ajaxOptions: { }, autoRows: 3, // Use when multi-line w/ rows == 1 cancelControl: 'link', // 'link'|'button'|false cancelText: 'cancel', clickToEditText: 'Click to edit', externalControl: null, // id|elt externalControlOnly: false, fieldPostCreation: 'activate', // 'activate'|'focus'|false formClassName: 'inplaceeditor-form', formId: null, // id|elt highlightColor: '#ffff99', highlightEndColor: '#ffffff', hoverClassName: '', htmlResponse: true, loadingClassName: 'inplaceeditor-loading', loadingText: 'Loading...', okControl: 'button', // 'link'|'button'|false okText: 'ok', paramName: 'value', rows: 1, // If 1 and multi-line, uses autoRows savingClassName: 'inplaceeditor-saving', savingText: 'Saving...', size: 0, stripLoadedTextTags: false, submitOnBlur: false, textAfterControls: '', textBeforeControls: '', textBetweenControls: '' }, DefaultCallbacks: { callback: function(form) { return Form.serialize(form); }, onComplete: function(transport, element) { // For backward compatibility, this one is bound to the IPE, and passes // the element directly. It was too often customized, so we don't break it. new Effect.Highlight(element, { startcolor: this.options.highlightColor, keepBackgroundImage: true }); }, onEnterEditMode: null, onEnterHover: function(ipe) { ipe.element.style.backgroundColor = ipe.options.highlightColor; if (ipe._effect) ipe._effect.cancel(); }, onFailure: function(transport, ipe) { alert('Error communication with the server: ' + transport.responseText.stripTags()); }, onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. onLeaveEditMode: null, onLeaveHover: function(ipe) { ipe._effect = new Effect.Highlight(ipe.element, { startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, restorecolor: ipe._originalBackground, keepBackgroundImage: true }); } }, Listeners: { click: 'enterEditMode', keydown: 'checkForEscapeOrReturn', mouseover: 'enterHover', mouseout: 'leaveHover' } }); Ajax.InPlaceCollectionEditor.DefaultOptions = { loadingCollectionText: 'Loading options...' }; // Delayed observer, like Form.Element.Observer, // but waits for delay after last key input // Ideal for live-search fields Form.Element.DelayedObserver = Class.create({ initialize: function(element, delay, callback) { this.delay = delay || 0.5; this.element = $(element); this.callback = callback; this.timer = null; this.lastValue = $F(this.element); Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); }, delayedListener: function(event) { if(this.lastValue == $F(this.element)) return; if(this.timer) clearTimeout(this.timer); this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); this.lastValue = $F(this.element); }, onTimerEvent: function() { this.timer = null; this.callback(this.element, $F(this.element)); } }); ================================================ FILE: test/apps/rails2/public/javascripts/dragdrop.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ if(Object.isUndefined(Effect)) throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { drops: [], remove: function(element) { this.drops = this.drops.reject(function(d) { return d.element==$(element) }); }, add: function(element) { element = $(element); var options = Object.extend({ greedy: true, hoverclass: null, tree: false }, arguments[1] || { }); // cache containers if(options.containment) { options._containers = []; var containment = options.containment; if(Object.isArray(containment)) { containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); } } if(options.accept) options.accept = [options.accept].flatten(); Element.makePositioned(element); // fix IE options.element = element; this.drops.push(options); }, findDeepestChild: function(drops) { deepest = drops[0]; for (i = 1; i < drops.length; ++i) if (Element.isParent(drops[i].element, deepest.element)) deepest = drops[i]; return deepest; }, isContained: function(element, drop) { var containmentNode; if(drop.tree) { containmentNode = element.treeNode; } else { containmentNode = element.parentNode; } return drop._containers.detect(function(c) { return containmentNode == c }); }, isAffected: function(point, element, drop) { return ( (drop.element!=element) && ((!drop._containers) || this.isContained(element, drop)) && ((!drop.accept) || (Element.classNames(element).detect( function(v) { return drop.accept.include(v) } ) )) && Position.within(drop.element, point[0], point[1]) ); }, deactivate: function(drop) { if(drop.hoverclass) Element.removeClassName(drop.element, drop.hoverclass); this.last_active = null; }, activate: function(drop) { if(drop.hoverclass) Element.addClassName(drop.element, drop.hoverclass); this.last_active = drop; }, show: function(point, element) { if(!this.drops.length) return; var drop, affected = []; this.drops.each( function(drop) { if(Droppables.isAffected(point, element, drop)) affected.push(drop); }); if(affected.length>0) drop = Droppables.findDeepestChild(affected); if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); if (drop) { Position.within(drop.element, point[0], point[1]); if(drop.onHover) drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); if (drop != this.last_active) Droppables.activate(drop); } }, fire: function(event, element) { if(!this.last_active) return; Position.prepare(); if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) if (this.last_active.onDrop) { this.last_active.onDrop(element, this.last_active.element, event); return true; } }, reset: function() { if(this.last_active) this.deactivate(this.last_active); } }; var Draggables = { drags: [], observers: [], register: function(draggable) { if(this.drags.length == 0) { this.eventMouseUp = this.endDrag.bindAsEventListener(this); this.eventMouseMove = this.updateDrag.bindAsEventListener(this); this.eventKeypress = this.keyPress.bindAsEventListener(this); Event.observe(document, "mouseup", this.eventMouseUp); Event.observe(document, "mousemove", this.eventMouseMove); Event.observe(document, "keypress", this.eventKeypress); } this.drags.push(draggable); }, unregister: function(draggable) { this.drags = this.drags.reject(function(d) { return d==draggable }); if(this.drags.length == 0) { Event.stopObserving(document, "mouseup", this.eventMouseUp); Event.stopObserving(document, "mousemove", this.eventMouseMove); Event.stopObserving(document, "keypress", this.eventKeypress); } }, activate: function(draggable) { if(draggable.options.delay) { this._timeout = setTimeout(function() { Draggables._timeout = null; window.focus(); Draggables.activeDraggable = draggable; }.bind(this), draggable.options.delay); } else { window.focus(); // allows keypress events if window isn't currently focused, fails for Safari this.activeDraggable = draggable; } }, deactivate: function() { this.activeDraggable = null; }, updateDrag: function(event) { if(!this.activeDraggable) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; // Mozilla-based browsers fire successive mousemove events with // the same coordinates, prevent needless redrawing (moz bug?) if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; this._lastPointer = pointer; this.activeDraggable.updateDrag(event, pointer); }, endDrag: function(event) { if(this._timeout) { clearTimeout(this._timeout); this._timeout = null; } if(!this.activeDraggable) return; this._lastPointer = null; this.activeDraggable.endDrag(event); this.activeDraggable = null; }, keyPress: function(event) { if(this.activeDraggable) this.activeDraggable.keyPress(event); }, addObserver: function(observer) { this.observers.push(observer); this._cacheObserverCallbacks(); }, removeObserver: function(element) { // element instead of observer fixes mem leaks this.observers = this.observers.reject( function(o) { return o.element==element }); this._cacheObserverCallbacks(); }, notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' if(this[eventName+'Count'] > 0) this.observers.each( function(o) { if(o[eventName]) o[eventName](eventName, draggable, event); }); if(draggable.options[eventName]) draggable.options[eventName](draggable, event); }, _cacheObserverCallbacks: function() { ['onStart','onEnd','onDrag'].each( function(eventName) { Draggables[eventName+'Count'] = Draggables.observers.select( function(o) { return o[eventName]; } ).length; }); } }; /*--------------------------------------------------------------------------*/ var Draggable = Class.create({ initialize: function(element) { var defaults = { handle: false, reverteffect: function(element, top_offset, left_offset) { var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, queue: {scope:'_draggable', position:'end'} }); }, endeffect: function(element) { var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, queue: {scope:'_draggable', position:'end'}, afterFinish: function(){ Draggable._dragging[element] = false } }); }, zindex: 1000, revert: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } delay: 0 }; if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) Object.extend(defaults, { starteffect: function(element) { element._opacity = Element.getOpacity(element); Draggable._dragging[element] = true; new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); } }); var options = Object.extend(defaults, arguments[1] || { }); this.element = $(element); if(options.handle && Object.isString(options.handle)) this.handle = this.element.down('.'+options.handle, 0); if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = this.element; if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { options.scroll = $(options.scroll); this._isScrollChild = Element.childOf(this.element, options.scroll); } Element.makePositioned(this.element); // fix IE this.options = options; this.dragging = false; this.eventMouseDown = this.initDrag.bindAsEventListener(this); Event.observe(this.handle, "mousedown", this.eventMouseDown); Draggables.register(this); }, destroy: function() { Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); Draggables.unregister(this); }, currentDelta: function() { return([ parseInt(Element.getStyle(this.element,'left') || '0'), parseInt(Element.getStyle(this.element,'top') || '0')]); }, initDrag: function(event) { if(!Object.isUndefined(Draggable._dragging[this.element]) && Draggable._dragging[this.element]) return; if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); if((tag_name = src.tagName.toUpperCase()) && ( tag_name=='INPUT' || tag_name=='SELECT' || tag_name=='OPTION' || tag_name=='BUTTON' || tag_name=='TEXTAREA')) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pos = Position.cumulativeOffset(this.element); this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); Draggables.activate(this); Event.stop(event); } }, startDrag: function(event) { this.dragging = true; if(!this.delta) this.delta = this.currentDelta(); if(this.options.zindex) { this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); this.element.style.zIndex = this.options.zindex; } if(this.options.ghosting) { this._clone = this.element.cloneNode(true); this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); if (!this._originallyAbsolute) Position.absolutize(this.element); this.element.parentNode.insertBefore(this._clone, this.element); } if(this.options.scroll) { if (this.options.scroll == window) { var where = this._getWindowScroll(this.options.scroll); this.originalScrollLeft = where.left; this.originalScrollTop = where.top; } else { this.originalScrollLeft = this.options.scroll.scrollLeft; this.originalScrollTop = this.options.scroll.scrollTop; } } Draggables.notify('onStart', this, event); if(this.options.starteffect) this.options.starteffect(this.element); }, updateDrag: function(event, pointer) { if(!this.dragging) this.startDrag(event); if(!this.options.quiet){ Position.prepare(); Droppables.show(pointer, this.element); } Draggables.notify('onDrag', this, event); this.draw(pointer); if(this.options.change) this.options.change(this); if(this.options.scroll) { this.stopScrolling(); var p; if (this.options.scroll == window) { with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } } else { p = Position.page(this.options.scroll); p[0] += this.options.scroll.scrollLeft + Position.deltaX; p[1] += this.options.scroll.scrollTop + Position.deltaY; p.push(p[0]+this.options.scroll.offsetWidth); p.push(p[1]+this.options.scroll.offsetHeight); } var speed = [0,0]; if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); this.startScrolling(speed); } // fix AppleWebKit rendering if(Prototype.Browser.WebKit) window.scrollBy(0,0); Event.stop(event); }, finishDrag: function(event, success) { this.dragging = false; if(this.options.quiet){ Position.prepare(); var pointer = [Event.pointerX(event), Event.pointerY(event)]; Droppables.show(pointer, this.element); } if(this.options.ghosting) { if (!this._originallyAbsolute) Position.relativize(this.element); delete this._originallyAbsolute; Element.remove(this._clone); this._clone = null; } var dropped = false; if(success) { dropped = Droppables.fire(event, this.element); if (!dropped) dropped = false; } if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; if(revert && Object.isFunction(revert)) revert = revert(this.element); var d = this.currentDelta(); if(revert && this.options.reverteffect) { if (dropped == 0 || revert != 'failure') this.options.reverteffect(this.element, d[1]-this.delta[1], d[0]-this.delta[0]); } else { this.delta = d; } if(this.options.zindex) this.element.style.zIndex = this.originalZ; if(this.options.endeffect) this.options.endeffect(this.element); Draggables.deactivate(this); Droppables.reset(); }, keyPress: function(event) { if(event.keyCode!=Event.KEY_ESC) return; this.finishDrag(event, false); Event.stop(event); }, endDrag: function(event) { if(!this.dragging) return; this.stopScrolling(); this.finishDrag(event, true); Event.stop(event); }, draw: function(point) { var pos = Position.cumulativeOffset(this.element); if(this.options.ghosting) { var r = Position.realOffset(this.element); pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; } var d = this.currentDelta(); pos[0] -= d[0]; pos[1] -= d[1]; if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; } var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this)); if(this.options.snap) { if(Object.isFunction(this.options.snap)) { p = this.options.snap(p[0],p[1],this); } else { if(Object.isArray(this.options.snap)) { p = p.map( function(v, i) { return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); } else { p = p.map( function(v) { return (v/this.options.snap).round()*this.options.snap }.bind(this)); } }} var style = this.element.style; if((!this.options.constraint) || (this.options.constraint=='horizontal')) style.left = p[0] + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) style.top = p[1] + "px"; if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering }, stopScrolling: function() { if(this.scrollInterval) { clearInterval(this.scrollInterval); this.scrollInterval = null; Draggables._lastScrollPointer = null; } }, startScrolling: function(speed) { if(!(speed[0] || speed[1])) return; this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; this.lastScrolled = new Date(); this.scrollInterval = setInterval(this.scroll.bind(this), 10); }, scroll: function() { var current = new Date(); var delta = current - this.lastScrolled; this.lastScrolled = current; if(this.options.scroll == window) { with (this._getWindowScroll(this.options.scroll)) { if (this.scrollSpeed[0] || this.scrollSpeed[1]) { var d = delta / 1000; this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); } } } else { this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; } Position.prepare(); Droppables.show(Draggables._lastPointer, this.element); Draggables.notify('onDrag', this); if (this._isScrollChild) { Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; if (Draggables._lastScrollPointer[0] < 0) Draggables._lastScrollPointer[0] = 0; if (Draggables._lastScrollPointer[1] < 0) Draggables._lastScrollPointer[1] = 0; this.draw(Draggables._lastScrollPointer); } if(this.options.change) this.options.change(this); }, _getWindowScroll: function(w) { var T, L, W, H; with (w.document) { if (w.document.documentElement && documentElement.scrollTop) { T = documentElement.scrollTop; L = documentElement.scrollLeft; } else if (w.document.body) { T = body.scrollTop; L = body.scrollLeft; } if (w.innerWidth) { W = w.innerWidth; H = w.innerHeight; } else if (w.document.documentElement && documentElement.clientWidth) { W = documentElement.clientWidth; H = documentElement.clientHeight; } else { W = body.offsetWidth; H = body.offsetHeight; } } return { top: T, left: L, width: W, height: H }; } }); Draggable._dragging = { }; /*--------------------------------------------------------------------------*/ var SortableObserver = Class.create({ initialize: function(element, observer) { this.element = $(element); this.observer = observer; this.lastValue = Sortable.serialize(this.element); }, onStart: function() { this.lastValue = Sortable.serialize(this.element); }, onEnd: function() { Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } }); var Sortable = { SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, sortables: { }, _findRootElement: function(element) { while (element.tagName.toUpperCase() != "BODY") { if(element.id && Sortable.sortables[element.id]) return element; element = element.parentNode; } }, options: function(element) { element = Sortable._findRootElement($(element)); if(!element) return; return Sortable.sortables[element.id]; }, destroy: function(element){ element = $(element); var s = Sortable.sortables[element.id]; if(s) { Draggables.removeObserver(s.element); s.droppables.each(function(d){ Droppables.remove(d) }); s.draggables.invoke('destroy'); delete Sortable.sortables[s.element.id]; } }, create: function(element) { element = $(element); var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' dropOnEmpty: false, tree: false, treeTag: 'ul', overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, delay: 0, hoverclass: null, ghosting: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, format: this.SERIALIZE_RULE, // these take arrays of elements or ids and can be // used for better initialization performance elements: false, handles: false, onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction }, arguments[1] || { }); // clear any old sortable with same element this.destroy(element); // build options for the draggables var options_for_draggable = { revert: true, quiet: options.quiet, scroll: options.scroll, scrollSpeed: options.scrollSpeed, scrollSensitivity: options.scrollSensitivity, delay: options.delay, ghosting: options.ghosting, constraint: options.constraint, handle: options.handle }; if(options.starteffect) options_for_draggable.starteffect = options.starteffect; if(options.reverteffect) options_for_draggable.reverteffect = options.reverteffect; else if(options.ghosting) options_for_draggable.reverteffect = function(element) { element.style.top = 0; element.style.left = 0; }; if(options.endeffect) options_for_draggable.endeffect = options.endeffect; if(options.zindex) options_for_draggable.zindex = options.zindex; // build options for the droppables var options_for_droppable = { overlap: options.overlap, containment: options.containment, tree: options.tree, hoverclass: options.hoverclass, onHover: Sortable.onHover }; var options_for_tree = { onHover: Sortable.onEmptyHover, overlap: options.overlap, containment: options.containment, hoverclass: options.hoverclass }; // fix for gecko engine Element.cleanWhitespace(element); options.draggables = []; options.droppables = []; // drop on empty handling if(options.dropOnEmpty || options.tree) { Droppables.add(element, options_for_tree); options.droppables.push(element); } (options.elements || this.findElements(element, options) || []).each( function(e,i) { var handle = options.handles ? $(options.handles[i]) : (options.handle ? $(e).select('.' + options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); if(options.tree) e.treeNode = element; options.droppables.push(e); }); if(options.tree) { (Sortable.findTreeElements(element, options) || []).each( function(e) { Droppables.add(e, options_for_tree); e.treeNode = element; options.droppables.push(e); }); } // keep reference this.sortables[element.id] = options; // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); }, // return all suitable-for-sortable elements in a guaranteed order findElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.tag); }, findTreeElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.treeTag); }, onHover: function(element, dropon, overlap) { if(Element.isParent(dropon, element)) return; if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { return; } else if(overlap>0.5) { Sortable.mark(dropon, 'before'); if(dropon.previousSibling != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, dropon); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } else { Sortable.mark(dropon, 'after'); var nextElement = dropon.nextSibling || null; if(nextElement != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, nextElement); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } }, onEmptyHover: function(element, dropon, overlap) { var oldParentNode = element.parentNode; var droponOptions = Sortable.options(dropon); if(!Element.isParent(dropon, element)) { var index; var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); var child = null; if(children) { var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); for (index = 0; index < children.length; index += 1) { if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { offset -= Element.offsetSize (children[index], droponOptions.overlap); } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { child = index + 1 < children.length ? children[index + 1] : null; break; } else { child = children[index]; break; } } } dropon.insertBefore(element, child); Sortable.options(oldParentNode).onChange(element); droponOptions.onChange(element); } }, unmark: function() { if(Sortable._marker) Sortable._marker.hide(); }, mark: function(dropon, position) { // mark on ghosting only var sortable = Sortable.options(dropon.parentNode); if(sortable && !sortable.ghosting) return; if(!Sortable._marker) { Sortable._marker = ($('dropmarker') || Element.extend(document.createElement('DIV'))). hide().addClassName('dropmarker').setStyle({position:'absolute'}); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); } var offsets = Position.cumulativeOffset(dropon); Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); if(position=='after') if(sortable.overlap == 'horizontal') Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); else Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); Sortable._marker.show(); }, _tree: function(element, options, parent) { var children = Sortable.findElements(element, options) || []; for (var i = 0; i < children.length; ++i) { var match = children[i].id.match(options.format); if (!match) continue; var child = { id: encodeURIComponent(match ? match[1] : null), element: element, parent: parent, children: [], position: parent.children.length, container: $(children[i]).down(options.treeTag) }; /* Get the element containing the children and recurse over it */ if (child.container) this._tree(child.container, options, child); parent.children.push (child); } return parent; }, tree: function(element) { element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ tag: sortableOptions.tag, treeTag: sortableOptions.treeTag, only: sortableOptions.only, name: element.id, format: sortableOptions.format }, arguments[1] || { }); var root = { id: null, parent: null, children: [], container: element, position: 0 }; return Sortable._tree(element, options, root); }, /* Construct a [i] index for a particular node */ _constructIndex: function(node) { var index = ''; do { if (node.id) index = '[' + node.position + ']' + index; } while ((node = node.parent) != null); return index; }, sequence: function(element) { element = $(element); var options = Object.extend(this.options(element), arguments[1] || { }); return $(this.findElements(element, options) || []).map( function(item) { return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; }); }, setSequence: function(element, new_sequence) { element = $(element); var options = Object.extend(this.options(element), arguments[2] || { }); var nodeMap = { }; this.findElements(element, options).each( function(n) { if (n.id.match(options.format)) nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; n.parentNode.removeChild(n); }); new_sequence.each(function(ident) { var n = nodeMap[ident]; if (n) { n[1].appendChild(n[0]); delete nodeMap[ident]; } }); }, serialize: function(element) { element = $(element); var options = Object.extend(Sortable.options(element), arguments[1] || { }); var name = encodeURIComponent( (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); if (options.tree) { return Sortable.tree(element, arguments[1]).children.map( function (item) { return [name + Sortable._constructIndex(item) + "[id]=" + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); }).flatten().join('&'); } else { return Sortable.sequence(element, arguments[1]).map( function(item) { return name + "[]=" + encodeURIComponent(item); }).join('&'); } } }; // Returns true if child is contained within element Element.isParent = function(child, element) { if (!child.parentNode || child == element) return false; if (child.parentNode == element) return true; return Element.isParent(child.parentNode, element); }; Element.findChildren = function(element, only, recursive, tagName) { if(!element.hasChildNodes()) return null; tagName = tagName.toUpperCase(); if(only) only = [only].flatten(); var elements = []; $A(element.childNodes).each( function(e) { if(e.tagName && e.tagName.toUpperCase()==tagName && (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) elements.push(e); if(recursive) { var grandchildren = Element.findChildren(e, only, recursive, tagName); if(grandchildren) elements.push(grandchildren); } }); return (elements.length>0 ? elements.flatten() : []); }; Element.offsetSize = function (element, type) { return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; }; ================================================ FILE: test/apps/rails2/public/javascripts/effects.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) // Martin Bialasinki // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // converts rgb() and #xxx to #xxxxxx format, // returns self (or first argument) if not convertable String.prototype.parseColor = function() { var color = '#'; if (this.slice(0,4) == 'rgb(') { var cols = this.slice(4,this.length-1).split(','); var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); } else { if (this.slice(0,1) == '#') { if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); if (this.length==7) color = this.toLowerCase(); } } return (color.length==7 ? color : (arguments[0] || this)); }; /*--------------------------------------------------------------------------*/ Element.collectTextNodes = function(element) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); }).flatten().join(''); }; Element.collectTextNodesIgnoreClass = function(element, className) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? Element.collectTextNodesIgnoreClass(node, className) : '')); }).flatten().join(''); }; Element.setContentZoom = function(element, percent) { element = $(element); element.setStyle({fontSize: (percent/100) + 'em'}); if (Prototype.Browser.WebKit) window.scrollBy(0,0); return element; }; Element.getInlineOpacity = function(element){ return $(element).style.opacity || ''; }; Element.forceRerendering = function(element) { try { element = $(element); var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch(e) { } }; /*--------------------------------------------------------------------------*/ var Effect = { _elementDoesNotExistError: { name: 'ElementDoesNotExistError', message: 'The specified DOM element does not exist, but is required for this effect to operate' }, Transitions: { linear: Prototype.K, sinoidal: function(pos) { return (-Math.cos(pos*Math.PI)/2) + .5; }, reverse: function(pos) { return 1-pos; }, flicker: function(pos) { var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; return pos > 1 ? 1 : pos; }, wobble: function(pos) { return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; }, pulse: function(pos, pulses) { return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; }, spring: function(pos) { return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); }, none: function(pos) { return 0; }, full: function(pos) { return 1; } }, DefaultOptions: { duration: 1.0, // seconds fps: 100, // 100= assume 66fps max. sync: false, // true for combining from: 0.0, to: 1.0, delay: 0.0, queue: 'parallel' }, tagifyText: function(element) { var tagifyStyle = 'position:relative'; if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; element = $(element); $A(element.childNodes).each( function(child) { if (child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( new Element('span', {style: tagifyStyle}).update( character == ' ' ? String.fromCharCode(160) : character), child); }); Element.remove(child); } }); }, multiple: function(element, effect) { var elements; if (((typeof element == 'object') || Object.isFunction(element)) && (element.length)) elements = element; else elements = $(element).childNodes; var options = Object.extend({ speed: 0.1, delay: 0.0 }, arguments[2] || { }); var masterDelay = options.delay; $A(elements).each( function(element, index) { new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); }); }, PAIRS: { 'slide': ['SlideDown','SlideUp'], 'blind': ['BlindDown','BlindUp'], 'appear': ['Appear','Fade'] }, toggle: function(element, effect) { element = $(element); effect = (effect || 'appear').toLowerCase(); var options = Object.extend({ queue: { position:'end', scope:(element.id || 'global'), limit: 1 } }, arguments[2] || { }); Effect[element.visible() ? Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); } }; Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; /* ------------- core effects ------------- */ Effect.ScopedQueue = Class.create(Enumerable, { initialize: function() { this.effects = []; this.interval = null; }, _each: function(iterator) { this.effects._each(iterator); }, add: function(effect) { var timestamp = new Date().getTime(); var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position; switch(position) { case 'front': // move unstarted effects after this effect this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { e.startOn += effect.finishOn; e.finishOn += effect.finishOn; }); break; case 'with-last': timestamp = this.effects.pluck('startOn').max() || timestamp; break; case 'end': // start effect after last queued effect has finished timestamp = this.effects.pluck('finishOn').max() || timestamp; break; } effect.startOn += timestamp; effect.finishOn += timestamp; if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) this.effects.push(effect); if (!this.interval) this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); if (this.effects.length == 0) { clearInterval(this.interval); this.interval = null; } }, loop: function() { var timePos = new Date().getTime(); for(var i=0, len=this.effects.length;i= this.startOn) { if (timePos >= this.finishOn) { this.render(1.0); this.cancel(); this.event('beforeFinish'); if (this.finish) this.finish(); this.event('afterFinish'); return; } var pos = (timePos - this.startOn) / this.totalTime, frame = (pos * this.totalFrames).round(); if (frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, cancel: function() { if (!this.options.sync) Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); if (this.options[eventName]) this.options[eventName](this); }, inspect: function() { var data = $H(); for(property in this) if (!Object.isFunction(this[property])) data.set(property, this[property]); return '#'; } }); Effect.Parallel = Class.create(Effect.Base, { initialize: function(effects) { this.effects = effects || []; this.start(arguments[1]); }, update: function(position) { this.effects.invoke('render', position); }, finish: function(position) { this.effects.each( function(effect) { effect.render(1.0); effect.cancel(); effect.event('beforeFinish'); if (effect.finish) effect.finish(position); effect.event('afterFinish'); }); } }); Effect.Tween = Class.create(Effect.Base, { initialize: function(object, from, to) { object = Object.isString(object) ? $(object) : object; var args = $A(arguments), method = args.last(), options = args.length == 5 ? args[3] : null; this.method = Object.isFunction(method) ? method.bind(object) : Object.isFunction(object[method]) ? object[method].bind(object) : function(value) { object[method] = value }; this.start(Object.extend({ from: from, to: to }, options || { })); }, update: function(position) { this.method(position); } }); Effect.Event = Class.create(Effect.Base, { initialize: function() { this.start(Object.extend({ duration: 0 }, arguments[0] || { })); }, update: Prototype.emptyFunction }); Effect.Opacity = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); var options = Object.extend({ from: this.element.getOpacity() || 0.0, to: 1.0 }, arguments[1] || { }); this.start(options); }, update: function(position) { this.element.setOpacity(position); } }); Effect.Move = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ x: 0, y: 0, mode: 'relative' }, arguments[1] || { }); this.start(options); }, setup: function() { this.element.makePositioned(); this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); this.originalTop = parseFloat(this.element.getStyle('top') || '0'); if (this.options.mode == 'absolute') { this.options.x = this.options.x - this.originalLeft; this.options.y = this.options.y - this.originalTop; } }, update: function(position) { this.element.setStyle({ left: (this.options.x * position + this.originalLeft).round() + 'px', top: (this.options.y * position + this.originalTop).round() + 'px' }); } }); // for backwards compatibility Effect.MoveBy = function(element, toTop, toLeft) { return new Effect.Move(element, Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); }; Effect.Scale = Class.create(Effect.Base, { initialize: function(element, percent) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, scaleMode: 'box', // 'box' or 'contents' or { } with provided values scaleFrom: 100.0, scaleTo: percent }, arguments[2] || { }); this.start(options); }, setup: function() { this.restoreAfterFinish = this.options.restoreAfterFinish || false; this.elementPositioning = this.element.getStyle('position'); this.originalStyle = { }; ['top','left','width','height','fontSize'].each( function(k) { this.originalStyle[k] = this.element.style[k]; }.bind(this)); this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; var fontSize = this.element.getStyle('font-size') || '100%'; ['em','px','%','pt'].each( function(fontSizeType) { if (fontSize.indexOf(fontSizeType)>0) { this.fontSize = parseFloat(fontSize); this.fontSizeType = fontSizeType; } }.bind(this)); this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; this.dims = null; if (this.options.scaleMode=='box') this.dims = [this.element.offsetHeight, this.element.offsetWidth]; if (/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; if (!this.dims) this.dims = [this.options.scaleMode.originalHeight, this.options.scaleMode.originalWidth]; }, update: function(position) { var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); if (this.options.scaleContent && this.fontSize) this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { var d = { }; if (this.options.scaleX) d.width = width.round() + 'px'; if (this.options.scaleY) d.height = height.round() + 'px'; if (this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; if (this.elementPositioning == 'absolute') { if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; } else { if (this.options.scaleY) d.top = -topd + 'px'; if (this.options.scaleX) d.left = -leftd + 'px'; } } this.element.setStyle(d); } }); Effect.Highlight = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow if (this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect this.oldStyle = { }; if (!this.options.keepBackgroundImage) { this.oldStyle.backgroundImage = this.element.getStyle('background-image'); this.element.setStyle({backgroundImage: 'none'}); } if (!this.options.endcolor) this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); if (!this.options.restorecolor) this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); }, update: function(position) { this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); }, finish: function() { this.element.setStyle(Object.extend(this.oldStyle, { backgroundColor: this.options.restorecolor })); } }); Effect.ScrollTo = function(element) { var options = arguments[1] || { }, scrollOffsets = document.viewport.getScrollOffsets(), elementOffsets = $(element).cumulativeOffset(); if (options.offset) elementOffsets[1] += options.offset; return new Effect.Tween(null, scrollOffsets.top, elementOffsets[1], options, function(p){ scrollTo(scrollOffsets.left, p.round()); } ); }; /* ------------- combination effects ------------- */ Effect.Fade = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ from: element.getOpacity() || 1.0, to: 0.0, afterFinishInternal: function(effect) { if (effect.options.to!=0) return; effect.element.hide().setStyle({opacity: oldOpacity}); } }, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Appear = function(element) { element = $(element); var options = Object.extend({ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), to: 1.0, // force Safari to render floated elements properly afterFinishInternal: function(effect) { effect.element.forceRerendering(); }, beforeSetup: function(effect) { effect.element.setOpacity(effect.options.from).show(); }}, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Puff = function(element) { element = $(element); var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position'), top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; return new Effect.Parallel( [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], Object.extend({ duration: 1.0, beforeSetupInternal: function(effect) { Position.absolutize(effect.effects[0].element); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().setStyle(oldStyle); } }, arguments[1] || { }) ); }; Effect.BlindUp = function(element) { element = $(element); element.makeClipping(); return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, restoreAfterFinish: true, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }, arguments[1] || { }) ); }; Effect.BlindDown = function(element) { element = $(element); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: 0, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.element.undoClipping(); } }, arguments[1] || { })); }; Effect.SwitchOff = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); return new Effect.Appear(element, Object.extend({ duration: 0.4, from: 0, transition: Effect.Transitions.flicker, afterFinishInternal: function(effect) { new Effect.Scale(effect.element, 1, { duration: 0.3, scaleFromCenter: true, scaleX: false, scaleContent: false, restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); } }); } }, arguments[1] || { })); }; Effect.DropOut = function(element) { element = $(element); var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left'), opacity: element.getInlineOpacity() }; return new Effect.Parallel( [ new Effect.Move(element, {x: 0, y: 100, sync: true }), new Effect.Opacity(element, { sync: true, to: 0.0 }) ], Object.extend( { duration: 0.5, beforeSetup: function(effect) { effect.effects[0].element.makePositioned(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); } }, arguments[1] || { })); }; Effect.Shake = function(element) { element = $(element); var options = Object.extend({ distance: 20, duration: 0.5 }, arguments[1] || {}); var distance = parseFloat(options.distance); var split = parseFloat(options.duration) / 10.0; var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left') }; return new Effect.Move(element, { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { effect.element.undoPositioned().setStyle(oldStyle); }}); }}); }}); }}); }}); }}); }; Effect.SlideDown = function(element) { element = $(element).cleanWhitespace(); // SlideDown need to have the content of the element wrapped in a container element with fixed height! var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: window.opera ? 0 : 1, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; Effect.SlideUp = function(element) { element = $(element).cleanWhitespace(); var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, window.opera ? 0 : 1, Object.extend({ scaleContent: false, scaleX: false, scaleMode: 'box', scaleFrom: 100, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; // Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { return new Effect.Scale(element, window.opera ? 1 : 0, { restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }); }; Effect.Grow = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.full }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var initialMoveX, initialMoveY; var moveX, moveY; switch (options.direction) { case 'top-left': initialMoveX = initialMoveY = moveX = moveY = 0; break; case 'top-right': initialMoveX = dims.width; initialMoveY = moveY = 0; moveX = -dims.width; break; case 'bottom-left': initialMoveX = moveX = 0; initialMoveY = dims.height; moveY = -dims.height; break; case 'bottom-right': initialMoveX = dims.width; initialMoveY = dims.height; moveX = -dims.width; moveY = -dims.height; break; case 'center': initialMoveX = dims.width / 2; initialMoveY = dims.height / 2; moveX = -dims.width / 2; moveY = -dims.height / 2; break; } return new Effect.Move(element, { x: initialMoveX, y: initialMoveY, duration: 0.01, beforeSetup: function(effect) { effect.element.hide().makeClipping().makePositioned(); }, afterFinishInternal: function(effect) { new Effect.Parallel( [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), new Effect.Scale(effect.element, 100, { scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) ], Object.extend({ beforeSetup: function(effect) { effect.effects[0].element.setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); } }); }; Effect.Shrink = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.none }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var moveX, moveY; switch (options.direction) { case 'top-left': moveX = moveY = 0; break; case 'top-right': moveX = dims.width; moveY = 0; break; case 'bottom-left': moveX = 0; moveY = dims.height; break; case 'bottom-right': moveX = dims.width; moveY = dims.height; break; case 'center': moveX = dims.width / 2; moveY = dims.height / 2; break; } return new Effect.Parallel( [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) ], Object.extend({ beforeStartInternal: function(effect) { effect.effects[0].element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); }; Effect.Pulsate = function(element) { element = $(element); var options = arguments[1] || { }, oldOpacity = element.getInlineOpacity(), transition = options.transition || Effect.Transitions.linear, reverser = function(pos){ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); }; return new Effect.Opacity(element, Object.extend(Object.extend({ duration: 2.0, from: 0, afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); }; Effect.Fold = function(element) { element = $(element); var oldStyle = { top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; element.makeClipping(); return new Effect.Scale(element, 5, Object.extend({ scaleContent: false, scaleX: false, afterFinishInternal: function(effect) { new Effect.Scale(element, 1, { scaleContent: false, scaleY: false, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().setStyle(oldStyle); } }); }}, arguments[1] || { })); }; Effect.Morph = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ style: { } }, arguments[1] || { }); if (!Object.isString(options.style)) this.style = $H(options.style); else { if (options.style.include(':')) this.style = options.style.parseStyle(); else { this.element.addClassName(options.style); this.style = $H(this.element.getStyles()); this.element.removeClassName(options.style); var css = this.element.getStyles(); this.style = this.style.reject(function(style) { return style.value == css[style.key]; }); options.afterFinishInternal = function(effect) { effect.element.addClassName(effect.options.style); effect.transforms.each(function(transform) { effect.element.style[transform.style] = ''; }); }; } } this.start(options); }, setup: function(){ function parseColor(color){ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; color = color.parseColor(); return $R(0,2).map(function(i){ return parseInt( color.slice(i*2+1,i*2+3), 16 ); }); } this.transforms = this.style.map(function(pair){ var property = pair[0], value = pair[1], unit = null; if (value.parseColor('#zzzzzz') != '#zzzzzz') { value = value.parseColor(); unit = 'color'; } else if (property == 'opacity') { value = parseFloat(value); if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); } else if (Element.CSS_LENGTH.test(value)) { var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); value = parseFloat(components[1]); unit = (components.length == 3) ? components[2] : null; } var originalValue = this.element.getStyle(property); return { style: property.camelize(), originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), targetValue: unit=='color' ? parseColor(value) : value, unit: unit }; }.bind(this)).reject(function(transform){ return ( (transform.originalValue == transform.targetValue) || ( transform.unit != 'color' && (isNaN(transform.originalValue) || isNaN(transform.targetValue)) ) ); }); }, update: function(position) { var style = { }, transform, i = this.transforms.length; while(i--) style[(transform = this.transforms[i]).style] = transform.unit=='color' ? '#'+ (Math.round(transform.originalValue[0]+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + (Math.round(transform.originalValue[1]+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + (Math.round(transform.originalValue[2]+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : (transform.originalValue + (transform.targetValue - transform.originalValue) * position).toFixed(3) + (transform.unit === null ? '' : transform.unit); this.element.setStyle(style, true); } }); Effect.Transform = Class.create({ initialize: function(tracks){ this.tracks = []; this.options = arguments[1] || { }; this.addTracks(tracks); }, addTracks: function(tracks){ tracks.each(function(track){ track = $H(track); var data = track.values().first(); this.tracks.push($H({ ids: track.keys().first(), effect: Effect.Morph, options: { style: data } })); }.bind(this)); return this; }, play: function(){ return new Effect.Parallel( this.tracks.map(function(track){ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); var elements = [$(ids) || $$(ids)].flatten(); return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); }).flatten(), this.options ); } }); Element.CSS_PROPERTIES = $w( 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 'fontSize fontWeight height left letterSpacing lineHeight ' + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 'right textIndent top width wordSpacing zIndex'); Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; String.__parseStyleElement = document.createElement('div'); String.prototype.parseStyle = function(){ var style, styleRules = $H(); if (Prototype.Browser.WebKit) style = new Element('div',{style:this}).style; else { String.__parseStyleElement.innerHTML = '
    '; style = String.__parseStyleElement.childNodes[0].style; } Element.CSS_PROPERTIES.each(function(property){ if (style[property]) styleRules.set(property, style[property]); }); if (Prototype.Browser.IE && this.include('opacity')) styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); return styleRules; }; if (document.defaultView && document.defaultView.getComputedStyle) { Element.getStyles = function(element) { var css = document.defaultView.getComputedStyle($(element), null); return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { styles[property] = css[property]; return styles; }); }; } else { Element.getStyles = function(element) { element = $(element); var css = element.currentStyle, styles; styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { results[property] = css[property]; return results; }); if (!styles.opacity) styles.opacity = element.getOpacity(); return styles; }; } Effect.Methods = { morph: function(element, style) { element = $(element); new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); return element; }, visualEffect: function(element, effect, options) { element = $(element); var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); new Effect[klass](element, options); return element; }, highlight: function(element, options) { element = $(element); new Effect.Highlight(element, options); return element; } }; $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 'pulsate shake puff squish switchOff dropOut').each( function(effect) { Effect.Methods[effect] = function(element, options){ element = $(element); Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); return element; }; } ); $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( function(f) { Effect.Methods[f] = Element[f]; } ); Element.addMethods(Effect.Methods); ================================================ FILE: test/apps/rails2/public/javascripts/prototype.js ================================================ /* Prototype JavaScript framework, version 1.6.0.3 * (c) 2005-2008 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ * *--------------------------------------------------------------------------*/ var Prototype = { Version: '1.6.0.3', Browser: { IE: !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1), Opera: navigator.userAgent.indexOf('Opera') > -1, WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) }, BrowserFeatures: { XPath: !!document.evaluate, SelectorsAPI: !!document.querySelector, ElementExtensions: !!window.HTMLElement, SpecificElementExtensions: document.createElement('div')['__proto__'] && document.createElement('div')['__proto__'] !== document.createElement('form')['__proto__'] }, ScriptFragment: ']*>([\\S\\s]*?)<\/script>', JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, emptyFunction: function() { }, K: function(x) { return x } }; if (Prototype.Browser.MobileSafari) Prototype.BrowserFeatures.SpecificElementExtensions = false; /* Based on Alex Arnell's inheritance implementation. */ var Class = { create: function() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } }; Class.Methods = { addMethods: function(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } }; var Abstract = { }; Object.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; Object.extend(Object, { inspect: function(object) { try { if (Object.isUndefined(object)) return 'undefined'; if (object === null) return 'null'; return object.inspect ? object.inspect() : String(object); } catch (e) { if (e instanceof RangeError) return '...'; throw e; } }, toJSON: function(object) { var type = typeof object; switch (type) { case 'undefined': case 'function': case 'unknown': return; case 'boolean': return object.toString(); } if (object === null) return 'null'; if (object.toJSON) return object.toJSON(); if (Object.isElement(object)) return; var results = []; for (var property in object) { var value = Object.toJSON(object[property]); if (!Object.isUndefined(value)) results.push(property.toJSON() + ': ' + value); } return '{' + results.join(', ') + '}'; }, toQueryString: function(object) { return $H(object).toQueryString(); }, toHTML: function(object) { return object && object.toHTML ? object.toHTML() : String.interpret(object); }, keys: function(object) { var keys = []; for (var property in object) keys.push(property); return keys; }, values: function(object) { var values = []; for (var property in object) values.push(object[property]); return values; }, clone: function(object) { return Object.extend({ }, object); }, isElement: function(object) { return !!(object && object.nodeType == 1); }, isArray: function(object) { return object != null && typeof object == "object" && 'splice' in object && 'join' in object; }, isHash: function(object) { return object instanceof Hash; }, isFunction: function(object) { return typeof object == "function"; }, isString: function(object) { return typeof object == "string"; }, isNumber: function(object) { return typeof object == "number"; }, isUndefined: function(object) { return typeof object == "undefined"; } }); Object.extend(Function.prototype, { argumentNames: function() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] .replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }, bind: function() { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }, bindAsEventListener: function() { var __method = this, args = $A(arguments), object = args.shift(); return function(event) { return __method.apply(object, [event || window.event].concat(args)); } }, curry: function() { if (!arguments.length) return this; var __method = this, args = $A(arguments); return function() { return __method.apply(this, args.concat($A(arguments))); } }, delay: function() { var __method = this, args = $A(arguments), timeout = args.shift() * 1000; return window.setTimeout(function() { return __method.apply(__method, args); }, timeout); }, defer: function() { var args = [0.01].concat($A(arguments)); return this.delay.apply(this, args); }, wrap: function(wrapper) { var __method = this; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } }, methodize: function() { if (this._methodized) return this._methodized; var __method = this; return this._methodized = function() { return __method.apply(null, [this].concat($A(arguments))); }; } }); Date.prototype.toJSON = function() { return '"' + this.getUTCFullYear() + '-' + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + this.getUTCDate().toPaddedString(2) + 'T' + this.getUTCHours().toPaddedString(2) + ':' + this.getUTCMinutes().toPaddedString(2) + ':' + this.getUTCSeconds().toPaddedString(2) + 'Z"'; }; var Try = { these: function() { var returnValue; for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); break; } catch (e) { } } return returnValue; } }; RegExp.prototype.match = RegExp.prototype.test; RegExp.escape = function(str) { return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }; /*--------------------------------------------------------------------------*/ var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; this.currentlyExecuting = false; this.registerCallback(); }, registerCallback: function() { this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, execute: function() { this.callback(this); }, stop: function() { if (!this.timer) return; clearInterval(this.timer); this.timer = null; }, onTimerEvent: function() { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; this.execute(); } finally { this.currentlyExecuting = false; } } } }); Object.extend(String, { interpret: function(value) { return value == null ? '' : String(value); }, specialChar: { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\\': '\\\\' } }); Object.extend(String.prototype, { gsub: function(pattern, replacement) { var result = '', source = this, match; replacement = arguments.callee.prepareReplacement(replacement); while (source.length > 0) { if (match = source.match(pattern)) { result += source.slice(0, match.index); result += String.interpret(replacement(match)); source = source.slice(match.index + match[0].length); } else { result += source, source = ''; } } return result; }, sub: function(pattern, replacement, count) { replacement = this.gsub.prepareReplacement(replacement); count = Object.isUndefined(count) ? 1 : count; return this.gsub(pattern, function(match) { if (--count < 0) return match[0]; return replacement(match); }); }, scan: function(pattern, iterator) { this.gsub(pattern, iterator); return String(this); }, truncate: function(length, truncation) { length = length || 30; truncation = Object.isUndefined(truncation) ? '...' : truncation; return this.length > length ? this.slice(0, length - truncation.length) + truncation : String(this); }, strip: function() { return this.replace(/^\s+/, '').replace(/\s+$/, ''); }, stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, stripScripts: function() { return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); }, extractScripts: function() { var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); return (this.match(matchAll) || []).map(function(scriptTag) { return (scriptTag.match(matchOne) || ['', ''])[1]; }); }, evalScripts: function() { return this.extractScripts().map(function(script) { return eval(script) }); }, escapeHTML: function() { var self = arguments.callee; self.text.data = this; return self.div.innerHTML; }, unescapeHTML: function() { var div = new Element('div'); div.innerHTML = this.stripTags(); return div.childNodes[0] ? (div.childNodes.length > 1 ? $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : div.childNodes[0].nodeValue) : ''; }, toQueryParams: function(separator) { var match = this.strip().match(/([^?#]*)(#.*)?$/); if (!match) return { }; return match[1].split(separator || '&').inject({ }, function(hash, pair) { if ((pair = pair.split('='))[0]) { var key = decodeURIComponent(pair.shift()); var value = pair.length > 1 ? pair.join('=') : pair[0]; if (value != undefined) value = decodeURIComponent(value); if (key in hash) { if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; hash[key].push(value); } else hash[key] = value; } return hash; }); }, toArray: function() { return this.split(''); }, succ: function() { return this.slice(0, this.length - 1) + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); }, times: function(count) { return count < 1 ? '' : new Array(count + 1).join(this); }, camelize: function() { var parts = this.split('-'), len = parts.length; if (len == 1) return parts[0]; var camelized = this.charAt(0) == '-' ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) : parts[0]; for (var i = 1; i < len; i++) camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); return camelized; }, capitalize: function() { return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); }, underscore: function() { return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); }, dasherize: function() { return this.gsub(/_/,'-'); }, inspect: function(useDoubleQuotes) { var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { var character = String.specialChar[match[0]]; return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); }); if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; return "'" + escapedString.replace(/'/g, '\\\'') + "'"; }, toJSON: function() { return this.inspect(true); }, unfilterJSON: function(filter) { return this.sub(filter || Prototype.JSONFilter, '#{1}'); }, isJSON: function() { var str = this; if (str.blank()) return false; str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); }, evalJSON: function(sanitize) { var json = this.unfilterJSON(); try { if (!sanitize || json.isJSON()) return eval('(' + json + ')'); } catch (e) { } throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); }, include: function(pattern) { return this.indexOf(pattern) > -1; }, startsWith: function(pattern) { return this.indexOf(pattern) === 0; }, endsWith: function(pattern) { var d = this.length - pattern.length; return d >= 0 && this.lastIndexOf(pattern) === d; }, empty: function() { return this == ''; }, blank: function() { return /^\s*$/.test(this); }, interpolate: function(object, pattern) { return new Template(this, pattern).evaluate(object); } }); if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { escapeHTML: function() { return this.replace(/&/g,'&').replace(//g,'>'); }, unescapeHTML: function() { return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } }); String.prototype.gsub.prepareReplacement = function(replacement) { if (Object.isFunction(replacement)) return replacement; var template = new Template(replacement); return function(match) { return template.evaluate(match) }; }; String.prototype.parseQuery = String.prototype.toQueryParams; Object.extend(String.prototype.escapeHTML, { div: document.createElement('div'), text: document.createTextNode('') }); String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); var Template = Class.create({ initialize: function(template, pattern) { this.template = template.toString(); this.pattern = pattern || Template.Pattern; }, evaluate: function(object) { if (Object.isFunction(object.toTemplateReplacements)) object = object.toTemplateReplacements(); return this.template.gsub(this.pattern, function(match) { if (object == null) return ''; var before = match[1] || ''; if (before == '\\') return match[2]; var ctx = object, expr = match[3]; var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; match = pattern.exec(expr); if (match == null) return before; while (match != null) { var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; ctx = ctx[comp]; if (null == ctx || '' == match[3]) break; expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); match = pattern.exec(expr); } return before + String.interpret(ctx); }); } }); Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; var $break = { }; var Enumerable = { each: function(iterator, context) { var index = 0; try { this._each(function(value) { iterator.call(context, value, index++); }); } catch (e) { if (e != $break) throw e; } return this; }, eachSlice: function(number, iterator, context) { var index = -number, slices = [], array = this.toArray(); if (number < 1) return array; while ((index += number) < array.length) slices.push(array.slice(index, index+number)); return slices.collect(iterator, context); }, all: function(iterator, context) { iterator = iterator || Prototype.K; var result = true; this.each(function(value, index) { result = result && !!iterator.call(context, value, index); if (!result) throw $break; }); return result; }, any: function(iterator, context) { iterator = iterator || Prototype.K; var result = false; this.each(function(value, index) { if (result = !!iterator.call(context, value, index)) throw $break; }); return result; }, collect: function(iterator, context) { iterator = iterator || Prototype.K; var results = []; this.each(function(value, index) { results.push(iterator.call(context, value, index)); }); return results; }, detect: function(iterator, context) { var result; this.each(function(value, index) { if (iterator.call(context, value, index)) { result = value; throw $break; } }); return result; }, findAll: function(iterator, context) { var results = []; this.each(function(value, index) { if (iterator.call(context, value, index)) results.push(value); }); return results; }, grep: function(filter, iterator, context) { iterator = iterator || Prototype.K; var results = []; if (Object.isString(filter)) filter = new RegExp(filter); this.each(function(value, index) { if (filter.match(value)) results.push(iterator.call(context, value, index)); }); return results; }, include: function(object) { if (Object.isFunction(this.indexOf)) if (this.indexOf(object) != -1) return true; var found = false; this.each(function(value) { if (value == object) { found = true; throw $break; } }); return found; }, inGroupsOf: function(number, fillWith) { fillWith = Object.isUndefined(fillWith) ? null : fillWith; return this.eachSlice(number, function(slice) { while(slice.length < number) slice.push(fillWith); return slice; }); }, inject: function(memo, iterator, context) { this.each(function(value, index) { memo = iterator.call(context, memo, value, index); }); return memo; }, invoke: function(method) { var args = $A(arguments).slice(1); return this.map(function(value) { return value[method].apply(value, args); }); }, max: function(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value >= result) result = value; }); return result; }, min: function(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value < result) result = value; }); return result; }, partition: function(iterator, context) { iterator = iterator || Prototype.K; var trues = [], falses = []; this.each(function(value, index) { (iterator.call(context, value, index) ? trues : falses).push(value); }); return [trues, falses]; }, pluck: function(property) { var results = []; this.each(function(value) { results.push(value[property]); }); return results; }, reject: function(iterator, context) { var results = []; this.each(function(value, index) { if (!iterator.call(context, value, index)) results.push(value); }); return results; }, sortBy: function(iterator, context) { return this.map(function(value, index) { return { value: value, criteria: iterator.call(context, value, index) }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }).pluck('value'); }, toArray: function() { return this.map(); }, zip: function() { var iterator = Prototype.K, args = $A(arguments); if (Object.isFunction(args.last())) iterator = args.pop(); var collections = [this].concat(args).map($A); return this.map(function(value, index) { return iterator(collections.pluck(index)); }); }, size: function() { return this.toArray().length; }, inspect: function() { return '#'; } }; Object.extend(Enumerable, { map: Enumerable.collect, find: Enumerable.detect, select: Enumerable.findAll, filter: Enumerable.findAll, member: Enumerable.include, entries: Enumerable.toArray, every: Enumerable.all, some: Enumerable.any }); function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } if (Prototype.Browser.WebKit) { $A = function(iterable) { if (!iterable) return []; // In Safari, only use the `toArray` method if it's not a NodeList. // A NodeList is a function, has an function `item` property, and a numeric // `length` property. Adapted from Google Doctype. if (!(typeof iterable === 'function' && typeof iterable.length === 'number' && typeof iterable.item === 'function') && iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; }; } Array.from = $A; Object.extend(Array.prototype, Enumerable); if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }, clear: function() { this.length = 0; return this; }, first: function() { return this[0]; }, last: function() { return this[this.length - 1]; }, compact: function() { return this.select(function(value) { return value != null; }); }, flatten: function() { return this.inject([], function(array, value) { return array.concat(Object.isArray(value) ? value.flatten() : [value]); }); }, without: function() { var values = $A(arguments); return this.select(function(value) { return !values.include(value); }); }, reverse: function(inline) { return (inline !== false ? this : this.toArray())._reverse(); }, reduce: function() { return this.length > 1 ? this : this[0]; }, uniq: function(sorted) { return this.inject([], function(array, value, index) { if (0 == index || (sorted ? array.last() != value : !array.include(value))) array.push(value); return array; }); }, intersect: function(array) { return this.uniq().findAll(function(item) { return array.detect(function(value) { return item === value }); }); }, clone: function() { return [].concat(this); }, size: function() { return this.length; }, inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; }, toJSON: function() { var results = []; this.each(function(object) { var value = Object.toJSON(object); if (!Object.isUndefined(value)) results.push(value); }); return '[' + results.join(', ') + ']'; } }); // use native browser JS 1.6 implementation if available if (Object.isFunction(Array.prototype.forEach)) Array.prototype._each = Array.prototype.forEach; if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { i || (i = 0); var length = this.length; if (i < 0) i = length + i; for (; i < length; i++) if (this[i] === item) return i; return -1; }; if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; var n = this.slice(0, i).reverse().indexOf(item); return (n < 0) ? n : i - n - 1; }; Array.prototype.toArray = Array.prototype.clone; function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); return string ? string.split(/\s+/) : []; } if (Prototype.Browser.Opera){ Array.prototype.concat = function() { var array = []; for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); for (var i = 0, length = arguments.length; i < length; i++) { if (Object.isArray(arguments[i])) { for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) array.push(arguments[i][j]); } else { array.push(arguments[i]); } } return array; }; } Object.extend(Number.prototype, { toColorPart: function() { return this.toPaddedString(2, 16); }, succ: function() { return this + 1; }, times: function(iterator, context) { $R(0, this, true).each(iterator, context); return this; }, toPaddedString: function(length, radix) { var string = this.toString(radix || 10); return '0'.times(length - string.length) + string; }, toJSON: function() { return isFinite(this) ? this.toString() : 'null'; } }); $w('abs round ceil floor').each(function(method){ Number.prototype[method] = Math[method].methodize(); }); function $H(object) { return new Hash(object); }; var Hash = Class.create(Enumerable, (function() { function toQueryPair(key, value) { if (Object.isUndefined(value)) return key; return key + '=' + encodeURIComponent(String.interpret(value)); } return { initialize: function(object) { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); }, _each: function(iterator) { for (var key in this._object) { var value = this._object[key], pair = [key, value]; pair.key = key; pair.value = value; iterator(pair); } }, set: function(key, value) { return this._object[key] = value; }, get: function(key) { // simulating poorly supported hasOwnProperty if (this._object[key] !== Object.prototype[key]) return this._object[key]; }, unset: function(key) { var value = this._object[key]; delete this._object[key]; return value; }, toObject: function() { return Object.clone(this._object); }, keys: function() { return this.pluck('key'); }, values: function() { return this.pluck('value'); }, index: function(value) { var match = this.detect(function(pair) { return pair.value === value; }); return match && match.key; }, merge: function(object) { return this.clone().update(object); }, update: function(object) { return new Hash(object).inject(this, function(result, pair) { result.set(pair.key, pair.value); return result; }); }, toQueryString: function() { return this.inject([], function(results, pair) { var key = encodeURIComponent(pair.key), values = pair.value; if (values && typeof values == 'object') { if (Object.isArray(values)) return results.concat(values.map(toQueryPair.curry(key))); } else results.push(toQueryPair(key, values)); return results; }).join('&'); }, inspect: function() { return '#'; }, toJSON: function() { return Object.toJSON(this.toObject()); }, clone: function() { return new Hash(this); } } })()); Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; Hash.from = $H; var ObjectRange = Class.create(Enumerable, { initialize: function(start, end, exclusive) { this.start = start; this.end = end; this.exclusive = exclusive; }, _each: function(iterator) { var value = this.start; while (this.include(value)) { iterator(value); value = value.succ(); } }, include: function(value) { if (value < this.start) return false; if (this.exclusive) return value < this.end; return value <= this.end; } }); var $R = function(start, end, exclusive) { return new ObjectRange(start, end, exclusive); }; var Ajax = { getTransport: function() { return Try.these( function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, activeRequestCount: 0 }; Ajax.Responders = { responders: [], _each: function(iterator) { this.responders._each(iterator); }, register: function(responder) { if (!this.include(responder)) this.responders.push(responder); }, unregister: function(responder) { this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { if (Object.isFunction(responder[callback])) { try { responder[callback].apply(responder, [request, transport, json]); } catch (e) { } } }); } }; Object.extend(Ajax.Responders, Enumerable); Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++ }, onComplete: function() { Ajax.activeRequestCount-- } }); Ajax.Base = Class.create({ initialize: function(options) { this.options = { method: 'post', asynchronous: true, contentType: 'application/x-www-form-urlencoded', encoding: 'UTF-8', parameters: '', evalJSON: true, evalJS: true }; Object.extend(this.options, options || { }); this.options.method = this.options.method.toLowerCase(); if (Object.isString(this.options.parameters)) this.options.parameters = this.options.parameters.toQueryParams(); else if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); Ajax.Request = Class.create(Ajax.Base, { _complete: false, initialize: function($super, url, options) { $super(options); this.transport = Ajax.getTransport(); this.request(url); }, request: function(url) { this.url = url; this.method = this.options.method; var params = Object.clone(this.options.parameters); if (!['get', 'post'].include(this.method)) { // simulate other verbs over post params['_method'] = this.method; this.method = 'post'; } this.parameters = params; if (params = Object.toQueryString(params)) { // when GET, append parameters to URL if (this.method == 'get') this.url += (this.url.include('?') ? '&' : '?') + params; else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='; } try { var response = new Ajax.Response(this); if (this.options.onCreate) this.options.onCreate(response); Ajax.Responders.dispatch('onCreate', this, response); this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); this.body = this.method == 'post' ? (this.options.postBody || params) : null; this.transport.send(this.body); /* Force Firefox to handle ready state 4 for synchronous requests */ if (!this.options.asynchronous && this.transport.overrideMimeType) this.onStateChange(); } catch (e) { this.dispatchException(e); } }, onStateChange: function() { var readyState = this.transport.readyState; if (readyState > 1 && !((readyState == 4) && this._complete)) this.respondToReadyState(this.transport.readyState); }, setRequestHeaders: function() { var headers = { 'X-Requested-With': 'XMLHttpRequest', 'X-Prototype-Version': Prototype.Version, 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' }; if (this.method == 'post') { headers['Content-type'] = this.options.contentType + (this.options.encoding ? '; charset=' + this.options.encoding : ''); /* Force "Connection: close" for older Mozilla browsers to work * around a bug where XMLHttpRequest sends an incorrect * Content-length header. See Mozilla Bugzilla #246651. */ if (this.transport.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) headers['Connection'] = 'close'; } // user-defined headers if (typeof this.options.requestHeaders == 'object') { var extras = this.options.requestHeaders; if (Object.isFunction(extras.push)) for (var i = 0, length = extras.length; i < length; i += 2) headers[extras[i]] = extras[i+1]; else $H(extras).each(function(pair) { headers[pair.key] = pair.value }); } for (var name in headers) this.transport.setRequestHeader(name, headers[name]); }, success: function() { var status = this.getStatus(); return !status || (status >= 200 && status < 300); }, getStatus: function() { try { return this.transport.status || 0; } catch (e) { return 0 } }, respondToReadyState: function(readyState) { var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); if (state == 'Complete') { try { this._complete = true; (this.options['on' + response.status] || this.options['on' + (this.success() ? 'Success' : 'Failure')] || Prototype.emptyFunction)(response, response.headerJSON); } catch (e) { this.dispatchException(e); } var contentType = response.getHeader('Content-type'); if (this.options.evalJS == 'force' || (this.options.evalJS && this.isSameOrigin() && contentType && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } try { (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); } catch (e) { this.dispatchException(e); } if (state == 'Complete') { // avoid memory leak in MSIE: clean up this.transport.onreadystatechange = Prototype.emptyFunction; } }, isSameOrigin: function() { var m = this.url.match(/^\s*https?:\/\/[^\/]*/); return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ protocol: location.protocol, domain: document.domain, port: location.port ? ':' + location.port : '' })); }, getHeader: function(name) { try { return this.transport.getResponseHeader(name) || null; } catch (e) { return null } }, evalResponse: function() { try { return eval((this.transport.responseText || '').unfilterJSON()); } catch (e) { this.dispatchException(e); } }, dispatchException: function(exception) { (this.options.onException || Prototype.emptyFunction)(this, exception); Ajax.Responders.dispatch('onException', this, exception); } }); Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; Ajax.Response = Class.create({ initialize: function(request){ this.request = request; var transport = this.transport = request.transport, readyState = this.readyState = transport.readyState; if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { this.status = this.getStatus(); this.statusText = this.getStatusText(); this.responseText = String.interpret(transport.responseText); this.headerJSON = this._getHeaderJSON(); } if(readyState == 4) { var xml = transport.responseXML; this.responseXML = Object.isUndefined(xml) ? null : xml; this.responseJSON = this._getResponseJSON(); } }, status: 0, statusText: '', getStatus: Ajax.Request.prototype.getStatus, getStatusText: function() { try { return this.transport.statusText || ''; } catch (e) { return '' } }, getHeader: Ajax.Request.prototype.getHeader, getAllHeaders: function() { try { return this.getAllResponseHeaders(); } catch (e) { return null } }, getResponseHeader: function(name) { return this.transport.getResponseHeader(name); }, getAllResponseHeaders: function() { return this.transport.getAllResponseHeaders(); }, _getHeaderJSON: function() { var json = this.getHeader('X-JSON'); if (!json) return null; json = decodeURIComponent(escape(json)); try { return json.evalJSON(this.request.options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } }, _getResponseJSON: function() { var options = this.request.options; if (!options.evalJSON || (options.evalJSON != 'force' && !(this.getHeader('Content-type') || '').include('application/json')) || this.responseText.blank()) return null; try { return this.responseText.evalJSON(options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } } }); Ajax.Updater = Class.create(Ajax.Request, { initialize: function($super, container, url, options) { this.container = { success: (container.success || container), failure: (container.failure || (container.success ? null : container)) }; options = Object.clone(options); var onComplete = options.onComplete; options.onComplete = (function(response, json) { this.updateContent(response.responseText); if (Object.isFunction(onComplete)) onComplete(response, json); }).bind(this); $super(url, options); }, updateContent: function(responseText) { var receiver = this.container[this.success() ? 'success' : 'failure'], options = this.options; if (!options.evalScripts) responseText = responseText.stripScripts(); if (receiver = $(receiver)) { if (options.insertion) { if (Object.isString(options.insertion)) { var insertion = { }; insertion[options.insertion] = responseText; receiver.insert(insertion); } else options.insertion(receiver, responseText); } else receiver.update(responseText); } } }); Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { initialize: function($super, container, url, options) { $super(options); this.onComplete = this.options.onComplete; this.frequency = (this.options.frequency || 2); this.decay = (this.options.decay || 1); this.updater = { }; this.container = container; this.url = url; this.start(); }, start: function() { this.options.onComplete = this.updateComplete.bind(this); this.onTimerEvent(); }, stop: function() { this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, updateComplete: function(response) { if (this.options.decay) { this.decay = (response.responseText == this.lastText ? this.decay * this.options.decay : 1); this.lastText = response.responseText; } this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); }, onTimerEvent: function() { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); function $(element) { if (arguments.length > 1) { for (var i = 0, elements = [], length = arguments.length; i < length; i++) elements.push($(arguments[i])); return elements; } if (Object.isString(element)) element = document.getElementById(element); return Element.extend(element); } if (Prototype.BrowserFeatures.XPath) { document._getElementsByXPath = function(expression, parentElement) { var results = []; var query = document.evaluate(expression, $(parentElement) || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i < length; i++) results.push(Element.extend(query.snapshotItem(i))); return results; }; } /*--------------------------------------------------------------------------*/ if (!window.Node) var Node = { }; if (!Node.ELEMENT_NODE) { // DOM level 2 ECMAScript Language Binding Object.extend(Node, { ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12 }); } (function() { var element = this.Element; this.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; if (Prototype.Browser.IE && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); }; Object.extend(this.Element, element || { }); if (element) this.Element.prototype = element.prototype; }).call(window); Element.cache = { }; Element.Methods = { visible: function(element) { return $(element).style.display != 'none'; }, toggle: function(element) { element = $(element); Element[Element.visible(element) ? 'hide' : 'show'](element); return element; }, hide: function(element) { element = $(element); element.style.display = 'none'; return element; }, show: function(element) { element = $(element); element.style.display = ''; return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); return element; }, update: function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) return element.update().insert(content); content = Object.toHTML(content); element.innerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }, replace: function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); else if (!Object.isElement(content)) { content = Object.toHTML(content); var range = element.ownerDocument.createRange(); range.selectNode(element); content.evalScripts.bind(content).defer(); content = range.createContextualFragment(content.stripScripts()); } element.parentNode.replaceChild(content, element); return element; }, insert: function(element, insertions) { element = $(element); if (Object.isString(insertions) || Object.isNumber(insertions) || Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) insertions = {bottom:insertions}; var content, insert, tagName, childNodes; for (var position in insertions) { content = insertions[position]; position = position.toLowerCase(); insert = Element._insertionTranslations[position]; if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { insert(element, content); continue; } content = Object.toHTML(content); tagName = ((position == 'before' || position == 'after') ? element.parentNode : element).tagName.toUpperCase(); childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); if (position == 'top' || position == 'after') childNodes.reverse(); childNodes.each(insert.curry(element)); content.evalScripts.bind(content).defer(); } return element; }, wrap: function(element, wrapper, attributes) { element = $(element); if (Object.isElement(wrapper)) $(wrapper).writeAttribute(attributes || { }); else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); else wrapper = new Element('div', wrapper); if (element.parentNode) element.parentNode.replaceChild(wrapper, element); wrapper.appendChild(element); return wrapper; }, inspect: function(element) { element = $(element); var result = '<' + element.tagName.toLowerCase(); $H({'id': 'id', 'className': 'class'}).each(function(pair) { var property = pair.first(), attribute = pair.last(); var value = (element[property] || '').toString(); if (value) result += ' ' + attribute + '=' + value.inspect(true); }); return result + '>'; }, recursivelyCollect: function(element, property) { element = $(element); var elements = []; while (element = element[property]) if (element.nodeType == 1) elements.push(Element.extend(element)); return elements; }, ancestors: function(element) { return $(element).recursivelyCollect('parentNode'); }, descendants: function(element) { return $(element).select("*"); }, firstDescendant: function(element) { element = $(element).firstChild; while (element && element.nodeType != 1) element = element.nextSibling; return $(element); }, immediateDescendants: function(element) { if (!(element = $(element).firstChild)) return []; while (element && element.nodeType != 1) element = element.nextSibling; if (element) return [element].concat($(element).nextSiblings()); return []; }, previousSiblings: function(element) { return $(element).recursivelyCollect('previousSibling'); }, nextSiblings: function(element) { return $(element).recursivelyCollect('nextSibling'); }, siblings: function(element) { element = $(element); return element.previousSiblings().reverse().concat(element.nextSiblings()); }, match: function(element, selector) { if (Object.isString(selector)) selector = new Selector(selector); return selector.match($(element)); }, up: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = element.ancestors(); return Object.isNumber(expression) ? ancestors[expression] : Selector.findElement(ancestors, expression, index); }, down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); return Object.isNumber(expression) ? element.descendants()[expression] : Element.select(element, expression)[index || 0]; }, previous: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); var previousSiblings = element.previousSiblings(); return Object.isNumber(expression) ? previousSiblings[expression] : Selector.findElement(previousSiblings, expression, index); }, next: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); var nextSiblings = element.nextSiblings(); return Object.isNumber(expression) ? nextSiblings[expression] : Selector.findElement(nextSiblings, expression, index); }, select: function() { var args = $A(arguments), element = $(args.shift()); return Selector.findChildElements(element, args); }, adjacent: function() { var args = $A(arguments), element = $(args.shift()); return Selector.findChildElements(element.parentNode, args).without(element); }, identify: function(element) { element = $(element); var id = element.readAttribute('id'), self = arguments.callee; if (id) return id; do { id = 'anonymous_element_' + self.counter++ } while ($(id)); element.writeAttribute('id', id); return id; }, readAttribute: function(element, name) { element = $(element); if (Prototype.Browser.IE) { var t = Element._attributeTranslations.read; if (t.values[name]) return t.values[name](element, name); if (t.names[name]) name = t.names[name]; if (name.include(':')) { return (!element.attributes || !element.attributes[name]) ? null : element.attributes[name].value; } } return element.getAttribute(name); }, writeAttribute: function(element, name, value) { element = $(element); var attributes = { }, t = Element._attributeTranslations.write; if (typeof name == 'object') attributes = name; else attributes[name] = Object.isUndefined(value) ? true : value; for (var attr in attributes) { name = t.names[attr] || attr; value = attributes[attr]; if (t.values[attr]) name = t.values[attr](element, value); if (value === false || value === null) element.removeAttribute(name); else if (value === true) element.setAttribute(name, name); else element.setAttribute(name, value); } return element; }, getHeight: function(element) { return $(element).getDimensions().height; }, getWidth: function(element) { return $(element).getDimensions().width; }, classNames: function(element) { return new Element.ClassNames(element); }, hasClassName: function(element, className) { if (!(element = $(element))) return; var elementClassName = element.className; return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }, addClassName: function(element, className) { if (!(element = $(element))) return; if (!element.hasClassName(className)) element.className += (element.className ? ' ' : '') + className; return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; element.className = element.className.replace( new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); return element; }, toggleClassName: function(element, className) { if (!(element = $(element))) return; return element[element.hasClassName(className) ? 'removeClassName' : 'addClassName'](className); }, // removes whitespace-only text node children cleanWhitespace: function(element) { element = $(element); var node = element.firstChild; while (node) { var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) element.removeChild(node); node = nextNode; } return element; }, empty: function(element) { return $(element).innerHTML.blank(); }, descendantOf: function(element, ancestor) { element = $(element), ancestor = $(ancestor); if (element.compareDocumentPosition) return (element.compareDocumentPosition(ancestor) & 8) === 8; if (ancestor.contains) return ancestor.contains(element) && ancestor !== element; while (element = element.parentNode) if (element == ancestor) return true; return false; }, scrollTo: function(element) { element = $(element); var pos = element.cumulativeOffset(); window.scrollTo(pos[0], pos[1]); return element; }, getStyle: function(element, style) { element = $(element); style = style == 'float' ? 'cssFloat' : style.camelize(); var value = element.style[style]; if (!value || value == 'auto') { var css = document.defaultView.getComputedStyle(element, null); value = css ? css[style] : null; } if (style == 'opacity') return value ? parseFloat(value) : 1.0; return value == 'auto' ? null : value; }, getOpacity: function(element) { return $(element).getStyle('opacity'); }, setStyle: function(element, styles) { element = $(element); var elementStyle = element.style, match; if (Object.isString(styles)) { element.style.cssText += ';' + styles; return styles.include('opacity') ? element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; } for (var property in styles) if (property == 'opacity') element.setOpacity(styles[property]); else elementStyle[(property == 'float' || property == 'cssFloat') ? (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : property] = styles[property]; return element; }, setOpacity: function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }, getDimensions: function(element) { element = $(element); var display = element.getStyle('display'); if (display != 'none' && display != null) // Safari bug return {width: element.offsetWidth, height: element.offsetHeight}; // All *Width and *Height properties give 0 on elements with display none, // so enable the element temporarily var els = element.style; var originalVisibility = els.visibility; var originalPosition = els.position; var originalDisplay = els.display; els.visibility = 'hidden'; els.position = 'absolute'; els.display = 'block'; var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; els.display = originalDisplay; els.position = originalPosition; els.visibility = originalVisibility; return {width: originalWidth, height: originalHeight}; }, makePositioned: function(element) { element = $(element); var pos = Element.getStyle(element, 'position'); if (pos == 'static' || !pos) { element._madePositioned = true; element.style.position = 'relative'; // Opera returns the offset relative to the positioning context, when an // element is position relative but top and left have not been defined if (Prototype.Browser.Opera) { element.style.top = 0; element.style.left = 0; } } return element; }, undoPositioned: function(element) { element = $(element); if (element._madePositioned) { element._madePositioned = undefined; element.style.position = element.style.top = element.style.left = element.style.bottom = element.style.right = ''; } return element; }, makeClipping: function(element) { element = $(element); if (element._overflow) return element; element._overflow = Element.getStyle(element, 'overflow') || 'auto'; if (element._overflow !== 'hidden') element.style.overflow = 'hidden'; return element; }, undoClipping: function(element) { element = $(element); if (!element._overflow) return element; element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; element._overflow = null; return element; }, cumulativeOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; } while (element); return Element._returnOffset(valueL, valueT); }, positionedOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { if (element.tagName.toUpperCase() == 'BODY') break; var p = Element.getStyle(element, 'position'); if (p !== 'static') break; } } while (element); return Element._returnOffset(valueL, valueT); }, absolutize: function(element) { element = $(element); if (element.getStyle('position') == 'absolute') return element; // Position.prepare(); // To be done manually by Scripty when it needs it. var offsets = element.positionedOffset(); var top = offsets[1]; var left = offsets[0]; var width = element.clientWidth; var height = element.clientHeight; element._originalLeft = left - parseFloat(element.style.left || 0); element._originalTop = top - parseFloat(element.style.top || 0); element._originalWidth = element.style.width; element._originalHeight = element.style.height; element.style.position = 'absolute'; element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.width = width + 'px'; element.style.height = height + 'px'; return element; }, relativize: function(element) { element = $(element); if (element.getStyle('position') == 'relative') return element; // Position.prepare(); // To be done manually by Scripty when it needs it. element.style.position = 'relative'; var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.height = element._originalHeight; element.style.width = element._originalWidth; return element; }, cumulativeScrollOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.scrollTop || 0; valueL += element.scrollLeft || 0; element = element.parentNode; } while (element); return Element._returnOffset(valueL, valueT); }, getOffsetParent: function(element) { if (element.offsetParent) return $(element.offsetParent); if (element == document.body) return $(element); while ((element = element.parentNode) && element != document.body) if (Element.getStyle(element, 'position') != 'static') return $(element); return $(document.body); }, viewportOffset: function(forElement) { var valueT = 0, valueL = 0; var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; // Safari fix if (element.offsetParent == document.body && Element.getStyle(element, 'position') == 'absolute') break; } while (element = element.offsetParent); element = forElement; do { if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } } while (element = element.parentNode); return Element._returnOffset(valueL, valueT); }, clonePosition: function(element, source) { var options = Object.extend({ setLeft: true, setTop: true, setWidth: true, setHeight: true, offsetTop: 0, offsetLeft: 0 }, arguments[2] || { }); // find page position of source source = $(source); var p = source.viewportOffset(); // find coordinate system to use element = $(element); var delta = [0, 0]; var parent = null; // delta [0,0] will do fine with position: fixed elements, // position:absolute needs offsetParent deltas if (Element.getStyle(element, 'position') == 'absolute') { parent = element.getOffsetParent(); delta = parent.viewportOffset(); } // correct by body offsets (fixes Safari) if (parent == document.body) { delta[0] -= document.body.offsetLeft; delta[1] -= document.body.offsetTop; } // set position if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; if (options.setWidth) element.style.width = source.offsetWidth + 'px'; if (options.setHeight) element.style.height = source.offsetHeight + 'px'; return element; } }; Element.Methods.identify.counter = 1; Object.extend(Element.Methods, { getElementsBySelector: Element.Methods.select, childElements: Element.Methods.immediateDescendants }); Element._attributeTranslations = { write: { names: { className: 'class', htmlFor: 'for' }, values: { } } }; if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { switch (style) { case 'left': case 'top': case 'right': case 'bottom': if (proceed(element, 'position') === 'static') return null; case 'height': case 'width': // returns '0px' for hidden elements; we want it to return null if (!Element.visible(element)) return null; // returns the border-box dimensions rather than the content-box // dimensions, so we subtract padding and borders from the value var dim = parseInt(proceed(element, style), 10); if (dim !== element['offset' + style.capitalize()]) return dim + 'px'; var properties; if (style === 'height') { properties = ['border-top-width', 'padding-top', 'padding-bottom', 'border-bottom-width']; } else { properties = ['border-left-width', 'padding-left', 'padding-right', 'border-right-width']; } return properties.inject(dim, function(memo, property) { var val = proceed(element, property); return val === null ? memo : memo - parseInt(val, 10); }) + 'px'; default: return proceed(element, style); } } ); Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( function(proceed, element, attribute) { if (attribute === 'title') return element.title; return proceed(element, attribute); } ); } else if (Prototype.Browser.IE) { // IE doesn't report offsets correctly for static elements, so we change them // to "relative" to get the values, then change them back. Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( function(proceed, element) { element = $(element); // IE throws an error if element is not in document try { element.offsetParent } catch(e) { return $(document.body) } var position = element.getStyle('position'); if (position !== 'static') return proceed(element); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); $w('positionedOffset viewportOffset').each(function(method) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); try { element.offsetParent } catch(e) { return Element._returnOffset(0,0) } var position = element.getStyle('position'); if (position !== 'static') return proceed(element); // Trigger hasLayout on the offset parent so that IE6 reports // accurate offsetTop and offsetLeft values for position: fixed. var offsetParent = element.getOffsetParent(); if (offsetParent && offsetParent.getStyle('position') === 'fixed') offsetParent.setStyle({ zoom: 1 }); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); }); Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( function(proceed, element) { try { element.offsetParent } catch(e) { return Element._returnOffset(0,0) } return proceed(element); } ); Element.Methods.getStyle = function(element, style) { element = $(element); style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); var value = element.style[style]; if (!value && element.currentStyle) value = element.currentStyle[style]; if (style == 'opacity') { if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) if (value[1]) return parseFloat(value[1]) / 100; return 1.0; } if (value == 'auto') { if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) return element['offset' + style.capitalize()] + 'px'; return null; } return value; }; Element.Methods.setOpacity = function(element, value) { function stripAlpha(filter){ return filter.replace(/alpha\([^\)]*\)/gi,''); } element = $(element); var currentStyle = element.currentStyle; if ((currentStyle && !currentStyle.hasLayout) || (!currentStyle && element.style.zoom == 'normal')) element.style.zoom = 1; var filter = element.getStyle('filter'), style = element.style; if (value == 1 || value === '') { (filter = stripAlpha(filter)) ? style.filter = filter : style.removeAttribute('filter'); return element; } else if (value < 0.00001) value = 0; style.filter = stripAlpha(filter) + 'alpha(opacity=' + (value * 100) + ')'; return element; }; Element._attributeTranslations = { read: { names: { 'class': 'className', 'for': 'htmlFor' }, values: { _getAttr: function(element, attribute) { return element.getAttribute(attribute, 2); }, _getAttrNode: function(element, attribute) { var node = element.getAttributeNode(attribute); return node ? node.value : ""; }, _getEv: function(element, attribute) { attribute = element.getAttribute(attribute); return attribute ? attribute.toString().slice(23, -2) : null; }, _flag: function(element, attribute) { return $(element).hasAttribute(attribute) ? attribute : null; }, style: function(element) { return element.style.cssText.toLowerCase(); }, title: function(element) { return element.title; } } } }; Element._attributeTranslations.write = { names: Object.extend({ cellpadding: 'cellPadding', cellspacing: 'cellSpacing' }, Element._attributeTranslations.read.names), values: { checked: function(element, value) { element.checked = !!value; }, style: function(element, value) { element.style.cssText = value ? value : ''; } } }; Element._attributeTranslations.has = {}; $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; Element._attributeTranslations.has[attr.toLowerCase()] = attr; }); (function(v) { Object.extend(v, { href: v._getAttr, src: v._getAttr, type: v._getAttr, action: v._getAttrNode, disabled: v._flag, checked: v._flag, readonly: v._flag, multiple: v._flag, onload: v._getEv, onunload: v._getEv, onclick: v._getEv, ondblclick: v._getEv, onmousedown: v._getEv, onmouseup: v._getEv, onmouseover: v._getEv, onmousemove: v._getEv, onmouseout: v._getEv, onfocus: v._getEv, onblur: v._getEv, onkeypress: v._getEv, onkeydown: v._getEv, onkeyup: v._getEv, onsubmit: v._getEv, onreset: v._getEv, onselect: v._getEv, onchange: v._getEv }); })(Element._attributeTranslations.read.values); } else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1) ? 0.999999 : (value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }; } else if (Prototype.Browser.WebKit) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; if (value == 1) if(element.tagName.toUpperCase() == 'IMG' && element.width) { element.width++; element.width--; } else try { var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch (e) { } return element; }; // Safari returns margins on body which is incorrect if the child is absolutely // positioned. For performance reasons, redefine Element#cumulativeOffset for // KHTML/WebKit only. Element.Methods.cumulativeOffset = function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if (element.offsetParent == document.body) if (Element.getStyle(element, 'position') == 'absolute') break; element = element.offsetParent; } while (element); return Element._returnOffset(valueL, valueT); }; } if (Prototype.Browser.IE || Prototype.Browser.Opera) { // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements Element.Methods.update = function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) return element.update().insert(content); content = Object.toHTML(content); var tagName = element.tagName.toUpperCase(); if (tagName in Element._insertionTranslations.tags) { $A(element.childNodes).each(function(node) { element.removeChild(node) }); Element._getContentFromAnonymousElement(tagName, content.stripScripts()) .each(function(node) { element.appendChild(node) }); } else element.innerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }; } if ('outerHTML' in document.createElement('div')) { Element.Methods.replace = function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { element.parentNode.replaceChild(content, element); return element; } content = Object.toHTML(content); var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); if (Element._insertionTranslations.tags[tagName]) { var nextSibling = element.next(); var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); parent.removeChild(element); if (nextSibling) fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); else fragments.each(function(node) { parent.appendChild(node) }); } else element.outerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }; } Element._returnOffset = function(l, t) { var result = [l, t]; result.left = l; result.top = t; return result; }; Element._getContentFromAnonymousElement = function(tagName, html) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; if (t) { div.innerHTML = t[0] + html + t[1]; t[2].times(function() { div = div.firstChild }); } else div.innerHTML = html; return $A(div.childNodes); }; Element._insertionTranslations = { before: function(element, node) { element.parentNode.insertBefore(node, element); }, top: function(element, node) { element.insertBefore(node, element.firstChild); }, bottom: function(element, node) { element.appendChild(node); }, after: function(element, node) { element.parentNode.insertBefore(node, element.nextSibling); }, tags: { TABLE: ['', '
    ', 1], TBODY: ['', '
    ', 2], TR: ['', '
    ', 3], TD: ['
    ', '
    ', 4], SELECT: ['', 1] } }; (function() { Object.extend(this.tags, { THEAD: this.tags.TBODY, TFOOT: this.tags.TBODY, TH: this.tags.TD }); }).call(Element._insertionTranslations); Element.Methods.Simulated = { hasAttribute: function(element, attribute) { attribute = Element._attributeTranslations.has[attribute] || attribute; var node = $(element).getAttributeNode(attribute); return !!(node && node.specified); } }; Element.Methods.ByTag = { }; Object.extend(Element, Element.Methods); if (!Prototype.BrowserFeatures.ElementExtensions && document.createElement('div')['__proto__']) { window.HTMLElement = { }; window.HTMLElement.prototype = document.createElement('div')['__proto__']; Prototype.BrowserFeatures.ElementExtensions = true; } Element.extend = (function() { if (Prototype.BrowserFeatures.SpecificElementExtensions) return Prototype.K; var Methods = { }, ByTag = Element.Methods.ByTag; var extend = Object.extend(function(element) { if (!element || element._extendedByPrototype || element.nodeType != 1 || element == window) return element; var methods = Object.clone(Methods), tagName = element.tagName.toUpperCase(), property, value; // extend methods for specific tags if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); for (property in methods) { value = methods[property]; if (Object.isFunction(value) && !(property in element)) element[property] = value.methodize(); } element._extendedByPrototype = Prototype.emptyFunction; return element; }, { refresh: function() { // extend methods for all tags (Safari doesn't need this) if (!Prototype.BrowserFeatures.ElementExtensions) { Object.extend(Methods, Element.Methods); Object.extend(Methods, Element.Methods.Simulated); } } }); extend.refresh(); return extend; })(); Element.hasAttribute = function(element, attribute) { if (element.hasAttribute) return element.hasAttribute(attribute); return Element.Methods.Simulated.hasAttribute(element, attribute); }; Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; if (!methods) { Object.extend(Form, Form.Methods); Object.extend(Form.Element, Form.Element.Methods); Object.extend(Element.Methods.ByTag, { "FORM": Object.clone(Form.Methods), "INPUT": Object.clone(Form.Element.Methods), "SELECT": Object.clone(Form.Element.Methods), "TEXTAREA": Object.clone(Form.Element.Methods) }); } if (arguments.length == 2) { var tagName = methods; methods = arguments[1]; } if (!tagName) Object.extend(Element.Methods, methods || { }); else { if (Object.isArray(tagName)) tagName.each(extend); else extend(tagName); } function extend(tagName) { tagName = tagName.toUpperCase(); if (!Element.Methods.ByTag[tagName]) Element.Methods.ByTag[tagName] = { }; Object.extend(Element.Methods.ByTag[tagName], methods); } function copy(methods, destination, onlyIfAbsent) { onlyIfAbsent = onlyIfAbsent || false; for (var property in methods) { var value = methods[property]; if (!Object.isFunction(value)) continue; if (!onlyIfAbsent || !(property in destination)) destination[property] = value.methodize(); } } function findDOMClass(tagName) { var klass; var trans = { "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": "FrameSet", "IFRAME": "IFrame" }; if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName.capitalize() + 'Element'; if (window[klass]) return window[klass]; window[klass] = { }; window[klass].prototype = document.createElement(tagName)['__proto__']; return window[klass]; } if (F.ElementExtensions) { copy(Element.Methods, HTMLElement.prototype); copy(Element.Methods.Simulated, HTMLElement.prototype, true); } if (F.SpecificElementExtensions) { for (var tag in Element.Methods.ByTag) { var klass = findDOMClass(tag); if (Object.isUndefined(klass)) continue; copy(T[tag], klass.prototype); } } Object.extend(Element, Element.Methods); delete Element.ByTag; if (Element.extend.refresh) Element.extend.refresh(); Element.cache = { }; }; document.viewport = { getDimensions: function() { var dimensions = { }, B = Prototype.Browser; $w('width height').each(function(d) { var D = d.capitalize(); if (B.WebKit && !document.evaluate) { // Safari <3.0 needs self.innerWidth/Height dimensions[d] = self['inner' + D]; } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { // Opera <9.5 needs document.body.clientWidth/Height dimensions[d] = document.body['client' + D] } else { dimensions[d] = document.documentElement['client' + D]; } }); return dimensions; }, getWidth: function() { return this.getDimensions().width; }, getHeight: function() { return this.getDimensions().height; }, getScrollOffsets: function() { return Element._returnOffset( window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; /* Portions of the Selector class are derived from Jack Slocum's DomQuery, * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * license. Please see http://www.yui-ext.com/ for more information. */ var Selector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); if (this.shouldUseSelectorsAPI()) { this.mode = 'selectorsAPI'; } else if (this.shouldUseXPath()) { this.mode = 'xpath'; this.compileXPathMatcher(); } else { this.mode = "normal"; this.compileMatcher(); } }, shouldUseXPath: function() { if (!Prototype.BrowserFeatures.XPath) return false; var e = this.expression; // Safari 3 chokes on :*-of-type and :empty if (Prototype.Browser.WebKit && (e.include("-of-type") || e.include(":empty"))) return false; // XPath can't do namespaced attributes, nor can it read // the "checked" property from DOM nodes if ((/(\[[\w-]*?:|:checked)/).test(e)) return false; return true; }, shouldUseSelectorsAPI: function() { if (!Prototype.BrowserFeatures.SelectorsAPI) return false; if (!Selector._div) Selector._div = new Element('div'); // Make sure the browser treats the selector as valid. Test on an // isolated element to minimize cost of this check. try { Selector._div.querySelector(this.expression); } catch(e) { return false; } return true; }, compileMatcher: function() { var e = this.expression, ps = Selector.patterns, h = Selector.handlers, c = Selector.criteria, le, p, m; if (Selector._cache[e]) { this.matcher = Selector._cache[e]; return; } this.matcher = ["this.matcher = function(root) {", "var r = root, h = Selector.handlers, c = false, n;"]; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in ps) { p = ps[i]; if (m = e.match(p)) { this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : new Template(c[i]).evaluate(m)); e = e.replace(m[0], ''); break; } } } this.matcher.push("return h.unique(n);\n}"); eval(this.matcher.join('\n')); Selector._cache[this.expression] = this.matcher; }, compileXPathMatcher: function() { var e = this.expression, ps = Selector.patterns, x = Selector.xpath, le, m; if (Selector._cache[e]) { this.xpath = Selector._cache[e]; return; } this.matcher = ['.//*']; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in ps) { if (m = e.match(ps[i])) { this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m)); e = e.replace(m[0], ''); break; } } } this.xpath = this.matcher.join(''); Selector._cache[this.expression] = this.xpath; }, findElements: function(root) { root = root || document; var e = this.expression, results; switch (this.mode) { case 'selectorsAPI': // querySelectorAll queries document-wide, then filters to descendants // of the context element. That's not what we want. // Add an explicit context to the selector if necessary. if (root !== document) { var oldId = root.id, id = $(root).identify(); e = "#" + id + " " + e; } results = $A(root.querySelectorAll(e)).map(Element.extend); root.id = oldId; return results; case 'xpath': return document._getElementsByXPath(this.xpath, root); default: return this.matcher(root); } }, match: function(element) { this.tokens = []; var e = this.expression, ps = Selector.patterns, as = Selector.assertions; var le, p, m; while (e && le !== e && (/\S/).test(e)) { le = e; for (var i in ps) { p = ps[i]; if (m = e.match(p)) { // use the Selector.assertions methods unless the selector // is too complex. if (as[i]) { this.tokens.push([i, Object.clone(m)]); e = e.replace(m[0], ''); } else { // reluctantly do a document-wide search // and look for a match in the array return this.findElements(document).include(element); } } } } var match = true, name, matches; for (var i = 0, token; token = this.tokens[i]; i++) { name = token[0], matches = token[1]; if (!Selector.assertions[name](element, matches)) { match = false; break; } } return match; }, toString: function() { return this.expression; }, inspect: function() { return "#"; } }); Object.extend(Selector, { _cache: { }, xpath: { descendant: "//*", child: "/*", adjacent: "/following-sibling::*[1]", laterSibling: '/following-sibling::*', tagName: function(m) { if (m[1] == '*') return ''; return "[local-name()='" + m[1].toLowerCase() + "' or local-name()='" + m[1].toUpperCase() + "']"; }, className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", id: "[@id='#{1}']", attrPresence: function(m) { m[1] = m[1].toLowerCase(); return new Template("[@#{1}]").evaluate(m); }, attr: function(m) { m[1] = m[1].toLowerCase(); m[3] = m[5] || m[6]; return new Template(Selector.xpath.operators[m[2]]).evaluate(m); }, pseudo: function(m) { var h = Selector.xpath.pseudos[m[1]]; if (!h) return ''; if (Object.isFunction(h)) return h(m); return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); }, operators: { '=': "[@#{1}='#{3}']", '!=': "[@#{1}!='#{3}']", '^=': "[starts-with(@#{1}, '#{3}')]", '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", '*=': "[contains(@#{1}, '#{3}')]", '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" }, pseudos: { 'first-child': '[not(preceding-sibling::*)]', 'last-child': '[not(following-sibling::*)]', 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', 'empty': "[count(*) = 0 and (count(text()) = 0)]", 'checked': "[@checked]", 'disabled': "[(@disabled) and (@type!='hidden')]", 'enabled': "[not(@disabled) and (@type!='hidden')]", 'not': function(m) { var e = m[6], p = Selector.patterns, x = Selector.xpath, le, v; var exclusion = []; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in p) { if (m = e.match(p[i])) { v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); exclusion.push("(" + v.substring(1, v.length - 1) + ")"); e = e.replace(m[0], ''); break; } } } return "[not(" + exclusion.join(" and ") + ")]"; }, 'nth-child': function(m) { return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); }, 'nth-last-child': function(m) { return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); }, 'nth-of-type': function(m) { return Selector.xpath.pseudos.nth("position() ", m); }, 'nth-last-of-type': function(m) { return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); }, 'first-of-type': function(m) { m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); }, 'last-of-type': function(m) { m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); }, 'only-of-type': function(m) { var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); }, nth: function(fragment, m) { var mm, formula = m[6], predicate; if (formula == 'even') formula = '2n+0'; if (formula == 'odd') formula = '2n+1'; if (mm = formula.match(/^(\d+)$/)) // digit only return '[' + fragment + "= " + mm[1] + ']'; if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b if (mm[1] == "-") mm[1] = -1; var a = mm[1] ? Number(mm[1]) : 1; var b = mm[2] ? Number(mm[2]) : 0; predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + "((#{fragment} - #{b}) div #{a} >= 0)]"; return new Template(predicate).evaluate({ fragment: fragment, a: a, b: b }); } } } }, criteria: { tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', className: 'n = h.className(n, r, "#{1}", c); c = false;', id: 'n = h.id(n, r, "#{1}", c); c = false;', attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', attr: function(m) { m[3] = (m[5] || m[6]); return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); }, pseudo: function(m) { if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); }, descendant: 'c = "descendant";', child: 'c = "child";', adjacent: 'c = "adjacent";', laterSibling: 'c = "laterSibling";' }, patterns: { // combinators must be listed first // (and descendant needs to be last combinator) laterSibling: /^\s*~\s*/, child: /^\s*>\s*/, adjacent: /^\s*\+\s*/, descendant: /^\s/, // selectors follow tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, id: /^#([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/, pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }, // for Selector.match and Element#match assertions: { tagName: function(element, matches) { return matches[1].toUpperCase() == element.tagName.toUpperCase(); }, className: function(element, matches) { return Element.hasClassName(element, matches[1]); }, id: function(element, matches) { return element.id === matches[1]; }, attrPresence: function(element, matches) { return Element.hasAttribute(element, matches[1]); }, attr: function(element, matches) { var nodeValue = Element.readAttribute(element, matches[1]); return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); } }, handlers: { // UTILITY FUNCTIONS // joins two collections concat: function(a, b) { for (var i = 0, node; node = b[i]; i++) a.push(node); return a; }, // marks an array of nodes for counting mark: function(nodes) { var _true = Prototype.emptyFunction; for (var i = 0, node; node = nodes[i]; i++) node._countedByPrototype = _true; return nodes; }, unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) node._countedByPrototype = undefined; return nodes; }, // mark each child node with its position (for nth calls) // "ofType" flag indicates whether we're indexing for nth-of-type // rather than nth-child index: function(parentNode, reverse, ofType) { parentNode._countedByPrototype = Prototype.emptyFunction; if (reverse) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { var node = nodes[i]; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } } else { for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } }, // filters out duplicates and extends all nodes unique: function(nodes) { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i < l; i++) if (!(n = nodes[i])._countedByPrototype) { n._countedByPrototype = Prototype.emptyFunction; results.push(Element.extend(n)); } return Selector.handlers.unmark(results); }, // COMBINATOR FUNCTIONS descendant: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, node.getElementsByTagName('*')); return results; }, child: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) { for (var j = 0, child; child = node.childNodes[j]; j++) if (child.nodeType == 1 && child.tagName != '!') results.push(child); } return results; }, adjacent: function(nodes) { for (var i = 0, results = [], node; node = nodes[i]; i++) { var next = this.nextElementSibling(node); if (next) results.push(next); } return results; }, laterSibling: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, Element.nextSiblings(node)); return results; }, nextElementSibling: function(node) { while (node = node.nextSibling) if (node.nodeType == 1) return node; return null; }, previousElementSibling: function(node) { while (node = node.previousSibling) if (node.nodeType == 1) return node; return null; }, // TOKEN FUNCTIONS tagName: function(nodes, root, tagName, combinator) { var uTagName = tagName.toUpperCase(); var results = [], h = Selector.handlers; if (nodes) { if (combinator) { // fastlane for ordinary descendant combinators if (combinator == "descendant") { for (var i = 0, node; node = nodes[i]; i++) h.concat(results, node.getElementsByTagName(tagName)); return results; } else nodes = this[combinator](nodes); if (tagName == "*") return nodes; } for (var i = 0, node; node = nodes[i]; i++) if (node.tagName.toUpperCase() === uTagName) results.push(node); return results; } else return root.getElementsByTagName(tagName); }, id: function(nodes, root, id, combinator) { var targetNode = $(id), h = Selector.handlers; if (!targetNode) return []; if (!nodes && root == document) return [targetNode]; if (nodes) { if (combinator) { if (combinator == 'child') { for (var i = 0, node; node = nodes[i]; i++) if (targetNode.parentNode == node) return [targetNode]; } else if (combinator == 'descendant') { for (var i = 0, node; node = nodes[i]; i++) if (Element.descendantOf(targetNode, node)) return [targetNode]; } else if (combinator == 'adjacent') { for (var i = 0, node; node = nodes[i]; i++) if (Selector.handlers.previousElementSibling(targetNode) == node) return [targetNode]; } else nodes = h[combinator](nodes); } for (var i = 0, node; node = nodes[i]; i++) if (node == targetNode) return [targetNode]; return []; } return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; }, className: function(nodes, root, className, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); return Selector.handlers.byClassName(nodes, root, className); }, byClassName: function(nodes, root, className) { if (!nodes) nodes = Selector.handlers.descendant([root]); var needle = ' ' + className + ' '; for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { nodeClassName = node.className; if (nodeClassName.length == 0) continue; if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) results.push(node); } return results; }, attrPresence: function(nodes, root, attr, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); var results = []; for (var i = 0, node; node = nodes[i]; i++) if (Element.hasAttribute(node, attr)) results.push(node); return results; }, attr: function(nodes, root, attr, value, operator, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); var handler = Selector.operators[operator], results = []; for (var i = 0, node; node = nodes[i]; i++) { var nodeValue = Element.readAttribute(node, attr); if (nodeValue === null) continue; if (handler(nodeValue, value)) results.push(node); } return results; }, pseudo: function(nodes, name, value, root, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); if (!nodes) nodes = root.getElementsByTagName("*"); return Selector.pseudos[name](nodes, value, root); } }, pseudos: { 'first-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { if (Selector.handlers.previousElementSibling(node)) continue; results.push(node); } return results; }, 'last-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { if (Selector.handlers.nextElementSibling(node)) continue; results.push(node); } return results; }, 'only-child': function(nodes, value, root) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) results.push(node); return results; }, 'nth-child': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root); }, 'nth-last-child': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, true); }, 'nth-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, false, true); }, 'nth-last-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, true, true); }, 'first-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, "1", root, false, true); }, 'last-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, "1", root, true, true); }, 'only-of-type': function(nodes, formula, root) { var p = Selector.pseudos; return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); }, // handles the an+b logic getIndices: function(a, b, total) { if (a == 0) return b > 0 ? [b] : []; return $R(1, total).inject([], function(memo, i) { if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); return memo; }); }, // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type nth: function(nodes, formula, root, reverse, ofType) { if (nodes.length == 0) return []; if (formula == 'even') formula = '2n+0'; if (formula == 'odd') formula = '2n+1'; var h = Selector.handlers, results = [], indexed = [], m; h.mark(nodes); for (var i = 0, node; node = nodes[i]; i++) { if (!node.parentNode._countedByPrototype) { h.index(node.parentNode, reverse, ofType); indexed.push(node.parentNode); } } if (formula.match(/^\d+$/)) { // just a number formula = Number(formula); for (var i = 0, node; node = nodes[i]; i++) if (node.nodeIndex == formula) results.push(node); } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b if (m[1] == "-") m[1] = -1; var a = m[1] ? Number(m[1]) : 1; var b = m[2] ? Number(m[2]) : 0; var indices = Selector.pseudos.getIndices(a, b, nodes.length); for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { for (var j = 0; j < l; j++) if (node.nodeIndex == indices[j]) results.push(node); } } h.unmark(nodes); h.unmark(indexed); return results; }, 'empty': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { // IE treats comments as element nodes if (node.tagName == '!' || node.firstChild) continue; results.push(node); } return results; }, 'not': function(nodes, selector, root) { var h = Selector.handlers, selectorType, m; var exclusions = new Selector(selector).findElements(root); h.mark(exclusions); for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node._countedByPrototype) results.push(node); h.unmark(exclusions); return results; }, 'enabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node.disabled && (!node.type || node.type !== 'hidden')) results.push(node); return results; }, 'disabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.disabled) results.push(node); return results; }, 'checked': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.checked) results.push(node); return results; } }, operators: { '=': function(nv, v) { return nv == v; }, '!=': function(nv, v) { return nv != v; }, '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, '$=': function(nv, v) { return nv.endsWith(v); }, '*=': function(nv, v) { return nv.include(v); }, '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + '-').include('-' + (v || "").toUpperCase() + '-'); } }, split: function(expression) { var expressions = []; expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { expressions.push(m[1].strip()); }); return expressions; }, matchElements: function(elements, expression) { var matches = $$(expression), h = Selector.handlers; h.mark(matches); for (var i = 0, results = [], element; element = elements[i]; i++) if (element._countedByPrototype) results.push(element); h.unmark(matches); return results; }, findElement: function(elements, expression, index) { if (Object.isNumber(expression)) { index = expression; expression = false; } return Selector.matchElements(elements, expression || '*')[index || 0]; }, findChildElements: function(element, expressions) { expressions = Selector.split(expressions.join(',')); var results = [], h = Selector.handlers; for (var i = 0, l = expressions.length, selector; i < l; i++) { selector = new Selector(expressions[i].strip()); h.concat(results, selector.findElements(element)); } return (l > 1) ? h.unique(results) : results; } }); if (Prototype.Browser.IE) { Object.extend(Selector.handlers, { // IE returns comment nodes on getElementsByTagName("*"). // Filter them out. concat: function(a, b) { for (var i = 0, node; node = b[i]; i++) if (node.tagName !== "!") a.push(node); return a; }, // IE improperly serializes _countedByPrototype in (inner|outer)HTML. unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) node.removeAttribute('_countedByPrototype'); return nodes; } }); } function $$() { return Selector.findChildElements(document, $A(arguments)); } var Form = { reset: function(form) { $(form).reset(); return form; }, serializeElements: function(elements, options) { if (typeof options != 'object') options = { hash: !!options }; else if (Object.isUndefined(options.hash)) options.hash = true; var key, value, submitted = false, submit = options.submit; var data = elements.inject({ }, function(result, element) { if (!element.disabled && element.name) { key = element.name; value = $(element).getValue(); if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && submit !== false && (!submit || key == submit) && (submitted = true)))) { if (key in result) { // a key is already present; construct an array of values if (!Object.isArray(result[key])) result[key] = [result[key]]; result[key].push(value); } else result[key] = value; } } return result; }); return options.hash ? data : Object.toQueryString(data); } }; Form.Methods = { serialize: function(form, options) { return Form.serializeElements(Form.getElements(form), options); }, getElements: function(form) { return $A($(form).getElementsByTagName('*')).inject([], function(elements, child) { if (Form.Element.Serializers[child.tagName.toLowerCase()]) elements.push(Element.extend(child)); return elements; } ); }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); if (!typeName && !name) return $A(inputs).map(Element.extend); for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; if ((typeName && input.type != typeName) || (name && input.name != name)) continue; matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { form = $(form); Form.getElements(form).invoke('disable'); return form; }, enable: function(form) { form = $(form); Form.getElements(form).invoke('enable'); return form; }, findFirstElement: function(form) { var elements = $(form).getElements().findAll(function(element) { return 'hidden' != element.type && !element.disabled; }); var firstByIndex = elements.findAll(function(element) { return element.hasAttribute('tabIndex') && element.tabIndex >= 0; }).sortBy(function(element) { return element.tabIndex }).first(); return firstByIndex ? firstByIndex : elements.find(function(element) { return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, focusFirstElement: function(form) { form = $(form); form.findFirstElement().activate(); return form; }, request: function(form, options) { form = $(form), options = Object.clone(options || { }); var params = options.parameters, action = form.readAttribute('action') || ''; if (action.blank()) action = window.location.href; options.parameters = form.serialize(true); if (params) { if (Object.isString(params)) params = params.toQueryParams(); Object.extend(options.parameters, params); } if (form.hasAttribute('method') && !options.method) options.method = form.method; return new Ajax.Request(action, options); } }; /*--------------------------------------------------------------------------*/ Form.Element = { focus: function(element) { $(element).focus(); return element; }, select: function(element) { $(element).select(); return element; } }; Form.Element.Methods = { serialize: function(element) { element = $(element); if (!element.disabled && element.name) { var value = element.getValue(); if (value != undefined) { var pair = { }; pair[element.name] = value; return Object.toQueryString(pair); } } return ''; }, getValue: function(element) { element = $(element); var method = element.tagName.toLowerCase(); return Form.Element.Serializers[method](element); }, setValue: function(element, value) { element = $(element); var method = element.tagName.toLowerCase(); Form.Element.Serializers[method](element, value); return element; }, clear: function(element) { $(element).value = ''; return element; }, present: function(element) { return $(element).value != ''; }, activate: function(element) { element = $(element); try { element.focus(); if (element.select && (element.tagName.toLowerCase() != 'input' || !['button', 'reset', 'submit'].include(element.type))) element.select(); } catch (e) { } return element; }, disable: function(element) { element = $(element); element.disabled = true; return element; }, enable: function(element) { element = $(element); element.disabled = false; return element; } }; /*--------------------------------------------------------------------------*/ var Field = Form.Element; var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ Form.Element.Serializers = { input: function(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': return Form.Element.Serializers.inputSelector(element, value); default: return Form.Element.Serializers.textarea(element, value); } }, inputSelector: function(element, value) { if (Object.isUndefined(value)) return element.checked ? element.value : null; else element.checked = !!value; }, textarea: function(element, value) { if (Object.isUndefined(value)) return element.value; else element.value = value; }, select: function(element, value) { if (Object.isUndefined(value)) return this[element.type == 'select-one' ? 'selectOne' : 'selectMany'](element); else { var opt, currentValue, single = !Object.isArray(value); for (var i = 0, length = element.length; i < length; i++) { opt = element.options[i]; currentValue = this.optionValue(opt); if (single) { if (currentValue == value) { opt.selected = true; return; } } else opt.selected = value.include(currentValue); } } }, selectOne: function(element) { var index = element.selectedIndex; return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { var values, length = element.length; if (!length) return null; for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; if (opt.selected) values.push(this.optionValue(opt)); } return values; }, optionValue: function(opt) { // extend element because hasAttribute may not be native return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } }; /*--------------------------------------------------------------------------*/ Abstract.TimedObserver = Class.create(PeriodicalExecuter, { initialize: function($super, element, frequency, callback) { $super(callback, frequency); this.element = $(element); this.lastValue = this.getValue(); }, execute: function() { var value = this.getValue(); if (Object.isString(this.lastValue) && Object.isString(value) ? this.lastValue != value : String(this.lastValue) != String(value)) { this.callback(this.element, value); this.lastValue = value; } } }); Form.Element.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.serialize(this.element); } }); /*--------------------------------------------------------------------------*/ Abstract.EventObserver = Class.create({ initialize: function(element, callback) { this.element = $(element); this.callback = callback; this.lastValue = this.getValue(); if (this.element.tagName.toLowerCase() == 'form') this.registerFormCallbacks(); else this.registerCallback(this.element); }, onElementEvent: function() { var value = this.getValue(); if (this.lastValue != value) { this.callback(this.element, value); this.lastValue = value; } }, registerFormCallbacks: function() { Form.getElements(this.element).each(this.registerCallback, this); }, registerCallback: function(element) { if (element.type) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } } } }); Form.Element.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.serialize(this.element); } }); if (!window.Event) var Event = { }; Object.extend(Event, { KEY_BACKSPACE: 8, KEY_TAB: 9, KEY_RETURN: 13, KEY_ESC: 27, KEY_LEFT: 37, KEY_UP: 38, KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, KEY_HOME: 36, KEY_END: 35, KEY_PAGEUP: 33, KEY_PAGEDOWN: 34, KEY_INSERT: 45, cache: { }, relatedTarget: function(event) { var element; switch(event.type) { case 'mouseover': element = event.fromElement; break; case 'mouseout': element = event.toElement; break; default: return null; } return Element.extend(element); } }); Event.Methods = (function() { var isButton; if (Prototype.Browser.IE) { var buttonMap = { 0: 1, 1: 4, 2: 2 }; isButton = function(event, code) { return event.button == buttonMap[code]; }; } else if (Prototype.Browser.WebKit) { isButton = function(event, code) { switch (code) { case 0: return event.which == 1 && !event.metaKey; case 1: return event.which == 1 && event.metaKey; default: return false; } }; } else { isButton = function(event, code) { return event.which ? (event.which === code + 1) : (event.button === code); }; } return { isLeftClick: function(event) { return isButton(event, 0) }, isMiddleClick: function(event) { return isButton(event, 1) }, isRightClick: function(event) { return isButton(event, 2) }, element: function(event) { event = Event.extend(event); var node = event.target, type = event.type, currentTarget = event.currentTarget; if (currentTarget && currentTarget.tagName) { // Firefox screws up the "click" event when moving between radio buttons // via arrow keys. It also screws up the "load" and "error" events on images, // reporting the document as the target instead of the original image. if (type === 'load' || type === 'error' || (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' && currentTarget.type === 'radio')) node = currentTarget; } if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; return Element.extend(node); }, findElement: function(event, expression) { var element = Event.element(event); if (!expression) return element; var elements = [element].concat(element.ancestors()); return Selector.findElement(elements, expression, 0); }, pointer: function(event) { var docElement = document.documentElement, body = document.body || { scrollLeft: 0, scrollTop: 0 }; return { x: event.pageX || (event.clientX + (docElement.scrollLeft || body.scrollLeft) - (docElement.clientLeft || 0)), y: event.pageY || (event.clientY + (docElement.scrollTop || body.scrollTop) - (docElement.clientTop || 0)) }; }, pointerX: function(event) { return Event.pointer(event).x }, pointerY: function(event) { return Event.pointer(event).y }, stop: function(event) { Event.extend(event); event.preventDefault(); event.stopPropagation(); event.stopped = true; } }; })(); Event.extend = (function() { var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); if (Prototype.Browser.IE) { Object.extend(methods, { stopPropagation: function() { this.cancelBubble = true }, preventDefault: function() { this.returnValue = false }, inspect: function() { return "[object Event]" } }); return function(event) { if (!event) return false; if (event._extendedByPrototype) return event; event._extendedByPrototype = Prototype.emptyFunction; var pointer = Event.pointer(event); Object.extend(event, { target: event.srcElement, relatedTarget: Event.relatedTarget(event), pageX: pointer.x, pageY: pointer.y }); return Object.extend(event, methods); }; } else { Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; Object.extend(Event.prototype, methods); return Prototype.K; } })(); Object.extend(Event, (function() { var cache = Event.cache; function getEventID(element) { if (element._prototypeEventID) return element._prototypeEventID[0]; arguments.callee.id = arguments.callee.id || 1; return element._prototypeEventID = [++arguments.callee.id]; } function getDOMEventName(eventName) { if (eventName && eventName.include(':')) return "dataavailable"; return eventName; } function getCacheForID(id) { return cache[id] = cache[id] || { }; } function getWrappersForEventName(id, eventName) { var c = getCacheForID(id); return c[eventName] = c[eventName] || []; } function createWrapper(element, eventName, handler) { var id = getEventID(element); var c = getWrappersForEventName(id, eventName); if (c.pluck("handler").include(handler)) return false; var wrapper = function(event) { if (!Event || !Event.extend || (event.eventName && event.eventName != eventName)) return false; Event.extend(event); handler.call(element, event); }; wrapper.handler = handler; c.push(wrapper); return wrapper; } function findWrapper(id, eventName, handler) { var c = getWrappersForEventName(id, eventName); return c.find(function(wrapper) { return wrapper.handler == handler }); } function destroyWrapper(id, eventName, handler) { var c = getCacheForID(id); if (!c[eventName]) return false; c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); } function destroyCache() { for (var id in cache) for (var eventName in cache[id]) cache[id][eventName] = null; } // Internet Explorer needs to remove event handlers on page unload // in order to avoid memory leaks. if (window.attachEvent) { window.attachEvent("onunload", destroyCache); } // Safari has a dummy event handler on page unload so that it won't // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" // object when page is returned to via the back button using its bfcache. if (Prototype.Browser.WebKit) { window.addEventListener('unload', Prototype.emptyFunction, false); } return { observe: function(element, eventName, handler) { element = $(element); var name = getDOMEventName(eventName); var wrapper = createWrapper(element, eventName, handler); if (!wrapper) return element; if (element.addEventListener) { element.addEventListener(name, wrapper, false); } else { element.attachEvent("on" + name, wrapper); } return element; }, stopObserving: function(element, eventName, handler) { element = $(element); var id = getEventID(element), name = getDOMEventName(eventName); if (!handler && eventName) { getWrappersForEventName(id, eventName).each(function(wrapper) { element.stopObserving(eventName, wrapper.handler); }); return element; } else if (!eventName) { Object.keys(getCacheForID(id)).each(function(eventName) { element.stopObserving(eventName); }); return element; } var wrapper = findWrapper(id, eventName, handler); if (!wrapper) return element; if (element.removeEventListener) { element.removeEventListener(name, wrapper, false); } else { element.detachEvent("on" + name, wrapper); } destroyWrapper(id, eventName, handler); return element; }, fire: function(element, eventName, memo) { element = $(element); if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; var event; if (document.createEvent) { event = document.createEvent("HTMLEvents"); event.initEvent("dataavailable", true, true); } else { event = document.createEventObject(); event.eventType = "ondataavailable"; } event.eventName = eventName; event.memo = memo || { }; if (document.createEvent) { element.dispatchEvent(event); } else { element.fireEvent(event.eventType, event); } return Event.extend(event); } }; })()); Object.extend(Event, Event.Methods); Element.addMethods({ fire: Event.fire, observe: Event.observe, stopObserving: Event.stopObserving }); Object.extend(document, { fire: Element.Methods.fire.methodize(), observe: Element.Methods.observe.methodize(), stopObserving: Element.Methods.stopObserving.methodize(), loaded: false }); (function() { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards and John Resig. */ var timer; function fireContentLoadedEvent() { if (document.loaded) return; if (timer) window.clearInterval(timer); document.fire("dom:loaded"); document.loaded = true; } if (document.addEventListener) { if (Prototype.Browser.WebKit) { timer = window.setInterval(function() { if (/loaded|complete/.test(document.readyState)) fireContentLoadedEvent(); }, 0); Event.observe(window, "load", fireContentLoadedEvent); } else { document.addEventListener("DOMContentLoaded", fireContentLoadedEvent, false); } } else { document.write("

    Getting started

    Here’s how to get rolling:

    1. Use rails generate to create your models and controllers

      To see all available options, run it without parameters.

    2. Set up a default route and remove or rename this file

      Routes are set up in config/routes.rb.

    3. Create your database

      Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    ================================================ FILE: test/apps/rails3/public/javascripts/application.js ================================================ // Place your application-specific JavaScript functions and classes here // This file is automatically included by javascript_include_tag :defaults ================================================ FILE: test/apps/rails3/public/javascripts/controls.js ================================================ // script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava // Rob Wills // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This // includes drawing the autocompletion menu, observing keyboard // and mouse events, and similar. // // Specific autocompleters need to provide, at the very least, // a getUpdatedChoices function that will be invoked every time // the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by // invoking this.getToken(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized // autocompletion. Specific auto-completion logic (AJAX, etc) // belongs in getUpdatedChoices. // // Tokenized incremental autocompletion is enabled automatically // when an autocompleter is instantiated with the 'tokens' option // in the options parameter, e.g.: // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with // a token array, e.g. { tokens: [',', '\n'] } which // enables autocompletion on multiple tokens. This is most // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. if(typeof Effect == 'undefined') throw("controls.js requires including script.aculo.us' effects.js library"); var Autocompleter = { }; Autocompleter.Base = Class.create({ baseInitialize: function(element, update, options) { element = $(element); this.element = element; this.update = $(update); this.hasFocus = false; this.changed = false; this.active = false; this.index = 0; this.entryCount = 0; this.oldElementValue = this.element.value; if(this.setOptions) this.setOptions(options); else this.options = options || { }; this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; this.options.minChars = this.options.minChars || 1; this.options.onShow = this.options.onShow || function(element, update){ if(!update.style.position || update.style.position=='absolute') { update.style.position = 'absolute'; Position.clone(element, update, { setHeight: false, offsetTop: element.offsetHeight }); } Effect.Appear(update,{duration:0.15}); }; this.options.onHide = this.options.onHide || function(element, update){ new Effect.Fade(update,{duration:0.15}) }; if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); // Force carriage returns as token delimiters anyway if (!this.options.tokens.include('\n')) this.options.tokens.push('\n'); this.observer = null; this.element.setAttribute('autocomplete','off'); Element.hide(this.update); Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); if(!this.iefix && (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, ''); this.iefix = $(this.update.id+'_iefix'); } if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); }, fixIEOverlapping: function() { Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); this.iefix.style.zIndex = 1; this.update.style.zIndex = 2; Element.show(this.iefix); }, hide: function() { this.stopIndicator(); if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); if(this.iefix) Element.hide(this.iefix); }, startIndicator: function() { if(this.options.indicator) Element.show(this.options.indicator); }, stopIndicator: function() { if(this.options.indicator) Element.hide(this.options.indicator); }, onKeyPress: function(event) { if(this.active) switch(event.keyCode) { case Event.KEY_TAB: case Event.KEY_RETURN: this.selectEntry(); Event.stop(event); case Event.KEY_ESC: this.hide(); this.active = false; Event.stop(event); return; case Event.KEY_LEFT: case Event.KEY_RIGHT: return; case Event.KEY_UP: this.markPrevious(); this.render(); Event.stop(event); return; case Event.KEY_DOWN: this.markNext(); this.render(); Event.stop(event); return; } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; this.changed = true; this.hasFocus = true; if(this.observer) clearTimeout(this.observer); this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, activate: function() { this.changed = false; this.hasFocus = true; this.getUpdatedChoices(); }, onHover: function(event) { var element = Event.findElement(event, 'LI'); if(this.index != element.autocompleteIndex) { this.index = element.autocompleteIndex; this.render(); } Event.stop(event); }, onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; this.selectEntry(); this.hide(); }, onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); this.hasFocus = false; this.active = false; }, render: function() { if(this.entryCount > 0) { for (var i = 0; i < this.entryCount; i++) this.index==i ? Element.addClassName(this.getEntry(i),"selected") : Element.removeClassName(this.getEntry(i),"selected"); if(this.hasFocus) { this.show(); this.active = true; } } else { this.active = false; this.hide(); } }, markPrevious: function() { if(this.index > 0) this.index--; else this.index = this.entryCount-1; this.getEntry(this.index).scrollIntoView(true); }, markNext: function() { if(this.index < this.entryCount-1) this.index++; else this.index = 0; this.getEntry(this.index).scrollIntoView(false); }, getEntry: function(index) { return this.update.firstChild.childNodes[index]; }, getCurrentEntry: function() { return this.getEntry(this.index); }, selectEntry: function() { this.active = false; this.updateElement(this.getCurrentEntry()); }, updateElement: function(selectedElement) { if (this.options.updateElement) { this.options.updateElement(selectedElement); return; } var value = ''; if (this.options.select) { var nodes = $(selectedElement).select('.' + this.options.select) || []; if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); } else value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); var bounds = this.getTokenBounds(); if (bounds[0] != -1) { var newValue = this.element.value.substr(0, bounds[0]); var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); if (whitespace) newValue += whitespace[0]; this.element.value = newValue + value + this.element.value.substr(bounds[1]); } else { this.element.value = value; } this.oldElementValue = this.element.value; this.element.focus(); if (this.options.afterUpdateElement) this.options.afterUpdateElement(this.element, selectedElement); }, updateChoices: function(choices) { if(!this.changed && this.hasFocus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update.down()); if(this.update.firstChild && this.update.down().childNodes) { this.entryCount = this.update.down().childNodes.length; for (var i = 0; i < this.entryCount; i++) { var entry = this.getEntry(i); entry.autocompleteIndex = i; this.addObservers(entry); } } else { this.entryCount = 0; } this.stopIndicator(); this.index = 0; if(this.entryCount==1 && this.options.autoSelect) { this.selectEntry(); this.hide(); } else { this.render(); } } }, addObservers: function(element) { Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); Event.observe(element, "click", this.onClick.bindAsEventListener(this)); }, onObserverEvent: function() { this.changed = false; this.tokenBounds = null; if(this.getToken().length>=this.options.minChars) { this.getUpdatedChoices(); } else { this.active = false; this.hide(); } this.oldElementValue = this.element.value; }, getToken: function() { var bounds = this.getTokenBounds(); return this.element.value.substring(bounds[0], bounds[1]).strip(); }, getTokenBounds: function() { if (null != this.tokenBounds) return this.tokenBounds; var value = this.element.value; if (value.strip().empty()) return [-1, 0]; var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); var offset = (diff == this.oldElementValue.length ? 1 : 0); var prevTokenPos = -1, nextTokenPos = value.length; var tp; for (var index = 0, l = this.options.tokens.length; index < l; ++index) { tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); if (tp > prevTokenPos) prevTokenPos = tp; tp = value.indexOf(this.options.tokens[index], diff + offset); if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; } return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); } }); Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { var boundary = Math.min(newS.length, oldS.length); for (var index = 0; index < boundary; ++index) if (newS[index] != oldS[index]) return index; return boundary; }; Ajax.Autocompleter = Class.create(Autocompleter.Base, { initialize: function(element, update, url, options) { this.baseInitialize(element, update, options); this.options.asynchronous = true; this.options.onComplete = this.onComplete.bind(this); this.options.defaultParams = this.options.parameters || null; this.url = url; }, getUpdatedChoices: function() { this.startIndicator(); var entry = encodeURIComponent(this.options.paramName) + '=' + encodeURIComponent(this.getToken()); this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; new Ajax.Request(this.url, this.options); }, onComplete: function(request) { this.updateChoices(request.responseText); } }); // The local array autocompleter. Used when you'd prefer to // inject an array of autocompletion options into the page, rather // than sending out Ajax queries, which can be quite slow sometimes. // // The constructor takes four parameters. The first two are, as usual, // the id of the monitored textbox, and id of the autocompletion menu. // The third is the array you want to autocomplete from, and the fourth // is the options block. // // Extra local autocompletion options: // - choices - How many autocompletion choices to offer // // - partialSearch - If false, the autocompleter will match entered // text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to // search anywhere in the string, additionally set // the option fullSearch to true (default: off). // // - fullSsearch - Search anywhere in autocomplete array strings. // // - partialChars - How many characters to enter before triggering // a partial match (unlike minChars, which defines // how many characters are required to do any match // at all). Defaults to 2. // // - ignoreCase - Whether to ignore case when autocompleting. // Defaults to true. // // It's possible to pass in a custom function as the 'selector' // option, if you prefer to write your own autocompletion logic. // In that case, the other options above will not apply unless // you support them. Autocompleter.Local = Class.create(Autocompleter.Base, { initialize: function(element, update, array, options) { this.baseInitialize(element, update, options); this.options.array = array; }, getUpdatedChoices: function() { this.updateChoices(this.options.selector(this)); }, setOptions: function(options) { this.options = Object.extend({ choices: 10, partialSearch: true, partialChars: 2, ignoreCase: true, fullSearch: false, selector: function(instance) { var ret = []; // Beginning matches var partial = []; // Inside matches var entry = instance.getToken(); var count = 0; for (var i = 0; i < instance.options.array.length && ret.length < instance.options.choices ; i++) { var elem = instance.options.array[i]; var foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); while (foundPos != -1) { if (foundPos == 0 && elem.length != entry.length) { ret.push("
  • " + elem.substr(0, entry.length) + "" + elem.substr(entry.length) + "
  • "); break; } else if (entry.length >= instance.options.partialChars && instance.options.partialSearch && foundPos != -1) { if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { partial.push("
  • " + elem.substr(0, foundPos) + "" + elem.substr(foundPos, entry.length) + "" + elem.substr( foundPos + entry.length) + "
  • "); break; } } foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : elem.indexOf(entry, foundPos + 1); } } if (partial.length) ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); return "
      " + ret.join('') + "
    "; } }, options || { }); } }); // AJAX in-place editor and collection editor // Full rewrite by Christophe Porteneuve (April 2007). // Use this if you notice weird scrolling problems on some browsers, // the DOM might be a bit confused when this gets called so do this // waits 1 ms (with setTimeout) until it does the activation Field.scrollFreeActivate = function(field) { setTimeout(function() { Field.activate(field); }, 1); }; Ajax.InPlaceEditor = Class.create({ initialize: function(element, url, options) { this.url = url; this.element = element = $(element); this.prepareOptions(); this._controls = { }; arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! Object.extend(this.options, options || { }); if (!this.options.formId && this.element.id) { this.options.formId = this.element.id + '-inplaceeditor'; if ($(this.options.formId)) this.options.formId = ''; } if (this.options.externalControl) this.options.externalControl = $(this.options.externalControl); if (!this.options.externalControl) this.options.externalControlOnly = false; this._originalBackground = this.element.getStyle('background-color') || 'transparent'; this.element.title = this.options.clickToEditText; this._boundCancelHandler = this.handleFormCancellation.bind(this); this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); this._boundFailureHandler = this.handleAJAXFailure.bind(this); this._boundSubmitHandler = this.handleFormSubmission.bind(this); this._boundWrapperHandler = this.wrapUp.bind(this); this.registerListeners(); }, checkForEscapeOrReturn: function(e) { if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; if (Event.KEY_ESC == e.keyCode) this.handleFormCancellation(e); else if (Event.KEY_RETURN == e.keyCode) this.handleFormSubmission(e); }, createControl: function(mode, handler, extraClasses) { var control = this.options[mode + 'Control']; var text = this.options[mode + 'Text']; if ('button' == control) { var btn = document.createElement('input'); btn.type = 'submit'; btn.value = text; btn.className = 'editor_' + mode + '_button'; if ('cancel' == mode) btn.onclick = this._boundCancelHandler; this._form.appendChild(btn); this._controls[mode] = btn; } else if ('link' == control) { var link = document.createElement('a'); link.href = '#'; link.appendChild(document.createTextNode(text)); link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; link.className = 'editor_' + mode + '_link'; if (extraClasses) link.className += ' ' + extraClasses; this._form.appendChild(link); this._controls[mode] = link; } }, createEditField: function() { var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); var fld; if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { fld = document.createElement('input'); fld.type = 'text'; var size = this.options.size || this.options.cols || 0; if (0 < size) fld.size = size; } else { fld = document.createElement('textarea'); fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); fld.cols = this.options.cols || 40; } fld.name = this.options.paramName; fld.value = text; // No HTML breaks conversion anymore fld.className = 'editor_field'; if (this.options.submitOnBlur) fld.onblur = this._boundSubmitHandler; this._controls.editor = fld; if (this.options.loadTextURL) this.loadExternalText(); this._form.appendChild(this._controls.editor); }, createForm: function() { var ipe = this; function addText(mode, condition) { var text = ipe.options['text' + mode + 'Controls']; if (!text || condition === false) return; ipe._form.appendChild(document.createTextNode(text)); }; this._form = $(document.createElement('form')); this._form.id = this.options.formId; this._form.addClassName(this.options.formClassName); this._form.onsubmit = this._boundSubmitHandler; this.createEditField(); if ('textarea' == this._controls.editor.tagName.toLowerCase()) this._form.appendChild(document.createElement('br')); if (this.options.onFormCustomization) this.options.onFormCustomization(this, this._form); addText('Before', this.options.okControl || this.options.cancelControl); this.createControl('ok', this._boundSubmitHandler); addText('Between', this.options.okControl && this.options.cancelControl); this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); addText('After', this.options.okControl || this.options.cancelControl); }, destroy: function() { if (this._oldInnerHTML) this.element.innerHTML = this._oldInnerHTML; this.leaveEditMode(); this.unregisterListeners(); }, enterEditMode: function(e) { if (this._saving || this._editing) return; this._editing = true; this.triggerCallback('onEnterEditMode'); if (this.options.externalControl) this.options.externalControl.hide(); this.element.hide(); this.createForm(); this.element.parentNode.insertBefore(this._form, this.element); if (!this.options.loadTextURL) this.postProcessEditField(); if (e) Event.stop(e); }, enterHover: function(e) { if (this.options.hoverClassName) this.element.addClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onEnterHover'); }, getText: function() { return this.element.innerHTML.unescapeHTML(); }, handleAJAXFailure: function(transport) { this.triggerCallback('onFailure', transport); if (this._oldInnerHTML) { this.element.innerHTML = this._oldInnerHTML; this._oldInnerHTML = null; } }, handleFormCancellation: function(e) { this.wrapUp(); if (e) Event.stop(e); }, handleFormSubmission: function(e) { var form = this._form; var value = $F(this._controls.editor); this.prepareSubmission(); var params = this.options.callback(form, value) || ''; if (Object.isString(params)) params = params.toQueryParams(); params.editorId = this.element.id; if (this.options.htmlResponse) { var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Updater({ success: this.element }, this.url, options); } else { var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Request(this.url, options); } if (e) Event.stop(e); }, leaveEditMode: function() { this.element.removeClassName(this.options.savingClassName); this.removeForm(); this.leaveHover(); this.element.style.backgroundColor = this._originalBackground; this.element.show(); if (this.options.externalControl) this.options.externalControl.show(); this._saving = false; this._editing = false; this._oldInnerHTML = null; this.triggerCallback('onLeaveEditMode'); }, leaveHover: function(e) { if (this.options.hoverClassName) this.element.removeClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onLeaveHover'); }, loadExternalText: function() { this._form.addClassName(this.options.loadingClassName); this._controls.editor.disabled = true; var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._form.removeClassName(this.options.loadingClassName); var text = transport.responseText; if (this.options.stripLoadedTextTags) text = text.stripTags(); this._controls.editor.value = text; this._controls.editor.disabled = false; this.postProcessEditField(); }.bind(this), onFailure: this._boundFailureHandler }); new Ajax.Request(this.options.loadTextURL, options); }, postProcessEditField: function() { var fpc = this.options.fieldPostCreation; if (fpc) $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); }, prepareOptions: function() { this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); [this._extraDefaultOptions].flatten().compact().each(function(defs) { Object.extend(this.options, defs); }.bind(this)); }, prepareSubmission: function() { this._saving = true; this.removeForm(); this.leaveHover(); this.showSaving(); }, registerListeners: function() { this._listeners = { }; var listener; $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { listener = this[pair.value].bind(this); this._listeners[pair.key] = listener; if (!this.options.externalControlOnly) this.element.observe(pair.key, listener); if (this.options.externalControl) this.options.externalControl.observe(pair.key, listener); }.bind(this)); }, removeForm: function() { if (!this._form) return; this._form.remove(); this._form = null; this._controls = { }; }, showSaving: function() { this._oldInnerHTML = this.element.innerHTML; this.element.innerHTML = this.options.savingText; this.element.addClassName(this.options.savingClassName); this.element.style.backgroundColor = this._originalBackground; this.element.show(); }, triggerCallback: function(cbName, arg) { if ('function' == typeof this.options[cbName]) { this.options[cbName](this, arg); } }, unregisterListeners: function() { $H(this._listeners).each(function(pair) { if (!this.options.externalControlOnly) this.element.stopObserving(pair.key, pair.value); if (this.options.externalControl) this.options.externalControl.stopObserving(pair.key, pair.value); }.bind(this)); }, wrapUp: function(transport) { this.leaveEditMode(); // Can't use triggerCallback due to backward compatibility: requires // binding + direct element this._boundComplete(transport, this.element); } }); Object.extend(Ajax.InPlaceEditor.prototype, { dispose: Ajax.InPlaceEditor.prototype.destroy }); Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { initialize: function($super, element, url, options) { this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; $super(element, url, options); }, createEditField: function() { var list = document.createElement('select'); list.name = this.options.paramName; list.size = 1; this._controls.editor = list; this._collection = this.options.collection || []; if (this.options.loadCollectionURL) this.loadCollection(); else this.checkForExternalText(); this._form.appendChild(this._controls.editor); }, loadCollection: function() { this._form.addClassName(this.options.loadingClassName); this.showLoadingText(this.options.loadingCollectionText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { var js = transport.responseText.strip(); if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check throw('Server returned an invalid collection representation.'); this._collection = eval(js); this.checkForExternalText(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadCollectionURL, options); }, showLoadingText: function(text) { this._controls.editor.disabled = true; var tempOption = this._controls.editor.firstChild; if (!tempOption) { tempOption = document.createElement('option'); tempOption.value = ''; this._controls.editor.appendChild(tempOption); tempOption.selected = true; } tempOption.update((text || '').stripScripts().stripTags()); }, checkForExternalText: function() { this._text = this.getText(); if (this.options.loadTextURL) this.loadExternalText(); else this.buildOptionList(); }, loadExternalText: function() { this.showLoadingText(this.options.loadingText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._text = transport.responseText.strip(); this.buildOptionList(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadTextURL, options); }, buildOptionList: function() { this._form.removeClassName(this.options.loadingClassName); this._collection = this._collection.map(function(entry) { return 2 === entry.length ? entry : [entry, entry].flatten(); }); var marker = ('value' in this.options) ? this.options.value : this._text; var textFound = this._collection.any(function(entry) { return entry[0] == marker; }.bind(this)); this._controls.editor.update(''); var option; this._collection.each(function(entry, index) { option = document.createElement('option'); option.value = entry[0]; option.selected = textFound ? entry[0] == marker : 0 == index; option.appendChild(document.createTextNode(entry[1])); this._controls.editor.appendChild(option); }.bind(this)); this._controls.editor.disabled = false; Field.scrollFreeActivate(this._controls.editor); } }); //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** //**** This only exists for a while, in order to let **** //**** users adapt to the new API. Read up on the new **** //**** API and convert your code to it ASAP! **** Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { if (!options) return; function fallback(name, expr) { if (name in options || expr === undefined) return; options[name] = expr; }; fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : options.cancelLink == options.cancelButton == false ? false : undefined))); fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : options.okLink == options.okButton == false ? false : undefined))); fallback('highlightColor', options.highlightcolor); fallback('highlightEndColor', options.highlightendcolor); }; Object.extend(Ajax.InPlaceEditor, { DefaultOptions: { ajaxOptions: { }, autoRows: 3, // Use when multi-line w/ rows == 1 cancelControl: 'link', // 'link'|'button'|false cancelText: 'cancel', clickToEditText: 'Click to edit', externalControl: null, // id|elt externalControlOnly: false, fieldPostCreation: 'activate', // 'activate'|'focus'|false formClassName: 'inplaceeditor-form', formId: null, // id|elt highlightColor: '#ffff99', highlightEndColor: '#ffffff', hoverClassName: '', htmlResponse: true, loadingClassName: 'inplaceeditor-loading', loadingText: 'Loading...', okControl: 'button', // 'link'|'button'|false okText: 'ok', paramName: 'value', rows: 1, // If 1 and multi-line, uses autoRows savingClassName: 'inplaceeditor-saving', savingText: 'Saving...', size: 0, stripLoadedTextTags: false, submitOnBlur: false, textAfterControls: '', textBeforeControls: '', textBetweenControls: '' }, DefaultCallbacks: { callback: function(form) { return Form.serialize(form); }, onComplete: function(transport, element) { // For backward compatibility, this one is bound to the IPE, and passes // the element directly. It was too often customized, so we don't break it. new Effect.Highlight(element, { startcolor: this.options.highlightColor, keepBackgroundImage: true }); }, onEnterEditMode: null, onEnterHover: function(ipe) { ipe.element.style.backgroundColor = ipe.options.highlightColor; if (ipe._effect) ipe._effect.cancel(); }, onFailure: function(transport, ipe) { alert('Error communication with the server: ' + transport.responseText.stripTags()); }, onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. onLeaveEditMode: null, onLeaveHover: function(ipe) { ipe._effect = new Effect.Highlight(ipe.element, { startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, restorecolor: ipe._originalBackground, keepBackgroundImage: true }); } }, Listeners: { click: 'enterEditMode', keydown: 'checkForEscapeOrReturn', mouseover: 'enterHover', mouseout: 'leaveHover' } }); Ajax.InPlaceCollectionEditor.DefaultOptions = { loadingCollectionText: 'Loading options...' }; // Delayed observer, like Form.Element.Observer, // but waits for delay after last key input // Ideal for live-search fields Form.Element.DelayedObserver = Class.create({ initialize: function(element, delay, callback) { this.delay = delay || 0.5; this.element = $(element); this.callback = callback; this.timer = null; this.lastValue = $F(this.element); Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); }, delayedListener: function(event) { if(this.lastValue == $F(this.element)) return; if(this.timer) clearTimeout(this.timer); this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); this.lastValue = $F(this.element); }, onTimerEvent: function() { this.timer = null; this.callback(this.element, $F(this.element)); } }); ================================================ FILE: test/apps/rails3/public/javascripts/dragdrop.js ================================================ // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ if(Object.isUndefined(Effect)) throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { drops: [], remove: function(element) { this.drops = this.drops.reject(function(d) { return d.element==$(element) }); }, add: function(element) { element = $(element); var options = Object.extend({ greedy: true, hoverclass: null, tree: false }, arguments[1] || { }); // cache containers if(options.containment) { options._containers = []; var containment = options.containment; if(Object.isArray(containment)) { containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); } } if(options.accept) options.accept = [options.accept].flatten(); Element.makePositioned(element); // fix IE options.element = element; this.drops.push(options); }, findDeepestChild: function(drops) { deepest = drops[0]; for (i = 1; i < drops.length; ++i) if (Element.isParent(drops[i].element, deepest.element)) deepest = drops[i]; return deepest; }, isContained: function(element, drop) { var containmentNode; if(drop.tree) { containmentNode = element.treeNode; } else { containmentNode = element.parentNode; } return drop._containers.detect(function(c) { return containmentNode == c }); }, isAffected: function(point, element, drop) { return ( (drop.element!=element) && ((!drop._containers) || this.isContained(element, drop)) && ((!drop.accept) || (Element.classNames(element).detect( function(v) { return drop.accept.include(v) } ) )) && Position.within(drop.element, point[0], point[1]) ); }, deactivate: function(drop) { if(drop.hoverclass) Element.removeClassName(drop.element, drop.hoverclass); this.last_active = null; }, activate: function(drop) { if(drop.hoverclass) Element.addClassName(drop.element, drop.hoverclass); this.last_active = drop; }, show: function(point, element) { if(!this.drops.length) return; var drop, affected = []; this.drops.each( function(drop) { if(Droppables.isAffected(point, element, drop)) affected.push(drop); }); if(affected.length>0) drop = Droppables.findDeepestChild(affected); if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); if (drop) { Position.within(drop.element, point[0], point[1]); if(drop.onHover) drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); if (drop != this.last_active) Droppables.activate(drop); } }, fire: function(event, element) { if(!this.last_active) return; Position.prepare(); if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) if (this.last_active.onDrop) { this.last_active.onDrop(element, this.last_active.element, event); return true; } }, reset: function() { if(this.last_active) this.deactivate(this.last_active); } }; var Draggables = { drags: [], observers: [], register: function(draggable) { if(this.drags.length == 0) { this.eventMouseUp = this.endDrag.bindAsEventListener(this); this.eventMouseMove = this.updateDrag.bindAsEventListener(this); this.eventKeypress = this.keyPress.bindAsEventListener(this); Event.observe(document, "mouseup", this.eventMouseUp); Event.observe(document, "mousemove", this.eventMouseMove); Event.observe(document, "keypress", this.eventKeypress); } this.drags.push(draggable); }, unregister: function(draggable) { this.drags = this.drags.reject(function(d) { return d==draggable }); if(this.drags.length == 0) { Event.stopObserving(document, "mouseup", this.eventMouseUp); Event.stopObserving(document, "mousemove", this.eventMouseMove); Event.stopObserving(document, "keypress", this.eventKeypress); } }, activate: function(draggable) { if(draggable.options.delay) { this._timeout = setTimeout(function() { Draggables._timeout = null; window.focus(); Draggables.activeDraggable = draggable; }.bind(this), draggable.options.delay); } else { window.focus(); // allows keypress events if window isn't currently focused, fails for Safari this.activeDraggable = draggable; } }, deactivate: function() { this.activeDraggable = null; }, updateDrag: function(event) { if(!this.activeDraggable) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; // Mozilla-based browsers fire successive mousemove events with // the same coordinates, prevent needless redrawing (moz bug?) if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; this._lastPointer = pointer; this.activeDraggable.updateDrag(event, pointer); }, endDrag: function(event) { if(this._timeout) { clearTimeout(this._timeout); this._timeout = null; } if(!this.activeDraggable) return; this._lastPointer = null; this.activeDraggable.endDrag(event); this.activeDraggable = null; }, keyPress: function(event) { if(this.activeDraggable) this.activeDraggable.keyPress(event); }, addObserver: function(observer) { this.observers.push(observer); this._cacheObserverCallbacks(); }, removeObserver: function(element) { // element instead of observer fixes mem leaks this.observers = this.observers.reject( function(o) { return o.element==element }); this._cacheObserverCallbacks(); }, notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' if(this[eventName+'Count'] > 0) this.observers.each( function(o) { if(o[eventName]) o[eventName](eventName, draggable, event); }); if(draggable.options[eventName]) draggable.options[eventName](draggable, event); }, _cacheObserverCallbacks: function() { ['onStart','onEnd','onDrag'].each( function(eventName) { Draggables[eventName+'Count'] = Draggables.observers.select( function(o) { return o[eventName]; } ).length; }); } }; /*--------------------------------------------------------------------------*/ var Draggable = Class.create({ initialize: function(element) { var defaults = { handle: false, reverteffect: function(element, top_offset, left_offset) { var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, queue: {scope:'_draggable', position:'end'} }); }, endeffect: function(element) { var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, queue: {scope:'_draggable', position:'end'}, afterFinish: function(){ Draggable._dragging[element] = false } }); }, zindex: 1000, revert: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } delay: 0 }; if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) Object.extend(defaults, { starteffect: function(element) { element._opacity = Element.getOpacity(element); Draggable._dragging[element] = true; new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); } }); var options = Object.extend(defaults, arguments[1] || { }); this.element = $(element); if(options.handle && Object.isString(options.handle)) this.handle = this.element.down('.'+options.handle, 0); if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = this.element; if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { options.scroll = $(options.scroll); this._isScrollChild = Element.childOf(this.element, options.scroll); } Element.makePositioned(this.element); // fix IE this.options = options; this.dragging = false; this.eventMouseDown = this.initDrag.bindAsEventListener(this); Event.observe(this.handle, "mousedown", this.eventMouseDown); Draggables.register(this); }, destroy: function() { Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); Draggables.unregister(this); }, currentDelta: function() { return([ parseInt(Element.getStyle(this.element,'left') || '0'), parseInt(Element.getStyle(this.element,'top') || '0')]); }, initDrag: function(event) { if(!Object.isUndefined(Draggable._dragging[this.element]) && Draggable._dragging[this.element]) return; if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); if((tag_name = src.tagName.toUpperCase()) && ( tag_name=='INPUT' || tag_name=='SELECT' || tag_name=='OPTION' || tag_name=='BUTTON' || tag_name=='TEXTAREA')) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pos = this.element.cumulativeOffset(); this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); Draggables.activate(this); Event.stop(event); } }, startDrag: function(event) { this.dragging = true; if(!this.delta) this.delta = this.currentDelta(); if(this.options.zindex) { this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); this.element.style.zIndex = this.options.zindex; } if(this.options.ghosting) { this._clone = this.element.cloneNode(true); this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); if (!this._originallyAbsolute) Position.absolutize(this.element); this.element.parentNode.insertBefore(this._clone, this.element); } if(this.options.scroll) { if (this.options.scroll == window) { var where = this._getWindowScroll(this.options.scroll); this.originalScrollLeft = where.left; this.originalScrollTop = where.top; } else { this.originalScrollLeft = this.options.scroll.scrollLeft; this.originalScrollTop = this.options.scroll.scrollTop; } } Draggables.notify('onStart', this, event); if(this.options.starteffect) this.options.starteffect(this.element); }, updateDrag: function(event, pointer) { if(!this.dragging) this.startDrag(event); if(!this.options.quiet){ Position.prepare(); Droppables.show(pointer, this.element); } Draggables.notify('onDrag', this, event); this.draw(pointer); if(this.options.change) this.options.change(this); if(this.options.scroll) { this.stopScrolling(); var p; if (this.options.scroll == window) { with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } } else { p = Position.page(this.options.scroll); p[0] += this.options.scroll.scrollLeft + Position.deltaX; p[1] += this.options.scroll.scrollTop + Position.deltaY; p.push(p[0]+this.options.scroll.offsetWidth); p.push(p[1]+this.options.scroll.offsetHeight); } var speed = [0,0]; if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); this.startScrolling(speed); } // fix AppleWebKit rendering if(Prototype.Browser.WebKit) window.scrollBy(0,0); Event.stop(event); }, finishDrag: function(event, success) { this.dragging = false; if(this.options.quiet){ Position.prepare(); var pointer = [Event.pointerX(event), Event.pointerY(event)]; Droppables.show(pointer, this.element); } if(this.options.ghosting) { if (!this._originallyAbsolute) Position.relativize(this.element); delete this._originallyAbsolute; Element.remove(this._clone); this._clone = null; } var dropped = false; if(success) { dropped = Droppables.fire(event, this.element); if (!dropped) dropped = false; } if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; if(revert && Object.isFunction(revert)) revert = revert(this.element); var d = this.currentDelta(); if(revert && this.options.reverteffect) { if (dropped == 0 || revert != 'failure') this.options.reverteffect(this.element, d[1]-this.delta[1], d[0]-this.delta[0]); } else { this.delta = d; } if(this.options.zindex) this.element.style.zIndex = this.originalZ; if(this.options.endeffect) this.options.endeffect(this.element); Draggables.deactivate(this); Droppables.reset(); }, keyPress: function(event) { if(event.keyCode!=Event.KEY_ESC) return; this.finishDrag(event, false); Event.stop(event); }, endDrag: function(event) { if(!this.dragging) return; this.stopScrolling(); this.finishDrag(event, true); Event.stop(event); }, draw: function(point) { var pos = this.element.cumulativeOffset(); if(this.options.ghosting) { var r = Position.realOffset(this.element); pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; } var d = this.currentDelta(); pos[0] -= d[0]; pos[1] -= d[1]; if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; } var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this)); if(this.options.snap) { if(Object.isFunction(this.options.snap)) { p = this.options.snap(p[0],p[1],this); } else { if(Object.isArray(this.options.snap)) { p = p.map( function(v, i) { return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); } else { p = p.map( function(v) { return (v/this.options.snap).round()*this.options.snap }.bind(this)); } }} var style = this.element.style; if((!this.options.constraint) || (this.options.constraint=='horizontal')) style.left = p[0] + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) style.top = p[1] + "px"; if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering }, stopScrolling: function() { if(this.scrollInterval) { clearInterval(this.scrollInterval); this.scrollInterval = null; Draggables._lastScrollPointer = null; } }, startScrolling: function(speed) { if(!(speed[0] || speed[1])) return; this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; this.lastScrolled = new Date(); this.scrollInterval = setInterval(this.scroll.bind(this), 10); }, scroll: function() { var current = new Date(); var delta = current - this.lastScrolled; this.lastScrolled = current; if(this.options.scroll == window) { with (this._getWindowScroll(this.options.scroll)) { if (this.scrollSpeed[0] || this.scrollSpeed[1]) { var d = delta / 1000; this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); } } } else { this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; } Position.prepare(); Droppables.show(Draggables._lastPointer, this.element); Draggables.notify('onDrag', this); if (this._isScrollChild) { Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; if (Draggables._lastScrollPointer[0] < 0) Draggables._lastScrollPointer[0] = 0; if (Draggables._lastScrollPointer[1] < 0) Draggables._lastScrollPointer[1] = 0; this.draw(Draggables._lastScrollPointer); } if(this.options.change) this.options.change(this); }, _getWindowScroll: function(w) { var T, L, W, H; with (w.document) { if (w.document.documentElement && documentElement.scrollTop) { T = documentElement.scrollTop; L = documentElement.scrollLeft; } else if (w.document.body) { T = body.scrollTop; L = body.scrollLeft; } if (w.innerWidth) { W = w.innerWidth; H = w.innerHeight; } else if (w.document.documentElement && documentElement.clientWidth) { W = documentElement.clientWidth; H = documentElement.clientHeight; } else { W = body.offsetWidth; H = body.offsetHeight; } } return { top: T, left: L, width: W, height: H }; } }); Draggable._dragging = { }; /*--------------------------------------------------------------------------*/ var SortableObserver = Class.create({ initialize: function(element, observer) { this.element = $(element); this.observer = observer; this.lastValue = Sortable.serialize(this.element); }, onStart: function() { this.lastValue = Sortable.serialize(this.element); }, onEnd: function() { Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } }); var Sortable = { SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, sortables: { }, _findRootElement: function(element) { while (element.tagName.toUpperCase() != "BODY") { if(element.id && Sortable.sortables[element.id]) return element; element = element.parentNode; } }, options: function(element) { element = Sortable._findRootElement($(element)); if(!element) return; return Sortable.sortables[element.id]; }, destroy: function(element){ element = $(element); var s = Sortable.sortables[element.id]; if(s) { Draggables.removeObserver(s.element); s.droppables.each(function(d){ Droppables.remove(d) }); s.draggables.invoke('destroy'); delete Sortable.sortables[s.element.id]; } }, create: function(element) { element = $(element); var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' dropOnEmpty: false, tree: false, treeTag: 'ul', overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, delay: 0, hoverclass: null, ghosting: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, format: this.SERIALIZE_RULE, // these take arrays of elements or ids and can be // used for better initialization performance elements: false, handles: false, onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction }, arguments[1] || { }); // clear any old sortable with same element this.destroy(element); // build options for the draggables var options_for_draggable = { revert: true, quiet: options.quiet, scroll: options.scroll, scrollSpeed: options.scrollSpeed, scrollSensitivity: options.scrollSensitivity, delay: options.delay, ghosting: options.ghosting, constraint: options.constraint, handle: options.handle }; if(options.starteffect) options_for_draggable.starteffect = options.starteffect; if(options.reverteffect) options_for_draggable.reverteffect = options.reverteffect; else if(options.ghosting) options_for_draggable.reverteffect = function(element) { element.style.top = 0; element.style.left = 0; }; if(options.endeffect) options_for_draggable.endeffect = options.endeffect; if(options.zindex) options_for_draggable.zindex = options.zindex; // build options for the droppables var options_for_droppable = { overlap: options.overlap, containment: options.containment, tree: options.tree, hoverclass: options.hoverclass, onHover: Sortable.onHover }; var options_for_tree = { onHover: Sortable.onEmptyHover, overlap: options.overlap, containment: options.containment, hoverclass: options.hoverclass }; // fix for gecko engine Element.cleanWhitespace(element); options.draggables = []; options.droppables = []; // drop on empty handling if(options.dropOnEmpty || options.tree) { Droppables.add(element, options_for_tree); options.droppables.push(element); } (options.elements || this.findElements(element, options) || []).each( function(e,i) { var handle = options.handles ? $(options.handles[i]) : (options.handle ? $(e).select('.' + options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); if(options.tree) e.treeNode = element; options.droppables.push(e); }); if(options.tree) { (Sortable.findTreeElements(element, options) || []).each( function(e) { Droppables.add(e, options_for_tree); e.treeNode = element; options.droppables.push(e); }); } // keep reference this.sortables[element.identify()] = options; // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); }, // return all suitable-for-sortable elements in a guaranteed order findElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.tag); }, findTreeElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.treeTag); }, onHover: function(element, dropon, overlap) { if(Element.isParent(dropon, element)) return; if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { return; } else if(overlap>0.5) { Sortable.mark(dropon, 'before'); if(dropon.previousSibling != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, dropon); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } else { Sortable.mark(dropon, 'after'); var nextElement = dropon.nextSibling || null; if(nextElement != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, nextElement); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } }, onEmptyHover: function(element, dropon, overlap) { var oldParentNode = element.parentNode; var droponOptions = Sortable.options(dropon); if(!Element.isParent(dropon, element)) { var index; var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); var child = null; if(children) { var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); for (index = 0; index < children.length; index += 1) { if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { offset -= Element.offsetSize (children[index], droponOptions.overlap); } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { child = index + 1 < children.length ? children[index + 1] : null; break; } else { child = children[index]; break; } } } dropon.insertBefore(element, child); Sortable.options(oldParentNode).onChange(element); droponOptions.onChange(element); } }, unmark: function() { if(Sortable._marker) Sortable._marker.hide(); }, mark: function(dropon, position) { // mark on ghosting only var sortable = Sortable.options(dropon.parentNode); if(sortable && !sortable.ghosting) return; if(!Sortable._marker) { Sortable._marker = ($('dropmarker') || Element.extend(document.createElement('DIV'))). hide().addClassName('dropmarker').setStyle({position:'absolute'}); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); } var offsets = dropon.cumulativeOffset(); Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); if(position=='after') if(sortable.overlap == 'horizontal') Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); else Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); Sortable._marker.show(); }, _tree: function(element, options, parent) { var children = Sortable.findElements(element, options) || []; for (var i = 0; i < children.length; ++i) { var match = children[i].id.match(options.format); if (!match) continue; var child = { id: encodeURIComponent(match ? match[1] : null), element: element, parent: parent, children: [], position: parent.children.length, container: $(children[i]).down(options.treeTag) }; /* Get the element containing the children and recurse over it */ if (child.container) this._tree(child.container, options, child); parent.children.push (child); } return parent; }, tree: function(element) { element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ tag: sortableOptions.tag, treeTag: sortableOptions.treeTag, only: sortableOptions.only, name: element.id, format: sortableOptions.format }, arguments[1] || { }); var root = { id: null, parent: null, children: [], container: element, position: 0 }; return Sortable._tree(element, options, root); }, /* Construct a [i] index for a particular node */ _constructIndex: function(node) { var index = ''; do { if (node.id) index = '[' + node.position + ']' + index; } while ((node = node.parent) != null); return index; }, sequence: function(element) { element = $(element); var options = Object.extend(this.options(element), arguments[1] || { }); return $(this.findElements(element, options) || []).map( function(item) { return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; }); }, setSequence: function(element, new_sequence) { element = $(element); var options = Object.extend(this.options(element), arguments[2] || { }); var nodeMap = { }; this.findElements(element, options).each( function(n) { if (n.id.match(options.format)) nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; n.parentNode.removeChild(n); }); new_sequence.each(function(ident) { var n = nodeMap[ident]; if (n) { n[1].appendChild(n[0]); delete nodeMap[ident]; } }); }, serialize: function(element) { element = $(element); var options = Object.extend(Sortable.options(element), arguments[1] || { }); var name = encodeURIComponent( (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); if (options.tree) { return Sortable.tree(element, arguments[1]).children.map( function (item) { return [name + Sortable._constructIndex(item) + "[id]=" + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); }).flatten().join('&'); } else { return Sortable.sequence(element, arguments[1]).map( function(item) { return name + "[]=" + encodeURIComponent(item); }).join('&'); } } }; // Returns true if child is contained within element Element.isParent = function(child, element) { if (!child.parentNode || child == element) return false; if (child.parentNode == element) return true; return Element.isParent(child.parentNode, element); }; Element.findChildren = function(element, only, recursive, tagName) { if(!element.hasChildNodes()) return null; tagName = tagName.toUpperCase(); if(only) only = [only].flatten(); var elements = []; $A(element.childNodes).each( function(e) { if(e.tagName && e.tagName.toUpperCase()==tagName && (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) elements.push(e); if(recursive) { var grandchildren = Element.findChildren(e, only, recursive, tagName); if(grandchildren) elements.push(grandchildren); } }); return (elements.length>0 ? elements.flatten() : []); }; Element.offsetSize = function (element, type) { return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; }; ================================================ FILE: test/apps/rails3/public/javascripts/effects.js ================================================ // script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) // Martin Bialasinki // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // converts rgb() and #xxx to #xxxxxx format, // returns self (or first argument) if not convertable String.prototype.parseColor = function() { var color = '#'; if (this.slice(0,4) == 'rgb(') { var cols = this.slice(4,this.length-1).split(','); var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); } else { if (this.slice(0,1) == '#') { if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); if (this.length==7) color = this.toLowerCase(); } } return (color.length==7 ? color : (arguments[0] || this)); }; /*--------------------------------------------------------------------------*/ Element.collectTextNodes = function(element) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); }).flatten().join(''); }; Element.collectTextNodesIgnoreClass = function(element, className) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? Element.collectTextNodesIgnoreClass(node, className) : '')); }).flatten().join(''); }; Element.setContentZoom = function(element, percent) { element = $(element); element.setStyle({fontSize: (percent/100) + 'em'}); if (Prototype.Browser.WebKit) window.scrollBy(0,0); return element; }; Element.getInlineOpacity = function(element){ return $(element).style.opacity || ''; }; Element.forceRerendering = function(element) { try { element = $(element); var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch(e) { } }; /*--------------------------------------------------------------------------*/ var Effect = { _elementDoesNotExistError: { name: 'ElementDoesNotExistError', message: 'The specified DOM element does not exist, but is required for this effect to operate' }, Transitions: { linear: Prototype.K, sinoidal: function(pos) { return (-Math.cos(pos*Math.PI)/2) + .5; }, reverse: function(pos) { return 1-pos; }, flicker: function(pos) { var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; return pos > 1 ? 1 : pos; }, wobble: function(pos) { return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; }, pulse: function(pos, pulses) { return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; }, spring: function(pos) { return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); }, none: function(pos) { return 0; }, full: function(pos) { return 1; } }, DefaultOptions: { duration: 1.0, // seconds fps: 100, // 100= assume 66fps max. sync: false, // true for combining from: 0.0, to: 1.0, delay: 0.0, queue: 'parallel' }, tagifyText: function(element) { var tagifyStyle = 'position:relative'; if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; element = $(element); $A(element.childNodes).each( function(child) { if (child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( new Element('span', {style: tagifyStyle}).update( character == ' ' ? String.fromCharCode(160) : character), child); }); Element.remove(child); } }); }, multiple: function(element, effect) { var elements; if (((typeof element == 'object') || Object.isFunction(element)) && (element.length)) elements = element; else elements = $(element).childNodes; var options = Object.extend({ speed: 0.1, delay: 0.0 }, arguments[2] || { }); var masterDelay = options.delay; $A(elements).each( function(element, index) { new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); }); }, PAIRS: { 'slide': ['SlideDown','SlideUp'], 'blind': ['BlindDown','BlindUp'], 'appear': ['Appear','Fade'] }, toggle: function(element, effect, options) { element = $(element); effect = (effect || 'appear').toLowerCase(); return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ queue: { position:'end', scope:(element.id || 'global'), limit: 1 } }, options || {})); } }; Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; /* ------------- core effects ------------- */ Effect.ScopedQueue = Class.create(Enumerable, { initialize: function() { this.effects = []; this.interval = null; }, _each: function(iterator) { this.effects._each(iterator); }, add: function(effect) { var timestamp = new Date().getTime(); var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position; switch(position) { case 'front': // move unstarted effects after this effect this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { e.startOn += effect.finishOn; e.finishOn += effect.finishOn; }); break; case 'with-last': timestamp = this.effects.pluck('startOn').max() || timestamp; break; case 'end': // start effect after last queued effect has finished timestamp = this.effects.pluck('finishOn').max() || timestamp; break; } effect.startOn += timestamp; effect.finishOn += timestamp; if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) this.effects.push(effect); if (!this.interval) this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); if (this.effects.length == 0) { clearInterval(this.interval); this.interval = null; } }, loop: function() { var timePos = new Date().getTime(); for(var i=0, len=this.effects.length;i= this.startOn) { if (timePos >= this.finishOn) { this.render(1.0); this.cancel(); this.event('beforeFinish'); if (this.finish) this.finish(); this.event('afterFinish'); return; } var pos = (timePos - this.startOn) / this.totalTime, frame = (pos * this.totalFrames).round(); if (frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, cancel: function() { if (!this.options.sync) Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); if (this.options[eventName]) this.options[eventName](this); }, inspect: function() { var data = $H(); for(property in this) if (!Object.isFunction(this[property])) data.set(property, this[property]); return '#'; } }); Effect.Parallel = Class.create(Effect.Base, { initialize: function(effects) { this.effects = effects || []; this.start(arguments[1]); }, update: function(position) { this.effects.invoke('render', position); }, finish: function(position) { this.effects.each( function(effect) { effect.render(1.0); effect.cancel(); effect.event('beforeFinish'); if (effect.finish) effect.finish(position); effect.event('afterFinish'); }); } }); Effect.Tween = Class.create(Effect.Base, { initialize: function(object, from, to) { object = Object.isString(object) ? $(object) : object; var args = $A(arguments), method = args.last(), options = args.length == 5 ? args[3] : null; this.method = Object.isFunction(method) ? method.bind(object) : Object.isFunction(object[method]) ? object[method].bind(object) : function(value) { object[method] = value }; this.start(Object.extend({ from: from, to: to }, options || { })); }, update: function(position) { this.method(position); } }); Effect.Event = Class.create(Effect.Base, { initialize: function() { this.start(Object.extend({ duration: 0 }, arguments[0] || { })); }, update: Prototype.emptyFunction }); Effect.Opacity = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); var options = Object.extend({ from: this.element.getOpacity() || 0.0, to: 1.0 }, arguments[1] || { }); this.start(options); }, update: function(position) { this.element.setOpacity(position); } }); Effect.Move = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ x: 0, y: 0, mode: 'relative' }, arguments[1] || { }); this.start(options); }, setup: function() { this.element.makePositioned(); this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); this.originalTop = parseFloat(this.element.getStyle('top') || '0'); if (this.options.mode == 'absolute') { this.options.x = this.options.x - this.originalLeft; this.options.y = this.options.y - this.originalTop; } }, update: function(position) { this.element.setStyle({ left: (this.options.x * position + this.originalLeft).round() + 'px', top: (this.options.y * position + this.originalTop).round() + 'px' }); } }); // for backwards compatibility Effect.MoveBy = function(element, toTop, toLeft) { return new Effect.Move(element, Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); }; Effect.Scale = Class.create(Effect.Base, { initialize: function(element, percent) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, scaleMode: 'box', // 'box' or 'contents' or { } with provided values scaleFrom: 100.0, scaleTo: percent }, arguments[2] || { }); this.start(options); }, setup: function() { this.restoreAfterFinish = this.options.restoreAfterFinish || false; this.elementPositioning = this.element.getStyle('position'); this.originalStyle = { }; ['top','left','width','height','fontSize'].each( function(k) { this.originalStyle[k] = this.element.style[k]; }.bind(this)); this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; var fontSize = this.element.getStyle('font-size') || '100%'; ['em','px','%','pt'].each( function(fontSizeType) { if (fontSize.indexOf(fontSizeType)>0) { this.fontSize = parseFloat(fontSize); this.fontSizeType = fontSizeType; } }.bind(this)); this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; this.dims = null; if (this.options.scaleMode=='box') this.dims = [this.element.offsetHeight, this.element.offsetWidth]; if (/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; if (!this.dims) this.dims = [this.options.scaleMode.originalHeight, this.options.scaleMode.originalWidth]; }, update: function(position) { var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); if (this.options.scaleContent && this.fontSize) this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { var d = { }; if (this.options.scaleX) d.width = width.round() + 'px'; if (this.options.scaleY) d.height = height.round() + 'px'; if (this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; if (this.elementPositioning == 'absolute') { if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; } else { if (this.options.scaleY) d.top = -topd + 'px'; if (this.options.scaleX) d.left = -leftd + 'px'; } } this.element.setStyle(d); } }); Effect.Highlight = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow if (this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect this.oldStyle = { }; if (!this.options.keepBackgroundImage) { this.oldStyle.backgroundImage = this.element.getStyle('background-image'); this.element.setStyle({backgroundImage: 'none'}); } if (!this.options.endcolor) this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); if (!this.options.restorecolor) this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); }, update: function(position) { this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); }, finish: function() { this.element.setStyle(Object.extend(this.oldStyle, { backgroundColor: this.options.restorecolor })); } }); Effect.ScrollTo = function(element) { var options = arguments[1] || { }, scrollOffsets = document.viewport.getScrollOffsets(), elementOffsets = $(element).cumulativeOffset(); if (options.offset) elementOffsets[1] += options.offset; return new Effect.Tween(null, scrollOffsets.top, elementOffsets[1], options, function(p){ scrollTo(scrollOffsets.left, p.round()); } ); }; /* ------------- combination effects ------------- */ Effect.Fade = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ from: element.getOpacity() || 1.0, to: 0.0, afterFinishInternal: function(effect) { if (effect.options.to!=0) return; effect.element.hide().setStyle({opacity: oldOpacity}); } }, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Appear = function(element) { element = $(element); var options = Object.extend({ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), to: 1.0, // force Safari to render floated elements properly afterFinishInternal: function(effect) { effect.element.forceRerendering(); }, beforeSetup: function(effect) { effect.element.setOpacity(effect.options.from).show(); }}, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Puff = function(element) { element = $(element); var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position'), top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; return new Effect.Parallel( [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], Object.extend({ duration: 1.0, beforeSetupInternal: function(effect) { Position.absolutize(effect.effects[0].element); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().setStyle(oldStyle); } }, arguments[1] || { }) ); }; Effect.BlindUp = function(element) { element = $(element); element.makeClipping(); return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, restoreAfterFinish: true, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }, arguments[1] || { }) ); }; Effect.BlindDown = function(element) { element = $(element); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: 0, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.element.undoClipping(); } }, arguments[1] || { })); }; Effect.SwitchOff = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); return new Effect.Appear(element, Object.extend({ duration: 0.4, from: 0, transition: Effect.Transitions.flicker, afterFinishInternal: function(effect) { new Effect.Scale(effect.element, 1, { duration: 0.3, scaleFromCenter: true, scaleX: false, scaleContent: false, restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); } }); } }, arguments[1] || { })); }; Effect.DropOut = function(element) { element = $(element); var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left'), opacity: element.getInlineOpacity() }; return new Effect.Parallel( [ new Effect.Move(element, {x: 0, y: 100, sync: true }), new Effect.Opacity(element, { sync: true, to: 0.0 }) ], Object.extend( { duration: 0.5, beforeSetup: function(effect) { effect.effects[0].element.makePositioned(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); } }, arguments[1] || { })); }; Effect.Shake = function(element) { element = $(element); var options = Object.extend({ distance: 20, duration: 0.5 }, arguments[1] || {}); var distance = parseFloat(options.distance); var split = parseFloat(options.duration) / 10.0; var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left') }; return new Effect.Move(element, { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { effect.element.undoPositioned().setStyle(oldStyle); }}); }}); }}); }}); }}); }}); }; Effect.SlideDown = function(element) { element = $(element).cleanWhitespace(); // SlideDown need to have the content of the element wrapped in a container element with fixed height! var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: window.opera ? 0 : 1, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; Effect.SlideUp = function(element) { element = $(element).cleanWhitespace(); var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, window.opera ? 0 : 1, Object.extend({ scaleContent: false, scaleX: false, scaleMode: 'box', scaleFrom: 100, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; // Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { return new Effect.Scale(element, window.opera ? 1 : 0, { restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }); }; Effect.Grow = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.full }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var initialMoveX, initialMoveY; var moveX, moveY; switch (options.direction) { case 'top-left': initialMoveX = initialMoveY = moveX = moveY = 0; break; case 'top-right': initialMoveX = dims.width; initialMoveY = moveY = 0; moveX = -dims.width; break; case 'bottom-left': initialMoveX = moveX = 0; initialMoveY = dims.height; moveY = -dims.height; break; case 'bottom-right': initialMoveX = dims.width; initialMoveY = dims.height; moveX = -dims.width; moveY = -dims.height; break; case 'center': initialMoveX = dims.width / 2; initialMoveY = dims.height / 2; moveX = -dims.width / 2; moveY = -dims.height / 2; break; } return new Effect.Move(element, { x: initialMoveX, y: initialMoveY, duration: 0.01, beforeSetup: function(effect) { effect.element.hide().makeClipping().makePositioned(); }, afterFinishInternal: function(effect) { new Effect.Parallel( [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), new Effect.Scale(effect.element, 100, { scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) ], Object.extend({ beforeSetup: function(effect) { effect.effects[0].element.setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); } }); }; Effect.Shrink = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.none }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var moveX, moveY; switch (options.direction) { case 'top-left': moveX = moveY = 0; break; case 'top-right': moveX = dims.width; moveY = 0; break; case 'bottom-left': moveX = 0; moveY = dims.height; break; case 'bottom-right': moveX = dims.width; moveY = dims.height; break; case 'center': moveX = dims.width / 2; moveY = dims.height / 2; break; } return new Effect.Parallel( [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) ], Object.extend({ beforeStartInternal: function(effect) { effect.effects[0].element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); }; Effect.Pulsate = function(element) { element = $(element); var options = arguments[1] || { }, oldOpacity = element.getInlineOpacity(), transition = options.transition || Effect.Transitions.linear, reverser = function(pos){ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); }; return new Effect.Opacity(element, Object.extend(Object.extend({ duration: 2.0, from: 0, afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); }; Effect.Fold = function(element) { element = $(element); var oldStyle = { top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; element.makeClipping(); return new Effect.Scale(element, 5, Object.extend({ scaleContent: false, scaleX: false, afterFinishInternal: function(effect) { new Effect.Scale(element, 1, { scaleContent: false, scaleY: false, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().setStyle(oldStyle); } }); }}, arguments[1] || { })); }; Effect.Morph = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ style: { } }, arguments[1] || { }); if (!Object.isString(options.style)) this.style = $H(options.style); else { if (options.style.include(':')) this.style = options.style.parseStyle(); else { this.element.addClassName(options.style); this.style = $H(this.element.getStyles()); this.element.removeClassName(options.style); var css = this.element.getStyles(); this.style = this.style.reject(function(style) { return style.value == css[style.key]; }); options.afterFinishInternal = function(effect) { effect.element.addClassName(effect.options.style); effect.transforms.each(function(transform) { effect.element.style[transform.style] = ''; }); }; } } this.start(options); }, setup: function(){ function parseColor(color){ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; color = color.parseColor(); return $R(0,2).map(function(i){ return parseInt( color.slice(i*2+1,i*2+3), 16 ); }); } this.transforms = this.style.map(function(pair){ var property = pair[0], value = pair[1], unit = null; if (value.parseColor('#zzzzzz') != '#zzzzzz') { value = value.parseColor(); unit = 'color'; } else if (property == 'opacity') { value = parseFloat(value); if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); } else if (Element.CSS_LENGTH.test(value)) { var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); value = parseFloat(components[1]); unit = (components.length == 3) ? components[2] : null; } var originalValue = this.element.getStyle(property); return { style: property.camelize(), originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), targetValue: unit=='color' ? parseColor(value) : value, unit: unit }; }.bind(this)).reject(function(transform){ return ( (transform.originalValue == transform.targetValue) || ( transform.unit != 'color' && (isNaN(transform.originalValue) || isNaN(transform.targetValue)) ) ); }); }, update: function(position) { var style = { }, transform, i = this.transforms.length; while(i--) style[(transform = this.transforms[i]).style] = transform.unit=='color' ? '#'+ (Math.round(transform.originalValue[0]+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + (Math.round(transform.originalValue[1]+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + (Math.round(transform.originalValue[2]+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : (transform.originalValue + (transform.targetValue - transform.originalValue) * position).toFixed(3) + (transform.unit === null ? '' : transform.unit); this.element.setStyle(style, true); } }); Effect.Transform = Class.create({ initialize: function(tracks){ this.tracks = []; this.options = arguments[1] || { }; this.addTracks(tracks); }, addTracks: function(tracks){ tracks.each(function(track){ track = $H(track); var data = track.values().first(); this.tracks.push($H({ ids: track.keys().first(), effect: Effect.Morph, options: { style: data } })); }.bind(this)); return this; }, play: function(){ return new Effect.Parallel( this.tracks.map(function(track){ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); var elements = [$(ids) || $$(ids)].flatten(); return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); }).flatten(), this.options ); } }); Element.CSS_PROPERTIES = $w( 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 'fontSize fontWeight height left letterSpacing lineHeight ' + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 'right textIndent top width wordSpacing zIndex'); Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; String.__parseStyleElement = document.createElement('div'); String.prototype.parseStyle = function(){ var style, styleRules = $H(); if (Prototype.Browser.WebKit) style = new Element('div',{style:this}).style; else { String.__parseStyleElement.innerHTML = '
    '; style = String.__parseStyleElement.childNodes[0].style; } Element.CSS_PROPERTIES.each(function(property){ if (style[property]) styleRules.set(property, style[property]); }); if (Prototype.Browser.IE && this.include('opacity')) styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); return styleRules; }; if (document.defaultView && document.defaultView.getComputedStyle) { Element.getStyles = function(element) { var css = document.defaultView.getComputedStyle($(element), null); return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { styles[property] = css[property]; return styles; }); }; } else { Element.getStyles = function(element) { element = $(element); var css = element.currentStyle, styles; styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { results[property] = css[property]; return results; }); if (!styles.opacity) styles.opacity = element.getOpacity(); return styles; }; } Effect.Methods = { morph: function(element, style) { element = $(element); new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); return element; }, visualEffect: function(element, effect, options) { element = $(element); var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); new Effect[klass](element, options); return element; }, highlight: function(element, options) { element = $(element); new Effect.Highlight(element, options); return element; } }; $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 'pulsate shake puff squish switchOff dropOut').each( function(effect) { Effect.Methods[effect] = function(element, options){ element = $(element); Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); return element; }; } ); $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( function(f) { Effect.Methods[f] = Element[f]; } ); Element.addMethods(Effect.Methods); ================================================ FILE: test/apps/rails3/public/javascripts/prototype.js ================================================ /* Prototype JavaScript framework, version 1.7_rc2 * (c) 2005-2010 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ * *--------------------------------------------------------------------------*/ var Prototype = { Version: '1.7_rc2', Browser: (function(){ var ua = navigator.userAgent; var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; return { IE: !!window.attachEvent && !isOpera, Opera: isOpera, WebKit: ua.indexOf('AppleWebKit/') > -1, Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, MobileSafari: /Apple.*Mobile/.test(ua) } })(), BrowserFeatures: { XPath: !!document.evaluate, SelectorsAPI: !!document.querySelector, ElementExtensions: (function() { var constructor = window.Element || window.HTMLElement; return !!(constructor && constructor.prototype); })(), SpecificElementExtensions: (function() { if (typeof window.HTMLDivElement !== 'undefined') return true; var div = document.createElement('div'), form = document.createElement('form'), isSupported = false; if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { isSupported = true; } div = form = null; return isSupported; })() }, ScriptFragment: ']*>([\\S\\s]*?)<\/script>', JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, emptyFunction: function() { }, K: function(x) { return x } }; if (Prototype.Browser.MobileSafari) Prototype.BrowserFeatures.SpecificElementExtensions = false; var Abstract = { }; var Try = { these: function() { var returnValue; for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); break; } catch (e) { } } return returnValue; } }; /* Based on Alex Arnell's inheritance implementation. */ var Class = (function() { var IS_DONTENUM_BUGGY = (function(){ for (var p in { toString: 1 }) { if (p === 'toString') return false; } return true; })(); function subclass() {}; function create() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0, length = properties.length; i < length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } function addMethods(source) { var ancestor = this.superclass && this.superclass.prototype, properties = Object.keys(source); if (IS_DONTENUM_BUGGY) { if (source.toString != Object.prototype.toString) properties.push("toString"); if (source.valueOf != Object.prototype.valueOf) properties.push("valueOf"); } for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames()[0] == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments); }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } return { create: create, Methods: { addMethods: addMethods } }; })(); (function() { var _toString = Object.prototype.toString, NULL_TYPE = 'Null', UNDEFINED_TYPE = 'Undefined', BOOLEAN_TYPE = 'Boolean', NUMBER_TYPE = 'Number', STRING_TYPE = 'String', OBJECT_TYPE = 'Object', BOOLEAN_CLASS = '[object Boolean]', NUMBER_CLASS = '[object Number]', STRING_CLASS = '[object String]', ARRAY_CLASS = '[object Array]', NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && typeof JSON.stringify === 'function' && JSON.stringify(0) === '0' && typeof JSON.stringify(Prototype.K) === 'undefined'; function Type(o) { switch(o) { case null: return NULL_TYPE; case (void 0): return UNDEFINED_TYPE; } var type = typeof o; switch(type) { case 'boolean': return BOOLEAN_TYPE; case 'number': return NUMBER_TYPE; case 'string': return STRING_TYPE; } return OBJECT_TYPE; } function extend(destination, source) { for (var property in source) destination[property] = source[property]; return destination; } function inspect(object) { try { if (isUndefined(object)) return 'undefined'; if (object === null) return 'null'; return object.inspect ? object.inspect() : String(object); } catch (e) { if (e instanceof RangeError) return '...'; throw e; } } function toJSON(value) { return Str('', { '': value }, []); } function Str(key, holder, stack) { var value = holder[key], type = typeof value; if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { value = value.toJSON(key); } var _class = _toString.call(value); switch (_class) { case NUMBER_CLASS: case BOOLEAN_CLASS: case STRING_CLASS: value = value.valueOf(); } switch (value) { case null: return 'null'; case true: return 'true'; case false: return 'false'; } type = typeof value; switch (type) { case 'string': return value.inspect(true); case 'number': return isFinite(value) ? String(value) : 'null'; case 'object': for (var i = 0, length = stack.length; i < length; i++) { if (stack[i] === value) { throw new TypeError(); } } stack.push(value); var partial = []; if (_class === ARRAY_CLASS) { for (var i = 0, length = value.length; i < length; i++) { var str = Str(i, value, stack); partial.push(typeof str === 'undefined' ? 'null' : str); } partial = '[' + partial.join(',') + ']'; } else { var keys = Object.keys(value); for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i], str = Str(key, value, stack); if (typeof str !== "undefined") { partial.push(key.inspect(true)+ ':' + str); } } partial = '{' + partial.join(',') + '}'; } stack.pop(); return partial; } } function stringify(object) { return JSON.stringify(object); } function toQueryString(object) { return $H(object).toQueryString(); } function toHTML(object) { return object && object.toHTML ? object.toHTML() : String.interpret(object); } function keys(object) { if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } var results = []; for (var property in object) { if (object.hasOwnProperty(property)) { results.push(property); } } return results; } function values(object) { var results = []; for (var property in object) results.push(object[property]); return results; } function clone(object) { return extend({ }, object); } function isElement(object) { return !!(object && object.nodeType == 1); } function isArray(object) { return _toString.call(object) === ARRAY_CLASS; } var hasNativeIsArray = (typeof Array.isArray == 'function') && Array.isArray([]) && !Array.isArray({}); if (hasNativeIsArray) { isArray = Array.isArray; } function isHash(object) { return object instanceof Hash; } function isFunction(object) { return typeof object === "function"; } function isString(object) { return _toString.call(object) === STRING_CLASS; } function isNumber(object) { return _toString.call(object) === NUMBER_CLASS; } function isUndefined(object) { return typeof object === "undefined"; } extend(Object, { extend: extend, inspect: inspect, toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON, toQueryString: toQueryString, toHTML: toHTML, keys: Object.keys || keys, values: values, clone: clone, isElement: isElement, isArray: isArray, isHash: isHash, isFunction: isFunction, isString: isString, isNumber: isNumber, isUndefined: isUndefined }); })(); Object.extend(Function.prototype, (function() { var slice = Array.prototype.slice; function update(array, args) { var arrayLength = array.length, length = args.length; while (length--) array[arrayLength + length] = args[length]; return array; } function merge(array, args) { array = slice.call(array, 0); return update(array, args); } function argumentNames() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') .replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; } function bind(context) { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = slice.call(arguments, 1); return function() { var a = merge(args, arguments); return __method.apply(context, a); } } function bindAsEventListener(context) { var __method = this, args = slice.call(arguments, 1); return function(event) { var a = update([event || window.event], args); return __method.apply(context, a); } } function curry() { if (!arguments.length) return this; var __method = this, args = slice.call(arguments, 0); return function() { var a = merge(args, arguments); return __method.apply(this, a); } } function delay(timeout) { var __method = this, args = slice.call(arguments, 1); timeout = timeout * 1000; return window.setTimeout(function() { return __method.apply(__method, args); }, timeout); } function defer() { var args = update([0.01], arguments); return this.delay.apply(this, args); } function wrap(wrapper) { var __method = this; return function() { var a = update([__method.bind(this)], arguments); return wrapper.apply(this, a); } } function methodize() { if (this._methodized) return this._methodized; var __method = this; return this._methodized = function() { var a = update([this], arguments); return __method.apply(null, a); }; } return { argumentNames: argumentNames, bind: bind, bindAsEventListener: bindAsEventListener, curry: curry, delay: delay, defer: defer, wrap: wrap, methodize: methodize } })()); (function(proto) { function toISOString() { return this.getUTCFullYear() + '-' + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + this.getUTCDate().toPaddedString(2) + 'T' + this.getUTCHours().toPaddedString(2) + ':' + this.getUTCMinutes().toPaddedString(2) + ':' + this.getUTCSeconds().toPaddedString(2) + 'Z'; } function toJSON() { return this.toISOString(); } if (!proto.toISOString) proto.toISOString = toISOString; if (!proto.toJSON) proto.toJSON = toJSON; })(Date.prototype); RegExp.prototype.match = RegExp.prototype.test; RegExp.escape = function(str) { return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }; var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; this.currentlyExecuting = false; this.registerCallback(); }, registerCallback: function() { this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, execute: function() { this.callback(this); }, stop: function() { if (!this.timer) return; clearInterval(this.timer); this.timer = null; }, onTimerEvent: function() { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; this.execute(); this.currentlyExecuting = false; } catch(e) { this.currentlyExecuting = false; throw e; } } } }); Object.extend(String, { interpret: function(value) { return value == null ? '' : String(value); }, specialChar: { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\\': '\\\\' } }); Object.extend(String.prototype, (function() { var NATIVE_JSON_PARSE_SUPPORT = window.JSON && typeof JSON.parse === 'function' && JSON.parse('{"test": true}').test; function prepareReplacement(replacement) { if (Object.isFunction(replacement)) return replacement; var template = new Template(replacement); return function(match) { return template.evaluate(match) }; } function gsub(pattern, replacement) { var result = '', source = this, match; replacement = prepareReplacement(replacement); if (Object.isString(pattern)) pattern = RegExp.escape(pattern); if (!(pattern.length || pattern.source)) { replacement = replacement(''); return replacement + source.split('').join(replacement) + replacement; } while (source.length > 0) { if (match = source.match(pattern)) { result += source.slice(0, match.index); result += String.interpret(replacement(match)); source = source.slice(match.index + match[0].length); } else { result += source, source = ''; } } return result; } function sub(pattern, replacement, count) { replacement = prepareReplacement(replacement); count = Object.isUndefined(count) ? 1 : count; return this.gsub(pattern, function(match) { if (--count < 0) return match[0]; return replacement(match); }); } function scan(pattern, iterator) { this.gsub(pattern, iterator); return String(this); } function truncate(length, truncation) { length = length || 30; truncation = Object.isUndefined(truncation) ? '...' : truncation; return this.length > length ? this.slice(0, length - truncation.length) + truncation : String(this); } function strip() { return this.replace(/^\s+/, '').replace(/\s+$/, ''); } function stripTags() { return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, ''); } function stripScripts() { return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); } function extractScripts() { var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), matchOne = new RegExp(Prototype.ScriptFragment, 'im'); return (this.match(matchAll) || []).map(function(scriptTag) { return (scriptTag.match(matchOne) || ['', ''])[1]; }); } function evalScripts() { return this.extractScripts().map(function(script) { return eval(script) }); } function escapeHTML() { return this.replace(/&/g,'&').replace(//g,'>'); } function unescapeHTML() { return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); } function toQueryParams(separator) { var match = this.strip().match(/([^?#]*)(#.*)?$/); if (!match) return { }; return match[1].split(separator || '&').inject({ }, function(hash, pair) { if ((pair = pair.split('='))[0]) { var key = decodeURIComponent(pair.shift()), value = pair.length > 1 ? pair.join('=') : pair[0]; if (value != undefined) value = decodeURIComponent(value); if (key in hash) { if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; hash[key].push(value); } else hash[key] = value; } return hash; }); } function toArray() { return this.split(''); } function succ() { return this.slice(0, this.length - 1) + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); } function times(count) { return count < 1 ? '' : new Array(count + 1).join(this); } function camelize() { return this.replace(/-+(.)?/g, function(match, chr) { return chr ? chr.toUpperCase() : ''; }); } function capitalize() { return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); } function underscore() { return this.replace(/::/g, '/') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') .replace(/([a-z\d])([A-Z])/g, '$1_$2') .replace(/-/g, '_') .toLowerCase(); } function dasherize() { return this.replace(/_/g, '-'); } function inspect(useDoubleQuotes) { var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) { if (character in String.specialChar) { return String.specialChar[character]; } return '\\u00' + character.charCodeAt().toPaddedString(2, 16); }); if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; return "'" + escapedString.replace(/'/g, '\\\'') + "'"; } function unfilterJSON(filter) { return this.replace(filter || Prototype.JSONFilter, '$1'); } function isJSON() { var str = this; if (str.blank()) return false; str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); return (/^[\],:{}\s]*$/).test(str); } function evalJSON(sanitize) { var json = this.unfilterJSON(), cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; if (cx.test(json)) { json = json.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } try { if (!sanitize || json.isJSON()) return eval('(' + json + ')'); } catch (e) { } throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); } function parseJSON() { var json = this.unfilterJSON(); return JSON.parse(json); } function include(pattern) { return this.indexOf(pattern) > -1; } function startsWith(pattern) { return this.lastIndexOf(pattern, 0) === 0; } function endsWith(pattern) { var d = this.length - pattern.length; return d >= 0 && this.indexOf(pattern, d) === d; } function empty() { return this == ''; } function blank() { return /^\s*$/.test(this); } function interpolate(object, pattern) { return new Template(this, pattern).evaluate(object); } return { gsub: gsub, sub: sub, scan: scan, truncate: truncate, strip: String.prototype.trim || strip, stripTags: stripTags, stripScripts: stripScripts, extractScripts: extractScripts, evalScripts: evalScripts, escapeHTML: escapeHTML, unescapeHTML: unescapeHTML, toQueryParams: toQueryParams, parseQuery: toQueryParams, toArray: toArray, succ: succ, times: times, camelize: camelize, capitalize: capitalize, underscore: underscore, dasherize: dasherize, inspect: inspect, unfilterJSON: unfilterJSON, isJSON: isJSON, evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON, include: include, startsWith: startsWith, endsWith: endsWith, empty: empty, blank: blank, interpolate: interpolate }; })()); var Template = Class.create({ initialize: function(template, pattern) { this.template = template.toString(); this.pattern = pattern || Template.Pattern; }, evaluate: function(object) { if (object && Object.isFunction(object.toTemplateReplacements)) object = object.toTemplateReplacements(); return this.template.gsub(this.pattern, function(match) { if (object == null) return (match[1] + ''); var before = match[1] || ''; if (before == '\\') return match[2]; var ctx = object, expr = match[3], pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; match = pattern.exec(expr); if (match == null) return before; while (match != null) { var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1]; ctx = ctx[comp]; if (null == ctx || '' == match[3]) break; expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); match = pattern.exec(expr); } return before + String.interpret(ctx); }); } }); Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; var $break = { }; var Enumerable = (function() { function each(iterator, context) { var index = 0; try { this._each(function(value) { iterator.call(context, value, index++); }); } catch (e) { if (e != $break) throw e; } return this; } function eachSlice(number, iterator, context) { var index = -number, slices = [], array = this.toArray(); if (number < 1) return array; while ((index += number) < array.length) slices.push(array.slice(index, index+number)); return slices.collect(iterator, context); } function all(iterator, context) { iterator = iterator || Prototype.K; var result = true; this.each(function(value, index) { result = result && !!iterator.call(context, value, index); if (!result) throw $break; }); return result; } function any(iterator, context) { iterator = iterator || Prototype.K; var result = false; this.each(function(value, index) { if (result = !!iterator.call(context, value, index)) throw $break; }); return result; } function collect(iterator, context) { iterator = iterator || Prototype.K; var results = []; this.each(function(value, index) { results.push(iterator.call(context, value, index)); }); return results; } function detect(iterator, context) { var result; this.each(function(value, index) { if (iterator.call(context, value, index)) { result = value; throw $break; } }); return result; } function findAll(iterator, context) { var results = []; this.each(function(value, index) { if (iterator.call(context, value, index)) results.push(value); }); return results; } function grep(filter, iterator, context) { iterator = iterator || Prototype.K; var results = []; if (Object.isString(filter)) filter = new RegExp(RegExp.escape(filter)); this.each(function(value, index) { if (filter.match(value)) results.push(iterator.call(context, value, index)); }); return results; } function include(object) { if (Object.isFunction(this.indexOf)) if (this.indexOf(object) != -1) return true; var found = false; this.each(function(value) { if (value == object) { found = true; throw $break; } }); return found; } function inGroupsOf(number, fillWith) { fillWith = Object.isUndefined(fillWith) ? null : fillWith; return this.eachSlice(number, function(slice) { while(slice.length < number) slice.push(fillWith); return slice; }); } function inject(memo, iterator, context) { this.each(function(value, index) { memo = iterator.call(context, memo, value, index); }); return memo; } function invoke(method) { var args = $A(arguments).slice(1); return this.map(function(value) { return value[method].apply(value, args); }); } function max(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value >= result) result = value; }); return result; } function min(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value < result) result = value; }); return result; } function partition(iterator, context) { iterator = iterator || Prototype.K; var trues = [], falses = []; this.each(function(value, index) { (iterator.call(context, value, index) ? trues : falses).push(value); }); return [trues, falses]; } function pluck(property) { var results = []; this.each(function(value) { results.push(value[property]); }); return results; } function reject(iterator, context) { var results = []; this.each(function(value, index) { if (!iterator.call(context, value, index)) results.push(value); }); return results; } function sortBy(iterator, context) { return this.map(function(value, index) { return { value: value, criteria: iterator.call(context, value, index) }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }).pluck('value'); } function toArray() { return this.map(); } function zip() { var iterator = Prototype.K, args = $A(arguments); if (Object.isFunction(args.last())) iterator = args.pop(); var collections = [this].concat(args).map($A); return this.map(function(value, index) { return iterator(collections.pluck(index)); }); } function size() { return this.toArray().length; } function inspect() { return '#'; } return { each: each, eachSlice: eachSlice, all: all, every: all, any: any, some: any, collect: collect, map: collect, detect: detect, findAll: findAll, select: findAll, filter: findAll, grep: grep, include: include, member: include, inGroupsOf: inGroupsOf, inject: inject, invoke: invoke, max: max, min: min, partition: partition, pluck: pluck, reject: reject, sortBy: sortBy, toArray: toArray, entries: toArray, zip: zip, size: size, inspect: inspect, find: detect }; })(); function $A(iterable) { if (!iterable) return []; if ('toArray' in Object(iterable)) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); return string ? string.split(/\s+/) : []; } Array.from = $A; (function() { var arrayProto = Array.prototype, slice = arrayProto.slice, _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available function each(iterator) { for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); } if (!_each) _each = each; function clear() { this.length = 0; return this; } function first() { return this[0]; } function last() { return this[this.length - 1]; } function compact() { return this.select(function(value) { return value != null; }); } function flatten() { return this.inject([], function(array, value) { if (Object.isArray(value)) return array.concat(value.flatten()); array.push(value); return array; }); } function without() { var values = slice.call(arguments, 0); return this.select(function(value) { return !values.include(value); }); } function reverse(inline) { return (inline === false ? this.toArray() : this)._reverse(); } function uniq(sorted) { return this.inject([], function(array, value, index) { if (0 == index || (sorted ? array.last() != value : !array.include(value))) array.push(value); return array; }); } function intersect(array) { return this.uniq().findAll(function(item) { return array.detect(function(value) { return item === value }); }); } function clone() { return slice.call(this, 0); } function size() { return this.length; } function inspect() { return '[' + this.map(Object.inspect).join(', ') + ']'; } function indexOf(item, i) { i || (i = 0); var length = this.length; if (i < 0) i = length + i; for (; i < length; i++) if (this[i] === item) return i; return -1; } function lastIndexOf(item, i) { i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; var n = this.slice(0, i).reverse().indexOf(item); return (n < 0) ? n : i - n - 1; } function concat() { var array = slice.call(this, 0), item; for (var i = 0, length = arguments.length; i < length; i++) { item = arguments[i]; if (Object.isArray(item) && !('callee' in item)) { for (var j = 0, arrayLength = item.length; j < arrayLength; j++) array.push(item[j]); } else { array.push(item); } } return array; } Object.extend(arrayProto, Enumerable); if (!arrayProto._reverse) arrayProto._reverse = arrayProto.reverse; Object.extend(arrayProto, { _each: _each, clear: clear, first: first, last: last, compact: compact, flatten: flatten, without: without, reverse: reverse, uniq: uniq, intersect: intersect, clone: clone, toArray: clone, size: size, inspect: inspect }); var CONCAT_ARGUMENTS_BUGGY = (function() { return [].concat(arguments)[0][0] !== 1; })(1,2) if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; })(); function $H(object) { return new Hash(object); }; var Hash = Class.create(Enumerable, (function() { function initialize(object) { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); } function _each(iterator) { for (var key in this._object) { var value = this._object[key], pair = [key, value]; pair.key = key; pair.value = value; iterator(pair); } } function set(key, value) { return this._object[key] = value; } function get(key) { if (this._object[key] !== Object.prototype[key]) return this._object[key]; } function unset(key) { var value = this._object[key]; delete this._object[key]; return value; } function toObject() { return Object.clone(this._object); } function keys() { return this.pluck('key'); } function values() { return this.pluck('value'); } function index(value) { var match = this.detect(function(pair) { return pair.value === value; }); return match && match.key; } function merge(object) { return this.clone().update(object); } function update(object) { return new Hash(object).inject(this, function(result, pair) { result.set(pair.key, pair.value); return result; }); } function toQueryPair(key, value) { if (Object.isUndefined(value)) return key; return key + '=' + encodeURIComponent(String.interpret(value)); } function toQueryString() { return this.inject([], function(results, pair) { var key = encodeURIComponent(pair.key), values = pair.value; if (values && typeof values == 'object') { if (Object.isArray(values)) return results.concat(values.map(toQueryPair.curry(key))); } else results.push(toQueryPair(key, values)); return results; }).join('&'); } function inspect() { return '#'; } function clone() { return new Hash(this); } return { initialize: initialize, _each: _each, set: set, get: get, unset: unset, toObject: toObject, toTemplateReplacements: toObject, keys: keys, values: values, index: index, merge: merge, update: update, toQueryString: toQueryString, inspect: inspect, toJSON: toObject, clone: clone }; })()); Hash.from = $H; Object.extend(Number.prototype, (function() { function toColorPart() { return this.toPaddedString(2, 16); } function succ() { return this + 1; } function times(iterator, context) { $R(0, this, true).each(iterator, context); return this; } function toPaddedString(length, radix) { var string = this.toString(radix || 10); return '0'.times(length - string.length) + string; } function abs() { return Math.abs(this); } function round() { return Math.round(this); } function ceil() { return Math.ceil(this); } function floor() { return Math.floor(this); } return { toColorPart: toColorPart, succ: succ, times: times, toPaddedString: toPaddedString, abs: abs, round: round, ceil: ceil, floor: floor }; })()); function $R(start, end, exclusive) { return new ObjectRange(start, end, exclusive); } var ObjectRange = Class.create(Enumerable, (function() { function initialize(start, end, exclusive) { this.start = start; this.end = end; this.exclusive = exclusive; } function _each(iterator) { var value = this.start; while (this.include(value)) { iterator(value); value = value.succ(); } } function include(value) { if (value < this.start) return false; if (this.exclusive) return value < this.end; return value <= this.end; } return { initialize: initialize, _each: _each, include: include }; })()); var Ajax = { getTransport: function() { return Try.these( function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, activeRequestCount: 0 }; Ajax.Responders = { responders: [], _each: function(iterator) { this.responders._each(iterator); }, register: function(responder) { if (!this.include(responder)) this.responders.push(responder); }, unregister: function(responder) { this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { if (Object.isFunction(responder[callback])) { try { responder[callback].apply(responder, [request, transport, json]); } catch (e) { } } }); } }; Object.extend(Ajax.Responders, Enumerable); Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++ }, onComplete: function() { Ajax.activeRequestCount-- } }); Ajax.Base = Class.create({ initialize: function(options) { this.options = { method: 'post', asynchronous: true, contentType: 'application/x-www-form-urlencoded', encoding: 'UTF-8', parameters: '', evalJSON: true, evalJS: true }; Object.extend(this.options, options || { }); this.options.method = this.options.method.toLowerCase(); if (Object.isString(this.options.parameters)) this.options.parameters = this.options.parameters.toQueryParams(); else if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); Ajax.Request = Class.create(Ajax.Base, { _complete: false, initialize: function($super, url, options) { $super(options); this.transport = Ajax.getTransport(); this.request(url); }, request: function(url) { this.url = url; this.method = this.options.method; var params = Object.clone(this.options.parameters); if (!['get', 'post'].include(this.method)) { params['_method'] = this.method; this.method = 'post'; } this.parameters = params; if (params = Object.toQueryString(params)) { if (this.method == 'get') this.url += (this.url.include('?') ? '&' : '?') + params; else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='; } try { var response = new Ajax.Response(this); if (this.options.onCreate) this.options.onCreate(response); Ajax.Responders.dispatch('onCreate', this, response); this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); this.body = this.method == 'post' ? (this.options.postBody || params) : null; this.transport.send(this.body); /* Force Firefox to handle ready state 4 for synchronous requests */ if (!this.options.asynchronous && this.transport.overrideMimeType) this.onStateChange(); } catch (e) { this.dispatchException(e); } }, onStateChange: function() { var readyState = this.transport.readyState; if (readyState > 1 && !((readyState == 4) && this._complete)) this.respondToReadyState(this.transport.readyState); }, setRequestHeaders: function() { var headers = { 'X-Requested-With': 'XMLHttpRequest', 'X-Prototype-Version': Prototype.Version, 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' }; if (this.method == 'post') { headers['Content-type'] = this.options.contentType + (this.options.encoding ? '; charset=' + this.options.encoding : ''); /* Force "Connection: close" for older Mozilla browsers to work * around a bug where XMLHttpRequest sends an incorrect * Content-length header. See Mozilla Bugzilla #246651. */ if (this.transport.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) headers['Connection'] = 'close'; } if (typeof this.options.requestHeaders == 'object') { var extras = this.options.requestHeaders; if (Object.isFunction(extras.push)) for (var i = 0, length = extras.length; i < length; i += 2) headers[extras[i]] = extras[i+1]; else $H(extras).each(function(pair) { headers[pair.key] = pair.value }); } for (var name in headers) this.transport.setRequestHeader(name, headers[name]); }, success: function() { var status = this.getStatus(); return !status || (status >= 200 && status < 300); }, getStatus: function() { try { return this.transport.status || 0; } catch (e) { return 0 } }, respondToReadyState: function(readyState) { var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); if (state == 'Complete') { try { this._complete = true; (this.options['on' + response.status] || this.options['on' + (this.success() ? 'Success' : 'Failure')] || Prototype.emptyFunction)(response, response.headerJSON); } catch (e) { this.dispatchException(e); } var contentType = response.getHeader('Content-type'); if (this.options.evalJS == 'force' || (this.options.evalJS && this.isSameOrigin() && contentType && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } try { (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); } catch (e) { this.dispatchException(e); } if (state == 'Complete') { this.transport.onreadystatechange = Prototype.emptyFunction; } }, isSameOrigin: function() { var m = this.url.match(/^\s*https?:\/\/[^\/]*/); return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ protocol: location.protocol, domain: document.domain, port: location.port ? ':' + location.port : '' })); }, getHeader: function(name) { try { return this.transport.getResponseHeader(name) || null; } catch (e) { return null; } }, evalResponse: function() { try { return eval((this.transport.responseText || '').unfilterJSON()); } catch (e) { this.dispatchException(e); } }, dispatchException: function(exception) { (this.options.onException || Prototype.emptyFunction)(this, exception); Ajax.Responders.dispatch('onException', this, exception); } }); Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; Ajax.Response = Class.create({ initialize: function(request){ this.request = request; var transport = this.transport = request.transport, readyState = this.readyState = transport.readyState; if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { this.status = this.getStatus(); this.statusText = this.getStatusText(); this.responseText = String.interpret(transport.responseText); this.headerJSON = this._getHeaderJSON(); } if (readyState == 4) { var xml = transport.responseXML; this.responseXML = Object.isUndefined(xml) ? null : xml; this.responseJSON = this._getResponseJSON(); } }, status: 0, statusText: '', getStatus: Ajax.Request.prototype.getStatus, getStatusText: function() { try { return this.transport.statusText || ''; } catch (e) { return '' } }, getHeader: Ajax.Request.prototype.getHeader, getAllHeaders: function() { try { return this.getAllResponseHeaders(); } catch (e) { return null } }, getResponseHeader: function(name) { return this.transport.getResponseHeader(name); }, getAllResponseHeaders: function() { return this.transport.getAllResponseHeaders(); }, _getHeaderJSON: function() { var json = this.getHeader('X-JSON'); if (!json) return null; json = decodeURIComponent(escape(json)); try { return json.evalJSON(this.request.options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } }, _getResponseJSON: function() { var options = this.request.options; if (!options.evalJSON || (options.evalJSON != 'force' && !(this.getHeader('Content-type') || '').include('application/json')) || this.responseText.blank()) return null; try { return this.responseText.evalJSON(options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } } }); Ajax.Updater = Class.create(Ajax.Request, { initialize: function($super, container, url, options) { this.container = { success: (container.success || container), failure: (container.failure || (container.success ? null : container)) }; options = Object.clone(options); var onComplete = options.onComplete; options.onComplete = (function(response, json) { this.updateContent(response.responseText); if (Object.isFunction(onComplete)) onComplete(response, json); }).bind(this); $super(url, options); }, updateContent: function(responseText) { var receiver = this.container[this.success() ? 'success' : 'failure'], options = this.options; if (!options.evalScripts) responseText = responseText.stripScripts(); if (receiver = $(receiver)) { if (options.insertion) { if (Object.isString(options.insertion)) { var insertion = { }; insertion[options.insertion] = responseText; receiver.insert(insertion); } else options.insertion(receiver, responseText); } else receiver.update(responseText); } } }); Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { initialize: function($super, container, url, options) { $super(options); this.onComplete = this.options.onComplete; this.frequency = (this.options.frequency || 2); this.decay = (this.options.decay || 1); this.updater = { }; this.container = container; this.url = url; this.start(); }, start: function() { this.options.onComplete = this.updateComplete.bind(this); this.onTimerEvent(); }, stop: function() { this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, updateComplete: function(response) { if (this.options.decay) { this.decay = (response.responseText == this.lastText ? this.decay * this.options.decay : 1); this.lastText = response.responseText; } this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); }, onTimerEvent: function() { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); function $(element) { if (arguments.length > 1) { for (var i = 0, elements = [], length = arguments.length; i < length; i++) elements.push($(arguments[i])); return elements; } if (Object.isString(element)) element = document.getElementById(element); return Element.extend(element); } if (Prototype.BrowserFeatures.XPath) { document._getElementsByXPath = function(expression, parentElement) { var results = []; var query = document.evaluate(expression, $(parentElement) || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i < length; i++) results.push(Element.extend(query.snapshotItem(i))); return results; }; } /*--------------------------------------------------------------------------*/ if (!Node) var Node = { }; if (!Node.ELEMENT_NODE) { Object.extend(Node, { ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12 }); } (function(global) { var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ try { var el = document.createElement(''); return el.tagName.toLowerCase() === 'input' && el.name === 'x'; } catch(err) { return false; } })(); var element = global.Element; global.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); }; Object.extend(global.Element, element || { }); if (element) global.Element.prototype = element.prototype; })(this); Element.idCounter = 1; Element.cache = { }; function purgeElement(element) { var uid = element._prototypeUID; if (uid) { Element.stopObserving(element); element._prototypeUID = void 0; delete Element.Storage[uid]; } } Element.Methods = { visible: function(element) { return $(element).style.display != 'none'; }, toggle: function(element) { element = $(element); Element[Element.visible(element) ? 'hide' : 'show'](element); return element; }, hide: function(element) { element = $(element); element.style.display = 'none'; return element; }, show: function(element) { element = $(element); element.style.display = ''; return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); return element; }, update: (function(){ var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){ var el = document.createElement("select"), isBuggy = true; el.innerHTML = ""; if (el.options && el.options[0]) { isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION"; } el = null; return isBuggy; })(); var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){ try { var el = document.createElement("table"); if (el && el.tBodies) { el.innerHTML = "test"; var isBuggy = typeof el.tBodies[0] == "undefined"; el = null; return isBuggy; } } catch (e) { return true; } })(); var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { var s = document.createElement("script"), isBuggy = false; try { s.appendChild(document.createTextNode("")); isBuggy = !s.firstChild || s.firstChild && s.firstChild.nodeType !== 3; } catch (e) { isBuggy = true; } s = null; return isBuggy; })(); function update(element, content) { element = $(element); var descendants = element.getElementsByTagName('*'), i = descendants.length; while (i--) purgeElement(descendants[i]); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) return element.update().insert(content); content = Object.toHTML(content); var tagName = element.tagName.toUpperCase(); if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) { element.text = content; return element; } if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { if (tagName in Element._insertionTranslations.tags) { while (element.firstChild) { element.removeChild(element.firstChild); } Element._getContentFromAnonymousElement(tagName, content.stripScripts()) .each(function(node) { element.appendChild(node) }); } else { element.innerHTML = content.stripScripts(); } } else { element.innerHTML = content.stripScripts(); } content.evalScripts.bind(content).defer(); return element; } return update; })(), replace: function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); else if (!Object.isElement(content)) { content = Object.toHTML(content); var range = element.ownerDocument.createRange(); range.selectNode(element); content.evalScripts.bind(content).defer(); content = range.createContextualFragment(content.stripScripts()); } element.parentNode.replaceChild(content, element); return element; }, insert: function(element, insertions) { element = $(element); if (Object.isString(insertions) || Object.isNumber(insertions) || Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) insertions = {bottom:insertions}; var content, insert, tagName, childNodes; for (var position in insertions) { content = insertions[position]; position = position.toLowerCase(); insert = Element._insertionTranslations[position]; if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { insert(element, content); continue; } content = Object.toHTML(content); tagName = ((position == 'before' || position == 'after') ? element.parentNode : element).tagName.toUpperCase(); childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); if (position == 'top' || position == 'after') childNodes.reverse(); childNodes.each(insert.curry(element)); content.evalScripts.bind(content).defer(); } return element; }, wrap: function(element, wrapper, attributes) { element = $(element); if (Object.isElement(wrapper)) $(wrapper).writeAttribute(attributes || { }); else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); else wrapper = new Element('div', wrapper); if (element.parentNode) element.parentNode.replaceChild(wrapper, element); wrapper.appendChild(element); return wrapper; }, inspect: function(element) { element = $(element); var result = '<' + element.tagName.toLowerCase(); $H({'id': 'id', 'className': 'class'}).each(function(pair) { var property = pair.first(), attribute = pair.last(), value = (element[property] || '').toString(); if (value) result += ' ' + attribute + '=' + value.inspect(true); }); return result + '>'; }, recursivelyCollect: function(element, property, maximumLength) { element = $(element); maximumLength = maximumLength || -1; var elements = []; while (element = element[property]) { if (element.nodeType == 1) elements.push(Element.extend(element)); if (elements.length == maximumLength) break; } return elements; }, ancestors: function(element) { return Element.recursivelyCollect(element, 'parentNode'); }, descendants: function(element) { return Element.select(element, "*"); }, firstDescendant: function(element) { element = $(element).firstChild; while (element && element.nodeType != 1) element = element.nextSibling; return $(element); }, immediateDescendants: function(element) { var results = [], child = $(element).firstChild; while (child) { if (child.nodeType === 1) { results.push(Element.extend(child)); } child = child.nextSibling; } return results; }, previousSiblings: function(element, maximumLength) { return Element.recursivelyCollect(element, 'previousSibling'); }, nextSiblings: function(element) { return Element.recursivelyCollect(element, 'nextSibling'); }, siblings: function(element) { element = $(element); return Element.previousSiblings(element).reverse() .concat(Element.nextSiblings(element)); }, match: function(element, selector) { element = $(element); if (Object.isString(selector)) return Prototype.Selector.match(element, selector); return selector.match(element); }, up: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = Element.ancestors(element); return Object.isNumber(expression) ? ancestors[expression] : Prototype.Selector.find(ancestors, expression, index); }, down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return Element.firstDescendant(element); return Object.isNumber(expression) ? Element.descendants(element)[expression] : Element.select(element, expression)[index || 0]; }, previous: function(element, expression, index) { element = $(element); if (Object.isNumber(expression)) index = expression, expression = false; if (!Object.isNumber(index)) index = 0; if (expression) { return Prototype.Selector.find(element.previousSiblings(), expression, index); } else { return element.recursivelyCollect("previousSibling", index + 1)[index]; } }, next: function(element, expression, index) { element = $(element); if (Object.isNumber(expression)) index = expression, expression = false; if (!Object.isNumber(index)) index = 0; if (expression) { return Prototype.Selector.find(element.nextSiblings(), expression, index); } else { var maximumLength = Object.isNumber(index) ? index + 1 : 1; return element.recursivelyCollect("nextSibling", index + 1)[index]; } }, select: function(element) { element = $(element); var expressions = Array.prototype.slice.call(arguments, 1).join(', '); return Prototype.Selector.select(expressions, element); }, adjacent: function(element) { element = $(element); var expressions = Array.prototype.slice.call(arguments, 1).join(', '); return Prototype.Selector.select(expressions, element.parentNode).without(element); }, identify: function(element) { element = $(element); var id = Element.readAttribute(element, 'id'); if (id) return id; do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id)); Element.writeAttribute(element, 'id', id); return id; }, readAttribute: function(element, name) { element = $(element); if (Prototype.Browser.IE) { var t = Element._attributeTranslations.read; if (t.values[name]) return t.values[name](element, name); if (t.names[name]) name = t.names[name]; if (name.include(':')) { return (!element.attributes || !element.attributes[name]) ? null : element.attributes[name].value; } } return element.getAttribute(name); }, writeAttribute: function(element, name, value) { element = $(element); var attributes = { }, t = Element._attributeTranslations.write; if (typeof name == 'object') attributes = name; else attributes[name] = Object.isUndefined(value) ? true : value; for (var attr in attributes) { name = t.names[attr] || attr; value = attributes[attr]; if (t.values[attr]) name = t.values[attr](element, value); if (value === false || value === null) element.removeAttribute(name); else if (value === true) element.setAttribute(name, name); else element.setAttribute(name, value); } return element; }, getHeight: function(element) { return Element.getDimensions(element).height; }, getWidth: function(element) { return Element.getDimensions(element).width; }, classNames: function(element) { return new Element.ClassNames(element); }, hasClassName: function(element, className) { if (!(element = $(element))) return; var elementClassName = element.className; return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }, addClassName: function(element, className) { if (!(element = $(element))) return; if (!Element.hasClassName(element, className)) element.className += (element.className ? ' ' : '') + className; return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; element.className = element.className.replace( new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); return element; }, toggleClassName: function(element, className) { if (!(element = $(element))) return; return Element[Element.hasClassName(element, className) ? 'removeClassName' : 'addClassName'](element, className); }, cleanWhitespace: function(element) { element = $(element); var node = element.firstChild; while (node) { var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) element.removeChild(node); node = nextNode; } return element; }, empty: function(element) { return $(element).innerHTML.blank(); }, descendantOf: function(element, ancestor) { element = $(element), ancestor = $(ancestor); if (element.compareDocumentPosition) return (element.compareDocumentPosition(ancestor) & 8) === 8; if (ancestor.contains) return ancestor.contains(element) && ancestor !== element; while (element = element.parentNode) if (element == ancestor) return true; return false; }, scrollTo: function(element) { element = $(element); var pos = Element.cumulativeOffset(element); window.scrollTo(pos[0], pos[1]); return element; }, getStyle: function(element, style) { element = $(element); style = style == 'float' ? 'cssFloat' : style.camelize(); var value = element.style[style]; if (!value || value == 'auto') { var css = document.defaultView.getComputedStyle(element, null); value = css ? css[style] : null; } if (style == 'opacity') return value ? parseFloat(value) : 1.0; return value == 'auto' ? null : value; }, getOpacity: function(element) { return $(element).getStyle('opacity'); }, setStyle: function(element, styles) { element = $(element); var elementStyle = element.style, match; if (Object.isString(styles)) { element.style.cssText += ';' + styles; return styles.include('opacity') ? element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; } for (var property in styles) if (property == 'opacity') element.setOpacity(styles[property]); else elementStyle[(property == 'float' || property == 'cssFloat') ? (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : property] = styles[property]; return element; }, setOpacity: function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }, makePositioned: function(element) { element = $(element); var pos = Element.getStyle(element, 'position'); if (pos == 'static' || !pos) { element._madePositioned = true; element.style.position = 'relative'; if (Prototype.Browser.Opera) { element.style.top = 0; element.style.left = 0; } } return element; }, undoPositioned: function(element) { element = $(element); if (element._madePositioned) { element._madePositioned = undefined; element.style.position = element.style.top = element.style.left = element.style.bottom = element.style.right = ''; } return element; }, makeClipping: function(element) { element = $(element); if (element._overflow) return element; element._overflow = Element.getStyle(element, 'overflow') || 'auto'; if (element._overflow !== 'hidden') element.style.overflow = 'hidden'; return element; }, undoClipping: function(element) { element = $(element); if (!element._overflow) return element; element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; element._overflow = null; return element; }, cumulativeOffset: function(element) { var valueT = 0, valueL = 0; if (element.parentNode) { do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; } while (element); } return Element._returnOffset(valueL, valueT); }, positionedOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { if (element.tagName.toUpperCase() == 'BODY') break; var p = Element.getStyle(element, 'position'); if (p !== 'static') break; } } while (element); return Element._returnOffset(valueL, valueT); }, absolutize: function(element) { element = $(element); if (Element.getStyle(element, 'position') == 'absolute') return element; var offsets = Element.positionedOffset(element), top = offsets[1], left = offsets[0], width = element.clientWidth, height = element.clientHeight; element._originalLeft = left - parseFloat(element.style.left || 0); element._originalTop = top - parseFloat(element.style.top || 0); element._originalWidth = element.style.width; element._originalHeight = element.style.height; element.style.position = 'absolute'; element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.width = width + 'px'; element.style.height = height + 'px'; return element; }, relativize: function(element) { element = $(element); if (Element.getStyle(element, 'position') == 'relative') return element; element.style.position = 'relative'; var top = parseFloat(element.style.top || 0) - (element._originalTop || 0), left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.height = element._originalHeight; element.style.width = element._originalWidth; return element; }, cumulativeScrollOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.scrollTop || 0; valueL += element.scrollLeft || 0; element = element.parentNode; } while (element); return Element._returnOffset(valueL, valueT); }, getOffsetParent: function(element) { if (element.offsetParent) return $(element.offsetParent); if (element == document.body) return $(element); while ((element = element.parentNode) && element != document.body) if (Element.getStyle(element, 'position') != 'static') return $(element); return $(document.body); }, viewportOffset: function(forElement) { var valueT = 0, valueL = 0, element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if (element.offsetParent == document.body && Element.getStyle(element, 'position') == 'absolute') break; } while (element = element.offsetParent); element = forElement; do { if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } } while (element = element.parentNode); return Element._returnOffset(valueL, valueT); }, clonePosition: function(element, source) { var options = Object.extend({ setLeft: true, setTop: true, setWidth: true, setHeight: true, offsetTop: 0, offsetLeft: 0 }, arguments[2] || { }); source = $(source); var p = Element.viewportOffset(source), delta = [0, 0], parent = null; element = $(element); if (Element.getStyle(element, 'position') == 'absolute') { parent = Element.getOffsetParent(element); delta = Element.viewportOffset(parent); } if (parent == document.body) { delta[0] -= document.body.offsetLeft; delta[1] -= document.body.offsetTop; } if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; if (options.setWidth) element.style.width = source.offsetWidth + 'px'; if (options.setHeight) element.style.height = source.offsetHeight + 'px'; return element; } }; Object.extend(Element.Methods, { getElementsBySelector: Element.Methods.select, childElements: Element.Methods.immediateDescendants }); Element._attributeTranslations = { write: { names: { className: 'class', htmlFor: 'for' }, values: { } } }; if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { switch (style) { case 'left': case 'top': case 'right': case 'bottom': if (proceed(element, 'position') === 'static') return null; case 'height': case 'width': if (!Element.visible(element)) return null; var dim = parseInt(proceed(element, style), 10); if (dim !== element['offset' + style.capitalize()]) return dim + 'px'; var properties; if (style === 'height') { properties = ['border-top-width', 'padding-top', 'padding-bottom', 'border-bottom-width']; } else { properties = ['border-left-width', 'padding-left', 'padding-right', 'border-right-width']; } return properties.inject(dim, function(memo, property) { var val = proceed(element, property); return val === null ? memo : memo - parseInt(val, 10); }) + 'px'; default: return proceed(element, style); } } ); Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( function(proceed, element, attribute) { if (attribute === 'title') return element.title; return proceed(element, attribute); } ); } else if (Prototype.Browser.IE) { Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( function(proceed, element) { element = $(element); if (!element.parentNode) return $(document.body); var position = element.getStyle('position'); if (position !== 'static') return proceed(element); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); $w('positionedOffset viewportOffset').each(function(method) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); if (!element.parentNode) return Element._returnOffset(0, 0); var position = element.getStyle('position'); if (position !== 'static') return proceed(element); var offsetParent = element.getOffsetParent(); if (offsetParent && offsetParent.getStyle('position') === 'fixed') offsetParent.setStyle({ zoom: 1 }); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); }); Element.Methods.getStyle = function(element, style) { element = $(element); style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); var value = element.style[style]; if (!value && element.currentStyle) value = element.currentStyle[style]; if (style == 'opacity') { if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) if (value[1]) return parseFloat(value[1]) / 100; return 1.0; } if (value == 'auto') { if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) return element['offset' + style.capitalize()] + 'px'; return null; } return value; }; Element.Methods.setOpacity = function(element, value) { function stripAlpha(filter){ return filter.replace(/alpha\([^\)]*\)/gi,''); } element = $(element); var currentStyle = element.currentStyle; if ((currentStyle && !currentStyle.hasLayout) || (!currentStyle && element.style.zoom == 'normal')) element.style.zoom = 1; var filter = element.getStyle('filter'), style = element.style; if (value == 1 || value === '') { (filter = stripAlpha(filter)) ? style.filter = filter : style.removeAttribute('filter'); return element; } else if (value < 0.00001) value = 0; style.filter = stripAlpha(filter) + 'alpha(opacity=' + (value * 100) + ')'; return element; }; Element._attributeTranslations = (function(){ var classProp = 'className', forProp = 'for', el = document.createElement('div'); el.setAttribute(classProp, 'x'); if (el.className !== 'x') { el.setAttribute('class', 'x'); if (el.className === 'x') { classProp = 'class'; } } el = null; el = document.createElement('label'); el.setAttribute(forProp, 'x'); if (el.htmlFor !== 'x') { el.setAttribute('htmlFor', 'x'); if (el.htmlFor === 'x') { forProp = 'htmlFor'; } } el = null; return { read: { names: { 'class': classProp, 'className': classProp, 'for': forProp, 'htmlFor': forProp }, values: { _getAttr: function(element, attribute) { return element.getAttribute(attribute); }, _getAttr2: function(element, attribute) { return element.getAttribute(attribute, 2); }, _getAttrNode: function(element, attribute) { var node = element.getAttributeNode(attribute); return node ? node.value : ""; }, _getEv: (function(){ var el = document.createElement('div'), f; el.onclick = Prototype.emptyFunction; var value = el.getAttribute('onclick'); if (String(value).indexOf('{') > -1) { f = function(element, attribute) { attribute = element.getAttribute(attribute); if (!attribute) return null; attribute = attribute.toString(); attribute = attribute.split('{')[1]; attribute = attribute.split('}')[0]; return attribute.strip(); }; } else if (value === '') { f = function(element, attribute) { attribute = element.getAttribute(attribute); if (!attribute) return null; return attribute.strip(); }; } el = null; return f; })(), _flag: function(element, attribute) { return $(element).hasAttribute(attribute) ? attribute : null; }, style: function(element) { return element.style.cssText.toLowerCase(); }, title: function(element) { return element.title; } } } } })(); Element._attributeTranslations.write = { names: Object.extend({ cellpadding: 'cellPadding', cellspacing: 'cellSpacing' }, Element._attributeTranslations.read.names), values: { checked: function(element, value) { element.checked = !!value; }, style: function(element, value) { element.style.cssText = value ? value : ''; } } }; Element._attributeTranslations.has = {}; $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; Element._attributeTranslations.has[attr.toLowerCase()] = attr; }); (function(v) { Object.extend(v, { href: v._getAttr2, src: v._getAttr2, type: v._getAttr, action: v._getAttrNode, disabled: v._flag, checked: v._flag, readonly: v._flag, multiple: v._flag, onload: v._getEv, onunload: v._getEv, onclick: v._getEv, ondblclick: v._getEv, onmousedown: v._getEv, onmouseup: v._getEv, onmouseover: v._getEv, onmousemove: v._getEv, onmouseout: v._getEv, onfocus: v._getEv, onblur: v._getEv, onkeypress: v._getEv, onkeydown: v._getEv, onkeyup: v._getEv, onsubmit: v._getEv, onreset: v._getEv, onselect: v._getEv, onchange: v._getEv }); })(Element._attributeTranslations.read.values); if (Prototype.BrowserFeatures.ElementExtensions) { (function() { function _descendants(element) { var nodes = element.getElementsByTagName('*'), results = []; for (var i = 0, node; node = nodes[i]; i++) if (node.tagName !== "!") // Filter out comment nodes. results.push(node); return results; } Element.Methods.down = function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); return Object.isNumber(expression) ? _descendants(element)[expression] : Element.select(element, expression)[index || 0]; } })(); } } else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1) ? 0.999999 : (value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }; } else if (Prototype.Browser.WebKit) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; if (value == 1) if (element.tagName.toUpperCase() == 'IMG' && element.width) { element.width++; element.width--; } else try { var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch (e) { } return element; }; Element.Methods.cumulativeOffset = function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if (element.offsetParent == document.body) if (Element.getStyle(element, 'position') == 'absolute') break; element = element.offsetParent; } while (element); return Element._returnOffset(valueL, valueT); }; } if ('outerHTML' in document.documentElement) { Element.Methods.replace = function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { element.parentNode.replaceChild(content, element); return element; } content = Object.toHTML(content); var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); if (Element._insertionTranslations.tags[tagName]) { var nextSibling = element.next(), fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); parent.removeChild(element); if (nextSibling) fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); else fragments.each(function(node) { parent.appendChild(node) }); } else element.outerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }; } Element._returnOffset = function(l, t) { var result = [l, t]; result.left = l; result.top = t; return result; }; Element._getContentFromAnonymousElement = function(tagName, html) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; if (t) { div.innerHTML = t[0] + html + t[1]; for (var i = t[2]; i--; ) { div = div.firstChild; } } else { div.innerHTML = html; } return $A(div.childNodes); }; Element._insertionTranslations = { before: function(element, node) { element.parentNode.insertBefore(node, element); }, top: function(element, node) { element.insertBefore(node, element.firstChild); }, bottom: function(element, node) { element.appendChild(node); }, after: function(element, node) { element.parentNode.insertBefore(node, element.nextSibling); }, tags: { TABLE: ['', '
    ', 1], TBODY: ['', '
    ', 2], TR: ['', '
    ', 3], TD: ['
    ', '
    ', 4], SELECT: ['', 1] } }; (function() { var tags = Element._insertionTranslations.tags; Object.extend(tags, { THEAD: tags.TBODY, TFOOT: tags.TBODY, TH: tags.TD }); })(); Element.Methods.Simulated = { hasAttribute: function(element, attribute) { attribute = Element._attributeTranslations.has[attribute] || attribute; var node = $(element).getAttributeNode(attribute); return !!(node && node.specified); } }; Element.Methods.ByTag = { }; Object.extend(Element, Element.Methods); (function(div) { if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) { window.HTMLElement = { }; window.HTMLElement.prototype = div['__proto__']; Prototype.BrowserFeatures.ElementExtensions = true; } div = null; })(document.createElement('div')); Element.extend = (function() { function checkDeficiency(tagName) { if (typeof window.Element != 'undefined') { var proto = window.Element.prototype; if (proto) { var id = '_' + (Math.random()+'').slice(2), el = document.createElement(tagName); proto[id] = 'x'; var isBuggy = (el[id] !== 'x'); delete proto[id]; el = null; return isBuggy; } } return false; } function extendElementWith(element, methods) { for (var property in methods) { var value = methods[property]; if (Object.isFunction(value) && !(property in element)) element[property] = value.methodize(); } } var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object'); if (Prototype.BrowserFeatures.SpecificElementExtensions) { if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) { return function(element) { if (element && typeof element._extendedByPrototype == 'undefined') { var t = element.tagName; if (t && (/^(?:object|applet|embed)$/i.test(t))) { extendElementWith(element, Element.Methods); extendElementWith(element, Element.Methods.Simulated); extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]); } } return element; } } return Prototype.K; } var Methods = { }, ByTag = Element.Methods.ByTag; var extend = Object.extend(function(element) { if (!element || typeof element._extendedByPrototype != 'undefined' || element.nodeType != 1 || element == window) return element; var methods = Object.clone(Methods), tagName = element.tagName.toUpperCase(); if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); extendElementWith(element, methods); element._extendedByPrototype = Prototype.emptyFunction; return element; }, { refresh: function() { if (!Prototype.BrowserFeatures.ElementExtensions) { Object.extend(Methods, Element.Methods); Object.extend(Methods, Element.Methods.Simulated); } } }); extend.refresh(); return extend; })(); if (document.documentElement.hasAttribute) { Element.hasAttribute = function(element, attribute) { return element.hasAttribute(attribute); }; } else { Element.hasAttribute = Element.Methods.Simulated.hasAttribute; } Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; if (!methods) { Object.extend(Form, Form.Methods); Object.extend(Form.Element, Form.Element.Methods); Object.extend(Element.Methods.ByTag, { "FORM": Object.clone(Form.Methods), "INPUT": Object.clone(Form.Element.Methods), "SELECT": Object.clone(Form.Element.Methods), "TEXTAREA": Object.clone(Form.Element.Methods) }); } if (arguments.length == 2) { var tagName = methods; methods = arguments[1]; } if (!tagName) Object.extend(Element.Methods, methods || { }); else { if (Object.isArray(tagName)) tagName.each(extend); else extend(tagName); } function extend(tagName) { tagName = tagName.toUpperCase(); if (!Element.Methods.ByTag[tagName]) Element.Methods.ByTag[tagName] = { }; Object.extend(Element.Methods.ByTag[tagName], methods); } function copy(methods, destination, onlyIfAbsent) { onlyIfAbsent = onlyIfAbsent || false; for (var property in methods) { var value = methods[property]; if (!Object.isFunction(value)) continue; if (!onlyIfAbsent || !(property in destination)) destination[property] = value.methodize(); } } function findDOMClass(tagName) { var klass; var trans = { "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": "FrameSet", "IFRAME": "IFrame" }; if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName.capitalize() + 'Element'; if (window[klass]) return window[klass]; var element = document.createElement(tagName), proto = element['__proto__'] || element.constructor.prototype; element = null; return proto; } var elementPrototype = window.HTMLElement ? HTMLElement.prototype : Element.prototype; if (F.ElementExtensions) { copy(Element.Methods, elementPrototype); copy(Element.Methods.Simulated, elementPrototype, true); } if (F.SpecificElementExtensions) { for (var tag in Element.Methods.ByTag) { var klass = findDOMClass(tag); if (Object.isUndefined(klass)) continue; copy(T[tag], klass.prototype); } } Object.extend(Element, Element.Methods); delete Element.ByTag; if (Element.extend.refresh) Element.extend.refresh(); Element.cache = { }; }; document.viewport = { getDimensions: function() { return { width: this.getWidth(), height: this.getHeight() }; }, getScrollOffsets: function() { return Element._returnOffset( window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; (function(viewport) { var B = Prototype.Browser, doc = document, element, property = {}; function getRootElement() { if (B.WebKit && !doc.evaluate) return document; if (B.Opera && window.parseFloat(window.opera.version()) < 9.5) return document.body; return document.documentElement; } function define(D) { if (!element) element = getRootElement(); property[D] = 'client' + D; viewport['get' + D] = function() { return element[property[D]] }; return viewport['get' + D](); } viewport.getWidth = define.curry('Width'); viewport.getHeight = define.curry('Height'); })(document.viewport); Element.Storage = { UID: 1 }; Element.addMethods({ getStorage: function(element) { if (!(element = $(element))) return; var uid; if (element === window) { uid = 0; } else { if (typeof element._prototypeUID === "undefined") element._prototypeUID = Element.Storage.UID++; uid = element._prototypeUID; } if (!Element.Storage[uid]) Element.Storage[uid] = $H(); return Element.Storage[uid]; }, store: function(element, key, value) { if (!(element = $(element))) return; if (arguments.length === 2) { Element.getStorage(element).update(key); } else { Element.getStorage(element).set(key, value); } return element; }, retrieve: function(element, key, defaultValue) { if (!(element = $(element))) return; var hash = Element.getStorage(element), value = hash.get(key); if (Object.isUndefined(value)) { hash.set(key, defaultValue); value = defaultValue; } return value; }, clone: function(element, deep) { if (!(element = $(element))) return; var clone = element.cloneNode(deep); clone._prototypeUID = void 0; if (deep) { var descendants = Element.select(clone, '*'), i = descendants.length; while (i--) { descendants[i]._prototypeUID = void 0; } } return Element.extend(clone); }, purge: function(element) { if (!(element = $(element))) return; purgeElement(element); var descendants = element.getElementsByTagName('*'), i = descendants.length; while (i--) purgeElement(descendants[i]); return null; } }); (function() { function toDecimal(pctString) { var match = pctString.match(/^(\d+)%?$/i); if (!match) return null; return (Number(match[1]) / 100); } function getPixelValue(value, property) { if (Object.isElement(value)) { element = value; value = element.getStyle(property); } if (value === null) { return null; } if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { return window.parseFloat(value); } if (/\d/.test(value) && element.runtimeStyle) { var style = element.style.left, rStyle = element.runtimeStyle.left; element.runtimeStyle.left = element.currentStyle.left; element.style.left = value || 0; value = element.style.pixelLeft; element.style.left = style; element.runtimeStyle.left = rStyle; return value; } if (value.include('%')) { var decimal = toDecimal(value); var whole; if (property.include('left') || property.include('right') || property.include('width')) { whole = $(element.parentNode).measure('width'); } else if (property.include('top') || property.include('bottom') || property.include('height')) { whole = $(element.parentNode).measure('height'); } return whole * decimal; } return 0; } function toCSSPixels(number) { if (Object.isString(number) && number.endsWith('px')) { return number; } return number + 'px'; } function isDisplayed(element) { var originalElement = element; while (element && element.parentNode) { var display = element.getStyle('display'); if (display === 'none') { return false; } element = $(element.parentNode); } return true; } var hasLayout = Prototype.K; if ('currentStyle' in document.documentElement) { hasLayout = function(element) { if (!element.currentStyle.hasLayout) { element.style.zoom = 1; } return element; }; } function cssNameFor(key) { if (key.include('border')) key = key + '-width'; return key.camelize(); } Element.Layout = Class.create(Hash, { initialize: function($super, element, preCompute) { $super(); this.element = $(element); Element.Layout.PROPERTIES.each( function(property) { this._set(property, null); }, this); if (preCompute) { this._preComputing = true; this._begin(); Element.Layout.PROPERTIES.each( this._compute, this ); this._end(); this._preComputing = false; } }, _set: function(property, value) { return Hash.prototype.set.call(this, property, value); }, set: function(property, value) { throw "Properties of Element.Layout are read-only."; }, get: function($super, property) { var value = $super(property); return value === null ? this._compute(property) : value; }, _begin: function() { if (this._prepared) return; var element = this.element; if (isDisplayed(element)) { this._prepared = true; return; } var originalStyles = { position: element.style.position || '', width: element.style.width || '', visibility: element.style.visibility || '', display: element.style.display || '' }; element.store('prototype_original_styles', originalStyles); var position = element.getStyle('position'), width = element.getStyle('width'); element.setStyle({ position: 'absolute', visibility: 'hidden', display: 'block' }); var positionedWidth = element.getStyle('width'); var newWidth; if (width && (positionedWidth === width)) { newWidth = getPixelValue(width); } else if (width && (position === 'absolute' || position === 'fixed')) { newWidth = getPixelValue(width); } else { var parent = element.parentNode, pLayout = $(parent).getLayout(); newWidth = pLayout.get('width') - this.get('margin-left') - this.get('border-left') - this.get('padding-left') - this.get('padding-right') - this.get('border-right') - this.get('margin-right'); } element.setStyle({ width: newWidth + 'px' }); this._prepared = true; }, _end: function() { var element = this.element; var originalStyles = element.retrieve('prototype_original_styles'); element.store('prototype_original_styles', null); element.setStyle(originalStyles); this._prepared = false; }, _compute: function(property) { var COMPUTATIONS = Element.Layout.COMPUTATIONS; if (!(property in COMPUTATIONS)) { throw "Property not found."; } return this._set(property, COMPUTATIONS[property].call(this, this.element)); }, toObject: function() { var args = $A(arguments); var keys = (args.length === 0) ? Element.Layout.PROPERTIES : args.join(' ').split(' '); var obj = {}; keys.each( function(key) { if (!Element.Layout.PROPERTIES.include(key)) return; var value = this.get(key); if (value != null) obj[key] = value; }, this); return obj; }, toHash: function() { var obj = this.toObject.apply(this, arguments); return new Hash(obj); }, toCSS: function() { var args = $A(arguments); var keys = (args.length === 0) ? Element.Layout.PROPERTIES : args.join(' ').split(' '); var css = {}; keys.each( function(key) { if (!Element.Layout.PROPERTIES.include(key)) return; if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; var value = this.get(key); if (value != null) css[cssNameFor(key)] = value + 'px'; }, this); return css; }, inspect: function() { return "#"; } }); Object.extend(Element.Layout, { PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), COMPUTATIONS: { 'height': function(element) { if (!this._preComputing) this._begin(); var bHeight = this.get('border-box-height'); if (bHeight <= 0) return 0; var bTop = this.get('border-top'), bBottom = this.get('border-bottom'); var pTop = this.get('padding-top'), pBottom = this.get('padding-bottom'); if (!this._preComputing) this._end(); return bHeight - bTop - bBottom - pTop - pBottom; }, 'width': function(element) { if (!this._preComputing) this._begin(); var bWidth = this.get('border-box-width'); if (bWidth <= 0) return 0; var bLeft = this.get('border-left'), bRight = this.get('border-right'); var pLeft = this.get('padding-left'), pRight = this.get('padding-right'); if (!this._preComputing) this._end(); return bWidth - bLeft - bRight - pLeft - pRight; }, 'padding-box-height': function(element) { var height = this.get('height'), pTop = this.get('padding-top'), pBottom = this.get('padding-bottom'); return height + pTop + pBottom; }, 'padding-box-width': function(element) { var width = this.get('width'), pLeft = this.get('padding-left'), pRight = this.get('padding-right'); return width + pLeft + pRight; }, 'border-box-height': function(element) { return element.offsetHeight; }, 'border-box-width': function(element) { return element.offsetWidth; }, 'margin-box-height': function(element) { var bHeight = this.get('border-box-height'), mTop = this.get('margin-top'), mBottom = this.get('margin-bottom'); if (bHeight <= 0) return 0; return bHeight + mTop + mBottom; }, 'margin-box-width': function(element) { var bWidth = this.get('border-box-width'), mLeft = this.get('margin-left'), mRight = this.get('margin-right'); if (bWidth <= 0) return 0; return bWidth + mLeft + mRight; }, 'top': function(element) { var offset = element.positionedOffset(); return offset.top; }, 'bottom': function(element) { var offset = element.positionedOffset(), parent = element.getOffsetParent(), pHeight = parent.measure('height'); var mHeight = this.get('border-box-height'); return pHeight - mHeight - offset.top; }, 'left': function(element) { var offset = element.positionedOffset(); return offset.left; }, 'right': function(element) { var offset = element.positionedOffset(), parent = element.getOffsetParent(), pWidth = parent.measure('width'); var mWidth = this.get('border-box-width'); return pWidth - mWidth - offset.left; }, 'padding-top': function(element) { return getPixelValue(element, 'paddingTop'); }, 'padding-bottom': function(element) { return getPixelValue(element, 'paddingBottom'); }, 'padding-left': function(element) { return getPixelValue(element, 'paddingLeft'); }, 'padding-right': function(element) { return getPixelValue(element, 'paddingRight'); }, 'border-top': function(element) { return Object.isNumber(element.clientTop) ? element.clientTop : getPixelValue(element, 'borderTopWidth'); }, 'border-bottom': function(element) { return Object.isNumber(element.clientBottom) ? element.clientBottom : getPixelValue(element, 'borderBottomWidth'); }, 'border-left': function(element) { return Object.isNumber(element.clientLeft) ? element.clientLeft : getPixelValue(element, 'borderLeftWidth'); }, 'border-right': function(element) { return Object.isNumber(element.clientRight) ? element.clientRight : getPixelValue(element, 'borderRightWidth'); }, 'margin-top': function(element) { return getPixelValue(element, 'marginTop'); }, 'margin-bottom': function(element) { return getPixelValue(element, 'marginBottom'); }, 'margin-left': function(element) { return getPixelValue(element, 'marginLeft'); }, 'margin-right': function(element) { return getPixelValue(element, 'marginRight'); } } }); if ('getBoundingClientRect' in document.documentElement) { Object.extend(Element.Layout.COMPUTATIONS, { 'right': function(element) { var parent = hasLayout(element.getOffsetParent()); var rect = element.getBoundingClientRect(), pRect = parent.getBoundingClientRect(); return (pRect.right - rect.right).round(); }, 'bottom': function(element) { var parent = hasLayout(element.getOffsetParent()); var rect = element.getBoundingClientRect(), pRect = parent.getBoundingClientRect(); return (pRect.bottom - rect.bottom).round(); } }); } Element.Offset = Class.create({ initialize: function(left, top) { this.left = left.round(); this.top = top.round(); this[0] = this.left; this[1] = this.top; }, relativeTo: function(offset) { return new Element.Offset( this.left - offset.left, this.top - offset.top ); }, inspect: function() { return "#".interpolate(this); }, toString: function() { return "[#{left}, #{top}]".interpolate(this); }, toArray: function() { return [this.left, this.top]; } }); function getLayout(element, preCompute) { return new Element.Layout(element, preCompute); } function measure(element, property) { return $(element).getLayout().get(property); } function getDimensions(element) { var layout = $(element).getLayout(); return { width: layout.get('width'), height: layout.get('height') }; } function getOffsetParent(element) { if (isDetached(element)) return $(document.body); var isInline = (Element.getStyle(element, 'display') === 'inline'); if (!isInline && element.offsetParent) return $(element.offsetParent); if (element === document.body) return $(element); while ((element = element.parentNode) && element !== document.body) { if (Element.getStyle(element, 'position') !== 'static') { return (element.nodeName === 'HTML') ? $(document.body) : $(element); } } return $(document.body); } function cumulativeOffset(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; } while (element); return new Element.Offset(valueL, valueT); } function positionedOffset(element) { var layout = element.getLayout(); var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { if (isBody(element)) break; var p = Element.getStyle(element, 'position'); if (p !== 'static') break; } } while (element); valueL -= layout.get('margin-top'); valueT -= layout.get('margin-left'); return new Element.Offset(valueL, valueT); } function cumulativeScrollOffset(element) { var valueT = 0, valueL = 0; do { valueT += element.scrollTop || 0; valueL += element.scrollLeft || 0; element = element.parentNode; } while (element); return new Element.Offset(valueL, valueT); } function viewportOffset(forElement) { var valueT = 0, valueL = 0, docBody = document.body; var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if (element.offsetParent == docBody && Element.getStyle(element, 'position') == 'absolute') break; } while (element = element.offsetParent); element = forElement; do { if (element != docBody) { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } } while (element = element.parentNode); return new Element.Offset(valueL, valueT); } function absolutize(element) { element = $(element); if (Element.getStyle(element, 'position') === 'absolute') { return element; } var offsetParent = getOffsetParent(element); var eOffset = element.viewportOffset(), pOffset = offsetParent.viewportOffset(); var offset = eOffset.relativeTo(pOffset); var layout = element.getLayout(); element.store('prototype_absolutize_original_styles', { left: element.getStyle('left'), top: element.getStyle('top'), width: element.getStyle('width'), height: element.getStyle('height') }); element.setStyle({ position: 'absolute', top: offset.top + 'px', left: offset.left + 'px', width: layout.get('width') + 'px', height: layout.get('height') + 'px' }); return element; } function relativize(element) { element = $(element); if (Element.getStyle(element, 'position') === 'relative') { return element; } var originalStyles = element.retrieve('prototype_absolutize_original_styles'); if (originalStyles) element.setStyle(originalStyles); return element; } Element.addMethods({ getLayout: getLayout, measure: measure, getDimensions: getDimensions, getOffsetParent: getOffsetParent, cumulativeOffset: cumulativeOffset, positionedOffset: positionedOffset, cumulativeScrollOffset: cumulativeScrollOffset, viewportOffset: viewportOffset, absolutize: absolutize, relativize: relativize }); function isBody(element) { return element.nodeName.toUpperCase() === 'BODY'; } function isDetached(element) { return element !== document.body && !Element.descendantOf(element, document.body); } if ('getBoundingClientRect' in document.documentElement) { Element.addMethods({ viewportOffset: function(element) { element = $(element); if (isDetached(element)) return new Element.Offset(0, 0); var rect = element.getBoundingClientRect(), docEl = document.documentElement; return new Element.Offset(rect.left - docEl.clientLeft, rect.top - docEl.clientTop); }, positionedOffset: function(element) { element = $(element); var parent = element.getOffsetParent(); if (isDetached(element)) return new Element.Offset(0, 0); if (element.offsetParent && element.offsetParent.nodeName.toUpperCase() === 'HTML') { return positionedOffset(element); } var eOffset = element.viewportOffset(), pOffset = isBody(parent) ? viewportOffset(parent) : parent.viewportOffset(); var retOffset = eOffset.relativeTo(pOffset); var layout = element.getLayout(); var top = retOffset.top - layout.get('margin-top'); var left = retOffset.left - layout.get('margin-left'); return new Element.Offset(left, top); } }); } })(); window.$$ = function() { var expression = $A(arguments).join(', '); return Prototype.Selector.select(expression, document); }; Prototype.Selector = (function() { function select() { throw new Error('Method "Prototype.Selector.select" must be defined.'); } function match() { throw new Error('Method "Prototype.Selector.match" must be defined.'); } function find(elements, expression, index) { index = index || 0; var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; for (i = 0; i < length; i++) { if (match(elements[i], expression) && index == matchIndex++) { return Element.extend(elements[i]); } } } function extendElements(elements) { for (var i = 0, length = elements.length; i < length; i++) { Element.extend(elements[i]); } return elements; } var K = Prototype.K; return { select: select, match: match, find: find, extendElements: (Element.extend === K) ? K : extendElements, extendElement: Element.extend }; })(); Prototype._original_property = window.Sizzle; /*! * Sizzle CSS Selector Engine - v1.0 * Copyright 2009, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, done = 0, toString = Object.prototype.toString, hasDuplicate = false, baseHasDuplicate = true; [0, 0].sort(function(){ baseHasDuplicate = false; return 0; }); var Sizzle = function(selector, context, results, seed) { results = results || []; var origContext = context = context || document; if ( context.nodeType !== 1 && context.nodeType !== 9 ) { return []; } if ( !selector || typeof selector !== "string" ) { return results; } var parts = [], m, set, checkSet, check, mode, extra, prune = true, contextXML = isXML(context), soFar = selector; while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { soFar = m[3]; parts.push( m[1] ); if ( m[2] ) { extra = m[3]; break; } } if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context ); } else { set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context ); while ( parts.length ) { selector = parts.shift(); if ( Expr.relative[ selector ] ) selector += parts.shift(); set = posProcess( selector, set ); } } } else { if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { var ret = Sizzle.find( parts.shift(), context, contextXML ); context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; } if ( context ) { var ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; if ( parts.length > 0 ) { checkSet = makeArray(set); } else { prune = false; } while ( parts.length ) { var cur = parts.pop(), pop = cur; if ( !Expr.relative[ cur ] ) { cur = ""; } else { pop = parts.pop(); } if ( pop == null ) { pop = context; } Expr.relative[ cur ]( checkSet, pop, contextXML ); } } else { checkSet = parts = []; } } if ( !checkSet ) { checkSet = set; } if ( !checkSet ) { throw "Syntax error, unrecognized expression: " + (cur || selector); } if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); } else if ( context && context.nodeType === 1 ) { for ( var i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { results.push( set[i] ); } } } else { for ( var i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { results.push( set[i] ); } } } } else { makeArray( checkSet, results ); } if ( extra ) { Sizzle( extra, origContext, results, seed ); Sizzle.uniqueSort( results ); } return results; }; Sizzle.uniqueSort = function(results){ if ( sortOrder ) { hasDuplicate = baseHasDuplicate; results.sort(sortOrder); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { if ( results[i] === results[i-1] ) { results.splice(i--, 1); } } } } return results; }; Sizzle.matches = function(expr, set){ return Sizzle(expr, null, null, set); }; Sizzle.find = function(expr, context, isXML){ var set, match; if ( !expr ) { return []; } for ( var i = 0, l = Expr.order.length; i < l; i++ ) { var type = Expr.order[i], match; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { var left = match[1]; match.splice(1,1); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace(/\\/g, ""); set = Expr.find[ type ]( match, context, isXML ); if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; } } } } if ( !set ) { set = context.getElementsByTagName("*"); } return {set: set, expr: expr}; }; Sizzle.filter = function(expr, set, inplace, not){ var old = expr, result = [], curLoop = set, match, anyFound, isXMLFilter = set && set[0] && isXML(set[0]); while ( expr && set.length ) { for ( var type in Expr.filter ) { if ( (match = Expr.match[ type ].exec( expr )) != null ) { var filter = Expr.filter[ type ], found, item; anyFound = false; if ( curLoop == result ) { result = []; } if ( Expr.preFilter[ type ] ) { match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); if ( !match ) { anyFound = found = true; } else if ( match === true ) { continue; } } if ( match ) { for ( var i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); var pass = not ^ !!found; if ( inplace && found != null ) { if ( pass ) { anyFound = true; } else { curLoop[i] = false; } } else if ( pass ) { result.push( item ); anyFound = true; } } } } if ( found !== undefined ) { if ( !inplace ) { curLoop = result; } expr = expr.replace( Expr.match[ type ], "" ); if ( !anyFound ) { return []; } break; } } } if ( expr == old ) { if ( anyFound == null ) { throw "Syntax error, unrecognized expression: " + expr; } else { break; } } old = expr; } return curLoop; }; var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], match: { ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/ }, leftMatch: {}, attrMap: { "class": "className", "for": "htmlFor" }, attrHandle: { href: function(elem){ return elem.getAttribute("href"); } }, relative: { "+": function(checkSet, part, isXML){ var isPartStr = typeof part === "string", isTag = isPartStr && !/\W/.test(part), isPartStrNotTag = isPartStr && !isTag; if ( isTag && !isXML ) { part = part.toUpperCase(); } for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { if ( (elem = checkSet[i]) ) { while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ? elem || false : elem === part; } } if ( isPartStrNotTag ) { Sizzle.filter( part, checkSet, true ); } }, ">": function(checkSet, part, isXML){ var isPartStr = typeof part === "string"; if ( isPartStr && !/\W/.test(part) ) { part = isXML ? part : part.toUpperCase(); for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName === part ? parent : false; } } } else { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : elem.parentNode === part; } } if ( isPartStr ) { Sizzle.filter( part, checkSet, true ); } } }, "": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; if ( !/\W/.test(part) ) { var nodeCheck = part = isXML ? part : part.toUpperCase(); checkFn = dirNodeCheck; } checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); }, "~": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !/\W/.test(part) ) { var nodeCheck = part = isXML ? part : part.toUpperCase(); checkFn = dirNodeCheck; } checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); } }, find: { ID: function(match, context, isXML){ if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? [m] : []; } }, NAME: function(match, context, isXML){ if ( typeof context.getElementsByName !== "undefined" ) { var ret = [], results = context.getElementsByName(match[1]); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { ret.push( results[i] ); } } return ret.length === 0 ? null : ret; } }, TAG: function(match, context){ return context.getElementsByTagName(match[1]); } }, preFilter: { CLASS: function(match, curLoop, inplace, result, not, isXML){ match = " " + match[1].replace(/\\/g, "") + " "; if ( isXML ) { return match; } for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) { if ( !inplace ) result.push( elem ); } else if ( inplace ) { curLoop[i] = false; } } } return false; }, ID: function(match){ return match[1].replace(/\\/g, ""); }, TAG: function(match, curLoop){ for ( var i = 0; curLoop[i] === false; i++ ){} return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase(); }, CHILD: function(match){ if ( match[1] == "nth" ) { var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } match[0] = done++; return match; }, ATTR: function(match, curLoop, inplace, result, not, isXML){ var name = match[1].replace(/\\/g, ""); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, PSEUDO: function(match, curLoop, inplace, result, not){ if ( match[1] === "not" ) { if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); if ( !inplace ) { result.push.apply( result, ret ); } return false; } } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, POS: function(match){ match.unshift( true ); return match; } }, filters: { enabled: function(elem){ return elem.disabled === false && elem.type !== "hidden"; }, disabled: function(elem){ return elem.disabled === true; }, checked: function(elem){ return elem.checked === true; }, selected: function(elem){ elem.parentNode.selectedIndex; return elem.selected === true; }, parent: function(elem){ return !!elem.firstChild; }, empty: function(elem){ return !elem.firstChild; }, has: function(elem, i, match){ return !!Sizzle( match[3], elem ).length; }, header: function(elem){ return /h\d/i.test( elem.nodeName ); }, text: function(elem){ return "text" === elem.type; }, radio: function(elem){ return "radio" === elem.type; }, checkbox: function(elem){ return "checkbox" === elem.type; }, file: function(elem){ return "file" === elem.type; }, password: function(elem){ return "password" === elem.type; }, submit: function(elem){ return "submit" === elem.type; }, image: function(elem){ return "image" === elem.type; }, reset: function(elem){ return "reset" === elem.type; }, button: function(elem){ return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON"; }, input: function(elem){ return /input|select|textarea|button/i.test(elem.nodeName); } }, setFilters: { first: function(elem, i){ return i === 0; }, last: function(elem, i, match, array){ return i === array.length - 1; }, even: function(elem, i){ return i % 2 === 0; }, odd: function(elem, i){ return i % 2 === 1; }, lt: function(elem, i, match){ return i < match[3] - 0; }, gt: function(elem, i, match){ return i > match[3] - 0; }, nth: function(elem, i, match){ return match[3] - 0 == i; }, eq: function(elem, i, match){ return match[3] - 0 == i; } }, filter: { PSEUDO: function(elem, match, i, array){ var name = match[1], filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } else if ( name === "contains" ) { return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; for ( var i = 0, l = not.length; i < l; i++ ) { if ( not[i] === elem ) { return false; } } return true; } }, CHILD: function(elem, match){ var type = match[1], node = elem; switch (type) { case 'only': case 'first': while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) return false; } if ( type == 'first') return true; node = elem; case 'last': while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) return false; } return true; case 'nth': var first = match[2], last = match[3]; if ( first == 1 && last == 0 ) { return true; } var doneName = match[0], parent = elem.parentNode; if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { var count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } parent.sizcache = doneName; } var diff = elem.nodeIndex - last; if ( first == 0 ) { return diff == 0; } else { return ( diff % first == 0 && diff / first >= 0 ); } } }, ID: function(elem, match){ return elem.nodeType === 1 && elem.getAttribute("id") === match; }, TAG: function(elem, match){ return (match === "*" && elem.nodeType === 1) || elem.nodeName === match; }, CLASS: function(elem, match){ return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, ATTR: function(elem, match){ var name = match[1], result = Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : elem.getAttribute( name ), value = result + "", type = match[2], check = match[4]; return result == null ? type === "!=" : type === "=" ? value === check : type === "*=" ? value.indexOf(check) >= 0 : type === "~=" ? (" " + value + " ").indexOf(check) >= 0 : !check ? value && result !== false : type === "!=" ? value != check : type === "^=" ? value.indexOf(check) === 0 : type === "$=" ? value.substr(value.length - check.length) === check : type === "|=" ? value === check || value.substr(0, check.length + 1) === check + "-" : false; }, POS: function(elem, match, i, array){ var name = match[2], filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } } } }; var origPOS = Expr.match.POS; for ( var type in Expr.match ) { Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source ); } var makeArray = function(array, results) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); return results; } return array; }; try { Array.prototype.slice.call( document.documentElement.childNodes, 0 ); } catch(e){ makeArray = function(array, results) { var ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); } else { if ( typeof array.length === "number" ) { for ( var i = 0, l = array.length; i < l; i++ ) { ret.push( array[i] ); } } else { for ( var i = 0; array[i]; i++ ) { ret.push( array[i] ); } } } return ret; }; } var sortOrder; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { if ( a == b ) { hasDuplicate = true; } return 0; } var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } else if ( "sourceIndex" in document.documentElement ) { sortOrder = function( a, b ) { if ( !a.sourceIndex || !b.sourceIndex ) { if ( a == b ) { hasDuplicate = true; } return 0; } var ret = a.sourceIndex - b.sourceIndex; if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } else if ( document.createRange ) { sortOrder = function( a, b ) { if ( !a.ownerDocument || !b.ownerDocument ) { if ( a == b ) { hasDuplicate = true; } return 0; } var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); aRange.setStart(a, 0); aRange.setEnd(a, 0); bRange.setStart(b, 0); bRange.setEnd(b, 0); var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } (function(){ var form = document.createElement("div"), id = "script" + (new Date).getTime(); form.innerHTML = ""; var root = document.documentElement; root.insertBefore( form, root.firstChild ); if ( !!document.getElementById( id ) ) { Expr.find.ID = function(match, context, isXML){ if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; } }; Expr.filter.ID = function(elem, match){ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); root = form = null; // release memory in IE })(); (function(){ var div = document.createElement("div"); div.appendChild( document.createComment("") ); if ( div.getElementsByTagName("*").length > 0 ) { Expr.find.TAG = function(match, context){ var results = context.getElementsByTagName(match[1]); if ( match[1] === "*" ) { var tmp = []; for ( var i = 0; results[i]; i++ ) { if ( results[i].nodeType === 1 ) { tmp.push( results[i] ); } } results = tmp; } return results; }; } div.innerHTML = ""; if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { Expr.attrHandle.href = function(elem){ return elem.getAttribute("href", 2); }; } div = null; // release memory in IE })(); if ( document.querySelectorAll ) (function(){ var oldSizzle = Sizzle, div = document.createElement("div"); div.innerHTML = "

    "; if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { return; } Sizzle = function(query, context, extra, seed){ context = context || document; if ( !seed && context.nodeType === 9 && !isXML(context) ) { try { return makeArray( context.querySelectorAll(query), extra ); } catch(e){} } return oldSizzle(query, context, extra, seed); }; for ( var prop in oldSizzle ) { Sizzle[ prop ] = oldSizzle[ prop ]; } div = null; // release memory in IE })(); if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){ var div = document.createElement("div"); div.innerHTML = "
    "; if ( div.getElementsByClassName("e").length === 0 ) return; div.lastChild.className = "e"; if ( div.getElementsByClassName("e").length === 1 ) return; Expr.order.splice(1, 0, "CLASS"); Expr.find.CLASS = function(match, context, isXML) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; div = null; // release memory in IE })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { var sibDir = dir == "previousSibling" && !isXML; for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { if ( sibDir && elem.nodeType === 1 ){ elem.sizcache = doneName; elem.sizset = i; } elem = elem[dir]; var match = false; while ( elem ) { if ( elem.sizcache === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ elem.sizcache = doneName; elem.sizset = i; } if ( elem.nodeName === cur ) { match = elem; break; } elem = elem[dir]; } checkSet[i] = match; } } } function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { var sibDir = dir == "previousSibling" && !isXML; for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { if ( sibDir && elem.nodeType === 1 ) { elem.sizcache = doneName; elem.sizset = i; } elem = elem[dir]; var match = false; while ( elem ) { if ( elem.sizcache === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { elem.sizcache = doneName; elem.sizset = i; } if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; break; } } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { match = elem; break; } } elem = elem[dir]; } checkSet[i] = match; } } } var contains = document.compareDocumentPosition ? function(a, b){ return a.compareDocumentPosition(b) & 16; } : function(a, b){ return a !== b && (a.contains ? a.contains(b) : true); }; var isXML = function(elem){ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML"; }; var posProcess = function(selector, context){ var tmpSet = [], later = "", match, root = context.nodeType ? [context] : context; while ( (match = Expr.match.PSEUDO.exec( selector )) ) { later += match[0]; selector = selector.replace( Expr.match.PSEUDO, "" ); } selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { Sizzle( selector, root[i], tmpSet ); } return Sizzle.filter( later, tmpSet ); }; window.Sizzle = Sizzle; })(); ;(function(engine) { var extendElements = Prototype.Selector.extendElements; function select(selector, scope) { return extendElements(engine(selector, scope || document)); } function match(element, selector) { return engine.matches(selector, [element]).length == 1; } Prototype.Selector.engine = engine; Prototype.Selector.select = select; Prototype.Selector.match = match; })(Sizzle); window.Sizzle = Prototype._original_property; delete Prototype._original_property; var Form = { reset: function(form) { form = $(form); form.reset(); return form; }, serializeElements: function(elements, options) { if (typeof options != 'object') options = { hash: !!options }; else if (Object.isUndefined(options.hash)) options.hash = true; var key, value, submitted = false, submit = options.submit; var data = elements.inject({ }, function(result, element) { if (!element.disabled && element.name) { key = element.name; value = $(element).getValue(); if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && submit !== false && (!submit || key == submit) && (submitted = true)))) { if (key in result) { if (!Object.isArray(result[key])) result[key] = [result[key]]; result[key].push(value); } else result[key] = value; } } return result; }); return options.hash ? data : Object.toQueryString(data); } }; Form.Methods = { serialize: function(form, options) { return Form.serializeElements(Form.getElements(form), options); }, getElements: function(form) { var elements = $(form).getElementsByTagName('*'), element, arr = [ ], serializers = Form.Element.Serializers; for (var i = 0; element = elements[i]; i++) { arr.push(element); } return arr.inject([], function(elements, child) { if (serializers[child.tagName.toLowerCase()]) elements.push(Element.extend(child)); return elements; }) }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); if (!typeName && !name) return $A(inputs).map(Element.extend); for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; if ((typeName && input.type != typeName) || (name && input.name != name)) continue; matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { form = $(form); Form.getElements(form).invoke('disable'); return form; }, enable: function(form) { form = $(form); Form.getElements(form).invoke('enable'); return form; }, findFirstElement: function(form) { var elements = $(form).getElements().findAll(function(element) { return 'hidden' != element.type && !element.disabled; }); var firstByIndex = elements.findAll(function(element) { return element.hasAttribute('tabIndex') && element.tabIndex >= 0; }).sortBy(function(element) { return element.tabIndex }).first(); return firstByIndex ? firstByIndex : elements.find(function(element) { return /^(?:input|select|textarea)$/i.test(element.tagName); }); }, focusFirstElement: function(form) { form = $(form); form.findFirstElement().activate(); return form; }, request: function(form, options) { form = $(form), options = Object.clone(options || { }); var params = options.parameters, action = form.readAttribute('action') || ''; if (action.blank()) action = window.location.href; options.parameters = form.serialize(true); if (params) { if (Object.isString(params)) params = params.toQueryParams(); Object.extend(options.parameters, params); } if (form.hasAttribute('method') && !options.method) options.method = form.method; return new Ajax.Request(action, options); } }; /*--------------------------------------------------------------------------*/ Form.Element = { focus: function(element) { $(element).focus(); return element; }, select: function(element) { $(element).select(); return element; } }; Form.Element.Methods = { serialize: function(element) { element = $(element); if (!element.disabled && element.name) { var value = element.getValue(); if (value != undefined) { var pair = { }; pair[element.name] = value; return Object.toQueryString(pair); } } return ''; }, getValue: function(element) { element = $(element); var method = element.tagName.toLowerCase(); return Form.Element.Serializers[method](element); }, setValue: function(element, value) { element = $(element); var method = element.tagName.toLowerCase(); Form.Element.Serializers[method](element, value); return element; }, clear: function(element) { $(element).value = ''; return element; }, present: function(element) { return $(element).value != ''; }, activate: function(element) { element = $(element); try { element.focus(); if (element.select && (element.tagName.toLowerCase() != 'input' || !(/^(?:button|reset|submit)$/i.test(element.type)))) element.select(); } catch (e) { } return element; }, disable: function(element) { element = $(element); element.disabled = true; return element; }, enable: function(element) { element = $(element); element.disabled = false; return element; } }; /*--------------------------------------------------------------------------*/ var Field = Form.Element; var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ Form.Element.Serializers = { input: function(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': return Form.Element.Serializers.inputSelector(element, value); default: return Form.Element.Serializers.textarea(element, value); } }, inputSelector: function(element, value) { if (Object.isUndefined(value)) return element.checked ? element.value : null; else element.checked = !!value; }, textarea: function(element, value) { if (Object.isUndefined(value)) return element.value; else element.value = value; }, select: function(element, value) { if (Object.isUndefined(value)) return this[element.type == 'select-one' ? 'selectOne' : 'selectMany'](element); else { var opt, currentValue, single = !Object.isArray(value); for (var i = 0, length = element.length; i < length; i++) { opt = element.options[i]; currentValue = this.optionValue(opt); if (single) { if (currentValue == value) { opt.selected = true; return; } } else opt.selected = value.include(currentValue); } } }, selectOne: function(element) { var index = element.selectedIndex; return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { var values, length = element.length; if (!length) return null; for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; if (opt.selected) values.push(this.optionValue(opt)); } return values; }, optionValue: function(opt) { return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } }; /*--------------------------------------------------------------------------*/ Abstract.TimedObserver = Class.create(PeriodicalExecuter, { initialize: function($super, element, frequency, callback) { $super(callback, frequency); this.element = $(element); this.lastValue = this.getValue(); }, execute: function() { var value = this.getValue(); if (Object.isString(this.lastValue) && Object.isString(value) ? this.lastValue != value : String(this.lastValue) != String(value)) { this.callback(this.element, value); this.lastValue = value; } } }); Form.Element.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.serialize(this.element); } }); /*--------------------------------------------------------------------------*/ Abstract.EventObserver = Class.create({ initialize: function(element, callback) { this.element = $(element); this.callback = callback; this.lastValue = this.getValue(); if (this.element.tagName.toLowerCase() == 'form') this.registerFormCallbacks(); else this.registerCallback(this.element); }, onElementEvent: function() { var value = this.getValue(); if (this.lastValue != value) { this.callback(this.element, value); this.lastValue = value; } }, registerFormCallbacks: function() { Form.getElements(this.element).each(this.registerCallback, this); }, registerCallback: function(element) { if (element.type) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } } } }); Form.Element.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.serialize(this.element); } }); (function() { var Event = { KEY_BACKSPACE: 8, KEY_TAB: 9, KEY_RETURN: 13, KEY_ESC: 27, KEY_LEFT: 37, KEY_UP: 38, KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, KEY_HOME: 36, KEY_END: 35, KEY_PAGEUP: 33, KEY_PAGEDOWN: 34, KEY_INSERT: 45, cache: {} }; var docEl = document.documentElement; var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl && 'onmouseleave' in docEl; var _isButton; if (Prototype.Browser.IE) { var buttonMap = { 0: 1, 1: 4, 2: 2 }; _isButton = function(event, code) { return event.button === buttonMap[code]; }; } else if (Prototype.Browser.WebKit) { _isButton = function(event, code) { switch (code) { case 0: return event.which == 1 && !event.metaKey; case 1: return event.which == 1 && event.metaKey; default: return false; } }; } else { _isButton = function(event, code) { return event.which ? (event.which === code + 1) : (event.button === code); }; } function isLeftClick(event) { return _isButton(event, 0) } function isMiddleClick(event) { return _isButton(event, 1) } function isRightClick(event) { return _isButton(event, 2) } function element(event) { event = Event.extend(event); var node = event.target, type = event.type, currentTarget = event.currentTarget; if (currentTarget && currentTarget.tagName) { if (type === 'load' || type === 'error' || (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' && currentTarget.type === 'radio')) node = currentTarget; } if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; return Element.extend(node); } function findElement(event, expression) { var element = Event.element(event); if (!expression) return element; while (element) { if (Object.isElement(element) && Prototype.Selector.match(element, expression)) { return Element.extend(element); } element = element.parentNode; } } function pointer(event) { return { x: pointerX(event), y: pointerY(event) }; } function pointerX(event) { var docElement = document.documentElement, body = document.body || { scrollLeft: 0 }; return event.pageX || (event.clientX + (docElement.scrollLeft || body.scrollLeft) - (docElement.clientLeft || 0)); } function pointerY(event) { var docElement = document.documentElement, body = document.body || { scrollTop: 0 }; return event.pageY || (event.clientY + (docElement.scrollTop || body.scrollTop) - (docElement.clientTop || 0)); } function stop(event) { Event.extend(event); event.preventDefault(); event.stopPropagation(); event.stopped = true; } Event.Methods = { isLeftClick: isLeftClick, isMiddleClick: isMiddleClick, isRightClick: isRightClick, element: element, findElement: findElement, pointer: pointer, pointerX: pointerX, pointerY: pointerY, stop: stop }; var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); if (Prototype.Browser.IE) { function _relatedTarget(event) { var element; switch (event.type) { case 'mouseover': element = event.fromElement; break; case 'mouseout': element = event.toElement; break; default: return null; } return Element.extend(element); } Object.extend(methods, { stopPropagation: function() { this.cancelBubble = true }, preventDefault: function() { this.returnValue = false }, inspect: function() { return '[object Event]' } }); Event.extend = function(event, element) { if (!event) return false; if (event._extendedByPrototype) return event; event._extendedByPrototype = Prototype.emptyFunction; var pointer = Event.pointer(event); Object.extend(event, { target: event.srcElement || element, relatedTarget: _relatedTarget(event), pageX: pointer.x, pageY: pointer.y }); return Object.extend(event, methods); }; } else { Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; Object.extend(Event.prototype, methods); Event.extend = Prototype.K; } function _createResponder(element, eventName, handler) { var registry = Element.retrieve(element, 'prototype_event_registry'); if (Object.isUndefined(registry)) { CACHE.push(element); registry = Element.retrieve(element, 'prototype_event_registry', $H()); } var respondersForEvent = registry.get(eventName); if (Object.isUndefined(respondersForEvent)) { respondersForEvent = []; registry.set(eventName, respondersForEvent); } if (respondersForEvent.pluck('handler').include(handler)) return false; var responder; if (eventName.include(":")) { responder = function(event) { if (Object.isUndefined(event.eventName)) return false; if (event.eventName !== eventName) return false; Event.extend(event, element); handler.call(element, event); }; } else { if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED && (eventName === "mouseenter" || eventName === "mouseleave")) { if (eventName === "mouseenter" || eventName === "mouseleave") { responder = function(event) { Event.extend(event, element); var parent = event.relatedTarget; while (parent && parent !== element) { try { parent = parent.parentNode; } catch(e) { parent = element; } } if (parent === element) return; handler.call(element, event); }; } } else { responder = function(event) { Event.extend(event, element); handler.call(element, event); }; } } responder.handler = handler; respondersForEvent.push(responder); return responder; } function _destroyCache() { for (var i = 0, length = CACHE.length; i < length; i++) { Event.stopObserving(CACHE[i]); CACHE[i] = null; } } var CACHE = []; if (Prototype.Browser.IE) window.attachEvent('onunload', _destroyCache); if (Prototype.Browser.WebKit) window.addEventListener('unload', Prototype.emptyFunction, false); var _getDOMEventName = Prototype.K, translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { _getDOMEventName = function(eventName) { return (translations[eventName] || eventName); }; } function observe(element, eventName, handler) { element = $(element); var responder = _createResponder(element, eventName, handler); if (!responder) return element; if (eventName.include(':')) { if (element.addEventListener) element.addEventListener("dataavailable", responder, false); else { element.attachEvent("ondataavailable", responder); element.attachEvent("onfilterchange", responder); } } else { var actualEventName = _getDOMEventName(eventName); if (element.addEventListener) element.addEventListener(actualEventName, responder, false); else element.attachEvent("on" + actualEventName, responder); } return element; } function stopObserving(element, eventName, handler) { element = $(element); var registry = Element.retrieve(element, 'prototype_event_registry'); if (!registry) return element; if (!eventName) { registry.each( function(pair) { var eventName = pair.key; stopObserving(element, eventName); }); return element; } var responders = registry.get(eventName); if (!responders) return element; if (!handler) { responders.each(function(r) { stopObserving(element, eventName, r.handler); }); return element; } var responder = responders.find( function(r) { return r.handler === handler; }); if (!responder) return element; if (eventName.include(':')) { if (element.removeEventListener) element.removeEventListener("dataavailable", responder, false); else { element.detachEvent("ondataavailable", responder); element.detachEvent("onfilterchange", responder); } } else { var actualEventName = _getDOMEventName(eventName); if (element.removeEventListener) element.removeEventListener(actualEventName, responder, false); else element.detachEvent('on' + actualEventName, responder); } registry.set(eventName, responders.without(responder)); return element; } function fire(element, eventName, memo, bubble) { element = $(element); if (Object.isUndefined(bubble)) bubble = true; if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; var event; if (document.createEvent) { event = document.createEvent('HTMLEvents'); event.initEvent('dataavailable', true, true); } else { event = document.createEventObject(); event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; } event.eventName = eventName; event.memo = memo || { }; if (document.createEvent) element.dispatchEvent(event); else element.fireEvent(event.eventType, event); return Event.extend(event); } Event.Handler = Class.create({ initialize: function(element, eventName, selector, callback) { this.element = $(element); this.eventName = eventName; this.selector = selector; this.callback = callback; this.handler = this.handleEvent.bind(this); }, start: function() { Event.observe(this.element, this.eventName, this.handler); return this; }, stop: function() { Event.stopObserving(this.element, this.eventName, this.handler); return this; }, handleEvent: function(event) { var element = event.findElement(this.selector); if (element) this.callback.call(this.element, event, element); } }); function on(element, eventName, selector, callback) { element = $(element); if (Object.isFunction(selector) && Object.isUndefined(callback)) { callback = selector, selector = null; } return new Event.Handler(element, eventName, selector, callback).start(); } Object.extend(Event, Event.Methods); Object.extend(Event, { fire: fire, observe: observe, stopObserving: stopObserving, on: on }); Element.addMethods({ fire: fire, observe: observe, stopObserving: stopObserving, on: on }); Object.extend(document, { fire: fire.methodize(), observe: observe.methodize(), stopObserving: stopObserving.methodize(), on: on.methodize(), loaded: false }); if (window.Event) Object.extend(window.Event, Event); else window.Event = Event; })(); (function() { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ var timer; function fireContentLoadedEvent() { if (document.loaded) return; if (timer) window.clearTimeout(timer); document.loaded = true; document.fire('dom:loaded'); } function checkReadyState() { if (document.readyState === 'complete') { document.stopObserving('readystatechange', checkReadyState); fireContentLoadedEvent(); } } function pollDoScroll() { try { document.documentElement.doScroll('left'); } catch(e) { timer = pollDoScroll.defer(); return; } fireContentLoadedEvent(); } if (document.addEventListener) { document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); } else { document.observe('readystatechange', checkReadyState); if (window == top) timer = pollDoScroll.defer(); } Event.observe(window, 'load', fireContentLoadedEvent); })(); Element.addMethods(); /*------------------------------- DEPRECATED -------------------------------*/ Hash.toQueryString = Object.toQueryString; var Toggle = { display: Element.toggle }; Element.Methods.childOf = Element.Methods.descendantOf; var Insertion = { Before: function(element, content) { return Element.insert(element, {before:content}); }, Top: function(element, content) { return Element.insert(element, {top:content}); }, Bottom: function(element, content) { return Element.insert(element, {bottom:content}); }, After: function(element, content) { return Element.insert(element, {after:content}); } }; var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); var Position = { includeScrollOffsets: false, prepare: function() { this.deltaX = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0; this.deltaY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; }, within: function(element, x, y) { if (this.includeScrollOffsets) return this.withinIncludingScrolloffsets(element, x, y); this.xcomp = x; this.ycomp = y; this.offset = Element.cumulativeOffset(element); return (y >= this.offset[1] && y < this.offset[1] + element.offsetHeight && x >= this.offset[0] && x < this.offset[0] + element.offsetWidth); }, withinIncludingScrolloffsets: function(element, x, y) { var offsetcache = Element.cumulativeScrollOffset(element); this.xcomp = x + offsetcache[0] - this.deltaX; this.ycomp = y + offsetcache[1] - this.deltaY; this.offset = Element.cumulativeOffset(element); return (this.ycomp >= this.offset[1] && this.ycomp < this.offset[1] + element.offsetHeight && this.xcomp >= this.offset[0] && this.xcomp < this.offset[0] + element.offsetWidth); }, overlap: function(mode, element) { if (!mode) return 0; if (mode == 'vertical') return ((this.offset[1] + element.offsetHeight) - this.ycomp) / element.offsetHeight; if (mode == 'horizontal') return ((this.offset[0] + element.offsetWidth) - this.xcomp) / element.offsetWidth; }, cumulativeOffset: Element.Methods.cumulativeOffset, positionedOffset: Element.Methods.positionedOffset, absolutize: function(element) { Position.prepare(); return Element.absolutize(element); }, relativize: function(element) { Position.prepare(); return Element.relativize(element); }, realOffset: Element.Methods.cumulativeScrollOffset, offsetParent: Element.Methods.getOffsetParent, page: Element.Methods.viewportOffset, clone: function(source, target, options) { options = options || { }; return Element.clonePosition(target, source, options); } }; /*--------------------------------------------------------------------------*/ if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ function iter(name) { return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; } instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? function(element, className) { className = className.toString().strip(); var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); return cond ? document._getElementsByXPath('.//*' + cond, element) : []; } : function(element, className) { className = className.toString().strip(); var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); if (!classNames && !className) return elements; var nodes = $(element).getElementsByTagName('*'); className = ' ' + className + ' '; for (var i = 0, child, cn; child = nodes[i]; i++) { if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || (classNames && classNames.all(function(name) { return !name.toString().blank() && cn.include(' ' + name + ' '); })))) elements.push(Element.extend(child)); } return elements; }; return function(className, parentElement) { return $(parentElement || document.body).getElementsByClassName(className); }; }(Element.Methods); /*--------------------------------------------------------------------------*/ Element.ClassNames = Class.create(); Element.ClassNames.prototype = { initialize: function(element) { this.element = $(element); }, _each: function(iterator) { this.element.className.split(/\s+/).select(function(name) { return name.length > 0; })._each(iterator); }, set: function(className) { this.element.className = className; }, add: function(classNameToAdd) { if (this.include(classNameToAdd)) return; this.set($A(this).concat(classNameToAdd).join(' ')); }, remove: function(classNameToRemove) { if (!this.include(classNameToRemove)) return; this.set($A(this).without(classNameToRemove).join(' ')); }, toString: function() { return $A(this).join(' '); } }; Object.extend(Element.ClassNames.prototype, Enumerable); /*--------------------------------------------------------------------------*/ (function() { window.Selector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); }, findElements: function(rootElement) { return Prototype.Selector.select(this.expression, rootElement); }, match: function(element) { return Prototype.Selector.match(element, this.expression); }, toString: function() { return this.expression; }, inspect: function() { return "#"; } }); Object.extend(Selector, { matchElements: function(elements, expression) { var match = Prototype.Selector.match, results = []; for (var i = 0, length = elements.length; i < length; i++) { var element = elements[i]; if (match(element, expression)) { results.push(Element.extend(element)); } } return results; }, findElement: function(elements, expression, index) { index = index || 0; var matchIndex = 0, element; for (var i = 0, length = elements.length; i < length; i++) { element = elements[i]; if (Prototype.Selector.match(element, expression) && index === matchIndex++) { return Element.extend(element); } } }, findChildElements: function(element, expressions) { var selector = expressions.toArray().join(', '); return Prototype.Selector.select(selector, element || document); } }); })(); ================================================ FILE: test/apps/rails3/public/javascripts/rails.js ================================================ (function() { // Technique from Juriy Zaytsev // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ function isEventSupported(eventName) { var el = document.createElement('div'); eventName = 'on' + eventName; var isSupported = (eventName in el); if (!isSupported) { el.setAttribute(eventName, 'return;'); isSupported = typeof el[eventName] == 'function'; } el = null; return isSupported; } function isForm(element) { return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' } function isInput(element) { if (Object.isElement(element)) { var name = element.nodeName.toUpperCase() return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' } else return false } var submitBubbles = isEventSupported('submit'), changeBubbles = isEventSupported('change') if (!submitBubbles || !changeBubbles) { // augment the Event.Handler class to observe custom events when needed Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( function(init, element, eventName, selector, callback) { init(element, eventName, selector, callback) // is the handler being attached to an element that doesn't support this event? if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { // "submit" => "emulated:submit" this.eventName = 'emulated:' + this.eventName } } ) } if (!submitBubbles) { // discover forms on the page by observing focus events which always bubble document.on('focusin', 'form', function(focusEvent, form) { // special handler for the real "submit" event (one-time operation) if (!form.retrieve('emulated:submit')) { form.on('submit', function(submitEvent) { var emulated = form.fire('emulated:submit', submitEvent, true) // if custom event received preventDefault, cancel the real one too if (emulated.returnValue === false) submitEvent.preventDefault() }) form.store('emulated:submit', true) } }) } if (!changeBubbles) { // discover form inputs on the page document.on('focusin', 'input, select, texarea', function(focusEvent, input) { // special handler for real "change" events if (!input.retrieve('emulated:change')) { input.on('change', function(changeEvent) { input.fire('emulated:change', changeEvent, true) }) input.store('emulated:change', true) } }) } function handleRemote(element) { var method, url, params; var event = element.fire("ajax:before"); if (event.stopped) return false; if (element.tagName.toLowerCase() === 'form') { method = element.readAttribute('method') || 'post'; url = element.readAttribute('action'); params = element.serialize(); } else { method = element.readAttribute('data-method') || 'get'; url = element.readAttribute('href'); params = {}; } new Ajax.Request(url, { method: method, parameters: params, evalScripts: true, onComplete: function(request) { element.fire("ajax:complete", request); }, onSuccess: function(request) { element.fire("ajax:success", request); }, onFailure: function(request) { element.fire("ajax:failure", request); } }); element.fire("ajax:after"); } function handleMethod(element) { var method = element.readAttribute('data-method'), url = element.readAttribute('href'), csrf_param = $$('meta[name=csrf-param]')[0], csrf_token = $$('meta[name=csrf-token]')[0]; var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); element.parentNode.insert(form); if (method !== 'post') { var field = new Element('input', { type: 'hidden', name: '_method', value: method }); form.insert(field); } if (csrf_param) { var param = csrf_param.readAttribute('content'), token = csrf_token.readAttribute('content'), field = new Element('input', { type: 'hidden', name: param, value: token }); form.insert(field); } form.submit(); } document.on("click", "*[data-confirm]", function(event, element) { var message = element.readAttribute('data-confirm'); if (!confirm(message)) event.stop(); }); document.on("click", "a[data-remote]", function(event, element) { if (event.stopped) return; handleRemote(element); event.stop(); }); document.on("click", "a[data-method]", function(event, element) { if (event.stopped) return; handleMethod(element); event.stop(); }); document.on("submit", function(event) { var element = event.findElement(), message = element.readAttribute('data-confirm'); if (message && !confirm(message)) { event.stop(); return false; } var inputs = element.select("input[type=submit][data-disable-with]"); inputs.each(function(input) { input.disabled = true; input.writeAttribute('data-original-value', input.value); input.value = input.readAttribute('data-disable-with'); }); var element = event.findElement("form[data-remote]"); if (element) { handleRemote(element); event.stop(); } }); document.on("ajax:after", "form", function(event, element) { var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); inputs.each(function(input) { input.value = input.readAttribute('data-original-value'); input.removeAttribute('data-original-value'); input.disabled = false; }); }); Ajax.Responders.register({ onCreate: function(request) { var csrf_meta_tag = $$('meta[name=csrf-token]')[0]; if (csrf_meta_tag) { var header = 'X-CSRF-Token', token = csrf_meta_tag.readAttribute('content'); if (!request.options.requestHeaders) { request.options.requestHeaders = {}; } request.options.requestHeaders[header] = token; } } }); })(); ================================================ FILE: test/apps/rails3/public/robots.txt ================================================ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / ================================================ FILE: test/apps/rails3/public/stylesheets/.gitkeep ================================================ ================================================ FILE: test/apps/rails3/script/rails ================================================ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) require File.expand_path('../../config/boot', __FILE__) require 'rails/commands' ================================================ FILE: test/apps/rails3/test/functional/home_controller_test.rb ================================================ require 'test_helper' class HomeControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success end test "should get test_params" do get :test_params assert_response :success end test "should get test_model" do get :test_model assert_response :success end test "should get test_cookie" do get :test_cookie assert_response :success end test "should get test_filter" do get :test_filter assert_response :success end test "should get test_file_access" do get :test_file_access assert_response :success end test "should get test_sql" do get :test_sql assert_response :success end test "should get test_command" do get :test_command assert_response :success end test "should get test_eval" do get :test_eval assert_response :success end test "should get test_redirect" do get :test_redirect assert_response :success end test "should get test_render" do get :test_render assert_response :success end test "should get test_mass_assignment" do get :test_mass_assignment assert_response :success end test "should get test_dynamic_render" do get :test_dynamic_render assert_response :success end end ================================================ FILE: test/apps/rails3/test/functional/other_controller_test.rb ================================================ require 'test_helper' class OtherControllerTest < ActionController::TestCase test "should get test_locals" do get :test_locals assert_response :success end test "should get test_object" do get :test_object assert_response :success end test "should get test_collection" do get :test_collection assert_response :success end test "should get test_iteration" do get :test_iteration assert_response :success end test "should get test_send_file" do get :test_send_file assert_response :success end end ================================================ FILE: test/apps/rails3/test/performance/browsing_test.rb ================================================ require 'test_helper' require 'rails/performance_test_help' # Profiling results for each test method are written to tmp/performance. class BrowsingTest < ActionDispatch::PerformanceTest def test_homepage get '/' end end ================================================ FILE: test/apps/rails3/test/test_helper.rb ================================================ ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails3/test/unit/helpers/home_helper_test.rb ================================================ require 'test_helper' class HomeHelperTest < ActionView::TestCase end ================================================ FILE: test/apps/rails3/test/unit/helpers/other_helper_test.rb ================================================ require 'test_helper' class OtherHelperTest < ActionView::TestCase end ================================================ FILE: test/apps/rails3/vendor/plugins/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/.gitignore ================================================ .bundle db/*.sqlite3 log/*.log tmp/ .sass-cache/ ================================================ FILE: test/apps/rails3.1/Gemfile ================================================ source 'http://rubygems.org' gem 'rails', '3.1.0' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'mysql' gem 'json' gem 'therubyracer' gem 'draper' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', " ~> 3.1.0" gem 'coffee-rails', "~> 3.1.0" gem 'uglifier' end gem 'jquery-rails' # Use unicorn as the web server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano' # To use debugger # gem 'ruby-debug' ================================================ FILE: test/apps/rails3.1/README ================================================ == Welcome to Rails Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Control pattern. This pattern splits the view (also called the presentation) into "dumb" templates that are primarily responsible for inserting pre-built data in between HTML tags. The model contains the "smart" domain objects (such as Account, Product, Person, Post) that holds all the business logic and knows how to persist themselves to a database. The controller handles the incoming requests (such as Save New Account, Update Product, Show Post) by manipulating the model and directing data to the view. In Rails, the model is handled by what's called an object-relational mapping layer entitled Active Record. This layer allows you to present the data from database rows as objects and embellish these data objects with business logic methods. You can read more about Active Record in link:files/vendor/rails/activerecord/README.html. The controller and view are handled by the Action Pack, which handles both layers by its two parts: Action View and Action Controller. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between the Active Record and Action Pack that is much more separate. Each of these packages can be used independently outside of Rails. You can read more about Action Pack in link:files/vendor/rails/actionpack/README.html. == Getting Started 1. At the command prompt, create a new Rails application: rails new myapp (where myapp is the application name) 2. Change directory to myapp and start the web server: cd myapp; rails server (run with --help for options) 3. Go to http://localhost:3000/ and you'll see: "Welcome aboard: You're riding Ruby on Rails!" 4. Follow the guidelines to start developing your application. You can find the following resources handy: * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ == Debugging Rails Sometimes your application goes wrong. Fortunately there are a lot of tools that will help you debug it and get it back on the rails. First area to check is the application log files. Have "tail -f" commands running on the server.log and development.log. Rails will automatically display debugging and runtime information to these files. Debugging info will also be shown in the browser on requests from 127.0.0.1. You can also log your own messages directly into the log file from your code using the Ruby logger class from inside your controllers. Example: class WeblogController < ActionController::Base def destroy @weblog = Weblog.find(params[:id]) @weblog.destroy logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") end end The result will be a message in your log file along the lines of: Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! More information on how to use the logger is at http://www.ruby-doc.org/core/ Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are several books available online as well: * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) These two books will bring you up to speed on the Ruby language and also on programming in general. == Debugger Debugger support is available through the debugger command when you start your Mongrel or WEBrick server with --debugger. This means that you can break out of execution at any point in the code, investigate and change the model, and then, resume execution! You need to install ruby-debug to run the server in debugging mode. With gems, use sudo gem install ruby-debug. Example: class WeblogController < ActionController::Base def index @posts = Post.all debugger end end So the controller will accept the action, run the first line, then present you with a IRB prompt in the server window. Here you can do things like: >> @posts.inspect => "[#nil, "body"=>nil, "id"=>"1"}>, #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" >> @posts.first.title = "hello from a debugger" => "hello from a debugger" ...and even better, you can examine how your runtime objects actually work: >> f = @posts.first => #nil, "body"=>nil, "id"=>"1"}> >> f. Display all 152 possibilities? (y or n) Finally, when you're ready to resume execution, you can enter "cont". == Console The console is a Ruby shell, which allows you to interact with your application's domain model. Here you'll have all parts of the application configured, just like it is when the application is running. You can inspect domain models, change values, and save to the database. Starting the script without arguments will launch it in the development environment. To start the console, run rails console from the application directory. Options: * Passing the -s, --sandbox argument will rollback any modifications made to the database. * Passing an environment name as an argument will load the corresponding environment. Example: rails console production. To reload your controllers and models after launching the console run reload! More information about irb can be found at: link:http://www.rubycentral.org/pickaxe/irb.html == dbconsole You can go to the command line of your database directly through rails dbconsole. You would be connected to the database with the credentials defined in database.yml. Starting the script without arguments will connect you to the development database. Passing an argument will connect you to a different database, like rails dbconsole production. Currently works for MySQL, PostgreSQL and SQLite 3. == Description of Contents The default directory structure of a generated Ruby on Rails application: |-- app | |-- assets | |-- images | |-- javascripts | `-- stylesheets | |-- controllers | |-- helpers | |-- mailers | |-- models | `-- views | `-- layouts |-- config | |-- environments | |-- initializers | `-- locales |-- db |-- doc |-- lib | `-- tasks |-- log |-- public |-- script |-- test | |-- fixtures | |-- functional | |-- integration | |-- performance | `-- unit |-- tmp | |-- cache | |-- pids | |-- sessions | `-- sockets `-- vendor |-- assets `-- stylesheets `-- plugins app Holds all the code that's specific to this particular application. app/assets Contains subdirectories for images, stylesheets, and JavaScript files. app/controllers Holds controllers that should be named like weblogs_controller.rb for automated URL mapping. All controllers should descend from ApplicationController which itself descends from ActionController::Base. app/models Holds models that should be named like post.rb. Models descend from ActiveRecord::Base by default. app/views Holds the template files for the view that should be named like weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby syntax by default. app/views/layouts Holds the template files for layouts to be used with views. This models the common header/footer method of wrapping views. In your views, define a layout using the layout :default and create a file named default.html.erb. Inside default.html.erb, call <% yield %> to render the view using this layout. app/helpers Holds view helpers that should be named like weblogs_helper.rb. These are generated for you automatically when using generators for controllers. Helpers can be used to wrap functionality for your views into methods. config Configuration files for the Rails environment, the routing map, the database, and other dependencies. db Contains the database schema in schema.rb. db/migrate contains all the sequence of Migrations for your schema. doc This directory is where your application documentation will be stored when generated using rake doc:app lib Application specific libraries. Basically, any kind of custom code that doesn't belong under controllers, models, or helpers. This directory is in the load path. public The directory available for the web server. Also contains the dispatchers and the default HTML files. This should be set as the DOCUMENT_ROOT of your web server. script Helper scripts for automation and generation. test Unit and functional tests along with fixtures. When using the rails generate command, template test files will be generated for you and placed in this directory. vendor External libraries that the application depends on. Also includes the plugins subdirectory. If the app has frozen rails, those gems also go here, under vendor/rails/. This directory is in the load path. ================================================ FILE: test/apps/rails3.1/Rakefile ================================================ #!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails31::Application.load_tasks ================================================ FILE: test/apps/rails3.1/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery_ujs //= require_tree . ================================================ FILE: test/apps/rails3.1/app/assets/javascripts/users.js.coffee ================================================ # Place all the behaviors and hooks related to the matching controller here. # All this logic will automatically be available in application.js. # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ ================================================ FILE: test/apps/rails3.1/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. *= require_self *= require_tree . */ ================================================ FILE: test/apps/rails3.1/app/assets/stylesheets/scaffolds.css.scss ================================================ body { background-color: #fff; color: #333; font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } pre { background-color: #eee; padding: 10px; font-size: 11px; } a { color: #000; &:visited { color: #666; } &:hover { color: #fff; background-color: #000; } } div { &.field, &.actions { margin-bottom: 10px; } } #notice { color: green; } .field_with_errors { padding: 2px; background-color: red; display: table; } #error_explanation { width: 450px; border: 2px solid red; padding: 7px; padding-bottom: 0; margin-bottom: 20px; background-color: #f0f0f0; h2 { text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; margin-bottom: 0px; background-color: #c00; color: #fff; } ul li { font-size: 12px; list-style: square; } } ================================================ FILE: test/apps/rails3.1/app/assets/stylesheets/users.css.scss ================================================ // Place all the styles related to the Users controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ ================================================ FILE: test/apps/rails3.1/app/controllers/admin_controller.rb ================================================ class AdminController < ApplicationController #Examples of skipping important filters with a blacklist instead of whitelist skip_before_filter :login_required, :except => :do_admin_stuff skip_filter :authenticate_user!, :except => :do_admin_stuff skip_before_filter :require_user, :except => [:do_admin_stuff, :do_other_stuff] before_filter -> { @thing = params[:t] }, only: [:use_lambda_filter] def constantize_some_stuff klass = params[:class].constantize klass.new.do_bad_things params[:class].safe_constantize.new(params[:arg]) Module.qualified_const_get(params[:const]) const_get(params[:arg]) some_method(params[:class]).constantize end def authenticate_user! correct_password = "7001337" authenticate_or_request_with_http_basic do |username, password| username == "foo" && password == correct_password end end def show_detailed_exceptions? yeah_sure_they_are_an_admin_right? current_user end def make_system_calls `#{"blah #{why?}"}` # Some command injection of literals # which should not raise warnings or_input = if admin "rm -rf" else :symbol end system "cd / && #{or_input}" `cd / && #{or_input}` system "echo #{1}" exec "nmap 192.168.#{1}.1" system @thing # no warning end def use_lambda_filter eval @thing end def authenticate_token! authenticate_token_or_basic do |username, password| username == "foo" end end def authenticate_token_or_basic(&block) authenticate_or_request_with_http_basic(&block) end end ================================================ FILE: test/apps/rails3.1/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base protect_from_forgery end ================================================ FILE: test/apps/rails3.1/app/controllers/mixins/user_mixin.rb ================================================ module UserMixin #Test mixin action method with explicit template def mixin_action @dangerous_input = params[:bad] render 'users/mixin_template' end #Test mixin action method with default template def mixin_default @dangerous_input = params[:bad] end def assign_if @value = if something this that end end end ================================================ FILE: test/apps/rails3.1/app/controllers/other_controller.rb ================================================ class OtherController < ApplicationController def a @a = params[:bad] end def b @b = params[:bad] end def c @c = params[:bad] end def d @d = params[:bad] end def e @e = params[:bad] end def f @f = params[:bad] end def g @g = params[:bad] end def test_partial1 @a = params[:bad!] render :test_partial end def test_partial2 @b = params[:badder!] render :test_partial end def test_string_interp @user = User.find(current_user) @greeting = "Hello, #{greeted += 1; @user.name}!" end def test_arel_table_access User.where(User.arel_table[:id].eq(params[:some_id])) end def test_draper_redirect redirect_to RecordDecorator.decorate(Record.where(:something => params[:access_key]).find(params[:id])) end def test_model_redirect_in_or if something user = User.find(params[:something]) else user = User.find(params[:else]) end redirect_to user end def test_sanitized_medium sanitize something @css = sanitize_css(some_css) end def test_deserialization CSV.load params[:csv] Marshal.load params[:object] Marshal.restore User.find(1).cool_stored_thing end def test_model_in_haml @user = User.new end end ================================================ FILE: test/apps/rails3.1/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController PASSWORD = "superdupersecret" http_basic_authenticate_with :name => "superduperadmin", :password => PASSWORD, :only => :create # GET /users # GET /users.json def index @users = User.all respond_to do |format| format.html # index.html.erb format.json { render :json => @users } end end # GET /users/1 # GET /users/1.json def show @user = User.find(params[:id]) respond_to do |format| format.html # show.html.erb format.json { render :json => @user } end end # GET /users/new # GET /users/new.json def new @user = User.new respond_to do |format| format.html # new.html.erb format.json { render :json => @user } end end # GET /users/1/edit def edit @user = User.find(params[:id]) end # POST /users # POST /users.json def create @user = User.new(params[:user], :without_protection => true) respond_to do |format| if @user.save format.html { redirect_to @user, :notice => 'User was successfully created.' } format.json { render :json => @user, :status => :created, :location => @user } else format.html { render :action => "new" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # PUT /users/1 # PUT /users/1.json def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to @user, :notice => 'User was successfully updated.' } format.json { head :ok } else format.html { render :action => "edit" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.json def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.html { redirect_to users_url } format.json { head :ok } end end def circular_render end skip_before_filter :verify_authenticity_token, :except => [:create, :edit] def redirect_to_new_user redirect_to User.new end def redirect_to_user_url redirect_to User.find(1).url end def redirect_to_user_find_by redirect_to User.find_by_name(params[:name]) end def test_file_access_params File.unlink(blah(params[:file])) Pathname.readlines("blah/#{cookies[:file]}") File.delete(params[:file]) IO.read(User.find_by_name('bob').file_path) end def redirect_to_user_as_param redirect_to blah(User.find(1)) #Don't warn end def redirect_to_association redirect_to User.first.account #Don't warn end def redirect_to_safe_second_param redirect_to :back, :notice => "Go back, #{params[:user]}!" #Don't warn end def test_simple_helper @user = simple_helper end def test_less_simple_helpers assign_ivar @input = less_simple_helper @other_thing = simple_helper_with_args(params[:x]) end def test_assign_twice assign_ivar end def update_all_users #Unsafe User.update_all params[:yaysql] User.update_all "name = 'Bob'", "name = '#{params[:name]}'" User.update_all "old = TRUE", ["name = '#{params[:name]}' AND age > ?", params[:age]] User.update_all "old = TRUE", ["name = ? AND age > ?", params[:name], params[:age]], :order => params[:order] User.where(:name => params[:name]).update_all(params[:update]) User.where(:admin => true).update_all("setting = #{params[:setting]}") User.where(:name => params[:name]).update_all(["active = ?, age = #{params[:age]}", params[:active]]).limit(1) #Safe(ish) User.update_all ["name = ?", params[:new_name]], ["name = ?", params[:old_name]] User.update_all({:old => true}, ["name = ? AND age > ?", params[:name], params[:age]]) User.update_all({:admin => true}, { :name => params[:name] }, :limit => params[:limit]) end def test_assign_if end private def simple_helper User.find(params[:id]) end def less_simple_helper params[:input] end def simple_helper_with_args arg arg end def assign_ivar @some_value = params[:badthing] end def pluck_something User.pluck params[:column] end include UserMixin before_filter :assign_if, :only => :test_assign_if def redirect_merge redirect_to params.merge(host: 'http://app.webthing.com/stuff', port: '80').except(:action, :controller, :auth_token) end def drape @user = (params[:id] ? User.find(params[:id]) : current_user).decorate end def mass_again User.new(stuff, :without_protection => true) end def dynamic_finders User.find_by_name_and_password(params[:name], params[:pass]) User.find_by_reset_code(params[:code]) Product.find_by_guid(params[:guid].to_s) # No warn end end ================================================ FILE: test/apps/rails3.1/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails3.1/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails3.1/app/mailers/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/app/models/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/app/models/account.rb ================================================ class Account < ActiveRecord::Base validates :username, :length => 6..20, :format => /([a-z][0-9])+/i validates :phone, :format => { :with => /(\d{3})-(\d{3})-(\d{4})/, :on => :create }, :presence => true validates :first_name, :format => /\w+/ serialize :cc_info #safe from CVE-2013-0277 attr_accessible :blah_admin_blah end ================================================ FILE: test/apps/rails3.1/app/models/product.rb ================================================ class Product < ActiveRecord::Base def test_find_order #Should warn, no escaping done for :order Product.find(:all, :order => params[:order]) Product.find(:all, :conditions => 'admin = 1', :order => "name #{params[:order]}") end def test_find_group #Should warn, no escaping done for :group Product.find(:all, :conditions => 'admin = 1', :group => params[:group]) Product.find(:all, :conditions => 'admin = 1', :group => "something, #{params[:group]}") end def test_find_having #Should warn Product.find(:first, :conditions => 'admin = 1', :having => "x = #{params[:having]}") #Should not warn, hash values are escaped Product.find(:first, :conditions => 'admin = 1', :having => { :x => params[:having]}) #Should not warn, properly interpolated Product.find(:first, :conditions => ['name = ?', params[:name]], :having => [ 'x = ?', params[:having]]) #Should warn, not quite properly interpolated Product.find(:first, :conditions => ['name = ?', params[:name]], :having => [ "admin = ? and x = #{params[:having]}", cookies[:admin]]) Product.find(:first, :conditions => ['name = ?', params[:name]], :having => [ "admin = ? and x = '" + params[:having] + "'", cookies[:admin]]) end def test_find_joins #Should not warn, string values are not going to have injection Product.find(:first, :conditions => 'admin = 1', :joins => "LEFT JOIN comments ON comments.post_id = id") #Should warn Product.find(:first, :conditions => 'admin = 1', :joins => "LEFT JOIN comments ON comments.#{params[:join]} = id") #Should not warn Product.find(:first, :conditions => 'admin = 1', :joins => [:x, :y]) #Should warn Product.find(:first, :conditions => 'admin = 1', :joins => ["LEFT JOIN comments ON comments.#{params[:join]} = id", :x, :y]) end def test_find_select #Should not warn, string values are not going to have injection Product.find(:last, :conditions => 'admin = 1', :select => "name") #Should warn Product.find(:last, :conditions => 'admin = 1', :select => params[:column]) Product.find(:last, :conditions => 'admin = 1', :select => "name, #{params[:column]}") Product.find(:last, :conditions => 'admin = 1', :select => "name, " + params[:column]) end def test_find_from #Should not warn, string values are not going to have injection Product.find(:last, :conditions => 'admin = 1', :from => "users") #Should warn Product.find(:last, :conditions => 'admin = 1', :from => params[:table]) Product.find(:last, :conditions => 'admin = 1', :from => "#{params[:table]}") end def test_find_lock #Should not warn Product.find(:last, :conditions => 'admin = 1', :lock => true) #Should warn Product.find(:last, :conditions => 'admin = 1', :lock => params[:lock]) Product.find(:last, :conditions => 'admin = 1', :lock => "LOCK #{params[:lock]}") end def test_where #Should not warn Product.where("admin = 1") Product.where("admin = ?", params[:admin]) Product.where(["admin = ?", params[:admin]]) Product.where(["admin = :admin", { :admin => params[:admin] }]) Product.where(:admin => params[:admin]) #Should warn Product.where("admin = '#{params[:admin]}'").first Product.where(["admin = ? AND user_name = #{@name}", params[:admin]]) end TOTALLY_SAFE = "some safe string" def test_constant_interpolation #Should not warn Product.first("blah = #{TOTALLY_SAFE}") end def test_local_interpolation #Should warn, medium confidence Product.first("blah = #{local_var}") end def test_conditional_args_in_sql #Should warn Product.last("blah = '#{something ? params[:blah] : TOTALLY_SAFE}'") #Should not warn Product.last("blah = '#{params[:blah] ? 1 : 0}'") end def test_params_in_args #Should warn Product.last("blah = '#{something(params[:blah])}'") end def test_params_to_i #Should not warn Product.last("blah = '#{params[:id].to_i}'") end def test_more_if_statements if some_condition x = params[:x] else x = "BLAH" end y = if some_other_condition params[:x] "blah" else params[:y] "blah" end #Should warn Product.last("blah = '#{x}'") #Should not warn Product.last("blah = '#{y}'") Product.where("blah = 1").group(y) end def test_calculations #Should warn Product.calculate(:count, :all, :conditions => "blah = '#{params[:blah]}'") Product.calculate(:sum, params[:column_name]) # Should warn - column name from params Product.minimum(:price, :conditions => "blah = #{params[:blach]}") Product.maximum(:price, :group => params[:columns]) Product.average(:price, :conditions => ["blah = #{params[:columns]} and x = ?", x]) Product.sum(params[:columns]) end def test_select #Should not warn Product.select([:price, :sku]) #Should warn Product.select params[:columns] end def test_conditional_in_options x = params[:x] == y ? "created_at ASC" : "created_at DESC" z = params[:y] == y ? "safe" : "totally safe" #Should not warn Product.all(:order => x, :having => z, :select => z, :from => z, :group => z) end def test_or_interpolation #Should not warn Product.where("blah = #{1 or 2}") end def test_params_to_f #Should not warn Product.last("blah = '#{params[:id].to_f}'") end def test_interpolation_in_first_arg Product.where("x = #{params[:x]} AND y = ?", y) end def test_to_sql_interpolation #Should not warn prices = Produt.select(:price).where("created_at < :time").to_sql where("price IN (#{prices}) OR whatever", :price => some_price) end end ================================================ FILE: test/apps/rails3.1/app/models/some_model.rb ================================================ class SomeModel < @some_variable end ================================================ FILE: test/apps/rails3.1/app/models/user.rb ================================================ class User < ActiveRecord::Base attr_accessible :name scope :tall, lambda {|*args| where("height > '#{User.average_height}'") } scope :blah, where("thinger = '#{BLAH}'") #No longer warns on constants scope :dah, lambda {|*args| { :conditions => "dah = '#{args[1]}'"}} scope :phooey, :conditions => "phoeey = '#{User.phooey}'" scope :this_is_safe, lambda { |name| where("name = ?", "%#{name.downcase}%") } scope :this_is_also_safe, where("name = ?", "%#{name.downcase}%") scope :should_not_warn, :conditions => ["name = ?", "%#{name.downcase}%"] scope :unsafe_multiline_scope, lambda { something = something_helper where("something = #{something}") } scope :all belongs_to :account attr_accessible :admin, :as => :admin def self.sql_stuff parent_id condition = parent_id.blank? ? " IS NULL" : " = #{parent_id}" self.connection.select_values("SELECT max(id) FROM content_pages WHERE parent_content_page_id #{condition}")[0].to_i self.connection.select_values("SELECT max(id) FROM content_pages WHERE child_content_page_id #{child_id}")[0].to_i # Should not warn User.where("#{table_name}.visibility = ?" + " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" + "SELECT DISTINCT a.id FROM #{table_name} a" + " INNER JOIN #{User.table_name} m ON m.id = mr.member_id AND m.user_id = ?" + " WHERE a.project_id IS NULL OR a.project_id = m.project_id))" + " OR #{table_name}.user_id = ?", stuff, stuff, user.id, user.id) end def self.safe_sql_using_quoted_table_name where("#{User.quoted_table_name}.id = ?", 1) end def self.more_safe_stuff where("#{User.primary_key} = #{table_name_prefix}a.thing") end end ================================================ FILE: test/apps/rails3.1/app/views/layouts/application.html.erb ================================================ Rails31 <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/apps/rails3.1/app/views/other/_partial.html.erb ================================================ <%= raw @a %> <%= raw @b %> ================================================ FILE: test/apps/rails3.1/app/views/other/a.html.erb ================================================ <%= raw @a %> ================================================ FILE: test/apps/rails3.1/app/views/other/b.html.erb ================================================ <%= raw @b %> ================================================ FILE: test/apps/rails3.1/app/views/other/c.html.erb ================================================ <%= raw @c %> ================================================ FILE: test/apps/rails3.1/app/views/other/d.html.erb ================================================ <%= raw @d %> ================================================ FILE: test/apps/rails3.1/app/views/other/e.html.erb ================================================ <%= raw @e %> ================================================ FILE: test/apps/rails3.1/app/views/other/f.html.erb ================================================ <%= raw @f %> ================================================ FILE: test/apps/rails3.1/app/views/other/g.html.erb ================================================ <%= raw @g %> ================================================ FILE: test/apps/rails3.1/app/views/other/test_model_in_haml.html.haml ================================================ %user %footer = @user.updated_at %h1= @user.name != @user.bio ================================================ FILE: test/apps/rails3.1/app/views/other/test_partial.html.erb ================================================ <%= render 'partial' %> ================================================ FILE: test/apps/rails3.1/app/views/other/test_select_tag.html.erb ================================================ <%= select_tag "name", options, :prompt => something_benign %> <%= select_tag "name", options, :prompt => "Select #{params[:name]}" %> ================================================ FILE: test/apps/rails3.1/app/views/other/test_string_interp.html.erb ================================================ <%= raw @greeting %> ================================================ FILE: test/apps/rails3.1/app/views/other/test_strip_tags.html.erb ================================================ <%= strip_tags params[:body] %> ================================================ FILE: test/apps/rails3.1/app/views/users/_bio.html.erb ================================================ <%= user_bio %> ================================================ FILE: test/apps/rails3.1/app/views/users/_circular.html.erb ================================================ <% @i = (@i ? @i + 1 : 2) %> <%= render :partial => "/users/circular_too" %> ================================================ FILE: test/apps/rails3.1/app/views/users/_circular_too.html.erb ================================================ <%= render :partial => "/users/circular" %> ================================================ FILE: test/apps/rails3.1/app/views/users/_form.html.erb ================================================ <%= form_for(@user) do |f| %> <% if @user.errors.any? %>

    <%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

      <% @user.errors.full_messages.each do |msg| %>
    • <%= msg %>
    • <% end %>
    <% end %>
    <%= f.label :name %>
    <%= f.text_field :name %>
    <%= f.label :bio %>
    <%= f.text_field :bio %>
    <%= f.label :password %>
    <%= f.text_field :password %>
    <%= f.label :email %>
    <%= f.text_field :email %>
    <%= f.label :role %>
    <%= f.text_field :role %>
    <%= f.label :something %>
    <%= f.submit %>
    <% end %> ================================================ FILE: test/apps/rails3.1/app/views/users/_test_layout.html.erb ================================================ <%= raw @something %> ================================================ FILE: test/apps/rails3.1/app/views/users/_user.html.erb ================================================ <%= user.name %> <%= render 'bio', :locals => { :user_bio => raw(user.bio) } %> <%= user.password %> <%= user.email %> <%= user.role %> <%= link_to 'Show', user %> <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %> ================================================ FILE: test/apps/rails3.1/app/views/users/circular_render.html.erb ================================================ <%= render :partial => "/users/circular" %> ================================================ FILE: test/apps/rails3.1/app/views/users/drape.html.erb ================================================ <%= link_to @user.name, @user %> ================================================ FILE: test/apps/rails3.1/app/views/users/edit.html.erb ================================================

    Editing user

    <%= select('post', 'author_id', "") %> <%= render 'form' %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails3.1/app/views/users/index.html.erb ================================================

    Listing users

    <%= render 'user', :collection => @users %>
    Name Bio Password Email Role

    <%= link_to 'New User', new_user_path %> <%= @something = params["something_bad"] %> <%= render :layout => "test_layout" %> ================================================ FILE: test/apps/rails3.1/app/views/users/interpolated_value.html.haml ================================================ .escaped_thing Hi #{params[:awesomeness]} ================================================ FILE: test/apps/rails3.1/app/views/users/json_test.html.erb ================================================ <%= raw({:donkey => params[:donkey]}.to_json) %> ================================================ FILE: test/apps/rails3.1/app/views/users/mixin_default.html.erb ================================================ <%= raw @dangerous_input %> ================================================ FILE: test/apps/rails3.1/app/views/users/mixin_template.html.erb ================================================ <%= raw @dangerous_input %> ================================================ FILE: test/apps/rails3.1/app/views/users/new.html.erb ================================================

    New user

    <%= render 'form' %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails3.1/app/views/users/show.html.erb ================================================

    <%= notice %>

    Name: <%= @user.name %>

    Bio: <%= @user.bio %>

    Password: <%= @user.password %>

    Email: <%= @user.email %>

    Role: <%= @user.role %>

    <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> <%= translate('some_html', :some => '') %> ================================================ FILE: test/apps/rails3.1/app/views/users/test_assign_if.html.erb ================================================ <%= @value %> ================================================ FILE: test/apps/rails3.1/app/views/users/test_assign_twice.html.erb ================================================ <%= raw @some_value %> ================================================ FILE: test/apps/rails3.1/app/views/users/test_less_simple_helpers.html.erb ================================================ <%= raw @input %> <%= raw @other_thing %> <%= raw @some_value %> ================================================ FILE: test/apps/rails3.1/app/views/users/test_simple_helper.html.erb ================================================ <%= raw @user %> ================================================ FILE: test/apps/rails3.1/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' if defined?(Bundler) # If you precompile assets before deploying to production, use this line Bundler.require *Rails.groups(:assets => %w(development test)) # If you want your assets lazily compiled in production, use this line # Bundler.require(:default, :assets, Rails.env) end module Rails31 class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. # config.autoload_paths += %W(#{config.root}/extras) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] # Enable the asset pipeline config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' config.active_record.whitelist_attributes = true end end Haml::Template.options[:format] = :html5 ================================================ FILE: test/apps/rails3.1/config/boot.rb ================================================ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) ================================================ FILE: test/apps/rails3.1/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails3.1/config/environment.rb ================================================ # Load the rails application require File.expand_path('../application', __FILE__) # Initialize the rails application Rails31::Application.initialize! ================================================ FILE: test/apps/rails3.1/config/environments/development.rb ================================================ Rails31::Application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger config.active_support.deprecation = :log # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin # Do not compress assets config.assets.compress = false # Expands the lines which load the assets config.assets.debug = true end ================================================ FILE: test/apps/rails3.1/config/environments/production.rb ================================================ Rails31::Application.configure do # Settings specified here will take precedence over those in config/application.rb # Code is not reloaded between requests config.cache_classes = true # Full error reports are disabled and caching is turned on config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = false # Compress JavaScripts and CSS config.assets.compress = true # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false # Generate digests for assets URLs config.assets.digest = true # Defaults to Rails.root.join("public/assets") # config.assets.manifest = YOUR_PATH # Specifies the header that your server uses for sending files # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # See everything in the log (default is :info) # config.log_level = :debug # Use a different logger for distributed setups # config.logger = SyslogLogger.new # Use a different cache store in production # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false # Enable threaded mode # config.threadsafe! # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify end ================================================ FILE: test/apps/rails3.1/config/environments/test.rb ================================================ Rails31::Application.configure do # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Configure static asset server for tests with Cache-Control for performance config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" # Log error messages when you accidentally call methods on nil config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Use SQL instead of Active Record's schema dumper when creating the test database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql # Print deprecation notices to the stderr config.active_support.deprecation = :stderr # Allow pass debug_assets=true as a query parameter to load pages with unpackaged assets config.assets.allow_debugging = true end ================================================ FILE: test/apps/rails3.1/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails3.1/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end ================================================ FILE: test/apps/rails3.1/config/initializers/mime_type_fix.rb ================================================ require 'action_dispatch/http/mime_type' Mime.const_set :LOOKUP, Hash.new { |h,k| Mime::Type.new(k) unless k.blank? } ================================================ FILE: test/apps/rails3.1/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails3.1/config/initializers/secret_token.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. Rails31::Application.config.secret_token = 'bc1889b8c46e86b3becee7c3abf7bb935b024b86eac43acb820478dc524417144a006e0edf8191d2d1017a6404922b7824e667c8c8656b60604c821e13283b72' ================================================ FILE: test/apps/rails3.1/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails31::Application.config.session_store :cookie_store, :key => '_rails3.1_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") # Rails31::Application.config.session_store :active_record_store ================================================ FILE: test/apps/rails3.1/config/initializers/set_escape_json.rb ================================================ # this value will be overwritten in unset_escape_json.rb ActiveSupport.escape_html_entities_in_json = true ================================================ FILE: test/apps/rails3.1/config/initializers/unset_escape_json.rb ================================================ # this overwrites the value set in set_escape_json ActiveSupport.escape_html_entities_in_json = false ================================================ FILE: test/apps/rails3.1/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters :format => [:json] end # Disable root element in JSON by default. ActiveSupport.on_load(:active_record) do self.include_root_in_json = false end ================================================ FILE: test/apps/rails3.1/config/initializers/xml_parsing.rb ================================================ ActiveSupport::XmlMini.backend = "REXML" ================================================ FILE: test/apps/rails3.1/config/initializers/yaml_parsing.rb ================================================ ActiveSupport::XmlMini::PARSING.delete("symbol") ActiveSupport::XmlMini::PARSING.delete("yaml") ================================================ FILE: test/apps/rails3.1/config/locales/en.yml ================================================ # Sample localization file for English. Add more files in this directory for other locales. # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: hello: "Hello world" ================================================ FILE: test/apps/rails3.1/config/routes.rb ================================================ Rails31::Application.routes.draw do resources :users match 'users/test_simple_helper' => "users#test_simple_helper" match 'users/test_less_simple_helpers' => "users#test_less_simple_helpers" match 'users/test_less_simple_helpers' => "users#test_assign_twice" resources :users do get 'mixin_action' get 'mixin_default' end resources :other do get :a delete 'f' end controller :other do get 'b' post 'something' => 'c' put 'dee', :to => :d get 'test_partial1' get 'test_partial2' get 'test_string_interp' end match 'e', :to => 'other#e', :as => 'eeeee' get 'g' => 'other#g' match 'blah/:id', :action => 'blarg' # The priority is based upon order of creation: # first created -> highest priority. # Sample of regular route: # match 'products/:id' => 'catalog#view' # Keep in mind you can assign values other than :controller and :action # Sample of named route: # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase # This route can be invoked with purchase_url(:id => product.id) # Sample resource route (maps HTTP verbs to controller actions automatically): # resources :products # Sample resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Sample resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Sample resource route with more complex sub-resources # resources :products do # resources :comments # resources :sales do # get 'recent', :on => :collection # end # end # Sample resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end # You can have the root of your site routed with "root" # just remember to delete public/index.html. # root :to => 'welcome#index' # See how all your routes lay out with "rake routes" # This is a legacy wild controller route that's not recommended for RESTful applications. # Note: This route will make all actions in every controller accessible via GET requests. # match ':controller(/:action(/:id(.:format)))' end ================================================ FILE: test/apps/rails3.1/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails31::Application ================================================ FILE: test/apps/rails3.1/db/migrate/20110908172338_create_users.rb ================================================ class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.string :bio t.string :password t.string :email t.string :role t.timestamps end end end ================================================ FILE: test/apps/rails3.1/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) # Mayor.create(:name => 'Emanuel', :city => cities.first) ================================================ FILE: test/apps/rails3.1/doc/README_FOR_APP ================================================ Use this README file to introduce your application and point to useful places in the API for learning more. Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. ================================================ FILE: test/apps/rails3.1/lib/alib.rb ================================================ class Alib < $SOME_CONSTANT end ================================================ FILE: test/apps/rails3.1/lib/assets/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/lib/somelib.rb ================================================ class MyLib def test_negative_array_index #This should not cause an error, but it used to [][-1] [-1][-1] end end ================================================ FILE: test/apps/rails3.1/lib/tasks/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/log/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

    ================================================ FILE: test/apps/rails3.1/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

    ================================================ FILE: test/apps/rails3.1/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

    We've been notified about this issue and we'll take a look at it shortly.

    ================================================ FILE: test/apps/rails3.1/public/index.html ================================================ Ruby on Rails: Welcome aboard

    Getting started

    Here’s how to get rolling:

    1. Use rails generate to create your models and controllers

      To see all available options, run it without parameters.

    2. Set up a default route and remove public/index.html

      Routes are set up in config/routes.rb.

    3. Create your database

      Run rake db:create to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    ================================================ FILE: test/apps/rails3.1/public/robots.txt ================================================ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / ================================================ FILE: test/apps/rails3.1/script/rails ================================================ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) require File.expand_path('../../config/boot', __FILE__) require 'rails/commands' ================================================ FILE: test/apps/rails3.1/test/fixtures/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/test/fixtures/users.yml ================================================ # Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html one: name: MyString bio: MyString password: MyString email: MyString role: MyString two: name: MyString bio: MyString password: MyString email: MyString role: MyString ================================================ FILE: test/apps/rails3.1/test/functional/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/test/functional/users_controller_test.rb ================================================ require 'test_helper' class UsersControllerTest < ActionController::TestCase setup do @user = users(:one) end test "should get index" do get :index assert_response :success assert_not_nil assigns(:users) end test "should get new" do get :new assert_response :success end test "should create user" do assert_difference('User.count') do post :create, :user => @user.attributes end assert_redirected_to user_path(assigns(:user)) end test "should show user" do get :show, :id => @user.to_param assert_response :success end test "should get edit" do get :edit, :id => @user.to_param assert_response :success end test "should update user" do put :update, :id => @user.to_param, :user => @user.attributes assert_redirected_to user_path(assigns(:user)) end test "should destroy user" do assert_difference('User.count', -1) do delete :destroy, :id => @user.to_param end assert_redirected_to users_path end end ================================================ FILE: test/apps/rails3.1/test/integration/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/test/performance/browsing_test.rb ================================================ require 'test_helper' require 'rails/performance_test_help' class BrowsingTest < ActionDispatch::PerformanceTest # Refer to the documentation for all available options # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] # :output => 'tmp/performance', :formats => [:flat] } def test_homepage get '/' end end ================================================ FILE: test/apps/rails3.1/test/test_helper.rb ================================================ ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails3.1/test/unit/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/test/unit/helpers/users_helper_test.rb ================================================ require 'test_helper' class UsersHelperTest < ActionView::TestCase end ================================================ FILE: test/apps/rails3.1/test/unit/user_test.rb ================================================ require 'test_helper' class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end ================================================ FILE: test/apps/rails3.1/vendor/assets/stylesheets/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.1/vendor/plugins/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.2/Gemfile ================================================ source 'https://rubygems.org' gem 'rails', '3.2.9.rc2' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' gem 'json' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', :platforms => :ruby gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' # To use Jbuilder templates for JSON # gem 'jbuilder' # Use unicorn as the app server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano' # To use debugger # gem 'ruby-debug' ================================================ FILE: test/apps/rails3.2/README.rdoc ================================================ == Welcome to Rails Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Control pattern. This pattern splits the view (also called the presentation) into "dumb" templates that are primarily responsible for inserting pre-built data in between HTML tags. The model contains the "smart" domain objects (such as Account, Product, Person, Post) that holds all the business logic and knows how to persist themselves to a database. The controller handles the incoming requests (such as Save New Account, Update Product, Show Post) by manipulating the model and directing data to the view. In Rails, the model is handled by what's called an object-relational mapping layer entitled Active Record. This layer allows you to present the data from database rows as objects and embellish these data objects with business logic methods. You can read more about Active Record in link:files/vendor/rails/activerecord/README.html. The controller and view are handled by the Action Pack, which handles both layers by its two parts: Action View and Action Controller. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between the Active Record and Action Pack that is much more separate. Each of these packages can be used independently outside of Rails. You can read more about Action Pack in link:files/vendor/rails/actionpack/README.html. == Getting Started 1. At the command prompt, create a new Rails application: rails new myapp (where myapp is the application name) 2. Change directory to myapp and start the web server: cd myapp; rails server (run with --help for options) 3. Go to http://localhost:3000/ and you'll see: "Welcome aboard: You're riding Ruby on Rails!" 4. Follow the guidelines to start developing your application. You can find the following resources handy: * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ == Debugging Rails Sometimes your application goes wrong. Fortunately there are a lot of tools that will help you debug it and get it back on the rails. First area to check is the application log files. Have "tail -f" commands running on the server.log and development.log. Rails will automatically display debugging and runtime information to these files. Debugging info will also be shown in the browser on requests from 127.0.0.1. You can also log your own messages directly into the log file from your code using the Ruby logger class from inside your controllers. Example: class WeblogController < ActionController::Base def destroy @weblog = Weblog.find(params[:id]) @weblog.destroy logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") end end The result will be a message in your log file along the lines of: Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! More information on how to use the logger is at http://www.ruby-doc.org/core/ Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are several books available online as well: * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) These two books will bring you up to speed on the Ruby language and also on programming in general. == Debugger Debugger support is available through the debugger command when you start your Mongrel or WEBrick server with --debugger. This means that you can break out of execution at any point in the code, investigate and change the model, and then, resume execution! You need to install ruby-debug to run the server in debugging mode. With gems, use sudo gem install ruby-debug. Example: class WeblogController < ActionController::Base def index @posts = Post.all debugger end end So the controller will accept the action, run the first line, then present you with a IRB prompt in the server window. Here you can do things like: >> @posts.inspect => "[#nil, "body"=>nil, "id"=>"1"}>, #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" >> @posts.first.title = "hello from a debugger" => "hello from a debugger" ...and even better, you can examine how your runtime objects actually work: >> f = @posts.first => #nil, "body"=>nil, "id"=>"1"}> >> f. Display all 152 possibilities? (y or n) Finally, when you're ready to resume execution, you can enter "cont". == Console The console is a Ruby shell, which allows you to interact with your application's domain model. Here you'll have all parts of the application configured, just like it is when the application is running. You can inspect domain models, change values, and save to the database. Starting the script without arguments will launch it in the development environment. To start the console, run rails console from the application directory. Options: * Passing the -s, --sandbox argument will rollback any modifications made to the database. * Passing an environment name as an argument will load the corresponding environment. Example: rails console production. To reload your controllers and models after launching the console run reload! More information about irb can be found at: link:http://www.rubycentral.org/pickaxe/irb.html == dbconsole You can go to the command line of your database directly through rails dbconsole. You would be connected to the database with the credentials defined in database.yml. Starting the script without arguments will connect you to the development database. Passing an argument will connect you to a different database, like rails dbconsole production. Currently works for MySQL, PostgreSQL and SQLite 3. == Description of Contents The default directory structure of a generated Ruby on Rails application: |-- app | |-- assets | |-- images | |-- javascripts | `-- stylesheets | |-- controllers | |-- helpers | |-- mailers | |-- models | `-- views | `-- layouts |-- config | |-- environments | |-- initializers | `-- locales |-- db |-- doc |-- lib | `-- tasks |-- log |-- public |-- script |-- test | |-- fixtures | |-- functional | |-- integration | |-- performance | `-- unit |-- tmp | |-- cache | |-- pids | |-- sessions | `-- sockets `-- vendor |-- assets `-- stylesheets `-- plugins app Holds all the code that's specific to this particular application. app/assets Contains subdirectories for images, stylesheets, and JavaScript files. app/controllers Holds controllers that should be named like weblogs_controller.rb for automated URL mapping. All controllers should descend from ApplicationController which itself descends from ActionController::Base. app/models Holds models that should be named like post.rb. Models descend from ActiveRecord::Base by default. app/views Holds the template files for the view that should be named like weblogs/index.html.erb for the WeblogsController#index action. All views use eRuby syntax by default. app/views/layouts Holds the template files for layouts to be used with views. This models the common header/footer method of wrapping views. In your views, define a layout using the layout :default and create a file named default.html.erb. Inside default.html.erb, call <% yield %> to render the view using this layout. app/helpers Holds view helpers that should be named like weblogs_helper.rb. These are generated for you automatically when using generators for controllers. Helpers can be used to wrap functionality for your views into methods. config Configuration files for the Rails environment, the routing map, the database, and other dependencies. db Contains the database schema in schema.rb. db/migrate contains all the sequence of Migrations for your schema. doc This directory is where your application documentation will be stored when generated using rake doc:app lib Application specific libraries. Basically, any kind of custom code that doesn't belong under controllers, models, or helpers. This directory is in the load path. public The directory available for the web server. Also contains the dispatchers and the default HTML files. This should be set as the DOCUMENT_ROOT of your web server. script Helper scripts for automation and generation. test Unit and functional tests along with fixtures. When using the rails generate command, template test files will be generated for you and placed in this directory. vendor External libraries that the application depends on. Also includes the plugins subdirectory. If the app has frozen rails, those gems also go here, under vendor/rails/. This directory is in the load path. ================================================ FILE: test/apps/rails3.2/Rakefile ================================================ #!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails32::Application.load_tasks ================================================ FILE: test/apps/rails3.2/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // //= require jquery //= require jquery_ujs //= require_tree . ================================================ FILE: test/apps/rails3.2/app/assets/javascripts/users.js.coffee ================================================ # Place all the behaviors and hooks related to the matching controller here. # All this logic will automatically be available in application.js. # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ ================================================ FILE: test/apps/rails3.2/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self *= require_tree . */ ================================================ FILE: test/apps/rails3.2/app/assets/stylesheets/scaffolds.css.scss ================================================ body { background-color: #fff; color: #333; font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } pre { background-color: #eee; padding: 10px; font-size: 11px; } a { color: #000; &:visited { color: #666; } &:hover { color: #fff; background-color: #000; } } div { &.field, &.actions { margin-bottom: 10px; } } #notice { color: green; } .field_with_errors { padding: 2px; background-color: red; display: table; } #error_explanation { width: 450px; border: 2px solid red; padding: 7px; padding-bottom: 0; margin-bottom: 20px; background-color: #f0f0f0; h2 { text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; margin-bottom: 0px; background-color: #c00; color: #fff; } ul li { font-size: 12px; list-style: square; } } ================================================ FILE: test/apps/rails3.2/app/assets/stylesheets/users.css.scss ================================================ // Place all the styles related to the Users controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ ================================================ FILE: test/apps/rails3.2/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base protect_from_forgery end ================================================ FILE: test/apps/rails3.2/app/controllers/exec_controller/command_dependency.rb ================================================ class ExecController def inner_exec system params[:user_input] end end ================================================ FILE: test/apps/rails3.2/app/controllers/exec_controller.rb ================================================ class ExecController < ApplicationController require_dependency "exec_controller/command_dependency" def outer_exec system params[:user_input] end end ================================================ FILE: test/apps/rails3.2/app/controllers/removal_controller.rb ================================================ class RemovalController < ApplicationController def change_lines <<-X this method is here for line numbers X end def remove_this redirect_to params[:url] end def remove_this_too @some_input = raw params[:input] @some_other_input = Account.first.name render 'removal/controller_removed' end def implicit_render @bad_stuff = raw params[:bad_stuff] end end ================================================ FILE: test/apps/rails3.2/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController include UserControllerMixin # GET /users # GET /users.json def index @users = User.all respond_to do |format| format.html # index.html.erb format.json { render :json => @users } end end # GET /users/1 # GET /users/1.json def show @user = User.find(params[:id]) @user_data = raw params[:user_data] respond_to do |format| format.html # show.html.erb format.json { render :json => @user } end end # GET /users/new # GET /users/new.json def new @user = User.new respond_to do |format| format.html # new.html.erb format.json { render :json => @user } end end # GET /users/1/edit def edit @user = User.find(params[:id]) end # POST /users # POST /users.json def create @user = User.new(params[:user]) respond_to do |format| if @user.save format.html { redirect_to @user, :notice => 'User was successfully created.' } format.json { render :json => @user, :status => :created, :location => @user } else format.html { render :action => "new" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # PUT /users/1 # PUT /users/1.json def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to @user, :notice => 'User was successfully updated.' } format.json { head :no_content } else format.html { render :action => "edit" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.json def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.html { redirect_to users_url } format.json { head :no_content } end end def slimming @user = User.find(params[:id]) @query = params[:query] end def show_detailed_exceptions? false # no warning end def render_text render :text => "oh noes my service" end def test_symbol_dos params[:x].to_sym # no warning because this is an optional check end end ================================================ FILE: test/apps/rails3.2/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails3.2/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails3.2/app/models/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.2/app/models/account.rb ================================================ class Account < ActiveRecord::Base attr_accessible :plan_id, :banned end ================================================ FILE: test/apps/rails3.2/app/models/multi_model.rb ================================================ module MultiModel class Model1 < ActiveRecord::Base def model_exec system params[:user_input] end end class Model2 < ActiveRecord::Base def model_exec system params[:user_input2] end end end ================================================ FILE: test/apps/rails3.2/app/models/no_protection.rb ================================================ class NoProtection < ActiveRecord::Base # Leave this class empty for Rescanner tests end ================================================ FILE: test/apps/rails3.2/app/models/user/command_dependency.rb ================================================ class User def inner_exec system params[:user_input] end end ================================================ FILE: test/apps/rails3.2/app/models/user.rb ================================================ class User < ActiveRecord::Base require_dependency "user/command_dependency" attr_accessible :bio, :name, :account_id, :admin, :status_id end ================================================ FILE: test/apps/rails3.2/app/views/layouts/application.html.erb ================================================ Rails32 <%= stylesheet_link_tag "application", :media => "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/apps/rails3.2/app/views/removal/_partial.html.erb ================================================ <%= raw @some_other_input %> ================================================ FILE: test/apps/rails3.2/app/views/removal/controller_removed.html.erb ================================================ <%= @some_input %> <%= render 'partial' %> ================================================ FILE: test/apps/rails3.2/app/views/removal/implicit_render.html.erb ================================================ <%= @bad_stuff %> ================================================ FILE: test/apps/rails3.2/app/views/users/_form.html.erb ================================================ You: <%= about %> <%= form_for(@user) do |f| %> <% if @user.errors.any? %>

    <%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

      <% @user.errors.full_messages.each do |msg| %>
    • <%= msg %>
    • <% end %>
    <% end %>
    <%= f.label :name %>
    <%= f.text_field :name %>
    <%= f.label :bio %>
    <%= f.text_field :bio %>
    <%= f.submit %>
    <% end %> ================================================ FILE: test/apps/rails3.2/app/views/users/_slimmer.html.slim ================================================ - if some_value div = params[:escaped] - else span == params[:unescaped] p== @user.profile - if x = params[:unescaped] - else = params[:escaped] ================================================ FILE: test/apps/rails3.2/app/views/users/edit.html.erb ================================================

    Editing user

    <%= render 'form', :locals => { :about => raw(@user.bio) } %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails3.2/app/views/users/index.html.erb ================================================

    Listing users

    <% @users.each do |user| %> <% end %>
    Name Bio
    <%= user.name %> <%= user.bio %> <%= link_to 'Show', user %> <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, :method => :delete, :data => { :confirm => 'Are you sure?' } %>

    <%= link_to 'New User', new_user_path %> ================================================ FILE: test/apps/rails3.2/app/views/users/mixed_in.html.erb ================================================ <%= raw @user.something %> ================================================ FILE: test/apps/rails3.2/app/views/users/new.html.erb ================================================

    New user

    <%= render 'form' %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails3.2/app/views/users/sanitized.html.erb ================================================ ================================================ FILE: test/apps/rails3.2/app/views/users/show.html.erb ================================================

    <%= notice %>

    Name: <%= @user.name %>

    Bio: <%= @user.bio %>

    Other Thing: <%= @user_data %>

    <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails3.2/app/views/users/slimming.html.slim ================================================ #content .container h2 Search for: #{{@query}} p== @user.name == render 'slimmer' ================================================ FILE: test/apps/rails3.2/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' if defined?(Bundler) # If you precompile assets before deploying to production, use this line Bundler.require(*Rails.groups(:assets => %w(development test))) # If you want your assets lazily compiled in production, use this line # Bundler.require(:default, :assets, Rails.env) end module Rails32 class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. # config.autoload_paths += %W(#{config.root}/extras) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true # Use SQL instead of Active Record's schema dumper when creating the database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql # Enforce whitelist mode for mass assignment. # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. config.active_record.whitelist_attributes = false # Enable the asset pipeline config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' config.mess_things_up_in_prod = ActiveSupport::OrderedOptions.new config.actually_a_hash = {} end end ================================================ FILE: test/apps/rails3.2/config/boot.rb ================================================ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) ================================================ FILE: test/apps/rails3.2/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails3.2/config/environment.rb ================================================ # Load the rails application require File.expand_path('../application', __FILE__) # Initialize the rails application Rails32::Application.initialize! ================================================ FILE: test/apps/rails3.2/config/environments/development.rb ================================================ Rails32::Application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger config.active_support.deprecation = :log # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin # Raise exception on mass assignment protection for Active Record models config.active_record.mass_assignment_sanitizer = :strict # Log the query plan for queries taking more than this (works # with SQLite, MySQL, and PostgreSQL) config.active_record.auto_explain_threshold_in_seconds = 0.5 # Do not compress assets config.assets.compress = false # Expands the lines which load the assets config.assets.debug = true end ================================================ FILE: test/apps/rails3.2/config/environments/production.rb ================================================ Rails32::Application.configure do # Settings specified here will take precedence over those in config/application.rb # Code is not reloaded between requests config.cache_classes = true # Full error reports are disabled and caching is turned on config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = false # Compress JavaScripts and CSS config.assets.compress = true # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = true # Generate digests for assets URLs config.assets.digest = true # Defaults to nil and saved in location specified by config.assets.prefix # config.assets.manifest = YOUR_PATH # Specifies the header that your server uses for sending files # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # See everything in the log (default is :info) # config.log_level = :debug # Prepend all log lines with the following tags # config.log_tags = [ :subdomain, :uuid ] # Use a different logger for distributed setups # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false # Enable threaded mode # config.threadsafe! # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify # Log the query plan for queries taking more than this (works # with SQLite, MySQL, and PostgreSQL) # config.active_record.auto_explain_threshold_in_seconds = 0.5 config.active_record.whitelist_attributes = true # These configs actually resolve to values from application.rb # but that's atypical, so going to skip for now or else errors config.mess_things_up_in_prod.right_here = :nope_ignored config.actually_a_hash[:thing] = 'cool setting' end ================================================ FILE: test/apps/rails3.2/config/environments/test.rb ================================================ Rails32::Application.configure do # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Configure static asset server for tests with Cache-Control for performance config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" # Log error messages when you accidentally call methods on nil config.whiny_nils = true # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Raise exception on mass assignment protection for Active Record models config.active_record.mass_assignment_sanitizer = :strict # Print deprecation notices to the stderr config.active_support.deprecation = :stderr end ================================================ FILE: test/apps/rails3.2/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails3.2/config/initializers/header_dos_protection.rb ================================================ # https://groups.google.com/d/msg/ruby-security-ann/A-ebV4WxzKg/KNPTbX8XAQUJ ActiveSupport.on_load(:action_view) do ActionView::LookupContext::DetailsKey.class_eval do class << self alias :old_get :get def get(details) if details[:formats] details = details.dup syms = Set.new Mime::SET.symbols details[:formats] = details[:formats].select { |v| syms.include? v } end old_get details end end end end ================================================ FILE: test/apps/rails3.2/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails3.2/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails3.2/config/initializers/secret_token.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. Rails32::Application.config.secret_token = 'e721d0d7e8e912026b379d7219b5947da6a954f6c1b7c09ab7b44b873346ee17a780890e6d034fe6bd5ac52cced7b4ebe1971c3f34d0d1e735302b0bd4a0bd62' ================================================ FILE: test/apps/rails3.2/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails32::Application.config.session_store :cookie_store, :key => '_rails3.2_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") # Rails32::Application.config.session_store :active_record_store ================================================ FILE: test/apps/rails3.2/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters :format => [:json] end # Disable root element in JSON by default. ActiveSupport.on_load(:active_record) do self.include_root_in_json = false end ================================================ FILE: test/apps/rails3.2/config/locales/en.yml ================================================ # Sample localization file for English. Add more files in this directory for other locales. # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: hello: "Hello world" ================================================ FILE: test/apps/rails3.2/config/routes.rb ================================================ Rails32::Application.routes.draw do resources :users do get 'mixed_in' member do put 'update_password' => redirect('/settings/update_password') end end match 'remove' => 'removal#remove_this_too' match 'implicit' => 'removal#implicit_render' match 'exec' => 'exec#exec_this' # Routes for 'Default Routes' tests get "/glob/*action", controller: 'glob_get' post '/glob_post/*action', controller: 'glob_post' put 'glob_put/*action', controller: 'glob_put' match "/glob/*action", controller: 'glob_match' get "/foo_get/:action", controller: 'foo_get' post "/foo_post/:action", controller: 'foo_post' put 'foo_put/:action', controller: 'foo_put' match '/bar/:action', controller: 'bar_match' # The priority is based upon order of creation: # first created -> highest priority. # Sample of regular route: # match 'products/:id' => 'catalog#view' # Keep in mind you can assign values other than :controller and :action # Sample of named route: # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase # This route can be invoked with purchase_url(:id => product.id) # Sample resource route (maps HTTP verbs to controller actions automatically): # resources :products # Sample resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Sample resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Sample resource route with more complex sub-resources # resources :products do # resources :comments # resources :sales do # get 'recent', :on => :collection # end # end # Sample resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end # You can have the root of your site routed with "root" # just remember to delete public/index.html. # root :to => 'welcome#index' # See how all your routes lay out with "rake routes" # This is a legacy wild controller route that's not recommended for RESTful applications. # Note: This route will make all actions in every controller accessible via GET requests. # match ':controller(/:action(/:id))(.:format)' end ================================================ FILE: test/apps/rails3.2/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails32::Application ================================================ FILE: test/apps/rails3.2/lib/assets/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.2/lib/tasks/.gitkeep ================================================ ================================================ FILE: test/apps/rails3.2/lib/user_controller_mixin.rb ================================================ module UserControllerMixin def mixed_in @user = User.find(params[:id]) end def [] index end end ================================================ FILE: test/apps/rails3.2/script/rails ================================================ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) require File.expand_path('../../config/boot', __FILE__) require 'rails/commands' ================================================ FILE: test/apps/rails4/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/*.log /tmp ================================================ FILE: test/apps/rails4/Gemfile ================================================ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.0.0' gem 'pg' gem 'haml' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 4.0.0' gem 'coffee-rails', '~> 4.0.0' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 1.0.1' gem people do strange things end # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' # Use unicorn as the app server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano', group: :development # To use debugger # gem 'debugger' ================================================ FILE: test/apps/rails4/README.rdoc ================================================ == README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... Please feel free to use a different markup language if you do not plan to run rake doc:app. ================================================ FILE: test/apps/rails4/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails4::Application.load_tasks ================================================ FILE: test/apps/rails4/app/api/api.rb ================================================ module API def insecure_command_execution Open3.capture2 "ls #{params[:dir]}" end end ================================================ FILE: test/apps/rails4/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // //= require jquery //= require jquery_ujs //= require turbolinks //= require_tree . ================================================ FILE: test/apps/rails4/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self *= require_tree . */ ================================================ FILE: test/apps/rails4/app/controllers/another_controller.rb ================================================ class AnotherController < ApplicationController def overflow @test = @test.where.all end before_filter do eval params[:x] end skip_before_action :set_bad_thing, :except => [:also_use_bad_thing] def use_bad_thing # This should not warn, because the filter is skipped! User.where(@bad_thing) end def also_use_bad_thing `#{@bad_thing}` end def render_stuff user_name = User.current_user.name render :text => "Welcome back, #{params[:name]}!}" render :text => "Welcome back, #{user_name}!}" render :text => params[:q] render :text => user_name render :inline => "<%= #{params[:name]} %>" render :inline => "<%= #{user_name} %>" render :text => CGI.escapeHTML(params[:q]) # should not warn render :text => "Welcome back, #{CGI::escapeHTML(params[:name])}!}" # should not warn render :text => params[:q], :content_type => "text/plain" # should not warn end def use_params_in_regex @x = something.match /#{params[:x]}/ end def building_strings_for_sql query = "SELECT * FROM users WHERE" if params[:search].to_i == 1 query << " role = 'admin'" else query << " role = 'admin' " + params[:search] end begin result = {:result => User.find_by_sql(query) } rescue result = {} end render json: result.as_json end def safe_renders render params[:action] render params[:controller] end end ================================================ FILE: test/apps/rails4/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::API # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. # protect_from_forgery with: :exception def show_detailed_exceptions? true end def redirect_to_created_model if create @model = User.create @model.save! redirect_to @model else @model = User.create! @model.save redirect_to @model end end def bypass_ssl_check # Should warn on self.verify_mode = OpenSSL::SSL::VERIFY_NONE self.verify_mode = OpenSSL::SSL::VERIFY_NONE end before_action :set_bad_thing def set_bad_thing @bad_thing = params[:x] end def wrong_redirect_only_path redirect_to(params.bla.merge(:only_path => true, :display => nil)) end def redirect_only_path_with_unsafe_hash redirect_to(params.to_unsafe_hash.merge(:only_path => true, :display => nil)) end def redirect_only_path_with_unsafe_h redirect_to(params.to_unsafe_h.merge(:only_path => true, :display => nil)) end end ================================================ FILE: test/apps/rails4/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4/app/controllers/friendly_controller.rb ================================================ class FriendlyController some_helper_thing do @user = User.current_user end def find @user = User.friendly.find(params[:id]) redirect_to @user end def some_user_thing redirect_to @user.url end def try_and_send User.stuff.try(:where, params[:query]) User.send(:from, params[:table]).all end def mass_assign_user # Should warn about permit! x = params.permit! @user = User.new(x) end def mass_assign_protected_model # Warns with medium confidence because Account uses attr_accessible params.permit! Account.new(params) end def permit_without_usage # Warns with medium confidence because there is no mass assignment params.permit! end def permit_after_usage # Warns with medium confidence because permit! is called after mass assignment User.new(params) params.permit! end def sql_with_exec User.connection.select_values <<-SQL SELECT id FROM collection_items WHERE id > #{last_collection_item.id} AND collection_id IN (#{destinations.map { |d| d.id}.join(',')})" SQL Account.connection.select_rows("select thing.id, count(*) from things_stuff toc join things dotoc on (toc.id=dotoc.toc_id) join things do on (dotoc.data_object_id=do.id) join thing_entries dohe on do.id = dohe.data_object_id where do.published=#{params[:published]} and dohe.visibility_id=#{something.id} group by toc.id") end def redirect_to_some_places if something redirect_to params.merge(:host => "example.com") # Should not warn elsif something_else redirect_to params.merge(:host => User.canonical_url) # Should not warn else redirect_to params.merge(:host => params[:host]) # Should warn end end def select_some_stuff User.select(:name, params[:x]) end def send_some_stuff blah.send(params[:x]).to_json end private def private_some_stuff eval params[:what_is_this_java?] end def where_hashes User.where('stuff' => params[:stuff]) # no warning User.where(params[:key] => params[:stuff]) # warn end def whitelistit whitelist = ["Post", "Comments"] whitelisted_class_name = whitelist.detect {|k| k == params[:a]} if whitelisted_class_name.nil? raise "Nope!" else whitelisted_class_name.constantize end end end ================================================ FILE: test/apps/rails4/app/controllers/mixed_controller.rb ================================================ class MixedController < ApplicationController include ProxyThing::Proxied end ================================================ FILE: test/apps/rails4/app/controllers/mixed_in_proxy.rb ================================================ module ProxyThing class X; end module Proxied def self.included(controller) end end end ================================================ FILE: test/apps/rails4/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController def test_sql_sanitize User.where("age > #{sanitize params[:age]}") end before_action :set_page prepend_before_action :safe_set_page, :only => :test_prepend_before_action append_before_action :safe_set_page, :only => :test_append_before_action skip_before_action :verify_authenticity_token, :except => [:unsafe_stuff] def test_before_action render @page end # Call safe_set_page then set_page def test_prepend_before_action render @page # should not be safe end # Call set_page then safe_set_page def test_append_before_action render @page # should be safe end def set_page @page = params[:page] end def safe_set_page @page = :cool_page_bro end def redirect_to_model # None of these should warn in Rails 4 if stuff redirect_to User.find_by(:name => params[:name]) elsif other_stuff redirect_to User.find_by!(:name => params[:name]) else redirect_to User.where(:stuff => 1).take end end def find_by_stuff User.find_by "age > #{params[:age_limit]}" User.find_by! params[:user_search] end def symbolize_safe_parameters params[:controller].to_sym params[:action].intern && params[:controller][/([^\/]+)$/].try(:to_sym) end def mass_assignment_bypass User.create_with(params) # high warning User.create_with(params).create # high warning User.create_with(params[:x].permit(:y)) # should not warn, workaround something.create_with({}) # should not warn on hash literals x.create_with(y(params)) # medium warning y.create_with(x) # weak warning end def email_finds Email.find_by_id! params[:email][:id] end def case_statement @x = case params[:x] when :yes "yep" when :no "nope" else "dunno" end end def open_stuff open(params[:url]) # remote code execution warning Kernel.open(URI(params[:url])) # file access and RCE warning open("#{params[:x]}/something/something") # remote code execution warning open("some_path/#{params[:x]}/something/something") # file access warning end def eval_it @x = eval(params[:x]) end def session_key session[params[:x]] = params[:y] session["blah-#{params[:token]}"] = user.thing end def hash_some_things Digest::MD5.base64digest(params[:password]) Digest::HMAC.new('that', 'thing', Digest::SHA1) Digest::SHA1.new.update(thing) Digest::SHA1.digest(current_user.password + current_user.salt)[0,15] OpenSSL::Digest::Digest.new('md5') OpenSSL::Digest.new("SHA1") OpenSSL::Digest::MD5.digest(password) end def redirector redirect_to current_user.place.find(params[:p]) end def more_haml end def without User.new({username: "jjconti", admin: false}, without_protection: true) end def permit_in_sql User.find_by(params.permit(:OMG)) # Don't warn User.find_by(params.permit(:OMG)[:OMG]) # Warn User.where("#{params.permit(:OMG)}") # Warn end def exists_with_to_s User.exists? params[:x].to_s # Don't warn end def find_and_create_em # These all call find_by(), which we already know is dangerous User.find_or_create_by(params[:user]) User.find_or_create_by!(params[:user]) User.find_or_initialize_by(params[:user]) end def email_find_by Email.find_by id: params[:email][:id] Email.find_by! id: params[:email][:id] end def haml_test; end end ================================================ FILE: test/apps/rails4/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails4/app/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4/app/models/.keep ================================================ ================================================ FILE: test/apps/rails4/app/models/account.rb ================================================ class Account < ActiveRecord::Base attr_accessible :name, :account_id, :admin def sql_it_up_yeah connection.exec_update "UPDATE `purchases` SET type = '#{self.type}' WHERE id = '#{self.id}'" sql = "UPDATE #{self.class.table_name} SET latest_version = #{version} where id = #{self.id}" self.class.connection.execute sql end def self.more_sql_connection self.connection.exec_query "UPDATE `purchases` SET type = '#{self.type}' WHERE id = '#{self.id}'" end def safe_sql_should_not_warn self.class.connection.execute "DESCRIBE #{self.business_object.table_name}" connection.select_one "SELECT * FROM somewhere WHERE x=#{connection.quote(params[:x])}" connection.execute "DELETE FROM stuff WHERE id=#{self.id}" end def lots_of_string_building_sql sql = 'SELECT count(*) as account_count, '+ 'FROM account_links stuff_links '+ "WHERE account_links.stuff_id = #{@stuff.id} " if params[:more_ids] sql += " AND stuff IN "+ "(SELECT something_id "+ "FROM some_join_thing "+ "WHERE something_id IN (#{params[:more_ids].join(',')}))" end sql += "GROUP BY title, id " Account.connection.select_all(sql) end def self.get_all_countries(locale) q = "country_#{locale} ASC".to_s c = User.order(q) end end ================================================ FILE: test/apps/rails4/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4/app/models/email.rb ================================================ class Email < ActiveRecord::Base attr_accessible :email belongs_to :user EMAIL_REGEX = /^[a-z0-9]+@[a-z0-9]+\.[a-z]+$/ validates_format_of :email, with: EMAIL_REGEX scope :assigned_to_user, ->(user) { task_table = User.table_name joins("INNER JOIN #{task_table} ON #{task_table}.user_id = #{user.id} AND (#{task_table}.type_id = #{table_name}.type_id) AND (#{task_table}.manager_id = #{table_name}.manager_id) ") } end ================================================ FILE: test/apps/rails4/app/models/phone.rb ================================================ class Phone < ActiveRecord::Base PHONE_NUMBER_REGEXP = %r{ \A +\d+ # counter prefix \ * # space \(\d+\) # city code \ * # space (\d+-)*\d+ \z }x validates_format_of :number, with: PHONE_NUMBER_REGEXP end ================================================ FILE: test/apps/rails4/app/models/recursive/stack_level.rb ================================================ class Exception < Exception end class DescendentException < Exception end class ExceptionA < ExceptionB end class ExceptionB < ExceptionA end ================================================ FILE: test/apps/rails4/app/models/user.rb ================================================ class User < ActiveRecord::Base def test_sql_sanitize(x) self.select("#{sanitize(x)}") end scope :hits_by_ip, ->(ip,col="*") { select("#{col}").order("id DESC") } def arel_exists where(User.where(User.arel_table[:object_id].eq(arel_table[:id])).exists) end def symbol_stuff self.where(User.table_name.to_sym) end scope :sorted_by, ->(field, asc) { asc = ['desc', 'asc'].include?(asc) ? asc : 'asc' ordering = if field == 'extension' "substring_index(#{table_name}.data_file_name, '.', -1) #{asc}" elsif SORTABLE_COLUMNS.include?(field) { field.to_sym => asc.to_sym } end order(ordering) # should not warn about `asc` interpolation } def much_arel # None of these should warn group_recipient = User.joins(:group).where(User.arel_table[:message_id].eq Email.arel_table[:id]) group_with_special_property = group_recipient.where(:groups => { :private => false, :special_property => true }) Email.where(group_recipient.exists.not.or(group_with_special_property.exists)) User.select('users.id').joins(User.joins(:deal_purchases).join_sources).where(Email.arel_table[:created_at].gt(last_activity)).group('users.id') User.where(User.joins(:group).where(User.arel_table[:message_id].eq arel_table[:id])) User.from(User.all) User.from(User.active.order(created_at: :desc).limit(30)) User.from(User.active.order(created_at: :desc)) User.from(User.project(users[:age].average.as("mean_age"))) User.from(User.group(user[:user_id]).having(thing[:id].count.gt(5))) end def self.encrypt_pass password Base64.encode64(Digest::MD5.hexdigest(password)) end accepts_nested_attributes_for :something, allow_destroy: false, reject_if: proc { |attributes| stuff } def more_symbol_stuff stuff User.find(stuff).attributes.symbolize_keys # meh, don't warn end end ================================================ FILE: test/apps/rails4/app/views/_global_partial.html.erb ================================================ <%= render 'something' %> ================================================ FILE: test/apps/rails4/app/views/another/html_safe_is_not.html.erb ================================================ <%= params[:x].html_safe %> ================================================ FILE: test/apps/rails4/app/views/another/overflow.html.erb ================================================ <% @test.each do |i| %> <%= i %> <% end %> ================================================ FILE: test/apps/rails4/app/views/another/use_params_in_regex.html.erb ================================================ <%= @x %> ================================================ FILE: test/apps/rails4/app/views/another/various_xss.html.erb ================================================ <%= link_to 'stuff', cool_thing_url(params[:x]) %> Don't warn <%= link_to 'stuff', User.find(params[:id]).home_url %> Warn <%= link_to 'stuff', User.find(params[:id]).home_path %> Don't warn <%= link_to 'other stuff', home_path(User.find(params[:id])) %> Don't warn <%= raw(x(params[:x])) %> Warn with warning code 2 not 5 <% form_for User.first do |t| %> <%== t.select(things, User.new(params[:t]).stuff) %> <% end %> <%= link_to 'Bars', current_user.bars.find(params[:id]) %> Don't warn ================================================ FILE: test/apps/rails4/app/views/layouts/application.html.erb ================================================ Rails4 <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> <%= javascript_include_tag "application", "data-turbolinks-track" => true %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/apps/rails4/app/views/users/eval_it.html.erb ================================================ <%= @x %> ================================================ FILE: test/apps/rails4/app/views/users/haml_test.html.haml ================================================ #content .some.stuff %p= params[:x] #innerstuff %h1= raw params[:y] =" #{User.first.name.html_safe}" :javascript var import_file_upload_id = "#{j(params[:id1])}"; :coffeescript import_file_upload_id_coffee = "#{j(params[:id2])}" ================================================ FILE: test/apps/rails4/app/views/users/index.html.erb ================================================ <%= raw User.find(params[:id]).to_json %> <%= raw inside_something(User.find(params[:id]).to_json) %> <%= raw call_something(params).to_json %> <%= raw params[:stuff].to_json %> <%= number_to_currency(params[:cost], params[:currency]) %> <%= number_to_human(params[:cost], format: h(params[:format])) %> Should not warn <%= number_to_percentage(params[:cost], negative_format: params[:format]) %> <%= render Thing.new(content: render(partial: "stuff")) %> <%== params[:double] %> <%== params[:x] == 1 %> ================================================ FILE: test/apps/rails4/app/views/users/more_haml.html.haml ================================================ %body :javascript $(function() { #{ # Ticket #9999 # my variable needs to be number 4 } var myVar = 4; }); ================================================ FILE: test/apps/rails4/app/views/users/test_parse.html.erb ================================================ Testing double == <%== %{t="#{stuff unless other? }"} if current_user %> ================================================ FILE: test/apps/rails4/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/apps/rails4/bin/rake ================================================ #!/usr/bin/env ruby require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/apps/rails4/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' # Assets should be precompiled for production (so we don't need the gems loaded then) Bundler.require(*Rails.groups(assets: %w(development test))) module Rails4 class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de end end ================================================ FILE: test/apps/rails4/config/boot.rb ================================================ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) ================================================ FILE: test/apps/rails4/config/brakeman.ignore ================================================ { "ignored_warnings": [ { "warning_type": "Mass Assignment", "warning_code": 60, "fingerprint": "cd83ecf615b17f849ba28050e7faf1d54f218dfa9435c3f65f47cb378c18cf98", "message": "Potentially dangerous attribute available for mass assignment", "file": "app/models/account.rb", "line": null, "link": "http://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": ":admin", "render_path": null, "location": { "type": "model", "model": "Account" }, "user_input": null, "confidence": "High", "note": "skipping this for a test" }, { "warning_type": "Mass Assignment", "warning_code": 60, "fingerprint": "abcdef01234567890ba28050e7faf1d54f218dfa9435c3f65f47cb378c18cf98", "message": "Potentially dangerous attribute available for mass assignment", "file": "app/models/account.rb", "line": null, "link": "http://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": ":admin", "render_path": null, "location": { "type": "model", "model": "Account" }, "user_input": null, "confidence": "High", "note": "this error should be detected as obsolete" } ], "brakeman_version": "2.3.1" } ================================================ FILE: test/apps/rails4/config/brakeman.yml ================================================ --- :run_all_checks: true :additional_libs_path: - app/api/ :rails4: true ================================================ FILE: test/apps/rails4/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails4/config/environment.rb ================================================ # Load the rails application. require File.expand_path('../application', __FILE__) # Initialize the rails application. Rails4::Application.initialize! ================================================ FILE: test/apps/rails4/config/environments/development.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. config.assets.debug = true end ================================================ FILE: test/apps/rails4/config/environments/production.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both thread web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = true config.action_controller.perform_caching = true # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true # Disable Rails's static asset server (Apache or nginx will already do this). config.serve_static_assets = true # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Whether to fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Generate digests for assets URLs. config.assets.digest = true # Version of your assets, change this if you want to expire all your assets. config.assets.version = '1.0' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Set to :debug to see everything in the log. config.log_level = :info # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production. # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = "http://assets.example.com" # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # config.assets.precompile += %w( search.js ) # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new end ================================================ FILE: test/apps/rails4/config/environments/test.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr end ================================================ FILE: test/apps/rails4/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails4/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails4/config/initializers/i18n.rb ================================================ require 'i18n' # Override exception handler to more carefully html-escape missing-key results. class HtmlSafeI18nExceptionHandler Missing = I18n.const_defined?(:MissingTranslation) ? I18n::MissingTranslation : I18n::MissingTranslationData def initialize(original_exception_handler) @original_exception_handler = original_exception_handler end def call(exception, locale, key, options) if exception.is_a?(Missing) && options[:rescue_format] == :html keys = exception.keys.map { |k| Rack::Utils.escape_html k } key = keys.last.to_s.gsub('_', ' ').gsub(/\b('?[a-z])/) { $1.capitalize } %(#{key}) else @original_exception_handler.call(exception, locale, key, options) end end end ================================================ FILE: test/apps/rails4/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails4/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails4/config/initializers/secret_token.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure your secret_key_base is kept private # if you're sharing your code publicly. Rails4::Application.config.secret_key_base = '3d90f727dcc14992232b9461fac5d31cf2bc184854e0afd90ae67e0ae48f22b676ee2529c84d4c23bc2a9c7be6eeefcf202b91ccb8d04e7b87a85c852f6784d6' ================================================ FILE: test/apps/rails4/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails4::Application.config.session_store :encrypted_cookie_store, key: '_rails4_session' ================================================ FILE: test/apps/rails4/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails4/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails4/config/routes.rb ================================================ something.root Rails4::Application.routes.draw do resources not_a_symbol, :controller => :whatever namespace also_not_a_symbol do resource :thing end get ':controller/stuff/:id/:user_id' # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". # You can have the root of your site routed with "root" # root to: 'welcome#index' # Example of regular route: # get 'products/:id' => 'catalog#view' # Example of named route that can be invoked with purchase_url(id: product.id) # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase # Example resource route (maps HTTP verbs to controller actions automatically): # resources :products # Example resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Example resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Example resource route with more complex sub-resources: # resources :products do # resources :comments # resources :sales do # get 'recent', on: :collection # end # end # Example resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end end ================================================ FILE: test/apps/rails4/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. development: secret_key_base: 4e0b21385c66e9e226bb066a8ca5a6fed0211228a81c1b986b9ec0f9719df67b1ddbeb435393121262493f171987318e5853bdfd9b7e1c17b3f6bc3a7c1fa8aa test: secret_key_base: d73778c248636d5540d8569e3cabf740b8ca85acc4fc5e4db5063386cbe9a68df3138ceb6b48fc7702300499a6a5626a5f7ba7649ca1f3c2c941cca128dd8c16 # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: super_duper_secret_key ================================================ FILE: test/apps/rails4/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails4::Application ================================================ FILE: test/apps/rails4/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) ================================================ FILE: test/apps/rails4/external_checks/check_external_check_test.rb ================================================ require 'brakeman/checks/base_check' #Verify that checks external to the checks/ dir are added by the additional_checks_path options flag class Brakeman::CheckExternalCheckTest < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "An external check that does nothing, used for testing" def run_check tracker.find_call(target: nil, method: :call_shady_method).each do |result| if user_input = has_immediate_user_input?(result[:call].first_arg) warn result: result, warning_type: "Shady Call", warning_code: :custom_check, message: "Called something shady!", confidence: :high, user_input: user_input, :cwe_id => [-1] end end end end ================================================ FILE: test/apps/rails4/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails4/lib/sweet_lib.rb ================================================ class SweetLib def do_some_cool_stuff bad `ls #{bad}` end def test_command_injection_in_lib IO.popen(['ls', params[:id]]) #Should not warn system("rm #{@bad}") #Should warn about command injection end def test_net_http_start_ssl Net::HTTP.start(uri.host, uri.port, :use_ssl => true, :verify_mode => OpenSSL::SSL::VERIFY_NONE) end def external_check_test call_shady_method(params[:x]) end end ================================================ FILE: test/apps/rails4/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails4/lib/tasks/some_task.rb ================================================ class SomeTask def some_task # Should not warn because we are ignoring tasks `#{x}` end end ================================================ FILE: test/apps/rails4/log/.keep ================================================ ================================================ FILE: test/apps/rails4/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

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

    ================================================ FILE: test/apps/rails4/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

    ================================================ FILE: test/apps/rails4/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

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

    ================================================ FILE: test/apps/rails4/public/robots.txt ================================================ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-agent: * # Disallow: / ================================================ FILE: test/apps/rails4/test/controllers/.keep ================================================ ================================================ FILE: test/apps/rails4/test/fixtures/.keep ================================================ ================================================ FILE: test/apps/rails4/test/helpers/.keep ================================================ ================================================ FILE: test/apps/rails4/test/integration/.keep ================================================ ================================================ FILE: test/apps/rails4/test/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4/test/models/.keep ================================================ ================================================ FILE: test/apps/rails4/test/test_helper.rb ================================================ ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase ActiveRecord::Migration.check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails4/vendor/assets/javascripts/.keep ================================================ ================================================ FILE: test/apps/rails4/vendor/assets/stylesheets/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/*.log /tmp ================================================ FILE: test/apps/rails4_non_standard_structure/README.rdoc ================================================ == README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... Please feel free to use a different markup language if you do not plan to run rake doc:app. ================================================ FILE: test/apps/rails4_non_standard_structure/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks ================================================ FILE: test/apps/rails4_non_standard_structure/app/assets/images/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details // about supported directives. // //= require jquery //= require jquery_ujs //= require turbolinks //= require_tree . ================================================ FILE: test/apps/rails4_non_standard_structure/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any styles * defined in the other CSS/SCSS files in this directory. It is generally better to create a new * file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/apps/rails4_non_standard_structure/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception end ================================================ FILE: test/apps/rails4_non_standard_structure/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/foo_team/controllers/api/foo_controller.rb ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/foo_team/models/foo.rb ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/foo_team/views/foo.html.erb ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails4_non_standard_structure/app/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/models/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/app/views/layouts/application.html.erb ================================================ Rails4NonStandardStructure <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/apps/rails4_non_standard_structure/bin/rails ================================================ #!/usr/bin/env ruby begin load File.expand_path("../spring", __FILE__) rescue LoadError end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/apps/rails4_non_standard_structure/bin/rake ================================================ #!/usr/bin/env ruby begin load File.expand_path("../spring", __FILE__) rescue LoadError end require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/apps/rails4_non_standard_structure/bin/spring ================================================ #!/usr/bin/env ruby # This file loads spring without using Bundler, in order to be fast # It gets overwritten when you run the `spring binstub` command unless defined?(Spring) require "rubygems" require "bundler" if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) ENV["GEM_HOME"] = "" Gem.paths = ENV gem "spring", match[1] require "spring/binstub" end end ================================================ FILE: test/apps/rails4_non_standard_structure/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails4NonStandardStructure class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de end end ================================================ FILE: test/apps/rails4_non_standard_structure/config/boot.rb ================================================ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) ================================================ FILE: test/apps/rails4_non_standard_structure/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' # default: &default adapter: sqlite3 pool: 5 timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 ================================================ FILE: test/apps/rails4_non_standard_structure/config/environment.rb ================================================ # Load the Rails application. require File.expand_path('../application', __FILE__) # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails4_non_standard_structure/config/environments/development.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. config.assets.raise_runtime_errors = true # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end ================================================ FILE: test/apps/rails4_non_standard_structure/config/environments/production.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true # Disable Rails's static asset server (Apache or nginx will already do this). config.serve_static_assets = false # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Generate digests for assets URLs. config.assets.digest = true # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Set to :debug to see everything in the log. config.log_level = :info # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production. # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = "http://assets.example.com" # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end ================================================ FILE: test/apps/rails4_non_standard_structure/config/environments/test.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_rails4_non_standard_structure_session' ================================================ FILE: test/apps/rails4_non_standard_structure/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails4_non_standard_structure/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails4_non_standard_structure/config/routes.rb ================================================ Rails.application.routes.draw do # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". # You can have the root of your site routed with "root" # root 'welcome#index' # Example of regular route: # get 'products/:id' => 'catalog#view' # Example of named route that can be invoked with purchase_url(id: product.id) # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase # Example resource route (maps HTTP verbs to controller actions automatically): # resources :products # Example resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Example resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Example resource route with more complex sub-resources: # resources :products do # resources :comments # resources :sales do # get 'recent', on: :collection # end # end # Example resource route with concerns: # concern :toggleable do # post 'toggle' # end # resources :posts, concerns: :toggleable # resources :photos, concerns: :toggleable # Example resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end end ================================================ FILE: test/apps/rails4_non_standard_structure/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. development: secret_key_base: f7810c6ed73622446438bbeb969d38d98baf5e9135c33edff60c21921beafd14cad58e908fdb25a214803d0231c981a380fca97d7877707b73db75df26be04e8 test: secret_key_base: ccc74872aaac828721f78621140e09c8d7186cab13887743429e2efe9ec4402e72adb2dd194a7507c13bf19bbbebf47b3570c729302fb18c706691de0e5dd979 # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ================================================ FILE: test/apps/rails4_non_standard_structure/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails.application ================================================ FILE: test/apps/rails4_non_standard_structure/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) ================================================ FILE: test/apps/rails4_non_standard_structure/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/log/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

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

    ================================================ FILE: test/apps/rails4_non_standard_structure/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

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

    ================================================ FILE: test/apps/rails4_non_standard_structure/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

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

    ================================================ FILE: test/apps/rails4_non_standard_structure/public/robots.txt ================================================ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-agent: * # Disallow: / ================================================ FILE: test/apps/rails4_non_standard_structure/rails4test.gemspec ================================================ Gem::Specification.new do |s| s.name = "rails4test" s.version = "0.0.1" s.summary = "Testing Brakeman with Rails 4" s.description = "And this file is to test Brakeman's handling of gemspecs" s.add_dependency 'rails', '>= 4.1.8' s.add_dependency 'haml' end ================================================ FILE: test/apps/rails4_non_standard_structure/test/controllers/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/fixtures/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/helpers/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/integration/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/models/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/test/test_helper.rb ================================================ ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails4_non_standard_structure/vendor/assets/javascripts/.keep ================================================ ================================================ FILE: test/apps/rails4_non_standard_structure/vendor/assets/stylesheets/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/README.rdoc ================================================ == README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... Please feel free to use a different markup language if you do not plan to run rake doc:app. ================================================ FILE: test/apps/rails4_with_engines/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails4::Application.load_tasks ================================================ FILE: test/apps/rails4_with_engines/alt_engines/admin_stuff/app/controllers/admin_controller.rb ================================================ class AdminController < ApplicationController def debug params[:class].classify.constantize.send(params[:meth]) end end ================================================ FILE: test/apps/rails4_with_engines/alt_engines/admin_stuff/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails4_with_engines/alt_engines/admin_stuff/app/views/admin/debug.html.erb ================================================ <%= raw params[:debug] %> ================================================ FILE: test/apps/rails4_with_engines/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // //= require jquery //= require jquery_ujs //= require turbolinks //= require_tree . ================================================ FILE: test/apps/rails4_with_engines/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the top of the * compiled file, but it's generally better to create a new file per style scope. * *= require_self *= require_tree . */ ================================================ FILE: test/apps/rails4_with_engines/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery end ================================================ FILE: test/apps/rails4_with_engines/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails4_with_engines/app/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/app/models/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/app/views/layouts/application.html.erb ================================================ Rails4 <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> <%= javascript_include_tag "application", "data-turbolinks-track" => true %> <%= csrf_meta_tags %> <%= yield %> ================================================ FILE: test/apps/rails4_with_engines/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/apps/rails4_with_engines/bin/rake ================================================ #!/usr/bin/env ruby require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/apps/rails4_with_engines/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' # Assets should be precompiled for production (so we don't need the gems loaded then) Bundler.require(*Rails.groups(assets: %w(development test))) module Rails4 class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de end end ================================================ FILE: test/apps/rails4_with_engines/config/boot.rb ================================================ # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) ================================================ FILE: test/apps/rails4_with_engines/config/brakeman.yml ================================================ --- :engine_paths: - engines/user_removal - alt_engines/* ================================================ FILE: test/apps/rails4_with_engines/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails4_with_engines/config/environment.rb ================================================ # Load the rails application. require File.expand_path('../application', __FILE__) # Initialize the rails application. Rails4::Application.initialize! ================================================ FILE: test/apps/rails4_with_engines/config/environments/development.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. config.assets.debug = true end ================================================ FILE: test/apps/rails4_with_engines/config/environments/production.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both thread web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true # Disable Rails's static asset server (Apache or nginx will already do this). config.serve_static_assets = false # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Whether to fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Generate digests for assets URLs. config.assets.digest = true # Version of your assets, change this if you want to expire all your assets. config.assets.version = '1.0' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Set to :debug to see everything in the log. config.log_level = :info # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production. # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = "http://assets.example.com" # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # config.assets.precompile += %w( search.js ) # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Enforce whitelist mode for mass assignment. (now used by protected_attributes gem) # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. config.active_record.whitelist_attributes = false # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new end ================================================ FILE: test/apps/rails4_with_engines/config/environments/test.rb ================================================ Rails4::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr end ================================================ FILE: test/apps/rails4_with_engines/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails4_with_engines/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails4_with_engines/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails4_with_engines/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails4_with_engines/config/initializers/nested_attributes_bypass_fix.rb ================================================ module ActiveRecord module NestedAttributes private def reject_new_record?(association_name, attributes) will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) end def call_reject_if(association_name, attributes) return false if will_be_destroyed?(association_name, attributes) case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc callback.call(attributes) end end def will_be_destroyed?(association_name, attributes) allow_destroy?(association_name) && has_destroy_flag?(attributes) end def allow_destroy?(association_name) self.nested_attributes_options[association_name][:allow_destroy] end end end ================================================ FILE: test/apps/rails4_with_engines/config/initializers/secret_token.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure your secret_key_base is kept private # if you're sharing your code publicly. Rails4::Application.config.secret_key_base = '3d90f727dcc14992232b9461fac5d31cf2bc184854e0afd90ae67e0ae48f22b676ee2529c84d4c23bc2a9c7be6eeefcf202b91ccb8d04e7b87a85c852f6784d6' ================================================ FILE: test/apps/rails4_with_engines/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails4::Application.config.session_store :encrypted_cookie_store, key: '_rails4_session' ================================================ FILE: test/apps/rails4_with_engines/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails4_with_engines/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails4_with_engines/config/routes.rb ================================================ Rails4::Application.routes.draw do # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". # You can have the root of your site routed with "root" # root to: 'welcome#index' # Example of regular route: # get 'products/:id' => 'catalog#view' # Example of named route that can be invoked with purchase_url(id: product.id) # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase # Example resource route (maps HTTP verbs to controller actions automatically): # resources :products # Example resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Example resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Example resource route with more complex sub-resources: # resources :products do # resources :comments # resources :sales do # get 'recent', on: :collection # end # end # Example resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end end ================================================ FILE: test/apps/rails4_with_engines/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails4::Application ================================================ FILE: test/apps/rails4_with_engines/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/assets/javascripts/users.js.coffee ================================================ # Place all the behaviors and hooks related to the matching controller here. # All this logic will automatically be available in application.js. # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/assets/stylesheets/users.css.scss ================================================ // Place all the styles related to the Users controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/controllers/base_controller.rb ================================================ class BaseController < ActionController::Base # missing protect_from_forgery call end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/controllers/removal_controller.rb ================================================ class RemovalController < ApplicationController def change_lines <<-X this method is here for line numbers X end def remove_this redirect_to params[:url] end def remove_this_too @some_input = raw params[:input] @some_other_input = Account.first.name render 'removal/controller_removed' end def implicit_render @bad_stuff = raw params[:bad_stuff] end end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController include UserControllerMixin # GET /users # GET /users.json def index @users = User.all respond_to do |format| format.html # index.html.erb format.json { render :json => @users } end end # GET /users/1 # GET /users/1.json def show @user = User.find(params[:id]) @user_data = raw params[:user_data] respond_to do |format| format.html # show.html.erb format.json { render :json => @user } end end # GET /users/new # GET /users/new.json def new @user = User.new respond_to do |format| format.html # new.html.erb format.json { render :json => @user } end end # GET /users/1/edit def edit @user = User.find(params[:id]) end # POST /users # POST /users.json def create @user = User.new(params[:user]) respond_to do |format| if @user.save format.html { redirect_to @user, :notice => 'User was successfully created.' } format.json { render :json => @user, :status => :created, :location => @user } else format.html { render :action => "new" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # PUT /users/1 # PUT /users/1.json def update @user = User.find(params[:id]) respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to @user, :notice => 'User was successfully updated.' } format.json { head :no_content } else format.html { render :action => "edit" } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.json def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.html { redirect_to users_url } format.json { head :no_content } end end def slimming @user = User.find(params[:id]) @query = params[:query] end end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/models/.gitkeep ================================================ ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/models/account.rb ================================================ class Account < ActiveRecord::Base attr_accessible :plan_id, :banned end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/models/no_protection.rb ================================================ class NoProtection < ActiveRecord::Base # Leave this class empty for Rescanner tests end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/models/user.rb ================================================ class User < ActiveRecord::Base attr_accessible :bio, :name, :account_id, :admin, :status_id accepts_nested_attributes_for :something, allow_destroy: false, reject_if: proc { |attributes| stuff } end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/removal/_partial.html.erb ================================================ <%= raw @some_other_input %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/removal/controller_removed.html.erb ================================================ <%= @some_input %> <%= render 'partial' %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/removal/implicit_render.html.erb ================================================ <%= @bad_stuff %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/_form.html.erb ================================================ You: <%= about %> <%= form_for(@user) do |f| %> <% if @user.errors.any? %>

    <%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

      <% @user.errors.full_messages.each do |msg| %>
    • <%= msg %>
    • <% end %>
    <% end %>
    <%= f.label :name %>
    <%= f.text_field :name %>
    <%= f.label :bio %>
    <%= f.text_field :bio %>
    <%= f.submit %>
    <% end %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/_slimmer.html.slim ================================================ - if some_value div = params[:escaped] - else span == params[:unescaped] p== @user.profile - if x = params[:unescaped] - else = params[:escaped] ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/edit.html.erb ================================================

    Editing user

    <%= render 'form', :locals => { :about => raw(@user.bio) } %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/index.html.erb ================================================

    Listing users

    <% @users.each do |user| %> <% end %>
    Name Bio
    <%= user.name %> <%= user.bio %> <%= link_to 'Show', user %> <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, :method => :delete, :data => { :confirm => 'Are you sure?' } %>

    <%= link_to 'New User', new_user_path %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/mixed_in.html.erb ================================================ <%= raw @user.something %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/new.html.erb ================================================

    New user

    <%= render 'form' %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/sanitized.html.erb ================================================ ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/show.html.erb ================================================

    <%= notice %>

    Name: <%= @user.name %>

    Bio: <%= @user.bio %>

    Other Thing: <%= @user_data %>

    Stuff I like: <%= simple_format(@user.likes, :class => "likes") %> <%= simple_format("some string", :color => params[:color]) %> <%= simple_format("some string", :id => h(params[:color])) %> should not warn

    <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/app/views/users/slimming.html.slim ================================================ #content .container h2 Search for: #{{@query}} p== @user.name == render 'slimmer' ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/config/routes.rb ================================================ Rails32::Application.routes.draw do resources :users do get 'mixed_in' end match 'remove' => 'removal#remove_this_too' match 'implicit' => 'removal#implicit_render' # The priority is based upon order of creation: # first created -> highest priority. # Sample of regular route: # match 'products/:id' => 'catalog#view' # Keep in mind you can assign values other than :controller and :action # Sample of named route: # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase # This route can be invoked with purchase_url(:id => product.id) # Sample resource route (maps HTTP verbs to controller actions automatically): # resources :products # Sample resource route with options: # resources :products do # member do # get 'short' # post 'toggle' # end # # collection do # get 'sold' # end # end # Sample resource route with sub-resources: # resources :products do # resources :comments, :sales # resource :seller # end # Sample resource route with more complex sub-resources # resources :products do # resources :comments # resources :sales do # get 'recent', :on => :collection # end # end # Sample resource route within a namespace: # namespace :admin do # # Directs /admin/products/* to Admin::ProductsController # # (app/controllers/admin/products_controller.rb) # resources :products # end # You can have the root of your site routed with "root" # just remember to delete public/index.html. # root :to => 'welcome#index' # See how all your routes lay out with "rake routes" # This is a legacy wild controller route that's not recommended for RESTful applications. # Note: This route will make all actions in every controller accessible via GET requests. # match ':controller(/:action(/:id))(.:format)' end ================================================ FILE: test/apps/rails4_with_engines/engines/user_removal/lib/user_removal.rb ================================================ module UserRemoval class Engine < Rails::Engine initializer :assets do |config| Rails.application.config.assets.precompile += Dir.glob(root.join('app/assets/stylesheets/**/*.css*')).collect {|f| f.gsub(%r{.*/app/assets/stylesheets/}, "").gsub(/\.css.*/, '.css') } Rails.application.config.assets.precompile += Dir.glob(root.join('app/assets/javascripts/**/*.js')).collect {|f| f.gsub(%r{.*/app/assets/javascripts/}, "") } end end end ================================================ FILE: test/apps/rails4_with_engines/gems.rb ================================================ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.0.0' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 4.0.0' gem 'coffee-rails', '~> 4.0.0' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 1.0.1' # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' # Use unicorn as the app server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano', group: :development # To use debugger # gem 'debugger' ================================================ FILE: test/apps/rails4_with_engines/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/log/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

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

    ================================================ FILE: test/apps/rails4_with_engines/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

    ================================================ FILE: test/apps/rails4_with_engines/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

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

    ================================================ FILE: test/apps/rails4_with_engines/public/robots.txt ================================================ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-agent: * # Disallow: / ================================================ FILE: test/apps/rails4_with_engines/script/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/controllers/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/fixtures/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/helpers/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/integration/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/mailers/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/models/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/test/test_helper.rb ================================================ ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase ActiveRecord::Migration.check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails4_with_engines/vendor/assets/javascripts/.keep ================================================ ================================================ FILE: test/apps/rails4_with_engines/vendor/assets/stylesheets/.keep ================================================ ================================================ FILE: test/apps/rails5/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore Byebug command history file. .byebug_history ================================================ FILE: test/apps/rails5/Gemfile ================================================ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '>= 5.0.0.beta1', '< 5.1' # Use sqlite3 as the database for Active Record gem 'sqlite3' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.1.0' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby # Use jquery as the JavaScript library gem 'jquery-rails' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.0' # Use Puma as the app server gem 'puma' gem 'actionpack-page_caching', '1.2.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 3.0' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] ================================================ FILE: test/apps/rails5/README.md ================================================ ## README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... ================================================ FILE: test/apps/rails5/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks ================================================ FILE: test/apps/rails5/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css ================================================ FILE: test/apps/rails5/app/assets/images/.keep ================================================ ================================================ FILE: test/apps/rails5/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require jquery //= require jquery_ujs //= require turbolinks //= require_tree . ================================================ FILE: test/apps/rails5/app/assets/javascripts/cable.coffee ================================================ # Action Cable provides the framework to deal with WebSockets in Rails. # You can generate new channels where WebSocket features live using the rails generate channel command. # # Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb). # #= require action_cable #= require_self #= require_tree ./channels # # @App ||= {} # App.cable = ActionCable.createConsumer() ================================================ FILE: test/apps/rails5/app/assets/javascripts/channels/.keep ================================================ ================================================ FILE: test/apps/rails5/app/assets/javascripts/users.coffee ================================================ # Place all the behaviors and hooks related to the matching controller here. # All this logic will automatically be available in application.js. # You can use CoffeeScript in this file: http://coffeescript.org/ ================================================ FILE: test/apps/rails5/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/apps/rails5/app/assets/stylesheets/scaffold.css ================================================ body { background-color: #fff; color: #333; } body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; margin: 33px; } pre { background-color: #eee; padding: 10px; font-size: 11px; } a { color: #000; } a:visited { color: #666; } a:hover { color: #fff; background-color: #000; } th { padding-bottom: 5px; } td { padding-bottom: 7px; padding-left: 5px; padding-right: 5px; } div.field, div.actions { margin-bottom: 10px; } #notice { color: green; } .field_with_errors { padding: 2px; background-color: red; display: table; } #error_explanation { width: 450px; border: 2px solid red; padding: 7px; padding-bottom: 0; margin-bottom: 20px; background-color: #f0f0f0; } #error_explanation h2 { text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; margin-bottom: 0; background-color: #c00; color: #fff; } #error_explanation ul li { font-size: 12px; list-style: square; } label { display: block; } ================================================ FILE: test/apps/rails5/app/assets/stylesheets/users.css ================================================ /* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ ================================================ FILE: test/apps/rails5/app/channels/application_cable/channel.rb ================================================ # Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading. module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/apps/rails5/app/channels/application_cable/connection.rb ================================================ # Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading. module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/apps/rails5/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception end ================================================ FILE: test/apps/rails5/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails5/app/controllers/concerns/concerning.rb ================================================ module Concerning extend ActiveSupport::Concern included do include Concerning end end ================================================ FILE: test/apps/rails5/app/controllers/concerns/forgery_protection.rb ================================================ module ForgeryProtection extend ActiveSupport::Concern included do protect_from_forgery with: :exception end end ================================================ FILE: test/apps/rails5/app/controllers/file_controller.rb ================================================ class FileController < ApplicationController def download_tempfile_with_params send_file Tempfile.new([params[:file_name], ".txt"]) end def download_sanitized_with_params send_file ActiveStorage::Filename.new("#{params[:file_name]}.jpg").sanitized end end ================================================ FILE: test/apps/rails5/app/controllers/mixed_controller.rb ================================================ class BaseController < ActionController::Base # No protect_from_forgery call, but one mixed in include ForgeryProtection include Concerning Statistics::AdminWithdrawal::BANK_LIST = [:deutsche, :boa, :jpm_chase, :cyprus] def another_early_return bank_name = params[:filename].first unless Statistics::AdminWithdrawal::BANK_LIST.include?(bank_name) flash[:alert] = 'Invalid filename' redirect_to :back return end Statistics::AdminWithdrawal.send("export_#{bank_name}_#inc!") end def yet_another_early_return scope_name = params[:scope].presence fail ActiveRecord::RecordNotFound unless ['safe', 'also_safe'].include?(scope_name) Model.public_send(scope_name) end def redirect_to_strong_params redirect_to params.permit(:domain) # should warn redirect_to params.permit(:page, :sort) # should not warn end end ================================================ FILE: test/apps/rails5/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] # GET /users # GET /users.json def index @users = User.all end # GET /users/1 # GET /users/1.json def show end # GET /users/new def new @user = User.new end # GET /users/1/edit def edit end # POST /users # POST /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to @user, notice: 'User was successfully created.' } format.json { render :show, status: :created, location: @user } else format.html { render :new } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # PATCH/PUT /users/1 # PATCH/PUT /users/1.json def update respond_to do |format| if @user.update(user_params) format.html { redirect_to @user, notice: 'User was successfully updated.' } format.json { render :show, status: :ok, location: @user } else format.html { render :edit } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.json def destroy @user.destroy respond_to do |format| format.html { redirect_to users_url, notice: 'User was successfully destroyed.' } format.json { head :no_content } end end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Never trust parameters from the scary internet, only allow the white list through. def user_params params[:user] end def ruby_230 x&.send(params[:x]) a&.b ||= blah params.permit! User.new&.attributes = params end def symbol params[:x].to_sym end def slice_then_permit User.new(params.slice(:id).permit!) User.find_by(params.slice(:id)) redirect_to params.slice(:back_to) end def nested_sql_interp User.connection.execute("SELECT * FROM foo WHERE #{true ? "bar = #{ActiveRecord::Base.connection.quote(true)}" : "bar = 0"}") end def arel_sql Arel.sql("select #{params[:s]}") end end ================================================ FILE: test/apps/rails5/app/controllers/widget_controller.rb ================================================ class WidgetController < ApplicationController def show end def dynamic_constant identifier_class = params[:IdentifierClass] namespace = identifier_class.constantize::IDENTIFIER_NAMESPACE # should warn end def render_thing render params[:x].thing? end def render_inline render :inline => "<%= xss.html_safe %>", :content_type => "text/html", :locals => { :xss => params[:xss] } end def sql_with_case group_by_col = case params[:group_by] when 'brand' then 'brand' when 'year' then 'YEAR(date_utc)' when 'month' then "CONCAT(YEAR(date_utc), '-', LPAD(MONTH(date_utc), 2, '0'))" when 'week' then "CONCAT(YEAR(date_utc), '-', LPAD(WEEK(date_utc, 1), 2, '0'))" when 'day' then "DATE(date_utc)" else raise ArgumentError, 'Invalid group by value' end query = "SELECT id, #{group_by_col} AS group_by_col, COUNT(*) FROM records" # No warnings rows = User.connection.select_rows(query) end def sql_with_another_case subset_clause = case script_subset when :greasyfork "AND `sensitive` = false" when :sleazyfork "AND `sensitive` = true" else "" end sql =<<-EOF SELECT text, SUM(daily_installs) install_count, COUNT(s.id) script_count FROM script_applies_tos JOIN scripts s ON script_id = s.id WHERE domain AND script_type_id = 1 AND script_delete_type_id IS NULL AND !tld_extra #{subset_clause} GROUP BY text ORDER BY text EOF # No warnings by_sites = User.connection.select_rows(sql) end def render_with_case # No warnings case params[:switch_case_on_this] when "one" render partial: params[:switch_case_on_this], locals: { x: 1 } when "two" render partial: params[:switch_case_on_this], locals: { x: 2 } end end def no_html @x = params[:x].html_safe end def guard_with_return goto = params[:goto] event = params[:event] return redirect_to user_path unless %w[comment subscribe].include?(goto) redirect_to send("#{goto}_event_path", event) # should not warn end def render_cookies render inline: request.cookies["value"] end def dangerous_permits params.permit(:admin) params.permit(:role_id) end def redirect_to_path session = User.find_by_token params[:session] if session # proceed with extracting user context from session and more and redirect to the last path the user was shown to login(session.user) redirect_to session.user.current_path else redirect_to expired_or_invalid_session_path end end def render_safely slug = params[:slug].to_s render slug if template_exists?(slug, 'pages') end def attributes end def haml_test end end IDENTIFIER_NAMESPACE = 'apis' ================================================ FILE: test/apps/rails5/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails5/app/helpers/users_helper.rb ================================================ module UsersHelper def bad_helper eval(params[:x]) end end ================================================ FILE: test/apps/rails5/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base end ================================================ FILE: test/apps/rails5/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: test/apps/rails5/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: test/apps/rails5/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails5/app/models/thing.rb ================================================ class Thing < ApplicationRecord def self.self_and_descendants_for(id) where(<<-SQL, id: id) #{quoted_table_name}.#{quoted_primary_key} IN ( WITH RECURSIVE descendant_tree(#{quoted_primary_key}, path) AS ( SELECT #{quoted_primary_key}, ARRAY[#{quoted_primary_key}] FROM #{quoted_table_name} WHERE #{quoted_primary_key} = :id UNION ALL SELECT #{quoted_table_name}.#{quoted_primary_key}, descendant_tree.path || #{quoted_table_name}.#{quoted_primary_key} FROM descendant_tree JOIN #{quoted_table_name} ON #{quoted_table_name}.parent_id = descendant_tree.#{quoted_primary_key} WHERE NOT #{quoted_table_name}.#{quoted_primary_key} = ANY(descendant_tree.path) ) SELECT #{quoted_primary_key} FROM descendant_tree ORDER BY path ) SQL end end ================================================ FILE: test/apps/rails5/app/models/user.rb ================================================ class User < ApplicationRecord def self.render_user_input ERB.new(params).result end def self.evaluate_user_input eval(params) end def evaluate_user_input self.class.evaluate_user_input end def test_stuff if Rails.env.test? User.where(params) end end has_many :things, -> { where(Thing.canadian.where_values_hash) } def self.all_that_jazz(user) User.where(User.access_condition(user)) end belongs_to :matched_user, class_name: 'User', optional: true end ================================================ FILE: test/apps/rails5/app/views/layouts/application.html.erb ================================================ Rails5 <%= csrf_meta_tags %> <%= action_cable_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= yield %> ================================================ FILE: test/apps/rails5/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails5/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails5/app/views/layouts/users.html.erb ================================================ <% if @user %> <%= @user.name.html_safe %> <% end %> ================================================ FILE: test/apps/rails5/app/views/users/_form.html.erb ================================================ <%= form_for(user) do |f| %> <% if user.errors.any? %>

    <%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:

      <% user.errors.full_messages.each do |message| %>
    • <%= message %>
    • <% end %>
    <% end %>
    <%= f.submit %>
    <% end %> ================================================ FILE: test/apps/rails5/app/views/users/edit.html.erb ================================================

    Editing User

    <%= render 'form', user: @user %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails5/app/views/users/find_and_preserve.html.haml ================================================ = find_and_preserve do %pre :escaped

    ================================================ FILE: test/apps/rails5/app/views/users/if_thing.html.haml ================================================ :javascript #{j(params[:a])} // Should not warn #{j(params[:b]) unless params[:c]} // Should not warn ================================================ FILE: test/apps/rails5/app/views/users/index.html.erb ================================================

    <%= notice %>

    Users

    <% @users.each do |user| %> <% end %>
    <%= link_to 'Show', user %> <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %>

    <%= link_to 'New User', new_user_path %> <%= link_to 'slice', params.slice(:url) %> ================================================ FILE: test/apps/rails5/app/views/users/index.json.jbuilder ================================================ json.array!(@users) do |user| json.extract! user, :id json.url user_url(user, format: :json) end ================================================ FILE: test/apps/rails5/app/views/users/new.html.erb ================================================

    New User

    <%= render 'form', user: @user %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails5/app/views/users/safe_call_params.html.haml ================================================ :javascript factory.printing.copies = #{params[:copies]&.to_i || 1}; ================================================ FILE: test/apps/rails5/app/views/users/sanitizing.html.erb ================================================ <%= raw sanitize(params[:x]) %> <%= strip_tags(params[:x]).html_safe %> ================================================ FILE: test/apps/rails5/app/views/users/show.html.erb ================================================

    <%= notice %>

    <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> <%= link_to("good", params.merge(:page => 2)) %> <%= link_to("xss", url_for(params[:bad])) %> <%= link_to(image_tag("icons/twitter-gray.svg"), sanitize(@user.home_page), target: "_blank") %> <%= link_to '', 'something_static', target: '_blank' %> No warning <%= link_to '', some_url, target: '_blank' %> Warn <%= link_to '', some_url, target: :_blank %> Warn <%= link_to some_url, target: '_blank' do -%> Warn <% end %> <%= link_to some_url, target: :_blank do -%> Warn <% end %> <%= link_to '', some_url, target: '_blank', rel: 'noopener' %> Weak warning <%= link_to '', some_url, target: '_blank', rel: 'noreferrer' %> Weak warning <%= link_to '', some_url, target: '_blank', rel: 'noopener noreferrer' %> No warning <%= link_to some_url, target: '_blank', rel: 'noopener noreferrer' do -%> No Warning <% end %> <%= link_to '', target: '_blank' %> No warning ================================================ FILE: test/apps/rails5/app/views/users/show.json.jbuilder ================================================ json.extract! @user, :id, :created_at, :updated_at ================================================ FILE: test/apps/rails5/app/views/widget/attributes.html.haml ================================================ %a{"data-text" => "#{params[:name]}"} No warning %pre.ksd.margin= params[:blah] = @user = User.first = form_tag create_thing_path do %p Stuff %ul %li things %li blah + #{@user.name.stuff_html.html_safe} %li other stuff #{@user.name_with_stuff} %li asdoasd %code arghlagb %li and the things #{@user.name_with_stuff} = blah = capture do %p= params[:x] = link_to stuff, blah ================================================ FILE: test/apps/rails5/app/views/widget/content_tag.html.erb ================================================ <%= content_tag(:div, "hi", title: params[:stuff].html_safe) %> <%= content_tag(:div, "hi", title: sanitize(params[:stuff])) %> ================================================ FILE: test/apps/rails5/app/views/widget/graphql.html.erb ================================================ <%graphql fragment Thing on Thing { thingHTML ...Views::Things::ThingHeader::Thing } %> ================================================ FILE: test/apps/rails5/app/views/widget/haml_test.html.haml ================================================ %li = "#{ params[:x] } No warning, escaped by default" %p :javascript var x = #{ raw params[:y] }; // This better warn! = form_for [:admin, @user], :html => { :onsubmit => "event.stop(); addField(this)" } do |f| %p %label{:for=>"page_field_name"}= t('name') %pre.unobstrusive= something? ? @user.name : "No warning, escaped" %td #{link_to @user.description, edit_user_path(@user)} # No warning ================================================ FILE: test/apps/rails5/app/views/widget/no_html.haml ================================================ %h1= @x ================================================ FILE: test/apps/rails5/app/views/widget/show.html.erb ================================================ <%= params[:x].html_safe unless this_is_a_bad_idea? %> <%= link_to("Thing", "#{ENV['SOME_URL']}#{params[:x]}") %> <%= link_to("Email!", "mailto:#{params[:x]}") %> ================================================ FILE: test/apps/rails5/bin/rails ================================================ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/apps/rails5/bin/rake ================================================ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/apps/rails5/bin/setup ================================================ #!/usr/bin/env ruby require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') or system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: test/apps/rails5/bin/spring ================================================ #!/usr/bin/env ruby # This file loads spring without using Bundler, in order to be fast. # It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) require 'rubygems' require 'bundler' if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq } gem 'spring', match[1] require 'spring/binstub' end end ================================================ FILE: test/apps/rails5/bin/update ================================================ #!/usr/bin/env ruby require 'pathname' require 'fileutils' include FileUtils # path to your application root. APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system 'bundle check' or system! 'bundle install' puts "\n== Updating database ==" system! 'bin/rails db:migrate' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: test/apps/rails5/config/application.rb ================================================ require File.expand_path('../boot', __FILE__) require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails5 class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. config.action_view.sanitized_allowed_tags = ["select", "strong", "style", "b"] end end ================================================ FILE: test/apps/rails5/config/boot.rb ================================================ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' # Set up gems listed in the Gemfile. ================================================ FILE: test/apps/rails5/config/brakeman.yml ================================================ --- :additional_checks_path: - "./test/apps/rails5/external_checks" ================================================ FILE: test/apps/rails5/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' # default: &default adapter: sqlite3 pool: 5 timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 ================================================ FILE: test/apps/rails5/config/environment.rb ================================================ # Load the Rails application. require File.expand_path('../application', __FILE__) # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails5/config/environments/development.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=172800' } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. config.assets.raise_runtime_errors = true # Raises error for missing translations # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker end ================================================ FILE: test/apps/rails5/config/environments/production.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = true # CVE-2018-3760 # Asset digests allow you to set far-future HTTP expiration dates on all assets, # yet still be able to expire them through the digest params. config.assets.digest = true # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Action Cable endpoint configuration # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :debug # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :request_id ] # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "rails5_#{Rails.env}" # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end ================================================ FILE: test/apps/rails5/config/environments/test.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Randomize the order test cases are executed. config.active_support.test_order = :random # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end ================================================ FILE: test/apps/rails5/config/initializers/active_record_belongs_to_required_by_default.rb ================================================ # Be sure to restart your server when you modify this file. # Require `belongs_to` associations by default. This is a new Rails 5.0 default, # so introduced as a config to ensure apps made with earlier versions of Rails aren't affected when upgrading. Rails.application.config.active_record.belongs_to_required_by_default = true ================================================ FILE: test/apps/rails5/config/initializers/application_controller_renderer.rb ================================================ # Be sure to restart your server when you modify this file. # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) ================================================ FILE: test/apps/rails5/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) ================================================ FILE: test/apps/rails5/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails5/config/initializers/callback_terminator.rb ================================================ # Be sure to restart your server when you modify this file. # Do not halt callback chains when a callback returns false. This is a new Rails 5.0 default, # so introduced as a config to ensure apps made with earlier versions of Rails aren't affected when upgrading. ActiveSupport.halt_callback_chains_on_return_false = false ================================================ FILE: test/apps/rails5/config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. # This is a new Rails 5.0 default, so introduced as a config to ensure apps made with earlier versions of Rails aren't affected when upgrading. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: test/apps/rails5/config/initializers/cors.rb ================================================ # Be sure to restart your server when you modify this file. # Avoid CORS issues when API is called from the frontend app. # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. # Read more: https://github.com/cyu/rack-cors # Rails.application.config.middleware.insert_before 0, Rack::Cors do # allow do # origins 'example.com' # # resource '*', # headers: :any, # methods: [:get, :post, :put, :patch, :delete, :options, :head] # end # end ================================================ FILE: test/apps/rails5/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails5/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails5/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: test/apps/rails5/config/initializers/request_forgery_protection.rb ================================================ # Be sure to restart your server when you modify this file. # Enable origin-checking CSRF mitigation. Rails.application.config.action_controller.forgery_protection_origin_check = true ================================================ FILE: test/apps/rails5/config/initializers/secrets.rb ================================================ DB_PASSWORD = "sup3rs3cr37" ================================================ FILE: test/apps/rails5/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_rails5_session' ================================================ FILE: test/apps/rails5/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails5/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails5/config/redis/cable.yml ================================================ # Action Cable uses Redis to administer connections, channels, and sending/receiving messages over the WebSocket. production: url: redis://localhost:6379/1 development: url: redis://localhost:6379/2 test: url: redis://localhost:6379/3 ================================================ FILE: test/apps/rails5/config/routes.rb ================================================ Rails.application.routes.draw do resources :users # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html # Serve websocket cable requests in-process # mount ActionCable.server => '/cable' if Rails.env.test? match '/:controller/:action' end end ================================================ FILE: test/apps/rails5/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. development: secret_key_base: 12d3735e1cdb18ef2eca25f9a370028ac096ff273c5a889ed7a49047d5e30c9dc7fe095792a71b60c3f37dd80efaeda44db75e73c9f60813550c875eee7a241f test: secret_key_base: 446b08c3cdeccdaf9e8b247a2624d45218c5d429e8acde61ddd87aa7b9dd50973e49e6d94378cb4bcf08b7818a90abb044b5c8886f94de6970ade4a496df22f3 # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> <% if Rails.root.join('config/ansible/secrets.yml').exist? %> <%= Rails.root.join('config/ansible/secrets.yml').read %> <% end %> ================================================ FILE: test/apps/rails5/config.ru ================================================ # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) # Action Cable uses EventMachine which requires that all classes are loaded in advance Rails.application.eager_load! require 'action_cable/process/logging' run Rails.application ================================================ FILE: test/apps/rails5/db/migrate/20160127223106_create_users.rb ================================================ class CreateUsers < ActiveRecord::Migration[5.0] def change create_table :users do |t| t.timestamps end end end ================================================ FILE: test/apps/rails5/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rails db:seed (or created alongside the db with db:setup). # # Examples: # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) ================================================ FILE: test/apps/rails5/external_checks/check_external_check_test.rb ================================================ require 'brakeman/checks/base_check' class Brakeman::CheckExternalCheckConfigTest < Brakeman::BaseCheck Brakeman::Checks.add_optional self @description = "An external check for testing" def run_check raise "This should not have been loaded!" end end ================================================ FILE: test/apps/rails5/lib/a_lib.rb ================================================ class JustAClass def do_sql_stuff joins("INNER JOIN things ON id = #{params[:id]}"). joins("INNER JOIN things ON stuff = 1") end def divide_by_zero whatever / 0 # warns x = 100 y = x - 100 z = x / y # warns 1.0 / 0 # does not warn end def tempfile FileUtils.move(params.permit(:my_upload => ([:upload])).dig("my_upload", "upload").tempfile.path, "/tmp/new_temp_file") FileUtils.move(params.permit(:my_upload => ([:upload])).dig("my_upload", "upload").path, "/tmp/new_temp_file") end end ================================================ FILE: test/apps/rails5/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails5/lib/lib.rb ================================================ class A def b $a, $b = a.b.c end end ================================================ FILE: test/apps/rails5/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails5/log/.keep ================================================ ================================================ FILE: test/apps/rails5/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

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

    ================================================ FILE: test/apps/rails5/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

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

    ================================================ FILE: test/apps/rails5/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

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

    ================================================ FILE: test/apps/rails5/public/robots.txt ================================================ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-agent: * # Disallow: / ================================================ FILE: test/apps/rails5/test/controllers/.keep ================================================ ================================================ FILE: test/apps/rails5/test/controllers/users_controller_test.rb ================================================ require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest setup do @user = users(:one) end test "should get index" do get users_url assert_response :success end test "should get new" do get new_user_url assert_response :success end test "should create user" do assert_difference('User.count') do post users_url, params: { user: { } } end assert_redirected_to user_path(User.last) end test "should show user" do get user_url(@user) assert_response :success end test "should get edit" do get edit_user_url(@user) assert_response :success end test "should update user" do patch user_url(@user), params: { user: { } } assert_redirected_to user_path(@user) end test "should destroy user" do assert_difference('User.count', -1) do delete user_url(@user) end assert_redirected_to users_path end end ================================================ FILE: test/apps/rails5/test/fixtures/.keep ================================================ ================================================ FILE: test/apps/rails5/test/fixtures/files/.keep ================================================ ================================================ FILE: test/apps/rails5/test/fixtures/users.yml ================================================ # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value ================================================ FILE: test/apps/rails5/test/helpers/.keep ================================================ ================================================ FILE: test/apps/rails5/test/integration/.keep ================================================ ================================================ FILE: test/apps/rails5/test/mailers/.keep ================================================ ================================================ FILE: test/apps/rails5/test/models/.keep ================================================ ================================================ FILE: test/apps/rails5/test/models/user_test.rb ================================================ require 'test_helper' class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end ================================================ FILE: test/apps/rails5/test/test_helper.rb ================================================ ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: test/apps/rails5/tmp/.keep ================================================ ================================================ FILE: test/apps/rails5/vendor/assets/javascripts/.keep ================================================ ================================================ FILE: test/apps/rails5/vendor/assets/stylesheets/.keep ================================================ ================================================ FILE: test/apps/rails5.2/.ruby-version ================================================ 2.3.1@something_weird ================================================ FILE: test/apps/rails5.2/Gemfile ================================================ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.3.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.0.beta2' # Use sqlite3 as the database for Active Record gem 'sqlite3' # Use Puma as the app server gem 'puma', '~> 3.11' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'mini_racer', platforms: :ruby # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.2' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use ActiveStorage variant # gem 'mini_magick', '~> 4.8' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false # Specify haml 5 so we can test our warning (brakeman doesn't support haml 5 yet) gem 'haml', '~> 5.0.3' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] # Adds support for Capybara system testing and selenium driver gem 'capybara', '~> 2.15' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome gem 'chromedriver-helper' end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem "slim", "~> 3.0.1", require: ["slim", "slim/smart"] ================================================ FILE: test/apps/rails5.2/README.md ================================================ # README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... ================================================ FILE: test/apps/rails5.2/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative 'config/application' Rails.application.load_tasks ================================================ FILE: test/apps/rails5.2/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css ================================================ FILE: test/apps/rails5.2/app/assets/images/.keep ================================================ ================================================ FILE: test/apps/rails5.2/app/assets/javascripts/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's // vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require rails-ujs //= require activestorage //= require turbolinks //= require_tree . ================================================ FILE: test/apps/rails5.2/app/assets/javascripts/cable.js ================================================ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. // //= require action_cable //= require_self //= require_tree ./channels (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer(); }).call(this); ================================================ FILE: test/apps/rails5.2/app/assets/javascripts/channels/.keep ================================================ ================================================ FILE: test/apps/rails5.2/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/apps/rails5.2/app/channels/application_cable/channel.rb ================================================ module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/apps/rails5.2/app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/apps/rails5.2/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base end ================================================ FILE: test/apps/rails5.2/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails5.2/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController def index index_params = params.permit(:name, friend_names: []).to_hash User.where(index_params).qualify.all end def show show_params = params.permit(:id, :name).to_hash.symbolize_keys User.where(show_params).qualify.all end ALLOWED_FOOS = [:bar, :baz].freeze def delete(foo) unless ALLOWED_FOOS.include? foo raise ArgumentError, "Unexpected foo: #{foo}" end Person.where("#{foo} >= 1") end def safe_one(foo) return if !ALLOWED_FOOS.include?(foo) Person.where("#{foo} >= 1") end def better_user_input_reporting table = Something.selection.select { |x| some_condition? x }.map { |x| "#{User.table_name}.#{x}" } # Should report SQLi, but not about User.table_name specifically User.find_by_sql("SELECT #{"#{table}.name"} where name = #{params[:name]}") end def splat_args Person.where(*params[:foo]).qualify.all end def splat_kwargs User.where(**params[:foo]).qualify.all end def one @user = User.find(params[:id]) end def two @user = User.find(params[:id]) end def some_api Oj.load(params[:json]) # Unsafe by default Oj.load(params[:json], mode: :object) # Unsafe, regardless of default Oj.object_load(params[:json], mode: :strict) # Always unsafe, regardless of mode Oj.load(params[:json], mode: :strict) # Safe end def not_not si = ManualCSVImport.new(header_row: !!params[:header_row], archive: !!params[:archive]) @errors = [si.results[:invalid_info], si.results[:ignored_info]].flatten end def test_empty_partial_name end end ================================================ FILE: test/apps/rails5.2/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails5.2/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails5.2/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base end ================================================ FILE: test/apps/rails5.2/app/jobs/delete_stuff_job.rb ================================================ class DeleteStuffJob < ApplicationJob def perform file `rm -rf #{file}` end end ================================================ FILE: test/apps/rails5.2/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: test/apps/rails5.2/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: test/apps/rails5.2/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails5.2/app/models/user.rb ================================================ class User < ActiveRecord::Base def not_something thing where.not("blah == #{thing}") end SUBQUERY_TABLE_ALIAS = "my_table_alias".freeze # This is used inside a larger query by using `inner_query.to_sql` def inner_query self.class. select("#{SUBQUERY_TABLE_ALIAS}.*"). from("#{table_name} AS #{SUBQUERY_TABLE_ALIAS}") end def singularize_safe_literal [:fees, :fair].each do |type| Money.new(articles.sum("calculated_#{type.to_s.singularize}_cents * quantity")) end end def foreign_key_thing assoc_reflection = reflect_on_association(:foos) foreign_key = assoc_reflection.foreign_key User.joins("INNER JOIN ") end def polymorphic_name_joins MediaFile.joins( "JOIN #{table_name} ON media_files.parent_type = '#{polymorphic_name}' AND media_files.parent_id = #{table_name}.id" ) end end ================================================ FILE: test/apps/rails5.2/app/views/home/index.html.erb ================================================ <%= t(:my_translation, timeago: timeago(user.created_at)) %> ================================================ FILE: test/apps/rails5.2/app/views/layouts/application.html.erb ================================================ Rails52 <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= yield %> ================================================ FILE: test/apps/rails5.2/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails5.2/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails5.2/app/views/users/_empty_partial_name.html.erb ================================================ <%= render(:partial => nested_partial) if nested_partial.present? %> ================================================ FILE: test/apps/rails5.2/app/views/users/_foo.html.haml ================================================ = raw(d) ================================================ FILE: test/apps/rails5.2/app/views/users/_foo2.html.haml ================================================ = raw(e) ================================================ FILE: test/apps/rails5.2/app/views/users/kwsplat.html.haml ================================================ - known = { d: params[:x] } = render partial: "foo", locals: { **known, a: 1 } = render partial: "foo2", locals: { **unknown, e: params[:x] } ================================================ FILE: test/apps/rails5.2/app/views/users/link.html.erb ================================================ <%= link_to params[:x] do %> stuff <% end %> ================================================ FILE: test/apps/rails5.2/app/views/users/not_not.html.erb ================================================ <%= @errors.join("
    ").html_safe %> ================================================ FILE: test/apps/rails5.2/app/views/users/one.html.haml ================================================ :sass #name border: 3px dashed orange color: #{raw @user.name} %p#name %b Name: = @user.name = sanitize @user.bio, tags: ['style', :select] = Rails::Html::SafeListSanitizer.new.sanitize(@user.description, tags: ["select", "style"]) :coffeescript $('h1').click -> alert('You clicked on the headline') :markdown # Jello! :sass #div font-weight: bold ================================================ FILE: test/apps/rails5.2/app/views/users/smart.html.slim ================================================ p Your credit card strong will not > be charged now. This is a text which spans several lines. footer Copyright © #{params[:x]} not xss? ================================================ FILE: test/apps/rails5.2/app/views/users/test_empty_partial_name.html.erb ================================================ <%= render :partial => 'empty_partial_name', :locals => {:nested_partial => ''} %> ================================================ FILE: test/apps/rails5.2/app/views/users/two.html.slim ================================================ scss: #someDiv border: 3px dashed orange javascript: var x = #{ raw @user.name } coffee: greeting = 'hi' ================================================ FILE: test/apps/rails5.2/bin/rails ================================================ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: test/apps/rails5.2/bin/rake ================================================ #!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: test/apps/rails5.2/bin/setup ================================================ #!/usr/bin/env ruby require 'fileutils' include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies if using Yarn # system('bin/yarn') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: test/apps/rails5.2/bin/spring ================================================ #!/usr/bin/env ruby # This file loads spring without using Bundler, in order to be fast. # It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) require 'rubygems' require 'bundler' lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) spring = lockfile.specs.detect { |spec| spec.name == "spring" } if spring Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path gem 'spring', spring.version require 'spring/binstub' end end ================================================ FILE: test/apps/rails5.2/bin/update ================================================ #!/usr/bin/env ruby require 'fileutils' include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies if using Yarn # system('bin/yarn') puts "\n== Updating database ==" system! 'bin/rails db:migrate' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: test/apps/rails5.2/bin/yarn ================================================ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do begin exec "yarnpkg #{ARGV.join(' ')}" rescue Errno::ENOENT $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 end end ================================================ FILE: test/apps/rails5.2/config/application.rb ================================================ require_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails52 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # https://guides.rubyonrails.org/v7.0/configuring.html#config-action-controller-per-form-csrf-tokens # config.action_controller.per_form_csrf_tokens = true end end ================================================ FILE: test/apps/rails5.2/config/boot.rb ================================================ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap/setup' # Speed up boot time by caching expensive operations. ================================================ FILE: test/apps/rails5.2/config/cable.yml ================================================ development: adapter: async test: adapter: async production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: rails5_2_production ================================================ FILE: test/apps/rails5.2/config/credentials.yml.enc ================================================ nlV8moQTYjDyR+oTWV1hJIFI6vPU6dhsBI4h7MDS6AFbUMa3bS9M/S1T3/Qj3R25HAYNuTsswB7sMgYlEELUdPCB2yxAekd8dKizHTBv23CGRplaLC198FC/VWf815SrjxmNlDMnuA9XxsUhvon7qTkCLOXKwsE1qQ8AOAwu4R86anJvdMyIiuvogRcgl6ePkdLe9thQiDw0Hr8CeiCs4AfzasU5Lk3pxVjlM59Va0ZrVXezlTMjajTeJtim9vEPIM0BBecgWzZySRCskA4L/xVwAEFWcerBoOyGMFoi7ZwmYkux/Q28oQUCq04iNmuLF4RMD75axhcD7o2ldML3k5O3mGIuYOi1dzHtibewGJlNjhBAcnapsZtbODGPM6Zrs79M137iQdQcpj83vEGFJ92u9xgQN74N--DrU0T/aJZtDsx0XU--cplS41E/sGK0o829mOPNdQ== ================================================ FILE: test/apps/rails5.2/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 ================================================ FILE: test/apps/rails5.2/config/environment.rb ================================================ # Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails5.2/config/environments/development.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options) config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker end ================================================ FILE: test/apps/rails5.2/config/environments/production.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options) config.active_storage.service = :local # Mount Action Cable outside main process or domain # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = false # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :debug # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "rails5_2_#{Rails.env}" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # https://guides.rubyonrails.org/v7.0/configuring.html#config-action-controller-default-protect-from-forgery # config.action_controller.default_protect_from_forgery = true end ================================================ FILE: test/apps/rails5.2/config/environments/test.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations # config.action_view.raise_on_missing_translations = true end ================================================ FILE: test/apps/rails5.2/config/initializers/application_controller_renderer.rb ================================================ # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: test/apps/rails5.2/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Add Yarn node_modules folder to the asset load path. Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: test/apps/rails5.2/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails5.2/config/initializers/content_security_policy.rb ================================================ # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy Rails.application.config.content_security_policy do |p| p.default_src :self, :https p.font_src :self, :https, :data p.img_src :self, :https, :data p.object_src :none p.script_src :self, :https p.style_src :self, :https, :unsafe_inline # Specify URI for violation reports # p.report_uri "/csp-violation-report-endpoint" end # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # Rails.application.config.content_security_policy_report_only = true ================================================ FILE: test/apps/rails5.2/config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :hybrid module Custom module Serializer end end Rails.application.config.action_dispatch.cookies_serializer = Custom::Serializer ================================================ FILE: test/apps/rails5.2/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails5.2/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails5.2/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: test/apps/rails5.2/config/initializers/oj.rb ================================================ # These are commented out by default and then turned on in test/tests/oj.rb for testing. # # Oj.mimic_JSON # Oj.default_options = { mode: :strict } ================================================ FILE: test/apps/rails5.2/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails5.2/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails5.2/config/puma.rb ================================================ # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. If you use this option # you need to make sure to reconnect any threads in the `on_worker_boot` # block. # # preload_app! # If you are preloading your application and using Active Record, it's # recommended that you close any connections to the database before workers # are forked to prevent connection leakage. # # before_fork do # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) # end # The code in the `on_worker_boot` will be called if you are using # clustered mode by specifying a number of `workers`. After each worker # process is booted, this block will be run. If you are using the `preload_app!` # option, you will want to use this block to reconnect to any threads # or connections that may have been created at application boot, as Ruby # cannot share connections between processes. # # on_worker_boot do # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) # end # # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart ================================================ FILE: test/apps/rails5.2/config/routes.rb ================================================ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end ================================================ FILE: test/apps/rails5.2/config/secrets.yml ================================================ # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rake secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. test_anchor: &test_anchor abc: 123 development: test_anchor: *test_anchor secret_key_base: 12d3735e1cdb18ef2eca25f9a370028ac096ff273c5a889ed7a49047d5e30c9dc7fe095792a71b60c3f37dd80efaeda44db75e73c9f60813550c875eee7a241f test: test_anchor: *test_anchor secret_key_base: 446b08c3cdeccdaf9e8b247a2624d45218c5d429e8acde61ddd87aa7b9dd50973e49e6d94378cb4bcf08b7818a90abb044b5c8886f94de6970ade4a496df22f3 # Do not keep production secrets in the repository, # instead read values from the environment. production: test_anchor: *test_anchor secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> <% if Rails.root.join('config/ansible/secrets.yml').exist? %> <%= Rails.root.join('config/ansible/secrets.yml').read %> <% end %> ================================================ FILE: test/apps/rails5.2/config/spring.rb ================================================ %w[ .ruby-version .rbenv-vars tmp/restart.txt tmp/caching-dev.txt ].each { |path| Spring.watch(path) } ================================================ FILE: test/apps/rails5.2/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # keyfile: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # path: your_azure_storage_path # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/apps/rails5.2/config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application ================================================ FILE: test/apps/rails5.2/db/migrate/20171208205700_create_active_storage_tables.active_storage.rb ================================================ # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| t.string :key, null: false t.string :filename, null: false t.string :content_type t.text :metadata t.bigint :byte_size, null: false t.string :checksum, null: false t.datetime :created_at, null: false t.index [ :key ], unique: true end create_table :active_storage_attachments do |t| t.string :name, null: false t.references :record, null: false, polymorphic: true, index: false t.references :blob, null: false t.datetime :created_at, null: false t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true end end end ================================================ FILE: test/apps/rails5.2/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # # Examples: # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) ================================================ FILE: test/apps/rails5.2/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails5.2/lib/factory_bot.rb ================================================ FactoryBot.define do factory :foo do included { "an attribute value" } end end ================================================ FILE: test/apps/rails5.2/lib/initthing.rb ================================================ class InitThing def initialize @blah = "some cool stuff" end def use_it `#{@blah}` end end ================================================ FILE: test/apps/rails5.2/lib/shell.rb ================================================ class ShellStuff def initialize(one, two) @one = Shellwords.shellescape(one) @two = Shellwords.escape(two) end def run(ip) ip = Shellwords.shellescape(ip) `dig +short -x #{ip} @#{@one} -p #{@two}` `command #{Shellwords.escape(@one)}` `command #{Shellwords.join(@two)}` `command #{Shellwords.shelljoin(@two)}` `command #{@one.shellescape}` `command #{@two.shelljoin}` end def backticks_target(path) `echo #{path}`.chomp end def process_pid # should not warn `something #{Process.pid}` end def nested_system_interp filename = Shellwords.escape("#{file_prefix}.txt") system "echo #{filename}" end def system_array_join command = ["ruby", method_that_returns_user_input, "--some-flag"].join(" ") system(command) end def system_as_target !system("echo #{foo}") end def interpolated_conditional_safe `echo #{"foo" if foo} bar` end def interpolated_ternary_safe `echo #{foo ? "bar" : "baz"}` end def interpolated_conditional_dangerous `echo #{bar if foo} baz` end def interpolated_ternary_dangerous `echo #{foo ? "bar" : bar} baz` end COMMANDS = { foo: "echo", bar: "cat" } MORE_COMMANDS = { foo: "touch" } def safe(arg) command = if Date.today.tuesday? # Some condition. COMMANDS[arg] else MORE_COMMANDS[arg] end `#{command} file1.txt` end EXPRESSIONS = ["users.email", "concat_ws(' ', users.first_name, users.last_name)"] def perform_commands EXPRESSIONS.each { |exp| `echo #{exp}` } end def scopes(base_scope) EXPRESSIONS.map { |exp| base_scope.where("#{exp} ILIKE '%foo%'") } end def shell_escape_model a = User.new z = Shellwords.escape(a.z) result, status = Open3.capture2e("ls", z) # Should not warn `ls #{z}` # Also should not warn end def file_constant_use # __FILE__ should not change based on absolute path `cp #{__FILE__} #{somewhere_else}` end def interpolated_in_percent_W # Should not warn system(*%W(foo bar #{value})) end def completely_external system(foo) # We assume this is safe and do not warn. end def string_concatenation system("echo " + foo) end def escaped_string_concatenation system("echo " + Shellwords.escape(foo)) end def safe_string_concatenation system("echo " + "foo") end def dash_c_dangerous_concatenation system("bash", "-c", "echo " + foo) end def dash_c_safe_concatenation system("bash", "-c", "echo " + Shellwords.escape(foo)) end def popen_dash_c IO.popen(["bash", "-c", params[:foo]]) {} end def popen_concatenation IO.popen("ls " + foo) do |ls_io| ls_io.read end end def open3_capture_stdin_data u = User.new # None of these should warn Open3.capture2("cat", stdin_data: "User.z = #{u.z}") Open3.capture2e("cat", stdin_data: "User.z = #{u.z}") Open3.capture3("cat", stdin_data: "User.z = #{u.z}") end def tempfile_create # these should not warn tempfile = Tempfile.create `something -out #{tempfile.path}` Tempfile.create do |tempfile| system("something -out #{tempfile.path}") end end end ================================================ FILE: test/apps/rails5.2/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails5.2/log/.keep ================================================ ================================================ FILE: test/apps/rails5.2/package.json ================================================ { "name": "rails5_2", "private": true, "dependencies": {} } ================================================ FILE: test/apps/rails5.2/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

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

    ================================================ FILE: test/apps/rails5.2/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

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

    ================================================ FILE: test/apps/rails5.2/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

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

    ================================================ FILE: test/apps/rails5.2/public/robots.txt ================================================ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file ================================================ FILE: test/apps/rails5.2/vendor/.keep ================================================ ================================================ FILE: test/apps/rails5.2/vendor/vendored_thing.rb ================================================ class Vendored def vendor `rm -rf #{stuff}` end end ================================================ FILE: test/apps/rails6/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore uploaded files in development. /storage/* !/storage/.keep /public/assets .byebug_history # Ignore master key for decrypting credentials and more. /config/master.key /public/packs /public/packs-test /node_modules /yarn-error.log yarn-debug.log* .yarn-integrity ================================================ FILE: test/apps/rails6/Gemfile ================================================ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.5.3' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.0.beta2' # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.3', '>= 1.3.6' # Use Puma as the app server gem 'puma', '~> 3.11' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '>= 4.0.0.rc.3' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.1', require: false gem 'safe_yaml' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome gem 'chromedriver-helper' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] ================================================ FILE: test/apps/rails6/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative 'config/application' Rails.application.load_tasks ================================================ FILE: test/apps/rails6/another_lib_dir/some_lib.rb ================================================ class A def something(thing) `rm -rf #{thing}` end end ================================================ FILE: test/apps/rails6/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../stylesheets .css ================================================ FILE: test/apps/rails6/app/assets/images/.keep ================================================ ================================================ FILE: test/apps/rails6/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/apps/rails6/app/assets/stylesheets/scaffolds.scss ================================================ body { background-color: #fff; color: #333; margin: 33px; font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; } pre { background-color: #eee; padding: 10px; font-size: 11px; } a { color: #000; &:visited { color: #666; } &:hover { color: #fff; background-color: #000; } } th { padding-bottom: 5px; } td { padding: 0 5px 7px; } div { &.field, &.actions { margin-bottom: 10px; } } #notice { color: green; } .field_with_errors { padding: 2px; background-color: red; display: table; } #error_explanation { width: 450px; border: 2px solid red; padding: 7px 7px 0; margin-bottom: 20px; background-color: #f0f0f0; h2 { text-align: left; font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px -7px 0; background-color: #c00; color: #fff; } ul li { font-size: 12px; list-style: square; } } label { display: block; } ================================================ FILE: test/apps/rails6/app/assets/stylesheets/users.scss ================================================ // Place all the styles related to the Users controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ ================================================ FILE: test/apps/rails6/app/channels/application_cable/channel.rb ================================================ module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/apps/rails6/app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/apps/rails6/app/components/base_component.rb ================================================ class BaseComponent def render_in "Hello, world" end end ================================================ FILE: test/apps/rails6/app/components/test_component.rb ================================================ class TestComponent < BaseComponent def initialize(prop) @prop = prop end end ================================================ FILE: test/apps/rails6/app/components/test_view_component.rb ================================================ class TestViewComponent < ViewComponent::Base def initialize(prop) @prop = prop end end ================================================ FILE: test/apps/rails6/app/components/test_view_component_contrib.rb ================================================ class TestViewComponentContrib < ViewComponentContrib::Base def initialize(prop) @prop = prop end end ================================================ FILE: test/apps/rails6/app/components/test_view_component_fully_qualified_ancestor.rb ================================================ class TestViewComponentFullyQualifiedAncestor < ::ViewComponent::Base def initialize(prop) @prop = prop end end ================================================ FILE: test/apps/rails6/app/components/text_phlex_component.rb ================================================ class TestPhlexComponent < Phlex::HTML def initialize(prop) @prop = prop end end ================================================ FILE: test/apps/rails6/app/controllers/accounts_controller.rb ================================================ class AccountsController < ApplicationController def login if request.get? # Do something benign else # Do something sensitive because it's a POST # but actually it could be a HEAD :( end end def auth_something # Does not warn because there is an elsif clause if request.get? # Do something benign elsif request.post? # Do something sensitive because it's a POST end if request.post? # Do something sensitive because it's a POST elsif request.get? # Do something benign end end def eval_something eval(params[:x]).to_s end def index params.values_at(:test).join("|") end def tr_sql Arel.sql(<<~SQL.tr("\n", " ")) CASE WHEN #{user_params[:field]} IS NULL OR TRIM(#{user_params[:field]}) = '' THEN 'Untitled' ELSE TRIM(#{user_params[:field]}) END SQL end end ================================================ FILE: test/apps/rails6/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base end ================================================ FILE: test/apps/rails6/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails6/app/controllers/groups_controller.rb ================================================ class GroupsController < ApplicationController def new_group @group = Group.find params[:id] new_group = @group.dup new_group.save! redirect_to new_group end def render_commands render text: `#{params.require('name')} some optional text` render json: `#{params.require('name')} some optional text` render(TestComponent.new(params.require('name'))) render(TestViewComponent.new(params.require('name'))) render(::TestViewComponent.new(params.require('name'))) render(TestViewComponentFullyQualifiedAncestor.new(params.require('name'))) end def squish_sql ActiveRecord::Base.connection.execute "SELECT * FROM #{user_input}".squish ActiveRecord::Base.connection.execute "SELECT * FROM #{user_input}".strip end def show template = params[:template] # Test file allowlist return redirect_to '/groups' unless FILE_LIST.include? template render "groups/#{template}" end def permit_bang_path redirect_to groups_path(params.permit!) end def permit_bang_slice params.permit!.slice(:whatever) end def safeish_yaml_load YAML.load(params[:yaml_stuff], safe: true) YAML.load(params[:yaml_stuff], safe: false) # not safe YAML.load(params[:yaml_stuff]) # not safe end def dynamic_method_invocations params[:method].to_sym.to_proc.call(Kernel) (params[:klass].to_s).method(params[:method]).(params[:argument]) Kernel.tap(¶ms[:method].to_sym) User.method("#{User.first.some_method_thing}_stuff") end def only_for_dev if Rails.env.development? eval(params[:x]) # should not warn end end def scope_with_custom_sanitization ActiveRecord::Base.connection.execute "SELECT * FROM #{sanitize_s(user_input)}" end def sanitize_s(input) input end def test_rails6_sqli User.select("stuff").reselect(params[:columns]) User.where("x = 1").rewhere("x = #{params[:x]}") User.pluck(params[:column]) # Warn in 6.0, not in 6.1 User.order("name #{params[:direction]}") # Warn in 6.0, not in 6.1 User.order(:name).reorder(params[:column]) # Warn in 6.0, not in 6.1 end # From https://github.com/presidentbeef/brakeman/issues/1492 def enum_include_check status = "#{params[:status]}" if Group.statuses.include? status @status = status.to_sym @countries = Group.send(@status) # Should not warn else redirect_to root_path, notice: 'Invalid status' end end def render_phlex_component render(TestPhlexComponent.new(params.require('name'))) end def render_view_component_contrib render(TestViewComponentContrib.new(params.require('name'))) end # Test ViewComponent with with_content chain (issue #1832) def render_view_component_with_content render(TestViewComponent.new(params.require('name')).with_content("string")) end end ================================================ FILE: test/apps/rails6/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] # GET /users # GET /users.json def index @users = User.all end # GET /users/1 # GET /users/1.json def show end # GET /users/new def new @user = User.new end def edit render :edit, locals: { some_name.to_sym => 'stuff' } end # POST /users # POST /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to @user, notice: 'User was successfully created.' } format.json { render :show, status: :created, location: @user } else format.html { render :new } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # PATCH/PUT /users/1 # PATCH/PUT /users/1.json def update respond_to do |format| if @user.update(user_params) format.html { redirect_to @user, notice: 'User was successfully updated.' } format.json { render :show, status: :ok, location: @user } else format.html { render :edit } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.json def destroy @user.destroy respond_to do |format| format.html { redirect_to users_url, notice: 'User was successfully destroyed.' } format.json { head :no_content } end end def destroy_them_all @user.destroy_by(params[:user]) @user.delete_by(params[:user]) end def dangerous_system_call system("bash", "-c", params[:script]) end def dangerous_exec_call shell = "zsh" exec(shell, SHELL_FLAG, "#{params[:script]} -e ./") end SHELL_FLAG = "-c" def safe_system_call system("bash", "-c", "echo", params[:argument]) end def safe_system_call_without_shell_dash_c system("echo", "-c", params[:argument]) end def example_redirect_to_request_params redirect_to request.params end def permit_bang # Both should warn SomeService.new(params: params.permit!).instance_method params.permit!.merge({ some: 'hash' }) end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Never trust parameters from the scary internet, only allow the white list through. def user_params params.require(:user).permit(:name) end end ================================================ FILE: test/apps/rails6/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails6/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails6/app/javascript/channels/consumer.js ================================================ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. import { createConsumer } from "@rails/actioncable" export default createConsumer() ================================================ FILE: test/apps/rails6/app/javascript/channels/index.js ================================================ // Load all the channels within this directory and all subdirectories. // Channel files must be named *_channel.js. const channels = require.context('.', true, /_channel\.js$/) channels.keys().forEach(channels) ================================================ FILE: test/apps/rails6/app/javascript/packs/application.js ================================================ // This file is automatically compiled by Webpack, along with any other files // present in this directory. You're encouraged to place your actual application logic in // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") ================================================ FILE: test/apps/rails6/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: test/apps/rails6/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: test/apps/rails6/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: test/apps/rails6/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails6/app/models/group.rb ================================================ class Group < ApplicationRecord def uuid_in_sql ActiveRecord::Base.connection.exec_query("select * where x = #{User.uuid}") end def date_in_sql date = 30.days.ago Arel.sql("created_at > '#{date}'") end def ar_sanitize_sql_like(query) query = ActiveRecord::Base.sanitize_sql_like(query) # escaped variable Arel.sql("name ILIKE '%#{query}%'") end def fetch_constant_hash_value(role_name) roles = { admin: 1, moderator: 2 }.freeze role = roles.fetch(role_name) Arel.sql("role = '#{role}'") end def use_simple_method # No warning self.where("thing = #{Group.simple_method}") end def self.simple_method "Hello" end enum status: { start: 0, stop: 2, in_process: 3 } def use_enum # No warning self.where("thing IN #{Group.statuses.values_at(*[:start, :stop]).join(',')}") end end ================================================ FILE: test/apps/rails6/app/models/user.rb ================================================ class User < ApplicationRecord STUFF = [ DEFAULT_PASSWORD = "p@ssw3rd2" ] def self.scope_with_strip_heredoc(name) conditions = <<-SQL.strip_heredoc name = '#{name}' SQL where(conditions) end def self.render_user_input ERB.new(params) end def self.more_heredocs ActiveRecord::Base.connection.delete <<~SQL.chomp DELETE FROM #{table} WHERE updated_at < now() - interval '#{period}' SQL end def recent_stuff where("date > #{Date.today - 1}") end enum state: ["pending", "active", "archived"] def check_enum where("state = #{User.states["pending"]}") end enum "stuff_#{stuff}": [:things] def locale User.where("lower(slug_#{I18n.locale.to_s.split("-").first}) = :country_id", country_id: params[:new_country_id]).first end end ================================================ FILE: test/apps/rails6/app/views/layouts/application.html.erb ================================================ Rails6 <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%= yield %> ================================================ FILE: test/apps/rails6/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails6/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails6/app/views/users/_form.html.erb ================================================ <%= form_with(model: user, local: true) do |form| %> <% if user.errors.any? %>

    <%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:

      <% user.errors.full_messages.each do |message| %>
    • <%= message %>
    • <% end %>
    <% end %>
    <%= form.label :name %> <%= form.text_field :name %>
    <%= form.submit %>
    <% end %> ================================================ FILE: test/apps/rails6/app/views/users/_user.json.jbuilder ================================================ json.extract! user, :id, :name, :created_at, :updated_at json.url user_url(user, format: :json) ================================================ FILE: test/apps/rails6/app/views/users/edit.html.erb ================================================

    Editing User

    <%= render 'form', user: @user %> <%= link_to 'Show', @user %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails6/app/views/users/index.html.erb ================================================

    <%= notice %>

    Users

    <% @users.each do |user| %> <% end %>
    Name
    <%= user.name %> <%= link_to 'Show', user %> <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %>

    <%= link_to 'New User', new_user_path %> ================================================ FILE: test/apps/rails6/app/views/users/index.json.jbuilder ================================================ json.array! @users, partial: 'users/user', as: :user ================================================ FILE: test/apps/rails6/app/views/users/new.html.erb ================================================

    New User

    <%= render 'form', user: @user %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails6/app/views/users/show.html.erb ================================================

    <%= notice %>

    Name: <%= @user.name.html_safe %> <%= @user.name.html_safe if x %> <%= @user.name.html_safe unless x %> <%= x ? @user.name.html_safe : y %>

    <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails6/app/views/users/show.json.jbuilder ================================================ json.partial! "users/user", user: @user ================================================ FILE: test/apps/rails6/app/widgets/widget.rb ================================================ class Widget < ApplicationRecord def spin(direction) where("direction = #{direction})").first.spin end end ================================================ FILE: test/apps/rails6/babel.config.js ================================================ module.exports = function(api) { var validEnv = ['development', 'test', 'production'] var currentEnv = api.env() var isDevelopmentEnv = api.env('development') var isProductionEnv = api.env('production') var isTestEnv = api.env('test') if (!validEnv.includes(currentEnv)) { throw new Error( 'Please specify a valid `NODE_ENV` or ' + '`BABEL_ENV` environment variables. Valid values are "development", ' + '"test", and "production". Instead, received: ' + JSON.stringify(currentEnv) + '.' ) } return { presets: [ isTestEnv && [ require('@babel/preset-env').default, { targets: { node: 'current' } } ], (isProductionEnv || isDevelopmentEnv) && [ require('@babel/preset-env').default, { forceAllTransforms: true, useBuiltIns: 'entry', modules: false, exclude: ['transform-typeof-symbol'] } ] ].filter(Boolean), plugins: [ require('babel-plugin-macros'), require('@babel/plugin-syntax-dynamic-import').default, isTestEnv && require('babel-plugin-dynamic-import-node'), require('@babel/plugin-transform-destructuring').default, [ require('@babel/plugin-proposal-class-properties').default, { loose: true } ], [ require('@babel/plugin-proposal-object-rest-spread').default, { useBuiltIns: true } ], [ require('@babel/plugin-transform-runtime').default, { helpers: false, regenerator: true } ], [ require('@babel/plugin-transform-regenerator').default, { async: false } ] ].filter(Boolean) } } ================================================ FILE: test/apps/rails6/config/application.rb ================================================ require_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails6 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. end end ================================================ FILE: test/apps/rails6/config/boot.rb ================================================ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap/setup' # Speed up boot time by caching expensive operations. ================================================ FILE: test/apps/rails6/config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: rails6_production ================================================ FILE: test/apps/rails6/config/credentials.yml.enc ================================================ b/xul5Q0qIjaK5d9W62i89WFeOjav+ubFRXhjdR2SuzPbzhT9xYyKpjRbeMbMz+j2zB4TM1vRxD3HJHF9izgwgMT8rnjA2U1nBLjxWqbTG/uKYfwIbMvKxeSbIGGdd/jmhqX1FkI9kjPh+UXbYp35Q/AuNnRVT4uYhKPhYkzED8YHla0zaGJw7IOe4v3OhEDXdYQJTDtuIU2tdS33L79/M/JCyjF1kMNfspf9imbYx/1JYNo4XmroAs6IYR+keuzYIIIb7kkLjaI61xGUoH0har7NIzPLbujGnAc+IIjPMf3sa2bmRVbTZgAkDcCmVO5bR1uGslroOyV/UEC74668doWZ304ywGlHxqi2f1/U2/t+Nhp562rDViy0QHiAgipKodv3Xn85BrQLIUvXBizVpf1WJgiCU2v5c7n--RZK7EAHLEtUv1WeQ--Bp+CMY4Ux5xBWji9MshqCA== ================================================ FILE: test/apps/rails6/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem 'sqlite3' # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 ================================================ FILE: test/apps/rails6/config/environment.rb ================================================ # Load the Rails application. require_relative 'application' # A Dir.glob constant for testing FILE_LIST = Dir.glob(File.join(Rails.root, "app", "views", "**", "*")).select do |f| f.end_with? ".html" end # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails6/config/environments/development.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker end ================================================ FILE: test/apps/rails6/config/environments/production.rb ================================================ ::Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :debug # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "rails6_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector # middleware. The `delay` is used to determine how long to wait after a write # to send a subsequent read to the primary. # # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # # The `database_resolver_context` class is used by the middleware to set # timestamps for the last write to the primary. The resolver uses the context # class timestamps to determine how long to wait before reading from the # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own # strategy for connection switching and pass that into the middleware through # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session # Stop messing up my JSON! config.active_support.escape_html_entities_in_json = false end ================================================ FILE: test/apps/rails6/config/environments/test.rb ================================================ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true end ================================================ FILE: test/apps/rails6/config/initializers/allow_all_parameters.rb ================================================ # Allows all parameters for StrongParameters ActionController::Parameters.permit_all_parameters = true ================================================ FILE: test/apps/rails6/config/initializers/application_controller_renderer.rb ================================================ # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: test/apps/rails6/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Add Yarn node_modules folder to the asset load path. Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: test/apps/rails6/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails6/config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # If you are using webpack-dev-server then specify webpack-dev-server host # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # Rails.application.config.content_security_policy_report_only = true ================================================ FILE: test/apps/rails6/config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :marshal ================================================ FILE: test/apps/rails6/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: test/apps/rails6/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: test/apps/rails6/config/initializers/json_escape.rb ================================================ # I like my HTML entities ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false ================================================ FILE: test/apps/rails6/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: test/apps/rails6/config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: test/apps/rails6/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails6/config/puma.rb ================================================ # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # # preload_app! # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart ================================================ FILE: test/apps/rails6/config/routes.rb ================================================ Rails.application.routes.draw do resources :users # https://github.com/presidentbeef/brakeman/issues/1410 x = ->(args) { } end ================================================ FILE: test/apps/rails6/config/spring.rb ================================================ Spring.watch( ".ruby-version", ".rbenv-vars", "tmp/restart.txt", "tmp/caching-dev.txt" ) ================================================ FILE: test/apps/rails6/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/apps/rails6/config/webpack/development.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || 'development' const environment = require('./environment') module.exports = environment.toWebpackConfig() ================================================ FILE: test/apps/rails6/config/webpack/environment.js ================================================ const { environment } = require('@rails/webpacker') module.exports = environment ================================================ FILE: test/apps/rails6/config/webpack/production.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || 'production' const environment = require('./environment') module.exports = environment.toWebpackConfig() ================================================ FILE: test/apps/rails6/config/webpack/test.js ================================================ process.env.NODE_ENV = process.env.NODE_ENV || 'development' const environment = require('./environment') module.exports = environment.toWebpackConfig() ================================================ FILE: test/apps/rails6/config/webpacker.yml ================================================ # Note: You must restart bin/webpack-dev-server for changes to take effect default: &default source_path: app/javascript source_entry_path: packs public_root_path: public public_output_path: packs cache_path: tmp/cache/webpacker check_yarn_integrity: false webpack_compile_output: false # Additional paths webpack should lookup modules # ['app/assets', 'engine/foo/app/assets'] resolved_paths: [] # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false # Extract and emit a css file extract_css: false static_assets_extensions: - .jpg - .jpeg - .png - .gif - .tiff - .ico - .svg - .eot - .otf - .ttf - .woff - .woff2 extensions: - .mjs - .js - .sass - .scss - .css - .module.sass - .module.scss - .module.css - .png - .svg - .gif - .jpeg - .jpg development: <<: *default compile: true # Verifies that versions and hashed value of the package contents in the project's package.json check_yarn_integrity: true # Reference: https://webpack.js.org/configuration/dev-server/ dev_server: https: false host: localhost port: 3035 public: localhost:3035 hmr: false # Inline should be set to true if using HMR inline: true overlay: true compress: true disable_host_check: true use_local_ip: false quiet: false headers: 'Access-Control-Allow-Origin': '*' watch_options: ignored: '**/node_modules/**' test: <<: *default compile: true # Compile test packs to a separate directory public_output_path: packs-test production: <<: *default # Production depends on precompilation of packs prior to booting for performance. compile: false # Extract and emit a css file extract_css: true # Cache manifest.json for performance cache_manifest: true ================================================ FILE: test/apps/rails6/config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application ================================================ FILE: test/apps/rails6/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails6/lib/run_stuff.rb ================================================ class RunStuff def run Tempfile.open("cool_stuff.txt") do |temp_file| `cat #{temp_file.path}` end end end ================================================ FILE: test/apps/rails6/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails6/lib/view_component/base.rb ================================================ module ViewComponent class Base # For testing 'known_renderable_class?' in CheckRender end end ================================================ FILE: test/apps/rails6/package.json ================================================ { "name": "rails6", "private": true, "dependencies": { "@rails/actioncable": "^6.0.0-alpha", "@rails/activestorage": "^6.0.0-alpha", "@rails/ujs": "^6.0.0-alpha", "@rails/webpacker": "^4.0.2", "turbolinks": "^5.2.0" }, "version": "0.1.0", "devDependencies": { "webpack-dev-server": "^3.2.1" } } ================================================ FILE: test/apps/rails6/postcss.config.js ================================================ module.exports = { plugins: [ require('postcss-import'), require('postcss-flexbugs-fixes'), require('postcss-preset-env')({ autoprefixer: { flexbox: 'no-2009' }, stage: 3 }) ] } ================================================ FILE: test/apps/rails7/MyGemfile ================================================ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "2.7.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.0.0" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails", ">= 3.4.1" # Use sqlite3 as the database for Active Record gem "sqlite3", "~> 1.4" # Use the Puma web server [https://github.com/puma/puma] gem "puma", "~> 5.0" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails", ">= 0.9.2" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] gem "turbo-rails", ">= 0.9.0" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails", ">= 0.7.3" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder", "~> 2.11" # Use Redis adapter to run Action Cable in production # gem "redis", "~> 4.0" # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", ">= 1.4.4", require: false gem 'ransack', '~>3.0.0' # Use Sass to process CSS # gem "sassc-rails", "~> 2.1" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" group :development, :test do # See https://edgeguides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", ">= 1.0.0", platforms: %i[ mri mingw x64_mingw ] end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console", ">= 4.1.0" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler", ">= 2.3.3" # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem "capybara", ">= 3.26" gem "selenium-webdriver", ">= 4.0.0" gem "webdrivers" end ================================================ FILE: test/apps/rails7/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../stylesheets .css //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js ================================================ FILE: test/apps/rails7/app/assets/images/.keep ================================================ ================================================ FILE: test/apps/rails7/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: test/apps/rails7/app/channels/application_cable/channel.rb ================================================ module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/apps/rails7/app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/apps/rails7/app/controllers/admin_controller.rb ================================================ class AdminController < ApplicationController def search_users # Medium warning because it's probably an admin interface User.ransack(params[:q]) end # Test kwsplats in filter options before_filter(**options) do |c| x end # Test weird option doesn't cause an error before_action :thing, if: -> { true } end ================================================ FILE: test/apps/rails7/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base def anonymouns_arguments(*, **, &) Time.use_zone(*, **, &) end def hash_value_omission x = 1 y = 2 {x:, y:} end def endless_method_definition(msg) = puts "#{Time.now}: #{msg}" def pattern_matching_parenthesis_ommission [0, 1] => _, x {y: 2} => y: {x:, y:} end def pattern_matching_non_local_variable_pin {timestamp: Time.now} in {timestamp: ^(Time.new(2021)..Time.new(2022))} end def pathname_stuff z = Pathname.new('a').join(params[:x], 'z').basename # should warn something(z) # should not be a duplicate warning Rails.root.join('a', 'b', "#{params[:c]}") # should warn end end ================================================ FILE: test/apps/rails7/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/apps/rails7/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController def redirect_to_last! redirect_to User.last! end def presence # Don't warn... @field is either "foo" or nil @field = params[:field].presence_in(%w[foo]) || raise(ActionController::BadRequest) render "admin2/fields/#{@field}" end def redirect_param_with_fallback redirect_to params[:redirect_url] || "/" end def redirect_url_from_param_with_fallback redirect_to url_from(params[:redirect_url]) || "/" end def redirect_with_allow_host redirect_to params[:x], allow_other_host: true # low confidence warning end def redirect_with_explicit_not_allow redirect_to params[:x], allow_other_host: false # no warning end def redirect_back_with_fallback redirect_back fallback_location: params[:x] end def redirect_back_or_to_with_fallback redirect_back_or_to params[:x] end def redirect_back_or_to_with_fallback_disallow_host redirect_back_or_to params[:x], allow_other_host: false # no warning end def search User.ransack(params[:q]) end def search_books # Should not warn - search limited appropriately Book.ransack(params[:q]) # Low confidence because no idea what `some_book` is some_book.things.ransack(params[:q]) end class << self def just_here_for_test_coverage_thanks end end end ================================================ FILE: test/apps/rails7/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails7/app/javascript/application.js ================================================ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" ================================================ FILE: test/apps/rails7/app/javascript/controllers/application.js ================================================ import { Application } from "@hotwired/stimulus" const application = Application.start() // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } ================================================ FILE: test/apps/rails7/app/javascript/controllers/hello_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { this.element.textContent = "Hello World!" } } ================================================ FILE: test/apps/rails7/app/javascript/controllers/index.js ================================================ // Import and register all your controllers from the importmap under controllers/* import { application } from "controllers/application" // Eager load all controllers defined in the import map under controllers/**/*_controller import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" // lazyLoadControllersFrom("controllers", application) ================================================ FILE: test/apps/rails7/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: test/apps/rails7/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end ================================================ FILE: test/apps/rails7/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base primary_abstract_class end ================================================ FILE: test/apps/rails7/app/models/book.rb ================================================ class Book < Thing def self.ransackable_attributes(auth_object = nil) [:author, :title] end end ================================================ FILE: test/apps/rails7/app/models/concerns/.keep ================================================ ================================================ FILE: test/apps/rails7/app/models/thing.rb ================================================ class Thing < ApplicationRecord class << self def ransackable_associations(auth_object = nil) [] end end end ================================================ FILE: test/apps/rails7/app/models/user.rb ================================================ class User < ApplicationRecord end ================================================ FILE: test/apps/rails7/app/views/layouts/application.html.erb ================================================ Rails7 <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <%= yield %> ================================================ FILE: test/apps/rails7/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails7/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails7/config/application.rb ================================================ require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails7 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults '7.0' # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") # Test with this off config.action_controller.raise_on_open_redirects = false end end ================================================ FILE: test/apps/rails7/config/boot.rb ================================================ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations. ================================================ FILE: test/apps/rails7/config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: rails7_production ================================================ FILE: test/apps/rails7/config/credentials.yml.enc ================================================ RPUdW3kTfs+b1n3IHxYV/61sD5RQDPjnXz3itzhf5fgWR/OMmzV6BLlq2InQ7ja4WaZ+9iqqCAJ/Z+FMyzTDCNnwt0KqIwVjkD9hrZcnP0WyKDt4d0kZLsKt7geymidFeIs6t8tO79sItn21GXa16ayVQ8Tj6YGD082E7cPGpbUTDOZqma+ohCGuejyrPEmpiE3S0MfxXF0Vg9zfX/s5G+394xz4U0nasdBvx3bqQdQh+FfjF03KUBSWVIYEZtI7xOmTMn2LmXvgEa9EgTI1w0dwrS1FqGi8Tm7kMo6w5/ZDDWwtCXL1BD41N5IEkAruT/KOdMvJ0Bq09DOE2Fy45KVAE5/iDaA/jFO8AMRmBu/DGPqENvHqyJOgF33QzhK4gMQcVyWdwJ6yJAMaUZ+R//vQE51o0hp15jBb--6Ew1EqhwGvVIZOCa--nwQu+e3pCVMFFcbvREqkGw== ================================================ FILE: test/apps/rails7/config/database.yml ================================================ # SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem "sqlite3" # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test.sqlite3 production: <<: *default database: db/production.sqlite3 ================================================ FILE: test/apps/rails7/config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails7/config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true end ================================================ FILE: test/apps/rails7/config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). config.log_level = :info # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "rails7_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector # middleware. The `delay` is used to determine how long to wait after a write # to send a subsequent read to the primary. # # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # # The `database_resolver_context` class is used by the middleware to set # timestamps for the last write to the primary. The resolver uses the context # class timestamps to determine how long to wait before reading from the # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own # strategy for connection switching and pass that into the middleware through # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session # Inserts middleware to perform automatic shard swapping. The `shard_selector` hash # can be used to pass options to the `ShardSelector` middleware. The `lock` option is # used to determine whether shard swapping should be prohibited for the request. # # The `shard_resolver` option is used by the middleware to determine which shard # to switch to. The application must provide a mechanism for finding the shard name # in a proc. See guides for an example. # config.active_record.shard_selector = { lock: true } # config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard } end ================================================ FILE: test/apps/rails7/config/environments/test.rb ================================================ require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Turn false under Spring and add config.action_view.cache_template_loading = true config.cache_classes = true # Eager loading loads your whole application. When running a single test locally, # this probably isn't necessary. It's a good idea to do in a continuous integration # system, or in some way before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true end ================================================ FILE: test/apps/rails7/config/importmap.rb ================================================ # Pin npm packages by running ./bin/importmap pin "application", preload: true pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true pin_all_from "app/javascript/controllers", under: "controllers" ================================================ FILE: test/apps/rails7/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: test/apps/rails7/config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap and inline scripts # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src) # # # Report CSP violations to a specified URI. See: # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # # config.content_security_policy_report_only = true # end ================================================ FILE: test/apps/rails7/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: test/apps/rails7/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: test/apps/rails7/config/initializers/permissions_policy.rb ================================================ # Define an application-wide HTTP permissions policy. For further # information see https://developers.google.com/web/updates/2018/06/feature-policy # # Rails.application.config.permissions_policy do |f| # f.camera :none # f.gyroscope :none # f.microphone :none # f.usb :none # f.fullscreen :self # f.payment :self, "https://secure.example.com" # end ================================================ FILE: test/apps/rails7/config/initializers/sanitizers.rb ================================================ Rails::Html::SafeListSanitizer.allowed_tags = ["select", "a", "style"] ================================================ FILE: test/apps/rails7/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t "hello" # # In views, this is aliased to just `t`: # # <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # "true": "foo" # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: test/apps/rails7/config/master.key ================================================ f5d5ab5ccde263f03a1221511f868f20 ================================================ FILE: test/apps/rails7/config/puma.rb ================================================ # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `worker_timeout` threshold that Puma will use to wait before # terminating a worker in development environments. # worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the `pidfile` that Puma will use. pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # # preload_app! # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart ================================================ FILE: test/apps/rails7/config/routes.rb ================================================ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Defines the root path route ("/") # root "articles#index" end ================================================ FILE: test/apps/rails7/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket-<%= Rails.env %> # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/apps/rails7/lib/assets/.keep ================================================ ================================================ FILE: test/apps/rails7/lib/some_lib.rb ================================================ class SomeLib def some_rsa_encrypting public_key = OpenSSL::PKey::RSA.new("grab the public 4096 bit key") encrypted = Base64.encode64(public_key.public_encrypt(payload.to_json)) # Weak padding mode default public_key.private_decrypt(Base64.decode64(encrypted)) # Weak padding mode default end def some_more_rsa_padding_modes public_key = OpenSSL::PKey::RSA.new("grab the public 4096 bit key") public_key.public_decrypt(data, OpenSSL::PKey::RSA::PKCS1_PADDING) public_key.private_encrypt(data, OpenSSL::PKey::RSA::NO_PADDING) public_key.private_encrypt(data, OpenSSL::PKey::RSA::SSLV23_PADDING) end def small_rsa_keys OpenSSL::PKey::RSA.generate(512) # Very weak OpenSSL::PKey::RSA.new(1024) # Weak OpenSSL::PKey::RSA.new(2048) # Okay end def pky_api weak_rsa = OpenSSL::PKey.generate_key("rsa", rsa_keygen_bits: 1024) # Medium warning about key size weak_encrypted = weak_rsa.encrypt("data", "rsa_padding_mode" => "pkcs1") weak_encrypted = weak_rsa.decrypt("data", "rsa_padding_mode" => "oaep") weak_signature_digest = weak_rsa.sign("SHA256", "data", rsa_padding_mode: "PKCS1") weak_rsa.verify("SHA256", "data", rsa_padding_mode: "none") weak_rsa.sign_raw(nil, "data", rsa_padding_mode: "none") weak_rsa.verify_raw(nil, "data", rsa_padding_mode: "none") weak_rsa.encrypt("data") # default is also pkcs1 end class << self def self.x # why end end end ================================================ FILE: test/apps/rails7/lib/tasks/.keep ================================================ ================================================ FILE: test/apps/rails8/Gemfile ================================================ source "https://rubygems.org" # Use main development branch of Rails gem "rails", "~> 8.0.0.alpha", github: "rails/rails", branch: "main" # The modern asset pipeline for Rails [https://github.com/rails/propshaft] gem "propshaft" # Use sqlite3 as the database for Active Record gem "sqlite3", ">= 1.4" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] gem "turbo-rails" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" # Use Redis adapter to run Action Cable in production gem "redis", ">= 4.0.1" # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] # gem "kamal", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] # gem "brakeman", require: false # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] # gem "rubocop-rails-omakase", require: false end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" end ================================================ FILE: test/apps/rails8/app/assets/stylesheets/application.css ================================================ /* Application styles */ ================================================ FILE: test/apps/rails8/app/channels/application_cable/channel.rb ================================================ module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: test/apps/rails8/app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: test/apps/rails8/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern def deserialize_it Marshal.load(something) # Always warns end end ================================================ FILE: test/apps/rails8/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController before_action :set_user, only: %i[ show edit update destroy ] # GET /users or /users.json def index @users = User.all end # GET /users/1 or /users/1.json def show end # GET /users/new def new @user = User.new end # GET /users/1/edit def edit end # POST /users or /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to user_url(@user), notice: "User was successfully created." } format.json { render :show, status: :created, location: @user } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # PATCH/PUT /users/1 or /users/1.json def update respond_to do |format| if @user.update(user_params) format.html { redirect_to user_url(@user), notice: "User was successfully updated." } format.json { render :show, status: :ok, location: @user } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 or /users/1.json def destroy @user.destroy! respond_to do |format| format.html { redirect_to users_url, notice: "User was successfully destroyed." } format.json { head :no_content } end end def things @things = Thing.all render 'things/index' end def permit_or if params[:foo_uid].present? field_name = :foo_uid query = params.permit(:foo_uid) elsif params[:model_id].present? field_name = :model_id query = { id: params.require(:model_id) } end Thing.find_by(query) if field_name end def stats_count $stats.count("thing.#{variable}", :tags => ({ :cool => "stuff" })) not_ar_model.count("something - #{params[:x]}") end private # Use callbacks to share common setup or constraints between actions. def set_user @user = User.find(params[:id]) end # Only allow a list of trusted parameters through. def user_params params.require(:user).permit(:name) end end ================================================ FILE: test/apps/rails8/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/apps/rails8/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails8/app/javascript/application.js ================================================ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" ================================================ FILE: test/apps/rails8/app/javascript/controllers/application.js ================================================ import { Application } from "@hotwired/stimulus" const application = Application.start() // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } ================================================ FILE: test/apps/rails8/app/javascript/controllers/hello_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { this.element.textContent = "Hello World!" } } ================================================ FILE: test/apps/rails8/app/javascript/controllers/index.js ================================================ // Import and register all your controllers from the importmap under controllers/* import { application } from "controllers/application" // Eager load all controllers defined in the import map under controllers/**/*_controller import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" // lazyLoadControllersFrom("controllers", application) ================================================ FILE: test/apps/rails8/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: test/apps/rails8/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end ================================================ FILE: test/apps/rails8/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base primary_abstract_class end ================================================ FILE: test/apps/rails8/app/models/thing.rb ================================================ class Thing < ApplicationRecord end ================================================ FILE: test/apps/rails8/app/models/user.rb ================================================ class User < ApplicationRecord end ================================================ FILE: test/apps/rails8/app/views/layouts/application.html.erb ================================================ <%= content_for(:title) || "My App" %> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= yield :head %> <%= stylesheet_link_tag :all, "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <%= yield %> ================================================ FILE: test/apps/rails8/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails8/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/apps/rails8/app/views/pwa/manifest.json.erb ================================================ { "name": "MyApp", "icons": [ { "src": "/icon.png", "type": "image/png", "sizes": "512x512" }, { "src": "/icon.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" } ], "start_url": "/", "display": "standalone", "scope": "/", "description": "MyApp.", "theme_color": "red", "background_color": "red" } ================================================ FILE: test/apps/rails8/app/views/pwa/service-worker.js ================================================ // Add a service worker for processing Web Push notifications: // // self.addEventListener("push", async (event) => { // const { title, options } = await event.data.json() // event.waitUntil(self.registration.showNotification(title, options)) // }) // // self.addEventListener("notificationclick", function(event) { // event.notification.close() // event.waitUntil( // clients.matchAll({ type: "window" }).then((clientList) => { // for (let i = 0; i < clientList.length; i++) { // let client = clientList[i] // let clientPath = (new URL(client.url)).pathname // // if (clientPath == event.notification.data.path && "focus" in client) { // return client.focus() // } // } // // if (clients.openWindow) { // return clients.openWindow(event.notification.data.path) // } // }) // ) // }) ================================================ FILE: test/apps/rails8/app/views/things/_thing.html.erb ================================================ <%= raw thing.name %> ================================================ FILE: test/apps/rails8/app/views/things/index.html.erb ================================================ <%= render @things %> ================================================ FILE: test/apps/rails8/app/views/users/_form.html.erb ================================================ <%= form_with(model: user) do |form| %> <% if user.errors.any? %>

    <%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:

      <% user.errors.each do |error| %>
    • <%= error.full_message %>
    • <% end %>
    <% end %>
    <%= form.label :name, style: "display: block" %> <%= form.text_field :name %>
    <%= form.submit %>
    <% end %> ================================================ FILE: test/apps/rails8/app/views/users/_user.html.erb ================================================

    Name: <%= user.name.html_safe %>

    ================================================ FILE: test/apps/rails8/app/views/users/_user.json.jbuilder ================================================ json.extract! user, :id, :name, :created_at, :updated_at json.url user_url(user, format: :json) ================================================ FILE: test/apps/rails8/app/views/users/dom_id.haml ================================================ .test-list__item{ id: dom_id(User.first) } ================================================ FILE: test/apps/rails8/app/views/users/edit.html.erb ================================================ <% content_for :title, "Editing user" %>

    Editing user

    <%= render "form", user: @user %>
    <%= link_to "Show this user", @user %> | <%= link_to "Back to users", users_path %>
    ================================================ FILE: test/apps/rails8/app/views/users/index.html.erb ================================================

    <%= notice %>

    <% content_for :title, "Users" %>

    Users

    <% @users.each do |user| %> <%= render user %>

    <%= link_to "Show this user", user %>

    <% end %>
    <%= link_to "New user", new_user_path %> ================================================ FILE: test/apps/rails8/app/views/users/index.json.jbuilder ================================================ json.array! @users, partial: "users/user", as: :user ================================================ FILE: test/apps/rails8/app/views/users/new.html.erb ================================================ <% content_for :title, "New user" %>

    New user

    <%= render "form", user: @user %>
    <%= link_to "Back to users", users_path %>
    ================================================ FILE: test/apps/rails8/app/views/users/show.html.erb ================================================

    <%= notice %>

    <%= render @user %>
    <%= link_to "Edit this user", edit_user_path(@user) %> | <%= link_to "Back to users", users_path %> <%= button_to "Destroy this user", @user, method: :delete %>
    ================================================ FILE: test/apps/rails8/app/views/users/show.json.jbuilder ================================================ json.partial! "users/user", user: @user ================================================ FILE: test/apps/rails8/bin/brakeman ================================================ #!/usr/bin/env ruby require "rubygems" require "bundler/setup" ARGV.unshift("--ensure-latest") load Gem.bin_path("brakeman", "brakeman") ================================================ FILE: test/apps/rails8/bin/importmap ================================================ #!/usr/bin/env ruby require_relative "../config/application" require "importmap/commands" ================================================ FILE: test/apps/rails8/bin/kamal ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'kamal' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("kamal", "kamal") ================================================ FILE: test/apps/rails8/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: test/apps/rails8/bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: test/apps/rails8/bin/rubocop ================================================ #!/usr/bin/env ruby require "rubygems" require "bundler/setup" # explicit rubocop config increases performance slightly while avoiding config confusion. ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) load Gem.bin_path("rubocop", "rubocop") ================================================ FILE: test/apps/rails8/bin/setup ================================================ #!/usr/bin/env ruby require "fileutils" APP_ROOT = File.expand_path("..", __dir__) APP_NAME = "my_app" def system!(*args) system(*args, exception: true) end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" system! "bin/rails restart" # puts "\n== Configuring puma-dev ==" # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" end ================================================ FILE: test/apps/rails8/config/application.rb ================================================ require_relative "boot" require "rails" # Pick the frameworks you want: require "active_model/railtie" require "active_job/railtie" require "active_record/railtie" require "active_storage/engine" require "action_controller/railtie" require "action_mailer/railtie" require "action_mailbox/engine" require "action_text/engine" require "action_view/railtie" require "action_cable/engine" # require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) class MyApp::Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 8.0 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") # Don't generate system test files. config.generators.system_tests = nil end ================================================ FILE: test/apps/rails8/config/boot.rb ================================================ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations. ================================================ FILE: test/apps/rails8/config/cable.yml ================================================ development: adapter: redis url: redis://localhost:6379/1 test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: my_app_production ================================================ FILE: test/apps/rails8/config/credentials.yml.enc ================================================ H0D18ePAXpz1hsN50y92mMXWQ81JfVpO4AcGAmekoYyhfKnV/axh7XnWCbo2QfCsMqtA3RFlexqE458SmUhC0ELC4XUKbf4YLlDZU3hPdC4AszUkqdZO5XxI9UT2HD77577sYni+mbbxpOESqMr7IvTDEpYvre79afPFBbodif8WEm6fQJVvQT4QbYsuRa1Rxg+cvtBh0MZZEhR2qGahs8b0xyHqKtRBts8NtDaDkM8E9nDQ6rm6/D6/cc9kLmLSvOrlLZcEZc999CyZKOwZKVETuGGGLWeGSL3XBMZdSI7lopzwHBSXn4bcYfuUI1gysxaIBu8X0Z/3Fb/dAIj3gpyE9kVJkC+82EoPpfrrNTu4W2FYxGX7PG+l58BBcpGCpzUOrW3KlOA6pPtwb9swkwlFunxl--I/0189+5PL944kTB--etq+j498EBV5ynkttoEY3w== ================================================ FILE: test/apps/rails8/config/database.yml ================================================ # SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem "sqlite3" # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: storage/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: storage/test.sqlite3 # Store production database in the storage/ directory, which by default # is mounted as a persistent Docker volume in config/deploy.yml. production: <<: *default database: storage/production.sqlite3 ================================================ FILE: test/apps/rails8/config/deploy.yml ================================================ # Name of your application. Used to uniquely configure containers. service: my_app # Name of the container image. image: your-user/my_app # Deploy to these servers. servers: web: - 192.168.0.1 # job: # hosts: # - 192.168.0.1 # cmd: bin/solid_queue work # Credentials for your image host. registry: # Specify the registry server, if you're not using Docker Hub # server: registry.digitalocean.com / ghcr.io / ... username: your-user # Always use an access token rather than real password when possible. password: - KAMAL_REGISTRY_PASSWORD # Inject ENV variables into containers (secrets come from .env). # Remember to run `kamal env push` after making changes! env: secret: - RAILS_MASTER_KEY # clear: # DB_HOST: 192.168.0.2 # Use a persistent storage volume for sqlite database files and local Active Storage files. # Recommended to change this to a mounted volume path that is backed up off server. volumes: - "my_app_storage:/rails/storage" # Bridge fingerprinted assets, like JS and CSS, between versions to avoid # hitting 404 on in-flight requests. Combines all files from new and old # version inside the asset_path. asset_path: /rails/public/assets # Use a different ssh user than root # ssh: # user: app # Configure builder setup (defaults to multi-arch images). # builder: # # Build same-arch image locally (use for x86->x86) # multiarch: false # # # Build diff-arch image via remote server # remote: # arch: amd64 # host: ssh://app@192.168.0.1 # # args: # RUBY_VERSION: ruby-3.3.0 # secrets: # - GITHUB_TOKEN # - RAILS_MASTER_KEY # Use accessory services (secrets come from .env). # accessories: # db: # image: mysql:8.0 # host: 192.168.0.2 # port: 3306 # env: # clear: # MYSQL_ROOT_HOST: '%' # secret: # - MYSQL_ROOT_PASSWORD # files: # - config/mysql/production.cnf:/etc/mysql/my.cnf # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql # directories: # - data:/var/lib/mysql # redis: # image: redis:7.0 # host: 192.168.0.2 # port: 6379 # directories: # - data:/data ================================================ FILE: test/apps/rails8/config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/apps/rails8/config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing. config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! end ================================================ FILE: test/apps/rails8/config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. # config.public_file_server.enabled = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Skip http-to-https redirect for the default health check endpoint. # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT by default config.logger = ActiveSupport::Logger.new(STDOUT) .tap { |logger| logger.formatter = ::Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # "info" includes generic and useful information about system operation, but avoids logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). If you # want to log everything, set the level to "debug". config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "my_app_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end ================================================ FILE: test/apps/rails8/config/environments/test.rb ================================================ require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Unlike controllers, the mailer instance doesn't have any context about the # incoming request so you'll need to provide the :host parameter yourself. config.action_mailer.default_url_options = { host: "www.example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true end ================================================ FILE: test/apps/rails8/config/importmap.rb ================================================ # Pin npm packages by running ./bin/importmap pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" ================================================ FILE: test/apps/rails8/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path ================================================ FILE: test/apps/rails8/config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: test/apps/rails8/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: test/apps/rails8/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: test/apps/rails8/config/initializers/permissions_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide HTTP permissions policy. For further # information see: https://developers.google.com/web/updates/2018/06/feature-policy # Rails.application.config.permissions_policy do |policy| # policy.camera :none # policy.gyroscope :none # policy.microphone :none # policy.usb :none # policy.fullscreen :self # policy.payment :self, "https://secure.example.com" # end ================================================ FILE: test/apps/rails8/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization and # are automatically loaded by Rails. If you want to use locales other than # English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t "hello" # # In views, this is aliased to just `t`: # # <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more about the API, please read the Rails Internationalization guide # at https://guides.rubyonrails.org/i18n.html. # # Be aware that YAML interprets the following case-insensitive strings as # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings # must be quoted to be interpreted as strings. For example: # # en: # "yes": yup # enabled: "ON" en: hello: "Hello world" ================================================ FILE: test/apps/rails8/config/master.key ================================================ 14cad5d3e84a5abfc93a8d61873a7f66 ================================================ FILE: test/apps/rails8/config/puma.rb ================================================ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to # prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's # Global VM Lock (GVL) it has diminishing returns and will degrade the # response time (latency) of the application. # # The default is set to 3 threads as it's deemed a decent compromise between # throughput and latency for the average Rails application. # # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `environment` that Puma will run in. rails_env = ENV.fetch("RAILS_ENV", "development") environment rails_env case rails_env when "production" # If you are running more than 1 thread per process, the workers count # should be equal to the number of processors (CPU cores) in production. # # Automatically detect the number of available processors in production. require "concurrent-ruby" workers_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.available_processor_count }) workers workers_count if workers_count > 1 preload_app! when "development" # Specifies a very generous `worker_timeout` so that the worker # isn't killed by Puma when suspended by a debugger. worker_timeout 3600 end # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart # Only use a pidfile when requested pidfile ENV["PIDFILE"] if ENV["PIDFILE"] ================================================ FILE: test/apps/rails8/config/routes.rb ================================================ Rails.application.routes.draw do resources :users # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Render dynamic PWA files from app/views/pwa/* get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # Defines the root path route ("/") # root "posts#index" end ================================================ FILE: test/apps/rails8/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket-<%= Rails.env %> # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/apps/rails8/config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server ================================================ FILE: test/apps/rails8/lib/evals.rb ================================================ class Evals def evals(something) instance_eval "plain string - no warning" instance_eval "interpolated #{string} - warning" instance_eval anything_else # no warning eval anything # warning self.class.class_eval do # no warning end if [1, 2, 3].include? code eval code # no warning end eval "interpolate #{something}" end def safe_strings ["good", "fine"].each do |suffix| class_eval <<-METHODS def method_that_is_#{suffix} puts suffix end METHODS end end class << self def defs_eval(string) eval("foo #{string}") end end def Object.object_defs_eval(string) eval("foo #{string}") end def @ivar.ivar_def_eval(string) eval("foo #{string}") end lvar = Object.new def lvar.lvar_def_eval(string) eval("foo #{string}") end end ================================================ FILE: test/apps/rails8/lib/masgn.rb ================================================ def test_masgn_recursion r = lambda { x, q = r } y, z = r end ================================================ FILE: test/apps/rails_with_xss_plugin/Gemfile ================================================ source 'http://rubygems.org' gem 'rails', '2.3.14' gem 'json', '1.1.0' gem 'sqlite3' ================================================ FILE: test/apps/rails_with_xss_plugin/README ================================================ This is a test application which uses the rails_xss plugin ================================================ FILE: test/apps/rails_with_xss_plugin/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require(File.join(File.dirname(__FILE__), 'config', 'boot')) require 'rake' require 'rake/testtask' require 'rake/rdoctask' require 'tasks/rails' ================================================ FILE: test/apps/rails_with_xss_plugin/app/controllers/application_controller.rb ================================================ # Filters added to this controller apply to all controllers in the application. # Likewise, all the methods added will be available for all controllers. class ApplicationController < ActionController::Base helper :all # include all helpers, all the time #protect_from_forgery # See ActionController::RequestForgeryProtection for details before_filter :current_user def current_user return @current_user if @current_user user_id = session[:user_id] || cookies[:user_id] if user_id @current_user = User.find(user_id) end end def require_logged_in unless @current_user redirect_to '/login', :page => request.path end end def require_admin unless @current_user and @current_user.admin? render :text => "Access denied" end end end ================================================ FILE: test/apps/rails_with_xss_plugin/app/controllers/posts_controller.rb ================================================ class PostsController < ApplicationController before_filter :require_logged_in, :except => [:index, :show] # GET /posts # GET /posts.xml def index @posts = Post.all(:conditions => { :in_reply_to => nil }) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } end end # GET /posts/1 # GET /posts/1.xml def show @post = Post.find(params[:id]) @user = User.find(@post.user_id) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @post } end end # GET /posts/new # GET /posts/new.xml def new @post = Post.new @post.in_reply_to = params[:in_reply_to] respond_to do |format| format.html # new.html.erb format.xml { render :xml => @post } end end # GET /posts/1/edit def edit @post = Post.find(params[:id]) end # POST /posts # POST /posts.xml def create @post = Post.new(params[:post]) @post.user_id = @current_user.id respond_to do |format| if @post.save format.html { redirect_to(@post, :notice => 'Post was successfully created.') } format.xml { render :xml => @post, :status => :created, :location => @post } else format.html { render :action => "new" } format.xml { render :xml => @post.errors, :status => :unprocessable_entity } end end end # PUT /posts/1 # PUT /posts/1.xml def update @post = Post.find(params[:id]) respond_to do |format| if @post.update_attributes(params[:post]) format.html { redirect_to(@post, :notice => 'Post was successfully updated.') } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @post.errors, :status => :unprocessable_entity } end end end # DELETE /posts/1 # DELETE /posts/1.xml def destroy @post = Post.find(params[:id]) @post.destroy respond_to do |format| format.html { redirect_to(posts_url) } format.xml { head :ok } end end end ================================================ FILE: test/apps/rails_with_xss_plugin/app/controllers/users_controller.rb ================================================ class UsersController < ApplicationController before_filter :require_logged_in, :only => [:edit, :destroy, :update] # GET /users # GET /users.xml def index @users = User.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @users } end end # GET /users/1 # GET /users/1.xml def show @evil_input = params[:of_doom] @user = User.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @user } end end # GET /users/new # GET /users/new.xml def new @user = User.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @user } end end # GET /users/1/edit def edit @user = User.find(params[:id]) @user.password = nil end # POST /users # POST /users.xml def create @user = User.new(params[:user]) @user.password = `echo #{params[:user][:password]} | shasum` @user.user_name.downcase! respond_to do |format| if User.find(:all, :conditions => { :user_name => params[:user][:user_name] }).empty? and @user.save session[:user_id] = @user.id format.html { redirect_to(@user, :notice => 'User was successfully created.') } format.xml { render :xml => @user, :status => :created, :location => @user } else @user = User.new format.html { render :action => "new" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end # PUT /users/1 # PUT /users/1.xml def update @user = User.find(params[:id]) @user.password = `echo #{params[:user][:password]} | shasum` respond_to do |format| if @user.update_attributes(params[:user]) format.html { redirect_to(@user, :notice => 'User was successfully updated.') } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end # DELETE /users/1 # DELETE /users/1.xml def destroy @user = User.find(params[:id]) @user.destroy respond_to do |format| format.html { redirect_to(users_url) } format.xml { head :ok } end end def login if current_user redirect_to params.update(:action => :index) else render :notice => "Could not login" end end def login_user password = `echo #{params[:user][:password]} | shasum` $stderr.puts "password: #{password}" user = User.find(:first, :conditions => { :user_name => params[:user][:user_name].downcase, :password => password }) if user.nil? redirect_to '/login' else session[:user_id] = user.id redirect_to :controller => :posts, :action => :index end end def logout session[:user_id] = nil redirect_to :action => :login end def search end def results @users = User.all(:conditions => "display_name like '%#{params[:query]}%'") end def to_json end def delete_them_all if User.connection.select_value("SELECT * from users WHERE name='#{params[:name]}'").nil? #should warn User.connection.execute("TRUNCATE users") #shouldn't warn end end def test_sanitize @x = params[:x] end def string_mass User.new("stuff") end end ================================================ FILE: test/apps/rails_with_xss_plugin/app/helpers/application_helper.rb ================================================ # Methods added to this helper will be available to all templates in the application. module ApplicationHelper def authorized? (@user and @current_user == @user[:id]) or (@current_user and @current_user.admin?) end end ================================================ FILE: test/apps/rails_with_xss_plugin/app/helpers/posts_helper.rb ================================================ module PostsHelper def author_of? post @current_user and post.user_id == @current_user.id end end ================================================ FILE: test/apps/rails_with_xss_plugin/app/helpers/users_helper.rb ================================================ module UsersHelper end ================================================ FILE: test/apps/rails_with_xss_plugin/app/models/post.rb ================================================ class Post < ActiveRecord::Base belongs_to :user end ================================================ FILE: test/apps/rails_with_xss_plugin/app/models/user.rb ================================================ class User < ActiveRecord::Base has_many :posts validates_uniqueness_of :user_name validates_format_of :user_name, :with => /^\w+$/ validates_length_of :user_name, :maximum => 10 validates_format_of :display_name, :with => /^(\w|\s)+$/ validates_presence_of :user_name, :display_name, :password end ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/layouts/posts.html.erb ================================================ Users: <%= controller.action_name %> <%= stylesheet_link_tag 'scaffold' %>

    <%= notice %>

    <%= yield %>

    • <%= link_to 'Posts', posts_url %>
    • <%= link_to 'Users', users_url %>
    • <% if @current_user %>
    • <%= link_to 'Logout', '/logout' %>
    • <% else %>
    • <%= link_to 'Login', '/login' %>
    • <% end %>
    ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/layouts/users.html.erb ================================================ Users: <%= controller.action_name %> <%= stylesheet_link_tag 'scaffold' %>

    <%= notice %>

    <%= yield %>

    • <%= link_to 'Posts', posts_url %>
    • <%= link_to 'Users', users_url %>
    • <%= link_to 'Search Users', '/search' %>
    • <% if @current_user %>
    • <%= link_to 'Edit User', :controller => 'users', :action => 'edit', :id => @current_user %>
    • <%= link_to 'Logout', '/logout' %>
    • <% else %>
    • <%= link_to 'Login', '/login' %>
    • <% end %>
    ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/_show.html.erb ================================================

    User: <%=h @post.user_id %>

    Title: <%=h @post.title %>

    Body: <%=h @post.body %>

    <% if @user == @post.user_id %> <%= link_to 'Edit', edit_post_path(@post) %> | <% end %> <%= link_to 'Back', posts_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/edit.html.erb ================================================

    Editing post

    <% form_for(@post) do |f| %> <%= f.error_messages %>

    <%= f.label :user_id %>
    <%= f.text_field :user_id %>

    <%= f.label :title %>
    <%= f.text_field :title %>

    <%= f.label :body %>
    <%= f.text_field :body %>

    <%= f.submit 'Update' %>

    <% end %> <%= link_to 'Show', @post %> | <%= link_to 'Back', posts_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/index.html.erb ================================================

    Topics

    <% unless @posts.empty? %> <% end %> <% @posts.each do |post| %> <% if author_of? post %> <% end %> <% end %>
    User Title
    <%=h User.find(post.user_id).user_name %> <%=h post.title %> <%= link_to 'View', post %><%= link_to 'Edit', edit_post_path(post) %> <%= link_to 'Delete', post, :method => :delete %>

    <%= link_to 'New post', new_post_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/new.html.erb ================================================

    New post

    <% form_for(@post) do |f| %> <%= f.error_messages %>

    <%= f.label :title %>
    <%= f.text_field :title %>

    <%= f.label :body %>
    <%= f.text_field :body %>

    <%= f.hidden_field :in_reply_to %>

    <%= f.submit 'Create' %>

    <% end %> <%= link_to 'Back', posts_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/show.html.erb ================================================

    User: <%= User.find(@post.user_id).user_name %>

    Title: <%=h @post.title %>

    Body: <%=h @post.body %>

    <% if @user == @post.user_id %> <%= link_to 'Edit', edit_post_path(@post) %> | <% end %> <%= link_to 'Back', posts_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/posts/show_topic.html.erb ================================================

    User: <%=h @post.user_id %>

    Title: <%=h @post.title %>

    Body: <%=h @post.body %>

    <% if @user == @post.user_id %> <%= link_to 'Edit', edit_post_path(@post) %> | <% end %> <%= link_to 'Back', posts_path %> <%= render :partial => 'show', :collection => @posts %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/_user.html.erb ================================================

    <%= link_to user.display_name, user %>

    ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/edit.html.erb ================================================

    Edit Account

    <% form_for(@user) do |f| %> <%= f.error_messages %>

    <%= f.label :display_name %>
    <%= f.text_field :display_name %>

    <%= f.label :password %>
    <%= f.password_field :password %>

    <% if @current_user and @current_user.admin? %>

    <%= f.label :admin %>
    <%= f.check_box :admin %>

    <% end %>

    <%= f.submit 'Save' %>

    <% end %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/index.html.erb ================================================

    Listing users

    <% @users.each do |user| %> <% if @current_user and (user.id == @current_user.id or @current_user.admin?) %> <% end %> <% end %>
    <%= raw link_to user.display_name, user %>| <%= link_to 'Edit', edit_user_path(user) %> <%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %>

    <%= link_to 'New user', new_user_path %> <%= select('post', 'author_id', "") %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/login.html.erb ================================================

    Login

    <% form_for(:user, :url => { :action => :login_user }) do |f| %> <%= f.error_messages %>

    <%= f.label :user_name %>
    <%= f.text_field :user_name %>

    <%= f.label :password %>
    <%= f.password_field :password %> <%= hidden_field_tag :page, params[:page] %>

    <%= f.submit 'Login' %>

    <% end %> <%= link_to 'Create new user', new_user_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/new.html.erb ================================================

    New user

    <% form_for(@user) do |f| %> <%= f.error_messages %>

    <%= f.label :display_name %>
    <%= f.text_field :display_name %>

    <%= f.label :user_name %>
    <%= f.text_field :user_name %>

    <%= f.label :password %>
    <%= f.password_field :password %>

    <%= f.submit 'Create' %>

    <% end %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/results.html.erb ================================================

    Results for <%= params[:query] %>:

    <% if @users.empty? %> No results. <% else %> <% @users.each do |user| %> <%= render user, :object => user %> <% end %> <% end %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/search.html.erb ================================================ <% form_tag '/results' do %> Find user: <%= text_field_tag :query %> <%= submit_tag 'Search' %> <% end %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/show.html.erb ================================================

    Display name: <%=h @user.display_name %>

    User name: <%=h @user.user_name %>

    Bio: <%= raw @user.bio %>

    Bad: <%= @evil_input %>

    <%= strip_tags @user.profile %> <% if @current_user and (@current_user.id == @user.id or @current_user.admin?) %> <%= link_to 'Edit', edit_user_path(@user) %> | <% end %> <%= link_to 'Back', users_path %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/test_sanitize.html.erb ================================================ <%= sanitize params[:x] %> <%= @x %> ================================================ FILE: test/apps/rails_with_xss_plugin/app/views/users/to_json.html.erb ================================================ <%= raw({:asdf => params[:asdf]}.to_json) %> ================================================ FILE: test/apps/rails_with_xss_plugin/config/boot.rb ================================================ # Don't change this file! # Configure your app in config/environment.rb and config/environments/*.rb RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) module Rails class << self def boot! unless booted? preinitialize pick_boot.run end end def booted? defined? Rails::Initializer end def pick_boot (vendor_rails? ? VendorBoot : GemBoot).new end def vendor_rails? File.exist?("#{RAILS_ROOT}/vendor/rails") end def preinitialize load(preinitializer_path) if File.exist?(preinitializer_path) end def preinitializer_path "#{RAILS_ROOT}/config/preinitializer.rb" end end class Boot def run load_initializer Rails::Initializer.run(:set_load_path) end end class VendorBoot < Boot def load_initializer require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" Rails::Initializer.run(:install_gem_spec_stubs) Rails::GemDependency.add_frozen_gem_path end end class GemBoot < Boot def load_initializer self.class.load_rubygems load_rails_gem require 'initializer' end def load_rails_gem if version = self.class.gem_version gem 'rails', version else gem 'rails' end rescue Gem::LoadError => load_error if load_error.message =~ /Could not find RubyGem rails/ STDERR.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) exit 1 else raise end end class << self def rubygems_version Gem::RubyGemsVersion rescue nil end def gem_version if defined? RAILS_GEM_VERSION RAILS_GEM_VERSION elsif ENV.include?('RAILS_GEM_VERSION') ENV['RAILS_GEM_VERSION'] else parse_gem_version(read_environment_rb) end end def load_rubygems min_version = '1.3.2' require 'rubygems' unless rubygems_version >= min_version $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) exit 1 end rescue LoadError $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) exit 1 end def parse_gem_version(text) $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ end private def read_environment_rb File.read("#{RAILS_ROOT}/config/environment.rb") end end end end # All that for this: Rails.boot! ================================================ FILE: test/apps/rails_with_xss_plugin/config/database.yml ================================================ # SQLite version 3.x # gem install sqlite3-ruby (not necessary on OS X Leopard) development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000 ================================================ FILE: test/apps/rails_with_xss_plugin/config/environment.rb ================================================ # Be sure to restart your server when you modify this file # Specifies gem version of Rails to use when vendor/rails is not present RAILS_GEM_VERSION = '2.3.14' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') Rails::Initializer.run do |config| # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Add additional load paths for your own custom dirs # config.autoload_paths += %W( #{RAILS_ROOT}/extras ) # Specify gems that this application depends on and have them installed with rake gems:install # config.gem "bj" # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" # config.gem "sqlite3-ruby", :lib => "sqlite3" # config.gem "aws-s3", :lib => "aws/s3" # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Skip frameworks you're not going to use. To use Rails without a database, # you must remove the Active Record framework. # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] # Activate observers that should always be running # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. config.time_zone = 'UTC' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] # config.i18n.default_locale = :de end ================================================ FILE: test/apps/rails_with_xss_plugin/config/environments/development.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the webserver when you make code changes. config.cache_classes = false # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_view.debug_rjs = true config.action_controller.perform_caching = false # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = false ================================================ FILE: test/apps/rails_with_xss_plugin/config/environments/production.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # The production environment is meant for finished, "live" apps. # Code is not reloaded between requests config.cache_classes = true # Full error reports are disabled and caching is turned on config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true config.action_view.cache_template_loading = true # See everything in the log (default is :info) # config.log_level = :debug # Use a different logger for distributed setups # config.logger = SyslogLogger.new # Use a different cache store in production # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and javascripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false # Enable threaded mode # config.threadsafe! ================================================ FILE: test/apps/rails_with_xss_plugin/config/environments/test.rb ================================================ # Settings specified here will take precedence over those in config/environment.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Log error messages when you accidentally call methods on nil. config.whiny_nils = true # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false config.action_view.cache_template_loading = true # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Use SQL instead of Active Record's schema dumper when creating the test database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying do debug a problem that might steem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/cookie_verification_secret.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. ActionController::Base.cookie_verifier_secret = 'b87b8a9b6268a2ef25ba27ec0c70f7bccf759d862cfe788de36c8db96bb9bfacf77057c49b1a4cc4936aff465883a395b9310a3264e1123e751190f38181456d'; ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format # (all these examples are active by default): # ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/json_parsing.rb ================================================ ActiveSupport::JSON.backend = "JSONGem" ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/new_rails_defaults.rb ================================================ # Be sure to restart your server when you modify this file. # These settings change the behavior of Rails 2 apps and will be defaults # for Rails 3. You can remove this initializer when Rails 3 is released. if defined?(ActiveRecord) # Include Active Record class name as root for JSON serialized output. ActiveRecord::Base.include_root_in_json = true # Store the full class name (including module namespace) in STI type column. ActiveRecord::Base.store_full_sti_class = true end ActionController::Routing.generate_best_match = false # Use ISO 8601 format for JSON serialized times and dates. ActiveSupport.use_standard_json_time_format = true # Don't escape HTML entities in JSON, leave that for the #json_escape helper. # if you're including raw json in an HTML page. ActiveSupport.escape_html_entities_in_json = false ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/session_store.rb ================================================ # Be sure to restart your server when you modify this file. # Your secret key for verifying cookie session data integrity. # If you change this key, all old sessions will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. ActionController::Base.session = { :key => '_forums_session', :secret => 'a2b5bb679742890a7af48afb795fc54b17b19670a6ebd713bfaa25c11988cf8619c7138b32913a825dfc37876aa011a195260feaa7219b37c1797762668eba8d' } # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rake db:sessions:create") # ActionController::Base.session_store = :active_record_store ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/single_quote_workaround.rb ================================================ class ERB module Util if "html_safe exists".respond_to?(:html_safe) def html_escape(s) s = s.to_s if s.html_safe? s else Rack::Utils.escape_html(s).html_safe end end else def html_escape(s) s = s.to_s Rack::Utils.escape_html(s).html_safe end end remove_method :h alias h html_escape class << self remove_method :html_escape remove_method :h end module_function :html_escape module_function :h end end ================================================ FILE: test/apps/rails_with_xss_plugin/config/initializers/yaml_parsing.rb ================================================ #Enable YAML parsing (bad) ActionController::Base.param_parsers[Mime::YAML] = :yaml #Disable YAML in XML (good) ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING.delete('symbol') ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING.delete('yaml') ================================================ FILE: test/apps/rails_with_xss_plugin/config/locales/en.yml ================================================ # Sample localization file for English. Add more files in this directory for other locales. # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: hello: "Hello world" ================================================ FILE: test/apps/rails_with_xss_plugin/config/routes.rb ================================================ ActionController::Routing::Routes.draw do |map| map.resources :posts map.resources :users map.connect 'login', :controller => 'users', :action => 'login' map.connect 'logout', :controller => 'users', :action => 'logout' map.connect 'results', :controller => 'users', :action => 'results' map.connect 'search', :controller => 'users', :action => 'search' # The priority is based upon order of creation: first created -> highest priority. # Sample of regular route: # map.connect 'products/:id', :controller => 'catalog', :action => 'view' # Keep in mind you can assign values other than :controller and :action # Sample of named route: # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' # This route can be invoked with purchase_url(:id => product.id) # Sample resource route (maps HTTP verbs to controller actions automatically): # map.resources :products # Sample resource route with options: # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } # Sample resource route with sub-resources: # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller # Sample resource route with more complex sub-resources # map.resources :products do |products| # products.resources :comments # products.resources :sales, :collection => { :recent => :get } # end # Sample resource route within a namespace: # map.namespace :admin do |admin| # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) # admin.resources :products # end # You can have the root of your site routed with map.root -- just remember to delete public/index.html. # map.root :controller => "welcome" # See how all your routes lay out with "rake routes" # Install the default routes as the lowest priority. # Note: These default routes make all actions in every controller accessible via GET requests. You should # consider removing or commenting them out if you're using named routes and resources. map.root :controller => "posts" map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end ================================================ FILE: test/apps/rails_with_xss_plugin/db/migrate/20120312064721_create_users.rb ================================================ class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :display_name t.string :user_name t.string :signature t.string :profile t.string :password t.boolean :admin t.timestamps end end def self.down drop_table :users end end ================================================ FILE: test/apps/rails_with_xss_plugin/db/migrate/20120312065023_create_posts.rb ================================================ class CreatePosts < ActiveRecord::Migration def self.up create_table :posts do |t| t.integer :user_id t.string :title t.string :body t.integer :in_reply_to t.timestamps end end def self.down drop_table :posts end end ================================================ FILE: test/apps/rails_with_xss_plugin/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead of editing this file, # please use the migrations feature of Active Record to incrementally modify your database, and # then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your database schema. If you need # to create the application database on another system, you should be using db:schema:load, not running # all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended to check this file into your version control system. ActiveRecord::Schema.define(:version => 20120312065023) do create_table "posts", :force => true do |t| t.integer "user_id" t.string "title" t.string "body" t.integer "in_reply_to" t.datetime "created_at" t.datetime "updated_at" end create_table "users", :force => true do |t| t.string "display_name" t.string "user_name" t.string "signature" t.string "profile" t.string "password" t.boolean "admin" t.datetime "created_at" t.datetime "updated_at" end end ================================================ FILE: test/apps/rails_with_xss_plugin/db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # # Examples: # # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) # Major.create(:name => 'Daley', :city => cities.first) ================================================ FILE: test/apps/rails_with_xss_plugin/doc/README_FOR_APP ================================================ Use this README file to introduce your application and point to useful places in the API for learning more. Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. ================================================ FILE: test/apps/rails_with_xss_plugin/public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

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

    ================================================ FILE: test/apps/rails_with_xss_plugin/public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

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

    ================================================ FILE: test/apps/rails_with_xss_plugin/public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

    We've been notified about this issue and we'll take a look at it shortly.

    ================================================ FILE: test/apps/rails_with_xss_plugin/public/javascripts/application.js ================================================ // Place your application-specific JavaScript functions and classes here // This file is automatically included by javascript_include_tag :defaults ================================================ FILE: test/apps/rails_with_xss_plugin/public/javascripts/controls.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava // Rob Wills // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This // includes drawing the autocompletion menu, observing keyboard // and mouse events, and similar. // // Specific autocompleters need to provide, at the very least, // a getUpdatedChoices function that will be invoked every time // the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by // invoking this.getToken(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized // autocompletion. Specific auto-completion logic (AJAX, etc) // belongs in getUpdatedChoices. // // Tokenized incremental autocompletion is enabled automatically // when an autocompleter is instantiated with the 'tokens' option // in the options parameter, e.g.: // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with // a token array, e.g. { tokens: [',', '\n'] } which // enables autocompletion on multiple tokens. This is most // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. if(typeof Effect == 'undefined') throw("controls.js requires including script.aculo.us' effects.js library"); var Autocompleter = { }; Autocompleter.Base = Class.create({ baseInitialize: function(element, update, options) { element = $(element); this.element = element; this.update = $(update); this.hasFocus = false; this.changed = false; this.active = false; this.index = 0; this.entryCount = 0; this.oldElementValue = this.element.value; if(this.setOptions) this.setOptions(options); else this.options = options || { }; this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; this.options.minChars = this.options.minChars || 1; this.options.onShow = this.options.onShow || function(element, update){ if(!update.style.position || update.style.position=='absolute') { update.style.position = 'absolute'; Position.clone(element, update, { setHeight: false, offsetTop: element.offsetHeight }); } Effect.Appear(update,{duration:0.15}); }; this.options.onHide = this.options.onHide || function(element, update){ new Effect.Fade(update,{duration:0.15}) }; if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); // Force carriage returns as token delimiters anyway if (!this.options.tokens.include('\n')) this.options.tokens.push('\n'); this.observer = null; this.element.setAttribute('autocomplete','off'); Element.hide(this.update); Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); if(!this.iefix && (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, ''); this.iefix = $(this.update.id+'_iefix'); } if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); }, fixIEOverlapping: function() { Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); this.iefix.style.zIndex = 1; this.update.style.zIndex = 2; Element.show(this.iefix); }, hide: function() { this.stopIndicator(); if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); if(this.iefix) Element.hide(this.iefix); }, startIndicator: function() { if(this.options.indicator) Element.show(this.options.indicator); }, stopIndicator: function() { if(this.options.indicator) Element.hide(this.options.indicator); }, onKeyPress: function(event) { if(this.active) switch(event.keyCode) { case Event.KEY_TAB: case Event.KEY_RETURN: this.selectEntry(); Event.stop(event); case Event.KEY_ESC: this.hide(); this.active = false; Event.stop(event); return; case Event.KEY_LEFT: case Event.KEY_RIGHT: return; case Event.KEY_UP: this.markPrevious(); this.render(); Event.stop(event); return; case Event.KEY_DOWN: this.markNext(); this.render(); Event.stop(event); return; } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; this.changed = true; this.hasFocus = true; if(this.observer) clearTimeout(this.observer); this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, activate: function() { this.changed = false; this.hasFocus = true; this.getUpdatedChoices(); }, onHover: function(event) { var element = Event.findElement(event, 'LI'); if(this.index != element.autocompleteIndex) { this.index = element.autocompleteIndex; this.render(); } Event.stop(event); }, onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; this.selectEntry(); this.hide(); }, onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); this.hasFocus = false; this.active = false; }, render: function() { if(this.entryCount > 0) { for (var i = 0; i < this.entryCount; i++) this.index==i ? Element.addClassName(this.getEntry(i),"selected") : Element.removeClassName(this.getEntry(i),"selected"); if(this.hasFocus) { this.show(); this.active = true; } } else { this.active = false; this.hide(); } }, markPrevious: function() { if(this.index > 0) this.index--; else this.index = this.entryCount-1; this.getEntry(this.index).scrollIntoView(true); }, markNext: function() { if(this.index < this.entryCount-1) this.index++; else this.index = 0; this.getEntry(this.index).scrollIntoView(false); }, getEntry: function(index) { return this.update.firstChild.childNodes[index]; }, getCurrentEntry: function() { return this.getEntry(this.index); }, selectEntry: function() { this.active = false; this.updateElement(this.getCurrentEntry()); }, updateElement: function(selectedElement) { if (this.options.updateElement) { this.options.updateElement(selectedElement); return; } var value = ''; if (this.options.select) { var nodes = $(selectedElement).select('.' + this.options.select) || []; if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); } else value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); var bounds = this.getTokenBounds(); if (bounds[0] != -1) { var newValue = this.element.value.substr(0, bounds[0]); var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); if (whitespace) newValue += whitespace[0]; this.element.value = newValue + value + this.element.value.substr(bounds[1]); } else { this.element.value = value; } this.oldElementValue = this.element.value; this.element.focus(); if (this.options.afterUpdateElement) this.options.afterUpdateElement(this.element, selectedElement); }, updateChoices: function(choices) { if(!this.changed && this.hasFocus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update.down()); if(this.update.firstChild && this.update.down().childNodes) { this.entryCount = this.update.down().childNodes.length; for (var i = 0; i < this.entryCount; i++) { var entry = this.getEntry(i); entry.autocompleteIndex = i; this.addObservers(entry); } } else { this.entryCount = 0; } this.stopIndicator(); this.index = 0; if(this.entryCount==1 && this.options.autoSelect) { this.selectEntry(); this.hide(); } else { this.render(); } } }, addObservers: function(element) { Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); Event.observe(element, "click", this.onClick.bindAsEventListener(this)); }, onObserverEvent: function() { this.changed = false; this.tokenBounds = null; if(this.getToken().length>=this.options.minChars) { this.getUpdatedChoices(); } else { this.active = false; this.hide(); } this.oldElementValue = this.element.value; }, getToken: function() { var bounds = this.getTokenBounds(); return this.element.value.substring(bounds[0], bounds[1]).strip(); }, getTokenBounds: function() { if (null != this.tokenBounds) return this.tokenBounds; var value = this.element.value; if (value.strip().empty()) return [-1, 0]; var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); var offset = (diff == this.oldElementValue.length ? 1 : 0); var prevTokenPos = -1, nextTokenPos = value.length; var tp; for (var index = 0, l = this.options.tokens.length; index < l; ++index) { tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); if (tp > prevTokenPos) prevTokenPos = tp; tp = value.indexOf(this.options.tokens[index], diff + offset); if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; } return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); } }); Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { var boundary = Math.min(newS.length, oldS.length); for (var index = 0; index < boundary; ++index) if (newS[index] != oldS[index]) return index; return boundary; }; Ajax.Autocompleter = Class.create(Autocompleter.Base, { initialize: function(element, update, url, options) { this.baseInitialize(element, update, options); this.options.asynchronous = true; this.options.onComplete = this.onComplete.bind(this); this.options.defaultParams = this.options.parameters || null; this.url = url; }, getUpdatedChoices: function() { this.startIndicator(); var entry = encodeURIComponent(this.options.paramName) + '=' + encodeURIComponent(this.getToken()); this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; new Ajax.Request(this.url, this.options); }, onComplete: function(request) { this.updateChoices(request.responseText); } }); // The local array autocompleter. Used when you'd prefer to // inject an array of autocompletion options into the page, rather // than sending out Ajax queries, which can be quite slow sometimes. // // The constructor takes four parameters. The first two are, as usual, // the id of the monitored textbox, and id of the autocompletion menu. // The third is the array you want to autocomplete from, and the fourth // is the options block. // // Extra local autocompletion options: // - choices - How many autocompletion choices to offer // // - partialSearch - If false, the autocompleter will match entered // text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to // search anywhere in the string, additionally set // the option fullSearch to true (default: off). // // - fullSsearch - Search anywhere in autocomplete array strings. // // - partialChars - How many characters to enter before triggering // a partial match (unlike minChars, which defines // how many characters are required to do any match // at all). Defaults to 2. // // - ignoreCase - Whether to ignore case when autocompleting. // Defaults to true. // // It's possible to pass in a custom function as the 'selector' // option, if you prefer to write your own autocompletion logic. // In that case, the other options above will not apply unless // you support them. Autocompleter.Local = Class.create(Autocompleter.Base, { initialize: function(element, update, array, options) { this.baseInitialize(element, update, options); this.options.array = array; }, getUpdatedChoices: function() { this.updateChoices(this.options.selector(this)); }, setOptions: function(options) { this.options = Object.extend({ choices: 10, partialSearch: true, partialChars: 2, ignoreCase: true, fullSearch: false, selector: function(instance) { var ret = []; // Beginning matches var partial = []; // Inside matches var entry = instance.getToken(); var count = 0; for (var i = 0; i < instance.options.array.length && ret.length < instance.options.choices ; i++) { var elem = instance.options.array[i]; var foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); while (foundPos != -1) { if (foundPos == 0 && elem.length != entry.length) { ret.push("
  • " + elem.substr(0, entry.length) + "" + elem.substr(entry.length) + "
  • "); break; } else if (entry.length >= instance.options.partialChars && instance.options.partialSearch && foundPos != -1) { if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { partial.push("
  • " + elem.substr(0, foundPos) + "" + elem.substr(foundPos, entry.length) + "" + elem.substr( foundPos + entry.length) + "
  • "); break; } } foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : elem.indexOf(entry, foundPos + 1); } } if (partial.length) ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); return "
      " + ret.join('') + "
    "; } }, options || { }); } }); // AJAX in-place editor and collection editor // Full rewrite by Christophe Porteneuve (April 2007). // Use this if you notice weird scrolling problems on some browsers, // the DOM might be a bit confused when this gets called so do this // waits 1 ms (with setTimeout) until it does the activation Field.scrollFreeActivate = function(field) { setTimeout(function() { Field.activate(field); }, 1); }; Ajax.InPlaceEditor = Class.create({ initialize: function(element, url, options) { this.url = url; this.element = element = $(element); this.prepareOptions(); this._controls = { }; arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! Object.extend(this.options, options || { }); if (!this.options.formId && this.element.id) { this.options.formId = this.element.id + '-inplaceeditor'; if ($(this.options.formId)) this.options.formId = ''; } if (this.options.externalControl) this.options.externalControl = $(this.options.externalControl); if (!this.options.externalControl) this.options.externalControlOnly = false; this._originalBackground = this.element.getStyle('background-color') || 'transparent'; this.element.title = this.options.clickToEditText; this._boundCancelHandler = this.handleFormCancellation.bind(this); this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); this._boundFailureHandler = this.handleAJAXFailure.bind(this); this._boundSubmitHandler = this.handleFormSubmission.bind(this); this._boundWrapperHandler = this.wrapUp.bind(this); this.registerListeners(); }, checkForEscapeOrReturn: function(e) { if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; if (Event.KEY_ESC == e.keyCode) this.handleFormCancellation(e); else if (Event.KEY_RETURN == e.keyCode) this.handleFormSubmission(e); }, createControl: function(mode, handler, extraClasses) { var control = this.options[mode + 'Control']; var text = this.options[mode + 'Text']; if ('button' == control) { var btn = document.createElement('input'); btn.type = 'submit'; btn.value = text; btn.className = 'editor_' + mode + '_button'; if ('cancel' == mode) btn.onclick = this._boundCancelHandler; this._form.appendChild(btn); this._controls[mode] = btn; } else if ('link' == control) { var link = document.createElement('a'); link.href = '#'; link.appendChild(document.createTextNode(text)); link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; link.className = 'editor_' + mode + '_link'; if (extraClasses) link.className += ' ' + extraClasses; this._form.appendChild(link); this._controls[mode] = link; } }, createEditField: function() { var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); var fld; if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { fld = document.createElement('input'); fld.type = 'text'; var size = this.options.size || this.options.cols || 0; if (0 < size) fld.size = size; } else { fld = document.createElement('textarea'); fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); fld.cols = this.options.cols || 40; } fld.name = this.options.paramName; fld.value = text; // No HTML breaks conversion anymore fld.className = 'editor_field'; if (this.options.submitOnBlur) fld.onblur = this._boundSubmitHandler; this._controls.editor = fld; if (this.options.loadTextURL) this.loadExternalText(); this._form.appendChild(this._controls.editor); }, createForm: function() { var ipe = this; function addText(mode, condition) { var text = ipe.options['text' + mode + 'Controls']; if (!text || condition === false) return; ipe._form.appendChild(document.createTextNode(text)); }; this._form = $(document.createElement('form')); this._form.id = this.options.formId; this._form.addClassName(this.options.formClassName); this._form.onsubmit = this._boundSubmitHandler; this.createEditField(); if ('textarea' == this._controls.editor.tagName.toLowerCase()) this._form.appendChild(document.createElement('br')); if (this.options.onFormCustomization) this.options.onFormCustomization(this, this._form); addText('Before', this.options.okControl || this.options.cancelControl); this.createControl('ok', this._boundSubmitHandler); addText('Between', this.options.okControl && this.options.cancelControl); this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); addText('After', this.options.okControl || this.options.cancelControl); }, destroy: function() { if (this._oldInnerHTML) this.element.innerHTML = this._oldInnerHTML; this.leaveEditMode(); this.unregisterListeners(); }, enterEditMode: function(e) { if (this._saving || this._editing) return; this._editing = true; this.triggerCallback('onEnterEditMode'); if (this.options.externalControl) this.options.externalControl.hide(); this.element.hide(); this.createForm(); this.element.parentNode.insertBefore(this._form, this.element); if (!this.options.loadTextURL) this.postProcessEditField(); if (e) Event.stop(e); }, enterHover: function(e) { if (this.options.hoverClassName) this.element.addClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onEnterHover'); }, getText: function() { return this.element.innerHTML.unescapeHTML(); }, handleAJAXFailure: function(transport) { this.triggerCallback('onFailure', transport); if (this._oldInnerHTML) { this.element.innerHTML = this._oldInnerHTML; this._oldInnerHTML = null; } }, handleFormCancellation: function(e) { this.wrapUp(); if (e) Event.stop(e); }, handleFormSubmission: function(e) { var form = this._form; var value = $F(this._controls.editor); this.prepareSubmission(); var params = this.options.callback(form, value) || ''; if (Object.isString(params)) params = params.toQueryParams(); params.editorId = this.element.id; if (this.options.htmlResponse) { var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Updater({ success: this.element }, this.url, options); } else { var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: params, onComplete: this._boundWrapperHandler, onFailure: this._boundFailureHandler }); new Ajax.Request(this.url, options); } if (e) Event.stop(e); }, leaveEditMode: function() { this.element.removeClassName(this.options.savingClassName); this.removeForm(); this.leaveHover(); this.element.style.backgroundColor = this._originalBackground; this.element.show(); if (this.options.externalControl) this.options.externalControl.show(); this._saving = false; this._editing = false; this._oldInnerHTML = null; this.triggerCallback('onLeaveEditMode'); }, leaveHover: function(e) { if (this.options.hoverClassName) this.element.removeClassName(this.options.hoverClassName); if (this._saving) return; this.triggerCallback('onLeaveHover'); }, loadExternalText: function() { this._form.addClassName(this.options.loadingClassName); this._controls.editor.disabled = true; var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._form.removeClassName(this.options.loadingClassName); var text = transport.responseText; if (this.options.stripLoadedTextTags) text = text.stripTags(); this._controls.editor.value = text; this._controls.editor.disabled = false; this.postProcessEditField(); }.bind(this), onFailure: this._boundFailureHandler }); new Ajax.Request(this.options.loadTextURL, options); }, postProcessEditField: function() { var fpc = this.options.fieldPostCreation; if (fpc) $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); }, prepareOptions: function() { this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); [this._extraDefaultOptions].flatten().compact().each(function(defs) { Object.extend(this.options, defs); }.bind(this)); }, prepareSubmission: function() { this._saving = true; this.removeForm(); this.leaveHover(); this.showSaving(); }, registerListeners: function() { this._listeners = { }; var listener; $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { listener = this[pair.value].bind(this); this._listeners[pair.key] = listener; if (!this.options.externalControlOnly) this.element.observe(pair.key, listener); if (this.options.externalControl) this.options.externalControl.observe(pair.key, listener); }.bind(this)); }, removeForm: function() { if (!this._form) return; this._form.remove(); this._form = null; this._controls = { }; }, showSaving: function() { this._oldInnerHTML = this.element.innerHTML; this.element.innerHTML = this.options.savingText; this.element.addClassName(this.options.savingClassName); this.element.style.backgroundColor = this._originalBackground; this.element.show(); }, triggerCallback: function(cbName, arg) { if ('function' == typeof this.options[cbName]) { this.options[cbName](this, arg); } }, unregisterListeners: function() { $H(this._listeners).each(function(pair) { if (!this.options.externalControlOnly) this.element.stopObserving(pair.key, pair.value); if (this.options.externalControl) this.options.externalControl.stopObserving(pair.key, pair.value); }.bind(this)); }, wrapUp: function(transport) { this.leaveEditMode(); // Can't use triggerCallback due to backward compatibility: requires // binding + direct element this._boundComplete(transport, this.element); } }); Object.extend(Ajax.InPlaceEditor.prototype, { dispose: Ajax.InPlaceEditor.prototype.destroy }); Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { initialize: function($super, element, url, options) { this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; $super(element, url, options); }, createEditField: function() { var list = document.createElement('select'); list.name = this.options.paramName; list.size = 1; this._controls.editor = list; this._collection = this.options.collection || []; if (this.options.loadCollectionURL) this.loadCollection(); else this.checkForExternalText(); this._form.appendChild(this._controls.editor); }, loadCollection: function() { this._form.addClassName(this.options.loadingClassName); this.showLoadingText(this.options.loadingCollectionText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { var js = transport.responseText.strip(); if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check throw('Server returned an invalid collection representation.'); this._collection = eval(js); this.checkForExternalText(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadCollectionURL, options); }, showLoadingText: function(text) { this._controls.editor.disabled = true; var tempOption = this._controls.editor.firstChild; if (!tempOption) { tempOption = document.createElement('option'); tempOption.value = ''; this._controls.editor.appendChild(tempOption); tempOption.selected = true; } tempOption.update((text || '').stripScripts().stripTags()); }, checkForExternalText: function() { this._text = this.getText(); if (this.options.loadTextURL) this.loadExternalText(); else this.buildOptionList(); }, loadExternalText: function() { this.showLoadingText(this.options.loadingText); var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); Object.extend(options, { parameters: 'editorId=' + encodeURIComponent(this.element.id), onComplete: Prototype.emptyFunction, onSuccess: function(transport) { this._text = transport.responseText.strip(); this.buildOptionList(); }.bind(this), onFailure: this.onFailure }); new Ajax.Request(this.options.loadTextURL, options); }, buildOptionList: function() { this._form.removeClassName(this.options.loadingClassName); this._collection = this._collection.map(function(entry) { return 2 === entry.length ? entry : [entry, entry].flatten(); }); var marker = ('value' in this.options) ? this.options.value : this._text; var textFound = this._collection.any(function(entry) { return entry[0] == marker; }.bind(this)); this._controls.editor.update(''); var option; this._collection.each(function(entry, index) { option = document.createElement('option'); option.value = entry[0]; option.selected = textFound ? entry[0] == marker : 0 == index; option.appendChild(document.createTextNode(entry[1])); this._controls.editor.appendChild(option); }.bind(this)); this._controls.editor.disabled = false; Field.scrollFreeActivate(this._controls.editor); } }); //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** //**** This only exists for a while, in order to let **** //**** users adapt to the new API. Read up on the new **** //**** API and convert your code to it ASAP! **** Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { if (!options) return; function fallback(name, expr) { if (name in options || expr === undefined) return; options[name] = expr; }; fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : options.cancelLink == options.cancelButton == false ? false : undefined))); fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : options.okLink == options.okButton == false ? false : undefined))); fallback('highlightColor', options.highlightcolor); fallback('highlightEndColor', options.highlightendcolor); }; Object.extend(Ajax.InPlaceEditor, { DefaultOptions: { ajaxOptions: { }, autoRows: 3, // Use when multi-line w/ rows == 1 cancelControl: 'link', // 'link'|'button'|false cancelText: 'cancel', clickToEditText: 'Click to edit', externalControl: null, // id|elt externalControlOnly: false, fieldPostCreation: 'activate', // 'activate'|'focus'|false formClassName: 'inplaceeditor-form', formId: null, // id|elt highlightColor: '#ffff99', highlightEndColor: '#ffffff', hoverClassName: '', htmlResponse: true, loadingClassName: 'inplaceeditor-loading', loadingText: 'Loading...', okControl: 'button', // 'link'|'button'|false okText: 'ok', paramName: 'value', rows: 1, // If 1 and multi-line, uses autoRows savingClassName: 'inplaceeditor-saving', savingText: 'Saving...', size: 0, stripLoadedTextTags: false, submitOnBlur: false, textAfterControls: '', textBeforeControls: '', textBetweenControls: '' }, DefaultCallbacks: { callback: function(form) { return Form.serialize(form); }, onComplete: function(transport, element) { // For backward compatibility, this one is bound to the IPE, and passes // the element directly. It was too often customized, so we don't break it. new Effect.Highlight(element, { startcolor: this.options.highlightColor, keepBackgroundImage: true }); }, onEnterEditMode: null, onEnterHover: function(ipe) { ipe.element.style.backgroundColor = ipe.options.highlightColor; if (ipe._effect) ipe._effect.cancel(); }, onFailure: function(transport, ipe) { alert('Error communication with the server: ' + transport.responseText.stripTags()); }, onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. onLeaveEditMode: null, onLeaveHover: function(ipe) { ipe._effect = new Effect.Highlight(ipe.element, { startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, restorecolor: ipe._originalBackground, keepBackgroundImage: true }); } }, Listeners: { click: 'enterEditMode', keydown: 'checkForEscapeOrReturn', mouseover: 'enterHover', mouseout: 'leaveHover' } }); Ajax.InPlaceCollectionEditor.DefaultOptions = { loadingCollectionText: 'Loading options...' }; // Delayed observer, like Form.Element.Observer, // but waits for delay after last key input // Ideal for live-search fields Form.Element.DelayedObserver = Class.create({ initialize: function(element, delay, callback) { this.delay = delay || 0.5; this.element = $(element); this.callback = callback; this.timer = null; this.lastValue = $F(this.element); Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); }, delayedListener: function(event) { if(this.lastValue == $F(this.element)) return; if(this.timer) clearTimeout(this.timer); this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); this.lastValue = $F(this.element); }, onTimerEvent: function() { this.timer = null; this.callback(this.element, $F(this.element)); } }); ================================================ FILE: test/apps/rails_with_xss_plugin/public/javascripts/dragdrop.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ if(Object.isUndefined(Effect)) throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { drops: [], remove: function(element) { this.drops = this.drops.reject(function(d) { return d.element==$(element) }); }, add: function(element) { element = $(element); var options = Object.extend({ greedy: true, hoverclass: null, tree: false }, arguments[1] || { }); // cache containers if(options.containment) { options._containers = []; var containment = options.containment; if(Object.isArray(containment)) { containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); } } if(options.accept) options.accept = [options.accept].flatten(); Element.makePositioned(element); // fix IE options.element = element; this.drops.push(options); }, findDeepestChild: function(drops) { deepest = drops[0]; for (i = 1; i < drops.length; ++i) if (Element.isParent(drops[i].element, deepest.element)) deepest = drops[i]; return deepest; }, isContained: function(element, drop) { var containmentNode; if(drop.tree) { containmentNode = element.treeNode; } else { containmentNode = element.parentNode; } return drop._containers.detect(function(c) { return containmentNode == c }); }, isAffected: function(point, element, drop) { return ( (drop.element!=element) && ((!drop._containers) || this.isContained(element, drop)) && ((!drop.accept) || (Element.classNames(element).detect( function(v) { return drop.accept.include(v) } ) )) && Position.within(drop.element, point[0], point[1]) ); }, deactivate: function(drop) { if(drop.hoverclass) Element.removeClassName(drop.element, drop.hoverclass); this.last_active = null; }, activate: function(drop) { if(drop.hoverclass) Element.addClassName(drop.element, drop.hoverclass); this.last_active = drop; }, show: function(point, element) { if(!this.drops.length) return; var drop, affected = []; this.drops.each( function(drop) { if(Droppables.isAffected(point, element, drop)) affected.push(drop); }); if(affected.length>0) drop = Droppables.findDeepestChild(affected); if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); if (drop) { Position.within(drop.element, point[0], point[1]); if(drop.onHover) drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); if (drop != this.last_active) Droppables.activate(drop); } }, fire: function(event, element) { if(!this.last_active) return; Position.prepare(); if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) if (this.last_active.onDrop) { this.last_active.onDrop(element, this.last_active.element, event); return true; } }, reset: function() { if(this.last_active) this.deactivate(this.last_active); } }; var Draggables = { drags: [], observers: [], register: function(draggable) { if(this.drags.length == 0) { this.eventMouseUp = this.endDrag.bindAsEventListener(this); this.eventMouseMove = this.updateDrag.bindAsEventListener(this); this.eventKeypress = this.keyPress.bindAsEventListener(this); Event.observe(document, "mouseup", this.eventMouseUp); Event.observe(document, "mousemove", this.eventMouseMove); Event.observe(document, "keypress", this.eventKeypress); } this.drags.push(draggable); }, unregister: function(draggable) { this.drags = this.drags.reject(function(d) { return d==draggable }); if(this.drags.length == 0) { Event.stopObserving(document, "mouseup", this.eventMouseUp); Event.stopObserving(document, "mousemove", this.eventMouseMove); Event.stopObserving(document, "keypress", this.eventKeypress); } }, activate: function(draggable) { if(draggable.options.delay) { this._timeout = setTimeout(function() { Draggables._timeout = null; window.focus(); Draggables.activeDraggable = draggable; }.bind(this), draggable.options.delay); } else { window.focus(); // allows keypress events if window isn't currently focused, fails for Safari this.activeDraggable = draggable; } }, deactivate: function() { this.activeDraggable = null; }, updateDrag: function(event) { if(!this.activeDraggable) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; // Mozilla-based browsers fire successive mousemove events with // the same coordinates, prevent needless redrawing (moz bug?) if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; this._lastPointer = pointer; this.activeDraggable.updateDrag(event, pointer); }, endDrag: function(event) { if(this._timeout) { clearTimeout(this._timeout); this._timeout = null; } if(!this.activeDraggable) return; this._lastPointer = null; this.activeDraggable.endDrag(event); this.activeDraggable = null; }, keyPress: function(event) { if(this.activeDraggable) this.activeDraggable.keyPress(event); }, addObserver: function(observer) { this.observers.push(observer); this._cacheObserverCallbacks(); }, removeObserver: function(element) { // element instead of observer fixes mem leaks this.observers = this.observers.reject( function(o) { return o.element==element }); this._cacheObserverCallbacks(); }, notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' if(this[eventName+'Count'] > 0) this.observers.each( function(o) { if(o[eventName]) o[eventName](eventName, draggable, event); }); if(draggable.options[eventName]) draggable.options[eventName](draggable, event); }, _cacheObserverCallbacks: function() { ['onStart','onEnd','onDrag'].each( function(eventName) { Draggables[eventName+'Count'] = Draggables.observers.select( function(o) { return o[eventName]; } ).length; }); } }; /*--------------------------------------------------------------------------*/ var Draggable = Class.create({ initialize: function(element) { var defaults = { handle: false, reverteffect: function(element, top_offset, left_offset) { var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, queue: {scope:'_draggable', position:'end'} }); }, endeffect: function(element) { var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, queue: {scope:'_draggable', position:'end'}, afterFinish: function(){ Draggable._dragging[element] = false } }); }, zindex: 1000, revert: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } delay: 0 }; if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) Object.extend(defaults, { starteffect: function(element) { element._opacity = Element.getOpacity(element); Draggable._dragging[element] = true; new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); } }); var options = Object.extend(defaults, arguments[1] || { }); this.element = $(element); if(options.handle && Object.isString(options.handle)) this.handle = this.element.down('.'+options.handle, 0); if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = this.element; if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { options.scroll = $(options.scroll); this._isScrollChild = Element.childOf(this.element, options.scroll); } Element.makePositioned(this.element); // fix IE this.options = options; this.dragging = false; this.eventMouseDown = this.initDrag.bindAsEventListener(this); Event.observe(this.handle, "mousedown", this.eventMouseDown); Draggables.register(this); }, destroy: function() { Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); Draggables.unregister(this); }, currentDelta: function() { return([ parseInt(Element.getStyle(this.element,'left') || '0'), parseInt(Element.getStyle(this.element,'top') || '0')]); }, initDrag: function(event) { if(!Object.isUndefined(Draggable._dragging[this.element]) && Draggable._dragging[this.element]) return; if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); if((tag_name = src.tagName.toUpperCase()) && ( tag_name=='INPUT' || tag_name=='SELECT' || tag_name=='OPTION' || tag_name=='BUTTON' || tag_name=='TEXTAREA')) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pos = Position.cumulativeOffset(this.element); this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); Draggables.activate(this); Event.stop(event); } }, startDrag: function(event) { this.dragging = true; if(!this.delta) this.delta = this.currentDelta(); if(this.options.zindex) { this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); this.element.style.zIndex = this.options.zindex; } if(this.options.ghosting) { this._clone = this.element.cloneNode(true); this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); if (!this._originallyAbsolute) Position.absolutize(this.element); this.element.parentNode.insertBefore(this._clone, this.element); } if(this.options.scroll) { if (this.options.scroll == window) { var where = this._getWindowScroll(this.options.scroll); this.originalScrollLeft = where.left; this.originalScrollTop = where.top; } else { this.originalScrollLeft = this.options.scroll.scrollLeft; this.originalScrollTop = this.options.scroll.scrollTop; } } Draggables.notify('onStart', this, event); if(this.options.starteffect) this.options.starteffect(this.element); }, updateDrag: function(event, pointer) { if(!this.dragging) this.startDrag(event); if(!this.options.quiet){ Position.prepare(); Droppables.show(pointer, this.element); } Draggables.notify('onDrag', this, event); this.draw(pointer); if(this.options.change) this.options.change(this); if(this.options.scroll) { this.stopScrolling(); var p; if (this.options.scroll == window) { with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } } else { p = Position.page(this.options.scroll); p[0] += this.options.scroll.scrollLeft + Position.deltaX; p[1] += this.options.scroll.scrollTop + Position.deltaY; p.push(p[0]+this.options.scroll.offsetWidth); p.push(p[1]+this.options.scroll.offsetHeight); } var speed = [0,0]; if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); this.startScrolling(speed); } // fix AppleWebKit rendering if(Prototype.Browser.WebKit) window.scrollBy(0,0); Event.stop(event); }, finishDrag: function(event, success) { this.dragging = false; if(this.options.quiet){ Position.prepare(); var pointer = [Event.pointerX(event), Event.pointerY(event)]; Droppables.show(pointer, this.element); } if(this.options.ghosting) { if (!this._originallyAbsolute) Position.relativize(this.element); delete this._originallyAbsolute; Element.remove(this._clone); this._clone = null; } var dropped = false; if(success) { dropped = Droppables.fire(event, this.element); if (!dropped) dropped = false; } if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; if(revert && Object.isFunction(revert)) revert = revert(this.element); var d = this.currentDelta(); if(revert && this.options.reverteffect) { if (dropped == 0 || revert != 'failure') this.options.reverteffect(this.element, d[1]-this.delta[1], d[0]-this.delta[0]); } else { this.delta = d; } if(this.options.zindex) this.element.style.zIndex = this.originalZ; if(this.options.endeffect) this.options.endeffect(this.element); Draggables.deactivate(this); Droppables.reset(); }, keyPress: function(event) { if(event.keyCode!=Event.KEY_ESC) return; this.finishDrag(event, false); Event.stop(event); }, endDrag: function(event) { if(!this.dragging) return; this.stopScrolling(); this.finishDrag(event, true); Event.stop(event); }, draw: function(point) { var pos = Position.cumulativeOffset(this.element); if(this.options.ghosting) { var r = Position.realOffset(this.element); pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; } var d = this.currentDelta(); pos[0] -= d[0]; pos[1] -= d[1]; if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; } var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this)); if(this.options.snap) { if(Object.isFunction(this.options.snap)) { p = this.options.snap(p[0],p[1],this); } else { if(Object.isArray(this.options.snap)) { p = p.map( function(v, i) { return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); } else { p = p.map( function(v) { return (v/this.options.snap).round()*this.options.snap }.bind(this)); } }} var style = this.element.style; if((!this.options.constraint) || (this.options.constraint=='horizontal')) style.left = p[0] + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) style.top = p[1] + "px"; if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering }, stopScrolling: function() { if(this.scrollInterval) { clearInterval(this.scrollInterval); this.scrollInterval = null; Draggables._lastScrollPointer = null; } }, startScrolling: function(speed) { if(!(speed[0] || speed[1])) return; this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; this.lastScrolled = new Date(); this.scrollInterval = setInterval(this.scroll.bind(this), 10); }, scroll: function() { var current = new Date(); var delta = current - this.lastScrolled; this.lastScrolled = current; if(this.options.scroll == window) { with (this._getWindowScroll(this.options.scroll)) { if (this.scrollSpeed[0] || this.scrollSpeed[1]) { var d = delta / 1000; this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); } } } else { this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; } Position.prepare(); Droppables.show(Draggables._lastPointer, this.element); Draggables.notify('onDrag', this); if (this._isScrollChild) { Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; if (Draggables._lastScrollPointer[0] < 0) Draggables._lastScrollPointer[0] = 0; if (Draggables._lastScrollPointer[1] < 0) Draggables._lastScrollPointer[1] = 0; this.draw(Draggables._lastScrollPointer); } if(this.options.change) this.options.change(this); }, _getWindowScroll: function(w) { var T, L, W, H; with (w.document) { if (w.document.documentElement && documentElement.scrollTop) { T = documentElement.scrollTop; L = documentElement.scrollLeft; } else if (w.document.body) { T = body.scrollTop; L = body.scrollLeft; } if (w.innerWidth) { W = w.innerWidth; H = w.innerHeight; } else if (w.document.documentElement && documentElement.clientWidth) { W = documentElement.clientWidth; H = documentElement.clientHeight; } else { W = body.offsetWidth; H = body.offsetHeight; } } return { top: T, left: L, width: W, height: H }; } }); Draggable._dragging = { }; /*--------------------------------------------------------------------------*/ var SortableObserver = Class.create({ initialize: function(element, observer) { this.element = $(element); this.observer = observer; this.lastValue = Sortable.serialize(this.element); }, onStart: function() { this.lastValue = Sortable.serialize(this.element); }, onEnd: function() { Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } }); var Sortable = { SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, sortables: { }, _findRootElement: function(element) { while (element.tagName.toUpperCase() != "BODY") { if(element.id && Sortable.sortables[element.id]) return element; element = element.parentNode; } }, options: function(element) { element = Sortable._findRootElement($(element)); if(!element) return; return Sortable.sortables[element.id]; }, destroy: function(element){ element = $(element); var s = Sortable.sortables[element.id]; if(s) { Draggables.removeObserver(s.element); s.droppables.each(function(d){ Droppables.remove(d) }); s.draggables.invoke('destroy'); delete Sortable.sortables[s.element.id]; } }, create: function(element) { element = $(element); var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' dropOnEmpty: false, tree: false, treeTag: 'ul', overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, delay: 0, hoverclass: null, ghosting: false, quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, format: this.SERIALIZE_RULE, // these take arrays of elements or ids and can be // used for better initialization performance elements: false, handles: false, onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction }, arguments[1] || { }); // clear any old sortable with same element this.destroy(element); // build options for the draggables var options_for_draggable = { revert: true, quiet: options.quiet, scroll: options.scroll, scrollSpeed: options.scrollSpeed, scrollSensitivity: options.scrollSensitivity, delay: options.delay, ghosting: options.ghosting, constraint: options.constraint, handle: options.handle }; if(options.starteffect) options_for_draggable.starteffect = options.starteffect; if(options.reverteffect) options_for_draggable.reverteffect = options.reverteffect; else if(options.ghosting) options_for_draggable.reverteffect = function(element) { element.style.top = 0; element.style.left = 0; }; if(options.endeffect) options_for_draggable.endeffect = options.endeffect; if(options.zindex) options_for_draggable.zindex = options.zindex; // build options for the droppables var options_for_droppable = { overlap: options.overlap, containment: options.containment, tree: options.tree, hoverclass: options.hoverclass, onHover: Sortable.onHover }; var options_for_tree = { onHover: Sortable.onEmptyHover, overlap: options.overlap, containment: options.containment, hoverclass: options.hoverclass }; // fix for gecko engine Element.cleanWhitespace(element); options.draggables = []; options.droppables = []; // drop on empty handling if(options.dropOnEmpty || options.tree) { Droppables.add(element, options_for_tree); options.droppables.push(element); } (options.elements || this.findElements(element, options) || []).each( function(e,i) { var handle = options.handles ? $(options.handles[i]) : (options.handle ? $(e).select('.' + options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); if(options.tree) e.treeNode = element; options.droppables.push(e); }); if(options.tree) { (Sortable.findTreeElements(element, options) || []).each( function(e) { Droppables.add(e, options_for_tree); e.treeNode = element; options.droppables.push(e); }); } // keep reference this.sortables[element.id] = options; // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); }, // return all suitable-for-sortable elements in a guaranteed order findElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.tag); }, findTreeElements: function(element, options) { return Element.findChildren( element, options.only, options.tree ? true : false, options.treeTag); }, onHover: function(element, dropon, overlap) { if(Element.isParent(dropon, element)) return; if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { return; } else if(overlap>0.5) { Sortable.mark(dropon, 'before'); if(dropon.previousSibling != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, dropon); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } else { Sortable.mark(dropon, 'after'); var nextElement = dropon.nextSibling || null; if(nextElement != element) { var oldParentNode = element.parentNode; element.style.visibility = "hidden"; // fix gecko rendering dropon.parentNode.insertBefore(element, nextElement); if(dropon.parentNode!=oldParentNode) Sortable.options(oldParentNode).onChange(element); Sortable.options(dropon.parentNode).onChange(element); } } }, onEmptyHover: function(element, dropon, overlap) { var oldParentNode = element.parentNode; var droponOptions = Sortable.options(dropon); if(!Element.isParent(dropon, element)) { var index; var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); var child = null; if(children) { var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); for (index = 0; index < children.length; index += 1) { if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { offset -= Element.offsetSize (children[index], droponOptions.overlap); } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { child = index + 1 < children.length ? children[index + 1] : null; break; } else { child = children[index]; break; } } } dropon.insertBefore(element, child); Sortable.options(oldParentNode).onChange(element); droponOptions.onChange(element); } }, unmark: function() { if(Sortable._marker) Sortable._marker.hide(); }, mark: function(dropon, position) { // mark on ghosting only var sortable = Sortable.options(dropon.parentNode); if(sortable && !sortable.ghosting) return; if(!Sortable._marker) { Sortable._marker = ($('dropmarker') || Element.extend(document.createElement('DIV'))). hide().addClassName('dropmarker').setStyle({position:'absolute'}); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); } var offsets = Position.cumulativeOffset(dropon); Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); if(position=='after') if(sortable.overlap == 'horizontal') Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); else Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); Sortable._marker.show(); }, _tree: function(element, options, parent) { var children = Sortable.findElements(element, options) || []; for (var i = 0; i < children.length; ++i) { var match = children[i].id.match(options.format); if (!match) continue; var child = { id: encodeURIComponent(match ? match[1] : null), element: element, parent: parent, children: [], position: parent.children.length, container: $(children[i]).down(options.treeTag) }; /* Get the element containing the children and recurse over it */ if (child.container) this._tree(child.container, options, child); parent.children.push (child); } return parent; }, tree: function(element) { element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ tag: sortableOptions.tag, treeTag: sortableOptions.treeTag, only: sortableOptions.only, name: element.id, format: sortableOptions.format }, arguments[1] || { }); var root = { id: null, parent: null, children: [], container: element, position: 0 }; return Sortable._tree(element, options, root); }, /* Construct a [i] index for a particular node */ _constructIndex: function(node) { var index = ''; do { if (node.id) index = '[' + node.position + ']' + index; } while ((node = node.parent) != null); return index; }, sequence: function(element) { element = $(element); var options = Object.extend(this.options(element), arguments[1] || { }); return $(this.findElements(element, options) || []).map( function(item) { return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; }); }, setSequence: function(element, new_sequence) { element = $(element); var options = Object.extend(this.options(element), arguments[2] || { }); var nodeMap = { }; this.findElements(element, options).each( function(n) { if (n.id.match(options.format)) nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; n.parentNode.removeChild(n); }); new_sequence.each(function(ident) { var n = nodeMap[ident]; if (n) { n[1].appendChild(n[0]); delete nodeMap[ident]; } }); }, serialize: function(element) { element = $(element); var options = Object.extend(Sortable.options(element), arguments[1] || { }); var name = encodeURIComponent( (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); if (options.tree) { return Sortable.tree(element, arguments[1]).children.map( function (item) { return [name + Sortable._constructIndex(item) + "[id]=" + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); }).flatten().join('&'); } else { return Sortable.sequence(element, arguments[1]).map( function(item) { return name + "[]=" + encodeURIComponent(item); }).join('&'); } } }; // Returns true if child is contained within element Element.isParent = function(child, element) { if (!child.parentNode || child == element) return false; if (child.parentNode == element) return true; return Element.isParent(child.parentNode, element); }; Element.findChildren = function(element, only, recursive, tagName) { if(!element.hasChildNodes()) return null; tagName = tagName.toUpperCase(); if(only) only = [only].flatten(); var elements = []; $A(element.childNodes).each( function(e) { if(e.tagName && e.tagName.toUpperCase()==tagName && (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) elements.push(e); if(recursive) { var grandchildren = Element.findChildren(e, only, recursive, tagName); if(grandchildren) elements.push(grandchildren); } }); return (elements.length>0 ? elements.flatten() : []); }; Element.offsetSize = function (element, type) { return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; }; ================================================ FILE: test/apps/rails_with_xss_plugin/public/javascripts/effects.js ================================================ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) // Martin Bialasinki // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ // converts rgb() and #xxx to #xxxxxx format, // returns self (or first argument) if not convertable String.prototype.parseColor = function() { var color = '#'; if (this.slice(0,4) == 'rgb(') { var cols = this.slice(4,this.length-1).split(','); var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); } else { if (this.slice(0,1) == '#') { if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); if (this.length==7) color = this.toLowerCase(); } } return (color.length==7 ? color : (arguments[0] || this)); }; /*--------------------------------------------------------------------------*/ Element.collectTextNodes = function(element) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); }).flatten().join(''); }; Element.collectTextNodesIgnoreClass = function(element, className) { return $A($(element).childNodes).collect( function(node) { return (node.nodeType==3 ? node.nodeValue : ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? Element.collectTextNodesIgnoreClass(node, className) : '')); }).flatten().join(''); }; Element.setContentZoom = function(element, percent) { element = $(element); element.setStyle({fontSize: (percent/100) + 'em'}); if (Prototype.Browser.WebKit) window.scrollBy(0,0); return element; }; Element.getInlineOpacity = function(element){ return $(element).style.opacity || ''; }; Element.forceRerendering = function(element) { try { element = $(element); var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch(e) { } }; /*--------------------------------------------------------------------------*/ var Effect = { _elementDoesNotExistError: { name: 'ElementDoesNotExistError', message: 'The specified DOM element does not exist, but is required for this effect to operate' }, Transitions: { linear: Prototype.K, sinoidal: function(pos) { return (-Math.cos(pos*Math.PI)/2) + .5; }, reverse: function(pos) { return 1-pos; }, flicker: function(pos) { var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; return pos > 1 ? 1 : pos; }, wobble: function(pos) { return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; }, pulse: function(pos, pulses) { return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; }, spring: function(pos) { return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); }, none: function(pos) { return 0; }, full: function(pos) { return 1; } }, DefaultOptions: { duration: 1.0, // seconds fps: 100, // 100= assume 66fps max. sync: false, // true for combining from: 0.0, to: 1.0, delay: 0.0, queue: 'parallel' }, tagifyText: function(element) { var tagifyStyle = 'position:relative'; if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; element = $(element); $A(element.childNodes).each( function(child) { if (child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( new Element('span', {style: tagifyStyle}).update( character == ' ' ? String.fromCharCode(160) : character), child); }); Element.remove(child); } }); }, multiple: function(element, effect) { var elements; if (((typeof element == 'object') || Object.isFunction(element)) && (element.length)) elements = element; else elements = $(element).childNodes; var options = Object.extend({ speed: 0.1, delay: 0.0 }, arguments[2] || { }); var masterDelay = options.delay; $A(elements).each( function(element, index) { new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); }); }, PAIRS: { 'slide': ['SlideDown','SlideUp'], 'blind': ['BlindDown','BlindUp'], 'appear': ['Appear','Fade'] }, toggle: function(element, effect) { element = $(element); effect = (effect || 'appear').toLowerCase(); var options = Object.extend({ queue: { position:'end', scope:(element.id || 'global'), limit: 1 } }, arguments[2] || { }); Effect[element.visible() ? Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); } }; Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; /* ------------- core effects ------------- */ Effect.ScopedQueue = Class.create(Enumerable, { initialize: function() { this.effects = []; this.interval = null; }, _each: function(iterator) { this.effects._each(iterator); }, add: function(effect) { var timestamp = new Date().getTime(); var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position; switch(position) { case 'front': // move unstarted effects after this effect this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { e.startOn += effect.finishOn; e.finishOn += effect.finishOn; }); break; case 'with-last': timestamp = this.effects.pluck('startOn').max() || timestamp; break; case 'end': // start effect after last queued effect has finished timestamp = this.effects.pluck('finishOn').max() || timestamp; break; } effect.startOn += timestamp; effect.finishOn += timestamp; if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) this.effects.push(effect); if (!this.interval) this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); if (this.effects.length == 0) { clearInterval(this.interval); this.interval = null; } }, loop: function() { var timePos = new Date().getTime(); for(var i=0, len=this.effects.length;i= this.startOn) { if (timePos >= this.finishOn) { this.render(1.0); this.cancel(); this.event('beforeFinish'); if (this.finish) this.finish(); this.event('afterFinish'); return; } var pos = (timePos - this.startOn) / this.totalTime, frame = (pos * this.totalFrames).round(); if (frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, cancel: function() { if (!this.options.sync) Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); if (this.options[eventName]) this.options[eventName](this); }, inspect: function() { var data = $H(); for(property in this) if (!Object.isFunction(this[property])) data.set(property, this[property]); return '#'; } }); Effect.Parallel = Class.create(Effect.Base, { initialize: function(effects) { this.effects = effects || []; this.start(arguments[1]); }, update: function(position) { this.effects.invoke('render', position); }, finish: function(position) { this.effects.each( function(effect) { effect.render(1.0); effect.cancel(); effect.event('beforeFinish'); if (effect.finish) effect.finish(position); effect.event('afterFinish'); }); } }); Effect.Tween = Class.create(Effect.Base, { initialize: function(object, from, to) { object = Object.isString(object) ? $(object) : object; var args = $A(arguments), method = args.last(), options = args.length == 5 ? args[3] : null; this.method = Object.isFunction(method) ? method.bind(object) : Object.isFunction(object[method]) ? object[method].bind(object) : function(value) { object[method] = value }; this.start(Object.extend({ from: from, to: to }, options || { })); }, update: function(position) { this.method(position); } }); Effect.Event = Class.create(Effect.Base, { initialize: function() { this.start(Object.extend({ duration: 0 }, arguments[0] || { })); }, update: Prototype.emptyFunction }); Effect.Opacity = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); var options = Object.extend({ from: this.element.getOpacity() || 0.0, to: 1.0 }, arguments[1] || { }); this.start(options); }, update: function(position) { this.element.setOpacity(position); } }); Effect.Move = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ x: 0, y: 0, mode: 'relative' }, arguments[1] || { }); this.start(options); }, setup: function() { this.element.makePositioned(); this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); this.originalTop = parseFloat(this.element.getStyle('top') || '0'); if (this.options.mode == 'absolute') { this.options.x = this.options.x - this.originalLeft; this.options.y = this.options.y - this.originalTop; } }, update: function(position) { this.element.setStyle({ left: (this.options.x * position + this.originalLeft).round() + 'px', top: (this.options.y * position + this.originalTop).round() + 'px' }); } }); // for backwards compatibility Effect.MoveBy = function(element, toTop, toLeft) { return new Effect.Move(element, Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); }; Effect.Scale = Class.create(Effect.Base, { initialize: function(element, percent) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, scaleMode: 'box', // 'box' or 'contents' or { } with provided values scaleFrom: 100.0, scaleTo: percent }, arguments[2] || { }); this.start(options); }, setup: function() { this.restoreAfterFinish = this.options.restoreAfterFinish || false; this.elementPositioning = this.element.getStyle('position'); this.originalStyle = { }; ['top','left','width','height','fontSize'].each( function(k) { this.originalStyle[k] = this.element.style[k]; }.bind(this)); this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; var fontSize = this.element.getStyle('font-size') || '100%'; ['em','px','%','pt'].each( function(fontSizeType) { if (fontSize.indexOf(fontSizeType)>0) { this.fontSize = parseFloat(fontSize); this.fontSizeType = fontSizeType; } }.bind(this)); this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; this.dims = null; if (this.options.scaleMode=='box') this.dims = [this.element.offsetHeight, this.element.offsetWidth]; if (/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; if (!this.dims) this.dims = [this.options.scaleMode.originalHeight, this.options.scaleMode.originalWidth]; }, update: function(position) { var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); if (this.options.scaleContent && this.fontSize) this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { var d = { }; if (this.options.scaleX) d.width = width.round() + 'px'; if (this.options.scaleY) d.height = height.round() + 'px'; if (this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; if (this.elementPositioning == 'absolute') { if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; } else { if (this.options.scaleY) d.top = -topd + 'px'; if (this.options.scaleX) d.left = -leftd + 'px'; } } this.element.setStyle(d); } }); Effect.Highlight = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow if (this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect this.oldStyle = { }; if (!this.options.keepBackgroundImage) { this.oldStyle.backgroundImage = this.element.getStyle('background-image'); this.element.setStyle({backgroundImage: 'none'}); } if (!this.options.endcolor) this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); if (!this.options.restorecolor) this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); }, update: function(position) { this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); }, finish: function() { this.element.setStyle(Object.extend(this.oldStyle, { backgroundColor: this.options.restorecolor })); } }); Effect.ScrollTo = function(element) { var options = arguments[1] || { }, scrollOffsets = document.viewport.getScrollOffsets(), elementOffsets = $(element).cumulativeOffset(); if (options.offset) elementOffsets[1] += options.offset; return new Effect.Tween(null, scrollOffsets.top, elementOffsets[1], options, function(p){ scrollTo(scrollOffsets.left, p.round()); } ); }; /* ------------- combination effects ------------- */ Effect.Fade = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ from: element.getOpacity() || 1.0, to: 0.0, afterFinishInternal: function(effect) { if (effect.options.to!=0) return; effect.element.hide().setStyle({opacity: oldOpacity}); } }, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Appear = function(element) { element = $(element); var options = Object.extend({ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), to: 1.0, // force Safari to render floated elements properly afterFinishInternal: function(effect) { effect.element.forceRerendering(); }, beforeSetup: function(effect) { effect.element.setOpacity(effect.options.from).show(); }}, arguments[1] || { }); return new Effect.Opacity(element,options); }; Effect.Puff = function(element) { element = $(element); var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position'), top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; return new Effect.Parallel( [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], Object.extend({ duration: 1.0, beforeSetupInternal: function(effect) { Position.absolutize(effect.effects[0].element); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().setStyle(oldStyle); } }, arguments[1] || { }) ); }; Effect.BlindUp = function(element) { element = $(element); element.makeClipping(); return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, restoreAfterFinish: true, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }, arguments[1] || { }) ); }; Effect.BlindDown = function(element) { element = $(element); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: 0, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.element.undoClipping(); } }, arguments[1] || { })); }; Effect.SwitchOff = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); return new Effect.Appear(element, Object.extend({ duration: 0.4, from: 0, transition: Effect.Transitions.flicker, afterFinishInternal: function(effect) { new Effect.Scale(effect.element, 1, { duration: 0.3, scaleFromCenter: true, scaleX: false, scaleContent: false, restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); } }); } }, arguments[1] || { })); }; Effect.DropOut = function(element) { element = $(element); var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left'), opacity: element.getInlineOpacity() }; return new Effect.Parallel( [ new Effect.Move(element, {x: 0, y: 100, sync: true }), new Effect.Opacity(element, { sync: true, to: 0.0 }) ], Object.extend( { duration: 0.5, beforeSetup: function(effect) { effect.effects[0].element.makePositioned(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); } }, arguments[1] || { })); }; Effect.Shake = function(element) { element = $(element); var options = Object.extend({ distance: 20, duration: 0.5 }, arguments[1] || {}); var distance = parseFloat(options.distance); var split = parseFloat(options.duration) / 10.0; var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left') }; return new Effect.Move(element, { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { effect.element.undoPositioned().setStyle(oldStyle); }}); }}); }}); }}); }}); }}); }; Effect.SlideDown = function(element) { element = $(element).cleanWhitespace(); // SlideDown need to have the content of the element wrapped in a container element with fixed height! var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, scaleFrom: window.opera ? 0 : 1, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; Effect.SlideUp = function(element) { element = $(element).cleanWhitespace(); var oldInnerBottom = element.down().getStyle('bottom'); var elementDimensions = element.getDimensions(); return new Effect.Scale(element, window.opera ? 0 : 1, Object.extend({ scaleContent: false, scaleX: false, scaleMode: 'box', scaleFrom: 100, scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().show(); }, afterUpdateInternal: function(effect) { effect.element.down().setStyle({bottom: (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || { }) ); }; // Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { return new Effect.Scale(element, window.opera ? 1 : 0, { restoreAfterFinish: true, beforeSetup: function(effect) { effect.element.makeClipping(); }, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }); }; Effect.Grow = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.full }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var initialMoveX, initialMoveY; var moveX, moveY; switch (options.direction) { case 'top-left': initialMoveX = initialMoveY = moveX = moveY = 0; break; case 'top-right': initialMoveX = dims.width; initialMoveY = moveY = 0; moveX = -dims.width; break; case 'bottom-left': initialMoveX = moveX = 0; initialMoveY = dims.height; moveY = -dims.height; break; case 'bottom-right': initialMoveX = dims.width; initialMoveY = dims.height; moveX = -dims.width; moveY = -dims.height; break; case 'center': initialMoveX = dims.width / 2; initialMoveY = dims.height / 2; moveX = -dims.width / 2; moveY = -dims.height / 2; break; } return new Effect.Move(element, { x: initialMoveX, y: initialMoveY, duration: 0.01, beforeSetup: function(effect) { effect.element.hide().makeClipping().makePositioned(); }, afterFinishInternal: function(effect) { new Effect.Parallel( [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), new Effect.Scale(effect.element, 100, { scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) ], Object.extend({ beforeSetup: function(effect) { effect.effects[0].element.setStyle({height: '0px'}).show(); }, afterFinishInternal: function(effect) { effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); } }); }; Effect.Shrink = function(element) { element = $(element); var options = Object.extend({ direction: 'center', moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.none }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, height: element.style.height, width: element.style.width, opacity: element.getInlineOpacity() }; var dims = element.getDimensions(); var moveX, moveY; switch (options.direction) { case 'top-left': moveX = moveY = 0; break; case 'top-right': moveX = dims.width; moveY = 0; break; case 'bottom-left': moveX = 0; moveY = dims.height; break; case 'bottom-right': moveX = dims.width; moveY = dims.height; break; case 'center': moveX = dims.width / 2; moveY = dims.height / 2; break; } return new Effect.Parallel( [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) ], Object.extend({ beforeStartInternal: function(effect) { effect.effects[0].element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); }; Effect.Pulsate = function(element) { element = $(element); var options = arguments[1] || { }, oldOpacity = element.getInlineOpacity(), transition = options.transition || Effect.Transitions.linear, reverser = function(pos){ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); }; return new Effect.Opacity(element, Object.extend(Object.extend({ duration: 2.0, from: 0, afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); }; Effect.Fold = function(element) { element = $(element); var oldStyle = { top: element.style.top, left: element.style.left, width: element.style.width, height: element.style.height }; element.makeClipping(); return new Effect.Scale(element, 5, Object.extend({ scaleContent: false, scaleX: false, afterFinishInternal: function(effect) { new Effect.Scale(element, 1, { scaleContent: false, scaleY: false, afterFinishInternal: function(effect) { effect.element.hide().undoClipping().setStyle(oldStyle); } }); }}, arguments[1] || { })); }; Effect.Morph = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ style: { } }, arguments[1] || { }); if (!Object.isString(options.style)) this.style = $H(options.style); else { if (options.style.include(':')) this.style = options.style.parseStyle(); else { this.element.addClassName(options.style); this.style = $H(this.element.getStyles()); this.element.removeClassName(options.style); var css = this.element.getStyles(); this.style = this.style.reject(function(style) { return style.value == css[style.key]; }); options.afterFinishInternal = function(effect) { effect.element.addClassName(effect.options.style); effect.transforms.each(function(transform) { effect.element.style[transform.style] = ''; }); }; } } this.start(options); }, setup: function(){ function parseColor(color){ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; color = color.parseColor(); return $R(0,2).map(function(i){ return parseInt( color.slice(i*2+1,i*2+3), 16 ); }); } this.transforms = this.style.map(function(pair){ var property = pair[0], value = pair[1], unit = null; if (value.parseColor('#zzzzzz') != '#zzzzzz') { value = value.parseColor(); unit = 'color'; } else if (property == 'opacity') { value = parseFloat(value); if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); } else if (Element.CSS_LENGTH.test(value)) { var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); value = parseFloat(components[1]); unit = (components.length == 3) ? components[2] : null; } var originalValue = this.element.getStyle(property); return { style: property.camelize(), originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), targetValue: unit=='color' ? parseColor(value) : value, unit: unit }; }.bind(this)).reject(function(transform){ return ( (transform.originalValue == transform.targetValue) || ( transform.unit != 'color' && (isNaN(transform.originalValue) || isNaN(transform.targetValue)) ) ); }); }, update: function(position) { var style = { }, transform, i = this.transforms.length; while(i--) style[(transform = this.transforms[i]).style] = transform.unit=='color' ? '#'+ (Math.round(transform.originalValue[0]+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + (Math.round(transform.originalValue[1]+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + (Math.round(transform.originalValue[2]+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : (transform.originalValue + (transform.targetValue - transform.originalValue) * position).toFixed(3) + (transform.unit === null ? '' : transform.unit); this.element.setStyle(style, true); } }); Effect.Transform = Class.create({ initialize: function(tracks){ this.tracks = []; this.options = arguments[1] || { }; this.addTracks(tracks); }, addTracks: function(tracks){ tracks.each(function(track){ track = $H(track); var data = track.values().first(); this.tracks.push($H({ ids: track.keys().first(), effect: Effect.Morph, options: { style: data } })); }.bind(this)); return this; }, play: function(){ return new Effect.Parallel( this.tracks.map(function(track){ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); var elements = [$(ids) || $$(ids)].flatten(); return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); }).flatten(), this.options ); } }); Element.CSS_PROPERTIES = $w( 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 'fontSize fontWeight height left letterSpacing lineHeight ' + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 'right textIndent top width wordSpacing zIndex'); Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; String.__parseStyleElement = document.createElement('div'); String.prototype.parseStyle = function(){ var style, styleRules = $H(); if (Prototype.Browser.WebKit) style = new Element('div',{style:this}).style; else { String.__parseStyleElement.innerHTML = '
    '; style = String.__parseStyleElement.childNodes[0].style; } Element.CSS_PROPERTIES.each(function(property){ if (style[property]) styleRules.set(property, style[property]); }); if (Prototype.Browser.IE && this.include('opacity')) styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); return styleRules; }; if (document.defaultView && document.defaultView.getComputedStyle) { Element.getStyles = function(element) { var css = document.defaultView.getComputedStyle($(element), null); return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { styles[property] = css[property]; return styles; }); }; } else { Element.getStyles = function(element) { element = $(element); var css = element.currentStyle, styles; styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { results[property] = css[property]; return results; }); if (!styles.opacity) styles.opacity = element.getOpacity(); return styles; }; } Effect.Methods = { morph: function(element, style) { element = $(element); new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); return element; }, visualEffect: function(element, effect, options) { element = $(element); var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); new Effect[klass](element, options); return element; }, highlight: function(element, options) { element = $(element); new Effect.Highlight(element, options); return element; } }; $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 'pulsate shake puff squish switchOff dropOut').each( function(effect) { Effect.Methods[effect] = function(element, options){ element = $(element); Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); return element; }; } ); $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( function(f) { Effect.Methods[f] = Element[f]; } ); Element.addMethods(Effect.Methods); ================================================ FILE: test/apps/rails_with_xss_plugin/public/javascripts/prototype.js ================================================ /* Prototype JavaScript framework, version 1.6.0.3 * (c) 2005-2008 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ * *--------------------------------------------------------------------------*/ var Prototype = { Version: '1.6.0.3', Browser: { IE: !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1), Opera: navigator.userAgent.indexOf('Opera') > -1, WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) }, BrowserFeatures: { XPath: !!document.evaluate, SelectorsAPI: !!document.querySelector, ElementExtensions: !!window.HTMLElement, SpecificElementExtensions: document.createElement('div')['__proto__'] && document.createElement('div')['__proto__'] !== document.createElement('form')['__proto__'] }, ScriptFragment: ']*>([\\S\\s]*?)<\/script>', JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, emptyFunction: function() { }, K: function(x) { return x } }; if (Prototype.Browser.MobileSafari) Prototype.BrowserFeatures.SpecificElementExtensions = false; /* Based on Alex Arnell's inheritance implementation. */ var Class = { create: function() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } }; Class.Methods = { addMethods: function(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } }; var Abstract = { }; Object.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; Object.extend(Object, { inspect: function(object) { try { if (Object.isUndefined(object)) return 'undefined'; if (object === null) return 'null'; return object.inspect ? object.inspect() : String(object); } catch (e) { if (e instanceof RangeError) return '...'; throw e; } }, toJSON: function(object) { var type = typeof object; switch (type) { case 'undefined': case 'function': case 'unknown': return; case 'boolean': return object.toString(); } if (object === null) return 'null'; if (object.toJSON) return object.toJSON(); if (Object.isElement(object)) return; var results = []; for (var property in object) { var value = Object.toJSON(object[property]); if (!Object.isUndefined(value)) results.push(property.toJSON() + ': ' + value); } return '{' + results.join(', ') + '}'; }, toQueryString: function(object) { return $H(object).toQueryString(); }, toHTML: function(object) { return object && object.toHTML ? object.toHTML() : String.interpret(object); }, keys: function(object) { var keys = []; for (var property in object) keys.push(property); return keys; }, values: function(object) { var values = []; for (var property in object) values.push(object[property]); return values; }, clone: function(object) { return Object.extend({ }, object); }, isElement: function(object) { return !!(object && object.nodeType == 1); }, isArray: function(object) { return object != null && typeof object == "object" && 'splice' in object && 'join' in object; }, isHash: function(object) { return object instanceof Hash; }, isFunction: function(object) { return typeof object == "function"; }, isString: function(object) { return typeof object == "string"; }, isNumber: function(object) { return typeof object == "number"; }, isUndefined: function(object) { return typeof object == "undefined"; } }); Object.extend(Function.prototype, { argumentNames: function() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1] .replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }, bind: function() { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }, bindAsEventListener: function() { var __method = this, args = $A(arguments), object = args.shift(); return function(event) { return __method.apply(object, [event || window.event].concat(args)); } }, curry: function() { if (!arguments.length) return this; var __method = this, args = $A(arguments); return function() { return __method.apply(this, args.concat($A(arguments))); } }, delay: function() { var __method = this, args = $A(arguments), timeout = args.shift() * 1000; return window.setTimeout(function() { return __method.apply(__method, args); }, timeout); }, defer: function() { var args = [0.01].concat($A(arguments)); return this.delay.apply(this, args); }, wrap: function(wrapper) { var __method = this; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } }, methodize: function() { if (this._methodized) return this._methodized; var __method = this; return this._methodized = function() { return __method.apply(null, [this].concat($A(arguments))); }; } }); Date.prototype.toJSON = function() { return '"' + this.getUTCFullYear() + '-' + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + this.getUTCDate().toPaddedString(2) + 'T' + this.getUTCHours().toPaddedString(2) + ':' + this.getUTCMinutes().toPaddedString(2) + ':' + this.getUTCSeconds().toPaddedString(2) + 'Z"'; }; var Try = { these: function() { var returnValue; for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); break; } catch (e) { } } return returnValue; } }; RegExp.prototype.match = RegExp.prototype.test; RegExp.escape = function(str) { return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }; /*--------------------------------------------------------------------------*/ var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; this.currentlyExecuting = false; this.registerCallback(); }, registerCallback: function() { this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, execute: function() { this.callback(this); }, stop: function() { if (!this.timer) return; clearInterval(this.timer); this.timer = null; }, onTimerEvent: function() { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; this.execute(); } finally { this.currentlyExecuting = false; } } } }); Object.extend(String, { interpret: function(value) { return value == null ? '' : String(value); }, specialChar: { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\\': '\\\\' } }); Object.extend(String.prototype, { gsub: function(pattern, replacement) { var result = '', source = this, match; replacement = arguments.callee.prepareReplacement(replacement); while (source.length > 0) { if (match = source.match(pattern)) { result += source.slice(0, match.index); result += String.interpret(replacement(match)); source = source.slice(match.index + match[0].length); } else { result += source, source = ''; } } return result; }, sub: function(pattern, replacement, count) { replacement = this.gsub.prepareReplacement(replacement); count = Object.isUndefined(count) ? 1 : count; return this.gsub(pattern, function(match) { if (--count < 0) return match[0]; return replacement(match); }); }, scan: function(pattern, iterator) { this.gsub(pattern, iterator); return String(this); }, truncate: function(length, truncation) { length = length || 30; truncation = Object.isUndefined(truncation) ? '...' : truncation; return this.length > length ? this.slice(0, length - truncation.length) + truncation : String(this); }, strip: function() { return this.replace(/^\s+/, '').replace(/\s+$/, ''); }, stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, stripScripts: function() { return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); }, extractScripts: function() { var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); return (this.match(matchAll) || []).map(function(scriptTag) { return (scriptTag.match(matchOne) || ['', ''])[1]; }); }, evalScripts: function() { return this.extractScripts().map(function(script) { return eval(script) }); }, escapeHTML: function() { var self = arguments.callee; self.text.data = this; return self.div.innerHTML; }, unescapeHTML: function() { var div = new Element('div'); div.innerHTML = this.stripTags(); return div.childNodes[0] ? (div.childNodes.length > 1 ? $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : div.childNodes[0].nodeValue) : ''; }, toQueryParams: function(separator) { var match = this.strip().match(/([^?#]*)(#.*)?$/); if (!match) return { }; return match[1].split(separator || '&').inject({ }, function(hash, pair) { if ((pair = pair.split('='))[0]) { var key = decodeURIComponent(pair.shift()); var value = pair.length > 1 ? pair.join('=') : pair[0]; if (value != undefined) value = decodeURIComponent(value); if (key in hash) { if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; hash[key].push(value); } else hash[key] = value; } return hash; }); }, toArray: function() { return this.split(''); }, succ: function() { return this.slice(0, this.length - 1) + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); }, times: function(count) { return count < 1 ? '' : new Array(count + 1).join(this); }, camelize: function() { var parts = this.split('-'), len = parts.length; if (len == 1) return parts[0]; var camelized = this.charAt(0) == '-' ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) : parts[0]; for (var i = 1; i < len; i++) camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); return camelized; }, capitalize: function() { return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); }, underscore: function() { return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); }, dasherize: function() { return this.gsub(/_/,'-'); }, inspect: function(useDoubleQuotes) { var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { var character = String.specialChar[match[0]]; return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); }); if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; return "'" + escapedString.replace(/'/g, '\\\'') + "'"; }, toJSON: function() { return this.inspect(true); }, unfilterJSON: function(filter) { return this.sub(filter || Prototype.JSONFilter, '#{1}'); }, isJSON: function() { var str = this; if (str.blank()) return false; str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); }, evalJSON: function(sanitize) { var json = this.unfilterJSON(); try { if (!sanitize || json.isJSON()) return eval('(' + json + ')'); } catch (e) { } throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); }, include: function(pattern) { return this.indexOf(pattern) > -1; }, startsWith: function(pattern) { return this.indexOf(pattern) === 0; }, endsWith: function(pattern) { var d = this.length - pattern.length; return d >= 0 && this.lastIndexOf(pattern) === d; }, empty: function() { return this == ''; }, blank: function() { return /^\s*$/.test(this); }, interpolate: function(object, pattern) { return new Template(this, pattern).evaluate(object); } }); if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { escapeHTML: function() { return this.replace(/&/g,'&').replace(//g,'>'); }, unescapeHTML: function() { return this.stripTags().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } }); String.prototype.gsub.prepareReplacement = function(replacement) { if (Object.isFunction(replacement)) return replacement; var template = new Template(replacement); return function(match) { return template.evaluate(match) }; }; String.prototype.parseQuery = String.prototype.toQueryParams; Object.extend(String.prototype.escapeHTML, { div: document.createElement('div'), text: document.createTextNode('') }); String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text); var Template = Class.create({ initialize: function(template, pattern) { this.template = template.toString(); this.pattern = pattern || Template.Pattern; }, evaluate: function(object) { if (Object.isFunction(object.toTemplateReplacements)) object = object.toTemplateReplacements(); return this.template.gsub(this.pattern, function(match) { if (object == null) return ''; var before = match[1] || ''; if (before == '\\') return match[2]; var ctx = object, expr = match[3]; var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; match = pattern.exec(expr); if (match == null) return before; while (match != null) { var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; ctx = ctx[comp]; if (null == ctx || '' == match[3]) break; expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); match = pattern.exec(expr); } return before + String.interpret(ctx); }); } }); Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; var $break = { }; var Enumerable = { each: function(iterator, context) { var index = 0; try { this._each(function(value) { iterator.call(context, value, index++); }); } catch (e) { if (e != $break) throw e; } return this; }, eachSlice: function(number, iterator, context) { var index = -number, slices = [], array = this.toArray(); if (number < 1) return array; while ((index += number) < array.length) slices.push(array.slice(index, index+number)); return slices.collect(iterator, context); }, all: function(iterator, context) { iterator = iterator || Prototype.K; var result = true; this.each(function(value, index) { result = result && !!iterator.call(context, value, index); if (!result) throw $break; }); return result; }, any: function(iterator, context) { iterator = iterator || Prototype.K; var result = false; this.each(function(value, index) { if (result = !!iterator.call(context, value, index)) throw $break; }); return result; }, collect: function(iterator, context) { iterator = iterator || Prototype.K; var results = []; this.each(function(value, index) { results.push(iterator.call(context, value, index)); }); return results; }, detect: function(iterator, context) { var result; this.each(function(value, index) { if (iterator.call(context, value, index)) { result = value; throw $break; } }); return result; }, findAll: function(iterator, context) { var results = []; this.each(function(value, index) { if (iterator.call(context, value, index)) results.push(value); }); return results; }, grep: function(filter, iterator, context) { iterator = iterator || Prototype.K; var results = []; if (Object.isString(filter)) filter = new RegExp(filter); this.each(function(value, index) { if (filter.match(value)) results.push(iterator.call(context, value, index)); }); return results; }, include: function(object) { if (Object.isFunction(this.indexOf)) if (this.indexOf(object) != -1) return true; var found = false; this.each(function(value) { if (value == object) { found = true; throw $break; } }); return found; }, inGroupsOf: function(number, fillWith) { fillWith = Object.isUndefined(fillWith) ? null : fillWith; return this.eachSlice(number, function(slice) { while(slice.length < number) slice.push(fillWith); return slice; }); }, inject: function(memo, iterator, context) { this.each(function(value, index) { memo = iterator.call(context, memo, value, index); }); return memo; }, invoke: function(method) { var args = $A(arguments).slice(1); return this.map(function(value) { return value[method].apply(value, args); }); }, max: function(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value >= result) result = value; }); return result; }, min: function(iterator, context) { iterator = iterator || Prototype.K; var result; this.each(function(value, index) { value = iterator.call(context, value, index); if (result == null || value < result) result = value; }); return result; }, partition: function(iterator, context) { iterator = iterator || Prototype.K; var trues = [], falses = []; this.each(function(value, index) { (iterator.call(context, value, index) ? trues : falses).push(value); }); return [trues, falses]; }, pluck: function(property) { var results = []; this.each(function(value) { results.push(value[property]); }); return results; }, reject: function(iterator, context) { var results = []; this.each(function(value, index) { if (!iterator.call(context, value, index)) results.push(value); }); return results; }, sortBy: function(iterator, context) { return this.map(function(value, index) { return { value: value, criteria: iterator.call(context, value, index) }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }).pluck('value'); }, toArray: function() { return this.map(); }, zip: function() { var iterator = Prototype.K, args = $A(arguments); if (Object.isFunction(args.last())) iterator = args.pop(); var collections = [this].concat(args).map($A); return this.map(function(value, index) { return iterator(collections.pluck(index)); }); }, size: function() { return this.toArray().length; }, inspect: function() { return '#'; } }; Object.extend(Enumerable, { map: Enumerable.collect, find: Enumerable.detect, select: Enumerable.findAll, filter: Enumerable.findAll, member: Enumerable.include, entries: Enumerable.toArray, every: Enumerable.all, some: Enumerable.any }); function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } if (Prototype.Browser.WebKit) { $A = function(iterable) { if (!iterable) return []; // In Safari, only use the `toArray` method if it's not a NodeList. // A NodeList is a function, has an function `item` property, and a numeric // `length` property. Adapted from Google Doctype. if (!(typeof iterable === 'function' && typeof iterable.length === 'number' && typeof iterable.item === 'function') && iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; }; } Array.from = $A; Object.extend(Array.prototype, Enumerable); if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }, clear: function() { this.length = 0; return this; }, first: function() { return this[0]; }, last: function() { return this[this.length - 1]; }, compact: function() { return this.select(function(value) { return value != null; }); }, flatten: function() { return this.inject([], function(array, value) { return array.concat(Object.isArray(value) ? value.flatten() : [value]); }); }, without: function() { var values = $A(arguments); return this.select(function(value) { return !values.include(value); }); }, reverse: function(inline) { return (inline !== false ? this : this.toArray())._reverse(); }, reduce: function() { return this.length > 1 ? this : this[0]; }, uniq: function(sorted) { return this.inject([], function(array, value, index) { if (0 == index || (sorted ? array.last() != value : !array.include(value))) array.push(value); return array; }); }, intersect: function(array) { return this.uniq().findAll(function(item) { return array.detect(function(value) { return item === value }); }); }, clone: function() { return [].concat(this); }, size: function() { return this.length; }, inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; }, toJSON: function() { var results = []; this.each(function(object) { var value = Object.toJSON(object); if (!Object.isUndefined(value)) results.push(value); }); return '[' + results.join(', ') + ']'; } }); // use native browser JS 1.6 implementation if available if (Object.isFunction(Array.prototype.forEach)) Array.prototype._each = Array.prototype.forEach; if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { i || (i = 0); var length = this.length; if (i < 0) i = length + i; for (; i < length; i++) if (this[i] === item) return i; return -1; }; if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; var n = this.slice(0, i).reverse().indexOf(item); return (n < 0) ? n : i - n - 1; }; Array.prototype.toArray = Array.prototype.clone; function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); return string ? string.split(/\s+/) : []; } if (Prototype.Browser.Opera){ Array.prototype.concat = function() { var array = []; for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); for (var i = 0, length = arguments.length; i < length; i++) { if (Object.isArray(arguments[i])) { for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) array.push(arguments[i][j]); } else { array.push(arguments[i]); } } return array; }; } Object.extend(Number.prototype, { toColorPart: function() { return this.toPaddedString(2, 16); }, succ: function() { return this + 1; }, times: function(iterator, context) { $R(0, this, true).each(iterator, context); return this; }, toPaddedString: function(length, radix) { var string = this.toString(radix || 10); return '0'.times(length - string.length) + string; }, toJSON: function() { return isFinite(this) ? this.toString() : 'null'; } }); $w('abs round ceil floor').each(function(method){ Number.prototype[method] = Math[method].methodize(); }); function $H(object) { return new Hash(object); }; var Hash = Class.create(Enumerable, (function() { function toQueryPair(key, value) { if (Object.isUndefined(value)) return key; return key + '=' + encodeURIComponent(String.interpret(value)); } return { initialize: function(object) { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); }, _each: function(iterator) { for (var key in this._object) { var value = this._object[key], pair = [key, value]; pair.key = key; pair.value = value; iterator(pair); } }, set: function(key, value) { return this._object[key] = value; }, get: function(key) { // simulating poorly supported hasOwnProperty if (this._object[key] !== Object.prototype[key]) return this._object[key]; }, unset: function(key) { var value = this._object[key]; delete this._object[key]; return value; }, toObject: function() { return Object.clone(this._object); }, keys: function() { return this.pluck('key'); }, values: function() { return this.pluck('value'); }, index: function(value) { var match = this.detect(function(pair) { return pair.value === value; }); return match && match.key; }, merge: function(object) { return this.clone().update(object); }, update: function(object) { return new Hash(object).inject(this, function(result, pair) { result.set(pair.key, pair.value); return result; }); }, toQueryString: function() { return this.inject([], function(results, pair) { var key = encodeURIComponent(pair.key), values = pair.value; if (values && typeof values == 'object') { if (Object.isArray(values)) return results.concat(values.map(toQueryPair.curry(key))); } else results.push(toQueryPair(key, values)); return results; }).join('&'); }, inspect: function() { return '#'; }, toJSON: function() { return Object.toJSON(this.toObject()); }, clone: function() { return new Hash(this); } } })()); Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; Hash.from = $H; var ObjectRange = Class.create(Enumerable, { initialize: function(start, end, exclusive) { this.start = start; this.end = end; this.exclusive = exclusive; }, _each: function(iterator) { var value = this.start; while (this.include(value)) { iterator(value); value = value.succ(); } }, include: function(value) { if (value < this.start) return false; if (this.exclusive) return value < this.end; return value <= this.end; } }); var $R = function(start, end, exclusive) { return new ObjectRange(start, end, exclusive); }; var Ajax = { getTransport: function() { return Try.these( function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, activeRequestCount: 0 }; Ajax.Responders = { responders: [], _each: function(iterator) { this.responders._each(iterator); }, register: function(responder) { if (!this.include(responder)) this.responders.push(responder); }, unregister: function(responder) { this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { if (Object.isFunction(responder[callback])) { try { responder[callback].apply(responder, [request, transport, json]); } catch (e) { } } }); } }; Object.extend(Ajax.Responders, Enumerable); Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++ }, onComplete: function() { Ajax.activeRequestCount-- } }); Ajax.Base = Class.create({ initialize: function(options) { this.options = { method: 'post', asynchronous: true, contentType: 'application/x-www-form-urlencoded', encoding: 'UTF-8', parameters: '', evalJSON: true, evalJS: true }; Object.extend(this.options, options || { }); this.options.method = this.options.method.toLowerCase(); if (Object.isString(this.options.parameters)) this.options.parameters = this.options.parameters.toQueryParams(); else if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); Ajax.Request = Class.create(Ajax.Base, { _complete: false, initialize: function($super, url, options) { $super(options); this.transport = Ajax.getTransport(); this.request(url); }, request: function(url) { this.url = url; this.method = this.options.method; var params = Object.clone(this.options.parameters); if (!['get', 'post'].include(this.method)) { // simulate other verbs over post params['_method'] = this.method; this.method = 'post'; } this.parameters = params; if (params = Object.toQueryString(params)) { // when GET, append parameters to URL if (this.method == 'get') this.url += (this.url.include('?') ? '&' : '?') + params; else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='; } try { var response = new Ajax.Response(this); if (this.options.onCreate) this.options.onCreate(response); Ajax.Responders.dispatch('onCreate', this, response); this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); this.body = this.method == 'post' ? (this.options.postBody || params) : null; this.transport.send(this.body); /* Force Firefox to handle ready state 4 for synchronous requests */ if (!this.options.asynchronous && this.transport.overrideMimeType) this.onStateChange(); } catch (e) { this.dispatchException(e); } }, onStateChange: function() { var readyState = this.transport.readyState; if (readyState > 1 && !((readyState == 4) && this._complete)) this.respondToReadyState(this.transport.readyState); }, setRequestHeaders: function() { var headers = { 'X-Requested-With': 'XMLHttpRequest', 'X-Prototype-Version': Prototype.Version, 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' }; if (this.method == 'post') { headers['Content-type'] = this.options.contentType + (this.options.encoding ? '; charset=' + this.options.encoding : ''); /* Force "Connection: close" for older Mozilla browsers to work * around a bug where XMLHttpRequest sends an incorrect * Content-length header. See Mozilla Bugzilla #246651. */ if (this.transport.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) headers['Connection'] = 'close'; } // user-defined headers if (typeof this.options.requestHeaders == 'object') { var extras = this.options.requestHeaders; if (Object.isFunction(extras.push)) for (var i = 0, length = extras.length; i < length; i += 2) headers[extras[i]] = extras[i+1]; else $H(extras).each(function(pair) { headers[pair.key] = pair.value }); } for (var name in headers) this.transport.setRequestHeader(name, headers[name]); }, success: function() { var status = this.getStatus(); return !status || (status >= 200 && status < 300); }, getStatus: function() { try { return this.transport.status || 0; } catch (e) { return 0 } }, respondToReadyState: function(readyState) { var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); if (state == 'Complete') { try { this._complete = true; (this.options['on' + response.status] || this.options['on' + (this.success() ? 'Success' : 'Failure')] || Prototype.emptyFunction)(response, response.headerJSON); } catch (e) { this.dispatchException(e); } var contentType = response.getHeader('Content-type'); if (this.options.evalJS == 'force' || (this.options.evalJS && this.isSameOrigin() && contentType && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) this.evalResponse(); } try { (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); } catch (e) { this.dispatchException(e); } if (state == 'Complete') { // avoid memory leak in MSIE: clean up this.transport.onreadystatechange = Prototype.emptyFunction; } }, isSameOrigin: function() { var m = this.url.match(/^\s*https?:\/\/[^\/]*/); return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ protocol: location.protocol, domain: document.domain, port: location.port ? ':' + location.port : '' })); }, getHeader: function(name) { try { return this.transport.getResponseHeader(name) || null; } catch (e) { return null } }, evalResponse: function() { try { return eval((this.transport.responseText || '').unfilterJSON()); } catch (e) { this.dispatchException(e); } }, dispatchException: function(exception) { (this.options.onException || Prototype.emptyFunction)(this, exception); Ajax.Responders.dispatch('onException', this, exception); } }); Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; Ajax.Response = Class.create({ initialize: function(request){ this.request = request; var transport = this.transport = request.transport, readyState = this.readyState = transport.readyState; if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { this.status = this.getStatus(); this.statusText = this.getStatusText(); this.responseText = String.interpret(transport.responseText); this.headerJSON = this._getHeaderJSON(); } if(readyState == 4) { var xml = transport.responseXML; this.responseXML = Object.isUndefined(xml) ? null : xml; this.responseJSON = this._getResponseJSON(); } }, status: 0, statusText: '', getStatus: Ajax.Request.prototype.getStatus, getStatusText: function() { try { return this.transport.statusText || ''; } catch (e) { return '' } }, getHeader: Ajax.Request.prototype.getHeader, getAllHeaders: function() { try { return this.getAllResponseHeaders(); } catch (e) { return null } }, getResponseHeader: function(name) { return this.transport.getResponseHeader(name); }, getAllResponseHeaders: function() { return this.transport.getAllResponseHeaders(); }, _getHeaderJSON: function() { var json = this.getHeader('X-JSON'); if (!json) return null; json = decodeURIComponent(escape(json)); try { return json.evalJSON(this.request.options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } }, _getResponseJSON: function() { var options = this.request.options; if (!options.evalJSON || (options.evalJSON != 'force' && !(this.getHeader('Content-type') || '').include('application/json')) || this.responseText.blank()) return null; try { return this.responseText.evalJSON(options.sanitizeJSON || !this.request.isSameOrigin()); } catch (e) { this.request.dispatchException(e); } } }); Ajax.Updater = Class.create(Ajax.Request, { initialize: function($super, container, url, options) { this.container = { success: (container.success || container), failure: (container.failure || (container.success ? null : container)) }; options = Object.clone(options); var onComplete = options.onComplete; options.onComplete = (function(response, json) { this.updateContent(response.responseText); if (Object.isFunction(onComplete)) onComplete(response, json); }).bind(this); $super(url, options); }, updateContent: function(responseText) { var receiver = this.container[this.success() ? 'success' : 'failure'], options = this.options; if (!options.evalScripts) responseText = responseText.stripScripts(); if (receiver = $(receiver)) { if (options.insertion) { if (Object.isString(options.insertion)) { var insertion = { }; insertion[options.insertion] = responseText; receiver.insert(insertion); } else options.insertion(receiver, responseText); } else receiver.update(responseText); } } }); Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { initialize: function($super, container, url, options) { $super(options); this.onComplete = this.options.onComplete; this.frequency = (this.options.frequency || 2); this.decay = (this.options.decay || 1); this.updater = { }; this.container = container; this.url = url; this.start(); }, start: function() { this.options.onComplete = this.updateComplete.bind(this); this.onTimerEvent(); }, stop: function() { this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, updateComplete: function(response) { if (this.options.decay) { this.decay = (response.responseText == this.lastText ? this.decay * this.options.decay : 1); this.lastText = response.responseText; } this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); }, onTimerEvent: function() { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); function $(element) { if (arguments.length > 1) { for (var i = 0, elements = [], length = arguments.length; i < length; i++) elements.push($(arguments[i])); return elements; } if (Object.isString(element)) element = document.getElementById(element); return Element.extend(element); } if (Prototype.BrowserFeatures.XPath) { document._getElementsByXPath = function(expression, parentElement) { var results = []; var query = document.evaluate(expression, $(parentElement) || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i < length; i++) results.push(Element.extend(query.snapshotItem(i))); return results; }; } /*--------------------------------------------------------------------------*/ if (!window.Node) var Node = { }; if (!Node.ELEMENT_NODE) { // DOM level 2 ECMAScript Language Binding Object.extend(Node, { ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12 }); } (function() { var element = this.Element; this.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; if (Prototype.Browser.IE && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); }; Object.extend(this.Element, element || { }); if (element) this.Element.prototype = element.prototype; }).call(window); Element.cache = { }; Element.Methods = { visible: function(element) { return $(element).style.display != 'none'; }, toggle: function(element) { element = $(element); Element[Element.visible(element) ? 'hide' : 'show'](element); return element; }, hide: function(element) { element = $(element); element.style.display = 'none'; return element; }, show: function(element) { element = $(element); element.style.display = ''; return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); return element; }, update: function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) return element.update().insert(content); content = Object.toHTML(content); element.innerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }, replace: function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); else if (!Object.isElement(content)) { content = Object.toHTML(content); var range = element.ownerDocument.createRange(); range.selectNode(element); content.evalScripts.bind(content).defer(); content = range.createContextualFragment(content.stripScripts()); } element.parentNode.replaceChild(content, element); return element; }, insert: function(element, insertions) { element = $(element); if (Object.isString(insertions) || Object.isNumber(insertions) || Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) insertions = {bottom:insertions}; var content, insert, tagName, childNodes; for (var position in insertions) { content = insertions[position]; position = position.toLowerCase(); insert = Element._insertionTranslations[position]; if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { insert(element, content); continue; } content = Object.toHTML(content); tagName = ((position == 'before' || position == 'after') ? element.parentNode : element).tagName.toUpperCase(); childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); if (position == 'top' || position == 'after') childNodes.reverse(); childNodes.each(insert.curry(element)); content.evalScripts.bind(content).defer(); } return element; }, wrap: function(element, wrapper, attributes) { element = $(element); if (Object.isElement(wrapper)) $(wrapper).writeAttribute(attributes || { }); else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); else wrapper = new Element('div', wrapper); if (element.parentNode) element.parentNode.replaceChild(wrapper, element); wrapper.appendChild(element); return wrapper; }, inspect: function(element) { element = $(element); var result = '<' + element.tagName.toLowerCase(); $H({'id': 'id', 'className': 'class'}).each(function(pair) { var property = pair.first(), attribute = pair.last(); var value = (element[property] || '').toString(); if (value) result += ' ' + attribute + '=' + value.inspect(true); }); return result + '>'; }, recursivelyCollect: function(element, property) { element = $(element); var elements = []; while (element = element[property]) if (element.nodeType == 1) elements.push(Element.extend(element)); return elements; }, ancestors: function(element) { return $(element).recursivelyCollect('parentNode'); }, descendants: function(element) { return $(element).select("*"); }, firstDescendant: function(element) { element = $(element).firstChild; while (element && element.nodeType != 1) element = element.nextSibling; return $(element); }, immediateDescendants: function(element) { if (!(element = $(element).firstChild)) return []; while (element && element.nodeType != 1) element = element.nextSibling; if (element) return [element].concat($(element).nextSiblings()); return []; }, previousSiblings: function(element) { return $(element).recursivelyCollect('previousSibling'); }, nextSiblings: function(element) { return $(element).recursivelyCollect('nextSibling'); }, siblings: function(element) { element = $(element); return element.previousSiblings().reverse().concat(element.nextSiblings()); }, match: function(element, selector) { if (Object.isString(selector)) selector = new Selector(selector); return selector.match($(element)); }, up: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(element.parentNode); var ancestors = element.ancestors(); return Object.isNumber(expression) ? ancestors[expression] : Selector.findElement(ancestors, expression, index); }, down: function(element, expression, index) { element = $(element); if (arguments.length == 1) return element.firstDescendant(); return Object.isNumber(expression) ? element.descendants()[expression] : Element.select(element, expression)[index || 0]; }, previous: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); var previousSiblings = element.previousSiblings(); return Object.isNumber(expression) ? previousSiblings[expression] : Selector.findElement(previousSiblings, expression, index); }, next: function(element, expression, index) { element = $(element); if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); var nextSiblings = element.nextSiblings(); return Object.isNumber(expression) ? nextSiblings[expression] : Selector.findElement(nextSiblings, expression, index); }, select: function() { var args = $A(arguments), element = $(args.shift()); return Selector.findChildElements(element, args); }, adjacent: function() { var args = $A(arguments), element = $(args.shift()); return Selector.findChildElements(element.parentNode, args).without(element); }, identify: function(element) { element = $(element); var id = element.readAttribute('id'), self = arguments.callee; if (id) return id; do { id = 'anonymous_element_' + self.counter++ } while ($(id)); element.writeAttribute('id', id); return id; }, readAttribute: function(element, name) { element = $(element); if (Prototype.Browser.IE) { var t = Element._attributeTranslations.read; if (t.values[name]) return t.values[name](element, name); if (t.names[name]) name = t.names[name]; if (name.include(':')) { return (!element.attributes || !element.attributes[name]) ? null : element.attributes[name].value; } } return element.getAttribute(name); }, writeAttribute: function(element, name, value) { element = $(element); var attributes = { }, t = Element._attributeTranslations.write; if (typeof name == 'object') attributes = name; else attributes[name] = Object.isUndefined(value) ? true : value; for (var attr in attributes) { name = t.names[attr] || attr; value = attributes[attr]; if (t.values[attr]) name = t.values[attr](element, value); if (value === false || value === null) element.removeAttribute(name); else if (value === true) element.setAttribute(name, name); else element.setAttribute(name, value); } return element; }, getHeight: function(element) { return $(element).getDimensions().height; }, getWidth: function(element) { return $(element).getDimensions().width; }, classNames: function(element) { return new Element.ClassNames(element); }, hasClassName: function(element, className) { if (!(element = $(element))) return; var elementClassName = element.className; return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }, addClassName: function(element, className) { if (!(element = $(element))) return; if (!element.hasClassName(className)) element.className += (element.className ? ' ' : '') + className; return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; element.className = element.className.replace( new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); return element; }, toggleClassName: function(element, className) { if (!(element = $(element))) return; return element[element.hasClassName(className) ? 'removeClassName' : 'addClassName'](className); }, // removes whitespace-only text node children cleanWhitespace: function(element) { element = $(element); var node = element.firstChild; while (node) { var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) element.removeChild(node); node = nextNode; } return element; }, empty: function(element) { return $(element).innerHTML.blank(); }, descendantOf: function(element, ancestor) { element = $(element), ancestor = $(ancestor); if (element.compareDocumentPosition) return (element.compareDocumentPosition(ancestor) & 8) === 8; if (ancestor.contains) return ancestor.contains(element) && ancestor !== element; while (element = element.parentNode) if (element == ancestor) return true; return false; }, scrollTo: function(element) { element = $(element); var pos = element.cumulativeOffset(); window.scrollTo(pos[0], pos[1]); return element; }, getStyle: function(element, style) { element = $(element); style = style == 'float' ? 'cssFloat' : style.camelize(); var value = element.style[style]; if (!value || value == 'auto') { var css = document.defaultView.getComputedStyle(element, null); value = css ? css[style] : null; } if (style == 'opacity') return value ? parseFloat(value) : 1.0; return value == 'auto' ? null : value; }, getOpacity: function(element) { return $(element).getStyle('opacity'); }, setStyle: function(element, styles) { element = $(element); var elementStyle = element.style, match; if (Object.isString(styles)) { element.style.cssText += ';' + styles; return styles.include('opacity') ? element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; } for (var property in styles) if (property == 'opacity') element.setOpacity(styles[property]); else elementStyle[(property == 'float' || property == 'cssFloat') ? (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : property] = styles[property]; return element; }, setOpacity: function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }, getDimensions: function(element) { element = $(element); var display = element.getStyle('display'); if (display != 'none' && display != null) // Safari bug return {width: element.offsetWidth, height: element.offsetHeight}; // All *Width and *Height properties give 0 on elements with display none, // so enable the element temporarily var els = element.style; var originalVisibility = els.visibility; var originalPosition = els.position; var originalDisplay = els.display; els.visibility = 'hidden'; els.position = 'absolute'; els.display = 'block'; var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; els.display = originalDisplay; els.position = originalPosition; els.visibility = originalVisibility; return {width: originalWidth, height: originalHeight}; }, makePositioned: function(element) { element = $(element); var pos = Element.getStyle(element, 'position'); if (pos == 'static' || !pos) { element._madePositioned = true; element.style.position = 'relative'; // Opera returns the offset relative to the positioning context, when an // element is position relative but top and left have not been defined if (Prototype.Browser.Opera) { element.style.top = 0; element.style.left = 0; } } return element; }, undoPositioned: function(element) { element = $(element); if (element._madePositioned) { element._madePositioned = undefined; element.style.position = element.style.top = element.style.left = element.style.bottom = element.style.right = ''; } return element; }, makeClipping: function(element) { element = $(element); if (element._overflow) return element; element._overflow = Element.getStyle(element, 'overflow') || 'auto'; if (element._overflow !== 'hidden') element.style.overflow = 'hidden'; return element; }, undoClipping: function(element) { element = $(element); if (!element._overflow) return element; element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; element._overflow = null; return element; }, cumulativeOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; } while (element); return Element._returnOffset(valueL, valueT); }, positionedOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { if (element.tagName.toUpperCase() == 'BODY') break; var p = Element.getStyle(element, 'position'); if (p !== 'static') break; } } while (element); return Element._returnOffset(valueL, valueT); }, absolutize: function(element) { element = $(element); if (element.getStyle('position') == 'absolute') return element; // Position.prepare(); // To be done manually by Scripty when it needs it. var offsets = element.positionedOffset(); var top = offsets[1]; var left = offsets[0]; var width = element.clientWidth; var height = element.clientHeight; element._originalLeft = left - parseFloat(element.style.left || 0); element._originalTop = top - parseFloat(element.style.top || 0); element._originalWidth = element.style.width; element._originalHeight = element.style.height; element.style.position = 'absolute'; element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.width = width + 'px'; element.style.height = height + 'px'; return element; }, relativize: function(element) { element = $(element); if (element.getStyle('position') == 'relative') return element; // Position.prepare(); // To be done manually by Scripty when it needs it. element.style.position = 'relative'; var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); element.style.top = top + 'px'; element.style.left = left + 'px'; element.style.height = element._originalHeight; element.style.width = element._originalWidth; return element; }, cumulativeScrollOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.scrollTop || 0; valueL += element.scrollLeft || 0; element = element.parentNode; } while (element); return Element._returnOffset(valueL, valueT); }, getOffsetParent: function(element) { if (element.offsetParent) return $(element.offsetParent); if (element == document.body) return $(element); while ((element = element.parentNode) && element != document.body) if (Element.getStyle(element, 'position') != 'static') return $(element); return $(document.body); }, viewportOffset: function(forElement) { var valueT = 0, valueL = 0; var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; // Safari fix if (element.offsetParent == document.body && Element.getStyle(element, 'position') == 'absolute') break; } while (element = element.offsetParent); element = forElement; do { if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } } while (element = element.parentNode); return Element._returnOffset(valueL, valueT); }, clonePosition: function(element, source) { var options = Object.extend({ setLeft: true, setTop: true, setWidth: true, setHeight: true, offsetTop: 0, offsetLeft: 0 }, arguments[2] || { }); // find page position of source source = $(source); var p = source.viewportOffset(); // find coordinate system to use element = $(element); var delta = [0, 0]; var parent = null; // delta [0,0] will do fine with position: fixed elements, // position:absolute needs offsetParent deltas if (Element.getStyle(element, 'position') == 'absolute') { parent = element.getOffsetParent(); delta = parent.viewportOffset(); } // correct by body offsets (fixes Safari) if (parent == document.body) { delta[0] -= document.body.offsetLeft; delta[1] -= document.body.offsetTop; } // set position if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; if (options.setWidth) element.style.width = source.offsetWidth + 'px'; if (options.setHeight) element.style.height = source.offsetHeight + 'px'; return element; } }; Element.Methods.identify.counter = 1; Object.extend(Element.Methods, { getElementsBySelector: Element.Methods.select, childElements: Element.Methods.immediateDescendants }); Element._attributeTranslations = { write: { names: { className: 'class', htmlFor: 'for' }, values: { } } }; if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { switch (style) { case 'left': case 'top': case 'right': case 'bottom': if (proceed(element, 'position') === 'static') return null; case 'height': case 'width': // returns '0px' for hidden elements; we want it to return null if (!Element.visible(element)) return null; // returns the border-box dimensions rather than the content-box // dimensions, so we subtract padding and borders from the value var dim = parseInt(proceed(element, style), 10); if (dim !== element['offset' + style.capitalize()]) return dim + 'px'; var properties; if (style === 'height') { properties = ['border-top-width', 'padding-top', 'padding-bottom', 'border-bottom-width']; } else { properties = ['border-left-width', 'padding-left', 'padding-right', 'border-right-width']; } return properties.inject(dim, function(memo, property) { var val = proceed(element, property); return val === null ? memo : memo - parseInt(val, 10); }) + 'px'; default: return proceed(element, style); } } ); Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( function(proceed, element, attribute) { if (attribute === 'title') return element.title; return proceed(element, attribute); } ); } else if (Prototype.Browser.IE) { // IE doesn't report offsets correctly for static elements, so we change them // to "relative" to get the values, then change them back. Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( function(proceed, element) { element = $(element); // IE throws an error if element is not in document try { element.offsetParent } catch(e) { return $(document.body) } var position = element.getStyle('position'); if (position !== 'static') return proceed(element); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); $w('positionedOffset viewportOffset').each(function(method) { Element.Methods[method] = Element.Methods[method].wrap( function(proceed, element) { element = $(element); try { element.offsetParent } catch(e) { return Element._returnOffset(0,0) } var position = element.getStyle('position'); if (position !== 'static') return proceed(element); // Trigger hasLayout on the offset parent so that IE6 reports // accurate offsetTop and offsetLeft values for position: fixed. var offsetParent = element.getOffsetParent(); if (offsetParent && offsetParent.getStyle('position') === 'fixed') offsetParent.setStyle({ zoom: 1 }); element.setStyle({ position: 'relative' }); var value = proceed(element); element.setStyle({ position: position }); return value; } ); }); Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( function(proceed, element) { try { element.offsetParent } catch(e) { return Element._returnOffset(0,0) } return proceed(element); } ); Element.Methods.getStyle = function(element, style) { element = $(element); style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); var value = element.style[style]; if (!value && element.currentStyle) value = element.currentStyle[style]; if (style == 'opacity') { if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) if (value[1]) return parseFloat(value[1]) / 100; return 1.0; } if (value == 'auto') { if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) return element['offset' + style.capitalize()] + 'px'; return null; } return value; }; Element.Methods.setOpacity = function(element, value) { function stripAlpha(filter){ return filter.replace(/alpha\([^\)]*\)/gi,''); } element = $(element); var currentStyle = element.currentStyle; if ((currentStyle && !currentStyle.hasLayout) || (!currentStyle && element.style.zoom == 'normal')) element.style.zoom = 1; var filter = element.getStyle('filter'), style = element.style; if (value == 1 || value === '') { (filter = stripAlpha(filter)) ? style.filter = filter : style.removeAttribute('filter'); return element; } else if (value < 0.00001) value = 0; style.filter = stripAlpha(filter) + 'alpha(opacity=' + (value * 100) + ')'; return element; }; Element._attributeTranslations = { read: { names: { 'class': 'className', 'for': 'htmlFor' }, values: { _getAttr: function(element, attribute) { return element.getAttribute(attribute, 2); }, _getAttrNode: function(element, attribute) { var node = element.getAttributeNode(attribute); return node ? node.value : ""; }, _getEv: function(element, attribute) { attribute = element.getAttribute(attribute); return attribute ? attribute.toString().slice(23, -2) : null; }, _flag: function(element, attribute) { return $(element).hasAttribute(attribute) ? attribute : null; }, style: function(element) { return element.style.cssText.toLowerCase(); }, title: function(element) { return element.title; } } } }; Element._attributeTranslations.write = { names: Object.extend({ cellpadding: 'cellPadding', cellspacing: 'cellSpacing' }, Element._attributeTranslations.read.names), values: { checked: function(element, value) { element.checked = !!value; }, style: function(element, value) { element.style.cssText = value ? value : ''; } } }; Element._attributeTranslations.has = {}; $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; Element._attributeTranslations.has[attr.toLowerCase()] = attr; }); (function(v) { Object.extend(v, { href: v._getAttr, src: v._getAttr, type: v._getAttr, action: v._getAttrNode, disabled: v._flag, checked: v._flag, readonly: v._flag, multiple: v._flag, onload: v._getEv, onunload: v._getEv, onclick: v._getEv, ondblclick: v._getEv, onmousedown: v._getEv, onmouseup: v._getEv, onmouseover: v._getEv, onmousemove: v._getEv, onmouseout: v._getEv, onfocus: v._getEv, onblur: v._getEv, onkeypress: v._getEv, onkeydown: v._getEv, onkeyup: v._getEv, onsubmit: v._getEv, onreset: v._getEv, onselect: v._getEv, onchange: v._getEv }); })(Element._attributeTranslations.read.values); } else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1) ? 0.999999 : (value === '') ? '' : (value < 0.00001) ? 0 : value; return element; }; } else if (Prototype.Browser.WebKit) { Element.Methods.setOpacity = function(element, value) { element = $(element); element.style.opacity = (value == 1 || value === '') ? '' : (value < 0.00001) ? 0 : value; if (value == 1) if(element.tagName.toUpperCase() == 'IMG' && element.width) { element.width++; element.width--; } else try { var n = document.createTextNode(' '); element.appendChild(n); element.removeChild(n); } catch (e) { } return element; }; // Safari returns margins on body which is incorrect if the child is absolutely // positioned. For performance reasons, redefine Element#cumulativeOffset for // KHTML/WebKit only. Element.Methods.cumulativeOffset = function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if (element.offsetParent == document.body) if (Element.getStyle(element, 'position') == 'absolute') break; element = element.offsetParent; } while (element); return Element._returnOffset(valueL, valueT); }; } if (Prototype.Browser.IE || Prototype.Browser.Opera) { // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements Element.Methods.update = function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) return element.update().insert(content); content = Object.toHTML(content); var tagName = element.tagName.toUpperCase(); if (tagName in Element._insertionTranslations.tags) { $A(element.childNodes).each(function(node) { element.removeChild(node) }); Element._getContentFromAnonymousElement(tagName, content.stripScripts()) .each(function(node) { element.appendChild(node) }); } else element.innerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }; } if ('outerHTML' in document.createElement('div')) { Element.Methods.replace = function(element, content) { element = $(element); if (content && content.toElement) content = content.toElement(); if (Object.isElement(content)) { element.parentNode.replaceChild(content, element); return element; } content = Object.toHTML(content); var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); if (Element._insertionTranslations.tags[tagName]) { var nextSibling = element.next(); var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); parent.removeChild(element); if (nextSibling) fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); else fragments.each(function(node) { parent.appendChild(node) }); } else element.outerHTML = content.stripScripts(); content.evalScripts.bind(content).defer(); return element; }; } Element._returnOffset = function(l, t) { var result = [l, t]; result.left = l; result.top = t; return result; }; Element._getContentFromAnonymousElement = function(tagName, html) { var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; if (t) { div.innerHTML = t[0] + html + t[1]; t[2].times(function() { div = div.firstChild }); } else div.innerHTML = html; return $A(div.childNodes); }; Element._insertionTranslations = { before: function(element, node) { element.parentNode.insertBefore(node, element); }, top: function(element, node) { element.insertBefore(node, element.firstChild); }, bottom: function(element, node) { element.appendChild(node); }, after: function(element, node) { element.parentNode.insertBefore(node, element.nextSibling); }, tags: { TABLE: ['', '
    ', 1], TBODY: ['', '
    ', 2], TR: ['', '
    ', 3], TD: ['
    ', '
    ', 4], SELECT: ['', 1] } }; (function() { Object.extend(this.tags, { THEAD: this.tags.TBODY, TFOOT: this.tags.TBODY, TH: this.tags.TD }); }).call(Element._insertionTranslations); Element.Methods.Simulated = { hasAttribute: function(element, attribute) { attribute = Element._attributeTranslations.has[attribute] || attribute; var node = $(element).getAttributeNode(attribute); return !!(node && node.specified); } }; Element.Methods.ByTag = { }; Object.extend(Element, Element.Methods); if (!Prototype.BrowserFeatures.ElementExtensions && document.createElement('div')['__proto__']) { window.HTMLElement = { }; window.HTMLElement.prototype = document.createElement('div')['__proto__']; Prototype.BrowserFeatures.ElementExtensions = true; } Element.extend = (function() { if (Prototype.BrowserFeatures.SpecificElementExtensions) return Prototype.K; var Methods = { }, ByTag = Element.Methods.ByTag; var extend = Object.extend(function(element) { if (!element || element._extendedByPrototype || element.nodeType != 1 || element == window) return element; var methods = Object.clone(Methods), tagName = element.tagName.toUpperCase(), property, value; // extend methods for specific tags if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); for (property in methods) { value = methods[property]; if (Object.isFunction(value) && !(property in element)) element[property] = value.methodize(); } element._extendedByPrototype = Prototype.emptyFunction; return element; }, { refresh: function() { // extend methods for all tags (Safari doesn't need this) if (!Prototype.BrowserFeatures.ElementExtensions) { Object.extend(Methods, Element.Methods); Object.extend(Methods, Element.Methods.Simulated); } } }); extend.refresh(); return extend; })(); Element.hasAttribute = function(element, attribute) { if (element.hasAttribute) return element.hasAttribute(attribute); return Element.Methods.Simulated.hasAttribute(element, attribute); }; Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; if (!methods) { Object.extend(Form, Form.Methods); Object.extend(Form.Element, Form.Element.Methods); Object.extend(Element.Methods.ByTag, { "FORM": Object.clone(Form.Methods), "INPUT": Object.clone(Form.Element.Methods), "SELECT": Object.clone(Form.Element.Methods), "TEXTAREA": Object.clone(Form.Element.Methods) }); } if (arguments.length == 2) { var tagName = methods; methods = arguments[1]; } if (!tagName) Object.extend(Element.Methods, methods || { }); else { if (Object.isArray(tagName)) tagName.each(extend); else extend(tagName); } function extend(tagName) { tagName = tagName.toUpperCase(); if (!Element.Methods.ByTag[tagName]) Element.Methods.ByTag[tagName] = { }; Object.extend(Element.Methods.ByTag[tagName], methods); } function copy(methods, destination, onlyIfAbsent) { onlyIfAbsent = onlyIfAbsent || false; for (var property in methods) { var value = methods[property]; if (!Object.isFunction(value)) continue; if (!onlyIfAbsent || !(property in destination)) destination[property] = value.methodize(); } } function findDOMClass(tagName) { var klass; var trans = { "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": "FrameSet", "IFRAME": "IFrame" }; if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName + 'Element'; if (window[klass]) return window[klass]; klass = 'HTML' + tagName.capitalize() + 'Element'; if (window[klass]) return window[klass]; window[klass] = { }; window[klass].prototype = document.createElement(tagName)['__proto__']; return window[klass]; } if (F.ElementExtensions) { copy(Element.Methods, HTMLElement.prototype); copy(Element.Methods.Simulated, HTMLElement.prototype, true); } if (F.SpecificElementExtensions) { for (var tag in Element.Methods.ByTag) { var klass = findDOMClass(tag); if (Object.isUndefined(klass)) continue; copy(T[tag], klass.prototype); } } Object.extend(Element, Element.Methods); delete Element.ByTag; if (Element.extend.refresh) Element.extend.refresh(); Element.cache = { }; }; document.viewport = { getDimensions: function() { var dimensions = { }, B = Prototype.Browser; $w('width height').each(function(d) { var D = d.capitalize(); if (B.WebKit && !document.evaluate) { // Safari <3.0 needs self.innerWidth/Height dimensions[d] = self['inner' + D]; } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) { // Opera <9.5 needs document.body.clientWidth/Height dimensions[d] = document.body['client' + D] } else { dimensions[d] = document.documentElement['client' + D]; } }); return dimensions; }, getWidth: function() { return this.getDimensions().width; }, getHeight: function() { return this.getDimensions().height; }, getScrollOffsets: function() { return Element._returnOffset( window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; /* Portions of the Selector class are derived from Jack Slocum's DomQuery, * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style * license. Please see http://www.yui-ext.com/ for more information. */ var Selector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); if (this.shouldUseSelectorsAPI()) { this.mode = 'selectorsAPI'; } else if (this.shouldUseXPath()) { this.mode = 'xpath'; this.compileXPathMatcher(); } else { this.mode = "normal"; this.compileMatcher(); } }, shouldUseXPath: function() { if (!Prototype.BrowserFeatures.XPath) return false; var e = this.expression; // Safari 3 chokes on :*-of-type and :empty if (Prototype.Browser.WebKit && (e.include("-of-type") || e.include(":empty"))) return false; // XPath can't do namespaced attributes, nor can it read // the "checked" property from DOM nodes if ((/(\[[\w-]*?:|:checked)/).test(e)) return false; return true; }, shouldUseSelectorsAPI: function() { if (!Prototype.BrowserFeatures.SelectorsAPI) return false; if (!Selector._div) Selector._div = new Element('div'); // Make sure the browser treats the selector as valid. Test on an // isolated element to minimize cost of this check. try { Selector._div.querySelector(this.expression); } catch(e) { return false; } return true; }, compileMatcher: function() { var e = this.expression, ps = Selector.patterns, h = Selector.handlers, c = Selector.criteria, le, p, m; if (Selector._cache[e]) { this.matcher = Selector._cache[e]; return; } this.matcher = ["this.matcher = function(root) {", "var r = root, h = Selector.handlers, c = false, n;"]; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in ps) { p = ps[i]; if (m = e.match(p)) { this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : new Template(c[i]).evaluate(m)); e = e.replace(m[0], ''); break; } } } this.matcher.push("return h.unique(n);\n}"); eval(this.matcher.join('\n')); Selector._cache[this.expression] = this.matcher; }, compileXPathMatcher: function() { var e = this.expression, ps = Selector.patterns, x = Selector.xpath, le, m; if (Selector._cache[e]) { this.xpath = Selector._cache[e]; return; } this.matcher = ['.//*']; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in ps) { if (m = e.match(ps[i])) { this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m)); e = e.replace(m[0], ''); break; } } } this.xpath = this.matcher.join(''); Selector._cache[this.expression] = this.xpath; }, findElements: function(root) { root = root || document; var e = this.expression, results; switch (this.mode) { case 'selectorsAPI': // querySelectorAll queries document-wide, then filters to descendants // of the context element. That's not what we want. // Add an explicit context to the selector if necessary. if (root !== document) { var oldId = root.id, id = $(root).identify(); e = "#" + id + " " + e; } results = $A(root.querySelectorAll(e)).map(Element.extend); root.id = oldId; return results; case 'xpath': return document._getElementsByXPath(this.xpath, root); default: return this.matcher(root); } }, match: function(element) { this.tokens = []; var e = this.expression, ps = Selector.patterns, as = Selector.assertions; var le, p, m; while (e && le !== e && (/\S/).test(e)) { le = e; for (var i in ps) { p = ps[i]; if (m = e.match(p)) { // use the Selector.assertions methods unless the selector // is too complex. if (as[i]) { this.tokens.push([i, Object.clone(m)]); e = e.replace(m[0], ''); } else { // reluctantly do a document-wide search // and look for a match in the array return this.findElements(document).include(element); } } } } var match = true, name, matches; for (var i = 0, token; token = this.tokens[i]; i++) { name = token[0], matches = token[1]; if (!Selector.assertions[name](element, matches)) { match = false; break; } } return match; }, toString: function() { return this.expression; }, inspect: function() { return "#"; } }); Object.extend(Selector, { _cache: { }, xpath: { descendant: "//*", child: "/*", adjacent: "/following-sibling::*[1]", laterSibling: '/following-sibling::*', tagName: function(m) { if (m[1] == '*') return ''; return "[local-name()='" + m[1].toLowerCase() + "' or local-name()='" + m[1].toUpperCase() + "']"; }, className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", id: "[@id='#{1}']", attrPresence: function(m) { m[1] = m[1].toLowerCase(); return new Template("[@#{1}]").evaluate(m); }, attr: function(m) { m[1] = m[1].toLowerCase(); m[3] = m[5] || m[6]; return new Template(Selector.xpath.operators[m[2]]).evaluate(m); }, pseudo: function(m) { var h = Selector.xpath.pseudos[m[1]]; if (!h) return ''; if (Object.isFunction(h)) return h(m); return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); }, operators: { '=': "[@#{1}='#{3}']", '!=': "[@#{1}!='#{3}']", '^=': "[starts-with(@#{1}, '#{3}')]", '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", '*=': "[contains(@#{1}, '#{3}')]", '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" }, pseudos: { 'first-child': '[not(preceding-sibling::*)]', 'last-child': '[not(following-sibling::*)]', 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', 'empty': "[count(*) = 0 and (count(text()) = 0)]", 'checked': "[@checked]", 'disabled': "[(@disabled) and (@type!='hidden')]", 'enabled': "[not(@disabled) and (@type!='hidden')]", 'not': function(m) { var e = m[6], p = Selector.patterns, x = Selector.xpath, le, v; var exclusion = []; while (e && le != e && (/\S/).test(e)) { le = e; for (var i in p) { if (m = e.match(p[i])) { v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); exclusion.push("(" + v.substring(1, v.length - 1) + ")"); e = e.replace(m[0], ''); break; } } } return "[not(" + exclusion.join(" and ") + ")]"; }, 'nth-child': function(m) { return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); }, 'nth-last-child': function(m) { return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); }, 'nth-of-type': function(m) { return Selector.xpath.pseudos.nth("position() ", m); }, 'nth-last-of-type': function(m) { return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); }, 'first-of-type': function(m) { m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); }, 'last-of-type': function(m) { m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); }, 'only-of-type': function(m) { var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); }, nth: function(fragment, m) { var mm, formula = m[6], predicate; if (formula == 'even') formula = '2n+0'; if (formula == 'odd') formula = '2n+1'; if (mm = formula.match(/^(\d+)$/)) // digit only return '[' + fragment + "= " + mm[1] + ']'; if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b if (mm[1] == "-") mm[1] = -1; var a = mm[1] ? Number(mm[1]) : 1; var b = mm[2] ? Number(mm[2]) : 0; predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + "((#{fragment} - #{b}) div #{a} >= 0)]"; return new Template(predicate).evaluate({ fragment: fragment, a: a, b: b }); } } } }, criteria: { tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', className: 'n = h.className(n, r, "#{1}", c); c = false;', id: 'n = h.id(n, r, "#{1}", c); c = false;', attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', attr: function(m) { m[3] = (m[5] || m[6]); return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); }, pseudo: function(m) { if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); }, descendant: 'c = "descendant";', child: 'c = "child";', adjacent: 'c = "adjacent";', laterSibling: 'c = "laterSibling";' }, patterns: { // combinators must be listed first // (and descendant needs to be last combinator) laterSibling: /^\s*~\s*/, child: /^\s*>\s*/, adjacent: /^\s*\+\s*/, descendant: /^\s/, // selectors follow tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, id: /^#([\w\-\*]+)(\b|$)/, className: /^\.([\w\-\*]+)(\b|$)/, pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }, // for Selector.match and Element#match assertions: { tagName: function(element, matches) { return matches[1].toUpperCase() == element.tagName.toUpperCase(); }, className: function(element, matches) { return Element.hasClassName(element, matches[1]); }, id: function(element, matches) { return element.id === matches[1]; }, attrPresence: function(element, matches) { return Element.hasAttribute(element, matches[1]); }, attr: function(element, matches) { var nodeValue = Element.readAttribute(element, matches[1]); return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); } }, handlers: { // UTILITY FUNCTIONS // joins two collections concat: function(a, b) { for (var i = 0, node; node = b[i]; i++) a.push(node); return a; }, // marks an array of nodes for counting mark: function(nodes) { var _true = Prototype.emptyFunction; for (var i = 0, node; node = nodes[i]; i++) node._countedByPrototype = _true; return nodes; }, unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) node._countedByPrototype = undefined; return nodes; }, // mark each child node with its position (for nth calls) // "ofType" flag indicates whether we're indexing for nth-of-type // rather than nth-child index: function(parentNode, reverse, ofType) { parentNode._countedByPrototype = Prototype.emptyFunction; if (reverse) { for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { var node = nodes[i]; if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } } else { for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; } }, // filters out duplicates and extends all nodes unique: function(nodes) { if (nodes.length == 0) return nodes; var results = [], n; for (var i = 0, l = nodes.length; i < l; i++) if (!(n = nodes[i])._countedByPrototype) { n._countedByPrototype = Prototype.emptyFunction; results.push(Element.extend(n)); } return Selector.handlers.unmark(results); }, // COMBINATOR FUNCTIONS descendant: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, node.getElementsByTagName('*')); return results; }, child: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) { for (var j = 0, child; child = node.childNodes[j]; j++) if (child.nodeType == 1 && child.tagName != '!') results.push(child); } return results; }, adjacent: function(nodes) { for (var i = 0, results = [], node; node = nodes[i]; i++) { var next = this.nextElementSibling(node); if (next) results.push(next); } return results; }, laterSibling: function(nodes) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) h.concat(results, Element.nextSiblings(node)); return results; }, nextElementSibling: function(node) { while (node = node.nextSibling) if (node.nodeType == 1) return node; return null; }, previousElementSibling: function(node) { while (node = node.previousSibling) if (node.nodeType == 1) return node; return null; }, // TOKEN FUNCTIONS tagName: function(nodes, root, tagName, combinator) { var uTagName = tagName.toUpperCase(); var results = [], h = Selector.handlers; if (nodes) { if (combinator) { // fastlane for ordinary descendant combinators if (combinator == "descendant") { for (var i = 0, node; node = nodes[i]; i++) h.concat(results, node.getElementsByTagName(tagName)); return results; } else nodes = this[combinator](nodes); if (tagName == "*") return nodes; } for (var i = 0, node; node = nodes[i]; i++) if (node.tagName.toUpperCase() === uTagName) results.push(node); return results; } else return root.getElementsByTagName(tagName); }, id: function(nodes, root, id, combinator) { var targetNode = $(id), h = Selector.handlers; if (!targetNode) return []; if (!nodes && root == document) return [targetNode]; if (nodes) { if (combinator) { if (combinator == 'child') { for (var i = 0, node; node = nodes[i]; i++) if (targetNode.parentNode == node) return [targetNode]; } else if (combinator == 'descendant') { for (var i = 0, node; node = nodes[i]; i++) if (Element.descendantOf(targetNode, node)) return [targetNode]; } else if (combinator == 'adjacent') { for (var i = 0, node; node = nodes[i]; i++) if (Selector.handlers.previousElementSibling(targetNode) == node) return [targetNode]; } else nodes = h[combinator](nodes); } for (var i = 0, node; node = nodes[i]; i++) if (node == targetNode) return [targetNode]; return []; } return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; }, className: function(nodes, root, className, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); return Selector.handlers.byClassName(nodes, root, className); }, byClassName: function(nodes, root, className) { if (!nodes) nodes = Selector.handlers.descendant([root]); var needle = ' ' + className + ' '; for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { nodeClassName = node.className; if (nodeClassName.length == 0) continue; if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) results.push(node); } return results; }, attrPresence: function(nodes, root, attr, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); var results = []; for (var i = 0, node; node = nodes[i]; i++) if (Element.hasAttribute(node, attr)) results.push(node); return results; }, attr: function(nodes, root, attr, value, operator, combinator) { if (!nodes) nodes = root.getElementsByTagName("*"); if (nodes && combinator) nodes = this[combinator](nodes); var handler = Selector.operators[operator], results = []; for (var i = 0, node; node = nodes[i]; i++) { var nodeValue = Element.readAttribute(node, attr); if (nodeValue === null) continue; if (handler(nodeValue, value)) results.push(node); } return results; }, pseudo: function(nodes, name, value, root, combinator) { if (nodes && combinator) nodes = this[combinator](nodes); if (!nodes) nodes = root.getElementsByTagName("*"); return Selector.pseudos[name](nodes, value, root); } }, pseudos: { 'first-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { if (Selector.handlers.previousElementSibling(node)) continue; results.push(node); } return results; }, 'last-child': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { if (Selector.handlers.nextElementSibling(node)) continue; results.push(node); } return results; }, 'only-child': function(nodes, value, root) { var h = Selector.handlers; for (var i = 0, results = [], node; node = nodes[i]; i++) if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) results.push(node); return results; }, 'nth-child': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root); }, 'nth-last-child': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, true); }, 'nth-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, false, true); }, 'nth-last-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, formula, root, true, true); }, 'first-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, "1", root, false, true); }, 'last-of-type': function(nodes, formula, root) { return Selector.pseudos.nth(nodes, "1", root, true, true); }, 'only-of-type': function(nodes, formula, root) { var p = Selector.pseudos; return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); }, // handles the an+b logic getIndices: function(a, b, total) { if (a == 0) return b > 0 ? [b] : []; return $R(1, total).inject([], function(memo, i) { if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); return memo; }); }, // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type nth: function(nodes, formula, root, reverse, ofType) { if (nodes.length == 0) return []; if (formula == 'even') formula = '2n+0'; if (formula == 'odd') formula = '2n+1'; var h = Selector.handlers, results = [], indexed = [], m; h.mark(nodes); for (var i = 0, node; node = nodes[i]; i++) { if (!node.parentNode._countedByPrototype) { h.index(node.parentNode, reverse, ofType); indexed.push(node.parentNode); } } if (formula.match(/^\d+$/)) { // just a number formula = Number(formula); for (var i = 0, node; node = nodes[i]; i++) if (node.nodeIndex == formula) results.push(node); } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b if (m[1] == "-") m[1] = -1; var a = m[1] ? Number(m[1]) : 1; var b = m[2] ? Number(m[2]) : 0; var indices = Selector.pseudos.getIndices(a, b, nodes.length); for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { for (var j = 0; j < l; j++) if (node.nodeIndex == indices[j]) results.push(node); } } h.unmark(nodes); h.unmark(indexed); return results; }, 'empty': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { // IE treats comments as element nodes if (node.tagName == '!' || node.firstChild) continue; results.push(node); } return results; }, 'not': function(nodes, selector, root) { var h = Selector.handlers, selectorType, m; var exclusions = new Selector(selector).findElements(root); h.mark(exclusions); for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node._countedByPrototype) results.push(node); h.unmark(exclusions); return results; }, 'enabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (!node.disabled && (!node.type || node.type !== 'hidden')) results.push(node); return results; }, 'disabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.disabled) results.push(node); return results; }, 'checked': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) if (node.checked) results.push(node); return results; } }, operators: { '=': function(nv, v) { return nv == v; }, '!=': function(nv, v) { return nv != v; }, '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, '$=': function(nv, v) { return nv.endsWith(v); }, '*=': function(nv, v) { return nv.include(v); }, '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + '-').include('-' + (v || "").toUpperCase() + '-'); } }, split: function(expression) { var expressions = []; expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { expressions.push(m[1].strip()); }); return expressions; }, matchElements: function(elements, expression) { var matches = $$(expression), h = Selector.handlers; h.mark(matches); for (var i = 0, results = [], element; element = elements[i]; i++) if (element._countedByPrototype) results.push(element); h.unmark(matches); return results; }, findElement: function(elements, expression, index) { if (Object.isNumber(expression)) { index = expression; expression = false; } return Selector.matchElements(elements, expression || '*')[index || 0]; }, findChildElements: function(element, expressions) { expressions = Selector.split(expressions.join(',')); var results = [], h = Selector.handlers; for (var i = 0, l = expressions.length, selector; i < l; i++) { selector = new Selector(expressions[i].strip()); h.concat(results, selector.findElements(element)); } return (l > 1) ? h.unique(results) : results; } }); if (Prototype.Browser.IE) { Object.extend(Selector.handlers, { // IE returns comment nodes on getElementsByTagName("*"). // Filter them out. concat: function(a, b) { for (var i = 0, node; node = b[i]; i++) if (node.tagName !== "!") a.push(node); return a; }, // IE improperly serializes _countedByPrototype in (inner|outer)HTML. unmark: function(nodes) { for (var i = 0, node; node = nodes[i]; i++) node.removeAttribute('_countedByPrototype'); return nodes; } }); } function $$() { return Selector.findChildElements(document, $A(arguments)); } var Form = { reset: function(form) { $(form).reset(); return form; }, serializeElements: function(elements, options) { if (typeof options != 'object') options = { hash: !!options }; else if (Object.isUndefined(options.hash)) options.hash = true; var key, value, submitted = false, submit = options.submit; var data = elements.inject({ }, function(result, element) { if (!element.disabled && element.name) { key = element.name; value = $(element).getValue(); if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && submit !== false && (!submit || key == submit) && (submitted = true)))) { if (key in result) { // a key is already present; construct an array of values if (!Object.isArray(result[key])) result[key] = [result[key]]; result[key].push(value); } else result[key] = value; } } return result; }); return options.hash ? data : Object.toQueryString(data); } }; Form.Methods = { serialize: function(form, options) { return Form.serializeElements(Form.getElements(form), options); }, getElements: function(form) { return $A($(form).getElementsByTagName('*')).inject([], function(elements, child) { if (Form.Element.Serializers[child.tagName.toLowerCase()]) elements.push(Element.extend(child)); return elements; } ); }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); if (!typeName && !name) return $A(inputs).map(Element.extend); for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; if ((typeName && input.type != typeName) || (name && input.name != name)) continue; matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { form = $(form); Form.getElements(form).invoke('disable'); return form; }, enable: function(form) { form = $(form); Form.getElements(form).invoke('enable'); return form; }, findFirstElement: function(form) { var elements = $(form).getElements().findAll(function(element) { return 'hidden' != element.type && !element.disabled; }); var firstByIndex = elements.findAll(function(element) { return element.hasAttribute('tabIndex') && element.tabIndex >= 0; }).sortBy(function(element) { return element.tabIndex }).first(); return firstByIndex ? firstByIndex : elements.find(function(element) { return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, focusFirstElement: function(form) { form = $(form); form.findFirstElement().activate(); return form; }, request: function(form, options) { form = $(form), options = Object.clone(options || { }); var params = options.parameters, action = form.readAttribute('action') || ''; if (action.blank()) action = window.location.href; options.parameters = form.serialize(true); if (params) { if (Object.isString(params)) params = params.toQueryParams(); Object.extend(options.parameters, params); } if (form.hasAttribute('method') && !options.method) options.method = form.method; return new Ajax.Request(action, options); } }; /*--------------------------------------------------------------------------*/ Form.Element = { focus: function(element) { $(element).focus(); return element; }, select: function(element) { $(element).select(); return element; } }; Form.Element.Methods = { serialize: function(element) { element = $(element); if (!element.disabled && element.name) { var value = element.getValue(); if (value != undefined) { var pair = { }; pair[element.name] = value; return Object.toQueryString(pair); } } return ''; }, getValue: function(element) { element = $(element); var method = element.tagName.toLowerCase(); return Form.Element.Serializers[method](element); }, setValue: function(element, value) { element = $(element); var method = element.tagName.toLowerCase(); Form.Element.Serializers[method](element, value); return element; }, clear: function(element) { $(element).value = ''; return element; }, present: function(element) { return $(element).value != ''; }, activate: function(element) { element = $(element); try { element.focus(); if (element.select && (element.tagName.toLowerCase() != 'input' || !['button', 'reset', 'submit'].include(element.type))) element.select(); } catch (e) { } return element; }, disable: function(element) { element = $(element); element.disabled = true; return element; }, enable: function(element) { element = $(element); element.disabled = false; return element; } }; /*--------------------------------------------------------------------------*/ var Field = Form.Element; var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ Form.Element.Serializers = { input: function(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': return Form.Element.Serializers.inputSelector(element, value); default: return Form.Element.Serializers.textarea(element, value); } }, inputSelector: function(element, value) { if (Object.isUndefined(value)) return element.checked ? element.value : null; else element.checked = !!value; }, textarea: function(element, value) { if (Object.isUndefined(value)) return element.value; else element.value = value; }, select: function(element, value) { if (Object.isUndefined(value)) return this[element.type == 'select-one' ? 'selectOne' : 'selectMany'](element); else { var opt, currentValue, single = !Object.isArray(value); for (var i = 0, length = element.length; i < length; i++) { opt = element.options[i]; currentValue = this.optionValue(opt); if (single) { if (currentValue == value) { opt.selected = true; return; } } else opt.selected = value.include(currentValue); } } }, selectOne: function(element) { var index = element.selectedIndex; return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { var values, length = element.length; if (!length) return null; for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; if (opt.selected) values.push(this.optionValue(opt)); } return values; }, optionValue: function(opt) { // extend element because hasAttribute may not be native return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } }; /*--------------------------------------------------------------------------*/ Abstract.TimedObserver = Class.create(PeriodicalExecuter, { initialize: function($super, element, frequency, callback) { $super(callback, frequency); this.element = $(element); this.lastValue = this.getValue(); }, execute: function() { var value = this.getValue(); if (Object.isString(this.lastValue) && Object.isString(value) ? this.lastValue != value : String(this.lastValue) != String(value)) { this.callback(this.element, value); this.lastValue = value; } } }); Form.Element.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.serialize(this.element); } }); /*--------------------------------------------------------------------------*/ Abstract.EventObserver = Class.create({ initialize: function(element, callback) { this.element = $(element); this.callback = callback; this.lastValue = this.getValue(); if (this.element.tagName.toLowerCase() == 'form') this.registerFormCallbacks(); else this.registerCallback(this.element); }, onElementEvent: function() { var value = this.getValue(); if (this.lastValue != value) { this.callback(this.element, value); this.lastValue = value; } }, registerFormCallbacks: function() { Form.getElements(this.element).each(this.registerCallback, this); }, registerCallback: function(element) { if (element.type) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } } } }); Form.Element.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); Form.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.serialize(this.element); } }); if (!window.Event) var Event = { }; Object.extend(Event, { KEY_BACKSPACE: 8, KEY_TAB: 9, KEY_RETURN: 13, KEY_ESC: 27, KEY_LEFT: 37, KEY_UP: 38, KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, KEY_HOME: 36, KEY_END: 35, KEY_PAGEUP: 33, KEY_PAGEDOWN: 34, KEY_INSERT: 45, cache: { }, relatedTarget: function(event) { var element; switch(event.type) { case 'mouseover': element = event.fromElement; break; case 'mouseout': element = event.toElement; break; default: return null; } return Element.extend(element); } }); Event.Methods = (function() { var isButton; if (Prototype.Browser.IE) { var buttonMap = { 0: 1, 1: 4, 2: 2 }; isButton = function(event, code) { return event.button == buttonMap[code]; }; } else if (Prototype.Browser.WebKit) { isButton = function(event, code) { switch (code) { case 0: return event.which == 1 && !event.metaKey; case 1: return event.which == 1 && event.metaKey; default: return false; } }; } else { isButton = function(event, code) { return event.which ? (event.which === code + 1) : (event.button === code); }; } return { isLeftClick: function(event) { return isButton(event, 0) }, isMiddleClick: function(event) { return isButton(event, 1) }, isRightClick: function(event) { return isButton(event, 2) }, element: function(event) { event = Event.extend(event); var node = event.target, type = event.type, currentTarget = event.currentTarget; if (currentTarget && currentTarget.tagName) { // Firefox screws up the "click" event when moving between radio buttons // via arrow keys. It also screws up the "load" and "error" events on images, // reporting the document as the target instead of the original image. if (type === 'load' || type === 'error' || (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' && currentTarget.type === 'radio')) node = currentTarget; } if (node.nodeType == Node.TEXT_NODE) node = node.parentNode; return Element.extend(node); }, findElement: function(event, expression) { var element = Event.element(event); if (!expression) return element; var elements = [element].concat(element.ancestors()); return Selector.findElement(elements, expression, 0); }, pointer: function(event) { var docElement = document.documentElement, body = document.body || { scrollLeft: 0, scrollTop: 0 }; return { x: event.pageX || (event.clientX + (docElement.scrollLeft || body.scrollLeft) - (docElement.clientLeft || 0)), y: event.pageY || (event.clientY + (docElement.scrollTop || body.scrollTop) - (docElement.clientTop || 0)) }; }, pointerX: function(event) { return Event.pointer(event).x }, pointerY: function(event) { return Event.pointer(event).y }, stop: function(event) { Event.extend(event); event.preventDefault(); event.stopPropagation(); event.stopped = true; } }; })(); Event.extend = (function() { var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); if (Prototype.Browser.IE) { Object.extend(methods, { stopPropagation: function() { this.cancelBubble = true }, preventDefault: function() { this.returnValue = false }, inspect: function() { return "[object Event]" } }); return function(event) { if (!event) return false; if (event._extendedByPrototype) return event; event._extendedByPrototype = Prototype.emptyFunction; var pointer = Event.pointer(event); Object.extend(event, { target: event.srcElement, relatedTarget: Event.relatedTarget(event), pageX: pointer.x, pageY: pointer.y }); return Object.extend(event, methods); }; } else { Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__']; Object.extend(Event.prototype, methods); return Prototype.K; } })(); Object.extend(Event, (function() { var cache = Event.cache; function getEventID(element) { if (element._prototypeEventID) return element._prototypeEventID[0]; arguments.callee.id = arguments.callee.id || 1; return element._prototypeEventID = [++arguments.callee.id]; } function getDOMEventName(eventName) { if (eventName && eventName.include(':')) return "dataavailable"; return eventName; } function getCacheForID(id) { return cache[id] = cache[id] || { }; } function getWrappersForEventName(id, eventName) { var c = getCacheForID(id); return c[eventName] = c[eventName] || []; } function createWrapper(element, eventName, handler) { var id = getEventID(element); var c = getWrappersForEventName(id, eventName); if (c.pluck("handler").include(handler)) return false; var wrapper = function(event) { if (!Event || !Event.extend || (event.eventName && event.eventName != eventName)) return false; Event.extend(event); handler.call(element, event); }; wrapper.handler = handler; c.push(wrapper); return wrapper; } function findWrapper(id, eventName, handler) { var c = getWrappersForEventName(id, eventName); return c.find(function(wrapper) { return wrapper.handler == handler }); } function destroyWrapper(id, eventName, handler) { var c = getCacheForID(id); if (!c[eventName]) return false; c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); } function destroyCache() { for (var id in cache) for (var eventName in cache[id]) cache[id][eventName] = null; } // Internet Explorer needs to remove event handlers on page unload // in order to avoid memory leaks. if (window.attachEvent) { window.attachEvent("onunload", destroyCache); } // Safari has a dummy event handler on page unload so that it won't // use its bfcache. Safari <= 3.1 has an issue with restoring the "document" // object when page is returned to via the back button using its bfcache. if (Prototype.Browser.WebKit) { window.addEventListener('unload', Prototype.emptyFunction, false); } return { observe: function(element, eventName, handler) { element = $(element); var name = getDOMEventName(eventName); var wrapper = createWrapper(element, eventName, handler); if (!wrapper) return element; if (element.addEventListener) { element.addEventListener(name, wrapper, false); } else { element.attachEvent("on" + name, wrapper); } return element; }, stopObserving: function(element, eventName, handler) { element = $(element); var id = getEventID(element), name = getDOMEventName(eventName); if (!handler && eventName) { getWrappersForEventName(id, eventName).each(function(wrapper) { element.stopObserving(eventName, wrapper.handler); }); return element; } else if (!eventName) { Object.keys(getCacheForID(id)).each(function(eventName) { element.stopObserving(eventName); }); return element; } var wrapper = findWrapper(id, eventName, handler); if (!wrapper) return element; if (element.removeEventListener) { element.removeEventListener(name, wrapper, false); } else { element.detachEvent("on" + name, wrapper); } destroyWrapper(id, eventName, handler); return element; }, fire: function(element, eventName, memo) { element = $(element); if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; var event; if (document.createEvent) { event = document.createEvent("HTMLEvents"); event.initEvent("dataavailable", true, true); } else { event = document.createEventObject(); event.eventType = "ondataavailable"; } event.eventName = eventName; event.memo = memo || { }; if (document.createEvent) { element.dispatchEvent(event); } else { element.fireEvent(event.eventType, event); } return Event.extend(event); } }; })()); Object.extend(Event, Event.Methods); Element.addMethods({ fire: Event.fire, observe: Event.observe, stopObserving: Event.stopObserving }); Object.extend(document, { fire: Element.Methods.fire.methodize(), observe: Element.Methods.observe.methodize(), stopObserving: Element.Methods.stopObserving.methodize(), loaded: false }); (function() { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards and John Resig. */ var timer; function fireContentLoadedEvent() { if (document.loaded) return; if (timer) window.clearInterval(timer); document.fire("dom:loaded"); document.loaded = true; } if (document.addEventListener) { if (Prototype.Browser.WebKit) { timer = window.setInterval(function() { if (/loaded|complete/.test(document.readyState)) fireContentLoadedEvent(); }, 0); Event.observe(window, "load", fireContentLoadedEvent); } else { document.addEventListener("DOMContentLoaded", fireContentLoadedEvent, false); } } else { document.write("