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