##// END OF EJS Templates
Send a single email to admins like other notifications (#21421)....
Jean-Philippe Lang -
r14884:46a4151f0935
parent child
Show More
@@ -1,917 +1,919
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastnamefirstname => {
51 51 :string => '#{lastname}#{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname_comma_firstname => {
56 56 :string => '#{lastname}, #{firstname}',
57 57 :order => %w(lastname firstname id),
58 58 :setting_order => 6
59 59 },
60 60 :lastname => {
61 61 :string => '#{lastname}',
62 62 :order => %w(lastname id),
63 63 :setting_order => 7
64 64 },
65 65 :username => {
66 66 :string => '#{login}',
67 67 :order => %w(login id),
68 68 :setting_order => 8
69 69 },
70 70 }
71 71
72 72 MAIL_NOTIFICATION_OPTIONS = [
73 73 ['all', :label_user_mail_option_all],
74 74 ['selected', :label_user_mail_option_selected],
75 75 ['only_my_events', :label_user_mail_option_only_my_events],
76 76 ['only_assigned', :label_user_mail_option_only_assigned],
77 77 ['only_owner', :label_user_mail_option_only_owner],
78 78 ['none', :label_user_mail_option_none]
79 79 ]
80 80
81 81 has_and_belongs_to_many :groups,
82 82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
83 83 :after_add => Proc.new {|user, group| group.user_added(user)},
84 84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
85 85 has_many :changesets, :dependent => :nullify
86 86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
87 87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
88 88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
90 90 has_many :email_addresses, :dependent => :delete_all
91 91 belongs_to :auth_source
92 92
93 93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
94 94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95 95
96 96 acts_as_customizable
97 97
98 98 attr_accessor :password, :password_confirmation, :generate_password
99 99 attr_accessor :last_before_login_on
100 100 attr_accessor :remote_ip
101 101
102 102 # Prevents unauthorized assignments
103 103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
104 104
105 105 LOGIN_LENGTH_LIMIT = 60
106 106 MAIL_LENGTH_LIMIT = 60
107 107
108 108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
109 109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
110 110 # Login must contain letters, numbers, underscores only
111 111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
112 112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
113 113 validates_length_of :firstname, :lastname, :maximum => 30
114 114 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
115 115 validate :validate_password_length
116 116 validate do
117 117 if password_confirmation && password != password_confirmation
118 118 errors.add(:password, :confirmation)
119 119 end
120 120 end
121 121
122 122 before_validation :instantiate_email_address
123 123 before_create :set_mail_notification
124 124 before_save :generate_password_if_needed, :update_hashed_password
125 125 before_destroy :remove_references_before_destroy
126 126 after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
127 127 after_destroy :deliver_security_notification
128 128
129 129 scope :in_group, lambda {|group|
130 130 group_id = group.is_a?(Group) ? group.id : group.to_i
131 131 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)
132 132 }
133 133 scope :not_in_group, lambda {|group|
134 134 group_id = group.is_a?(Group) ? group.id : group.to_i
135 135 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)
136 136 }
137 137 scope :sorted, lambda { order(*User.fields_for_order_statement)}
138 138 scope :having_mail, lambda {|arg|
139 139 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
140 140 if addresses.any?
141 141 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
142 142 else
143 143 none
144 144 end
145 145 }
146 146
147 147 def set_mail_notification
148 148 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
149 149 true
150 150 end
151 151
152 152 def update_hashed_password
153 153 # update hashed_password if password was set
154 154 if self.password && self.auth_source_id.blank?
155 155 salt_password(password)
156 156 end
157 157 end
158 158
159 159 alias :base_reload :reload
160 160 def reload(*args)
161 161 @name = nil
162 162 @projects_by_role = nil
163 163 @membership_by_project_id = nil
164 164 @notified_projects_ids = nil
165 165 @notified_projects_ids_changed = false
166 166 @builtin_role = nil
167 167 @visible_project_ids = nil
168 168 @managed_roles = nil
169 169 base_reload(*args)
170 170 end
171 171
172 172 def mail
173 173 email_address.try(:address)
174 174 end
175 175
176 176 def mail=(arg)
177 177 email = email_address || build_email_address
178 178 email.address = arg
179 179 end
180 180
181 181 def mail_changed?
182 182 email_address.try(:address_changed?)
183 183 end
184 184
185 185 def mails
186 186 email_addresses.pluck(:address)
187 187 end
188 188
189 189 def self.find_or_initialize_by_identity_url(url)
190 190 user = where(:identity_url => url).first
191 191 unless user
192 192 user = User.new
193 193 user.identity_url = url
194 194 end
195 195 user
196 196 end
197 197
198 198 def identity_url=(url)
199 199 if url.blank?
200 200 write_attribute(:identity_url, '')
201 201 else
202 202 begin
203 203 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
204 204 rescue OpenIdAuthentication::InvalidOpenId
205 205 # Invalid url, don't save
206 206 end
207 207 end
208 208 self.read_attribute(:identity_url)
209 209 end
210 210
211 211 # Returns the user that matches provided login and password, or nil
212 212 def self.try_to_login(login, password, active_only=true)
213 213 login = login.to_s
214 214 password = password.to_s
215 215
216 216 # Make sure no one can sign in with an empty login or password
217 217 return nil if login.empty? || password.empty?
218 218 user = find_by_login(login)
219 219 if user
220 220 # user is already in local database
221 221 return nil unless user.check_password?(password)
222 222 return nil if !user.active? && active_only
223 223 else
224 224 # user is not yet registered, try to authenticate with available sources
225 225 attrs = AuthSource.authenticate(login, password)
226 226 if attrs
227 227 user = new(attrs)
228 228 user.login = login
229 229 user.language = Setting.default_language
230 230 if user.save
231 231 user.reload
232 232 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
233 233 end
234 234 end
235 235 end
236 236 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
237 237 user
238 238 rescue => text
239 239 raise text
240 240 end
241 241
242 242 # Returns the user who matches the given autologin +key+ or nil
243 243 def self.try_to_autologin(key)
244 244 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
245 245 if user
246 246 user.update_column(:last_login_on, Time.now)
247 247 user
248 248 end
249 249 end
250 250
251 251 def self.name_formatter(formatter = nil)
252 252 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
253 253 end
254 254
255 255 # Returns an array of fields names than can be used to make an order statement for users
256 256 # according to how user names are displayed
257 257 # Examples:
258 258 #
259 259 # User.fields_for_order_statement => ['users.login', 'users.id']
260 260 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
261 261 def self.fields_for_order_statement(table=nil)
262 262 table ||= table_name
263 263 name_formatter[:order].map {|field| "#{table}.#{field}"}
264 264 end
265 265
266 266 # Return user's full name for display
267 267 def name(formatter = nil)
268 268 f = self.class.name_formatter(formatter)
269 269 if formatter
270 270 eval('"' + f[:string] + '"')
271 271 else
272 272 @name ||= eval('"' + f[:string] + '"')
273 273 end
274 274 end
275 275
276 276 def active?
277 277 self.status == STATUS_ACTIVE
278 278 end
279 279
280 280 def registered?
281 281 self.status == STATUS_REGISTERED
282 282 end
283 283
284 284 def locked?
285 285 self.status == STATUS_LOCKED
286 286 end
287 287
288 288 def activate
289 289 self.status = STATUS_ACTIVE
290 290 end
291 291
292 292 def register
293 293 self.status = STATUS_REGISTERED
294 294 end
295 295
296 296 def lock
297 297 self.status = STATUS_LOCKED
298 298 end
299 299
300 300 def activate!
301 301 update_attribute(:status, STATUS_ACTIVE)
302 302 end
303 303
304 304 def register!
305 305 update_attribute(:status, STATUS_REGISTERED)
306 306 end
307 307
308 308 def lock!
309 309 update_attribute(:status, STATUS_LOCKED)
310 310 end
311 311
312 312 # Returns true if +clear_password+ is the correct user's password, otherwise false
313 313 def check_password?(clear_password)
314 314 if auth_source_id.present?
315 315 auth_source.authenticate(self.login, clear_password)
316 316 else
317 317 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
318 318 end
319 319 end
320 320
321 321 # Generates a random salt and computes hashed_password for +clear_password+
322 322 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
323 323 def salt_password(clear_password)
324 324 self.salt = User.generate_salt
325 325 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
326 326 self.passwd_changed_on = Time.now.change(:usec => 0)
327 327 end
328 328
329 329 # Does the backend storage allow this user to change their password?
330 330 def change_password_allowed?
331 331 return true if auth_source.nil?
332 332 return auth_source.allow_password_changes?
333 333 end
334 334
335 335 # Returns true if the user password has expired
336 336 def password_expired?
337 337 period = Setting.password_max_age.to_i
338 338 if period.zero?
339 339 false
340 340 else
341 341 changed_on = self.passwd_changed_on || Time.at(0)
342 342 changed_on < period.days.ago
343 343 end
344 344 end
345 345
346 346 def must_change_password?
347 347 (must_change_passwd? || password_expired?) && change_password_allowed?
348 348 end
349 349
350 350 def generate_password?
351 351 generate_password == '1' || generate_password == true
352 352 end
353 353
354 354 # Generate and set a random password on given length
355 355 def random_password(length=40)
356 356 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
357 357 chars -= %w(0 O 1 l)
358 358 password = ''
359 359 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
360 360 self.password = password
361 361 self.password_confirmation = password
362 362 self
363 363 end
364 364
365 365 def pref
366 366 self.preference ||= UserPreference.new(:user => self)
367 367 end
368 368
369 369 def time_zone
370 370 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
371 371 end
372 372
373 373 def force_default_language?
374 374 Setting.force_default_language_for_loggedin?
375 375 end
376 376
377 377 def language
378 378 if force_default_language?
379 379 Setting.default_language
380 380 else
381 381 super
382 382 end
383 383 end
384 384
385 385 def wants_comments_in_reverse_order?
386 386 self.pref[:comments_sorting] == 'desc'
387 387 end
388 388
389 389 # Return user's RSS key (a 40 chars long string), used to access feeds
390 390 def rss_key
391 391 if rss_token.nil?
392 392 create_rss_token(:action => 'feeds')
393 393 end
394 394 rss_token.value
395 395 end
396 396
397 397 # Return user's API key (a 40 chars long string), used to access the API
398 398 def api_key
399 399 if api_token.nil?
400 400 create_api_token(:action => 'api')
401 401 end
402 402 api_token.value
403 403 end
404 404
405 405 # Generates a new session token and returns its value
406 406 def generate_session_token
407 407 token = Token.create!(:user_id => id, :action => 'session')
408 408 token.value
409 409 end
410 410
411 411 # Returns true if token is a valid session token for the user whose id is user_id
412 412 def self.verify_session_token(user_id, token)
413 413 return false if user_id.blank? || token.blank?
414 414
415 415 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
416 416 if Setting.session_lifetime?
417 417 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
418 418 end
419 419 if Setting.session_timeout?
420 420 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
421 421 end
422 422 scope.update_all(:updated_on => Time.now) == 1
423 423 end
424 424
425 425 # Return an array of project ids for which the user has explicitly turned mail notifications on
426 426 def notified_projects_ids
427 427 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
428 428 end
429 429
430 430 def notified_project_ids=(ids)
431 431 @notified_projects_ids_changed = true
432 432 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
433 433 end
434 434
435 435 # Updates per project notifications (after_save callback)
436 436 def update_notified_project_ids
437 437 if @notified_projects_ids_changed
438 438 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
439 439 members.update_all(:mail_notification => false)
440 440 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
441 441 end
442 442 end
443 443 private :update_notified_project_ids
444 444
445 445 def valid_notification_options
446 446 self.class.valid_notification_options(self)
447 447 end
448 448
449 449 # Only users that belong to more than 1 project can select projects for which they are notified
450 450 def self.valid_notification_options(user=nil)
451 451 # Note that @user.membership.size would fail since AR ignores
452 452 # :include association option when doing a count
453 453 if user.nil? || user.memberships.length < 1
454 454 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
455 455 else
456 456 MAIL_NOTIFICATION_OPTIONS
457 457 end
458 458 end
459 459
460 460 # Find a user account by matching the exact login and then a case-insensitive
461 461 # version. Exact matches will be given priority.
462 462 def self.find_by_login(login)
463 463 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
464 464 if login.present?
465 465 # First look for an exact match
466 466 user = where(:login => login).detect {|u| u.login == login}
467 467 unless user
468 468 # Fail over to case-insensitive if none was found
469 469 user = where("LOWER(login) = ?", login.downcase).first
470 470 end
471 471 user
472 472 end
473 473 end
474 474
475 475 def self.find_by_rss_key(key)
476 476 Token.find_active_user('feeds', key)
477 477 end
478 478
479 479 def self.find_by_api_key(key)
480 480 Token.find_active_user('api', key)
481 481 end
482 482
483 483 # Makes find_by_mail case-insensitive
484 484 def self.find_by_mail(mail)
485 485 having_mail(mail).first
486 486 end
487 487
488 488 # Returns true if the default admin account can no longer be used
489 489 def self.default_admin_account_changed?
490 490 !User.active.find_by_login("admin").try(:check_password?, "admin")
491 491 end
492 492
493 493 def to_s
494 494 name
495 495 end
496 496
497 497 CSS_CLASS_BY_STATUS = {
498 498 STATUS_ANONYMOUS => 'anon',
499 499 STATUS_ACTIVE => 'active',
500 500 STATUS_REGISTERED => 'registered',
501 501 STATUS_LOCKED => 'locked'
502 502 }
503 503
504 504 def css_classes
505 505 "user #{CSS_CLASS_BY_STATUS[status]}"
506 506 end
507 507
508 508 # Returns the current day according to user's time zone
509 509 def today
510 510 if time_zone.nil?
511 511 Date.today
512 512 else
513 513 Time.now.in_time_zone(time_zone).to_date
514 514 end
515 515 end
516 516
517 517 # Returns the day of +time+ according to user's time zone
518 518 def time_to_date(time)
519 519 if time_zone.nil?
520 520 time.to_date
521 521 else
522 522 time.in_time_zone(time_zone).to_date
523 523 end
524 524 end
525 525
526 526 def logged?
527 527 true
528 528 end
529 529
530 530 def anonymous?
531 531 !logged?
532 532 end
533 533
534 534 # Returns user's membership for the given project
535 535 # or nil if the user is not a member of project
536 536 def membership(project)
537 537 project_id = project.is_a?(Project) ? project.id : project
538 538
539 539 @membership_by_project_id ||= Hash.new {|h, project_id|
540 540 h[project_id] = memberships.where(:project_id => project_id).first
541 541 }
542 542 @membership_by_project_id[project_id]
543 543 end
544 544
545 545 # Returns the user's bult-in role
546 546 def builtin_role
547 547 @builtin_role ||= Role.non_member
548 548 end
549 549
550 550 # Return user's roles for project
551 551 def roles_for_project(project)
552 552 # No role on archived projects
553 553 return [] if project.nil? || project.archived?
554 554 if membership = membership(project)
555 555 membership.roles.to_a
556 556 elsif project.is_public?
557 557 project.override_roles(builtin_role)
558 558 else
559 559 []
560 560 end
561 561 end
562 562
563 563 # Returns a hash of user's projects grouped by roles
564 564 def projects_by_role
565 565 return @projects_by_role if @projects_by_role
566 566
567 567 hash = Hash.new([])
568 568
569 569 group_class = anonymous? ? GroupAnonymous : GroupNonMember
570 570 members = Member.joins(:project, :principal).
571 571 where("#{Project.table_name}.status <> 9").
572 572 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
573 573 preload(:project, :roles).
574 574 to_a
575 575
576 576 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
577 577 members.each do |member|
578 578 if member.project
579 579 member.roles.each do |role|
580 580 hash[role] = [] unless hash.key?(role)
581 581 hash[role] << member.project
582 582 end
583 583 end
584 584 end
585 585
586 586 hash.each do |role, projects|
587 587 projects.uniq!
588 588 end
589 589
590 590 @projects_by_role = hash
591 591 end
592 592
593 593 # Returns the ids of visible projects
594 594 def visible_project_ids
595 595 @visible_project_ids ||= Project.visible(self).pluck(:id)
596 596 end
597 597
598 598 # Returns the roles that the user is allowed to manage for the given project
599 599 def managed_roles(project)
600 600 if admin?
601 601 @managed_roles ||= Role.givable.to_a
602 602 else
603 603 membership(project).try(:managed_roles) || []
604 604 end
605 605 end
606 606
607 607 # Returns true if user is arg or belongs to arg
608 608 def is_or_belongs_to?(arg)
609 609 if arg.is_a?(User)
610 610 self == arg
611 611 elsif arg.is_a?(Group)
612 612 arg.users.include?(self)
613 613 else
614 614 false
615 615 end
616 616 end
617 617
618 618 # Return true if the user is allowed to do the specified action on a specific context
619 619 # Action can be:
620 620 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
621 621 # * a permission Symbol (eg. :edit_project)
622 622 # Context can be:
623 623 # * a project : returns true if user is allowed to do the specified action on this project
624 624 # * an array of projects : returns true if user is allowed on every project
625 625 # * nil with options[:global] set : check if user has at least one role allowed for this action,
626 626 # or falls back to Non Member / Anonymous permissions depending if the user is logged
627 627 def allowed_to?(action, context, options={}, &block)
628 628 if context && context.is_a?(Project)
629 629 return false unless context.allows_to?(action)
630 630 # Admin users are authorized for anything else
631 631 return true if admin?
632 632
633 633 roles = roles_for_project(context)
634 634 return false unless roles
635 635 roles.any? {|role|
636 636 (context.is_public? || role.member?) &&
637 637 role.allowed_to?(action) &&
638 638 (block_given? ? yield(role, self) : true)
639 639 }
640 640 elsif context && context.is_a?(Array)
641 641 if context.empty?
642 642 false
643 643 else
644 644 # Authorize if user is authorized on every element of the array
645 645 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
646 646 end
647 647 elsif context
648 648 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
649 649 elsif options[:global]
650 650 # Admin users are always authorized
651 651 return true if admin?
652 652
653 653 # authorize if user has at least one role that has this permission
654 654 roles = memberships.collect {|m| m.roles}.flatten.uniq
655 655 roles << (self.logged? ? Role.non_member : Role.anonymous)
656 656 roles.any? {|role|
657 657 role.allowed_to?(action) &&
658 658 (block_given? ? yield(role, self) : true)
659 659 }
660 660 else
661 661 false
662 662 end
663 663 end
664 664
665 665 # Is the user allowed to do the specified action on any project?
666 666 # See allowed_to? for the actions and valid options.
667 667 #
668 668 # NB: this method is not used anywhere in the core codebase as of
669 669 # 2.5.2, but it's used by many plugins so if we ever want to remove
670 670 # it it has to be carefully deprecated for a version or two.
671 671 def allowed_to_globally?(action, options={}, &block)
672 672 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
673 673 end
674 674
675 675 def allowed_to_view_all_time_entries?(context)
676 676 allowed_to?(:view_time_entries, context) do |role, user|
677 677 role.time_entries_visibility == 'all'
678 678 end
679 679 end
680 680
681 681 # Returns true if the user is allowed to delete the user's own account
682 682 def own_account_deletable?
683 683 Setting.unsubscribe? &&
684 684 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
685 685 end
686 686
687 687 safe_attributes 'firstname',
688 688 'lastname',
689 689 'mail',
690 690 'mail_notification',
691 691 'notified_project_ids',
692 692 'language',
693 693 'custom_field_values',
694 694 'custom_fields',
695 695 'identity_url'
696 696
697 697 safe_attributes 'status',
698 698 'auth_source_id',
699 699 'generate_password',
700 700 'must_change_passwd',
701 701 :if => lambda {|user, current_user| current_user.admin?}
702 702
703 703 safe_attributes 'group_ids',
704 704 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
705 705
706 706 # Utility method to help check if a user should be notified about an
707 707 # event.
708 708 #
709 709 # TODO: only supports Issue events currently
710 710 def notify_about?(object)
711 711 if mail_notification == 'all'
712 712 true
713 713 elsif mail_notification.blank? || mail_notification == 'none'
714 714 false
715 715 else
716 716 case object
717 717 when Issue
718 718 case mail_notification
719 719 when 'selected', 'only_my_events'
720 720 # user receives notifications for created/assigned issues on unselected projects
721 721 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
722 722 when 'only_assigned'
723 723 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
724 724 when 'only_owner'
725 725 object.author == self
726 726 end
727 727 when News
728 728 # always send to project members except when mail_notification is set to 'none'
729 729 true
730 730 end
731 731 end
732 732 end
733 733
734 734 def self.current=(user)
735 735 RequestStore.store[:current_user] = user
736 736 end
737 737
738 738 def self.current
739 739 RequestStore.store[:current_user] ||= User.anonymous
740 740 end
741 741
742 742 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
743 743 # one anonymous user per database.
744 744 def self.anonymous
745 745 anonymous_user = AnonymousUser.first
746 746 if anonymous_user.nil?
747 747 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
748 748 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
749 749 end
750 750 anonymous_user
751 751 end
752 752
753 753 # Salts all existing unsalted passwords
754 754 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
755 755 # This method is used in the SaltPasswords migration and is to be kept as is
756 756 def self.salt_unsalted_passwords!
757 757 transaction do
758 758 User.where("salt IS NULL OR salt = ''").find_each do |user|
759 759 next if user.hashed_password.blank?
760 760 salt = User.generate_salt
761 761 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
762 762 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
763 763 end
764 764 end
765 765 end
766 766
767 767 protected
768 768
769 769 def validate_password_length
770 770 return if password.blank? && generate_password?
771 771 # Password length validation based on setting
772 772 if !password.nil? && password.size < Setting.password_min_length.to_i
773 773 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
774 774 end
775 775 end
776 776
777 777 def instantiate_email_address
778 778 email_address || build_email_address
779 779 end
780 780
781 781 private
782 782
783 783 def generate_password_if_needed
784 784 if generate_password? && auth_source.nil?
785 785 length = [Setting.password_min_length.to_i + 2, 10].max
786 786 random_password(length)
787 787 end
788 788 end
789 789
790 790 # Delete all outstanding password reset tokens on password change.
791 791 # Delete the autologin tokens on password change to prohibit session leakage.
792 792 # This helps to keep the account secure in case the associated email account
793 793 # was compromised.
794 794 def destroy_tokens
795 795 if hashed_password_changed? || (status_changed? && !active?)
796 796 tokens = ['recovery', 'autologin', 'session']
797 797 Token.where(:user_id => id, :action => tokens).delete_all
798 798 end
799 799 end
800 800
801 801 # Removes references that are not handled by associations
802 802 # Things that are not deleted are reassociated with the anonymous user
803 803 def remove_references_before_destroy
804 804 return if self.id.nil?
805 805
806 806 substitute = User.anonymous
807 807 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
808 808 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
809 809 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
810 810 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
811 811 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
812 812 JournalDetail.
813 813 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
814 814 update_all(['old_value = ?', substitute.id.to_s])
815 815 JournalDetail.
816 816 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
817 817 update_all(['value = ?', substitute.id.to_s])
818 818 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
819 819 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
820 820 # Remove private queries and keep public ones
821 821 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
822 822 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
823 823 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
824 824 Token.delete_all ['user_id = ?', id]
825 825 Watcher.delete_all ['user_id = ?', id]
826 826 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
827 827 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
828 828 end
829 829
830 830 # Return password digest
831 831 def self.hash_password(clear_password)
832 832 Digest::SHA1.hexdigest(clear_password || "")
833 833 end
834 834
835 835 # Returns a 128bits random salt as a hex string (32 chars long)
836 836 def self.generate_salt
837 837 Redmine::Utils.random_hex(16)
838 838 end
839
839 840 # Send a security notification to all admins if the user has gained/lost admin privileges
840 841 def deliver_security_notification
841 842 options = {
842 843 field: :field_admin,
843 844 value: login,
844 845 title: :label_user_plural,
845 846 url: {controller: 'users', action: 'index'}
846 847 }
848
847 849 deliver = false
848 850 if (admin? && id_changed? && active?) || # newly created admin
849 851 (admin? && admin_changed? && active?) || # regular user became admin
850 852 (admin? && status_changed? && active?) # locked admin became active again
851 853
852 854 deliver = true
853 855 options[:message] = :mail_body_security_notification_add
854 856
855 857 elsif (admin? && destroyed? && active?) || # active admin user was deleted
856 858 (!admin? && admin_changed? && active?) || # admin is no longer admin
857 859 (admin? && status_changed? && !active?) # admin was locked
858 860
859 861 deliver = true
860 862 options[:message] = :mail_body_security_notification_remove
861 863 end
862 864
863 User.where(admin: true, status: Principal::STATUS_ACTIVE).each{|u| Mailer.security_notification(u, options).deliver} if deliver
865 if deliver
866 users = User.active.where(admin: true).to_a
867 Mailer.security_notification(users, options).deliver
868 end
864 869 end
865
866
867
868 870 end
869 871
870 872 class AnonymousUser < User
871 873 validate :validate_anonymous_uniqueness, :on => :create
872 874
873 875 def validate_anonymous_uniqueness
874 876 # There should be only one AnonymousUser in the database
875 877 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
876 878 end
877 879
878 880 def available_custom_fields
879 881 []
880 882 end
881 883
882 884 # Overrides a few properties
883 885 def logged?; false end
884 886 def admin; false end
885 887 def name(*args); I18n.t(:label_user_anonymous) end
886 888 def mail=(*args); nil end
887 889 def mail; nil end
888 890 def time_zone; nil end
889 891 def rss_key; nil end
890 892
891 893 def pref
892 894 UserPreference.new(:user => self)
893 895 end
894 896
895 897 # Returns the user's bult-in role
896 898 def builtin_role
897 899 @builtin_role ||= Role.anonymous
898 900 end
899 901
900 902 def membership(*args)
901 903 nil
902 904 end
903 905
904 906 def member_of?(*args)
905 907 false
906 908 end
907 909
908 910 # Anonymous user can not be destroyed
909 911 def destroy
910 912 false
911 913 end
912 914
913 915 protected
914 916
915 917 def instantiate_email_address
916 918 end
917 919 end
@@ -1,203 +1,204
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 module Redmine
19 19 module I18n
20 20 def self.included(base)
21 21 base.extend Redmine::I18n
22 22 end
23 23
24 24 def l(*args)
25 25 case args.size
26 26 when 1
27 27 ::I18n.t(*args)
28 28 when 2
29 29 if args.last.is_a?(Hash)
30 30 ::I18n.t(*args)
31 31 elsif args.last.is_a?(String)
32 32 ::I18n.t(args.first, :value => args.last)
33 33 else
34 34 ::I18n.t(args.first, :count => args.last)
35 35 end
36 36 else
37 37 raise "Translation string with multiple values: #{args.first}"
38 38 end
39 39 end
40 40
41 41 def l_or_humanize(s, options={})
42 42 k = "#{options[:prefix]}#{s}".to_sym
43 43 ::I18n.t(k, :default => s.to_s.humanize)
44 44 end
45 45
46 46 def l_hours(hours)
47 47 hours = hours.to_f
48 48 l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f))
49 49 end
50 50
51 51 def l_hours_short(hours)
52 52 l(:label_f_hour_short, :value => ("%.2f" % hours.to_f))
53 53 end
54 54
55 55 def ll(lang, str, arg=nil)
56 56 options = arg.is_a?(Hash) ? arg : {:value => arg}
57 57 locale = lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" }
58 58 ::I18n.t(str.to_s, options.merge(:locale => locale))
59 59 end
60 60
61 61 # Localizes the given args with user's language
62 62 def lu(user, *args)
63 63 lang = user.try(:language).presence || Setting.default_language
64 64 ll(lang, *args)
65 65 end
66 66
67 67 def format_date(date)
68 68 return nil unless date
69 69 options = {}
70 70 options[:format] = Setting.date_format unless Setting.date_format.blank?
71 71 ::I18n.l(date.to_date, options)
72 72 end
73 73
74 def format_time(time, include_date=true, user=User.current)
74 def format_time(time, include_date=true, user=nil)
75 75 return nil unless time
76 user ||= User.current
76 77 options = {}
77 78 options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format)
78 79 time = time.to_time if time.is_a?(String)
79 80 zone = user.time_zone
80 81 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
81 82 (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options)
82 83 end
83 84
84 85 def day_name(day)
85 86 ::I18n.t('date.day_names')[day % 7]
86 87 end
87 88
88 89 def day_letter(day)
89 90 ::I18n.t('date.abbr_day_names')[day % 7].first
90 91 end
91 92
92 93 def month_name(month)
93 94 ::I18n.t('date.month_names')[month]
94 95 end
95 96
96 97 def valid_languages
97 98 ::I18n.available_locales
98 99 end
99 100
100 101 # Returns an array of languages names and code sorted by names, example:
101 102 # [["Deutsch", "de"], ["English", "en"] ...]
102 103 #
103 104 # The result is cached to prevent from loading all translations files
104 105 # unless :cache => false option is given
105 106 def languages_options(options={})
106 107 options = if options[:cache] == false
107 108 valid_languages.
108 109 select {|locale| ::I18n.exists?(:general_lang_name, locale)}.
109 110 map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.
110 111 sort {|x,y| x.first <=> y.first }
111 112 else
112 113 ActionController::Base.cache_store.fetch "i18n/languages_options/#{Redmine::VERSION}" do
113 114 languages_options :cache => false
114 115 end
115 116 end
116 117 options.map {|name, lang| [name.force_encoding("UTF-8"), lang.force_encoding("UTF-8")]}
117 118 end
118 119
119 120 def find_language(lang)
120 121 @@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k }
121 122 @@languages_lookup[lang.to_s.downcase]
122 123 end
123 124
124 125 def set_language_if_valid(lang)
125 126 if l = find_language(lang)
126 127 ::I18n.locale = l
127 128 end
128 129 end
129 130
130 131 def current_language
131 132 ::I18n.locale
132 133 end
133 134
134 135 # Custom backend based on I18n::Backend::Simple with the following changes:
135 136 # * lazy loading of translation files
136 137 # * available_locales are determined by looking at translation file names
137 138 class Backend
138 139 (class << self; self; end).class_eval { public :include }
139 140
140 141 module Implementation
141 142 include ::I18n::Backend::Base
142 143
143 144 # Stores translations for the given locale in memory.
144 145 # This uses a deep merge for the translations hash, so existing
145 146 # translations will be overwritten by new ones only at the deepest
146 147 # level of the hash.
147 148 def store_translations(locale, data, options = {})
148 149 locale = locale.to_sym
149 150 translations[locale] ||= {}
150 151 data = data.deep_symbolize_keys
151 152 translations[locale].deep_merge!(data)
152 153 end
153 154
154 155 # Get available locales from the translations filenames
155 156 def available_locales
156 157 @available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym)
157 158 end
158 159
159 160 # Clean up translations
160 161 def reload!
161 162 @translations = nil
162 163 @available_locales = nil
163 164 super
164 165 end
165 166
166 167 protected
167 168
168 169 def init_translations(locale)
169 170 locale = locale.to_s
170 171 paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
171 172 load_translations(paths)
172 173 translations[locale] ||= {}
173 174 end
174 175
175 176 def translations
176 177 @translations ||= {}
177 178 end
178 179
179 180 # Looks up a translation from the translations hash. Returns nil if
180 181 # eiher key is nil, or locale, scope or key do not exist as a key in the
181 182 # nested translations hash. Splits keys or scopes containing dots
182 183 # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
183 184 # <tt>%w(currency format)</tt>.
184 185 def lookup(locale, key, scope = [], options = {})
185 186 init_translations(locale) unless translations.key?(locale)
186 187 keys = ::I18n.normalize_keys(locale, key, scope, options[:separator])
187 188
188 189 keys.inject(translations) do |result, _key|
189 190 _key = _key.to_sym
190 191 return nil unless result.is_a?(Hash) && result.has_key?(_key)
191 192 result = result[_key]
192 193 result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
193 194 result
194 195 end
195 196 end
196 197 end
197 198
198 199 include Implementation
199 200 # Adds fallback to default locale for untranslated strings
200 201 include ::I18n::Backend::Fallbacks
201 202 end
202 203 end
203 204 end
General Comments 0
You need to be logged in to leave comments. Login now