##// END OF EJS Templates
Rails3: model: replace deprecated errors.add_to_base at validate_on_create of User...
Toshi MARUYAMA -
r7488:d986a35a9b85
parent child
Show More
@@ -1,619 +1,619
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 USER_FORMATS = {
30 30 :firstname_lastname => '#{firstname} #{lastname}',
31 31 :firstname => '#{firstname}',
32 32 :lastname_firstname => '#{lastname} #{firstname}',
33 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 34 :username => '#{login}'
35 35 }
36 36
37 37 MAIL_NOTIFICATION_OPTIONS = [
38 38 ['all', :label_user_mail_option_all],
39 39 ['selected', :label_user_mail_option_selected],
40 40 ['only_my_events', :label_user_mail_option_only_my_events],
41 41 ['only_assigned', :label_user_mail_option_only_assigned],
42 42 ['only_owner', :label_user_mail_option_only_owner],
43 43 ['none', :label_user_mail_option_none]
44 44 ]
45 45
46 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 48 has_many :changesets, :dependent => :nullify
49 49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 50 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 51 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 52 belongs_to :auth_source
53 53
54 54 # Active non-anonymous users scope
55 55 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 56
57 57 acts_as_customizable
58 58
59 59 attr_accessor :password, :password_confirmation
60 60 attr_accessor :last_before_login_on
61 61 # Prevents unauthorized assignments
62 62 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
63 63
64 64 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 65 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 66 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 67 # Login must contain lettres, numbers, underscores only
68 68 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 69 validates_length_of :login, :maximum => 30
70 70 validates_length_of :firstname, :lastname, :maximum => 30
71 71 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
72 72 validates_length_of :mail, :maximum => 60, :allow_nil => true
73 73 validates_confirmation_of :password, :allow_nil => true
74 74 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
75 75 validate :validate_password_length
76 76
77 77 before_create :set_mail_notification
78 78 before_save :update_hashed_password
79 79 before_destroy :remove_references_before_destroy
80 80
81 81 named_scope :in_group, lambda {|group|
82 82 group_id = group.is_a?(Group) ? group.id : group.to_i
83 83 { :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] }
84 84 }
85 85 named_scope :not_in_group, lambda {|group|
86 86 group_id = group.is_a?(Group) ? group.id : group.to_i
87 87 { :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] }
88 88 }
89 89
90 90 def set_mail_notification
91 91 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
92 92 true
93 93 end
94 94
95 95 def update_hashed_password
96 96 # update hashed_password if password was set
97 97 if self.password && self.auth_source_id.blank?
98 98 salt_password(password)
99 99 end
100 100 end
101 101
102 102 def reload(*args)
103 103 @name = nil
104 104 @projects_by_role = nil
105 105 super
106 106 end
107 107
108 108 def mail=(arg)
109 109 write_attribute(:mail, arg.to_s.strip)
110 110 end
111 111
112 112 def identity_url=(url)
113 113 if url.blank?
114 114 write_attribute(:identity_url, '')
115 115 else
116 116 begin
117 117 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
118 118 rescue OpenIdAuthentication::InvalidOpenId
119 119 # Invlaid url, don't save
120 120 end
121 121 end
122 122 self.read_attribute(:identity_url)
123 123 end
124 124
125 125 # Returns the user that matches provided login and password, or nil
126 126 def self.try_to_login(login, password)
127 127 # Make sure no one can sign in with an empty password
128 128 return nil if password.to_s.empty?
129 129 user = find_by_login(login)
130 130 if user
131 131 # user is already in local database
132 132 return nil if !user.active?
133 133 if user.auth_source
134 134 # user has an external authentication method
135 135 return nil unless user.auth_source.authenticate(login, password)
136 136 else
137 137 # authentication with local password
138 138 return nil unless user.check_password?(password)
139 139 end
140 140 else
141 141 # user is not yet registered, try to authenticate with available sources
142 142 attrs = AuthSource.authenticate(login, password)
143 143 if attrs
144 144 user = new(attrs)
145 145 user.login = login
146 146 user.language = Setting.default_language
147 147 if user.save
148 148 user.reload
149 149 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
150 150 end
151 151 end
152 152 end
153 153 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
154 154 user
155 155 rescue => text
156 156 raise text
157 157 end
158 158
159 159 # Returns the user who matches the given autologin +key+ or nil
160 160 def self.try_to_autologin(key)
161 161 tokens = Token.find_all_by_action_and_value('autologin', key)
162 162 # Make sure there's only 1 token that matches the key
163 163 if tokens.size == 1
164 164 token = tokens.first
165 165 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
166 166 token.user.update_attribute(:last_login_on, Time.now)
167 167 token.user
168 168 end
169 169 end
170 170 end
171 171
172 172 # Return user's full name for display
173 173 def name(formatter = nil)
174 174 if formatter
175 175 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
176 176 else
177 177 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
178 178 end
179 179 end
180 180
181 181 def active?
182 182 self.status == STATUS_ACTIVE
183 183 end
184 184
185 185 def registered?
186 186 self.status == STATUS_REGISTERED
187 187 end
188 188
189 189 def locked?
190 190 self.status == STATUS_LOCKED
191 191 end
192 192
193 193 def activate
194 194 self.status = STATUS_ACTIVE
195 195 end
196 196
197 197 def register
198 198 self.status = STATUS_REGISTERED
199 199 end
200 200
201 201 def lock
202 202 self.status = STATUS_LOCKED
203 203 end
204 204
205 205 def activate!
206 206 update_attribute(:status, STATUS_ACTIVE)
207 207 end
208 208
209 209 def register!
210 210 update_attribute(:status, STATUS_REGISTERED)
211 211 end
212 212
213 213 def lock!
214 214 update_attribute(:status, STATUS_LOCKED)
215 215 end
216 216
217 217 # Returns true if +clear_password+ is the correct user's password, otherwise false
218 218 def check_password?(clear_password)
219 219 if auth_source_id.present?
220 220 auth_source.authenticate(self.login, clear_password)
221 221 else
222 222 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
223 223 end
224 224 end
225 225
226 226 # Generates a random salt and computes hashed_password for +clear_password+
227 227 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
228 228 def salt_password(clear_password)
229 229 self.salt = User.generate_salt
230 230 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
231 231 end
232 232
233 233 # Does the backend storage allow this user to change their password?
234 234 def change_password_allowed?
235 235 return true if auth_source_id.blank?
236 236 return auth_source.allow_password_changes?
237 237 end
238 238
239 239 # Generate and set a random password. Useful for automated user creation
240 240 # Based on Token#generate_token_value
241 241 #
242 242 def random_password
243 243 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
244 244 password = ''
245 245 40.times { |i| password << chars[rand(chars.size-1)] }
246 246 self.password = password
247 247 self.password_confirmation = password
248 248 self
249 249 end
250 250
251 251 def pref
252 252 self.preference ||= UserPreference.new(:user => self)
253 253 end
254 254
255 255 def time_zone
256 256 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
257 257 end
258 258
259 259 def wants_comments_in_reverse_order?
260 260 self.pref[:comments_sorting] == 'desc'
261 261 end
262 262
263 263 # Return user's RSS key (a 40 chars long string), used to access feeds
264 264 def rss_key
265 265 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
266 266 token.value
267 267 end
268 268
269 269 # Return user's API key (a 40 chars long string), used to access the API
270 270 def api_key
271 271 token = self.api_token || self.create_api_token(:action => 'api')
272 272 token.value
273 273 end
274 274
275 275 # Return an array of project ids for which the user has explicitly turned mail notifications on
276 276 def notified_projects_ids
277 277 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
278 278 end
279 279
280 280 def notified_project_ids=(ids)
281 281 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
282 282 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
283 283 @notified_projects_ids = nil
284 284 notified_projects_ids
285 285 end
286 286
287 287 def valid_notification_options
288 288 self.class.valid_notification_options(self)
289 289 end
290 290
291 291 # Only users that belong to more than 1 project can select projects for which they are notified
292 292 def self.valid_notification_options(user=nil)
293 293 # Note that @user.membership.size would fail since AR ignores
294 294 # :include association option when doing a count
295 295 if user.nil? || user.memberships.length < 1
296 296 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
297 297 else
298 298 MAIL_NOTIFICATION_OPTIONS
299 299 end
300 300 end
301 301
302 302 # Find a user account by matching the exact login and then a case-insensitive
303 303 # version. Exact matches will be given priority.
304 304 def self.find_by_login(login)
305 305 # force string comparison to be case sensitive on MySQL
306 306 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
307 307
308 308 # First look for an exact match
309 309 user = first(:conditions => ["#{type_cast} login = ?", login])
310 310 # Fail over to case-insensitive if none was found
311 311 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
312 312 end
313 313
314 314 def self.find_by_rss_key(key)
315 315 token = Token.find_by_value(key)
316 316 token && token.user.active? ? token.user : nil
317 317 end
318 318
319 319 def self.find_by_api_key(key)
320 320 token = Token.find_by_action_and_value('api', key)
321 321 token && token.user.active? ? token.user : nil
322 322 end
323 323
324 324 # Makes find_by_mail case-insensitive
325 325 def self.find_by_mail(mail)
326 326 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
327 327 end
328 328
329 329 def to_s
330 330 name
331 331 end
332 332
333 333 # Returns the current day according to user's time zone
334 334 def today
335 335 if time_zone.nil?
336 336 Date.today
337 337 else
338 338 Time.now.in_time_zone(time_zone).to_date
339 339 end
340 340 end
341 341
342 342 def logged?
343 343 true
344 344 end
345 345
346 346 def anonymous?
347 347 !logged?
348 348 end
349 349
350 350 # Return user's roles for project
351 351 def roles_for_project(project)
352 352 roles = []
353 353 # No role on archived projects
354 354 return roles unless project && project.active?
355 355 if logged?
356 356 # Find project membership
357 357 membership = memberships.detect {|m| m.project_id == project.id}
358 358 if membership
359 359 roles = membership.roles
360 360 else
361 361 @role_non_member ||= Role.non_member
362 362 roles << @role_non_member
363 363 end
364 364 else
365 365 @role_anonymous ||= Role.anonymous
366 366 roles << @role_anonymous
367 367 end
368 368 roles
369 369 end
370 370
371 371 # Return true if the user is a member of project
372 372 def member_of?(project)
373 373 !roles_for_project(project).detect {|role| role.member?}.nil?
374 374 end
375 375
376 376 # Returns a hash of user's projects grouped by roles
377 377 def projects_by_role
378 378 return @projects_by_role if @projects_by_role
379 379
380 380 @projects_by_role = Hash.new {|h,k| h[k]=[]}
381 381 memberships.each do |membership|
382 382 membership.roles.each do |role|
383 383 @projects_by_role[role] << membership.project if membership.project
384 384 end
385 385 end
386 386 @projects_by_role.each do |role, projects|
387 387 projects.uniq!
388 388 end
389 389
390 390 @projects_by_role
391 391 end
392 392
393 393 # Returns true if user is arg or belongs to arg
394 394 def is_or_belongs_to?(arg)
395 395 if arg.is_a?(User)
396 396 self == arg
397 397 elsif arg.is_a?(Group)
398 398 arg.users.include?(self)
399 399 else
400 400 false
401 401 end
402 402 end
403 403
404 404 # Return true if the user is allowed to do the specified action on a specific context
405 405 # Action can be:
406 406 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
407 407 # * a permission Symbol (eg. :edit_project)
408 408 # Context can be:
409 409 # * a project : returns true if user is allowed to do the specified action on this project
410 410 # * an array of projects : returns true if user is allowed on every project
411 411 # * nil with options[:global] set : check if user has at least one role allowed for this action,
412 412 # or falls back to Non Member / Anonymous permissions depending if the user is logged
413 413 def allowed_to?(action, context, options={}, &block)
414 414 if context && context.is_a?(Project)
415 415 # No action allowed on archived projects
416 416 return false unless context.active?
417 417 # No action allowed on disabled modules
418 418 return false unless context.allows_to?(action)
419 419 # Admin users are authorized for anything else
420 420 return true if admin?
421 421
422 422 roles = roles_for_project(context)
423 423 return false unless roles
424 424 roles.detect {|role|
425 425 (context.is_public? || role.member?) &&
426 426 role.allowed_to?(action) &&
427 427 (block_given? ? yield(role, self) : true)
428 428 }
429 429 elsif context && context.is_a?(Array)
430 430 # Authorize if user is authorized on every element of the array
431 431 context.map do |project|
432 432 allowed_to?(action, project, options, &block)
433 433 end.inject do |memo,allowed|
434 434 memo && allowed
435 435 end
436 436 elsif options[:global]
437 437 # Admin users are always authorized
438 438 return true if admin?
439 439
440 440 # authorize if user has at least one role that has this permission
441 441 roles = memberships.collect {|m| m.roles}.flatten.uniq
442 442 roles << (self.logged? ? Role.non_member : Role.anonymous)
443 443 roles.detect {|role|
444 444 role.allowed_to?(action) &&
445 445 (block_given? ? yield(role, self) : true)
446 446 }
447 447 else
448 448 false
449 449 end
450 450 end
451 451
452 452 # Is the user allowed to do the specified action on any project?
453 453 # See allowed_to? for the actions and valid options.
454 454 def allowed_to_globally?(action, options, &block)
455 455 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
456 456 end
457 457
458 458 safe_attributes 'login',
459 459 'firstname',
460 460 'lastname',
461 461 'mail',
462 462 'mail_notification',
463 463 'language',
464 464 'custom_field_values',
465 465 'custom_fields',
466 466 'identity_url'
467 467
468 468 safe_attributes 'status',
469 469 'auth_source_id',
470 470 :if => lambda {|user, current_user| current_user.admin?}
471 471
472 472 safe_attributes 'group_ids',
473 473 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
474 474
475 475 # Utility method to help check if a user should be notified about an
476 476 # event.
477 477 #
478 478 # TODO: only supports Issue events currently
479 479 def notify_about?(object)
480 480 case mail_notification
481 481 when 'all'
482 482 true
483 483 when 'selected'
484 484 # user receives notifications for created/assigned issues on unselected projects
485 485 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
486 486 true
487 487 else
488 488 false
489 489 end
490 490 when 'none'
491 491 false
492 492 when 'only_my_events'
493 493 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
494 494 true
495 495 else
496 496 false
497 497 end
498 498 when 'only_assigned'
499 499 if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to)
500 500 true
501 501 else
502 502 false
503 503 end
504 504 when 'only_owner'
505 505 if object.is_a?(Issue) && object.author == self
506 506 true
507 507 else
508 508 false
509 509 end
510 510 else
511 511 false
512 512 end
513 513 end
514 514
515 515 def self.current=(user)
516 516 @current_user = user
517 517 end
518 518
519 519 def self.current
520 520 @current_user ||= User.anonymous
521 521 end
522 522
523 523 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
524 524 # one anonymous user per database.
525 525 def self.anonymous
526 526 anonymous_user = AnonymousUser.find(:first)
527 527 if anonymous_user.nil?
528 528 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
529 529 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
530 530 end
531 531 anonymous_user
532 532 end
533 533
534 534 # Salts all existing unsalted passwords
535 535 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
536 536 # This method is used in the SaltPasswords migration and is to be kept as is
537 537 def self.salt_unsalted_passwords!
538 538 transaction do
539 539 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
540 540 next if user.hashed_password.blank?
541 541 salt = User.generate_salt
542 542 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
543 543 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
544 544 end
545 545 end
546 546 end
547 547
548 548 protected
549 549
550 550 def validate_password_length
551 551 # Password length validation based on setting
552 552 if !password.nil? && password.size < Setting.password_min_length.to_i
553 553 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
554 554 end
555 555 end
556 556
557 557 private
558 558
559 559 # Removes references that are not handled by associations
560 560 # Things that are not deleted are reassociated with the anonymous user
561 561 def remove_references_before_destroy
562 562 return if self.id.nil?
563 563
564 564 substitute = User.anonymous
565 565 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
566 566 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
567 567 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
568 568 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
569 569 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
570 570 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
571 571 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
572 572 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
573 573 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
574 574 # Remove private queries and keep public ones
575 575 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
576 576 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
577 577 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
578 578 Token.delete_all ['user_id = ?', id]
579 579 Watcher.delete_all ['user_id = ?', id]
580 580 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
581 581 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
582 582 end
583 583
584 584 # Return password digest
585 585 def self.hash_password(clear_password)
586 586 Digest::SHA1.hexdigest(clear_password || "")
587 587 end
588 588
589 589 # Returns a 128bits random salt as a hex string (32 chars long)
590 590 def self.generate_salt
591 591 ActiveSupport::SecureRandom.hex(16)
592 592 end
593 593
594 594 end
595 595
596 596 class AnonymousUser < User
597 597
598 598 def validate_on_create
599 599 # There should be only one AnonymousUser in the database
600 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
600 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
601 601 end
602 602
603 603 def available_custom_fields
604 604 []
605 605 end
606 606
607 607 # Overrides a few properties
608 608 def logged?; false end
609 609 def admin; false end
610 610 def name(*args); I18n.t(:label_user_anonymous) end
611 611 def mail; nil end
612 612 def time_zone; nil end
613 613 def rss_key; nil end
614 614
615 615 # Anonymous user can not be destroyed
616 616 def destroy
617 617 false
618 618 end
619 619 end
General Comments 0
You need to be logged in to leave comments. Login now