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