comparison app/models/.svn/text-base/user.rb.svn-base @ 0:513646585e45

* Import Redmine trunk SVN rev 3859
author Chris Cannam
date Fri, 23 Jul 2010 15:52:44 +0100
parents
children cca12e1c1fd4
comparison
equal deleted inserted replaced
-1:000000000000 0:513646585e45
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require "digest/sha1"
19
20 class User < Principal
21
22 # Account statuses
23 STATUS_ANONYMOUS = 0
24 STATUS_ACTIVE = 1
25 STATUS_REGISTERED = 2
26 STATUS_LOCKED = 3
27
28 USER_FORMATS = {
29 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname => '#{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :username => '#{login}'
34 }
35
36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 has_many :changesets, :dependent => :nullify
40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
43 belongs_to :auth_source
44
45 # Active non-anonymous users scope
46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47
48 acts_as_customizable
49
50 attr_accessor :password, :password_confirmation
51 attr_accessor :last_before_login_on
52 # Prevents unauthorized assignments
53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54
55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 # Login must contain lettres, numbers, underscores only
59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 validates_length_of :login, :maximum => 30
61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 validates_length_of :firstname, :lastname, :maximum => 30
63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 validates_confirmation_of :password, :allow_nil => true
66
67 def before_create
68 self.mail_notification = false
69 true
70 end
71
72 def before_save
73 # update hashed_password if password was set
74 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
75 end
76
77 def reload(*args)
78 @name = nil
79 super
80 end
81
82 def identity_url=(url)
83 if url.blank?
84 write_attribute(:identity_url, '')
85 else
86 begin
87 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
88 rescue OpenIdAuthentication::InvalidOpenId
89 # Invlaid url, don't save
90 end
91 end
92 self.read_attribute(:identity_url)
93 end
94
95 # Returns the user that matches provided login and password, or nil
96 def self.try_to_login(login, password)
97 # Make sure no one can sign in with an empty password
98 return nil if password.to_s.empty?
99 user = find_by_login(login)
100 if user
101 # user is already in local database
102 return nil if !user.active?
103 if user.auth_source
104 # user has an external authentication method
105 return nil unless user.auth_source.authenticate(login, password)
106 else
107 # authentication with local password
108 return nil unless User.hash_password(password) == user.hashed_password
109 end
110 else
111 # user is not yet registered, try to authenticate with available sources
112 attrs = AuthSource.authenticate(login, password)
113 if attrs
114 user = new(attrs)
115 user.login = login
116 user.language = Setting.default_language
117 if user.save
118 user.reload
119 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
120 end
121 end
122 end
123 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
124 user
125 rescue => text
126 raise text
127 end
128
129 # Returns the user who matches the given autologin +key+ or nil
130 def self.try_to_autologin(key)
131 tokens = Token.find_all_by_action_and_value('autologin', key)
132 # Make sure there's only 1 token that matches the key
133 if tokens.size == 1
134 token = tokens.first
135 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
136 token.user.update_attribute(:last_login_on, Time.now)
137 token.user
138 end
139 end
140 end
141
142 # Return user's full name for display
143 def name(formatter = nil)
144 if formatter
145 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
146 else
147 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
148 end
149 end
150
151 def active?
152 self.status == STATUS_ACTIVE
153 end
154
155 def registered?
156 self.status == STATUS_REGISTERED
157 end
158
159 def locked?
160 self.status == STATUS_LOCKED
161 end
162
163 def check_password?(clear_password)
164 if auth_source_id.present?
165 auth_source.authenticate(self.login, clear_password)
166 else
167 User.hash_password(clear_password) == self.hashed_password
168 end
169 end
170
171 # Does the backend storage allow this user to change their password?
172 def change_password_allowed?
173 return true if auth_source_id.blank?
174 return auth_source.allow_password_changes?
175 end
176
177 # Generate and set a random password. Useful for automated user creation
178 # Based on Token#generate_token_value
179 #
180 def random_password
181 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
182 password = ''
183 40.times { |i| password << chars[rand(chars.size-1)] }
184 self.password = password
185 self.password_confirmation = password
186 self
187 end
188
189 def pref
190 self.preference ||= UserPreference.new(:user => self)
191 end
192
193 def time_zone
194 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
195 end
196
197 def wants_comments_in_reverse_order?
198 self.pref[:comments_sorting] == 'desc'
199 end
200
201 # Return user's RSS key (a 40 chars long string), used to access feeds
202 def rss_key
203 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
204 token.value
205 end
206
207 # Return user's API key (a 40 chars long string), used to access the API
208 def api_key
209 token = self.api_token || self.create_api_token(:action => 'api')
210 token.value
211 end
212
213 # Return an array of project ids for which the user has explicitly turned mail notifications on
214 def notified_projects_ids
215 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
216 end
217
218 def notified_project_ids=(ids)
219 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
220 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
221 @notified_projects_ids = nil
222 notified_projects_ids
223 end
224
225 # Find a user account by matching the exact login and then a case-insensitive
226 # version. Exact matches will be given priority.
227 def self.find_by_login(login)
228 # force string comparison to be case sensitive on MySQL
229 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
230
231 # First look for an exact match
232 user = first(:conditions => ["#{type_cast} login = ?", login])
233 # Fail over to case-insensitive if none was found
234 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
235 end
236
237 def self.find_by_rss_key(key)
238 token = Token.find_by_value(key)
239 token && token.user.active? ? token.user : nil
240 end
241
242 def self.find_by_api_key(key)
243 token = Token.find_by_action_and_value('api', key)
244 token && token.user.active? ? token.user : nil
245 end
246
247 # Makes find_by_mail case-insensitive
248 def self.find_by_mail(mail)
249 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
250 end
251
252 def to_s
253 name
254 end
255
256 # Returns the current day according to user's time zone
257 def today
258 if time_zone.nil?
259 Date.today
260 else
261 Time.now.in_time_zone(time_zone).to_date
262 end
263 end
264
265 def logged?
266 true
267 end
268
269 def anonymous?
270 !logged?
271 end
272
273 # Return user's roles for project
274 def roles_for_project(project)
275 roles = []
276 # No role on archived projects
277 return roles unless project && project.active?
278 if logged?
279 # Find project membership
280 membership = memberships.detect {|m| m.project_id == project.id}
281 if membership
282 roles = membership.roles
283 else
284 @role_non_member ||= Role.non_member
285 roles << @role_non_member
286 end
287 else
288 @role_anonymous ||= Role.anonymous
289 roles << @role_anonymous
290 end
291 roles
292 end
293
294 # Return true if the user is a member of project
295 def member_of?(project)
296 !roles_for_project(project).detect {|role| role.member?}.nil?
297 end
298
299 # Return true if the user is allowed to do the specified action on project
300 # action can be:
301 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
302 # * a permission Symbol (eg. :edit_project)
303 def allowed_to?(action, project, options={})
304 if project
305 # No action allowed on archived projects
306 return false unless project.active?
307 # No action allowed on disabled modules
308 return false unless project.allows_to?(action)
309 # Admin users are authorized for anything else
310 return true if admin?
311
312 roles = roles_for_project(project)
313 return false unless roles
314 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
315
316 elsif options[:global]
317 # Admin users are always authorized
318 return true if admin?
319
320 # authorize if user has at least one role that has this permission
321 roles = memberships.collect {|m| m.roles}.flatten.uniq
322 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
323 else
324 false
325 end
326 end
327
328 def self.current=(user)
329 @current_user = user
330 end
331
332 def self.current
333 @current_user ||= User.anonymous
334 end
335
336 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
337 # one anonymous user per database.
338 def self.anonymous
339 anonymous_user = AnonymousUser.find(:first)
340 if anonymous_user.nil?
341 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
342 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
343 end
344 anonymous_user
345 end
346
347 protected
348
349 def validate
350 # Password length validation based on setting
351 if !password.nil? && password.size < Setting.password_min_length.to_i
352 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
353 end
354 end
355
356 private
357
358 # Return password digest
359 def self.hash_password(clear_password)
360 Digest::SHA1.hexdigest(clear_password || "")
361 end
362 end
363
364 class AnonymousUser < User
365
366 def validate_on_create
367 # There should be only one AnonymousUser in the database
368 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
369 end
370
371 def available_custom_fields
372 []
373 end
374
375 # Overrides a few properties
376 def logged?; false end
377 def admin; false end
378 def name(*args); I18n.t(:label_user_anonymous) end
379 def mail; nil end
380 def time_zone; nil end
381 def rss_key; nil end
382 end