search: fix parsing of invalid metatag values.

* Change `age:` metatag to require time units. This means e.g.
  `age:<600` no longer works; instead you have to say `age:<600sec`.

* Allow time units in the `age:` metatag to be abbreviated as long as
  they're unambiguous. This means `age:<60sec`, `age:<5min`, and
  `age:<5mon` now work, in addition to `age:<60s` and `age:<60seconds`.

* Allow the `ratio:` metatag to be written like `ratio:16/9` in addition
  to `ratio:16:9`.

* Fix invalid date searches like `date:foo` or `date:05-15-2021`
  to return nothing instead of raising an "undefined method
  'beginning_of_day' for nil" exception. (`date:05-15-2021` is invalid
  because it's parsed as DD-MM-YYYY).

* Fix invalid searches like `score:foo`, `ratio:foo`, and `mpixels:foo`
  to return nothing instead of being treated like `score:0`, `ratio:0`,
  `mpixels:0`.

* Fix `age:<60m` to return nothing instead of silently being treated
  like `age:<60seconds`.

* Fix `age:foo` to return nothing instead of silently being treated like
  `age:0d` (return all uploads from today).

Fixes #4389.
This commit is contained in:
evazion
2021-11-02 00:41:05 -05:00
parent 788dcbd87b
commit a5ed8c72c9
3 changed files with 130 additions and 48 deletions

View File

@@ -1,27 +1,32 @@
require 'abbrev'
module DurationParser
def self.parse(string)
string =~ /(\d+)(s(econds?)?|mi(nutes?)?|h(ours?)?|d(ays?)?|w(eeks?)?|mo(nths?)?|y(ears?)?)?/i
abbrevs = Abbrev.abbrev(%w[seconds minutes hours days weeks months years])
size = $1.to_i
unit = $2
raise unless string =~ /(.*?)([a-z]+)\z/i
size = Float($1)
unit = abbrevs.fetch($2.downcase)
case unit
when /^s/i
when "seconds"
size.seconds
when /^mi/i
when "minutes"
size.minutes
when /^h/i
when "hours"
size.hours
when /^d/i
when "days"
size.days
when /^w/i
when "weeks"
size.weeks
when /^mo/i
size.months
when /^y/i
size.years
when "months"
size * (365.25.days / 12)
when "years"
size * (365.25.days)
else
size.seconds
raise NotImplementedError
end
rescue
raise ArgumentError, "'#{string}' is not a valid duration"
end
end

View File

@@ -12,6 +12,7 @@ class PostQueryBuilder
# Raised when the number of tags exceeds the user's tag limit.
class TagLimitError < StandardError; end
class ParseError < StandardError; end
# How many tags a `blah*` search should match.
MAX_WILDCARD_TAGS = 100
@@ -259,6 +260,8 @@ class PostQueryBuilder
def attribute_matches(value, field, type = :integer)
operator, *args = parse_metatag_value(value, type)
Post.where_operator(field, operator, *args)
rescue ParseError
Post.none
end
def user_matches(field, username)
@@ -815,45 +818,43 @@ class PostQueryBuilder
end
# Parse a simple string value into a Ruby type.
# @param object [String] the value to parse
# @param string [String] the value to parse
# @param type [Symbol] the value's type
# @return [Object] the parsed value
def parse_cast(object, type)
def parse_cast(string, type)
case type
when :enum
object.to_s.downcase
string.downcase
when :integer
object.to_i
Integer(string) # raises ArgumentError if string is invalid
when :float
object.to_f
Float(string) # raises ArgumentError if string is invalid
when :md5
object.to_s.downcase
raise ParseError, "#{string} is not a valid MD5" unless string.match?(/\A[0-9a-fA-F]{32}\z/)
string.downcase
when :date, :datetime
Time.zone.parse(object) rescue nil
date = Time.zone.parse(string)
raise ParseError, "#{string} is not a valid date" if date.nil?
date
when :age
DurationParser.parse(object).ago
DurationParser.parse(string).ago
when :interval
DurationParser.parse(object)
DurationParser.parse(string)
when :ratio
object =~ /\A(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)\Z/i
if $1 && $2.to_f != 0.0
($1.to_f / $2.to_f).round(2)
else
object.to_f.round(2)
end
string = string.tr(":", "/") # "2:3" => "2/3"
Rational(string).to_f.round(2) # raises ArgumentError or ZeroDivisionError if string is invalid
when :filesize
object =~ /\A(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?\Z/
raise ParseError, "#{string} is not a valid filesize" unless string =~ /\A(\d+(?:\.\d*)?|\d*\.\d+)([kKmM]?)[bB]?\Z/
size = $1.to_f
size = Float($1)
unit = $2
conversion_factor = case unit
@@ -868,8 +869,11 @@ class PostQueryBuilder
(size * conversion_factor).to_i
else
raise NotImplementedError, "unrecognized type #{type} for #{object}"
raise NotImplementedError, "unrecognized type #{type} for #{string}"
end
rescue ArgumentError, ZeroDivisionError => e
raise ParseError, e.message
end
def parse_metatag_value(string, type)