honeybadger 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/lib/honeybadger/cli/heroku.rb +1 -1
  4. data/lib/honeybadger/init/rails.rb +2 -2
  5. data/lib/honeybadger/notice.rb +3 -3
  6. data/lib/honeybadger/plugins/delayed_job/plugin.rb +11 -3
  7. data/lib/honeybadger/rack/request_hash.rb +12 -1
  8. data/lib/honeybadger/rack/user_feedback.rb +1 -5
  9. data/lib/honeybadger/rack/user_informer.rb +1 -1
  10. data/lib/honeybadger/util/request_payload.rb +8 -2
  11. data/lib/honeybadger/util/sanitizer.rb +18 -1
  12. data/lib/honeybadger/version.rb +1 -1
  13. metadata +2 -35
  14. data/vendor/inifile/lib/inifile.rb +0 -628
  15. data/vendor/thor/lib/thor.rb +0 -484
  16. data/vendor/thor/lib/thor/actions.rb +0 -319
  17. data/vendor/thor/lib/thor/actions/create_file.rb +0 -103
  18. data/vendor/thor/lib/thor/actions/create_link.rb +0 -59
  19. data/vendor/thor/lib/thor/actions/directory.rb +0 -118
  20. data/vendor/thor/lib/thor/actions/empty_directory.rb +0 -135
  21. data/vendor/thor/lib/thor/actions/file_manipulation.rb +0 -316
  22. data/vendor/thor/lib/thor/actions/inject_into_file.rb +0 -107
  23. data/vendor/thor/lib/thor/base.rb +0 -656
  24. data/vendor/thor/lib/thor/command.rb +0 -133
  25. data/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +0 -77
  26. data/vendor/thor/lib/thor/core_ext/io_binary_read.rb +0 -10
  27. data/vendor/thor/lib/thor/core_ext/ordered_hash.rb +0 -98
  28. data/vendor/thor/lib/thor/error.rb +0 -32
  29. data/vendor/thor/lib/thor/group.rb +0 -281
  30. data/vendor/thor/lib/thor/invocation.rb +0 -178
  31. data/vendor/thor/lib/thor/line_editor.rb +0 -17
  32. data/vendor/thor/lib/thor/line_editor/basic.rb +0 -35
  33. data/vendor/thor/lib/thor/line_editor/readline.rb +0 -88
  34. data/vendor/thor/lib/thor/parser.rb +0 -4
  35. data/vendor/thor/lib/thor/parser/argument.rb +0 -73
  36. data/vendor/thor/lib/thor/parser/arguments.rb +0 -175
  37. data/vendor/thor/lib/thor/parser/option.rb +0 -125
  38. data/vendor/thor/lib/thor/parser/options.rb +0 -218
  39. data/vendor/thor/lib/thor/rake_compat.rb +0 -71
  40. data/vendor/thor/lib/thor/runner.rb +0 -322
  41. data/vendor/thor/lib/thor/shell.rb +0 -81
  42. data/vendor/thor/lib/thor/shell/basic.rb +0 -421
  43. data/vendor/thor/lib/thor/shell/color.rb +0 -149
  44. data/vendor/thor/lib/thor/shell/html.rb +0 -126
  45. data/vendor/thor/lib/thor/util.rb +0 -267
  46. data/vendor/thor/lib/thor/version.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e792f463bd86a18b5499b2e995407f014db4e82
4
- data.tar.gz: 868f7529ee321a42dc00f783ab41cf6182262308
3
+ metadata.gz: a304866efae8f56e2fda056d380f07171ce9a678
4
+ data.tar.gz: a9e6a086d55d5384d4d776bc5d4e208fd2f34924
5
5
  SHA512:
