From ae073103d1ef46c50a5d376ce996e20f7170a949 Mon Sep 17 00:00:00 2001 From: turly221 Date: Mon, 9 Dec 2024 18:14:02 +0000 Subject: [PATCH 1/6] commit patch 20727277 --- .../lib/action_controller/vendor/html-scanner/html/node.rb | 2 +- actionpack/test/template/html-scanner/sanitizer_test.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index 85250721e71f2..74c381b95c4fb 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -156,7 +156,7 @@ def parse(parent, line, pos, content, strict=true) end closing = ( scanner.scan(/\//) ? :close : nil ) - return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/) + return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/) name.downcase! unless closing diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb index 3e80317b307e8..889a0f7fd4c98 100644 --- a/actionpack/test/template/html-scanner/sanitizer_test.rb +++ b/actionpack/test/template/html-scanner/sanitizer_test.rb @@ -5,6 +5,13 @@ def setup @sanitizer = nil # used by assert_sanitizer end + def test_strip_tags_with_quote + sanitizer = HTML::FullSanitizer.new + string = '<" hi' + + assert_equal ' hi', sanitizer.sanitize(string) + end + def test_strip_tags sanitizer = HTML::FullSanitizer.new assert_equal("<< Date: Mon, 9 Dec 2024 18:14:04 +0000 Subject: [PATCH 2/6] commit patch 19123050 --- .../core_ext/string/output_safety.rb | 2 +- .../test/core_ext/string_ext_test.rb | 7 + .../test/core_ext/string_ext_test.rb.orig | 524 ++++++++++++++++++ 3 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 activesupport/test/core_ext/string_ext_test.rb.orig diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index f6c24023494ca..fe927b40f7a44 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -20,7 +20,7 @@ def html_escape(s) if s.html_safe? s else - s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }.html_safe + s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/ "[...]") + assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ') + assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ') + assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ') + end + + if RUBY_VERSION < '1.9.0' + def test_truncate_multibyte + with_kcode 'none' do + assert_equal "\354\225\210\353\205\225\355...", "\354\225\210\353\205\225\355\225\230\354\204\270\354\232\224".truncate(10) + end + with_kcode 'u' do + assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...", + "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".truncate(10) + end + end + else + def test_truncate_multibyte + assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('UTF-8'), + "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8').truncate(10) + end + end +end + +class StringBehaviourTest < Test::Unit::TestCase + def test_acts_like_string + assert 'Bambi'.acts_like_string? + end +end + +class CoreExtStringMultibyteTest < ActiveSupport::TestCase + UNICODE_STRING = 'こにちわ' + ASCII_STRING = 'ohayo' + BYTE_STRING = "\270\236\010\210\245" + + def test_core_ext_adds_mb_chars + assert_respond_to UNICODE_STRING, :mb_chars + end + + def test_string_should_recognize_utf8_strings + assert UNICODE_STRING.is_utf8? + assert ASCII_STRING.is_utf8? + assert !BYTE_STRING.is_utf8? + end + + if RUBY_VERSION < '1.9' + def test_mb_chars_returns_self_when_kcode_not_set + with_kcode('none') do + assert_kind_of String, UNICODE_STRING.mb_chars + end + end + + def test_mb_chars_returns_an_instance_of_the_chars_proxy_when_kcode_utf8 + with_kcode('UTF8') do + assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars + end + end + else + def test_mb_chars_returns_instance_of_proxy_class + assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars + end + end +end + +=begin + string.rb - Interpolation for String. + + Copyright (C) 2005-2009 Masao Mutoh + + You may redistribute it and/or modify it under the same + license terms as Ruby. +=end +class TestGetTextString < Test::Unit::TestCase + def test_sprintf + assert_equal("foo is a number", "%{msg} is a number" % {:msg => "foo"}) + assert_equal("bar is a number", "%s is a number" % ["bar"]) + assert_equal("bar is a number", "%s is a number" % "bar") + assert_equal("1, test", "%{num}, %{record}" % {:num => 1, :record => "test"}) + assert_equal("test, 1", "%{record}, %{num}" % {:num => 1, :record => "test"}) + assert_equal("1, test", "%d, %s" % [1, "test"]) + assert_equal("test, 1", "%2$s, %1$d" % [1, "test"]) + assert_raise(ArgumentError) { "%-%" % [1] } + end + + def test_percent + assert_equal("% 1", "%% %d" % {:num => 1.0}) + assert_equal("%{num} %d 1", "%%{num} %%d %d" % {:num => 1}) + end + + def test_sprintf_percent_in_replacement + assert_equal("%s", "%{msg}" % { :msg => '%s', :not_translated => 'should not happen' }) + end + + def test_sprintf_lack_argument + assert_raises(KeyError) { "%{num}, %{record}" % {:record => "test"} } + assert_raises(KeyError) { "%{record}" % {:num => 1} } + end + + def test_no_placeholder + # Causes a "too many arguments for format string" warning + # on 1.8.7 and 1.9 but we still want to make sure the behavior works + silence_warnings do + assert_equal("aaa", "aaa" % {:num => 1}) + assert_equal("bbb", "bbb" % [1]) + end + end + + def test_sprintf_ruby19_style + assert_equal("1", "%d" % {:num => 1}) + assert_equal("0b1", "%#b" % {:num => 1}) + assert_equal("foo", "%s" % {:msg => "foo"}) + assert_equal("1.000000", "%f" % {:num => 1.0}) + assert_equal(" 1", "%3.0f" % {:num => 1.0}) + assert_equal("100.00", "%2.2f" % {:num => 100.0}) + assert_equal("0x64", "%#x" % {:num => 100.0}) + assert_raise(ArgumentError) { "%,d" % {:num => 100} } + assert_raise(ArgumentError) { "%/d" % {:num => 100} } + end + + def test_sprintf_old_style + assert_equal("foo 1.000000", "%s %f" % ["foo", 1.0]) + end + + def test_sprintf_mix_unformatted_and_formatted_named_placeholders + assert_equal("foo 1.000000", "%{name} %f" % {:name => "foo", :num => 1.0}) + end + + def test_string_interpolation_raises_an_argument_error_when_mixing_named_and_unnamed_placeholders + assert_raises(ArgumentError) { "%{name} %f" % [1.0] } + assert_raises(ArgumentError) { "%{name} %f" % [1.0, 2.0] } + end +end + +class OutputSafetyTest < ActiveSupport::TestCase + def setup + @string = "hello" + @object = Class.new(Object) do + def to_s + "other" + end + end.new + end + + test "A string is unsafe by default" do + assert !@string.html_safe? + end + + test "A string can be marked safe" do + string = @string.html_safe + assert string.html_safe? + end + + test "Marking a string safe returns the string" do + assert_equal @string, @string.html_safe + end + + test "A fixnum is safe by default" do + assert 5.html_safe? + end + + test "An object is unsafe by default" do + assert !@object.html_safe? + end + + test "Adding an object to a safe string returns a safe string" do + string = @string.html_safe + string << @object + + assert_equal "helloother", string + assert string.html_safe? + end + + test "Adding a safe string to another safe string returns a safe string" do + @other_string = "other".html_safe + string = @string.html_safe + @combination = @other_string + string + + assert_equal "otherhello", @combination + assert @combination.html_safe? + end + + test "Adding an unsafe string to a safe string escapes it and returns a safe string" do + @other_string = "other".html_safe + @combination = @other_string + "" + @other_combination = @string + "" + + assert_equal "other<foo>", @combination + assert_equal "hello", @other_combination + + assert @combination.html_safe? + assert !@other_combination.html_safe? + end + + test "Concatting safe onto unsafe yields unsafe" do + @other_string = "other" + + string = @string.html_safe + @other_string.concat(string) + assert !@other_string.html_safe? + end + + test "Concatting unsafe onto safe yields escaped safe" do + @other_string = "other".html_safe + string = @other_string.concat("") + assert_equal "other<foo>", string + assert string.html_safe? + end + + test "Concatting safe onto safe yields safe" do + @other_string = "other".html_safe + string = @string.html_safe + + @other_string.concat(string) + assert @other_string.html_safe? + end + + test "Concatting safe onto unsafe with << yields unsafe" do + @other_string = "other" + string = @string.html_safe + + @other_string << string + assert !@other_string.html_safe? + end + + test "Concatting unsafe onto safe with << yields escaped safe" do + @other_string = "other".html_safe + string = @other_string << "" + assert_equal "other<foo>", string + assert string.html_safe? + end + + test "Concatting safe onto safe with << yields safe" do + @other_string = "other".html_safe + string = @string.html_safe + + @other_string << string + assert @other_string.html_safe? + end + + test "Concatting a fixnum to safe always yields safe" do + string = @string.html_safe + string = string.concat(13) + assert_equal "hello".concat(13), string + assert string.html_safe? + end + + test 'emits normal string yaml' do + assert_equal 'foo'.to_yaml, 'foo'.html_safe.to_yaml(:foo => 1) + end + + test 'knows whether it is encoding aware' do + if RUBY_VERSION >= "1.9" + assert 'ruby'.encoding_aware? + else + assert !'ruby'.encoding_aware? + end + end +end + +class StringExcludeTest < ActiveSupport::TestCase + test 'inverse of #include' do + assert_equal false, 'foo'.exclude?('o') + assert_equal true, 'foo'.exclude?('p') + end +end From 92cbd9f82b72c7d4c1b2481d7e911bb05f17360c Mon Sep 17 00:00:00 2001 From: turly221 Date: Mon, 9 Dec 2024 18:14:05 +0000 Subject: [PATCH 3/6] commit patch 24446354 --- .../lib/action_view/helpers/number_helper.rb | 2 +- .../action_view/helpers/number_helper.rb.orig | 494 ++++++++++++++++++ .../test/template/number_helper_test.rb | 7 +- .../test/template/number_helper_test.rb.orig | 377 +++++++++++++ 4 files changed, 876 insertions(+), 4 deletions(-) create mode 100644 actionpack/lib/action_view/helpers/number_helper.rb.orig create mode 100644 actionpack/test/template/number_helper_test.rb.orig diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index b23109c08e19b..aad0e733bfc5b 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -130,7 +130,7 @@ def number_to_currency(number, options = {}) begin value = number_with_precision(number, options.merge(:raise => true)) - format.gsub(/%n/, value).gsub(/%u/, unit).html_safe + format.gsub(/%n/, ERB::Util.html_escape(value)).gsub(/%u/, ERB::Util.html_escape(unit)).html_safe rescue InvalidNumberError => e if options[:raise] raise diff --git a/actionpack/lib/action_view/helpers/number_helper.rb.orig b/actionpack/lib/action_view/helpers/number_helper.rb.orig new file mode 100644 index 0000000000000..b23109c08e19b --- /dev/null +++ b/actionpack/lib/action_view/helpers/number_helper.rb.orig @@ -0,0 +1,494 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/core_ext/float/rounding' +require 'active_support/core_ext/object/blank' + +module ActionView + # = Action View Number Helpers + module Helpers #:nodoc: + + # Provides methods for converting numbers into formatted strings. + # Methods are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # Most methods expect a +number+ argument, and will return it + # unchanged if can't be converted into a valid number. + module NumberHelper + + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", + :precision => 2, :significant => false, :strip_insignificant_zeros => false } + + # Raised when argument +number+ param given to the helpers is invalid and + # the option :raise is set to +true+. + class InvalidNumberError < StandardError + attr_accessor :number + def initialize(number) + @number = number + end + end + + # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format + # in the +options+ hash. + # + # ==== Options + # * :area_code - Adds parentheses around the area code. + # * :delimiter - Specifies the delimiter to use (defaults to "-"). + # * :extension - Specifies an extension to add to the end of the + # generated number. + # * :country_code - Sets the country code for the phone number. + # + # ==== Examples + # number_to_phone(5551234) # => 555-1234 + # number_to_phone(1235551234) # => 123-555-1234 + # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 + # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 + # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 + # + # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") + # => +1.123.555.1234 x 1343 + def number_to_phone(number, options = {}) + return nil if number.nil? + + begin + Float(number) + is_number_html_safe = true + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + is_number_html_safe = number.to_s.html_safe? + end + end + + number = number.to_s.strip + options = options.symbolize_keys + area_code = options[:area_code] || nil + delimiter = options[:delimiter] || "-" + extension = options[:extension].to_s.strip || nil + country_code = options[:country_code] || nil + + str = "" + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << if area_code + number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3") + else + number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.starts_with?('-') ? number.slice!(1..-1) : number + end + str << " x #{extension}" unless extension.blank? + is_number_html_safe ? str.html_safe : str + end + + # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format + # in the +options+ hash. + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the level of precision (defaults to 2). + # * :unit - Sets the denomination of the currency (defaults to "$"). + # * :separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ","). + # * :format - Sets the format for non-negative numbers (defaults to "%u%n"). + # Fields are %u for the currency, and %n + # for the number. + # * :negative_format - Sets the format for negative numbers (defaults to prepending + # an hyphen to the formatted number given by :format). + # Accepts the same fields than :format, except + # %n is here the absolute value of the number. + # + # ==== Examples + # number_to_currency(1234567890.50) # => $1,234,567,890.50 + # number_to_currency(1234567890.506) # => $1,234,567,890.51 + # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 + # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € + # + # number_to_currency(1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.51) + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") + # # => 1234567890,50 £ + def number_to_currency(number, options = {}) + return nil if number.nil? + + options.symbolize_keys! + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) + + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] + options = defaults.merge!(options) + + unit = options.delete(:unit) + format = options.delete(:format) + + if number.to_f < 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + + begin + value = number_with_precision(number, options.merge(:raise => true)) + format.gsub(/%n/, value).gsub(/%u/, unit).html_safe + rescue InvalidNumberError => e + if options[:raise] + raise + else + formatted_number = format.gsub(/%n/, e.number).gsub(/%u/, unit) + e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number + end + end + + end + + # Formats a +number+ as a percentage string (e.g., 65%). You can customize the + # format in the +options+ hash. + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the precision of the number (defaults to 3). + # * :significant - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) + # * :separator - Sets the separator between the fractional and integer digits (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). + # * :strip_insignificant_zeros - If +true+ removes insignificant zeros after the decimal separator (defaults to +false+) + # + # ==== Examples + # number_to_percentage(100) # => 100.000% + # number_to_percentage(100, :precision => 0) # => 100% + # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% + # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% + # number_to_percentage(1000, :locale => :fr) # => 1 000,000% + def number_to_percentage(number, options = {}) + return nil if number.nil? + + options.symbolize_keys! + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :default => {}) + defaults = defaults.merge(percentage) + + options = options.reverse_merge(defaults) + + begin + "#{number_with_precision(number, options.merge(:raise => true))}%".html_safe + rescue InvalidNumberError => e + if options[:raise] + raise + else + e.number.to_s.html_safe? ? "#{e.number}%".html_safe : "#{e.number}%" + end + end + end + + # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You can + # customize the format in the +options+ hash. + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :delimiter - Sets the thousands delimiter (defaults to ","). + # * :separator - Sets the separator between the fractional and integer digits (defaults to "."). + # + # ==== Examples + # number_with_delimiter(12345678) # => 12,345,678 + # number_with_delimiter(12345678.05) # => 12,345,678.05 + # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678 + # number_with_delimiter(12345678, :separator => ",") # => 12,345,678 + # number_with_delimiter(12345678.05, :locale => :fr) # => 12 345 678,05 + # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",") + # # => 98 765 432,98 + def number_with_delimiter(number, options = {}) + options.symbolize_keys! + + begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + options = options.reverse_merge(defaults) + + parts = number.to_s.split('.') + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") + parts.join(options[:separator]).html_safe + + end + + # Formats a +number+ with the specified level of :precision (e.g., 112.32 has a precision + # of 2 if +:significant+ is +false+, and 5 if +:significant+ is +true+). + # You can customize the format in the +options+ hash. + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the precision of the number (defaults to 3). + # * :significant - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) + # * :separator - Sets the separator between the fractional and integer digits (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). + # * :strip_insignificant_zeros - If +true+ removes insignificant zeros after the decimal separator (defaults to +false+) + # + # ==== Examples + # number_with_precision(111.2345) # => 111.235 + # number_with_precision(111.2345, :precision => 2) # => 111.23 + # number_with_precision(13, :precision => 5) # => 13.00000 + # number_with_precision(389.32314, :precision => 0) # => 389 + # number_with_precision(111.2345, :significant => true) # => 111 + # number_with_precision(111.2345, :precision => 1, :significant => true) # => 100 + # number_with_precision(13, :precision => 5, :significant => true) # => 13.000 + # number_with_precision(111.234, :locale => :fr) # => 111,234 + # number_with_precision(13, :precision => 5, :significant => true, strip_insignificant_zeros => true) + # # => 13 + # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3 + # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') + # # => 1.111,23 + def number_with_precision(number, options = {}) + options.symbolize_keys! + + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], :default => {}) + defaults = defaults.merge(precision_defaults) + + options = options.reverse_merge(defaults) # Allow the user to unset default values: Eg.: :significant => false + precision = options.delete :precision + significant = options.delete :significant + strip_insignificant_zeros = options.delete :strip_insignificant_zeros + + if significant and precision > 0 + if number == 0 + digits, rounded_number = 1, 0 + else + digits = (Math.log10(number.abs) + 1).floor + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed + end + precision = precision - digits + precision = precision > 0 ? precision : 0 #don't let it be negative + else + rounded_number = BigDecimal.new(number.to_s).round(precision).to_f + end + formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '').html_safe + else + formatted_number + end + + end + + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze + + # Formats the bytes in +number+ into a more understandable representation + # (e.g., giving it 1500 yields 1.5 KB). This method is useful for + # reporting file sizes to users. You can customize the + # format in the +options+ hash. + # + # See number_to_human if you want to pretty-print a generic number. + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the precision of the number (defaults to 3). + # * :significant - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) + # * :separator - Sets the separator between the fractional and integer digits (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). + # * :strip_insignificant_zeros - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) + # ==== Examples + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.21 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.18 MB + # number_to_human_size(1234567890) # => 1.15 GB + # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567, :precision => 2) # => 1.2 MB + # number_to_human_size(483989, :precision => 2) # => 470 KB + # number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,2 MB + # + # Non-significant zeros after the fractional separator are stripped out by default (set + # :strip_insignificant_zeros to +false+ to change that): + # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" + # number_to_human_size(524288000, :precision=>5) # => "500 MB" + def number_to_human_size(number, options = {}) + options.symbolize_keys! + + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) + defaults = defaults.merge(human) + + options = options.reverse_merge(defaults) + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) + + if number.to_i < 1024 + unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) + storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit).html_safe + else + max_exp = STORAGE_UNITS.size - 1 + exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024 + exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit + number /= 1024 ** exponent + + unit_key = STORAGE_UNITS[exponent] + unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) + + formatted_number = number_with_precision(number, options) + storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).html_safe + end + end + + DECIMAL_UNITS = {0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, + -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto}.freeze + + # Pretty prints (formats and approximates) a number in a way it is more readable by humans + # (eg.: 1200000000 becomes "1.2 Billion"). This is useful for numbers that + # can get very large (and too hard to read). + # + # See number_to_human_size if you want to print a file size. + # + # You can also define you own unit-quantifier names if you want to use other decimal units + # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 mililiters", etc). You may define + # a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc). + # + # ==== Options + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the precision of the number (defaults to 3). + # * :significant - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +true+) + # * :separator - Sets the separator between the fractional and integer digits (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ""). + # * :strip_insignificant_zeros - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) + # * :units - A Hash of unit quantifier names. Or a string containing an i18n scope where to find this hash. It might have the following keys: + # * *integers*: :unit, :ten, :hundred, :thousand, :million, :billion, :trillion, :quadrillion + # * *fractionals*: :deci, :centi, :mili, :micro, :nano, :pico, :femto + # * :format - Sets the format of the output string (defaults to "%n %u"). The field types are: + # + # %u The quantifier (ex.: 'thousand') + # %n The number + # + # ==== Examples + # number_to_human(123) # => "123" + # number_to_human(1234) # => "1.23 Thousand" + # number_to_human(12345) # => "12.3 Thousand" + # number_to_human(1234567) # => "1.23 Million" + # number_to_human(1234567890) # => "1.23 Billion" + # number_to_human(1234567890123) # => "1.23 Trillion" + # number_to_human(1234567890123456) # => "1.23 Quadrillion" + # number_to_human(1234567890123456789) # => "1230 Quadrillion" + # number_to_human(489939, :precision => 2) # => "490 Thousand" + # number_to_human(489939, :precision => 4) # => "489.9 Thousand" + # number_to_human(1234567, :precision => 4, + # :significant => false) # => "1.2346 Million" + # number_to_human(1234567, :precision => 1, + # :separator => ',', + # :significant => false) # => "1,2 Million" + # + # Unsignificant zeros after the decimal separator are stripped out by default (set + # :strip_insignificant_zeros to +false+ to change that): + # number_to_human(12345012345, :significant_digits => 6) # => "12.345 Billion" + # number_to_human(500000000, :precision=>5) # => "500 Million" + # + # ==== Custom Unit Quantifiers + # + # You can also use your own custom unit quantifiers: + # number_to_human(500000, :units => {:unit => "ml", :thousand => "lt"}) # => "500 lt" + # + # If in your I18n locale you have: + # distance: + # centi: + # one: "centimeter" + # other: "centimeters" + # unit: + # one: "meter" + # other: "meters" + # thousand: + # one: "kilometer" + # other: "kilometers" + # billion: "gazilion-distance" + # + # Then you could do: + # + # number_to_human(543934, :units => :distance) # => "544 kilometers" + # number_to_human(54393498, :units => :distance) # => "54400 kilometers" + # number_to_human(54393498000, :units => :distance) # => "54.4 gazilion-distance" + # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters" + # number_to_human(1, :units => :distance) # => "1 meter" + # number_to_human(0.34, :units => :distance) # => "34 centimeters" + # + def number_to_human(number, options = {}) + options.symbolize_keys! + + number = begin + Float(number) + rescue ArgumentError, TypeError + if options[:raise] + raise InvalidNumberError, number + else + return number + end + end + + defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) + human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) + defaults = defaults.merge(human) + + options = options.reverse_merge(defaults) + #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + + units = options.delete :units + unit_exponents = case units + when Hash + units + when String, Symbol + I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) + when nil + I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map{|e_name| DECIMAL_UNITS.invert[e_name] }.sort_by{|e| -e} + + number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 + display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 + number /= 10 ** display_exponent + + unit = case units + when Hash + units[DECIMAL_UNITS[display_exponent]] + when String, Symbol + I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + else + I18n.translate(:"number.human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) + end + + decimal_format = options[:format] || I18n.translate(:'number.human.decimal_units.format', :locale => options[:locale], :default => "%n %u") + formatted_number = number_with_precision(number, options) + decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe + end + + end + end +end diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb index a676fbf5748db..dd1d5c2d82c4c 100644 --- a/actionpack/test/template/number_helper_test.rb +++ b/actionpack/test/template/number_helper_test.rb @@ -50,10 +50,11 @@ def test_number_to_currency assert_equal("($1,234,567,890.50)", number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0})) assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1})) - assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) + assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => raw("£"), :separator => ",", :delimiter => ""})) + assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) - assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) - assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => raw("Kč"), :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => raw("Kč"), :format => "%n %u", :negative_format => "%n - %u"})) end def test_number_to_percentage diff --git a/actionpack/test/template/number_helper_test.rb.orig b/actionpack/test/template/number_helper_test.rb.orig new file mode 100644 index 0000000000000..a676fbf5748db --- /dev/null +++ b/actionpack/test/template/number_helper_test.rb.orig @@ -0,0 +1,377 @@ +require 'abstract_unit' + +class NumberHelperTest < ActionView::TestCase + tests ActionView::Helpers::NumberHelper + + def kilobytes(number) + number * 1024 + end + + def megabytes(number) + kilobytes(number) * 1024 + end + + def gigabytes(number) + megabytes(number) * 1024 + end + + def terabytes(number) + gigabytes(number) * 1024 + end + + def silence_deprecation_warnings + @old_deprecatios_silenced = ActiveSupport::Deprecation.silenced + ActiveSupport::Deprecation.silenced = true + end + + def restore_deprecation_warnings + ActiveSupport::Deprecation.silenced = @old_deprecatios_silenced + end + + def test_number_to_phone + assert_equal("555-1234", number_to_phone(5551234)) + assert_equal("800-555-1212", number_to_phone(8005551212)) + assert_equal("(800) 555-1212", number_to_phone(8005551212, {:area_code => true})) + assert_equal("800 555 1212", number_to_phone(8005551212, {:delimiter => " "})) + assert_equal("(800) 555-1212 x 123", number_to_phone(8005551212, {:area_code => true, :extension => 123})) + assert_equal("800-555-1212", number_to_phone(8005551212, :extension => " ")) + assert_equal("800-555-1212", number_to_phone("8005551212")) + assert_equal("+1-800-555-1212", number_to_phone(8005551212, :country_code => 1)) + assert_equal("+18005551212", number_to_phone(8005551212, :country_code => 1, :delimiter => '')) + assert_equal("22-555-1212", number_to_phone(225551212)) + assert_equal("+45-22-555-1212", number_to_phone(225551212, :country_code => 45)) + end + + def test_number_to_currency + assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50)) + assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506)) + assert_equal("-$1,234,567,890.50", number_to_currency(-1234567890.50)) + assert_equal("-$ 1,234,567,890.50", number_to_currency(-1234567890.50, {:format => "%u %n"})) + assert_equal("($1,234,567,890.50)", number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"})) + assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0})) + assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1})) + assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""})) + assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50")) + assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) + assert_equal("1,234,567,890.50 - Kč", number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) + end + + def test_number_to_percentage + assert_equal("100.000%", number_to_percentage(100)) + assert_equal("100%", number_to_percentage(100, {:precision => 0})) + assert_equal("302.06%", number_to_percentage(302.0574, {:precision => 2})) + assert_equal("100.000%", number_to_percentage("100")) + assert_equal("1000.000%", number_to_percentage("1000")) + assert_equal("123.4%", number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) + assert_equal("1.000,000%", number_to_percentage(1000, :delimiter => '.', :separator => ',')) + end + + def test_number_with_delimiter + assert_equal("12,345,678", number_with_delimiter(12345678)) + assert_equal("0", number_with_delimiter(0)) + assert_equal("123", number_with_delimiter(123)) + assert_equal("123,456", number_with_delimiter(123456)) + assert_equal("123,456.78", number_with_delimiter(123456.78)) + assert_equal("123,456.789", number_with_delimiter(123456.789)) + assert_equal("123,456.78901", number_with_delimiter(123456.78901)) + assert_equal("123,456,789.78901", number_with_delimiter(123456789.78901)) + assert_equal("0.78901", number_with_delimiter(0.78901)) + assert_equal("123,456.78", number_with_delimiter("123456.78")) + end + + def test_number_with_delimiter_with_options_hash + assert_equal '12 345 678', number_with_delimiter(12345678, :delimiter => ' ') + assert_equal '12,345,678-05', number_with_delimiter(12345678.05, :separator => '-') + assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :separator => ',', :delimiter => '.') + assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :delimiter => '.', :separator => ',') + end + + def test_number_with_precision + assert_equal("-111.235", number_with_precision(-111.2346)) + assert_equal("111.235", number_with_precision(111.2346)) + assert_equal("31.83", number_with_precision(31.825, :precision => 2)) + assert_equal("111.23", number_with_precision(111.2346, :precision => 2)) + assert_equal("111.00", number_with_precision(111, :precision => 2)) + assert_equal("111.235", number_with_precision("111.2346")) + assert_equal("31.83", number_with_precision("31.825", :precision => 2)) + assert_equal("3268", number_with_precision((32.6751 * 100.00), :precision => 0)) + assert_equal("112", number_with_precision(111.50, :precision => 0)) + assert_equal("1234567892", number_with_precision(1234567891.50, :precision => 0)) + assert_equal("0", number_with_precision(0, :precision => 0)) + assert_equal("0.00100", number_with_precision(0.001, :precision => 5)) + assert_equal("0.001", number_with_precision(0.00111, :precision => 3)) + assert_equal("10.00", number_with_precision(9.995, :precision => 2)) + assert_equal("11.00", number_with_precision(10.995, :precision => 2)) + end + + def test_number_with_precision_with_custom_delimiter_and_separator + assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',') + assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.') + end + + def test_number_with_precision_with_significant_digits + assert_equal "124000", number_with_precision(123987, :precision => 3, :significant => true) + assert_equal "120000000", number_with_precision(123987876, :precision => 2, :significant => true ) + assert_equal "40000", number_with_precision("43523", :precision => 1, :significant => true ) + assert_equal "9775", number_with_precision(9775, :precision => 4, :significant => true ) + assert_equal "5.4", number_with_precision(5.3923, :precision => 2, :significant => true ) + assert_equal "5", number_with_precision(5.3923, :precision => 1, :significant => true ) + assert_equal "1", number_with_precision(1.232, :precision => 1, :significant => true ) + assert_equal "7", number_with_precision(7, :precision => 1, :significant => true ) + assert_equal "1", number_with_precision(1, :precision => 1, :significant => true ) + assert_equal "53", number_with_precision(52.7923, :precision => 2, :significant => true ) + assert_equal "9775.00", number_with_precision(9775, :precision => 6, :significant => true ) + assert_equal "5.392900", number_with_precision(5.3929, :precision => 7, :significant => true ) + assert_equal "0.0", number_with_precision(0, :precision => 2, :significant => true ) + assert_equal "0", number_with_precision(0, :precision => 1, :significant => true ) + assert_equal "0.0001", number_with_precision(0.0001, :precision => 1, :significant => true ) + assert_equal "0.000100", number_with_precision(0.0001, :precision => 3, :significant => true ) + assert_equal "0.0001", number_with_precision(0.0001111, :precision => 1, :significant => true ) + assert_equal "10.0", number_with_precision(9.995, :precision => 3, :significant => true) + assert_equal "9.99", number_with_precision(9.994, :precision => 3, :significant => true) + assert_equal "11.0", number_with_precision(10.995, :precision => 3, :significant => true) + end + + def test_number_with_precision_with_strip_insignificant_zeros + assert_equal "9775.43", number_with_precision(9775.43, :precision => 4, :strip_insignificant_zeros => true ) + assert_equal "9775.2", number_with_precision(9775.2, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + assert_equal "0", number_with_precision(0, :precision => 6, :significant => true, :strip_insignificant_zeros => true ) + end + + def test_number_with_precision_with_significant_true_and_zero_precision + # Zero precision with significant is a mistake (would always return zero), + # so we treat it as if significant was false (increases backwards compatibily for number_to_human_size) + assert_equal "124", number_with_precision(123.987, :precision => 0, :significant => true) + assert_equal "12", number_with_precision(12, :precision => 0, :significant => true ) + assert_equal "12", number_with_precision("12.3", :precision => 0, :significant => true ) + end + + def test_number_to_human_size + assert_equal '0 Bytes', number_to_human_size(0) + assert_equal '1 Byte', number_to_human_size(1) + assert_equal '3 Bytes', number_to_human_size(3.14159265) + assert_equal '123 Bytes', number_to_human_size(123.0) + assert_equal '123 Bytes', number_to_human_size(123) + assert_equal '1.21 KB', number_to_human_size(1234) + assert_equal '12.1 KB', number_to_human_size(12345) + assert_equal '1.18 MB', number_to_human_size(1234567) + assert_equal '1.15 GB', number_to_human_size(1234567890) + assert_equal '1.12 TB', number_to_human_size(1234567890123) + assert_equal '1030 TB', number_to_human_size(terabytes(1026)) + assert_equal '444 KB', number_to_human_size(kilobytes(444)) + assert_equal '1020 MB', number_to_human_size(megabytes(1023)) + assert_equal '3 TB', number_to_human_size(terabytes(3)) + assert_equal '1.2 MB', number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4) + assert_equal '123 Bytes', number_to_human_size('123') + assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 Byte', number_to_human_size(1.1) + assert_equal '10 Bytes', number_to_human_size(10) + end + + def test_number_to_human_size_with_options_hash + assert_equal '1.2 MB', number_to_human_size(1234567, :precision => 2) + assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4) + assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 2) + assert_equal '1.01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4) + assert_equal '10 KB', number_to_human_size(kilobytes(10.000), :precision => 4) + assert_equal '1 TB', number_to_human_size(1234567890123, :precision => 1) + assert_equal '500 MB', number_to_human_size(524288000, :precision=>3) + assert_equal '10 MB', number_to_human_size(9961472, :precision=>0) + assert_equal '40 KB', number_to_human_size(41010, :precision => 1) + assert_equal '40 KB', number_to_human_size(41100, :precision => 2) + assert_equal '1.0 KB', number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false) + assert_equal '1.012 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false) + assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0 + end + + def test_number_to_human_size_with_custom_delimiter_and_separator + assert_equal '1,01 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :separator => ',') + assert_equal '1,01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4, :separator => ',') + assert_equal '1.000,1 TB', number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',') + end + + def test_number_to_human + assert_equal '-123', number_to_human(-123) + assert_equal '-0.5', number_to_human(-0.5) + assert_equal '0', number_to_human(0) + assert_equal '0.5', number_to_human(0.5) + assert_equal '123', number_to_human(123) + assert_equal '1.23 Thousand', number_to_human(1234) + assert_equal '12.3 Thousand', number_to_human(12345) + assert_equal '1.23 Million', number_to_human(1234567) + assert_equal '1.23 Billion', number_to_human(1234567890) + assert_equal '1.23 Trillion', number_to_human(1234567890123) + assert_equal '1.23 Quadrillion', number_to_human(1234567890123456) + assert_equal '1230 Quadrillion', number_to_human(1234567890123456789) + assert_equal '490 Thousand', number_to_human(489939, :precision => 2) + assert_equal '489.9 Thousand', number_to_human(489939, :precision => 4) + assert_equal '489 Thousand', number_to_human(489000, :precision => 4) + assert_equal '489.0 Thousand', number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false) + assert_equal '1.2346 Million', number_to_human(1234567, :precision => 4, :significant => false) + assert_equal '1,2 Million', number_to_human(1234567, :precision => 1, :significant => false, :separator => ',') + assert_equal '1 Million', number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false + end + + def test_number_to_human_with_custom_units + #Only integers + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123 lt', number_to_human(123456, :units => volume) + assert_equal '12 ml', number_to_human(12, :units => volume) + assert_equal '1.23 m3', number_to_human(1234567, :units => volume) + + #Including fractionals + distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} + assert_equal '1.23 mm', number_to_human(0.00123, :units => distance) + assert_equal '1.23 cm', number_to_human(0.0123, :units => distance) + assert_equal '1.23 dm', number_to_human(0.123, :units => distance) + assert_equal '1.23 m', number_to_human(1.23, :units => distance) + assert_equal '1.23 dam', number_to_human(12.3, :units => distance) + assert_equal '1.23 hm', number_to_human(123, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '1.23 km', number_to_human(1230, :units => distance) + assert_equal '12.3 km', number_to_human(12300, :units => distance) + + #The quantifiers don't need to be a continuous sequence + gangster = {:hundred => "hundred bucks", :million => "thousand quids"} + assert_equal '1 hundred bucks', number_to_human(100, :units => gangster) + assert_equal '25 hundred bucks', number_to_human(2500, :units => gangster) + assert_equal '25 thousand quids', number_to_human(25000000, :units => gangster) + assert_equal '12300 thousand quids', number_to_human(12345000000, :units => gangster) + + #Spaces are stripped from the resulting string + assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '}) + assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '}) + end + + def test_number_to_human_with_custom_format + assert_equal '123 times Thousand', number_to_human(123456, :format => "%n times %u") + volume = {:unit => "ml", :thousand => "lt", :million => "m3"} + assert_equal '123.lt', number_to_human(123456, :units => volume, :format => "%n.%u") + end + + def test_number_helpers_should_return_nil_when_given_nil + assert_nil number_to_phone(nil) + assert_nil number_to_currency(nil) + assert_nil number_to_percentage(nil) + assert_nil number_with_delimiter(nil) + assert_nil number_with_precision(nil) + assert_nil number_to_human_size(nil) + assert_nil number_to_human(nil) + end + + def test_number_helpers_should_return_non_numeric_param_unchanged + assert_equal("+1-x x 123", number_to_phone("x", :country_code => 1, :extension => 123)) + assert_equal("x", number_to_phone("x")) + assert_equal("$x.", number_to_currency("x.")) + assert_equal("$x", number_to_currency("x")) + assert_equal("x%", number_to_percentage("x")) + assert_equal("x", number_with_delimiter("x")) + assert_equal("x.", number_with_precision("x.")) + assert_equal("x", number_with_precision("x")) + assert_equal "x", number_to_human_size('x') + assert_equal "x", number_to_human('x') + end + + def test_number_helpers_outputs_are_html_safe + assert number_to_human(1).html_safe? + assert !number_to_human("").html_safe? + assert number_to_human("asdf".html_safe).html_safe? + + assert number_to_human_size(1).html_safe? + assert number_to_human_size(1000000).html_safe? + assert !number_to_human_size("").html_safe? + assert number_to_human_size("asdf".html_safe).html_safe? + + assert number_with_precision(1, :strip_insignificant_zeros => false).html_safe? + assert number_with_precision(1, :strip_insignificant_zeros => true).html_safe? + assert !number_with_precision("").html_safe? + assert number_with_precision("asdf".html_safe).html_safe? + + assert number_to_currency(1).html_safe? + assert !number_to_currency("").html_safe? + assert number_to_currency("asdf".html_safe).html_safe? + + assert number_to_percentage(1).html_safe? + assert !number_to_percentage("").html_safe? + assert number_to_percentage("asdf".html_safe).html_safe? + + assert number_to_phone(1).html_safe? + assert !number_to_phone("").html_safe? + assert number_to_phone("asdf".html_safe).html_safe? + + assert number_with_delimiter(1).html_safe? + assert !number_with_delimiter("").html_safe? + assert number_with_delimiter("asdf".html_safe).html_safe? + end + + def test_number_helpers_should_raise_error_if_invalid_when_specified + assert_raise InvalidNumberError do + number_to_human("x", :raise => true) + end + begin + number_to_human("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_to_human_size("x", :raise => true) + end + begin + number_to_human_size("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_with_precision("x", :raise => true) + end + begin + number_with_precision("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_to_currency("x", :raise => true) + end + begin + number_with_precision("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_to_percentage("x", :raise => true) + end + begin + number_to_percentage("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_with_delimiter("x", :raise => true) + end + begin + number_with_delimiter("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + assert_raise InvalidNumberError do + number_to_phone("x", :raise => true) + end + begin + number_to_phone("x", :raise => true) + rescue InvalidNumberError => e + assert_equal "x", e.number + end + + end + +end From 1ce9776a6c3e1adae0e9cb86abbbcf04e3d8f32d Mon Sep 17 00:00:00 2001 From: turly221 Date: Mon, 9 Dec 2024 18:14:07 +0000 Subject: [PATCH 4/6] commit patch 23410749 --- actionpack/lib/action_view/template/text.rb | 2 +- .../lib/action_view/template/text.rb.orig | 34 +++++++++++++++++++ actionpack/test/template/text_test.rb | 17 ++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 actionpack/lib/action_view/template/text.rb.orig create mode 100644 actionpack/test/template/text_test.rb diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index 51be831dfbfc9..12c9ed968d4fb 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -23,7 +23,7 @@ def render(*args) end def formats - [@mime_type.to_sym] + [@mime_type.respond_to?(:ref) ? @mime_type.ref : @mime_type.to_s] end def partial? diff --git a/actionpack/lib/action_view/template/text.rb.orig b/actionpack/lib/action_view/template/text.rb.orig new file mode 100644 index 0000000000000..51be831dfbfc9 --- /dev/null +++ b/actionpack/lib/action_view/template/text.rb.orig @@ -0,0 +1,34 @@ +module ActionView #:nodoc: + # = Action View Text Template + class Template + class Text < String #:nodoc: + attr_accessor :mime_type + + def initialize(string, mime_type = nil) + super(string.to_s) + @mime_type = Mime[mime_type] || mime_type if mime_type + @mime_type ||= Mime::TEXT + end + + def identifier + 'text template' + end + + def inspect + 'text template' + end + + def render(*args) + to_s + end + + def formats + [@mime_type.to_sym] + end + + def partial? + false + end + end + end +end diff --git a/actionpack/test/template/text_test.rb b/actionpack/test/template/text_test.rb new file mode 100644 index 0000000000000..d899d54589482 --- /dev/null +++ b/actionpack/test/template/text_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' + +class TextTest < ActiveSupport::TestCase + test 'formats returns symbol for recognized MIME type' do + assert_equal [:text], ActionView::Template::Text.new('', :text).formats + end + + test 'formats returns string for recognized MIME type when MIME does not have symbol' do + foo = Mime::Type.lookup("foo") + assert_nil foo.to_sym + assert_equal ['foo'], ActionView::Template::Text.new('', foo).formats + end + + test 'formats returns string for unknown MIME type' do + assert_equal ['foo'], ActionView::Template::Text.new('', 'foo').formats + end +end From 02e7f188b1d423a281d7cf5fad0af575b10fb841 Mon Sep 17 00:00:00 2001 From: turly221 Date: Mon, 9 Dec 2024 18:14:09 +0000 Subject: [PATCH 5/6] commit patch 17602150 --- .../lib/action_view/helpers/tag_helper.rb | 1 + .../action_view/helpers/tag_helper.rb.orig | 134 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 actionpack/lib/action_view/helpers/tag_helper.rb.orig diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 5d032b32a7f41..5eb16c760f80d 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -11,6 +11,7 @@ module TagHelper extend ActiveSupport::Concern include CaptureHelper + include OutputSafetyHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb.orig b/actionpack/lib/action_view/helpers/tag_helper.rb.orig new file mode 100644 index 0000000000000..5d032b32a7f41 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tag_helper.rb.orig @@ -0,0 +1,134 @@ +require 'active_support/core_ext/object/blank' +require 'set' + +module ActionView + # = Action View Tag Helpers + module Helpers #:nodoc: + # Provides methods to generate HTML tags programmatically when you can't use + # a Builder. By default, they output XHTML compliant tags. + module TagHelper + include ERB::Util + + extend ActiveSupport::Concern + include CaptureHelper + + BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer + autoplay controls loop selected hidden scoped async + defer reversed ismap seemless muted required + autofocus novalidate formnovalidate open).to_set + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attr| attr.to_sym }) + + # Returns an empty HTML tag of type +name+ which by default is XHTML + # compliant. Set +open+ to true to create an open tag compatible + # with HTML 4.0 and below. Add HTML attributes by passing an attributes + # hash to +options+. Set +escape+ to false to disable attribute value + # escaping. + # + # ==== Options + # The +options+ hash is used with attributes with no value like (disabled and + # readonly), which you can give a value of true in the +options+ hash. You can use + # symbols or strings for the attribute names. + # + # ==== Examples + # tag("br") + # # =>
+ # + # tag("br", nil, true) + # # =>
+ # + # tag("input", { :type => 'text', :disabled => true }) + # # => + # + # tag("img", { :src => "open & shut.png" }) + # # => + # + # tag("img", { :src => "open & shut.png" }, false, false) + # # => + def tag(name, options = nil, open = false, escape = true) + "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + end + + # Returns an HTML block tag of type +name+ surrounding the +content+. Add + # HTML attributes by passing an attributes hash to +options+. + # Instead of passing the content as an argument, you can also use a block + # in which case, you pass your +options+ as the second parameter. + # Set escape to false to disable attribute value escaping. + # + # ==== Options + # The +options+ hash is used with attributes with no value like (disabled and + # readonly), which you can give a value of true in the +options+ hash. You can use + # symbols or strings for the attribute names. + # + # ==== Examples + # content_tag(:p, "Hello world!") + # # =>

Hello world!

+ # content_tag(:div, content_tag(:p, "Hello world!"), :class => "strong") + # # =>

Hello world!

+ # content_tag("select", options, :multiple => true) + # # => + # + # <%= content_tag :div, :class => "strong" do -%> + # Hello world! + # <% end -%> + # # =>
Hello world!
+ def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) + if block_given? + options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + content_tag_string(name, capture(&block), options, escape) + else + content_tag_string(name, content_or_options_with_block, options, escape) + end + end + + # Returns a CDATA section with the given +content+. CDATA sections + # are used to escape blocks of text containing characters which would + # otherwise be recognized as markup. CDATA sections begin with the string + # and end with (and may not contain) the string ]]>. + # + # ==== Examples + # cdata_section("") + # # => ]]> + # + # cdata_section(File.read("hello_world.txt")) + # # => + def cdata_section(content) + "".html_safe + end + + # Returns an escaped version of +html+ without affecting existing escaped entities. + # + # ==== Examples + # escape_once("1 < 2 & 3") + # # => "1 < 2 & 3" + # + # escape_once("<< Accept & Checkout") + # # => "<< Accept & Checkout" + def escape_once(html) + ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } + end + + private + + def content_tag_string(name, content, options, escape = true) + tag_options = tag_options(options, escape) if options + "<#{name}#{tag_options}>#{escape ? ERB::Util.h(content) : content}".html_safe + end + + def tag_options(options, escape = true) + unless options.blank? + attrs = [] + options.each_pair do |key, value| + if BOOLEAN_ATTRIBUTES.include?(key) + attrs << %(#{key}="#{key}") if value + elsif !value.nil? + final_value = value.is_a?(Array) ? value.join(" ") : value + final_value = html_escape(final_value) if escape + attrs << %(#{key}="#{final_value}") + end + end + " #{attrs.sort * ' '}".html_safe unless attrs.empty? + end + end + end + end +end From 470eaf9df10d93e34239c7d5fa70038559b791d4 Mon Sep 17 00:00:00 2001 From: turly221 Date: Mon, 9 Dec 2024 18:14:11 +0000 Subject: [PATCH 6/6] commit patch 19987991 --- .../lib/action_dispatch/http/mime_type.rb | 18 +- .../action_dispatch/http/mime_type.rb.orig | 230 ++++++++++++++++++ 2 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 actionpack/lib/action_dispatch/http/mime_type.rb.orig diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index ea7adf3ef3888..020432fa8fe5b 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -22,7 +22,7 @@ def #{method}(*) SET = Mimes.new EXTENSION_LOOKUP = {} - LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = {} def self.[](type) return type if type.is_a?(Type) @@ -81,7 +81,7 @@ def ==(item) class << self def lookup(string) - LOOKUP[string] + LOOKUP[string] || Type.new(string) end def lookup_by_extension(extension) @@ -162,9 +162,12 @@ def parse(accept_header) end end + attr_reader :hash + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string + @hash = [@string, @synonyms, @symbol].hash end def to_s @@ -198,6 +201,13 @@ def ==(mime_type) end end + def eql?(other) + super || (self.class == other.class && + @string == other.string && + @synonyms == other.synonyms && + @symbol == other.symbol) + end + def =~(mime_type) return false if mime_type.blank? regexp = Regexp.new(Regexp.quote(mime_type.to_s)) @@ -216,6 +226,10 @@ def html? @@html_types.include?(to_sym) || @string =~ /html/ end + protected + + attr_reader :string, :synonyms + private def method_missing(method, *args) if method.to_s =~ /(\w+)\?$/ diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb.orig b/actionpack/lib/action_dispatch/http/mime_type.rb.orig new file mode 100644 index 0000000000000..ea7adf3ef3888 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/mime_type.rb.orig @@ -0,0 +1,230 @@ +require 'set' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/object/blank' + +module Mime + class Mimes < Array + def symbols + @symbols ||= map {|m| m.to_sym } + end + + %w(<< concat shift unshift push pop []= clear compact! collect! + delete delete_at delete_if flatten! map! insert reject! reverse! + replace slice! sort! uniq!).each do |method| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{method}(*) + @symbols = nil + super + end + CODE + end + end + + SET = Mimes.new + EXTENSION_LOOKUP = {} + LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + + def self.[](type) + return type if type.is_a?(Type) + Type.lookup_by_extension(type.to_s) + end + + # Encapsulates the notion of a mime type. Can be used at render time, for example, with: + # + # class PostsController < ActionController::Base + # def show + # @post = Post.find(params[:id]) + # + # respond_to do |format| + # format.html + # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] } + # format.xml { render :xml => @people.to_xml } + # end + # end + # end + class Type + @@html_types = Set.new [:html, :all] + cattr_reader :html_types + + # These are the content types which browsers can generate without using ajax, flash, etc + # i.e. following a link, getting an image or posting a form. CSRF protection + # only needs to protect against these types. + @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text] + cattr_reader :browser_generated_types + attr_reader :symbol + + # A simple helper class used in parsing the accept header + class AcceptItem #:nodoc: + attr_accessor :order, :name, :q + + def initialize(order, name, q=nil) + @order = order + @name = name.strip + q ||= 0.0 if @name == Mime::ALL # default wildcard match to end of list + @q = ((q || 1.0).to_f * 100).to_i + end + + def to_s + @name + end + + def <=>(item) + result = item.q <=> q + result = order <=> item.order if result == 0 + result + end + + def ==(item) + name == (item.respond_to?(:name) ? item.name : item) + end + end + + class << self + def lookup(string) + LOOKUP[string] + end + + def lookup_by_extension(extension) + EXTENSION_LOOKUP[extension.to_s] + end + + # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for + # rendering different HTML versions depending on the user agent, like an iPhone. + def register_alias(string, symbol, extension_synonyms = []) + register(string, symbol, [], extension_synonyms, true) + end + + def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) + Mime.const_set(symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms)) + + SET << Mime.const_get(symbol.to_s.upcase) + + ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup + ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last } + end + + def parse(accept_header) + if accept_header !~ /,/ + [Mime::Type.lookup(accept_header)] + else + # keep track of creation order to keep the subsequent sort stable + list = [] + accept_header.split(/,/).each_with_index do |header, index| + params, q = header.split(/;\s*q=/) + if params + params.strip! + list << AcceptItem.new(index, params, q) unless params.empty? + end + end + list.sort! + + # Take care of the broken text/xml entry by renaming or deleting it + text_xml = list.index("text/xml") + app_xml = list.index(Mime::XML.to_s) + + if text_xml && app_xml + # set the q value to the max of the two + list[app_xml].q = [list[text_xml].q, list[app_xml].q].max + + # make sure app_xml is ahead of text_xml in the list + if app_xml > text_xml + list[app_xml], list[text_xml] = list[text_xml], list[app_xml] + app_xml, text_xml = text_xml, app_xml + end + + # delete text_xml from the list + list.delete_at(text_xml) + + elsif text_xml + list[text_xml].name = Mime::XML.to_s + end + + # Look for more specific XML-based types and sort them ahead of app/xml + + if app_xml + idx = app_xml + app_xml_type = list[app_xml] + + while(idx < list.length) + type = list[idx] + break if type.q < app_xml_type.q + if type.name =~ /\+xml$/ + list[app_xml], list[idx] = list[idx], list[app_xml] + app_xml = idx + end + idx += 1 + end + end + + list.map! { |i| Mime::Type.lookup(i.name) }.uniq! + list + end + end + end + + def initialize(string, symbol = nil, synonyms = []) + @symbol, @synonyms = symbol, synonyms + @string = string + end + + def to_s + @string + end + + def to_str + to_s + end + + def to_sym + @symbol + end + + def ref + to_sym || to_s + end + + def ===(list) + if list.is_a?(Array) + (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) } + else + super + end + end + + def ==(mime_type) + return false if mime_type.blank? + (@synonyms + [ self ]).any? do |synonym| + synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym + end + end + + def =~(mime_type) + return false if mime_type.blank? + regexp = Regexp.new(Regexp.quote(mime_type.to_s)) + (@synonyms + [ self ]).any? do |synonym| + synonym.to_s =~ regexp + end + end + + # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See + # ActionController::RequestForgeryProtection. + def verify_request? + @@browser_generated_types.include?(to_sym) + end + + def html? + @@html_types.include?(to_sym) || @string =~ /html/ + end + + private + def method_missing(method, *args) + if method.to_s =~ /(\w+)\?$/ + $1.downcase.to_sym == to_sym + else + super + end + end + end +end + +require 'action_dispatch/http/mime_types'