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