##// END OF EJS Templates
Makes email adress uniqueness case-insensitive (#2473)....
Jean-Philippe Lang -
r2251:212bf1e2bbe6
parent child
Show More
@@ -1,294 +1,294
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < ActiveRecord::Base
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 37 has_many :members, :dependent => :delete_all
38 38 has_many :projects, :through => :memberships
39 39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 40 has_many :changesets, :dependent => :nullify
41 41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
42 42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
43 43 belongs_to :auth_source
44 44
45 45 # Active non-anonymous users scope
46 46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 47
48 48 acts_as_customizable
49 49
50 50 attr_accessor :password, :password_confirmation
51 51 attr_accessor :last_before_login_on
52 52 # Prevents unauthorized assignments
53 53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
54 54
55 55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 58 # Login must contain lettres, numbers, underscores only
59 59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 60 validates_length_of :login, :maximum => 30
61 61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 62 validates_length_of :firstname, :lastname, :maximum => 30
63 63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 65 validates_length_of :password, :minimum => 4, :allow_nil => true
66 66 validates_confirmation_of :password, :allow_nil => true
67 67
68 68 def before_create
69 69 self.mail_notification = false
70 70 true
71 71 end
72 72
73 73 def before_save
74 74 # update hashed_password if password was set
75 75 self.hashed_password = User.hash_password(self.password) if self.password
76 76 end
77 77
78 78 def reload(*args)
79 79 @name = nil
80 80 super
81 81 end
82 82
83 83 # Returns the user that matches provided login and password, or nil
84 84 def self.try_to_login(login, password)
85 85 # Make sure no one can sign in with an empty password
86 86 return nil if password.to_s.empty?
87 87 user = find(:first, :conditions => ["login=?", login])
88 88 if user
89 89 # user is already in local database
90 90 return nil if !user.active?
91 91 if user.auth_source
92 92 # user has an external authentication method
93 93 return nil unless user.auth_source.authenticate(login, password)
94 94 else
95 95 # authentication with local password
96 96 return nil unless User.hash_password(password) == user.hashed_password
97 97 end
98 98 else
99 99 # user is not yet registered, try to authenticate with available sources
100 100 attrs = AuthSource.authenticate(login, password)
101 101 if attrs
102 102 user = new(*attrs)
103 103 user.login = login
104 104 user.language = Setting.default_language
105 105 if user.save
106 106 user.reload
107 107 logger.info("User '#{user.login}' created from the LDAP") if logger
108 108 end
109 109 end
110 110 end
111 111 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
112 112 user
113 113 rescue => text
114 114 raise text
115 115 end
116 116
117 117 # Return user's full name for display
118 118 def name(formatter = nil)
119 119 if formatter
120 120 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
121 121 else
122 122 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
123 123 end
124 124 end
125 125
126 126 def active?
127 127 self.status == STATUS_ACTIVE
128 128 end
129 129
130 130 def registered?
131 131 self.status == STATUS_REGISTERED
132 132 end
133 133
134 134 def locked?
135 135 self.status == STATUS_LOCKED
136 136 end
137 137
138 138 def check_password?(clear_password)
139 139 User.hash_password(clear_password) == self.hashed_password
140 140 end
141 141
142 142 def pref
143 143 self.preference ||= UserPreference.new(:user => self)
144 144 end
145 145
146 146 def time_zone
147 147 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
148 148 end
149 149
150 150 def wants_comments_in_reverse_order?
151 151 self.pref[:comments_sorting] == 'desc'
152 152 end
153 153
154 154 # Return user's RSS key (a 40 chars long string), used to access feeds
155 155 def rss_key
156 156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
157 157 token.value
158 158 end
159 159
160 160 # Return an array of project ids for which the user has explicitly turned mail notifications on
161 161 def notified_projects_ids
162 162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
163 163 end
164 164
165 165 def notified_project_ids=(ids)
166 166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
167 167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
168 168 @notified_projects_ids = nil
169 169 notified_projects_ids
170 170 end
171 171
172 172 def self.find_by_rss_key(key)
173 173 token = Token.find_by_value(key)
174 174 token && token.user.active? ? token.user : nil
175 175 end
176 176
177 177 def self.find_by_autologin_key(key)
178 178 token = Token.find_by_action_and_value('autologin', key)
179 179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
180 180 end
181 181
182 182 # Makes find_by_mail case-insensitive
183 183 def self.find_by_mail(mail)
184 184 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
185 185 end
186 186
187 187 # Sort users by their display names
188 188 def <=>(user)
189 189 self.to_s.downcase <=> user.to_s.downcase
190 190 end
191 191
192 192 def to_s
193 193 name
194 194 end
195 195
196 196 def logged?
197 197 true
198 198 end
199 199
200 200 def anonymous?
201 201 !logged?
202 202 end
203 203
204 204 # Return user's role for project
205 205 def role_for_project(project)
206 206 # No role on archived projects
207 207 return nil unless project && project.active?
208 208 if logged?
209 209 # Find project membership
210 210 membership = memberships.detect {|m| m.project_id == project.id}
211 211 if membership
212 212 membership.role
213 213 else
214 214 @role_non_member ||= Role.non_member
215 215 end
216 216 else
217 217 @role_anonymous ||= Role.anonymous
218 218 end
219 219 end
220 220
221 221 # Return true if the user is a member of project
222 222 def member_of?(project)
223 223 role_for_project(project).member?
224 224 end
225 225
226 226 # Return true if the user is allowed to do the specified action on project
227 227 # action can be:
228 228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
229 229 # * a permission Symbol (eg. :edit_project)
230 230 def allowed_to?(action, project, options={})
231 231 if project
232 232 # No action allowed on archived projects
233 233 return false unless project.active?
234 234 # No action allowed on disabled modules
235 235 return false unless project.allows_to?(action)
236 236 # Admin users are authorized for anything else
237 237 return true if admin?
238 238
239 239 role = role_for_project(project)
240 240 return false unless role
241 241 role.allowed_to?(action) && (project.is_public? || role.member?)
242 242
243 243 elsif options[:global]
244 244 # authorize if user has at least one role that has this permission
245 245 roles = memberships.collect {|m| m.role}.uniq
246 246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
247 247 else
248 248 false
249 249 end
250 250 end
251 251
252 252 def self.current=(user)
253 253 @current_user = user
254 254 end
255 255
256 256 def self.current
257 257 @current_user ||= User.anonymous
258 258 end
259 259
260 260 def self.anonymous
261 261 anonymous_user = AnonymousUser.find(:first)
262 262 if anonymous_user.nil?
263 263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
264 264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
265 265 end
266 266 anonymous_user
267 267 end
268 268
269 269 private
270 270 # Return password digest
271 271 def self.hash_password(clear_password)
272 272 Digest::SHA1.hexdigest(clear_password || "")
273 273 end
274 274 end
275 275
276 276 class AnonymousUser < User
277 277
278 278 def validate_on_create
279 279 # There should be only one AnonymousUser in the database
280 280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
281 281 end
282 282
283 283 def available_custom_fields
284 284 []
285 285 end
286 286
287 287 # Overrides a few properties
288 288 def logged?; false end
289 289 def admin; false end
290 290 def name; 'Anonymous' end
291 291 def mail; nil end
292 292 def time_zone; nil end
293 293 def rss_key; nil end
294 294 end
@@ -1,167 +1,180
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class UserTest < Test::Unit::TestCase
21 21 fixtures :users, :members, :projects
22 22
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 def test_truth
30 30 assert_kind_of User, @jsmith
31 31 end
32 32
33 33 def test_create
34 34 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
35 35
36 36 user.login = "jsmith"
37 37 user.password, user.password_confirmation = "password", "password"
38 38 # login uniqueness
39 39 assert !user.save
40 40 assert_equal 1, user.errors.count
41 41
42 42 user.login = "newuser"
43 43 user.password, user.password_confirmation = "passwd", "password"
44 44 # password confirmation
45 45 assert !user.save
46 46 assert_equal 1, user.errors.count
47 47
48 48 user.password, user.password_confirmation = "password", "password"
49 49 assert user.save
50 end
51
52 def test_mail_uniqueness_should_not_be_case_sensitive
53 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
54 u.login = 'newuser1'
55 u.password, u.password_confirmation = "password", "password"
56 assert u.save
57
58 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
59 u.login = 'newuser2'
60 u.password, u.password_confirmation = "password", "password"
61 assert !u.save
62 assert_equal 'activerecord_error_taken', u.errors.on(:mail)
50 63 end
51 64
52 65 def test_update
53 66 assert_equal "admin", @admin.login
54 67 @admin.login = "john"
55 68 assert @admin.save, @admin.errors.full_messages.join("; ")
56 69 @admin.reload
57 70 assert_equal "john", @admin.login
58 71 end
59 72
60 73 def test_destroy
61 74 User.find(2).destroy
62 75 assert_nil User.find_by_id(2)
63 76 assert Member.find_all_by_user_id(2).empty?
64 77 end
65 78
66 79 def test_validate
67 80 @admin.login = ""
68 81 assert !@admin.save
69 82 assert_equal 1, @admin.errors.count
70 83 end
71 84
72 85 def test_password
73 86 user = User.try_to_login("admin", "admin")
74 87 assert_kind_of User, user
75 88 assert_equal "admin", user.login
76 89 user.password = "hello"
77 90 assert user.save
78 91
79 92 user = User.try_to_login("admin", "hello")
80 93 assert_kind_of User, user
81 94 assert_equal "admin", user.login
82 95 assert_equal User.hash_password("hello"), user.hashed_password
83 96 end
84 97
85 98 def test_name_format
86 99 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
87 100 Setting.user_format = :firstname_lastname
88 101 assert_equal 'John Smith', @jsmith.reload.name
89 102 Setting.user_format = :username
90 103 assert_equal 'jsmith', @jsmith.reload.name
91 104 end
92 105
93 106 def test_lock
94 107 user = User.try_to_login("jsmith", "jsmith")
95 108 assert_equal @jsmith, user
96 109
97 110 @jsmith.status = User::STATUS_LOCKED
98 111 assert @jsmith.save
99 112
100 113 user = User.try_to_login("jsmith", "jsmith")
101 114 assert_equal nil, user
102 115 end
103 116
104 117 def test_create_anonymous
105 118 AnonymousUser.delete_all
106 119 anon = User.anonymous
107 120 assert !anon.new_record?
108 121 assert_kind_of AnonymousUser, anon
109 122 end
110 123
111 124 def test_rss_key
112 125 assert_nil @jsmith.rss_token
113 126 key = @jsmith.rss_key
114 127 assert_equal 40, key.length
115 128
116 129 @jsmith.reload
117 130 assert_equal key, @jsmith.rss_key
118 131 end
119 132
120 133 def test_role_for_project
121 134 # user with a role
122 135 role = @jsmith.role_for_project(Project.find(1))
123 136 assert_kind_of Role, role
124 137 assert_equal "Manager", role.name
125 138
126 139 # user with no role
127 140 assert !@dlopper.role_for_project(Project.find(2)).member?
128 141 end
129 142
130 143 def test_mail_notification_all
131 144 @jsmith.mail_notification = true
132 145 @jsmith.notified_project_ids = []
133 146 @jsmith.save
134 147 @jsmith.reload
135 148 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
136 149 end
137 150
138 151 def test_mail_notification_selected
139 152 @jsmith.mail_notification = false
140 153 @jsmith.notified_project_ids = [1]
141 154 @jsmith.save
142 155 @jsmith.reload
143 156 assert Project.find(1).recipients.include?(@jsmith.mail)
144 157 end
145 158
146 159 def test_mail_notification_none
147 160 @jsmith.mail_notification = false
148 161 @jsmith.notified_project_ids = []
149 162 @jsmith.save
150 163 @jsmith.reload
151 164 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
152 165 end
153 166
154 167 def test_comments_sorting_preference
155 168 assert !@jsmith.wants_comments_in_reverse_order?
156 169 @jsmith.pref.comments_sorting = 'asc'
157 170 assert !@jsmith.wants_comments_in_reverse_order?
158 171 @jsmith.pref.comments_sorting = 'desc'
159 172 assert @jsmith.wants_comments_in_reverse_order?
160 173 end
161 174
162 175 def test_find_by_mail_should_be_case_insensitive
163 176 u = User.find_by_mail('JSmith@somenet.foo')
164 177 assert_not_nil u
165 178 assert_equal 'jsmith@somenet.foo', u.mail
166 179 end
167 180 end
General Comments 0
You need to be logged in to leave comments. Login now