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