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