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