##// END OF EJS Templates
Merged r9798 to r9801 from trunk....
Jean-Philippe Lang -
r9620:fe1a152e02b4
parent child
Show More
@@ -1,524 +1,524
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25
26 26 layout 'base'
27 27 exempt_from_layout 'builder', 'rsb'
28 28
29 29 protect_from_forgery
30 30 def handle_unverified_request
31 31 super
32 32 cookies.delete(:autologin)
33 33 end
34 34
35 35 # FIXME: Remove this when all of Rack and Rails have learned how to
36 36 # properly use encodings
37 37 before_filter :params_filter
38 38
39 39 def params_filter
40 40 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
41 41 self.utf8nize!(params)
42 42 end
43 43 end
44 44
45 45 def utf8nize!(obj)
46 46 if obj.frozen?
47 47 obj
48 48 elsif obj.is_a? String
49 49 obj.respond_to?(:force_encoding) ? obj.force_encoding("UTF-8") : obj
50 50 elsif obj.is_a? Hash
51 51 obj.each {|k, v| obj[k] = self.utf8nize!(v)}
52 52 elsif obj.is_a? Array
53 53 obj.each {|v| self.utf8nize!(v)}
54 54 else
55 55 obj
56 56 end
57 57 end
58 58
59 59 before_filter :user_setup, :check_if_login_required, :set_localization
60 60 filter_parameter_logging :password
61 61
62 62 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
63 63 rescue_from ::Unauthorized, :with => :deny_access
64 64
65 65 include Redmine::Search::Controller
66 66 include Redmine::MenuManager::MenuController
67 67 helper Redmine::MenuManager::MenuHelper
68 68
69 69 Redmine::Scm::Base.all.each do |scm|
70 70 require_dependency "repository/#{scm.underscore}"
71 71 end
72 72
73 73 def user_setup
74 74 # Check the settings cache for each request
75 75 Setting.check_cache
76 76 # Find the current user
77 77 User.current = find_current_user
78 78 end
79 79
80 80 # Returns the current user or nil if no user is logged in
81 81 # and starts a session if needed
82 82 def find_current_user
83 83 if session[:user_id]
84 84 # existing session
85 85 (User.active.find(session[:user_id]) rescue nil)
86 86 elsif cookies[:autologin] && Setting.autologin?
87 87 # auto-login feature starts a new session
88 88 user = User.try_to_autologin(cookies[:autologin])
89 89 session[:user_id] = user.id if user
90 90 user
91 91 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
92 92 # RSS key authentication does not start a session
93 93 User.find_by_rss_key(params[:key])
94 94 elsif Setting.rest_api_enabled? && accept_api_auth?
95 95 if (key = api_key_from_request)
96 96 # Use API key
97 97 User.find_by_api_key(key)
98 98 else
99 99 # HTTP Basic, either username/password or API key/random
100 100 authenticate_with_http_basic do |username, password|
101 101 User.try_to_login(username, password) || User.find_by_api_key(username)
102 102 end
103 103 end
104 104 end
105 105 end
106 106
107 107 # Sets the logged in user
108 108 def logged_user=(user)
109 109 reset_session
110 110 if user && user.is_a?(User)
111 111 User.current = user
112 112 session[:user_id] = user.id
113 113 else
114 114 User.current = User.anonymous
115 115 end
116 116 end
117 117
118 118 # Logs out current user
119 119 def logout_user
120 120 if User.current.logged?
121 121 cookies.delete :autologin
122 122 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
123 123 self.logged_user = nil
124 124 end
125 125 end
126 126
127 127 # check if login is globally required to access the application
128 128 def check_if_login_required
129 129 # no check needed if user is already logged in
130 130 return true if User.current.logged?
131 131 require_login if Setting.login_required?
132 132 end
133 133
134 134 def set_localization
135 135 lang = nil
136 136 if User.current.logged?
137 137 lang = find_language(User.current.language)
138 138 end
139 139 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
140 140 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
141 141 if !accept_lang.blank?
142 142 accept_lang = accept_lang.downcase
143 143 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
144 144 end
145 145 end
146 146 lang ||= Setting.default_language
147 147 set_language_if_valid(lang)
148 148 end
149 149
150 150 def require_login
151 151 if !User.current.logged?
152 152 # Extract only the basic url parameters on non-GET requests
153 153 if request.get?
154 154 url = url_for(params)
155 155 else
156 156 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
157 157 end
158 158 respond_to do |format|
159 159 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
160 160 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
161 161 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
162 162 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
163 163 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
164 164 end
165 165 return false
166 166 end
167 167 true
168 168 end
169 169
170 170 def require_admin
171 171 return unless require_login
172 172 if !User.current.admin?
173 173 render_403
174 174 return false
175 175 end
176 176 true
177 177 end
178 178
179 179 def deny_access
180 180 User.current.logged? ? render_403 : require_login
181 181 end
182 182
183 183 # Authorize the user for the requested action
184 184 def authorize(ctrl = params[:controller], action = params[:action], global = false)
185 185 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
186 186 if allowed
187 187 true
188 188 else
189 189 if @project && @project.archived?
190 190 render_403 :message => :notice_not_authorized_archived_project
191 191 else
192 192 deny_access
193 193 end
194 194 end
195 195 end
196 196
197 197 # Authorize the user for the requested action outside a project
198 198 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
199 199 authorize(ctrl, action, global)
200 200 end
201 201
202 202 # Find project of id params[:id]
203 203 def find_project
204 204 @project = Project.find(params[:id])
205 205 rescue ActiveRecord::RecordNotFound
206 206 render_404
207 207 end
208 208
209 209 # Find project of id params[:project_id]
210 210 def find_project_by_project_id
211 211 @project = Project.find(params[:project_id])
212 212 rescue ActiveRecord::RecordNotFound
213 213 render_404
214 214 end
215 215
216 216 # Find a project based on params[:project_id]
217 217 # TODO: some subclasses override this, see about merging their logic
218 218 def find_optional_project
219 219 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
220 220 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
221 221 allowed ? true : deny_access
222 222 rescue ActiveRecord::RecordNotFound
223 223 render_404
224 224 end
225 225
226 226 # Finds and sets @project based on @object.project
227 227 def find_project_from_association
228 228 render_404 unless @object.present?
229 229
230 230 @project = @object.project
231 231 end
232 232
233 233 def find_model_object
234 234 model = self.class.read_inheritable_attribute('model_object')
235 235 if model
236 236 @object = model.find(params[:id])
237 237 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
238 238 end
239 239 rescue ActiveRecord::RecordNotFound
240 240 render_404
241 241 end
242 242
243 243 def self.model_object(model)
244 244 write_inheritable_attribute('model_object', model)
245 245 end
246 246
247 247 # Filter for bulk issue operations
248 248 def find_issues
249 249 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
250 250 raise ActiveRecord::RecordNotFound if @issues.empty?
251 251 if @issues.detect {|issue| !issue.visible?}
252 252 deny_access
253 253 return
254 254 end
255 255 @projects = @issues.collect(&:project).compact.uniq
256 256 @project = @projects.first if @projects.size == 1
257 257 rescue ActiveRecord::RecordNotFound
258 258 render_404
259 259 end
260 260
261 261 # make sure that the user is a member of the project (or admin) if project is private
262 262 # used as a before_filter for actions that do not require any particular permission on the project
263 263 def check_project_privacy
264 264 if @project && @project.active?
265 265 if @project.visible?
266 266 true
267 267 else
268 268 deny_access
269 269 end
270 270 else
271 271 @project = nil
272 272 render_404
273 273 false
274 274 end
275 275 end
276 276
277 277 def back_url
278 278 params[:back_url] || request.env['HTTP_REFERER']
279 279 end
280 280
281 281 def redirect_back_or_default(default)
282 282 back_url = CGI.unescape(params[:back_url].to_s)
283 283 if !back_url.blank?
284 284 begin
285 285 uri = URI.parse(back_url)
286 286 # do not redirect user to another host or to the login or register page
287 287 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
288 288 redirect_to(back_url)
289 289 return
290 290 end
291 291 rescue URI::InvalidURIError
292 292 # redirect to default
293 293 end
294 294 end
295 295 redirect_to default
296 296 false
297 297 end
298 298
299 299 def render_403(options={})
300 300 @project = nil
301 301 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
302 302 return false
303 303 end
304 304
305 305 def render_404(options={})
306 306 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
307 307 return false
308 308 end
309 309
310 310 # Renders an error response
311 311 def render_error(arg)
312 312 arg = {:message => arg} unless arg.is_a?(Hash)
313 313
314 314 @message = arg[:message]
315 315 @message = l(@message) if @message.is_a?(Symbol)
316 316 @status = arg[:status] || 500
317 317
318 318 respond_to do |format|
319 319 format.html {
320 320 render :template => 'common/error', :layout => use_layout, :status => @status
321 321 }
322 322 format.atom { head @status }
323 323 format.xml { head @status }
324 324 format.js { head @status }
325 325 format.json { head @status }
326 326 end
327 327 end
328 328
329 329 # Filter for actions that provide an API response
330 330 # but have no HTML representation for non admin users
331 331 def require_admin_or_api_request
332 332 return true if api_request?
333 333 if User.current.admin?
334 334 true
335 335 elsif User.current.logged?
336 336 render_error(:status => 406)
337 337 else
338 338 deny_access
339 339 end
340 340 end
341 341
342 342 # Picks which layout to use based on the request
343 343 #
344 344 # @return [boolean, string] name of the layout to use or false for no layout
345 345 def use_layout
346 346 request.xhr? ? false : 'base'
347 347 end
348 348
349 349 def invalid_authenticity_token
350 350 if api_request?
351 351 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
352 352 end
353 353 render_error "Invalid form authenticity token."
354 354 end
355 355
356 356 def render_feed(items, options={})
357 357 @items = items || []
358 358 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
359 359 @items = @items.slice(0, Setting.feeds_limit.to_i)
360 360 @title = options[:title] || Setting.app_title
361 361 render :template => "common/feed.atom", :layout => false,
362 362 :content_type => 'application/atom+xml'
363 363 end
364 364
365 365 def self.accept_rss_auth(*actions)
366 366 if actions.any?
367 367 write_inheritable_attribute('accept_rss_auth_actions', actions)
368 368 else
369 369 read_inheritable_attribute('accept_rss_auth_actions') || []
370 370 end
371 371 end
372 372
373 373 def accept_rss_auth?(action=action_name)
374 374 self.class.accept_rss_auth.include?(action.to_sym)
375 375 end
376 376
377 377 def self.accept_api_auth(*actions)
378 378 if actions.any?
379 379 write_inheritable_attribute('accept_api_auth_actions', actions)
380 380 else
381 381 read_inheritable_attribute('accept_api_auth_actions') || []
382 382 end
383 383 end
384 384
385 385 def accept_api_auth?(action=action_name)
386 386 self.class.accept_api_auth.include?(action.to_sym)
387 387 end
388 388
389 389 # Returns the number of objects that should be displayed
390 390 # on the paginated list
391 391 def per_page_option
392 392 per_page = nil
393 393 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
394 394 per_page = params[:per_page].to_s.to_i
395 395 session[:per_page] = per_page
396 396 elsif session[:per_page]
397 397 per_page = session[:per_page]
398 398 else
399 399 per_page = Setting.per_page_options_array.first || 25
400 400 end
401 401 per_page
402 402 end
403 403
404 404 # Returns offset and limit used to retrieve objects
405 405 # for an API response based on offset, limit and page parameters
406 406 def api_offset_and_limit(options=params)
407 407 if options[:offset].present?
408 408 offset = options[:offset].to_i
409 409 if offset < 0
410 410 offset = 0
411 411 end
412 412 end
413 413 limit = options[:limit].to_i
414 414 if limit < 1
415 415 limit = 25
416 416 elsif limit > 100
417 417 limit = 100
418 418 end
419 419 if offset.nil? && options[:page].present?
420 420 offset = (options[:page].to_i - 1) * limit
421 421 offset = 0 if offset < 0
422 422 end
423 423 offset ||= 0
424 424
425 425 [offset, limit]
426 426 end
427 427
428 428 # qvalues http header parser
429 429 # code taken from webrick
430 430 def parse_qvalues(value)
431 431 tmp = []
432 432 if value
433 433 parts = value.split(/,\s*/)
434 434 parts.each {|part|
435 435 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
436 436 val = m[1]
437 437 q = (m[2] or 1).to_f
438 438 tmp.push([val, q])
439 439 end
440 440 }
441 441 tmp = tmp.sort_by{|val, q| -q}
442 442 tmp.collect!{|val, q| val}
443 443 end
444 444 return tmp
445 445 rescue
446 446 nil
447 447 end
448 448
449 449 # Returns a string that can be used as filename value in Content-Disposition header
450 450 def filename_for_content_disposition(name)
451 451 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
452 452 end
453 453
454 454 def api_request?
455 455 %w(xml json).include? params[:format]
456 456 end
457 457
458 458 # Returns the API key present in the request
459 459 def api_key_from_request
460 460 if params[:key].present?
461 params[:key]
461 params[:key].to_s
462 462 elsif request.headers["X-Redmine-API-Key"].present?
463 request.headers["X-Redmine-API-Key"]
463 request.headers["X-Redmine-API-Key"].to_s
464 464 end
465 465 end
466 466
467 467 # Renders a warning flash if obj has unsaved attachments
468 468 def render_attachment_warning_if_needed(obj)
469 469 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
470 470 end
471 471
472 472 # Sets the `flash` notice or error based the number of issues that did not save
473 473 #
474 474 # @param [Array, Issue] issues all of the saved and unsaved Issues
475 475 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
476 476 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
477 477 if unsaved_issue_ids.empty?
478 478 flash[:notice] = l(:notice_successful_update) unless issues.empty?
479 479 else
480 480 flash[:error] = l(:notice_failed_to_save_issues,
481 481 :count => unsaved_issue_ids.size,
482 482 :total => issues.size,
483 483 :ids => '#' + unsaved_issue_ids.join(', #'))
484 484 end
485 485 end
486 486
487 487 # Rescues an invalid query statement. Just in case...
488 488 def query_statement_invalid(exception)
489 489 logger.error "Query::StatementInvalid: #{exception.message}" if logger
490 490 session.delete(:query)
491 491 sort_clear if respond_to?(:sort_clear)
492 492 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
493 493 end
494 494
495 495 # Renders API response on validation failure
496 496 def render_validation_errors(objects)
497 497 if objects.is_a?(Array)
498 498 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
499 499 else
500 500 @error_messages = objects.errors.full_messages
501 501 end
502 502 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => false
503 503 end
504 504
505 505 # Overrides #default_template so that the api template
506 506 # is used automatically if it exists
507 507 def default_template(action_name = self.action_name)
508 508 if api_request?
509 509 begin
510 510 return self.view_paths.find_template(default_template_name(action_name), 'api')
511 511 rescue ::ActionView::MissingTemplate
512 512 # the api template was not found
513 513 # fallback to the default behaviour
514 514 end
515 515 end
516 516 super
517 517 end
518 518
519 519 # Overrides #pick_layout so that #render with no arguments
520 520 # doesn't use the layout for api requests
521 521 def pick_layout(*args)
522 522 api_request? ? nil : super
523 523 end
524 524 end
@@ -1,657 +1,660
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Account statuses
24 24 STATUS_ANONYMOUS = 0
25 25 STATUS_ACTIVE = 1
26 26 STATUS_REGISTERED = 2
27 27 STATUS_LOCKED = 3
28 28
29 29 # Different ways of displaying/sorting users
30 30 USER_FORMATS = {
31 31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
32 32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
33 33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
34 34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
35 35 :username => {:string => '#{login}', :order => %w(login id)},
36 36 }
37 37
38 38 MAIL_NOTIFICATION_OPTIONS = [
39 39 ['all', :label_user_mail_option_all],
40 40 ['selected', :label_user_mail_option_selected],
41 41 ['only_my_events', :label_user_mail_option_only_my_events],
42 42 ['only_assigned', :label_user_mail_option_only_assigned],
43 43 ['only_owner', :label_user_mail_option_only_owner],
44 44 ['none', :label_user_mail_option_none]
45 45 ]
46 46
47 47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
48 48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
49 49 has_many :changesets, :dependent => :nullify
50 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 53 belongs_to :auth_source
54 54
55 55 # Active non-anonymous users scope
56 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 57 named_scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
58 58 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
59 59
60 60 acts_as_customizable
61 61
62 62 attr_accessor :password, :password_confirmation
63 63 attr_accessor :last_before_login_on
64 64 # Prevents unauthorized assignments
65 65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
66 66
67 67 LOGIN_LENGTH_LIMIT = 60
68 68 MAIL_LENGTH_LIMIT = 60
69 69
70 70 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
71 71 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
72 72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
73 73 # Login must contain lettres, numbers, underscores only
74 74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
75 75 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
76 76 validates_length_of :firstname, :lastname, :maximum => 30
77 77 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
78 78 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
79 79 validates_confirmation_of :password, :allow_nil => true
80 80 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
81 81 validate :validate_password_length
82 82
83 83 before_create :set_mail_notification
84 84 before_save :update_hashed_password
85 85 before_destroy :remove_references_before_destroy
86 86
87 87 named_scope :in_group, lambda {|group|
88 88 group_id = group.is_a?(Group) ? group.id : group.to_i
89 89 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
90 90 }
91 91 named_scope :not_in_group, lambda {|group|
92 92 group_id = group.is_a?(Group) ? group.id : group.to_i
93 93 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
94 94 }
95 95
96 96 def set_mail_notification
97 97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
98 98 true
99 99 end
100 100
101 101 def update_hashed_password
102 102 # update hashed_password if password was set
103 103 if self.password && self.auth_source_id.blank?
104 104 salt_password(password)
105 105 end
106 106 end
107 107
108 108 def reload(*args)
109 109 @name = nil
110 110 @projects_by_role = nil
111 111 super
112 112 end
113 113
114 114 def mail=(arg)
115 115 write_attribute(:mail, arg.to_s.strip)
116 116 end
117 117
118 118 def identity_url=(url)
119 119 if url.blank?
120 120 write_attribute(:identity_url, '')
121 121 else
122 122 begin
123 123 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
124 124 rescue OpenIdAuthentication::InvalidOpenId
125 125 # Invlaid url, don't save
126 126 end
127 127 end
128 128 self.read_attribute(:identity_url)
129 129 end
130 130
131 131 # Returns the user that matches provided login and password, or nil
132 132 def self.try_to_login(login, password)
133 login = login.to_s
134 password = password.to_s
135
133 136 # Make sure no one can sign in with an empty password
134 return nil if password.to_s.empty?
137 return nil if password.empty?
135 138 user = find_by_login(login)
136 139 if user
137 140 # user is already in local database
138 141 return nil if !user.active?
139 142 if user.auth_source
140 143 # user has an external authentication method
141 144 return nil unless user.auth_source.authenticate(login, password)
142 145 else
143 146 # authentication with local password
144 147 return nil unless user.check_password?(password)
145 148 end
146 149 else
147 150 # user is not yet registered, try to authenticate with available sources
148 151 attrs = AuthSource.authenticate(login, password)
149 152 if attrs
150 153 user = new(attrs)
151 154 user.login = login
152 155 user.language = Setting.default_language
153 156 if user.save
154 157 user.reload
155 158 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
156 159 end
157 160 end
158 161 end
159 162 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
160 163 user
161 164 rescue => text
162 165 raise text
163 166 end
164 167
165 168 # Returns the user who matches the given autologin +key+ or nil
166 169 def self.try_to_autologin(key)
167 tokens = Token.find_all_by_action_and_value('autologin', key)
170 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
168 171 # Make sure there's only 1 token that matches the key
169 172 if tokens.size == 1
170 173 token = tokens.first
171 174 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
172 175 token.user.update_attribute(:last_login_on, Time.now)
173 176 token.user
174 177 end
175 178 end
176 179 end
177 180
178 181 def self.name_formatter(formatter = nil)
179 182 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
180 183 end
181 184
182 185 # Returns an array of fields names than can be used to make an order statement for users
183 186 # according to how user names are displayed
184 187 # Examples:
185 188 #
186 189 # User.fields_for_order_statement => ['users.login', 'users.id']
187 190 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
188 191 def self.fields_for_order_statement(table=nil)
189 192 table ||= table_name
190 193 name_formatter[:order].map {|field| "#{table}.#{field}"}
191 194 end
192 195
193 196 # Return user's full name for display
194 197 def name(formatter = nil)
195 198 f = self.class.name_formatter(formatter)
196 199 if formatter
197 200 eval('"' + f[:string] + '"')
198 201 else
199 202 @name ||= eval('"' + f[:string] + '"')
200 203 end
201 204 end
202 205
203 206 def active?
204 207 self.status == STATUS_ACTIVE
205 208 end
206 209
207 210 def registered?
208 211 self.status == STATUS_REGISTERED
209 212 end
210 213
211 214 def locked?
212 215 self.status == STATUS_LOCKED
213 216 end
214 217
215 218 def activate
216 219 self.status = STATUS_ACTIVE
217 220 end
218 221
219 222 def register
220 223 self.status = STATUS_REGISTERED
221 224 end
222 225
223 226 def lock
224 227 self.status = STATUS_LOCKED
225 228 end
226 229
227 230 def activate!
228 231 update_attribute(:status, STATUS_ACTIVE)
229 232 end
230 233
231 234 def register!
232 235 update_attribute(:status, STATUS_REGISTERED)
233 236 end
234 237
235 238 def lock!
236 239 update_attribute(:status, STATUS_LOCKED)
237 240 end
238 241
239 242 # Returns true if +clear_password+ is the correct user's password, otherwise false
240 243 def check_password?(clear_password)
241 244 if auth_source_id.present?
242 245 auth_source.authenticate(self.login, clear_password)
243 246 else
244 247 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
245 248 end
246 249 end
247 250
248 251 # Generates a random salt and computes hashed_password for +clear_password+
249 252 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
250 253 def salt_password(clear_password)
251 254 self.salt = User.generate_salt
252 255 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
253 256 end
254 257
255 258 # Does the backend storage allow this user to change their password?
256 259 def change_password_allowed?
257 260 return true if auth_source.nil?
258 261 return auth_source.allow_password_changes?
259 262 end
260 263
261 264 # Generate and set a random password. Useful for automated user creation
262 265 # Based on Token#generate_token_value
263 266 #
264 267 def random_password
265 268 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
266 269 password = ''
267 270 40.times { |i| password << chars[rand(chars.size-1)] }
268 271 self.password = password
269 272 self.password_confirmation = password
270 273 self
271 274 end
272 275
273 276 def pref
274 277 self.preference ||= UserPreference.new(:user => self)
275 278 end
276 279
277 280 def time_zone
278 281 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
279 282 end
280 283
281 284 def wants_comments_in_reverse_order?
282 285 self.pref[:comments_sorting] == 'desc'
283 286 end
284 287
285 288 # Return user's RSS key (a 40 chars long string), used to access feeds
286 289 def rss_key
287 290 if rss_token.nil?
288 291 create_rss_token(:action => 'feeds')
289 292 end
290 293 rss_token.value
291 294 end
292 295
293 296 # Return user's API key (a 40 chars long string), used to access the API
294 297 def api_key
295 298 if api_token.nil?
296 299 create_api_token(:action => 'api')
297 300 end
298 301 api_token.value
299 302 end
300 303
301 304 # Return an array of project ids for which the user has explicitly turned mail notifications on
302 305 def notified_projects_ids
303 306 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
304 307 end
305 308
306 309 def notified_project_ids=(ids)
307 310 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
308 311 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
309 312 @notified_projects_ids = nil
310 313 notified_projects_ids
311 314 end
312 315
313 316 def valid_notification_options
314 317 self.class.valid_notification_options(self)
315 318 end
316 319
317 320 # Only users that belong to more than 1 project can select projects for which they are notified
318 321 def self.valid_notification_options(user=nil)
319 322 # Note that @user.membership.size would fail since AR ignores
320 323 # :include association option when doing a count
321 324 if user.nil? || user.memberships.length < 1
322 325 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
323 326 else
324 327 MAIL_NOTIFICATION_OPTIONS
325 328 end
326 329 end
327 330
328 331 # Find a user account by matching the exact login and then a case-insensitive
329 332 # version. Exact matches will be given priority.
330 333 def self.find_by_login(login)
331 334 # First look for an exact match
332 335 user = all(:conditions => {:login => login}).detect {|u| u.login == login}
333 336 unless user
334 337 # Fail over to case-insensitive if none was found
335 338 user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
336 339 end
337 340 user
338 341 end
339 342
340 343 def self.find_by_rss_key(key)
341 token = Token.find_by_value(key)
344 token = Token.find_by_action_and_value('feeds', key.to_s)
342 345 token && token.user.active? ? token.user : nil
343 346 end
344 347
345 348 def self.find_by_api_key(key)
346 token = Token.find_by_action_and_value('api', key)
349 token = Token.find_by_action_and_value('api', key.to_s)
347 350 token && token.user.active? ? token.user : nil
348 351 end
349 352
350 353 # Makes find_by_mail case-insensitive
351 354 def self.find_by_mail(mail)
352 355 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
353 356 end
354 357
355 358 # Returns true if the default admin account can no longer be used
356 359 def self.default_admin_account_changed?
357 360 !User.active.find_by_login("admin").try(:check_password?, "admin")
358 361 end
359 362
360 363 def to_s
361 364 name
362 365 end
363 366
364 367 # Returns the current day according to user's time zone
365 368 def today
366 369 if time_zone.nil?
367 370 Date.today
368 371 else
369 372 Time.now.in_time_zone(time_zone).to_date
370 373 end
371 374 end
372 375
373 376 def logged?
374 377 true
375 378 end
376 379
377 380 def anonymous?
378 381 !logged?
379 382 end
380 383
381 384 # Return user's roles for project
382 385 def roles_for_project(project)
383 386 roles = []
384 387 # No role on archived projects
385 388 return roles unless project && project.active?
386 389 if logged?
387 390 # Find project membership
388 391 membership = memberships.detect {|m| m.project_id == project.id}
389 392 if membership
390 393 roles = membership.roles
391 394 else
392 395 @role_non_member ||= Role.non_member
393 396 roles << @role_non_member
394 397 end
395 398 else
396 399 @role_anonymous ||= Role.anonymous
397 400 roles << @role_anonymous
398 401 end
399 402 roles
400 403 end
401 404
402 405 # Return true if the user is a member of project
403 406 def member_of?(project)
404 407 !roles_for_project(project).detect {|role| role.member?}.nil?
405 408 end
406 409
407 410 # Returns a hash of user's projects grouped by roles
408 411 def projects_by_role
409 412 return @projects_by_role if @projects_by_role
410 413
411 414 @projects_by_role = Hash.new {|h,k| h[k]=[]}
412 415 memberships.each do |membership|
413 416 membership.roles.each do |role|
414 417 @projects_by_role[role] << membership.project if membership.project
415 418 end
416 419 end
417 420 @projects_by_role.each do |role, projects|
418 421 projects.uniq!
419 422 end
420 423
421 424 @projects_by_role
422 425 end
423 426
424 427 # Returns true if user is arg or belongs to arg
425 428 def is_or_belongs_to?(arg)
426 429 if arg.is_a?(User)
427 430 self == arg
428 431 elsif arg.is_a?(Group)
429 432 arg.users.include?(self)
430 433 else
431 434 false
432 435 end
433 436 end
434 437
435 438 # Return true if the user is allowed to do the specified action on a specific context
436 439 # Action can be:
437 440 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
438 441 # * a permission Symbol (eg. :edit_project)
439 442 # Context can be:
440 443 # * a project : returns true if user is allowed to do the specified action on this project
441 444 # * an array of projects : returns true if user is allowed on every project
442 445 # * nil with options[:global] set : check if user has at least one role allowed for this action,
443 446 # or falls back to Non Member / Anonymous permissions depending if the user is logged
444 447 def allowed_to?(action, context, options={}, &block)
445 448 if context && context.is_a?(Project)
446 449 # No action allowed on archived projects
447 450 return false unless context.active?
448 451 # No action allowed on disabled modules
449 452 return false unless context.allows_to?(action)
450 453 # Admin users are authorized for anything else
451 454 return true if admin?
452 455
453 456 roles = roles_for_project(context)
454 457 return false unless roles
455 458 roles.detect {|role|
456 459 (context.is_public? || role.member?) &&
457 460 role.allowed_to?(action) &&
458 461 (block_given? ? yield(role, self) : true)
459 462 }
460 463 elsif context && context.is_a?(Array)
461 464 # Authorize if user is authorized on every element of the array
462 465 context.map do |project|
463 466 allowed_to?(action, project, options, &block)
464 467 end.inject do |memo,allowed|
465 468 memo && allowed
466 469 end
467 470 elsif options[:global]
468 471 # Admin users are always authorized
469 472 return true if admin?
470 473
471 474 # authorize if user has at least one role that has this permission
472 475 roles = memberships.collect {|m| m.roles}.flatten.uniq
473 476 roles << (self.logged? ? Role.non_member : Role.anonymous)
474 477 roles.detect {|role|
475 478 role.allowed_to?(action) &&
476 479 (block_given? ? yield(role, self) : true)
477 480 }
478 481 else
479 482 false
480 483 end
481 484 end
482 485
483 486 # Is the user allowed to do the specified action on any project?
484 487 # See allowed_to? for the actions and valid options.
485 488 def allowed_to_globally?(action, options, &block)
486 489 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
487 490 end
488 491
489 492 # Returns true if the user is allowed to delete his own account
490 493 def own_account_deletable?
491 494 Setting.unsubscribe? &&
492 495 (!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
493 496 end
494 497
495 498 safe_attributes 'login',
496 499 'firstname',
497 500 'lastname',
498 501 'mail',
499 502 'mail_notification',
500 503 'language',
501 504 'custom_field_values',
502 505 'custom_fields',
503 506 'identity_url'
504 507
505 508 safe_attributes 'status',
506 509 'auth_source_id',
507 510 :if => lambda {|user, current_user| current_user.admin?}
508 511
509 512 safe_attributes 'group_ids',
510 513 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
511 514
512 515 # Utility method to help check if a user should be notified about an
513 516 # event.
514 517 #
515 518 # TODO: only supports Issue events currently
516 519 def notify_about?(object)
517 520 case mail_notification
518 521 when 'all'
519 522 true
520 523 when 'selected'
521 524 # user receives notifications for created/assigned issues on unselected projects
522 525 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
523 526 true
524 527 else
525 528 false
526 529 end
527 530 when 'none'
528 531 false
529 532 when 'only_my_events'
530 533 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
531 534 true
532 535 else
533 536 false
534 537 end
535 538 when 'only_assigned'
536 539 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
537 540 true
538 541 else
539 542 false
540 543 end
541 544 when 'only_owner'
542 545 if object.is_a?(Issue) && object.author == self
543 546 true
544 547 else
545 548 false
546 549 end
547 550 else
548 551 false
549 552 end
550 553 end
551 554
552 555 def self.current=(user)
553 556 @current_user = user
554 557 end
555 558
556 559 def self.current
557 560 @current_user ||= User.anonymous
558 561 end
559 562
560 563 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
561 564 # one anonymous user per database.
562 565 def self.anonymous
563 566 anonymous_user = AnonymousUser.find(:first)
564 567 if anonymous_user.nil?
565 568 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
566 569 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
567 570 end
568 571 anonymous_user
569 572 end
570 573
571 574 # Salts all existing unsalted passwords
572 575 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
573 576 # This method is used in the SaltPasswords migration and is to be kept as is
574 577 def self.salt_unsalted_passwords!
575 578 transaction do
576 579 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
577 580 next if user.hashed_password.blank?
578 581 salt = User.generate_salt
579 582 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
580 583 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
581 584 end
582 585 end
583 586 end
584 587
585 588 protected
586 589
587 590 def validate_password_length
588 591 # Password length validation based on setting
589 592 if !password.nil? && password.size < Setting.password_min_length.to_i
590 593 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
591 594 end
592 595 end
593 596
594 597 private
595 598
596 599 # Removes references that are not handled by associations
597 600 # Things that are not deleted are reassociated with the anonymous user
598 601 def remove_references_before_destroy
599 602 return if self.id.nil?
600 603
601 604 substitute = User.anonymous
602 605 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
603 606 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
604 607 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
605 608 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
606 609 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
607 610 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
608 611 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
609 612 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
610 613 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
611 614 # Remove private queries and keep public ones
612 615 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
613 616 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
614 617 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
615 618 Token.delete_all ['user_id = ?', id]
616 619 Watcher.delete_all ['user_id = ?', id]
617 620 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
618 621 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
619 622 end
620 623
621 624 # Return password digest
622 625 def self.hash_password(clear_password)
623 626 Digest::SHA1.hexdigest(clear_password || "")
624 627 end
625 628
626 629 # Returns a 128bits random salt as a hex string (32 chars long)
627 630 def self.generate_salt
628 631 Redmine::Utils.random_hex(16)
629 632 end
630 633
631 634 end
632 635
633 636 class AnonymousUser < User
634 637 validate :validate_anonymous_uniqueness, :on => :create
635 638
636 639 def validate_anonymous_uniqueness
637 640 # There should be only one AnonymousUser in the database
638 641 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
639 642 end
640 643
641 644 def available_custom_fields
642 645 []
643 646 end
644 647
645 648 # Overrides a few properties
646 649 def logged?; false end
647 650 def admin; false end
648 651 def name(*args); I18n.t(:label_user_anonymous) end
649 652 def mail; nil end
650 653 def time_zone; nil end
651 654 def rss_key; nil end
652 655
653 656 # Anonymous user can not be destroyed
654 657 def destroy
655 658 false
656 659 end
657 660 end
General Comments 0
You need to be logged in to leave comments. Login now