arpitjain11-rubycas-server 0.8.0.20090612

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG.txt +1 -0
  2. data/History.txt +272 -0
  3. data/LICENSE.txt +504 -0
  4. data/Manifest.txt +85 -0
  5. data/PostInstall.txt +3 -0
  6. data/README.rdoc +26 -0
  7. data/Rakefile +4 -0
  8. data/bin/rubycas-server +13 -0
  9. data/bin/rubycas-server-ctl +9 -0
  10. data/config/hoe.rb +78 -0
  11. data/config/requirements.rb +15 -0
  12. data/config.example.yml +544 -0
  13. data/config.ru +38 -0
  14. data/custom_views.example.rb +11 -0
  15. data/lib/casserver/authenticators/active_directory_ldap.rb +11 -0
  16. data/lib/casserver/authenticators/base.rb +48 -0
  17. data/lib/casserver/authenticators/client_certificate.rb +46 -0
  18. data/lib/casserver/authenticators/google.rb +54 -0
  19. data/lib/casserver/authenticators/ldap.rb +147 -0
  20. data/lib/casserver/authenticators/ntlm.rb +88 -0
  21. data/lib/casserver/authenticators/open_id.rb +22 -0
  22. data/lib/casserver/authenticators/sql.rb +102 -0
  23. data/lib/casserver/authenticators/sql_encrypted.rb +76 -0
  24. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  25. data/lib/casserver/authenticators/sql_rest_auth.rb +77 -0
  26. data/lib/casserver/authenticators/test.rb +19 -0
  27. data/lib/casserver/cas.rb +322 -0
  28. data/lib/casserver/conf.rb +75 -0
  29. data/lib/casserver/controllers.rb +457 -0
  30. data/lib/casserver/load_picnic.rb +19 -0
  31. data/lib/casserver/localization.rb +82 -0
  32. data/lib/casserver/models.rb +265 -0
  33. data/lib/casserver/postambles.rb +174 -0
  34. data/lib/casserver/utils.rb +30 -0
  35. data/lib/casserver/version.rb +9 -0
  36. data/lib/casserver/views.rb +245 -0
  37. data/lib/casserver.rb +58 -0
  38. data/lib/rubycas-server/version.rb +1 -0
  39. data/lib/rubycas-server.rb +1 -0
  40. data/po/de_DE/rubycas-server.po +119 -0
  41. data/po/es_ES/rubycas-server.po +115 -0
  42. data/po/fr_FR/rubycas-server.po +116 -0
  43. data/po/ja_JP/rubycas-server.po +118 -0
  44. data/po/pl_PL/rubycas-server.po +115 -0
  45. data/po/pt_BR/rubycas-server.po +115 -0
  46. data/po/ru_RU/rubycas-server.po +110 -0
  47. data/po/rubycas-server.pot +104 -0
  48. data/public/themes/cas.css +121 -0
  49. data/public/themes/notice.png +0 -0
  50. data/public/themes/ok.png +0 -0
  51. data/public/themes/simple/bg.png +0 -0
  52. data/public/themes/simple/login_box_bg.png +0 -0
  53. data/public/themes/simple/logo.png +0 -0
  54. data/public/themes/simple/theme.css +28 -0
  55. data/public/themes/urbacon/bg.png +0 -0
  56. data/public/themes/urbacon/login_box_bg.png +0 -0
  57. data/public/themes/urbacon/logo.png +0 -0
  58. data/public/themes/urbacon/theme.css +33 -0
  59. data/public/themes/warning.png +0 -0
  60. data/resources/init.d.sh +58 -0
  61. data/script/console +10 -0
  62. data/script/destroy +14 -0
  63. data/script/generate +14 -0
  64. data/script/txt2html +82 -0
  65. data/setup.rb +1585 -0
  66. data/tasks/deployment.rake +34 -0
  67. data/tasks/environment.rake +7 -0
  68. data/tasks/localization.rake +11 -0
  69. data/tasks/website.rake +17 -0
  70. data/vendor/isaac_0.9.1/LICENSE +26 -0
  71. data/vendor/isaac_0.9.1/README +78 -0
  72. data/vendor/isaac_0.9.1/TODO +3 -0
  73. data/vendor/isaac_0.9.1/VERSIONS +3 -0
  74. data/vendor/isaac_0.9.1/crypt/ISAAC.rb +171 -0
  75. data/vendor/isaac_0.9.1/isaac.gemspec +39 -0
  76. data/vendor/isaac_0.9.1/setup.rb +596 -0
  77. data/vendor/isaac_0.9.1/test/TC_ISAAC.rb +76 -0
  78. metadata +193 -0
