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