How to convert string to ActiveSupport::Duration?
in rails console this code works
Date.today + 1.month (or 22.days)
but this not work
Date.today + '1.month'
it says TypeError: expected numeric
'1.month' comes from db record.
How to convert string to ActiveSupport::Duration?
in rails console this code works
Date.today + 1.month (or 22.days)
but this not work
Date.today + '1.month'
it says TypeError: expected numeric
'1.month' comes from db record.
As others have pointed out, using eval on your string creates a security vulnerability.
Instead, you can convert your string to an
ActiveSupport::Duration using .to_i
on the first part of your string to convert it to an Integer, and then .send
the second part of your string to the integer to convert it to a Duration. Like this:
parts = '1.month'.split('.')
parts.first.to_i.send(parts.last)
For convenience, you could extend the String class by adding this file config/initializers/string.rb:
# Helper extension to String class
class String
# Convert strings like "3.days" into Duration objects (via Integer)
def to_duration
super unless self =~ /\A\d+\.\w+\z/
parts = split('.')
allowed = %w(minutes hours days months years) # whitelist for .send
super unless allowed.include? parts.last
parts.first.to_i.send(parts.last)
end
end
Then in your app, you can call .to_duration
on strings.
If you can choose the string format, then you should use ActiveSupport::Duration.parse
, using the ISO8601 duration format: https://api.rubyonrails.org/classes/ActiveSupport/Duration.html#method-c-parse
Example:
ActiveSupport::Duration.parse('P1M')
=> 1 month
Yes, eval
does the trick. But you should be sure the code in the database always is safe. If '22.days', '1.month' and so on come from user input, using eval
is a huge security hole.
In this case try to use some kind of natural language parsers, like https://github.com/hpoydar/chronic_duration
In Rails month
or 'months'
are Integer methods.
So when you use:
1.month
You are applying Integer#month
to 1 (which is an integer).
However '1.month'
is just a string. You can write anything between quotes and they are treated a String and not evaluated in any way unless you specifically ask for it.
eval('1.month')
is one such instance where you specifically ask for your String to get evaluated and hence you get the desired result. However this can be dangerous as you do not have any check over your input String.
You can find a lot of references as to why eval
is nasty.
In case you are doing this because your input is a String instead of Integer, you can always convert it to Integer using to_i
. Example:
time = "1"
time.to_i.month
#=> 2592000
This will work even for Integer values:
time = 1
time.to_i.month
=> 2592000
If you have code like this Date.today + eval('1.month')
(could lead to anybody executing any code they want on your server) what does it do? In the end its roughly the same as Date.today+30
(that is add 1.month to today's date.)
So you can get rid of the eval by instead storing perhaps just the days.
But it depends on your application, a date object plus 1.month actually moves the date out exactly 1 month (so if you start with Jan 29 2016 and add 3.months you get March 29, 2016 which is NOT 90 days away
So lets say your strings in that db are always 1.month or 2.months or 7.months (i.e. just some number of months) well in that case all you need to store in the DB is an integer value (or store it as a string like "7", then do "7".to_i)
But perhaps you want to allow months, days, weeks, and years... Well you could do this: {months: 7, days: 3, weeks: 1, years: 2}.to_json and store that in your db, then to use it you would say:
JSON.parse(some_string_from_db).inject(Date.today) do |date, inc|
date+inc.last.send(inc.first.to_s)
end
This is only marginally better than the eval since you are allowing arbitrary object and methods to be evaluated, but you could just add some protection like this:
JSON.parse(some_string_from_db).inject(Date.today) do |date, inc|
if [:years, :months, :weeks, :days].include? inc.first.to_s
date+inc.last.send(inc.first.to_s)
else
Raise "don't try it buddy"
end
end
Now you are back controlling what you are executing!
Of course it all depends on your application, but hopefully this answers your question.
You can store the human readable form in the DB, and then use the Fugit gem to parse the string into a duration - ActiveSupport::Duration does not seem to have as extensive parsing facilities. The Duration can be converted to seconds and then applied to normal Time/Date.
deletion_deadline = "30 days"
Time.zone.now + Fugit.parse_duration(deletion_deadline).to_sec
Ohhooo I got answer on my own Need to add eval like this
Date.today + eval('1.month')
works perfectly fine
Is there any another way? what are pros and cons to use this way?
This could lead to a remote code execution exploit, e.g. random people doing whatever they want on your servers