@@ -0,0 +1,457 @@
1
+ # The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
2
+ # under http://www.ja-sig.org/products/cas/overview/protocol/index.html
3
+
4
+ require 'casserver/cas'
5
+
6
+ module CASServer::Controllers
7
+
8
+ # 2.1
9
+ class Login < R '/', '/login'
10
+ include CASServer::CAS
11
+
12
+ # 2.1.1
13
+ def get
14
+ CASServer::Utils::log_controller_action(self.class, input)
15
+
16
+ # make sure there's no caching
17
+ headers['Pragma'] = 'no-cache'
18
+ headers['Cache-Control'] = 'no-store'
19
+ headers['Expires'] = (Time.now - 1.year).rfc2822
20
+
21
+ # optional params
22
+ @service = clean_service_url(input['service'])
23
+ @renew = input['renew']
24
+ @gateway = input['gateway'] == 'true' || input['gateway'] == '1'
25
+
26
+ if tgc = cookies['tgt']
27
+ tgt, tgt_error = validate_ticket_granting_ticket(tgc)
28
+ end
29
+
30
+ if tgt and !tgt_error
31
+ @message = {:type => 'notice',
32
+ :message => _("You are currently logged in as '%s'. If this is not you, please log in below.") % tgt.username }
33
+ end
34
+
35
+ if input['redirection_loop_intercepted']
36
+ @message = {:type => 'mistake',
37
+ :message => _("The client and server are unable to negotiate authentication. Please try logging in again later.")}
38
+ end
39
+
40
+ begin
41
+ if @service
42
+ if !@renew && tgt && !tgt_error
43
+ st = generate_service_ticket(@service, tgt.username, tgt)
44
+ service_with_ticket = service_uri_with_ticket(@service, st)
45
+ $LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
46
+ return redirect(service_with_ticket, :status => 303) # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
47
+ elsif @gateway
48
+ $LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
49
+ return redirect(@service, :status => 303)
50
+ end
51
+ elsif @gateway
52
+ $LOG.error("This is a gateway request but no service parameter was given!")
53
+ @message = {:type => 'mistake',
54
+ :message => _("The server cannot fulfill this gateway request because no service parameter was given.")}
55
+ end
56
+ rescue URI::InvalidURIError
57
+ $LOG.error("The service '#{@service}' is not a valid URI!")
58
+ @message = {:type => 'mistake',
59
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")}
60
+ end
61
+
62
+ lt = generate_login_ticket
63
+
64
+ $LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
65
+
66
+ @lt = lt.ticket
67
+
68
+ #$LOG.debug(env)
69
+
70
+ # If the 'onlyLoginForm' parameter is specified, we will only return the
71
+ # login form part of the page. This is useful for when you want to
72
+ # embed the login form in some external page (as an IFRAME, or otherwise).
73
+ # The optional 'submitToURI' parameter can be given to explicitly set the
74
+ # action for the form, otherwise the server will try to guess this for you.
75
+ if input.has_key? 'onlyLoginForm'
76
+ if @env['HTTP_HOST']
77
+ guessed_login_uri = "http#{@env['HTTPS'] && @env['HTTPS'] == 'on' ? 's' : ''}://#{@env['REQUEST_URI']}#{self / '/login'}"
78
+ else
79
+ guessed_login_uri = nil
80
+ end
81
+
82
+ @form_action = input['submitToURI'] || guessed_login_uri
83
+
84
+ if @form_action
85
+ render :login_form
86
+ else
87
+ @status = 500
88
+ _("Could not guess the CAS login URI. Please supply a submitToURI parameter with your request.")
89
+ end
90
+ else
91
+ render :login
92
+ end
93
+ end
94
+
95
+ # 2.2
96
+ def post
97
+ CASServer::Utils::log_controller_action(self.class, input)
98
+
99
+ # 2.2.1 (optional)
100
+ @service = clean_service_url(input['service'])
101
+
102
+ # 2.2.2 (required)
103
+ @username = input['username']
104
+ @password = input['password']
105
+ @lt = input['lt']
106
+
107
+ # Remove leading and trailing widespace from username.
108
+ @username.strip! if @username
109
+
110
+ if @username && $CONF[:downcase_username]
111
+ $LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
112
+ @username.downcase!
113
+ end
114
+
115
+ if error = validate_login_ticket(@lt)
116
+ @message = {:type => 'mistake', :message => error}
117
+ # generate another login ticket to allow for re-submitting the form
118
+ @lt = generate_login_ticket.ticket
119
+ @status = 401
120
+ return render(:login)
121
+ end
122
+
123
+ # generate another login ticket to allow for re-submitting the form after a post
124
+ @lt = generate_login_ticket.ticket
125
+
126
+ if $CONF[:authenticator].instance_of? Array
127
+ $AUTH.each_index {|auth_index| $AUTH[auth_index].configure($CONF.authenticator[auth_index])}
128
+ else
129
+ $AUTH[0].configure($CONF.authenticator)
130
+ end
131
+
132
+ $LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{$AUTH}")
133
+
134
+ credentials_are_valid = false
135
+ extra_attributes = {}
136
+ successful_authenticator = nil
137
+ begin
138
+ $AUTH.each do |auth|
139
+ credentials_are_valid = auth.validate(
140
+ :username => @username,
141
+ :password => @password,
142
+ :service => @service,
143
+ :request => @env
144
+ )
145
+ if credentials_are_valid
146
+ extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
147
+ successful_authenticator = auth
148
+ break
149
+ end
150
+ end
151
+ rescue CASServer::AuthenticatorError => e
152
+ $LOG.error(e)
153
+ @message = {:type => 'mistake', :message => e.to_s}
154
+ return render(:login)
155
+ end
156
+
157
+ if credentials_are_valid
158
+ $LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
159
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
160
+
161
+ # 3.6 (ticket-granting cookie)
162
+ tgt = generate_ticket_granting_ticket(@username, extra_attributes)
163
+
164
+ if $CONF.maximum_session_lifetime
165
+ expires = $CONF.maximum_session_lifetime.to_i.from_now
166
+ expiry_info = " It will expire on #{expires}."
167
+ else
168
+ expiry_info = " It will not expire."
169
+ end
170
+
171
+ if $CONF.maximum_session_lifetime
172
+ cookies['tgt'] = {
173
+ :value => tgt.to_s,
174
+ :expires => Time.now + $CONF.maximum_session_lifetime
175
+ }
176
+ else
177
+ cookies['tgt'] = tgt.to_s
178
+ end
179
+
180
+ $LOG.debug("Ticket granting cookie '#{cookies['tgt'].inspect}' granted to #{@username.inspect}. #{expiry_info}")
181
+
182
+ if @service.blank?
183
+ $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
184
+ @message = {:type => 'confirmation', :message => _("You have successfully logged in.")}
185
+ else
186
+ @st = generate_service_ticket(@service, @username, tgt)
187
+ begin
188
+ service_with_ticket = service_uri_with_ticket(@service, @st)
189
+
190
+ $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
191
+ return redirect(service_with_ticket, :status => 303) # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
192
+ rescue URI::InvalidURIError
193
+ $LOG.error("The service '#{@service}' is not a valid URI!")
194
+ @message = {:type => 'mistake',
195
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")}
196
+ end
197
+ end
198
+ else
199
+ $LOG.warn("Invalid credentials given for user '#{@username}'")
200
+ @message = {:type => 'mistake', :message => _("Incorrect username or password.")}
201
+ @status = 401
202
+ end
203
+
204
+ render :login
205
+ end
206
+ end
207
+
208
+ # 2.3
209
+ class Logout < R '/logout'
210
+ include CASServer::CAS
211
+
212
+ # 2.3.1
213
+ def get
214
+ CASServer::Utils::log_controller_action(self.class, input)
215
+
216
+ # The behaviour here is somewhat non-standard. Rather than showing just a blank
217
+ # "logout" page, we take the user back to the login page with a "you have been logged out"
218
+ # message, allowing for an opportunity to immediately log back in. This makes it
219
+ # easier for the user to log out and log in as someone else.
220
+ @service = clean_service_url(input['service'] || input['destination'])
221
+ @continue_url = input['url']
222
+
223
+ @gateway = input['gateway'] == 'true' || input['gateway'] == '1'
224
+
225
+ tgt = CASServer::Models::TicketGrantingTicket.find_by_ticket(cookies['tgt'])
226
+
227
+ cookies.delete 'tgt'
228
+
229
+ if tgt
230
+ CASServer::Models::TicketGrantingTicket.transaction do
231
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
232
+ tgt.granted_service_tickets.each do |st|
233
+ send_logout_notification_for_service_ticket(st) if $CONF.enable_single_sign_out
234
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
235
+ # (the above method returns false if the POST results in a non-200 HTTP response).
236
+ $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
237
+ st.destroy
238
+ end
239
+
240
+ pgts = CASServer::Models::ProxyGrantingTicket.find(:all,
241
+ :conditions => [CASServer::Models::Base.connection.quote_table_name(CASServer::Models::ServiceTicket.table_name)+".username = ?", tgt.username],
242
+ :include => :service_ticket)
243
+ pgts.each do |pgt|
244
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
245
+ pgt.destroy
246
+ end
247
+
248
+ $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
249
+ tgt.destroy
250
+ end
251
+
252
+ $LOG.info("User '#{tgt.username}' logged out.")
253
+ else
254
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
255
+ end
256
+
257
+ @message = {:type => 'confirmation', :message => _("You have successfully logged out.")}
258
+
259
+ @message[:message] <<
260
+ _(" Please click on the following link to continue:") if @continue_url
261
+
262
+ @lt = generate_login_ticket
263
+
264
+ if @gateway && @service
265
+ redirect(@service, :status => 303)
266
+ elsif @continue_url
267
+ render :logout
268
+ else
269
+ render :login
270
+ end
271
+ end
272
+ end
273
+
274
+ # 2.4
275
+ class Validate < R '/validate'
276
+ include CASServer::CAS
277
+
278
+ # 2.4.1
279
+ def get
280
+ CASServer::Utils::log_controller_action(self.class, input)
281
+
282
+ # required
283
+ @service = clean_service_url(input['service'])
284
+ @ticket = input['ticket']
285
+ # optional
286
+ @renew = input['renew']
287
+
288
+ st, @error = validate_service_ticket(@service, @ticket)
289
+ @success = st && !@error
290
+
291
+ @username = st.username if @success
292
+
293
+ @status = CASServer::Controllers.response_status_from_error(@error) if @error
294
+
295
+ render :validate
296
+ end
297
+ end
298
+
299
+ # 2.5
300
+ class ServiceValidate < R '/serviceValidate'
301
+ include CASServer::CAS
302
+
303
+ # 2.5.1
304
+ def get
305
+ CASServer::Utils::log_controller_action(self.class, input)
306
+
307
+ # required
308
+ @service = clean_service_url(input['service'])
309
+ @ticket = input['ticket']
310
+ # optional
311
+ @pgt_url = input['pgtUrl']
312
+ @renew = input['renew']
313
+
314
+ st, @error = validate_service_ticket(@service, @ticket)
315
+ @success = st && !@error
316
+
317
+ if @success
318
+ @username = st.username
319
+ if @pgt_url
320
+ pgt = generate_proxy_granting_ticket(@pgt_url, st)
321
+ @pgtiou = pgt.iou if pgt
322
+ end
323
+ @extra_attributes = st.granted_by_tgt.extra_attributes || {}
324
+ end
325
+
326
+ @status = CASServer::Controllers.response_status_from_error(@error) if @error
327
+
328
+ render :service_validate
329
+ end
330
+ end
331
+
332
+ # 2.6
333
+ class ProxyValidate < R '/proxyValidate'
334
+ include CASServer::CAS
335
+
336
+ # 2.6.1
337
+ def get
338
+ CASServer::Utils::log_controller_action(self.class, input)
339
+
340
+ # required
341
+ @service = clean_service_url(input['service'])
342
+ @ticket = input['ticket']
343
+ # optional
344
+ @pgt_url = input['pgtUrl']
345
+ @renew = input['renew']
346
+
347
+ @proxies = []
348
+
349
+ t, @error = validate_proxy_ticket(@service, @ticket)
350
+ @success = t && !@error
351
+
352
+ @extra_attributes = {}
353
+ if @success
354
+ @username = t.username
355
+
356
+ if t.kind_of? CASServer::Models::ProxyTicket
357
+ @proxies << t.granted_by_pgt.service_ticket.service
358
+ end
359
+
360
+ if @pgt_url
361
+ pgt = generate_proxy_granting_ticket(@pgt_url, t)
362
+ @pgtiou = pgt.iou if pgt
363
+ end
364
+
365
+ @extra_attributes = t.granted_by_tgt.extra_attributes || {}
366
+ end
367
+
368
+ @status = CASServer::Controllers.response_status_from_error(@error) if @error
369
+
370
+ render :proxy_validate
371
+ end
372
+ end
373
+
374
+ class Proxy < R '/proxy'
375
+ include CASServer::CAS
376
+
377
+ # 2.7
378
+ def get
379
+ CASServer::Utils::log_controller_action(self.class, input)
380
+
381
+ # required
382
+ @ticket = input['pgt']
383
+ @target_service = input['targetService']
384
+
385
+ pgt, @error = validate_proxy_granting_ticket(@ticket)
386
+ @success = pgt && !@error
387
+
388
+ if @success
389
+ @pt = generate_proxy_ticket(@target_service, pgt)
390
+ end
391
+
392
+ @status = CASServer::Controllers.response_status_from_error(@error) if @error
393
+
394
+ render :proxy
395
+ end
396
+ end
397
+
398
+ # Controller for obtaining login tickets.
399
+ # This is useful when you want to build a custom login form located on a
400
+ # remote server. Your form will have to include a valid login ticket
401
+ # value, and this can be fetched from the CAS server using this controller'
402
+ # POST method.
403
+ class LoginTicketDispenser < R '/loginTicket'
404
+ include CASServer::CAS
405
+
406
+ def get
407
+ CASServer::Utils::log_controller_action(self.class, input)
408
+ $LOG.error("Tried to use login ticket dispenser with get method!")
409
+ @status = 422
410
+ _("To generate a login ticket, you must make a POST request.")
411
+ end
412
+
413
+ # Renders a page with a login ticket (and only the login ticket)
414
+ # in the response body.
415
+ def post
416
+ CASServer::Utils::log_controller_action(self.class, input)
417
+ lt = generate_login_ticket
418
+
419
+ $LOG.debug("Dispensing login ticket #{lt} to host #{(@env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']).inspect}")
420
+
421
+ @lt = lt.ticket
422
+
423
+ @lt
424
+ end
425
+ end
426
+
427
+ # class Themes < R '/themes/(.+)'
428
+ # MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript',
429
+ # '.jpg' => 'image/jpeg'}
430
+ # PATH = $CONF.themes_dir || File.expand_path(File.dirname(__FILE__))+'/../themes'
431
+ #
432
+ # def get(path)
433
+ # headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain"
434
+ # unless path.include? ".." # prevent directory traversal attacks
435
+ # headers['X-Sendfile'] = "#{PATH}/#{path}"
436
+ # data = File.read(headers['X-Sendfile'])
437
+ # headers['Content-Length'] = data.size.to_s # Rack Camping adapter chokes without this
438
+ # return data
439
+ # else
440
+ # status = "403"
441
+ # "403 - Invalid path"
442
+ # end
443
+ # end
444
+ # end
445
+
446
+ def response_status_from_error(error)
447
+ case error.code.to_s
448
+ when /^INVALID_/, 'BAD_PGT'
449
+ 422
450
+ when 'INTERNAL_ERROR'
451
+ 500
452
+ else
453
+ 500
454
+ end
455
+ end
456
+ module_function :response_status_from_error
457
+ end
@@ -0,0 +1,19 @@
1
+ if File.exists?(picnic = File.expand_path(File.dirname(File.expand_path(__FILE__))+'/../../vendor/picnic/lib'))
2
+ puts "Loading picnic from #{picnic.inspect}..."
3
+ $: << picnic
4
+ elsif File.exists?(picnic = File.expand_path(File.dirname(File.expand_path(__FILE__))+'/../../../picnic/lib'))
5
+ puts "Loading picnic from #{picnic.inspect}..."
6
+ $: << picnic
7
+ else
8
+ puts "Loading picnic from rubygems..."
9
+ require 'rubygems'
10
+
11
+ begin
12
+ # Try to load dev version of picnic if available (for example 'zuk-picnic' from Github)
13
+ gem /^.*?-picnic$/
14
+ rescue Gem::LoadError
15
+ gem 'picnic'
16
+ end
17
+ end
18
+
19
+
@@ -0,0 +1,82 @@
1
+ require "gettext"
2
+ require "gettext/cgi"
3
+
4
+ module CASServer
5
+ include GetText
6
+ bindtextdomain("rubycas-server", :path => File.join(File.dirname(File.expand_path(__FILE__)), "../../locale"))
7
+
8
+ def service(*a)
9
+ GetText.locale = determine_locale
10
+ #puts GetText.locale.inspect
11
+ super(*a)
12
+ end
13
+
14
+ def determine_locale
15
+
16
+ source = nil
17
+ lang = case
18
+ when !input['lang'].blank?
19
+ source = "'lang' request variable"
20
+ cookies['lang'] = input['lang']
21
+ input['lang']
22
+ when !cookies['lang'].blank?
23
+ source = "'lang' cookie"
24
+ cookies['lang']
25
+ when !@env['HTTP_ACCEPT_LANGUAGE'].blank?
26
+ source = "'HTTP_ACCEPT_LANGUAGE' header"
27
+ lang = @env['HTTP_ACCEPT_LANGUAGE']
28
+ when !@env['HTTP_USER_AGENT'].blank? && @env['HTTP_USER_AGENT'] =~ /[^a-z]([a-z]{2}(-[a-z]{2})?)[^a-z]/i
29
+ source = "'HTTP_USER_AGENT' header"
30
+ $~[1]
31
+ when !$CONF['default_locale'].blank?
32
+ source = "'default_locale' config option"
33
+ $CONF[:default_locale]
34
+ else
35
+ source = "default"
36
+ "en"
37
+ end
38
+
39
+ $LOG.debug "Detected locale is #{lang.inspect} (from #{source})"
40
+
41
+ lang.gsub!('_','-')
42
+
43
+ # TODO: Need to confirm that this method of splitting the accepted
44
+ # language string is correct.
45
+ if lang =~ /[,;\|]/
46
+ langs = lang.split(/[,;\|]/)
47
+ else
48
+ langs = [lang]
49
+ end
50
+
51
+ # TODO: This method of selecting the desired language might not be
52
+ # standards-compliant. For example, http://www.w3.org/TR/ltli/
53
+ # suggests that de-de and de-*-DE might be acceptable identifiers
54
+ # for selecting various wildcards. The algorithm below does not
55
+ # currently support anything like this.
56
+
57
+ available = available_locales
58
+
59
+ # Try to pick a locale exactly matching the desired identifier, otherwise
60
+ # fall back to locale without region (i.e. given "en-US; de-DE", we would
61
+ # first look for "en-US", then "en", then "de-DE", then "de").
62
+
63
+ chosen_lang = nil
64
+ langs.each do |l|
65
+ a = available.find{|a| a == l || a =~ Regexp.new("#{l}-\w*")}
66
+ if a
67
+ chosen_lang = a
68
+ break
69
+ end
70
+ end
71
+
72
+ chosen_lang = "en" if chosen_lang.blank?
73
+
74
+ $LOG.debug "Chosen locale is #{chosen_lang.inspect}"
75
+
76
+ return chosen_lang
77
+ end
78
+
79
+ def available_locales
80
+ (Dir.glob(File.join(File.dirname(File.expand_path(__FILE__)), "../../locale/[a-z]*")).map{|path| File.basename(path)} << "en").uniq.collect{|l| l.gsub('_','-')}
81
+ end
82
+ end