quoting.rb 6.5 KB
Newer Older
1
require "active_support/core_ext/big_decimal/conversions"
J
Jeremy Kemper 已提交
2

3 4 5
module ActiveRecord
  module ConnectionAdapters # :nodoc:
    module Quoting
6 7
      # Quotes the column value to help prevent
      # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
8
      def quote(value, column = nil)
9 10 11
        # records are quoted as their primary key
        return value.quoted_id if value.respond_to?(:quoted_id)

12
        if column
13 14 15 16 17 18
          ActiveSupport::Deprecation.warn(<<-MSG.squish)
            Passing a column to `quote` has been deprecated. It is only used
            for type casting, which should be handled elsewhere. See
            https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
            for more information.
          MSG
19
          value = type_cast_from_column(column, value)
20 21
        end

22
        _quote(value)
23 24
      end

25 26 27
      # Cast a +value+ to a type that the database understands. For example,
      # SQLite does not understand dates, so this method will convert a Date
      # to a String.
28
      def type_cast(value, column = nil)
29 30 31
        if value.respond_to?(:quoted_id) && value.respond_to?(:id)
          return value.id
        end
32

33
        if column
34
          value = type_cast_from_column(column, value)
35 36
        end

37 38 39 40
        _type_cast(value)
      rescue TypeError
        to_type = column ? " to #{column.type}" : ""
        raise TypeError, "can't cast #{value.class}#{to_type}"
41 42
      end

43 44 45
      # If you are having to call this function, you are likely doing something
      # wrong. The column does not have sufficient type information if the user
      # provided a custom type on the class level either explicitly (via
46 47 48
      # Attributes::ClassMethods#attribute) or implicitly (via
      # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+).
      # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to
49 50 51 52 53 54
      # represent the type doesn't sufficiently reflect the differences
      # (varchar vs binary) for example. The type used to get this primitive
      # should have been provided before reaching the connection adapter.
      def type_cast_from_column(column, value) # :nodoc:
        if column
          type = lookup_cast_type_from_column(column)
55
          type.serialize(value)
56 57 58 59 60
        else
          value
        end
      end

61
      # See docs for #type_cast_from_column
62 63 64 65
      def lookup_cast_type_from_column(column) # :nodoc:
        lookup_cast_type(column.sql_type)
      end

S
Sean Griffin 已提交
66 67 68 69 70 71 72 73 74 75 76
      def fetch_type_metadata(sql_type)
        cast_type = lookup_cast_type(sql_type)
        SqlTypeMetadata.new(
          sql_type: sql_type,
          type: cast_type.type,
          limit: cast_type.limit,
          precision: cast_type.precision,
          scale: cast_type.scale,
        )
      end

77 78
      # Quotes a string, escaping any ' (single quote) and \ (backslash)
      # characters.
79
      def quote_string(s)
B
brainopia 已提交
80
        s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode)
81 82
      end

83 84
      # Quotes the column name. Defaults to no quoting.
      def quote_column_name(column_name)
85
        column_name.to_s
86 87 88 89 90
      end

      # Quotes the table name. Defaults to column name quoting.
      def quote_table_name(table_name)
        quote_column_name(table_name)
91 92
      end

93 94 95
      # Override to return the quoted table name for assignment. Defaults to
      # table quoting.
      #
96
      # This works for mysql2 where table.column can be used to
97 98
      # resolve ambiguity.
      #
99
      # We override this in the sqlite3 and postgresql adapters to use only
100 101 102 103 104
      # the column name (as per syntax requirements).
      def quote_table_name_for_assignment(table, attr)
        quote_table_name("#{table}.#{attr}")
      end

105 106 107 108 109 110 111
      def quote_default_expression(value, column) # :nodoc:
        if value.is_a?(Proc)
          value.call
        else
          value = lookup_cast_type(column.sql_type).serialize(value)
          quote(value)
        end
112 113
      end

114
      def quoted_true
115
        "'t'".freeze
116
      end
117

118
      def unquoted_true
119
        "t".freeze
120 121
      end

122
      def quoted_false
123
        "'f'".freeze
124
      end
125

126
      def unquoted_false
127
        "f".freeze
128 129
      end

130 131
      # Quote date/time values for use in SQL input. Includes microseconds
      # if the value is a Time responding to usec.
132
      def quoted_date(value)
133 134
        if value.acts_like?(:time)
          zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
J
Jon Leighton 已提交
135 136 137 138 139 140

          if value.respond_to?(zone_conversion_method)
            value = value.send(zone_conversion_method)
          end
        end

141 142 143 144 145 146
        result = value.to_s(:db)
        if value.respond_to?(:usec) && value.usec > 0
          "#{result}.#{sprintf("%06d", value.usec)}"
        else
          result
        end
147
      end
148

149
      def quoted_time(value) # :nodoc:
150
        quoted_date(value).sub(/\A2000-01-01 /, "")
151 152
      end

153 154
      private

155 156
        def type_casted_binds(binds)
          binds.map { |attr| type_cast(attr.value_for_database) }
157 158
        end

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
        def types_which_need_no_typecasting
          [nil, Numeric, String]
        end

        def _quote(value)
          case value
          when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
            "'#{quote_string(value.to_s)}'"
          when true       then quoted_true
          when false      then quoted_false
          when nil        then "NULL"
          # BigDecimals need to be put in a non-normalized form and quoted.
          when BigDecimal then value.to_s("F")
          when Numeric, ActiveSupport::Duration then value.to_s
          when Type::Time::Value then "'#{quoted_time(value)}'"
          when Date, Time then "'#{quoted_date(value)}'"
          when Symbol     then "'#{quote_string(value.to_s)}'"
          when Class      then "'#{value}'"
          else raise TypeError, "can't quote #{value.class.name}"
          end
        end

        def _type_cast(value)
          case value
          when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
            value.to_s
          when true       then unquoted_true
          when false      then unquoted_false
          # BigDecimals need to be put in a non-normalized form and quoted.
          when BigDecimal then value.to_s("F")
          when Type::Time::Value then quoted_time(value)
          when Date, Time then quoted_date(value)
          when *types_which_need_no_typecasting
            value
          else raise TypeError
          end
195
        end
196 197
    end
  end
198
end