1#--
2# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
3# All rights reserved.
4# See LICENSE.txt for permissions.
5#++
6
7require 'rubygems/user_interaction'
8require 'rbconfig'
9
10##
11# Gem::ConfigFile RubyGems options and gem command options from gemrc.
12#
13# gemrc is a YAML file that uses strings to match gem command arguments and
14# symbols to match RubyGems options.
15#
16# Gem command arguments use a String key that matches the command name and
17# allow you to specify default arguments:
18#
19#   install: --no-rdoc --no-ri
20#   update: --no-rdoc --no-ri
21#
22# You can use <tt>gem:</tt> to set default arguments for all commands.
23#
24# RubyGems options use symbol keys.  Valid options are:
25#
26# +:backtrace+:: See #backtrace
27# +:sources+:: Sets Gem::sources
28# +:verbose+:: See #verbose
29#
30# gemrc files may exist in various locations and are read and merged in
31# the following order:
32#
33# - system wide (/etc/gemrc)
34# - per user (~/.gemrc)
35# - per environment (gemrc files listed in the GEMRC environment variable)
36
37class Gem::ConfigFile
38
39  include Gem::UserInteraction
40
41  DEFAULT_BACKTRACE = false
42  DEFAULT_BULK_THRESHOLD = 1000
43  DEFAULT_VERBOSITY = true
44  DEFAULT_UPDATE_SOURCES = true
45
46  ##
47  # For Ruby packagers to set configuration defaults.  Set in
48  # rubygems/defaults/operating_system.rb
49
50  OPERATING_SYSTEM_DEFAULTS = {}
51
52  ##
53  # For Ruby implementers to set configuration defaults.  Set in
54  # rubygems/defaults/#{RUBY_ENGINE}.rb
55
56  PLATFORM_DEFAULTS = {}
57
58  # :stopdoc:
59
60  system_config_path =
61    begin
62      require "etc"
63      Etc.sysconfdir
64    rescue LoadError, NoMethodError
65      begin
66        # TODO: remove after we drop 1.8.7 and 1.9.1
67        require 'Win32API'
68
69        CSIDL_COMMON_APPDATA = 0x0023
70        path = 0.chr * 260
71        if RUBY_VERSION > '1.9' then
72          SHGetFolderPath = Win32API.new 'shell32', 'SHGetFolderPath', 'PLPLP',
73          'L', :stdcall
74          SHGetFolderPath.call nil, CSIDL_COMMON_APPDATA, nil, 1, path
75        else
76          SHGetFolderPath = Win32API.new 'shell32', 'SHGetFolderPath', 'LLLLP',
77          'L'
78          SHGetFolderPath.call 0, CSIDL_COMMON_APPDATA, 0, 1, path
79        end
80
81        path.strip
82      rescue LoadError
83        RbConfig::CONFIG["sysconfdir"] || "/etc"
84      end
85    end
86
87  # :startdoc:
88
89  SYSTEM_WIDE_CONFIG_FILE = File.join system_config_path, 'gemrc'
90
91  ##
92  # List of arguments supplied to the config file object.
93
94  attr_reader :args
95
96  ##
97  # Where to look for gems (deprecated)
98
99  attr_accessor :path
100
101  ##
102  # Where to install gems (deprecated)
103
104  attr_accessor :home
105
106  ##
107  # True if we print backtraces on errors.
108
109  attr_writer :backtrace
110
111  ##
112  # Bulk threshold value.  If the number of missing gems are above this
113  # threshold value, then a bulk download technique is used.  (deprecated)
114
115  attr_accessor :bulk_threshold
116
117  ##
118  # Verbose level of output:
119  # * false -- No output
120  # * true -- Normal output
121  # * :loud -- Extra output
122
123  attr_accessor :verbose
124
125  ##
126  # True if we want to update the SourceInfoCache every time, false otherwise
127
128  attr_accessor :update_sources
129
130  ##
131  # True if we want to force specification of gem server when pushing a gem
132
133  attr_accessor :disable_default_gem_server
134
135  # openssl verify mode value, used for remote https connection
136
137  attr_reader :ssl_verify_mode
138
139  ##
140  # Path name of directory or file of openssl CA certificate, used for remote https connection
141
142  attr_reader :ssl_ca_cert
143
144  ##
145  # Create the config file object.  +args+ is the list of arguments
146  # from the command line.
147  #
148  # The following command line options are handled early here rather
149  # than later at the time most command options are processed.
150  #
151  # <tt>--config-file</tt>, <tt>--config-file==NAME</tt>::
152  #   Obviously these need to be handled by the ConfigFile object to ensure we
153  #   get the right config file.
154  #
155  # <tt>--backtrace</tt>::
156  #   Backtrace needs to be turned on early so that errors before normal
157  #   option parsing can be properly handled.
158  #
159  # <tt>--debug</tt>::
160  #   Enable Ruby level debug messages.  Handled early for the same reason as
161  #   --backtrace.
162  #--
163  # TODO: parse options upstream, pass in options directly
164
165  def initialize(args)
166    @config_file_name = nil
167    need_config_file_name = false
168
169    arg_list = []
170
171    args.each do |arg|
172      if need_config_file_name then
173        @config_file_name = arg
174        need_config_file_name = false
175      elsif arg =~ /^--config-file=(.*)/ then
176        @config_file_name = $1
177      elsif arg =~ /^--config-file$/ then
178        need_config_file_name = true
179      else
180        arg_list << arg
181      end
182    end
183
184    @backtrace = DEFAULT_BACKTRACE
185    @bulk_threshold = DEFAULT_BULK_THRESHOLD
186    @verbose = DEFAULT_VERBOSITY
187    @update_sources = DEFAULT_UPDATE_SOURCES
188
189    operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
190    platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
191    system_config = load_file SYSTEM_WIDE_CONFIG_FILE
192    user_config = load_file config_file_name.dup.untaint
193    environment_config = (ENV['GEMRC'] || '').split(/[:;]/).inject({}) do |result, file|
194      result.merge load_file file
195    end
196
197
198    @hash = operating_system_config.merge platform_config
199    @hash = @hash.merge system_config
200    @hash = @hash.merge user_config
201    @hash = @hash.merge environment_config
202
203    # HACK these override command-line args, which is bad
204    @backtrace                  = @hash[:backtrace]                  if @hash.key? :backtrace
205    @bulk_threshold             = @hash[:bulk_threshold]             if @hash.key? :bulk_threshold
206    @home                       = @hash[:gemhome]                    if @hash.key? :gemhome
207    @path                       = @hash[:gempath]                    if @hash.key? :gempath
208    @update_sources             = @hash[:update_sources]             if @hash.key? :update_sources
209    @verbose                    = @hash[:verbose]                    if @hash.key? :verbose
210    @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server
211
212    @ssl_verify_mode  = @hash[:ssl_verify_mode]  if @hash.key? :ssl_verify_mode
213    @ssl_ca_cert      = @hash[:ssl_ca_cert]      if @hash.key? :ssl_ca_cert
214
215    @api_keys         = nil
216    @rubygems_api_key = nil
217
218    Gem.sources = @hash[:sources] if @hash.key? :sources
219    handle_arguments arg_list
220  end
221
222  ##
223  # Hash of RubyGems.org and alternate API keys
224
225  def api_keys
226    load_api_keys unless @api_keys
227
228    @api_keys
229  end
230
231  ##
232  # Checks the permissions of the credentials file.  If they are not 0600 an
233  # error message is displayed and RubyGems aborts.
234
235  def check_credentials_permissions
236    return if Gem.win_platform? # windows doesn't write 0600 as 0600
237    return unless File.exist? credentials_path
238
239    existing_permissions = File.stat(credentials_path).mode & 0777
240
241    return if existing_permissions == 0600
242
243    alert_error <<-ERROR
244Your gem push credentials file located at:
245
246\t#{credentials_path}
247
248has file permissions of 0#{existing_permissions.to_s 8} but 0600 is required.
249
250You should reset your credentials at:
251
252\thttps://rubygems.org/profile/edit
253
254if you believe they were disclosed to a third party.
255    ERROR
256
257    terminate_interaction 1
258  end
259
260  ##
261  # Location of RubyGems.org credentials
262
263  def credentials_path
264    File.join Gem.user_home, '.gem', 'credentials'
265  end
266
267  def load_api_keys
268    check_credentials_permissions
269
270    @api_keys = if File.exist? credentials_path then
271                  load_file(credentials_path)
272                else
273                  @hash
274                end
275
276    if @api_keys.key? :rubygems_api_key then
277      @rubygems_api_key    = @api_keys[:rubygems_api_key]
278      @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless
279        @api_keys.key? :rubygems
280    end
281  end
282
283  ##
284  # Returns the RubyGems.org API key
285
286  def rubygems_api_key
287    load_api_keys unless @rubygems_api_key
288
289    @rubygems_api_key
290  end
291
292  ##
293  # Sets the RubyGems.org API key to +api_key+
294
295  def rubygems_api_key= api_key
296    check_credentials_permissions
297
298    config = load_file(credentials_path).merge(:rubygems_api_key => api_key)
299
300    dirname = File.dirname credentials_path
301    Dir.mkdir(dirname) unless File.exist? dirname
302
303    Gem.load_yaml
304
305    permissions = 0600 & (~File.umask)
306    File.open(credentials_path, 'w', permissions) do |f|
307      f.write config.to_yaml
308    end
309
310    @rubygems_api_key = api_key
311  end
312
313  def load_file(filename)
314    Gem.load_yaml
315
316    return {} unless filename and File.exist? filename
317
318    begin
319      content = YAML.load(File.read(filename))
320      unless content.kind_of? Hash
321        warn "Failed to load #{filename} because it doesn't contain valid YAML hash"
322        return {}
323      end
324      return content
325    rescue ArgumentError
326      warn "Failed to load #{filename}"
327    rescue Errno::EACCES
328      warn "Failed to load #{filename} due to permissions problem."
329    end
330
331    {}
332  end
333
334  # True if the backtrace option has been specified, or debug is on.
335  def backtrace
336    @backtrace or $DEBUG
337  end
338
339  # The name of the configuration file.
340  def config_file_name
341    @config_file_name || Gem.config_file
342  end
343
344  # Delegates to @hash
345  def each(&block)
346    hash = @hash.dup
347    hash.delete :update_sources
348    hash.delete :verbose
349    hash.delete :backtrace
350    hash.delete :bulk_threshold
351
352    yield :update_sources, @update_sources
353    yield :verbose, @verbose
354    yield :backtrace, @backtrace
355    yield :bulk_threshold, @bulk_threshold
356
357    yield 'config_file_name', @config_file_name if @config_file_name
358
359    hash.each(&block)
360  end
361
362  # Handle the command arguments.
363  def handle_arguments(arg_list)
364    @args = []
365
366    arg_list.each do |arg|
367      case arg
368      when /^--(backtrace|traceback)$/ then
369        @backtrace = true
370      when /^--debug$/ then
371        $DEBUG = true
372      else
373        @args << arg
374      end
375    end
376  end
377
378  # Really verbose mode gives you extra output.
379  def really_verbose
380    case verbose
381    when true, false, nil then
382      false
383    else
384      true
385    end
386  end
387
388  # to_yaml only overwrites things you can't override on the command line.
389  def to_yaml # :nodoc:
390    yaml_hash = {}
391    yaml_hash[:backtrace] = if @hash.key?(:backtrace)
392                              @hash[:backtrace]
393                            else
394                              DEFAULT_BACKTRACE
395                            end
396
397    yaml_hash[:bulk_threshold] = if @hash.key?(:bulk_threshold)
398                                   @hash[:bulk_threshold]
399                                 else
400                                   DEFAULT_BULK_THRESHOLD
401                                 end
402
403    yaml_hash[:sources] = Gem.sources.to_a
404
405    yaml_hash[:update_sources] = if @hash.key?(:update_sources)
406                                   @hash[:update_sources]
407                                 else
408                                   DEFAULT_UPDATE_SOURCES
409                                 end
410
411    yaml_hash[:verbose] = if @hash.key?(:verbose)
412                            @hash[:verbose]
413                          else
414                            DEFAULT_VERBOSITY
415                          end
416
417    keys = yaml_hash.keys.map { |key| key.to_s }
418    keys << 'debug'
419    re = Regexp.union(*keys)
420
421    @hash.each do |key, value|
422      key = key.to_s
423      next if key =~ re
424      yaml_hash[key.to_s] = value
425    end
426
427    yaml_hash.to_yaml
428  end
429
430  # Writes out this config file, replacing its source.
431  def write
432    open config_file_name, 'w' do |io|
433      io.write to_yaml
434    end
435  end
436
437  # Return the configuration information for +key+.
438  def [](key)
439    @hash[key.to_s]
440  end
441
442  # Set configuration option +key+ to +value+.
443  def []=(key, value)
444    @hash[key.to_s] = value
445  end
446
447  def ==(other) # :nodoc:
448    self.class === other and
449      @backtrace == other.backtrace and
450      @bulk_threshold == other.bulk_threshold and
451      @verbose == other.verbose and
452      @update_sources == other.update_sources and
453      @hash == other.hash
454  end
455
456  attr_reader :hash
457  protected :hash
458end
459