##// END OF EJS Templates
explicitly define fix has_and_belongs_to_many join_table at Group and User...
Toshi MARUYAMA -
r12196:e9470e55857a
parent child
Show More
@@ -1,90 +1,92
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 class Group < Principal
19 19 include Redmine::SafeAttributes
20 20
21 has_and_belongs_to_many :users, :after_add => :user_added,
21 has_and_belongs_to_many :users,
22 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
23 :after_add => :user_added,
22 24 :after_remove => :user_removed
23 25
24 26 acts_as_customizable
25 27
26 28 validates_presence_of :lastname
27 29 validates_uniqueness_of :lastname, :case_sensitive => false
28 30 validates_length_of :lastname, :maximum => 255
29 31
30 32 before_destroy :remove_references_before_destroy
31 33
32 34 scope :sorted, lambda { order("#{table_name}.lastname ASC") }
33 35 scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
34 36
35 37 safe_attributes 'name',
36 38 'user_ids',
37 39 'custom_field_values',
38 40 'custom_fields',
39 41 :if => lambda {|group, user| user.admin?}
40 42
41 43 def to_s
42 44 lastname.to_s
43 45 end
44 46
45 47 def name
46 48 lastname
47 49 end
48 50
49 51 def name=(arg)
50 52 self.lastname = arg
51 53 end
52 54
53 55 def user_added(user)
54 56 members.each do |member|
55 57 next if member.project.nil?
56 58 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
57 59 member.member_roles.each do |member_role|
58 60 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
59 61 end
60 62 user_member.save!
61 63 end
62 64 end
63 65
64 66 def user_removed(user)
65 67 members.each do |member|
66 68 MemberRole.
67 69 includes(:member).
68 70 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
69 71 all.
70 72 each(&:destroy)
71 73 end
72 74 end
73 75
74 76 def self.human_attribute_name(attribute_key_name, *args)
75 77 attr_name = attribute_key_name.to_s
76 78 if attr_name == 'lastname'
77 79 attr_name = "name"
78 80 end
79 81 super(attr_name, *args)
80 82 end
81 83
82 84 private
83 85
84 86 # Removes references that are not handled by associations
85 87 def remove_references_before_destroy
86 88 return if self.id.nil?
87 89
88 90 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
89 91 end
90 92 end
@@ -1,739 +1,741
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstname => {
36 36 :string => '#{firstname}',
37 37 :order => %w(firstname id),
38 38 :setting_order => 3
39 39 },
40 40 :lastname_firstname => {
41 41 :string => '#{lastname} #{firstname}',
42 42 :order => %w(lastname firstname id),
43 43 :setting_order => 4
44 44 },
45 45 :lastname_coma_firstname => {
46 46 :string => '#{lastname}, #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 5
49 49 },
50 50 :lastname => {
51 51 :string => '#{lastname}',
52 52 :order => %w(lastname id),
53 53 :setting_order => 6
54 54 },
55 55 :username => {
56 56 :string => '#{login}',
57 57 :order => %w(login id),
58 58 :setting_order => 7
59 59 },
60 60 }
61 61
62 62 MAIL_NOTIFICATION_OPTIONS = [
63 63 ['all', :label_user_mail_option_all],
64 64 ['selected', :label_user_mail_option_selected],
65 65 ['only_my_events', :label_user_mail_option_only_my_events],
66 66 ['only_assigned', :label_user_mail_option_only_assigned],
67 67 ['only_owner', :label_user_mail_option_only_owner],
68 68 ['none', :label_user_mail_option_none]
69 69 ]
70 70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups,
72 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
73 :after_add => Proc.new {|user, group| group.user_added(user)},
72 74 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 75 has_many :changesets, :dependent => :nullify
74 76 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 77 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 78 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 79 belongs_to :auth_source
78 80
79 81 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 82 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81 83
82 84 acts_as_customizable
83 85
84 86 attr_accessor :password, :password_confirmation, :generate_password
85 87 attr_accessor :last_before_login_on
86 88 # Prevents unauthorized assignments
87 89 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88 90
89 91 LOGIN_LENGTH_LIMIT = 60
90 92 MAIL_LENGTH_LIMIT = 60
91 93
92 94 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 95 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 96 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 97 # Login must contain letters, numbers, underscores only
96 98 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 99 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 100 validates_length_of :firstname, :lastname, :maximum => 30
99 101 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 102 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 103 validates_confirmation_of :password, :allow_nil => true
102 104 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 105 validate :validate_password_length
104 106
105 107 before_create :set_mail_notification
106 108 before_save :generate_password_if_needed, :update_hashed_password
107 109 before_destroy :remove_references_before_destroy
108 110 after_save :update_notified_project_ids
109 111
110 112 scope :in_group, lambda {|group|
111 113 group_id = group.is_a?(Group) ? group.id : group.to_i
112 114 where("#{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)
113 115 }
114 116 scope :not_in_group, lambda {|group|
115 117 group_id = group.is_a?(Group) ? group.id : group.to_i
116 118 where("#{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)
117 119 }
118 120 scope :sorted, lambda { order(*User.fields_for_order_statement)}
119 121
120 122 def set_mail_notification
121 123 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
122 124 true
123 125 end
124 126
125 127 def update_hashed_password
126 128 # update hashed_password if password was set
127 129 if self.password && self.auth_source_id.blank?
128 130 salt_password(password)
129 131 end
130 132 end
131 133
132 134 alias :base_reload :reload
133 135 def reload(*args)
134 136 @name = nil
135 137 @projects_by_role = nil
136 138 @membership_by_project_id = nil
137 139 @notified_projects_ids = nil
138 140 @notified_projects_ids_changed = false
139 141 @builtin_role = nil
140 142 base_reload(*args)
141 143 end
142 144
143 145 def mail=(arg)
144 146 write_attribute(:mail, arg.to_s.strip)
145 147 end
146 148
147 149 def identity_url=(url)
148 150 if url.blank?
149 151 write_attribute(:identity_url, '')
150 152 else
151 153 begin
152 154 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
153 155 rescue OpenIdAuthentication::InvalidOpenId
154 156 # Invalid url, don't save
155 157 end
156 158 end
157 159 self.read_attribute(:identity_url)
158 160 end
159 161
160 162 # Returns the user that matches provided login and password, or nil
161 163 def self.try_to_login(login, password, active_only=true)
162 164 login = login.to_s
163 165 password = password.to_s
164 166
165 167 # Make sure no one can sign in with an empty login or password
166 168 return nil if login.empty? || password.empty?
167 169 user = find_by_login(login)
168 170 if user
169 171 # user is already in local database
170 172 return nil unless user.check_password?(password)
171 173 return nil if !user.active? && active_only
172 174 else
173 175 # user is not yet registered, try to authenticate with available sources
174 176 attrs = AuthSource.authenticate(login, password)
175 177 if attrs
176 178 user = new(attrs)
177 179 user.login = login
178 180 user.language = Setting.default_language
179 181 if user.save
180 182 user.reload
181 183 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
182 184 end
183 185 end
184 186 end
185 187 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
186 188 user
187 189 rescue => text
188 190 raise text
189 191 end
190 192
191 193 # Returns the user who matches the given autologin +key+ or nil
192 194 def self.try_to_autologin(key)
193 195 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
194 196 if user
195 197 user.update_column(:last_login_on, Time.now)
196 198 user
197 199 end
198 200 end
199 201
200 202 def self.name_formatter(formatter = nil)
201 203 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
202 204 end
203 205
204 206 # Returns an array of fields names than can be used to make an order statement for users
205 207 # according to how user names are displayed
206 208 # Examples:
207 209 #
208 210 # User.fields_for_order_statement => ['users.login', 'users.id']
209 211 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
210 212 def self.fields_for_order_statement(table=nil)
211 213 table ||= table_name
212 214 name_formatter[:order].map {|field| "#{table}.#{field}"}
213 215 end
214 216
215 217 # Return user's full name for display
216 218 def name(formatter = nil)
217 219 f = self.class.name_formatter(formatter)
218 220 if formatter
219 221 eval('"' + f[:string] + '"')
220 222 else
221 223 @name ||= eval('"' + f[:string] + '"')
222 224 end
223 225 end
224 226
225 227 def active?
226 228 self.status == STATUS_ACTIVE
227 229 end
228 230
229 231 def registered?
230 232 self.status == STATUS_REGISTERED
231 233 end
232 234
233 235 def locked?
234 236 self.status == STATUS_LOCKED
235 237 end
236 238
237 239 def activate
238 240 self.status = STATUS_ACTIVE
239 241 end
240 242
241 243 def register
242 244 self.status = STATUS_REGISTERED
243 245 end
244 246
245 247 def lock
246 248 self.status = STATUS_LOCKED
247 249 end
248 250
249 251 def activate!
250 252 update_attribute(:status, STATUS_ACTIVE)
251 253 end
252 254
253 255 def register!
254 256 update_attribute(:status, STATUS_REGISTERED)
255 257 end
256 258
257 259 def lock!
258 260 update_attribute(:status, STATUS_LOCKED)
259 261 end
260 262
261 263 # Returns true if +clear_password+ is the correct user's password, otherwise false
262 264 def check_password?(clear_password)
263 265 if auth_source_id.present?
264 266 auth_source.authenticate(self.login, clear_password)
265 267 else
266 268 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
267 269 end
268 270 end
269 271
270 272 # Generates a random salt and computes hashed_password for +clear_password+
271 273 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
272 274 def salt_password(clear_password)
273 275 self.salt = User.generate_salt
274 276 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
275 277 end
276 278
277 279 # Does the backend storage allow this user to change their password?
278 280 def change_password_allowed?
279 281 return true if auth_source.nil?
280 282 return auth_source.allow_password_changes?
281 283 end
282 284
283 285 def must_change_password?
284 286 must_change_passwd? && change_password_allowed?
285 287 end
286 288
287 289 def generate_password?
288 290 generate_password == '1' || generate_password == true
289 291 end
290 292
291 293 # Generate and set a random password on given length
292 294 def random_password(length=40)
293 295 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
294 296 chars -= %w(0 O 1 l)
295 297 password = ''
296 298 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
297 299 self.password = password
298 300 self.password_confirmation = password
299 301 self
300 302 end
301 303
302 304 def pref
303 305 self.preference ||= UserPreference.new(:user => self)
304 306 end
305 307
306 308 def time_zone
307 309 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
308 310 end
309 311
310 312 def wants_comments_in_reverse_order?
311 313 self.pref[:comments_sorting] == 'desc'
312 314 end
313 315
314 316 # Return user's RSS key (a 40 chars long string), used to access feeds
315 317 def rss_key
316 318 if rss_token.nil?
317 319 create_rss_token(:action => 'feeds')
318 320 end
319 321 rss_token.value
320 322 end
321 323
322 324 # Return user's API key (a 40 chars long string), used to access the API
323 325 def api_key
324 326 if api_token.nil?
325 327 create_api_token(:action => 'api')
326 328 end
327 329 api_token.value
328 330 end
329 331
330 332 # Return an array of project ids for which the user has explicitly turned mail notifications on
331 333 def notified_projects_ids
332 334 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
333 335 end
334 336
335 337 def notified_project_ids=(ids)
336 338 @notified_projects_ids_changed = true
337 339 @notified_projects_ids = ids
338 340 end
339 341
340 342 # Updates per project notifications (after_save callback)
341 343 def update_notified_project_ids
342 344 if @notified_projects_ids_changed
343 345 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
344 346 members.update_all(:mail_notification => false)
345 347 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
346 348 end
347 349 end
348 350 private :update_notified_project_ids
349 351
350 352 def valid_notification_options
351 353 self.class.valid_notification_options(self)
352 354 end
353 355
354 356 # Only users that belong to more than 1 project can select projects for which they are notified
355 357 def self.valid_notification_options(user=nil)
356 358 # Note that @user.membership.size would fail since AR ignores
357 359 # :include association option when doing a count
358 360 if user.nil? || user.memberships.length < 1
359 361 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
360 362 else
361 363 MAIL_NOTIFICATION_OPTIONS
362 364 end
363 365 end
364 366
365 367 # Find a user account by matching the exact login and then a case-insensitive
366 368 # version. Exact matches will be given priority.
367 369 def self.find_by_login(login)
368 370 if login.present?
369 371 login = login.to_s
370 372 # First look for an exact match
371 373 user = where(:login => login).all.detect {|u| u.login == login}
372 374 unless user
373 375 # Fail over to case-insensitive if none was found
374 376 user = where("LOWER(login) = ?", login.downcase).first
375 377 end
376 378 user
377 379 end
378 380 end
379 381
380 382 def self.find_by_rss_key(key)
381 383 Token.find_active_user('feeds', key)
382 384 end
383 385
384 386 def self.find_by_api_key(key)
385 387 Token.find_active_user('api', key)
386 388 end
387 389
388 390 # Makes find_by_mail case-insensitive
389 391 def self.find_by_mail(mail)
390 392 where("LOWER(mail) = ?", mail.to_s.downcase).first
391 393 end
392 394
393 395 # Returns true if the default admin account can no longer be used
394 396 def self.default_admin_account_changed?
395 397 !User.active.find_by_login("admin").try(:check_password?, "admin")
396 398 end
397 399
398 400 def to_s
399 401 name
400 402 end
401 403
402 404 CSS_CLASS_BY_STATUS = {
403 405 STATUS_ANONYMOUS => 'anon',
404 406 STATUS_ACTIVE => 'active',
405 407 STATUS_REGISTERED => 'registered',
406 408 STATUS_LOCKED => 'locked'
407 409 }
408 410
409 411 def css_classes
410 412 "user #{CSS_CLASS_BY_STATUS[status]}"
411 413 end
412 414
413 415 # Returns the current day according to user's time zone
414 416 def today
415 417 if time_zone.nil?
416 418 Date.today
417 419 else
418 420 Time.now.in_time_zone(time_zone).to_date
419 421 end
420 422 end
421 423
422 424 # Returns the day of +time+ according to user's time zone
423 425 def time_to_date(time)
424 426 if time_zone.nil?
425 427 time.to_date
426 428 else
427 429 time.in_time_zone(time_zone).to_date
428 430 end
429 431 end
430 432
431 433 def logged?
432 434 true
433 435 end
434 436
435 437 def anonymous?
436 438 !logged?
437 439 end
438 440
439 441 # Returns user's membership for the given project
440 442 # or nil if the user is not a member of project
441 443 def membership(project)
442 444 project_id = project.is_a?(Project) ? project.id : project
443 445
444 446 @membership_by_project_id ||= Hash.new {|h, project_id|
445 447 h[project_id] = memberships.where(:project_id => project_id).first
446 448 }
447 449 @membership_by_project_id[project_id]
448 450 end
449 451
450 452 # Returns the user's bult-in role
451 453 def builtin_role
452 454 @builtin_role ||= Role.non_member
453 455 end
454 456
455 457 # Return user's roles for project
456 458 def roles_for_project(project)
457 459 roles = []
458 460 # No role on archived projects
459 461 return roles if project.nil? || project.archived?
460 462 if membership = membership(project)
461 463 roles = membership.roles
462 464 else
463 465 roles << builtin_role
464 466 end
465 467 roles
466 468 end
467 469
468 470 # Return true if the user is a member of project
469 471 def member_of?(project)
470 472 projects.to_a.include?(project)
471 473 end
472 474
473 475 # Returns a hash of user's projects grouped by roles
474 476 def projects_by_role
475 477 return @projects_by_role if @projects_by_role
476 478
477 479 @projects_by_role = Hash.new([])
478 480 memberships.each do |membership|
479 481 if membership.project
480 482 membership.roles.each do |role|
481 483 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
482 484 @projects_by_role[role] << membership.project
483 485 end
484 486 end
485 487 end
486 488 @projects_by_role.each do |role, projects|
487 489 projects.uniq!
488 490 end
489 491
490 492 @projects_by_role
491 493 end
492 494
493 495 # Returns true if user is arg or belongs to arg
494 496 def is_or_belongs_to?(arg)
495 497 if arg.is_a?(User)
496 498 self == arg
497 499 elsif arg.is_a?(Group)
498 500 arg.users.include?(self)
499 501 else
500 502 false
501 503 end
502 504 end
503 505
504 506 # Return true if the user is allowed to do the specified action on a specific context
505 507 # Action can be:
506 508 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
507 509 # * a permission Symbol (eg. :edit_project)
508 510 # Context can be:
509 511 # * a project : returns true if user is allowed to do the specified action on this project
510 512 # * an array of projects : returns true if user is allowed on every project
511 513 # * nil with options[:global] set : check if user has at least one role allowed for this action,
512 514 # or falls back to Non Member / Anonymous permissions depending if the user is logged
513 515 def allowed_to?(action, context, options={}, &block)
514 516 if context && context.is_a?(Project)
515 517 return false unless context.allows_to?(action)
516 518 # Admin users are authorized for anything else
517 519 return true if admin?
518 520
519 521 roles = roles_for_project(context)
520 522 return false unless roles
521 523 roles.any? {|role|
522 524 (context.is_public? || role.member?) &&
523 525 role.allowed_to?(action) &&
524 526 (block_given? ? yield(role, self) : true)
525 527 }
526 528 elsif context && context.is_a?(Array)
527 529 if context.empty?
528 530 false
529 531 else
530 532 # Authorize if user is authorized on every element of the array
531 533 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
532 534 end
533 535 elsif options[:global]
534 536 # Admin users are always authorized
535 537 return true if admin?
536 538
537 539 # authorize if user has at least one role that has this permission
538 540 roles = memberships.collect {|m| m.roles}.flatten.uniq
539 541 roles << (self.logged? ? Role.non_member : Role.anonymous)
540 542 roles.any? {|role|
541 543 role.allowed_to?(action) &&
542 544 (block_given? ? yield(role, self) : true)
543 545 }
544 546 else
545 547 false
546 548 end
547 549 end
548 550
549 551 # Is the user allowed to do the specified action on any project?
550 552 # See allowed_to? for the actions and valid options.
551 553 def allowed_to_globally?(action, options, &block)
552 554 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
553 555 end
554 556
555 557 # Returns true if the user is allowed to delete the user's own account
556 558 def own_account_deletable?
557 559 Setting.unsubscribe? &&
558 560 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
559 561 end
560 562
561 563 safe_attributes 'login',
562 564 'firstname',
563 565 'lastname',
564 566 'mail',
565 567 'mail_notification',
566 568 'notified_project_ids',
567 569 'language',
568 570 'custom_field_values',
569 571 'custom_fields',
570 572 'identity_url'
571 573
572 574 safe_attributes 'status',
573 575 'auth_source_id',
574 576 'generate_password',
575 577 'must_change_passwd',
576 578 :if => lambda {|user, current_user| current_user.admin?}
577 579
578 580 safe_attributes 'group_ids',
579 581 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
580 582
581 583 # Utility method to help check if a user should be notified about an
582 584 # event.
583 585 #
584 586 # TODO: only supports Issue events currently
585 587 def notify_about?(object)
586 588 if mail_notification == 'all'
587 589 true
588 590 elsif mail_notification.blank? || mail_notification == 'none'
589 591 false
590 592 else
591 593 case object
592 594 when Issue
593 595 case mail_notification
594 596 when 'selected', 'only_my_events'
595 597 # user receives notifications for created/assigned issues on unselected projects
596 598 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
597 599 when 'only_assigned'
598 600 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
599 601 when 'only_owner'
600 602 object.author == self
601 603 end
602 604 when News
603 605 # always send to project members except when mail_notification is set to 'none'
604 606 true
605 607 end
606 608 end
607 609 end
608 610
609 611 def self.current=(user)
610 612 Thread.current[:current_user] = user
611 613 end
612 614
613 615 def self.current
614 616 Thread.current[:current_user] ||= User.anonymous
615 617 end
616 618
617 619 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
618 620 # one anonymous user per database.
619 621 def self.anonymous
620 622 anonymous_user = AnonymousUser.first
621 623 if anonymous_user.nil?
622 624 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
623 625 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
624 626 end
625 627 anonymous_user
626 628 end
627 629
628 630 # Salts all existing unsalted passwords
629 631 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
630 632 # This method is used in the SaltPasswords migration and is to be kept as is
631 633 def self.salt_unsalted_passwords!
632 634 transaction do
633 635 User.where("salt IS NULL OR salt = ''").find_each do |user|
634 636 next if user.hashed_password.blank?
635 637 salt = User.generate_salt
636 638 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
637 639 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
638 640 end
639 641 end
640 642 end
641 643
642 644 protected
643 645
644 646 def validate_password_length
645 647 return if password.blank? && generate_password?
646 648 # Password length validation based on setting
647 649 if !password.nil? && password.size < Setting.password_min_length.to_i
648 650 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
649 651 end
650 652 end
651 653
652 654 private
653 655
654 656 def generate_password_if_needed
655 657 if generate_password? && auth_source.nil?
656 658 length = [Setting.password_min_length.to_i + 2, 10].max
657 659 random_password(length)
658 660 end
659 661 end
660 662
661 663 # Removes references that are not handled by associations
662 664 # Things that are not deleted are reassociated with the anonymous user
663 665 def remove_references_before_destroy
664 666 return if self.id.nil?
665 667
666 668 substitute = User.anonymous
667 669 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
668 670 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
669 671 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
670 672 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
671 673 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
672 674 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
673 675 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
674 676 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
675 677 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
676 678 # Remove private queries and keep public ones
677 679 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
678 680 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
679 681 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
680 682 Token.delete_all ['user_id = ?', id]
681 683 Watcher.delete_all ['user_id = ?', id]
682 684 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
683 685 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
684 686 end
685 687
686 688 # Return password digest
687 689 def self.hash_password(clear_password)
688 690 Digest::SHA1.hexdigest(clear_password || "")
689 691 end
690 692
691 693 # Returns a 128bits random salt as a hex string (32 chars long)
692 694 def self.generate_salt
693 695 Redmine::Utils.random_hex(16)
694 696 end
695 697
696 698 end
697 699
698 700 class AnonymousUser < User
699 701 validate :validate_anonymous_uniqueness, :on => :create
700 702
701 703 def validate_anonymous_uniqueness
702 704 # There should be only one AnonymousUser in the database
703 705 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
704 706 end
705 707
706 708 def available_custom_fields
707 709 []
708 710 end
709 711
710 712 # Overrides a few properties
711 713 def logged?; false end
712 714 def admin; false end
713 715 def name(*args); I18n.t(:label_user_anonymous) end
714 716 def mail; nil end
715 717 def time_zone; nil end
716 718 def rss_key; nil end
717 719
718 720 def pref
719 721 UserPreference.new(:user => self)
720 722 end
721 723
722 724 # Returns the user's bult-in role
723 725 def builtin_role
724 726 @builtin_role ||= Role.anonymous
725 727 end
726 728
727 729 def membership(*args)
728 730 nil
729 731 end
730 732
731 733 def member_of?(*args)
732 734 false
733 735 end
734 736
735 737 # Anonymous user can not be destroyed
736 738 def destroy
737 739 false
738 740 end
739 741 end
General Comments 0
You need to be logged in to leave comments. Login now