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