##// END OF EJS Templates
Fixed tests broken by r11965....
Jean-Philippe Lang -
r11736:07d88851bf82
parent child
Show More
@@ -1,734 +1,731
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 base_reload(*args)
140 base_reload(*args)
140 end
141 end
141
142
142 def mail=(arg)
143 def mail=(arg)
143 write_attribute(:mail, arg.to_s.strip)
144 write_attribute(:mail, arg.to_s.strip)
144 end
145 end
145
146
146 def identity_url=(url)
147 def identity_url=(url)
147 if url.blank?
148 if url.blank?
148 write_attribute(:identity_url, '')
149 write_attribute(:identity_url, '')
149 else
150 else
150 begin
151 begin
151 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
152 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
152 rescue OpenIdAuthentication::InvalidOpenId
153 rescue OpenIdAuthentication::InvalidOpenId
153 # Invalid url, don't save
154 # Invalid url, don't save
154 end
155 end
155 end
156 end
156 self.read_attribute(:identity_url)
157 self.read_attribute(:identity_url)
157 end
158 end
158
159
159 # Returns the user that matches provided login and password, or nil
160 # Returns the user that matches provided login and password, or nil
160 def self.try_to_login(login, password, active_only=true)
161 def self.try_to_login(login, password, active_only=true)
161 login = login.to_s
162 login = login.to_s
162 password = password.to_s
163 password = password.to_s
163
164
164 # 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
165 return nil if login.empty? || password.empty?
166 return nil if login.empty? || password.empty?
166 user = find_by_login(login)
167 user = find_by_login(login)
167 if user
168 if user
168 # user is already in local database
169 # user is already in local database
169 return nil unless user.check_password?(password)
170 return nil unless user.check_password?(password)
170 return nil if !user.active? && active_only
171 return nil if !user.active? && active_only
171 else
172 else
172 # user is not yet registered, try to authenticate with available sources
173 # user is not yet registered, try to authenticate with available sources
173 attrs = AuthSource.authenticate(login, password)
174 attrs = AuthSource.authenticate(login, password)
174 if attrs
175 if attrs
175 user = new(attrs)
176 user = new(attrs)
176 user.login = login
177 user.login = login
177 user.language = Setting.default_language
178 user.language = Setting.default_language
178 if user.save
179 if user.save
179 user.reload
180 user.reload
180 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
181 end
182 end
182 end
183 end
183 end
184 end
184 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?
185 user
186 user
186 rescue => text
187 rescue => text
187 raise text
188 raise text
188 end
189 end
189
190
190 # Returns the user who matches the given autologin +key+ or nil
191 # Returns the user who matches the given autologin +key+ or nil
191 def self.try_to_autologin(key)
192 def self.try_to_autologin(key)
192 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
193 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
193 if user
194 if user
194 user.update_column(:last_login_on, Time.now)
195 user.update_column(:last_login_on, Time.now)
195 user
196 user
196 end
197 end
197 end
198 end
198
199
199 def self.name_formatter(formatter = nil)
200 def self.name_formatter(formatter = nil)
200 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
201 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
201 end
202 end
202
203
203 # 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
204 # according to how user names are displayed
205 # according to how user names are displayed
205 # Examples:
206 # Examples:
206 #
207 #
207 # User.fields_for_order_statement => ['users.login', 'users.id']
208 # User.fields_for_order_statement => ['users.login', 'users.id']
208 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
209 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
209 def self.fields_for_order_statement(table=nil)
210 def self.fields_for_order_statement(table=nil)
210 table ||= table_name
211 table ||= table_name
211 name_formatter[:order].map {|field| "#{table}.#{field}"}
212 name_formatter[:order].map {|field| "#{table}.#{field}"}
212 end
213 end
213
214
214 # Return user's full name for display
215 # Return user's full name for display
215 def name(formatter = nil)
216 def name(formatter = nil)
216 f = self.class.name_formatter(formatter)
217 f = self.class.name_formatter(formatter)
217 if formatter
218 if formatter
218 eval('"' + f[:string] + '"')
219 eval('"' + f[:string] + '"')
219 else
220 else
220 @name ||= eval('"' + f[:string] + '"')
221 @name ||= eval('"' + f[:string] + '"')
221 end
222 end
222 end
223 end
223
224
224 def active?
225 def active?
225 self.status == STATUS_ACTIVE
226 self.status == STATUS_ACTIVE
226 end
227 end
227
228
228 def registered?
229 def registered?
229 self.status == STATUS_REGISTERED
230 self.status == STATUS_REGISTERED
230 end
231 end
231
232
232 def locked?
233 def locked?
233 self.status == STATUS_LOCKED
234 self.status == STATUS_LOCKED
234 end
235 end
235
236
236 def activate
237 def activate
237 self.status = STATUS_ACTIVE
238 self.status = STATUS_ACTIVE
238 end
239 end
239
240
240 def register
241 def register
241 self.status = STATUS_REGISTERED
242 self.status = STATUS_REGISTERED
242 end
243 end
243
244
244 def lock
245 def lock
245 self.status = STATUS_LOCKED
246 self.status = STATUS_LOCKED
246 end
247 end
247
248
248 def activate!
249 def activate!
249 update_attribute(:status, STATUS_ACTIVE)
250 update_attribute(:status, STATUS_ACTIVE)
250 end
251 end
251
252
252 def register!
253 def register!
253 update_attribute(:status, STATUS_REGISTERED)
254 update_attribute(:status, STATUS_REGISTERED)
254 end
255 end
255
256
256 def lock!
257 def lock!
257 update_attribute(:status, STATUS_LOCKED)
258 update_attribute(:status, STATUS_LOCKED)
258 end
259 end
259
260
260 # 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
261 def check_password?(clear_password)
262 def check_password?(clear_password)
262 if auth_source_id.present?
263 if auth_source_id.present?
263 auth_source.authenticate(self.login, clear_password)
264 auth_source.authenticate(self.login, clear_password)
264 else
265 else
265 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
266 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
266 end
267 end
267 end
268 end
268
269
269 # Generates a random salt and computes hashed_password for +clear_password+
270 # Generates a random salt and computes hashed_password for +clear_password+
270 # 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))
271 def salt_password(clear_password)
272 def salt_password(clear_password)
272 self.salt = User.generate_salt
273 self.salt = User.generate_salt
273 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}")
274 end
275 end
275
276
276 # Does the backend storage allow this user to change their password?
277 # Does the backend storage allow this user to change their password?
277 def change_password_allowed?
278 def change_password_allowed?
278 return true if auth_source.nil?
279 return true if auth_source.nil?
279 return auth_source.allow_password_changes?
280 return auth_source.allow_password_changes?
280 end
281 end
281
282
282 def generate_password?
283 def generate_password?
283 generate_password == '1' || generate_password == true
284 generate_password == '1' || generate_password == true
284 end
285 end
285
286
286 # Generate and set a random password on given length
287 # Generate and set a random password on given length
287 def random_password(length=40)
288 def random_password(length=40)
288 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
289 chars -= %w(0 O 1 l)
290 chars -= %w(0 O 1 l)
290 password = ''
291 password = ''
291 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
292 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
292 self.password = password
293 self.password = password
293 self.password_confirmation = password
294 self.password_confirmation = password
294 self
295 self
295 end
296 end
296
297
297 def pref
298 def pref
298 self.preference ||= UserPreference.new(:user => self)
299 self.preference ||= UserPreference.new(:user => self)
299 end
300 end
300
301
301 def time_zone
302 def time_zone
302 @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])
303 end
304 end
304
305
305 def wants_comments_in_reverse_order?
306 def wants_comments_in_reverse_order?
306 self.pref[:comments_sorting] == 'desc'
307 self.pref[:comments_sorting] == 'desc'
307 end
308 end
308
309
309 # 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
310 def rss_key
311 def rss_key
311 if rss_token.nil?
312 if rss_token.nil?
312 create_rss_token(:action => 'feeds')
313 create_rss_token(:action => 'feeds')
313 end
314 end
314 rss_token.value
315 rss_token.value
315 end
316 end
316
317
317 # 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
318 def api_key
319 def api_key
319 if api_token.nil?
320 if api_token.nil?
320 create_api_token(:action => 'api')
321 create_api_token(:action => 'api')
321 end
322 end
322 api_token.value
323 api_token.value
323 end
324 end
324
325
325 # 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
326 def notified_projects_ids
327 def notified_projects_ids
327 @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)
328 end
329 end
329
330
330 def notified_project_ids=(ids)
331 def notified_project_ids=(ids)
331 @notified_projects_ids_changed = true
332 @notified_projects_ids_changed = true
332 @notified_projects_ids = ids
333 @notified_projects_ids = ids
333 end
334 end
334
335
335 # Updates per project notifications (after_save callback)
336 # Updates per project notifications (after_save callback)
336 def update_notified_project_ids
337 def update_notified_project_ids
337 if @notified_projects_ids_changed
338 if @notified_projects_ids_changed
338 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
339 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
339 members.update_all(:mail_notification => false)
340 members.update_all(:mail_notification => false)
340 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?
341 end
342 end
342 end
343 end
343 private :update_notified_project_ids
344 private :update_notified_project_ids
344
345
345 def valid_notification_options
346 def valid_notification_options
346 self.class.valid_notification_options(self)
347 self.class.valid_notification_options(self)
347 end
348 end
348
349
349 # 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
350 def self.valid_notification_options(user=nil)
351 def self.valid_notification_options(user=nil)
351 # Note that @user.membership.size would fail since AR ignores
352 # Note that @user.membership.size would fail since AR ignores
352 # :include association option when doing a count
353 # :include association option when doing a count
353 if user.nil? || user.memberships.length < 1
354 if user.nil? || user.memberships.length < 1
354 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
355 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
355 else
356 else
356 MAIL_NOTIFICATION_OPTIONS
357 MAIL_NOTIFICATION_OPTIONS
357 end
358 end
358 end
359 end
359
360
360 # 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
361 # version. Exact matches will be given priority.
362 # version. Exact matches will be given priority.
362 def self.find_by_login(login)
363 def self.find_by_login(login)
363 if login.present?
364 if login.present?
364 login = login.to_s
365 login = login.to_s
365 # First look for an exact match
366 # First look for an exact match
366 user = where(:login => login).all.detect {|u| u.login == login}
367 user = where(:login => login).all.detect {|u| u.login == login}
367 unless user
368 unless user
368 # Fail over to case-insensitive if none was found
369 # Fail over to case-insensitive if none was found
369 user = where("LOWER(login) = ?", login.downcase).first
370 user = where("LOWER(login) = ?", login.downcase).first
370 end
371 end
371 user
372 user
372 end
373 end
373 end
374 end
374
375
375 def self.find_by_rss_key(key)
376 def self.find_by_rss_key(key)
376 Token.find_active_user('feeds', key)
377 Token.find_active_user('feeds', key)
377 end
378 end
378
379
379 def self.find_by_api_key(key)
380 def self.find_by_api_key(key)
380 Token.find_active_user('api', key)
381 Token.find_active_user('api', key)
381 end
382 end
382
383
383 # Makes find_by_mail case-insensitive
384 # Makes find_by_mail case-insensitive
384 def self.find_by_mail(mail)
385 def self.find_by_mail(mail)
385 where("LOWER(mail) = ?", mail.to_s.downcase).first
386 where("LOWER(mail) = ?", mail.to_s.downcase).first
386 end
387 end
387
388
388 # 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
389 def self.default_admin_account_changed?
390 def self.default_admin_account_changed?
390 !User.active.find_by_login("admin").try(:check_password?, "admin")
391 !User.active.find_by_login("admin").try(:check_password?, "admin")
391 end
392 end
392
393
393 def to_s
394 def to_s
394 name
395 name
395 end
396 end
396
397
397 CSS_CLASS_BY_STATUS = {
398 CSS_CLASS_BY_STATUS = {
398 STATUS_ANONYMOUS => 'anon',
399 STATUS_ANONYMOUS => 'anon',
399 STATUS_ACTIVE => 'active',
400 STATUS_ACTIVE => 'active',
400 STATUS_REGISTERED => 'registered',
401 STATUS_REGISTERED => 'registered',
401 STATUS_LOCKED => 'locked'
402 STATUS_LOCKED => 'locked'
402 }
403 }
403
404
404 def css_classes
405 def css_classes
405 "user #{CSS_CLASS_BY_STATUS[status]}"
406 "user #{CSS_CLASS_BY_STATUS[status]}"
406 end
407 end
407
408
408 # Returns the current day according to user's time zone
409 # Returns the current day according to user's time zone
409 def today
410 def today
410 if time_zone.nil?
411 if time_zone.nil?
411 Date.today
412 Date.today
412 else
413 else
413 Time.now.in_time_zone(time_zone).to_date
414 Time.now.in_time_zone(time_zone).to_date
414 end
415 end
415 end
416 end
416
417
417 # Returns the day of +time+ according to user's time zone
418 # Returns the day of +time+ according to user's time zone
418 def time_to_date(time)
419 def time_to_date(time)
419 if time_zone.nil?
420 if time_zone.nil?
420 time.to_date
421 time.to_date
421 else
422 else
422 time.in_time_zone(time_zone).to_date
423 time.in_time_zone(time_zone).to_date
423 end
424 end
424 end
425 end
425
426
426 def logged?
427 def logged?
427 true
428 true
428 end
429 end
429
430
430 def anonymous?
431 def anonymous?
431 !logged?
432 !logged?
432 end
433 end
433
434
434 # Returns user's membership for the given project
435 # Returns user's membership for the given project
435 # or nil if the user is not a member of project
436 # or nil if the user is not a member of project
436 def membership(project)
437 def membership(project)
437 project_id = project.is_a?(Project) ? project.id : project
438 project_id = project.is_a?(Project) ? project.id : project
438
439
439 @membership_by_project_id ||= Hash.new {|h, project_id|
440 @membership_by_project_id ||= Hash.new {|h, project_id|
440 h[project_id] = memberships.where(:project_id => project_id).first
441 h[project_id] = memberships.where(:project_id => project_id).first
441 }
442 }
442 @membership_by_project_id[project_id]
443 @membership_by_project_id[project_id]
443 end
444 end
444
445
445 # Returns the user's bult-in role
446 # Returns the user's bult-in role
446 def builtin_role
447 def builtin_role
447 if logged?
448 @builtin_role ||= (logged? ? Role.non_member : Role.anonymous)
448 @role_non_member ||= Role.non_member
449 else
450 @role_anonymous ||= Role.anonymous
451 end
452 end
449 end
453
450
454 # Return user's roles for project
451 # Return user's roles for project
455 def roles_for_project(project)
452 def roles_for_project(project)
456 roles = []
453 roles = []
457 # No role on archived projects
454 # No role on archived projects
458 return roles if project.nil? || project.archived?
455 return roles if project.nil? || project.archived?
459 if logged?
456 if logged?
460 # Find project membership
457 # Find project membership
461 membership = membership(project)
458 membership = membership(project)
462 if membership
459 if membership
463 roles = membership.roles
460 roles = membership.roles
464 else
461 else
465 roles << builtin_role
462 roles << builtin_role
466 end
463 end
467 else
464 else
468 roles << builtin_role
465 roles << builtin_role
469 end
466 end
470 roles
467 roles
471 end
468 end
472
469
473 # Return true if the user is a member of project
470 # Return true if the user is a member of project
474 def member_of?(project)
471 def member_of?(project)
475 projects.to_a.include?(project)
472 projects.to_a.include?(project)
476 end
473 end
477
474
478 # Returns a hash of user's projects grouped by roles
475 # Returns a hash of user's projects grouped by roles
479 def projects_by_role
476 def projects_by_role
480 return @projects_by_role if @projects_by_role
477 return @projects_by_role if @projects_by_role
481
478
482 @projects_by_role = Hash.new([])
479 @projects_by_role = Hash.new([])
483 memberships.each do |membership|
480 memberships.each do |membership|
484 if membership.project
481 if membership.project
485 membership.roles.each do |role|
482 membership.roles.each do |role|
486 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
483 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
487 @projects_by_role[role] << membership.project
484 @projects_by_role[role] << membership.project
488 end
485 end
489 end
486 end
490 end
487 end
491 @projects_by_role.each do |role, projects|
488 @projects_by_role.each do |role, projects|
492 projects.uniq!
489 projects.uniq!
493 end
490 end
494
491
495 @projects_by_role
492 @projects_by_role
496 end
493 end
497
494
498 # Returns true if user is arg or belongs to arg
495 # Returns true if user is arg or belongs to arg
499 def is_or_belongs_to?(arg)
496 def is_or_belongs_to?(arg)
500 if arg.is_a?(User)
497 if arg.is_a?(User)
501 self == arg
498 self == arg
502 elsif arg.is_a?(Group)
499 elsif arg.is_a?(Group)
503 arg.users.include?(self)
500 arg.users.include?(self)
504 else
501 else
505 false
502 false
506 end
503 end
507 end
504 end
508
505
509 # Return true if the user is allowed to do the specified action on a specific context
506 # Return true if the user is allowed to do the specified action on a specific context
510 # Action can be:
507 # Action can be:
511 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
508 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
512 # * a permission Symbol (eg. :edit_project)
509 # * a permission Symbol (eg. :edit_project)
513 # Context can be:
510 # Context can be:
514 # * a project : returns true if user is allowed to do the specified action on this project
511 # * a project : returns true if user is allowed to do the specified action on this project
515 # * an array of projects : returns true if user is allowed on every project
512 # * an array of projects : returns true if user is allowed on every project
516 # * nil with options[:global] set : check if user has at least one role allowed for this action,
513 # * nil with options[:global] set : check if user has at least one role allowed for this action,
517 # or falls back to Non Member / Anonymous permissions depending if the user is logged
514 # or falls back to Non Member / Anonymous permissions depending if the user is logged
518 def allowed_to?(action, context, options={}, &block)
515 def allowed_to?(action, context, options={}, &block)
519 if context && context.is_a?(Project)
516 if context && context.is_a?(Project)
520 return false unless context.allows_to?(action)
517 return false unless context.allows_to?(action)
521 # Admin users are authorized for anything else
518 # Admin users are authorized for anything else
522 return true if admin?
519 return true if admin?
523
520
524 roles = roles_for_project(context)
521 roles = roles_for_project(context)
525 return false unless roles
522 return false unless roles
526 roles.any? {|role|
523 roles.any? {|role|
527 (context.is_public? || role.member?) &&
524 (context.is_public? || role.member?) &&
528 role.allowed_to?(action) &&
525 role.allowed_to?(action) &&
529 (block_given? ? yield(role, self) : true)
526 (block_given? ? yield(role, self) : true)
530 }
527 }
531 elsif context && context.is_a?(Array)
528 elsif context && context.is_a?(Array)
532 if context.empty?
529 if context.empty?
533 false
530 false
534 else
531 else
535 # Authorize if user is authorized on every element of the array
532 # Authorize if user is authorized on every element of the array
536 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
533 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
537 end
534 end
538 elsif options[:global]
535 elsif options[:global]
539 # Admin users are always authorized
536 # Admin users are always authorized
540 return true if admin?
537 return true if admin?
541
538
542 # authorize if user has at least one role that has this permission
539 # authorize if user has at least one role that has this permission
543 roles = memberships.collect {|m| m.roles}.flatten.uniq
540 roles = memberships.collect {|m| m.roles}.flatten.uniq
544 roles << (self.logged? ? Role.non_member : Role.anonymous)
541 roles << (self.logged? ? Role.non_member : Role.anonymous)
545 roles.any? {|role|
542 roles.any? {|role|
546 role.allowed_to?(action) &&
543 role.allowed_to?(action) &&
547 (block_given? ? yield(role, self) : true)
544 (block_given? ? yield(role, self) : true)
548 }
545 }
549 else
546 else
550 false
547 false
551 end
548 end
552 end
549 end
553
550
554 # Is the user allowed to do the specified action on any project?
551 # Is the user allowed to do the specified action on any project?
555 # See allowed_to? for the actions and valid options.
552 # See allowed_to? for the actions and valid options.
556 def allowed_to_globally?(action, options, &block)
553 def allowed_to_globally?(action, options, &block)
557 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
554 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
558 end
555 end
559
556
560 # Returns true if the user is allowed to delete his own account
557 # Returns true if the user is allowed to delete his own account
561 def own_account_deletable?
558 def own_account_deletable?
562 Setting.unsubscribe? &&
559 Setting.unsubscribe? &&
563 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
560 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
564 end
561 end
565
562
566 safe_attributes 'login',
563 safe_attributes 'login',
567 'firstname',
564 'firstname',
568 'lastname',
565 'lastname',
569 'mail',
566 'mail',
570 'mail_notification',
567 'mail_notification',
571 'notified_project_ids',
568 'notified_project_ids',
572 'language',
569 'language',
573 'custom_field_values',
570 'custom_field_values',
574 'custom_fields',
571 'custom_fields',
575 'identity_url'
572 'identity_url'
576
573
577 safe_attributes 'status',
574 safe_attributes 'status',
578 'auth_source_id',
575 'auth_source_id',
579 'generate_password',
576 'generate_password',
580 :if => lambda {|user, current_user| current_user.admin?}
577 :if => lambda {|user, current_user| current_user.admin?}
581
578
582 safe_attributes 'group_ids',
579 safe_attributes 'group_ids',
583 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
580 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
584
581
585 # Utility method to help check if a user should be notified about an
582 # Utility method to help check if a user should be notified about an
586 # event.
583 # event.
587 #
584 #
588 # TODO: only supports Issue events currently
585 # TODO: only supports Issue events currently
589 def notify_about?(object)
586 def notify_about?(object)
590 if mail_notification == 'all'
587 if mail_notification == 'all'
591 true
588 true
592 elsif mail_notification.blank? || mail_notification == 'none'
589 elsif mail_notification.blank? || mail_notification == 'none'
593 false
590 false
594 else
591 else
595 case object
592 case object
596 when Issue
593 when Issue
597 case mail_notification
594 case mail_notification
598 when 'selected', 'only_my_events'
595 when 'selected', 'only_my_events'
599 # user receives notifications for created/assigned issues on unselected projects
596 # user receives notifications for created/assigned issues on unselected projects
600 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
597 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
601 when 'only_assigned'
598 when 'only_assigned'
602 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
599 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
603 when 'only_owner'
600 when 'only_owner'
604 object.author == self
601 object.author == self
605 end
602 end
606 when News
603 when News
607 # always send to project members except when mail_notification is set to 'none'
604 # always send to project members except when mail_notification is set to 'none'
608 true
605 true
609 end
606 end
610 end
607 end
611 end
608 end
612
609
613 def self.current=(user)
610 def self.current=(user)
614 Thread.current[:current_user] = user
611 Thread.current[:current_user] = user
615 end
612 end
616
613
617 def self.current
614 def self.current
618 Thread.current[:current_user] ||= User.anonymous
615 Thread.current[:current_user] ||= User.anonymous
619 end
616 end
620
617
621 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
618 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
622 # one anonymous user per database.
619 # one anonymous user per database.
623 def self.anonymous
620 def self.anonymous
624 anonymous_user = AnonymousUser.first
621 anonymous_user = AnonymousUser.first
625 if anonymous_user.nil?
622 if anonymous_user.nil?
626 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
623 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
627 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
624 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
628 end
625 end
629 anonymous_user
626 anonymous_user
630 end
627 end
631
628
632 # Salts all existing unsalted passwords
629 # Salts all existing unsalted passwords
633 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
630 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
634 # This method is used in the SaltPasswords migration and is to be kept as is
631 # This method is used in the SaltPasswords migration and is to be kept as is
635 def self.salt_unsalted_passwords!
632 def self.salt_unsalted_passwords!
636 transaction do
633 transaction do
637 User.where("salt IS NULL OR salt = ''").find_each do |user|
634 User.where("salt IS NULL OR salt = ''").find_each do |user|
638 next if user.hashed_password.blank?
635 next if user.hashed_password.blank?
639 salt = User.generate_salt
636 salt = User.generate_salt
640 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
637 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
641 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
638 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
642 end
639 end
643 end
640 end
644 end
641 end
645
642
646 protected
643 protected
647
644
648 def validate_password_length
645 def validate_password_length
649 return if password.blank? && generate_password?
646 return if password.blank? && generate_password?
650 # Password length validation based on setting
647 # Password length validation based on setting
651 if !password.nil? && password.size < Setting.password_min_length.to_i
648 if !password.nil? && password.size < Setting.password_min_length.to_i
652 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
649 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
653 end
650 end
654 end
651 end
655
652
656 private
653 private
657
654
658 def generate_password_if_needed
655 def generate_password_if_needed
659 if generate_password? && auth_source.nil?
656 if generate_password? && auth_source.nil?
660 length = [Setting.password_min_length.to_i + 2, 10].max
657 length = [Setting.password_min_length.to_i + 2, 10].max
661 random_password(length)
658 random_password(length)
662 end
659 end
663 end
660 end
664
661
665 # Removes references that are not handled by associations
662 # Removes references that are not handled by associations
666 # Things that are not deleted are reassociated with the anonymous user
663 # Things that are not deleted are reassociated with the anonymous user
667 def remove_references_before_destroy
664 def remove_references_before_destroy
668 return if self.id.nil?
665 return if self.id.nil?
669
666
670 substitute = User.anonymous
667 substitute = User.anonymous
671 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
668 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
672 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
669 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
673 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
670 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
674 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
671 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
675 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
672 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
676 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
673 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
677 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
674 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
678 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
675 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
679 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
676 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
680 # Remove private queries and keep public ones
677 # Remove private queries and keep public ones
681 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
678 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
682 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
679 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
683 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
680 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
684 Token.delete_all ['user_id = ?', id]
681 Token.delete_all ['user_id = ?', id]
685 Watcher.delete_all ['user_id = ?', id]
682 Watcher.delete_all ['user_id = ?', id]
686 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
683 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
687 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
684 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
688 end
685 end
689
686
690 # Return password digest
687 # Return password digest
691 def self.hash_password(clear_password)
688 def self.hash_password(clear_password)
692 Digest::SHA1.hexdigest(clear_password || "")
689 Digest::SHA1.hexdigest(clear_password || "")
693 end
690 end
694
691
695 # Returns a 128bits random salt as a hex string (32 chars long)
692 # Returns a 128bits random salt as a hex string (32 chars long)
696 def self.generate_salt
693 def self.generate_salt
697 Redmine::Utils.random_hex(16)
694 Redmine::Utils.random_hex(16)
698 end
695 end
699
696
700 end
697 end
701
698
702 class AnonymousUser < User
699 class AnonymousUser < User
703 validate :validate_anonymous_uniqueness, :on => :create
700 validate :validate_anonymous_uniqueness, :on => :create
704
701
705 def validate_anonymous_uniqueness
702 def validate_anonymous_uniqueness
706 # There should be only one AnonymousUser in the database
703 # There should be only one AnonymousUser in the database
707 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
704 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
708 end
705 end
709
706
710 def available_custom_fields
707 def available_custom_fields
711 []
708 []
712 end
709 end
713
710
714 # Overrides a few properties
711 # Overrides a few properties
715 def logged?; false end
712 def logged?; false end
716 def admin; false end
713 def admin; false end
717 def name(*args); I18n.t(:label_user_anonymous) end
714 def name(*args); I18n.t(:label_user_anonymous) end
718 def mail; nil end
715 def mail; nil end
719 def time_zone; nil end
716 def time_zone; nil end
720 def rss_key; nil end
717 def rss_key; nil end
721
718
722 def pref
719 def pref
723 UserPreference.new(:user => self)
720 UserPreference.new(:user => self)
724 end
721 end
725
722
726 def member_of?(project)
723 def member_of?(project)
727 false
724 false
728 end
725 end
729
726
730 # Anonymous user can not be destroyed
727 # Anonymous user can not be destroyed
731 def destroy
728 def destroy
732 false
729 false
733 end
730 end
734 end
731 end
@@ -1,144 +1,146
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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class SearchTest < ActiveSupport::TestCase
20 class SearchTest < ActiveSupport::TestCase
21 fixtures :users,
21 fixtures :users,
22 :members,
22 :members,
23 :member_roles,
23 :member_roles,
24 :projects,
24 :projects,
25 :roles,
25 :roles,
26 :enabled_modules,
26 :enabled_modules,
27 :issues,
27 :issues,
28 :trackers,
28 :trackers,
29 :journals,
29 :journals,
30 :journal_details,
30 :journal_details,
31 :repositories,
31 :repositories,
32 :changesets
32 :changesets
33
33
34 def setup
34 def setup
35 @project = Project.find(1)
35 @project = Project.find(1)
36 @issue_keyword = '%unable to print recipes%'
36 @issue_keyword = '%unable to print recipes%'
37 @issue = Issue.find(1)
37 @issue = Issue.find(1)
38 @changeset_keyword = '%very first commit%'
38 @changeset_keyword = '%very first commit%'
39 @changeset = Changeset.find(100)
39 @changeset = Changeset.find(100)
40 end
40 end
41
41
42 def test_search_by_anonymous
42 def test_search_by_anonymous
43 User.current = nil
43 User.current = nil
44
44
45 r = Issue.search(@issue_keyword).first
45 r = Issue.search(@issue_keyword).first
46 assert r.include?(@issue)
46 assert r.include?(@issue)
47 r = Changeset.search(@changeset_keyword).first
47 r = Changeset.search(@changeset_keyword).first
48 assert r.include?(@changeset)
48 assert r.include?(@changeset)
49
49
50 # Removes the :view_changesets permission from Anonymous role
50 # Removes the :view_changesets permission from Anonymous role
51 remove_permission Role.anonymous, :view_changesets
51 remove_permission Role.anonymous, :view_changesets
52 User.current = nil
52
53
53 r = Issue.search(@issue_keyword).first
54 r = Issue.search(@issue_keyword).first
54 assert r.include?(@issue)
55 assert r.include?(@issue)
55 r = Changeset.search(@changeset_keyword).first
56 r = Changeset.search(@changeset_keyword).first
56 assert !r.include?(@changeset)
57 assert !r.include?(@changeset)
57
58
58 # Make the project private
59 # Make the project private
59 @project.update_attribute :is_public, false
60 @project.update_attribute :is_public, false
60 r = Issue.search(@issue_keyword).first
61 r = Issue.search(@issue_keyword).first
61 assert !r.include?(@issue)
62 assert !r.include?(@issue)
62 r = Changeset.search(@changeset_keyword).first
63 r = Changeset.search(@changeset_keyword).first
63 assert !r.include?(@changeset)
64 assert !r.include?(@changeset)
64 end
65 end
65
66
66 def test_search_by_user
67 def test_search_by_user
67 User.current = User.find_by_login('rhill')
68 User.current = User.find_by_login('rhill')
68 assert User.current.memberships.empty?
69 assert User.current.memberships.empty?
69
70
70 r = Issue.search(@issue_keyword).first
71 r = Issue.search(@issue_keyword).first
71 assert r.include?(@issue)
72 assert r.include?(@issue)
72 r = Changeset.search(@changeset_keyword).first
73 r = Changeset.search(@changeset_keyword).first
73 assert r.include?(@changeset)
74 assert r.include?(@changeset)
74
75
75 # Removes the :view_changesets permission from Non member role
76 # Removes the :view_changesets permission from Non member role
76 remove_permission Role.non_member, :view_changesets
77 remove_permission Role.non_member, :view_changesets
78 User.current = User.find_by_login('rhill')
77
79
78 r = Issue.search(@issue_keyword).first
80 r = Issue.search(@issue_keyword).first
79 assert r.include?(@issue)
81 assert r.include?(@issue)
80 r = Changeset.search(@changeset_keyword).first
82 r = Changeset.search(@changeset_keyword).first
81 assert !r.include?(@changeset)
83 assert !r.include?(@changeset)
82
84
83 # Make the project private
85 # Make the project private
84 @project.update_attribute :is_public, false
86 @project.update_attribute :is_public, false
85 r = Issue.search(@issue_keyword).first
87 r = Issue.search(@issue_keyword).first
86 assert !r.include?(@issue)
88 assert !r.include?(@issue)
87 r = Changeset.search(@changeset_keyword).first
89 r = Changeset.search(@changeset_keyword).first
88 assert !r.include?(@changeset)
90 assert !r.include?(@changeset)
89 end
91 end
90
92
91 def test_search_by_allowed_member
93 def test_search_by_allowed_member
92 User.current = User.find_by_login('jsmith')
94 User.current = User.find_by_login('jsmith')
93 assert User.current.projects.include?(@project)
95 assert User.current.projects.include?(@project)
94
96
95 r = Issue.search(@issue_keyword).first
97 r = Issue.search(@issue_keyword).first
96 assert r.include?(@issue)
98 assert r.include?(@issue)
97 r = Changeset.search(@changeset_keyword).first
99 r = Changeset.search(@changeset_keyword).first
98 assert r.include?(@changeset)
100 assert r.include?(@changeset)
99
101
100 # Make the project private
102 # Make the project private
101 @project.update_attribute :is_public, false
103 @project.update_attribute :is_public, false
102 r = Issue.search(@issue_keyword).first
104 r = Issue.search(@issue_keyword).first
103 assert r.include?(@issue)
105 assert r.include?(@issue)
104 r = Changeset.search(@changeset_keyword).first
106 r = Changeset.search(@changeset_keyword).first
105 assert r.include?(@changeset)
107 assert r.include?(@changeset)
106 end
108 end
107
109
108 def test_search_by_unallowed_member
110 def test_search_by_unallowed_member
109 # Removes the :view_changesets permission from user's and non member role
111 # Removes the :view_changesets permission from user's and non member role
110 remove_permission Role.find(1), :view_changesets
112 remove_permission Role.find(1), :view_changesets
111 remove_permission Role.non_member, :view_changesets
113 remove_permission Role.non_member, :view_changesets
112
114
113 User.current = User.find_by_login('jsmith')
115 User.current = User.find_by_login('jsmith')
114 assert User.current.projects.include?(@project)
116 assert User.current.projects.include?(@project)
115
117
116 r = Issue.search(@issue_keyword).first
118 r = Issue.search(@issue_keyword).first
117 assert r.include?(@issue)
119 assert r.include?(@issue)
118 r = Changeset.search(@changeset_keyword).first
120 r = Changeset.search(@changeset_keyword).first
119 assert !r.include?(@changeset)
121 assert !r.include?(@changeset)
120
122
121 # Make the project private
123 # Make the project private
122 @project.update_attribute :is_public, false
124 @project.update_attribute :is_public, false
123 r = Issue.search(@issue_keyword).first
125 r = Issue.search(@issue_keyword).first
124 assert r.include?(@issue)
126 assert r.include?(@issue)
125 r = Changeset.search(@changeset_keyword).first
127 r = Changeset.search(@changeset_keyword).first
126 assert !r.include?(@changeset)
128 assert !r.include?(@changeset)
127 end
129 end
128
130
129 def test_search_issue_with_multiple_hits_in_journals
131 def test_search_issue_with_multiple_hits_in_journals
130 i = Issue.find(1)
132 i = Issue.find(1)
131 assert_equal 2, i.journals.where("notes LIKE '%notes%'").count
133 assert_equal 2, i.journals.where("notes LIKE '%notes%'").count
132
134
133 r = Issue.search('%notes%').first
135 r = Issue.search('%notes%').first
134 assert_equal 1, r.size
136 assert_equal 1, r.size
135 assert_equal i, r.first
137 assert_equal i, r.first
136 end
138 end
137
139
138 private
140 private
139
141
140 def remove_permission(role, permission)
142 def remove_permission(role, permission)
141 role.permissions = role.permissions - [ permission ]
143 role.permissions = role.permissions - [ permission ]
142 role.save
144 role.save
143 end
145 end
144 end
146 end
General Comments 0
You need to be logged in to leave comments. Login now