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