honeybadger 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
-