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