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