Mercurial > hg > soundsoftware-site
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 |