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