6
- metadata.gz: da5baa248b75404411f27261afa1821d7dd721257498a9a5edf299016bf14e28860aa86897c9f71cb0d8becb2e3e089148bdddcf89a32115c363b3340cfafe46
7
- data.tar.gz: 47ca59a742c15f5a69df9604483e2c79fd7f8e5cd219ce72440f800dc28584b1676d301e842b293ac92a052aeb01e0dd552c934dbb08e774e7a888017a1c6add
6
+ metadata.gz: 2126e97c4ffb1162d2840a535e714565be7aa7ff2822755c382a247ff3fb3bf0ecefcc121bd9c97878573d25dbcbac6ea1ae8ade525a628c15d32dd3e705dea7
7
+ data.tar.gz: 23b99fcda16cfc38ab3e50f5628f96d4bd3a4fe22ea74e576e149ebda236281fb4936cd5ec5445418ee4b8021d6ff9003b936a423cfab0d4ca657b1a872f0ead
@@ -1,3 +1,28 @@
1
+ * Update heroku cli deprecations.
2
+
3
+ *@adamkuipers*
4
+
5
+ * Don't insert middleware if they're disabled.
6
+
7
+ *Bradley Priest*
8
+
9
+ * Don't send RAW_POST_DATA.
10
+
11
+ *Joshua Wood*
12
+
13
+ * Filter HTTP_COOKIE in request cgi_data.
14
+
15
+ *Sam McTaggart*
16
+
17
+ * Fix breakage relating to Exceptions containing a :cause attribute which is not
18
+ itself an Exception.
19
+
20
+ *Gabe da Silveira*
21
+
22
+ * Support for extracting the correct component & action for Rails Active Jobs (previously all Active Jobs where reported as 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper')
23
+
24
+ *Panos Korros*
25
+
1
26
  * Allow API key to be overridden from `Honeybadger.notify`.
2
27
 
3
28
  *Joshua Wood*
@@ -29,7 +29,7 @@ module Honeybadger
29
29
  exit(1)
30
30
  end
31
31
 
32
- cmd = %Q(heroku addons:add deployhooks:http --url="https://api.honeybadger.io/v1/deploys?deploy[environment]=#{rails_env}&deploy[local_username]={{user}}&deploy[revision]={{head}}&api_key=#{api_key}"#{app ? " --app #{app}" : ''})
32
+ cmd = %Q(heroku addons:create deployhooks:http --url="https://api.honeybadger.io/v1/deploys?deploy[environment]=#{rails_env}&deploy[local_username]={{user}}&deploy[revision]={{head}}&api_key=#{api_key}"#{app ? " --app #{app}" : ''})
33
33
 
34
34
  say("Running: `#{cmd}`")
35
35
  say(run(cmd))
@@ -17,8 +17,8 @@ module Honeybadger
17
17
  if config.feature?(:notices) && config[:'exceptions.enabled']
18
18
  ::Rails.application.config.middleware.tap do |middleware|
19
19
  middleware.insert(0, 'Honeybadger::Rack::ErrorNotifier', config)
20
- middleware.insert_before('Honeybadger::Rack::ErrorNotifier', 'Honeybadger::Rack::UserFeedback', config)
21
- middleware.insert_before('Honeybadger::Rack::UserFeedback', 'Honeybadger::Rack::UserInformer', config)
20
+ middleware.insert_before('Honeybadger::Rack::ErrorNotifier', 'Honeybadger::Rack::UserInformer', config) if config[:'user_informer.enabled']
21
+ middleware.insert_before('Honeybadger::Rack::ErrorNotifier', 'Honeybadger::Rack::UserFeedback', config) if config[:'feedback.enabled']
22
22
  end
23
23
  end
24
24
 
@@ -423,11 +423,11 @@ module Honeybadger
423
423
  # Returns the Exception cause.
424
424
  def exception_cause(exception)
425
425
  e = exception
426
- if e.respond_to?(:cause) && e.cause
426
+ if e.respond_to?(:cause) && e.cause && e.cause.is_a?(Exception)
427
427
  e.cause
428
- elsif e.respond_to?(:original_exception) && e.original_exception
428
+ elsif e.respond_to?(:original_exception) && e.original_exception && e.original_exception.is_a?(Exception)
429
429
  e.original_exception
430
- elsif e.respond_to?(:continued_exception) && e.continued_exception
430
+ elsif e.respond_to?(:continued_exception) && e.continued_exception && e.continued_exception.is_a?(Exception)
431
431
  e.continued_exception
432
432
  end
433
433
  end
@@ -8,9 +8,17 @@ module Honeybadger
8
8
  callbacks do |lifecycle|
9
9
  lifecycle.around(:invoke_job) do |job, &block|
10
10
  begin
11
- begin #buildin suport for Delayed::PerformableMethod
12
- component = job.payload_object.object.is_a?(Class) ? job.payload_object.object.name : job.payload_object.object.class.name
13
- action = job.payload_object.method_name.to_s
11
+
12
+ begin
13
+ if job.payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'
14
+ #buildin support for Rails 4.2 ActiveJob
15
+ component = job.payload_object.job_data['job_class']
16
+ action = 'perform'
17
+ else
18
+ #buildin support for Delayed::PerformableMethod
19
+ component = job.payload_object.object.is_a?(Class) ? job.payload_object.object.name : job.payload_object.object.class.name
20
+ action = job.payload_object.method_name.to_s
21
+ end
14
22
  rescue #fallback to support all other classes
15
23
  component = job.payload_object.class.name
16
24
  action = 'perform'
@@ -3,6 +3,10 @@ module Honeybadger
3
3
  # Internal: Constructs a request hash from a Rack::Request matching the
4
4
  # /v1/notices API specification.
5
5
  class RequestHash < ::Hash
6
+ # Internal
7
+ CGI_BLACKLIST = ['QUERY_STRING', 'RAW_POST_DATA'].freeze
8
+ CGI_KEY_REGEXP = /\A[A-Z_]+\Z/
9
+
6
10
  def initialize(request)
7
11
  self[:url] = extract_url(request)
8
12
  self[:params] = extract_params(request)
@@ -37,7 +41,14 @@ module Honeybadger
37
41
  end
38
42
 
39
43
  def extract_cgi_data(request)
40
- request.env.reject {|k,_| k == 'QUERY_STRING' || !k.match(/\A[A-Z_]+\Z/) }
44
+ request.env.reject {|k,_| cgi_blacklist?(k) }
45
+ end
46
+
47
+ def cgi_blacklist?(key)
48
+ return true if CGI_BLACKLIST.include?(key)
49
+ return true unless key.match(CGI_KEY_REGEXP)
50
+
51
+ false
41
52
  end
42
53
  end
43
54
  end
@@ -26,7 +26,7 @@ module Honeybadger
26
26
 
27
27
  def call(env)
28
28
  status, headers, body = @app.call(env)
29
- if enabled? && env['honeybadger.error_id'] && form = render_form(env['honeybadger.error_id'])
29
+ if env['honeybadger.error_id'] && form = render_form(env['honeybadger.error_id'])
30
30
  new_body = []
31
31
  body.each do |chunk|
32
32
  new_body << chunk.gsub("<!-- HONEYBADGER FEEDBACK -->", form)
@@ -38,10 +38,6 @@ module Honeybadger
38
38
  [status, headers, body]
39
39
  end
40
40
 
41
- def enabled?
42
- config[:'feedback.enabled']
43
- end
44
-
45
41
  def action
46
42
  URI.parse("#{config.connection_protocol}://#{config[:'connection.host']}:#{config.connection_port}/v1/feedback/").to_s
47
43
  rescue URI::InvalidURIError
@@ -16,7 +16,7 @@ module Honeybadger
16
16
 
17
17
  def call(env)
18
18
  status, headers, body = @app.call(env)
19
- if env['honeybadger.error_id'] && config[:'user_informer.enabled']
19
+ if env['honeybadger.error_id']
20
20
  new_body = []
21
21
  replace = replacement(env['honeybadger.error_id'])
22
22
  body.each do |chunk|
@@ -4,7 +4,7 @@ module Honeybadger
4
4
  module Util
5
5
  # Internal: Constructs/sanitizes request data for notices and traces.
6
6
  module RequestPayload
7
- # Internal: default values to use for request data.
7
+ # Internal: Default values to use for request data.
8
8
  DEFAULTS = {
9
9
  url: nil,
10
10
  component: nil,
@@ -14,9 +14,12 @@ module Honeybadger
14
14
  cgi_data: {}.freeze
15
15
  }.freeze
16
16
 
17
- # Internal: allowed keys.
17
+ # Internal: Allowed keys.
18
18
  KEYS = DEFAULTS.keys.freeze
19
19
 
20
+ # Internal: The cgi_data key where the raw Cookie header is stored.
21
+ HTTP_COOKIE_KEY = 'HTTP_COOKIE'.freeze
22
+
20
23
  def self.build(opts = {})
21
24
  sanitizer = opts.fetch(:sanitizer) { Sanitizer.new }
22
25
 
@@ -28,6 +31,9 @@ module Honeybadger
28
31
 
29
32
  payload[:session] = opts[:session][:data] if opts[:session] && opts[:session][:data]
30
33
  payload[:url] = sanitizer.filter_url(payload[:url]) if payload[:url]
34
+ if payload[:cgi_data][HTTP_COOKIE_KEY]
35
+ payload[:cgi_data][HTTP_COOKIE_KEY] = sanitizer.filter_cookies(payload[:cgi_data][HTTP_COOKIE_KEY])
36
+ end
31
37
 
32
38
  payload
33
39
  end
@@ -9,6 +9,10 @@ module Honeybadger
9
9
 
10
10
  MAX_STRING_SIZE = 2048
11
11
 
12
+ COOKIE_PAIRS = /[;,]\s?/
13
+ COOKIE_SEP = '='.freeze
14
+ COOKIE_PAIR_SEP = '; '.freeze
15
+
12
16
  def initialize(opts = {})
13
17
  @max_depth = opts.fetch(:max_depth, 20)
14
18
 
@@ -53,13 +57,26 @@ module Honeybadger
53
57
  end
54
58
  end
55
59
 
60
+ def filter_cookies(raw_cookies)
61
+ return raw_cookies unless filters
62
+
63
+ cookies = []
64
+ raw_cookies.split(COOKIE_PAIRS).each do |pair|
65
+ name, values = pair.split(COOKIE_SEP, 2)
66
+ values = FILTERED_REPLACEMENT if filter_key?(name)
67
+ cookies << "#{ name }=#{ values }"
68
+ end
69
+
70
+ cookies.join(COOKIE_PAIR_SEP)
71
+ end
72
+
56
73
  def filter_url(url)
57
74
  return url unless filters
58
75
 
59
76
  filtered_url = url.to_s.dup
60
77
  filtered_url.scan(/(?:^|&|\?)([^=?&]+)=([^&]+)/).each do |m|
61
78
  next unless filter_key?(m[0])
62
- filtered_url.gsub!(/#{m[1]}/, '[FILTERED]')
79
+ filtered_url.gsub!(/#{m[1]}/, FILTERED_REPLACEMENT)
63
80
  end
64
81
 
65
82
  filtered_url
@@ -1,4 +1,4 @@
1
1
  module Honeybadger
2
2
  # Public: The current String Honeybadger version.
3
- VERSION = '2.1.0'.freeze
3
+ VERSION = '2.1.1'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeybadger
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Honeybadger Industries LLC
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-04 00:00:00.000000000 Z
11
+ date: 2015-07-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Make managing application errors a more pleasant experience.
14
14
  email:
@@ -83,39 +83,6 @@ files:
83
83
  - vendor/capistrano-honeybadger/lib/capistrano/tasks/deploy.cap
84
84
  - vendor/capistrano-honeybadger/lib/honeybadger/capistrano.rb
85
85
  - vendor/capistrano-honeybadger/lib/honeybadger/capistrano/legacy.rb
86
- - vendor/inifile/lib/inifile.rb
87
- - vendor/thor/lib/thor.rb
88
- - vendor/thor/lib/thor/actions.rb
89
- - vendor/thor/lib/thor/actions/create_file.rb
90
- - vendor/thor/lib/thor/actions/create_link.rb
91
- - vendor/thor/lib/thor/actions/directory.rb
92
- - vendor/thor/lib/thor/actions/empty_directory.rb
93
- - vendor/thor/lib/thor/actions/file_manipulation.rb
94
- - vendor/thor/lib/thor/actions/inject_into_file.rb
95
- - vendor/thor/lib/thor/base.rb
96
- - vendor/thor/lib/thor/command.rb
97
- - vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb
98
- - vendor/thor/lib/thor/core_ext/io_binary_read.rb
99
- - vendor/thor/lib/thor/core_ext/ordered_hash.rb
100
- - vendor/thor/lib/thor/error.rb
101
- - vendor/thor/lib/thor/group.rb
102
- - vendor/thor/lib/thor/invocation.rb
103
- - vendor/thor/lib/thor/line_editor.rb
104
- - vendor/thor/lib/thor/line_editor/basic.rb
105
- - vendor/thor/lib/thor/line_editor/readline.rb
106
- - vendor/thor/lib/thor/parser.rb
107
- - vendor/thor/lib/thor/parser/argument.rb
108
- - vendor/thor/lib/thor/parser/arguments.rb
109
- - vendor/thor/lib/thor/parser/option.rb
110
- - vendor/thor/lib/thor/parser/options.rb
111
- - vendor/thor/lib/thor/rake_compat.rb
112
- - vendor/thor/lib/thor/runner.rb
113
- - vendor/thor/lib/thor/shell.rb
114
- - vendor/thor/lib/thor/shell/basic.rb
115
- - vendor/thor/lib/thor/shell/color.rb
116
- - vendor/thor/lib/thor/shell/html.rb
117
- - vendor/thor/lib/thor/util.rb
118
- - vendor/thor/lib/thor/version.rb
119
86
  homepage: https://github.com/honeybadger-io/honeybadger-ruby
120
87
  licenses:
121
88
  - MIT
@@ -1,628 +0,0 @@
1
- #encoding: UTF-8
2
-
3
- # This class represents the INI file and can be used to parse, modify,
4
- # and write INI files.
5
- class IniFile
6
- include Enumerable
7
-
8
- class Error < StandardError; end
9
- VERSION = '3.0.0'
10
-
11
- # Public: Open an INI file and load the contents.
12
- #
13
- # filename - The name of the file as a String
14
- # opts - The Hash of options (default: {})
15
- # :comment - String containing the comment character(s)
16
- # :parameter - String used to separate parameter and value
17
- # :encoding - Encoding String for reading / writing
18
- # :default - The String name of the default global section
19
- #
20
- # Examples
21
- #
22
- # IniFile.load('file.ini')
23
- # #=> IniFile instance
24
- #
25
- # IniFile.load('does/not/exist.ini')
26
- # #=> nil
27
- #
28
- # Returns an IniFile instance or nil if the file could not be opened.
29
- def self.load( filename, opts = {} )
30
- return unless File.file? filename
31
- new(opts.merge(:filename => filename))
32
- end
33
-
34
- # Get and set the filename
35
- attr_accessor :filename
36
-
37
- # Get and set the encoding
38
- attr_accessor :encoding
39
-
40
- # Public: Create a new INI file from the given set of options. If :content
41
- # is provided then it will be used to populate the INI file. If a :filename
42
- # is provided then the contents of the file will be parsed and stored in the
43
- # INI file. If neither the :content or :filename is provided then an empty
44
- # INI file is created.
45
- #
46
- # opts - The Hash of options (default: {})
47
- # :content - The String/Hash containing the INI contents
48
- # :comment - String containing the comment character(s)
49
- # :parameter - String used to separate parameter and value
50
- # :encoding - Encoding String for reading / writing
51
- # :default - The String name of the default global section
52
- # :filename - The filename as a String
53
- #
54
- # Examples
55
- #
56
- # IniFile.new
57
- # #=> an empty IniFile instance
58
- #
59
- # IniFile.new( :content => "[global]\nfoo=bar" )
60
- # #=> an IniFile instance
61
- #
62
- # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
63
- # #=> an IniFile instance
64
- #
65
- # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' )
66
- # #=> an IniFile instance
67
- #
68
- def initialize( opts = {} )
69
- @comment = opts.fetch(:comment, ';#')
70
- @param = opts.fetch(:parameter, '=')
71
- @encoding = opts.fetch(:encoding, nil)
72
- @default = opts.fetch(:default, 'global')
73
- @filename = opts.fetch(:filename, nil)
74
- content = opts.fetch(:content, nil)
75
-
76
- @ini = Hash.new {|h,k| h[k] = Hash.new}
77
-
78
- if content.is_a?(Hash) then merge!(content)
79
- elsif content then parse(content)
80
- elsif @filename then read
81
- end
82
- end
83
-
84
- # Public: Write the contents of this IniFile to the file system. If left
85
- # unspecified, the currently configured filename and encoding will be used.
86
- # Otherwise the filename and encoding can be specified in the options hash.
87
- #
88
- # opts - The default options Hash
89
- # :filename - The filename as a String
90
- # :encoding - The encoding as a String
91
- #
92
- # Returns this IniFile instance.
93
- def write( opts = {} )
94
- filename = opts.fetch(:filename, @filename)
95
- encoding = opts.fetch(:encoding, @encoding)
96
- mode = encoding ? "w:#{encoding}" : "w"
97
-
98
- File.open(filename, mode) do |f|
99
- @ini.each do |section,hash|
100
- f.puts "[#{section}]"
101
- hash.each {|param,val| f.puts "#{param} #{@param} #{escape_value val}"}
102
- f.puts
103
- end
104
- end
105
-
106
- self
107
- end
108
- alias :save :write
109
-
110
- # Public: Read the contents of the INI file from the file system and replace
111
- # and set the state of this IniFile instance. If left unspecified the
112
- # currently configured filename and encoding will be used when reading from
113
- # the file system. Otherwise the filename and encoding can be specified in
114
- # the options hash.
115
- #
116
- # opts - The default options Hash
117
- # :filename - The filename as a String
118
- # :encoding - The encoding as a String
119
- #
120
- # Returns this IniFile instance if the read was successful; nil is returned
121
- # if the file could not be read.
122
- def read( opts = {} )
123
- filename = opts.fetch(:filename, @filename)
124
- encoding = opts.fetch(:encoding, @encoding)
125
- return unless File.file? filename
126
-
127
- mode = encoding ? "r:#{encoding}" : "r"
128
- File.open(filename, mode) { |fd| parse fd }
129
- self
130
- end
131
- alias :restore :read
132
-
133
- # Returns this IniFile converted to a String.
134
- def to_s
135
- s = []
136
- @ini.each do |section,hash|
137
- s << "[#{section}]"
138
- hash.each {|param,val| s << "#{param} #{@param} #{escape_value val}"}
139
- s << ""
140
- end
141
- s.join("\n")
142
- end
143
-
144
- # Returns this IniFile converted to a Hash.
145
- def to_h
146
- @ini.dup
147
- end
148
-
149
- # Public: Creates a copy of this inifile with the entries from the
150
- # other_inifile merged into the copy.
151
- #
152
- # other - The other IniFile.
153
- #
154
- # Returns a new IniFile.
155
- def merge( other )
156
- self.dup.merge!(other)
157
- end
158
-
159
- # Public: Merges other_inifile into this inifile, overwriting existing
160
- # entries. Useful for having a system inifile with user overridable settings
161
- # elsewhere.
162
- #
163
- # other - The other IniFile.
164
- #
165
- # Returns this IniFile.
166
- def merge!( other )
167
- return self if other.nil?
168
-
169
- my_keys = @ini.keys
170
- other_keys = case other
171
- when IniFile
172
- other.instance_variable_get(:@ini).keys
173
- when Hash
174
- other.keys
175
- else
176
- raise Error, "cannot merge contents from '#{other.class.name}'"
177
- end
178
-
179
- (my_keys & other_keys).each do |key|
180
- case other[key]
181
- when Hash
182
- @ini[key].merge!(other[key])
183
- when nil
184
- nil
185
- else
186
- raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
187
- end
188
- end
189
-
190
- (other_keys - my_keys).each do |key|
191
- @ini[key] = case other[key]
192
- when Hash
193
- other[key].dup
194
- when nil
195
- {}
196
- else
197
- raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
198
- end
199
- end
200
-
201
- self
202
- end
203
-
204
- # Public: Yield each INI file section, parameter, and value in turn to the
205
- # given block.
206
- #
207
- # block - The block that will be iterated by the each method. The block will
208
- # be passed the current section and the parameter/value pair.
209
- #
210
- # Examples
211
- #
212
- # inifile.each do |section, parameter, value|
213
- # puts "#{parameter} = #{value} [in section - #{section}]"
214
- # end
215
- #
216
- # Returns this IniFile.
217
- def each
218
- return unless block_given?
219
- @ini.each do |section,hash|
220
- hash.each do |param,val|
221
- yield section, param, val
222
- end
223
- end
224
- self
225
- end
226
-
227
- # Public: Yield each section in turn to the given block.
228
- #
229
- # block - The block that will be iterated by the each method. The block will
230
- # be passed the current section as a Hash.
231
- #
232
- # Examples
233
- #
234
- # inifile.each_section do |section|
235
- # puts section.inspect
236
- # end
237
- #
238
- # Returns this IniFile.
239
- def each_section
240
- return unless block_given?
241
- @ini.each_key {|section| yield section}
242
- self
243
- end
244
-
245
- # Public: Remove a section identified by name from the IniFile.
246
- #
247
- # section - The section name as a String.
248
- #
249
- # Returns the deleted section Hash.
250
- def delete_section( section )
251
- @ini.delete section.to_s
252
- end
253
-
254
- # Public: Get the section Hash by name. If the section does not exist, then
255
- # it will be created.
256
- #
257
- # section - The section name as a String.
258
- #
259
- # Examples
260
- #
261
- # inifile['global']
262
- # #=> global section Hash
263
- #
264
- # Returns the Hash of parameter/value pairs for this section.
265
- def []( section )
266
- return nil if section.nil?
267
- @ini[section.to_s]
268
- end
269
-
270
- # Public: Set the section to a hash of parameter/value pairs.
271
- #
272
- # section - The section name as a String.
273
- # value - The Hash of parameter/value pairs.
274
- #
275
- # Examples
276
- #
277
- # inifile['tenderloin'] = { 'gritty' => 'yes' }
278
- # #=> { 'gritty' => 'yes' }
279
- #
280
- # Returns the value Hash.
281
- def []=( section, value )
282
- @ini[section.to_s] = value
283
- end
284
-
285
- # Public: Create a Hash containing only those INI file sections whose names
286
- # match the given regular expression.
287
- #
288
- # regex - The Regexp used to match section names.
289
- #
290
- # Examples
291
- #
292
- # inifile.match(/^tree_/)
293
- # #=> Hash of matching sections
294
- #
295
- # Return a Hash containing only those sections that match the given regular
296
- # expression.
297
- def match( regex )
298
- @ini.dup.delete_if { |section, _| section !~ regex }
299
- end
300
-
301
- # Public: Check to see if the IniFile contains the section.
302
- #
303
- # section - The section name as a String.
304
- #
305
- # Returns true if the section exists in the IniFile.
306
- def has_section?( section )
307
- @ini.has_key? section.to_s
308
- end
309
-
310
- # Returns an Array of section names contained in this IniFile.
311
- def sections
312
- @ini.keys
313
- end
314
-
315
- # Public: Freeze the state of this IniFile object. Any attempts to change
316
- # the object will raise an error.
317
- #
318
- # Returns this IniFile.
319
- def freeze
320
- super
321
- @ini.each_value {|h| h.freeze}
322
- @ini.freeze
323
- self
324
- end
325
-
326
- # Public: Mark this IniFile as tainted -- this will traverse each section
327
- # marking each as tainted.
328
- #
329
- # Returns this IniFile.
330
- def taint
331
- super
332
- @ini.each_value {|h| h.taint}
333
- @ini.taint
334
- self
335
- end
336
-
337
- # Public: Produces a duplicate of this IniFile. The duplicate is independent
338
- # of the original -- i.e. the duplicate can be modified without changing the
339
- # original. The tainted state of the original is copied to the duplicate.
340
- #
341
- # Returns a new IniFile.
342
- def dup
343
- other = super
344
- other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
345
- @ini.each_pair {|s,h| other[s].merge! h}
346
- other.taint if self.tainted?
347
- other
348
- end
349
-
350
- # Public: Produces a duplicate of this IniFile. The duplicate is independent
351
- # of the original -- i.e. the duplicate can be modified without changing the
352
- # original. The tainted state and the frozen state of the original is copied
353
- # to the duplicate.
354
- #
355
- # Returns a new IniFile.
356
- def clone
357
- other = dup
358
- other.freeze if self.frozen?
359
- other
360
- end
361
-
362
- # Public: Compare this IniFile to some other IniFile. For two INI files to
363
- # be equivalent, they must have the same sections with the same parameter /
364
- # value pairs in each section.
365
- #
366
- # other - The other IniFile.
367
- #
368
- # Returns true if the INI files are equivalent and false if they differ.
369
- def eql?( other )
370
- return true if equal? other
371
- return false unless other.instance_of? self.class
372
- @ini == other.instance_variable_get(:@ini)
373
- end
374
- alias :== :eql?
375
-
376
- # Escape special characters.
377
- #
378
- # value - The String value to escape.
379
- #
380
- # Returns the escaped value.
381
- def escape_value( value )
382
- value = value.to_s.dup
383
- value.gsub!(%r/\\([0nrt])/, '\\\\\1')
384
- value.gsub!(%r/\n/, '\n')
385
- value.gsub!(%r/\r/, '\r')
386
- value.gsub!(%r/\t/, '\t')
387
- value.gsub!(%r/\0/, '\0')
388
- value
389
- end
390
-
391
- # Parse the given content and store the information in this IniFile
392
- # instance. All data will be cleared out and replaced with the information
393
- # read from the content.
394
- #
395
- # content - A String or a file descriptor (must respond to `each_line`)
396
- #
397
- # Returns this IniFile.
398
- def parse( content )
399
- parser = Parser.new(@ini, @param, @comment, @default)
400
- parser.parse(content)
401
- self
402
- end
403
-
404
- # The IniFile::Parser has the responsibility of reading the contents of an
405
- # .ini file and storing that information into a ruby Hash. The object being
406
- # parsed must respond to `each_line` - this includes Strings and any IO
407
- # object.
408
- class Parser
409
-
410
- attr_writer :section
411
- attr_accessor :property
412
- attr_accessor :value
413
-
414
- # Create a new IniFile::Parser that can be used to parse the contents of
415
- # an .ini file.
416
- #
417
- # hash - The Hash where parsed information will be stored
418
- # param - String used to separate parameter and value
419
- # comment - String containing the comment character(s)
420
- # default - The String name of the default global section
421
- #
422
- def initialize( hash, param, comment, default )
423
- @hash = hash
424
- @default = default
425
-
426
- comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z"
427
-
428
- @section_regexp = %r/\A\s*\[([^\]]+)\]#{comment}/
429
- @ignore_regexp = %r/\A#{comment}/
430
- @property_regexp = %r/\A(.*?)(?<!\\)#{param}(.*)\z/
431
-
432
- @open_quote = %r/\A\s*(".*)\z/
433
- @close_quote = %r/\A(.*(?<!\\)")#{comment}/
434
- @full_quote = %r/\A\s*(".*(?<!\\)")#{comment}/
435
- @trailing_slash = %r/\A(.*)(?<!\\)\\#{comment}/
436
- @normal_value = %r/\A(.*?)#{comment}/
437
- end
438
-
439
- # Returns `true` if the current value starts with a leading double quote.
440
- # Otherwise returns false.
441
- def leading_quote?
442
- value && value =~ %r/\A"/
443
- end
444
-
445
- # Given a string, attempt to parse out a value from that string. This
446
- # value might be continued on the following line. So this method returns
447
- # `true` if it is expecting more data.
448
- #
449
- # string - String to parse
450
- #
451
- # Returns `true` if the next line is also part of the current value.
452
- # Returns `fase` if the string contained a complete value.
453
- def parse_value( string )
454
- continuation = false
455
-
456
- # if our value starts with a double quote, then we are in a
457
- # line continuation situation
458
- if leading_quote?
459
- # check for a closing quote at the end of the string
460
- if string =~ @close_quote
461
- value << $1
462
-
463
- # otherwise just append the string to the value
464
- else
465
- value << string
466
- continuation = true
467
- end
468
-
469
- # not currently processing a continuation line
470
- else
471
- case string
472
- when @full_quote
473
- self.value = $1
474
-
475
- when @open_quote
476
- self.value = $1
477
- continuation = true
478
-
479
- when @trailing_slash
480
- self.value ? self.value << $1 : self.value = $1
481
- continuation = true
482
-
483
- when @normal_value
484
- self.value ? self.value << $1 : self.value = $1
485
-
486
- else
487
- error
488
- end
489
- end
490
-
491
- if continuation
492
- self.value << $/ if leading_quote?
493
- else
494
- process_property
495
- end
496
-
497
- continuation
498
- end
499
-
500
- # Parse the ini file contents. This will clear any values currently stored
501
- # in the ini hash.
502
- #
503
- # content - Any object that responds to `each_line`
504
- #
505
- # Returns nil.
506
- def parse( content )
507
- return unless content
508
-
509
- continuation = false
510
-
511
- @hash.clear
512
- @line = nil
513
- self.section = nil
514
-
515
- content.each_line do |line|
516
- @line = line.chomp
517
-
518
- if continuation
519
- continuation = parse_value @line
520
- else
521
- case @line
522
- when @ignore_regexp
523
- nil
524
- when @section_regexp
525
- self.section = @hash[$1]
526
- when @property_regexp
527
- self.property = $1.strip
528
- error if property.empty?
529
-
530
- continuation = parse_value $2
531
- else
532
- error
533
- end
534
- end
535
- end
536
-
537
- # check here if we have a dangling value ... usually means we have an
538
- # unmatched open quote
539
- if leading_quote?
540
- error "Unmatched open quote"
541
- elsif property && value
542
- process_property
543
- elsif value
544
- error
545
- end
546
-
547
- nil
548
- end
549
-
550
- # Store the property/value pair in the currently active section. This
551
- # method checks for continuation of the value to the next line.
552
- #
553
- # Returns nil.
554
- def process_property
555
- property.strip!
556
- value.strip!
557
-
558
- self.value = $1 if value =~ %r/\A"(.*)(?<!\\)"\z/m
559
-
560
- section[property] = typecast(value)
561
-
562
- self.property = nil
563
- self.value = nil
564
- end
565
-
566
- # Returns the current section Hash.
567
- def section
568
- @section ||= @hash[@default]
569
- end
570
-
571
- # Raise a parse error using the given message and appending the current line
572
- # being parsed.
573
- #
574
- # msg - The message String to use.
575
- #
576
- # Raises IniFile::Error
577
- def error( msg = 'Could not parse line' )
578
- raise Error, "#{msg}: #{@line.inspect}"
579
- end
580
-
581
- # Attempt to typecast the value string. We are looking for boolean values,
582
- # integers, floats, and empty strings. Below is how each gets cast, but it
583
- # is pretty logical and straightforward.
584
- #
585
- # "true" --> true
586
- # "false" --> false
587
- # "" --> nil
588
- # "42" --> 42
589
- # "3.14" --> 3.14
590
- # "foo" --> "foo"
591
- #
592
- # Returns the typecast value.
593
- def typecast( value )
594
- case value
595
- when %r/\Atrue\z/i; true
596
- when %r/\Afalse\z/i; false
597
- when %r/\A\s*\z/i; nil
598
- else
599
- Integer(value) rescue \
600
- Float(value) rescue \
601
- unescape_value(value)
602
- end
603
- end
604
-
605
- # Unescape special characters found in the value string. This will convert
606
- # escaped null, tab, carriage return, newline, and backslash into their
607
- # literal equivalents.
608
- #
609
- # value - The String value to unescape.
610
- #
611
- # Returns the unescaped value.
612
- def unescape_value( value )
613
- value = value.to_s
614
- value.gsub!(%r/\\[0nrt\\]/) { |char|
615
- case char
616
- when '\0'; "\0"
617
- when '\n'; "\n"
618
- when '\r'; "\r"
619
- when '\t'; "\t"
620
- when '\\\\'; "\\"
621
- end
622
- }
623
- value
624
- end
625
- end
626
-
627
- end # IniFile
628
-