##// END OF EJS Templates
Workaround for timestamps rounding issues with Rails4.2 and mysql5.7 that may kill user session after password is changed (#17460)....
Jean-Philippe Lang -
r13629:76e7025f0716
parent child
Show More
@@ -1,204 +1,204
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 class MyController < ApplicationController
19 19 before_filter :require_login
20 20 # let user change user's password when user has to
21 21 skip_before_filter :check_password_change, :only => :password
22 22
23 23 helper :issues
24 24 helper :users
25 25 helper :custom_fields
26 26
27 27 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
28 28 'issuesreportedbyme' => :label_reported_issues,
29 29 'issueswatched' => :label_watched_issues,
30 30 'news' => :label_news_latest,
31 31 'calendar' => :label_calendar,
32 32 'documents' => :label_document_plural,
33 33 'timelog' => :label_spent_time
34 34 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
35 35
36 36 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
37 37 'right' => ['issuesreportedbyme']
38 38 }.freeze
39 39
40 40 def index
41 41 page
42 42 render :action => 'page'
43 43 end
44 44
45 45 # Show user's page
46 46 def page
47 47 @user = User.current
48 48 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
49 49 end
50 50
51 51 # Edit user's account
52 52 def account
53 53 @user = User.current
54 54 @pref = @user.pref
55 55 if request.post?
56 56 @user.safe_attributes = params[:user] if params[:user]
57 57 @user.pref.attributes = params[:pref] if params[:pref]
58 58 if @user.save
59 59 @user.pref.save
60 60 set_language_if_valid @user.language
61 61 flash[:notice] = l(:notice_account_updated)
62 62 redirect_to my_account_path
63 63 return
64 64 end
65 65 end
66 66 end
67 67
68 68 # Destroys user's account
69 69 def destroy
70 70 @user = User.current
71 71 unless @user.own_account_deletable?
72 72 redirect_to my_account_path
73 73 return
74 74 end
75 75
76 76 if request.post? && params[:confirm]
77 77 @user.destroy
78 78 if @user.destroyed?
79 79 logout_user
80 80 flash[:notice] = l(:notice_account_deleted)
81 81 end
82 82 redirect_to home_path
83 83 end
84 84 end
85 85
86 86 # Manage user's password
87 87 def password
88 88 @user = User.current
89 89 unless @user.change_password_allowed?
90 90 flash[:error] = l(:notice_can_t_change_password)
91 91 redirect_to my_account_path
92 92 return
93 93 end
94 94 if request.post?
95 95 if !@user.check_password?(params[:password])
96 96 flash.now[:error] = l(:notice_account_wrong_password)
97 97 elsif params[:password] == params[:new_password]
98 98 flash.now[:error] = l(:notice_new_password_must_be_different)
99 99 else
100 100 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
101 101 @user.must_change_passwd = false
102 102 if @user.save
103 103 # Reset the session creation time to not log out this session on next
104 104 # request due to ApplicationController#force_logout_if_password_changed
105 session[:ctime] = Time.now.utc.to_i
105 session[:ctime] = User.current.passwd_changed_on.utc.to_i
106 106 flash[:notice] = l(:notice_account_password_updated)
107 107 redirect_to my_account_path
108 108 end
109 109 end
110 110 end
111 111 end
112 112
113 113 # Create a new feeds key
114 114 def reset_rss_key
115 115 if request.post?
116 116 if User.current.rss_token
117 117 User.current.rss_token.destroy
118 118 User.current.reload
119 119 end
120 120 User.current.rss_key
121 121 flash[:notice] = l(:notice_feeds_access_key_reseted)
122 122 end
123 123 redirect_to my_account_path
124 124 end
125 125
126 126 # Create a new API key
127 127 def reset_api_key
128 128 if request.post?
129 129 if User.current.api_token
130 130 User.current.api_token.destroy
131 131 User.current.reload
132 132 end
133 133 User.current.api_key
134 134 flash[:notice] = l(:notice_api_access_key_reseted)
135 135 end
136 136 redirect_to my_account_path
137 137 end
138 138
139 139 # User's page layout configuration
140 140 def page_layout
141 141 @user = User.current
142 142 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
143 143 @block_options = []
144 144 BLOCKS.each do |k, v|
145 145 unless @blocks.values.flatten.include?(k)
146 146 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
147 147 end
148 148 end
149 149 end
150 150
151 151 # Add a block to user's page
152 152 # The block is added on top of the page
153 153 # params[:block] : id of the block to add
154 154 def add_block
155 155 block = params[:block].to_s.underscore
156 156 if block.present? && BLOCKS.key?(block)
157 157 @user = User.current
158 158 layout = @user.pref[:my_page_layout] || {}
159 159 # remove if already present in a group
160 160 %w(top left right).each {|f| (layout[f] ||= []).delete block }
161 161 # add it on top
162 162 layout['top'].unshift block
163 163 @user.pref[:my_page_layout] = layout
164 164 @user.pref.save
165 165 end
166 166 redirect_to my_page_layout_path
167 167 end
168 168
169 169 # Remove a block to user's page
170 170 # params[:block] : id of the block to remove
171 171 def remove_block
172 172 block = params[:block].to_s.underscore
173 173 @user = User.current
174 174 # remove block in all groups
175 175 layout = @user.pref[:my_page_layout] || {}
176 176 %w(top left right).each {|f| (layout[f] ||= []).delete block }
177 177 @user.pref[:my_page_layout] = layout
178 178 @user.pref.save
179 179 redirect_to my_page_layout_path
180 180 end
181 181
182 182 # Change blocks order on user's page
183 183 # params[:group] : group to order (top, left or right)
184 184 # params[:list-(top|left|right)] : array of block ids of the group
185 185 def order_blocks
186 186 group = params[:group]
187 187 @user = User.current
188 188 if group.is_a?(String)
189 189 group_items = (params["blocks"] || []).collect(&:underscore)
190 190 group_items.each {|s| s.sub!(/^block_/, '')}
191 191 if group_items and group_items.is_a? Array
192 192 layout = @user.pref[:my_page_layout] || {}
193 193 # remove group blocks if they are presents in other groups
194 194 %w(top left right).each {|f|
195 195 layout[f] = (layout[f] || []) - group_items
196 196 }
197 197 layout[group] = group_items
198 198 @user.pref[:my_page_layout] = layout
199 199 @user.pref.save
200 200 end
201 201 end
202 202 render :nothing => true
203 203 end
204 204 end
@@ -1,835 +1,835
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastname_coma_firstname => {
51 51 :string => '#{lastname}, #{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname => {
56 56 :string => '#{lastname}',
57 57 :order => %w(lastname id),
58 58 :setting_order => 6
59 59 },
60 60 :username => {
61 61 :string => '#{login}',
62 62 :order => %w(login id),
63 63 :setting_order => 7
64 64 },
65 65 }
66 66
67 67 MAIL_NOTIFICATION_OPTIONS = [
68 68 ['all', :label_user_mail_option_all],
69 69 ['selected', :label_user_mail_option_selected],
70 70 ['only_my_events', :label_user_mail_option_only_my_events],
71 71 ['only_assigned', :label_user_mail_option_only_assigned],
72 72 ['only_owner', :label_user_mail_option_only_owner],
73 73 ['none', :label_user_mail_option_none]
74 74 ]
75 75
76 76 has_and_belongs_to_many :groups,
77 77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
78 78 :after_add => Proc.new {|user, group| group.user_added(user)},
79 79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
80 80 has_many :changesets, :dependent => :nullify
81 81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
82 82 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
83 83 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
84 84 has_one :email_address, lambda {where :is_default => true}, :autosave => true
85 85 has_many :email_addresses, :dependent => :delete_all
86 86 belongs_to :auth_source
87 87
88 88 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
89 89 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 90
91 91 acts_as_customizable
92 92
93 93 attr_accessor :password, :password_confirmation, :generate_password
94 94 attr_accessor :last_before_login_on
95 95 # Prevents unauthorized assignments
96 96 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
97 97
98 98 LOGIN_LENGTH_LIMIT = 60
99 99 MAIL_LENGTH_LIMIT = 60
100 100
101 101 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
102 102 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
103 103 # Login must contain letters, numbers, underscores only
104 104 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
105 105 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
106 106 validates_length_of :firstname, :lastname, :maximum => 30
107 107 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
108 108 validate :validate_password_length
109 109 validate do
110 110 if password_confirmation && password != password_confirmation
111 111 errors.add(:password, :confirmation)
112 112 end
113 113 end
114 114
115 115 before_validation :instantiate_email_address
116 116 before_create :set_mail_notification
117 117 before_save :generate_password_if_needed, :update_hashed_password
118 118 before_destroy :remove_references_before_destroy
119 119 after_save :update_notified_project_ids, :destroy_tokens
120 120
121 121 scope :in_group, lambda {|group|
122 122 group_id = group.is_a?(Group) ? group.id : group.to_i
123 123 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)
124 124 }
125 125 scope :not_in_group, lambda {|group|
126 126 group_id = group.is_a?(Group) ? group.id : group.to_i
127 127 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)
128 128 }
129 129 scope :sorted, lambda { order(*User.fields_for_order_statement)}
130 130 scope :having_mail, lambda {|arg|
131 131 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
132 132 if addresses.any?
133 133 joins(:email_addresses).where("LOWER(address) IN (?)", addresses).uniq
134 134 else
135 135 none
136 136 end
137 137 }
138 138
139 139 def set_mail_notification
140 140 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
141 141 true
142 142 end
143 143
144 144 def update_hashed_password
145 145 # update hashed_password if password was set
146 146 if self.password && self.auth_source_id.blank?
147 147 salt_password(password)
148 148 end
149 149 end
150 150
151 151 alias :base_reload :reload
152 152 def reload(*args)
153 153 @name = nil
154 154 @projects_by_role = nil
155 155 @membership_by_project_id = nil
156 156 @notified_projects_ids = nil
157 157 @notified_projects_ids_changed = false
158 158 @builtin_role = nil
159 159 @visible_project_ids = nil
160 160 base_reload(*args)
161 161 end
162 162
163 163 def mail
164 164 email_address.try(:address)
165 165 end
166 166
167 167 def mail=(arg)
168 168 email = email_address || build_email_address
169 169 email.address = arg
170 170 end
171 171
172 172 def mail_changed?
173 173 email_address.try(:address_changed?)
174 174 end
175 175
176 176 def mails
177 177 email_addresses.pluck(:address)
178 178 end
179 179
180 180 def self.find_or_initialize_by_identity_url(url)
181 181 user = where(:identity_url => url).first
182 182 unless user
183 183 user = User.new
184 184 user.identity_url = url
185 185 end
186 186 user
187 187 end
188 188
189 189 def identity_url=(url)
190 190 if url.blank?
191 191 write_attribute(:identity_url, '')
192 192 else
193 193 begin
194 194 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
195 195 rescue OpenIdAuthentication::InvalidOpenId
196 196 # Invalid url, don't save
197 197 end
198 198 end
199 199 self.read_attribute(:identity_url)
200 200 end
201 201
202 202 # Returns the user that matches provided login and password, or nil
203 203 def self.try_to_login(login, password, active_only=true)
204 204 login = login.to_s
205 205 password = password.to_s
206 206
207 207 # Make sure no one can sign in with an empty login or password
208 208 return nil if login.empty? || password.empty?
209 209 user = find_by_login(login)
210 210 if user
211 211 # user is already in local database
212 212 return nil unless user.check_password?(password)
213 213 return nil if !user.active? && active_only
214 214 else
215 215 # user is not yet registered, try to authenticate with available sources
216 216 attrs = AuthSource.authenticate(login, password)
217 217 if attrs
218 218 user = new(attrs)
219 219 user.login = login
220 220 user.language = Setting.default_language
221 221 if user.save
222 222 user.reload
223 223 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
224 224 end
225 225 end
226 226 end
227 227 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
228 228 user
229 229 rescue => text
230 230 raise text
231 231 end
232 232
233 233 # Returns the user who matches the given autologin +key+ or nil
234 234 def self.try_to_autologin(key)
235 235 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
236 236 if user
237 237 user.update_column(:last_login_on, Time.now)
238 238 user
239 239 end
240 240 end
241 241
242 242 def self.name_formatter(formatter = nil)
243 243 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
244 244 end
245 245
246 246 # Returns an array of fields names than can be used to make an order statement for users
247 247 # according to how user names are displayed
248 248 # Examples:
249 249 #
250 250 # User.fields_for_order_statement => ['users.login', 'users.id']
251 251 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
252 252 def self.fields_for_order_statement(table=nil)
253 253 table ||= table_name
254 254 name_formatter[:order].map {|field| "#{table}.#{field}"}
255 255 end
256 256
257 257 # Return user's full name for display
258 258 def name(formatter = nil)
259 259 f = self.class.name_formatter(formatter)
260 260 if formatter
261 261 eval('"' + f[:string] + '"')
262 262 else
263 263 @name ||= eval('"' + f[:string] + '"')
264 264 end
265 265 end
266 266
267 267 def active?
268 268 self.status == STATUS_ACTIVE
269 269 end
270 270
271 271 def registered?
272 272 self.status == STATUS_REGISTERED
273 273 end
274 274
275 275 def locked?
276 276 self.status == STATUS_LOCKED
277 277 end
278 278
279 279 def activate
280 280 self.status = STATUS_ACTIVE
281 281 end
282 282
283 283 def register
284 284 self.status = STATUS_REGISTERED
285 285 end
286 286
287 287 def lock
288 288 self.status = STATUS_LOCKED
289 289 end
290 290
291 291 def activate!
292 292 update_attribute(:status, STATUS_ACTIVE)
293 293 end
294 294
295 295 def register!
296 296 update_attribute(:status, STATUS_REGISTERED)
297 297 end
298 298
299 299 def lock!
300 300 update_attribute(:status, STATUS_LOCKED)
301 301 end
302 302
303 303 # Returns true if +clear_password+ is the correct user's password, otherwise false
304 304 def check_password?(clear_password)
305 305 if auth_source_id.present?
306 306 auth_source.authenticate(self.login, clear_password)
307 307 else
308 308 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
309 309 end
310 310 end
311 311
312 312 # Generates a random salt and computes hashed_password for +clear_password+
313 313 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
314 314 def salt_password(clear_password)
315 315 self.salt = User.generate_salt
316 316 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
317 self.passwd_changed_on = Time.now
317 self.passwd_changed_on = Time.now.change(:usec => 0)
318 318 end
319 319
320 320 # Does the backend storage allow this user to change their password?
321 321 def change_password_allowed?
322 322 return true if auth_source.nil?
323 323 return auth_source.allow_password_changes?
324 324 end
325 325
326 326 def must_change_password?
327 327 must_change_passwd? && change_password_allowed?
328 328 end
329 329
330 330 def generate_password?
331 331 generate_password == '1' || generate_password == true
332 332 end
333 333
334 334 # Generate and set a random password on given length
335 335 def random_password(length=40)
336 336 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
337 337 chars -= %w(0 O 1 l)
338 338 password = ''
339 339 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
340 340 self.password = password
341 341 self.password_confirmation = password
342 342 self
343 343 end
344 344
345 345 def pref
346 346 self.preference ||= UserPreference.new(:user => self)
347 347 end
348 348
349 349 def time_zone
350 350 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
351 351 end
352 352
353 353 def force_default_language?
354 354 Setting.force_default_language_for_loggedin?
355 355 end
356 356
357 357 def language
358 358 if force_default_language?
359 359 Setting.default_language
360 360 else
361 361 super
362 362 end
363 363 end
364 364
365 365 def wants_comments_in_reverse_order?
366 366 self.pref[:comments_sorting] == 'desc'
367 367 end
368 368
369 369 # Return user's RSS key (a 40 chars long string), used to access feeds
370 370 def rss_key
371 371 if rss_token.nil?
372 372 create_rss_token(:action => 'feeds')
373 373 end
374 374 rss_token.value
375 375 end
376 376
377 377 # Return user's API key (a 40 chars long string), used to access the API
378 378 def api_key
379 379 if api_token.nil?
380 380 create_api_token(:action => 'api')
381 381 end
382 382 api_token.value
383 383 end
384 384
385 385 # Return an array of project ids for which the user has explicitly turned mail notifications on
386 386 def notified_projects_ids
387 387 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
388 388 end
389 389
390 390 def notified_project_ids=(ids)
391 391 @notified_projects_ids_changed = true
392 392 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
393 393 end
394 394
395 395 # Updates per project notifications (after_save callback)
396 396 def update_notified_project_ids
397 397 if @notified_projects_ids_changed
398 398 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
399 399 members.update_all(:mail_notification => false)
400 400 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
401 401 end
402 402 end
403 403 private :update_notified_project_ids
404 404
405 405 def valid_notification_options
406 406 self.class.valid_notification_options(self)
407 407 end
408 408
409 409 # Only users that belong to more than 1 project can select projects for which they are notified
410 410 def self.valid_notification_options(user=nil)
411 411 # Note that @user.membership.size would fail since AR ignores
412 412 # :include association option when doing a count
413 413 if user.nil? || user.memberships.length < 1
414 414 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
415 415 else
416 416 MAIL_NOTIFICATION_OPTIONS
417 417 end
418 418 end
419 419
420 420 # Find a user account by matching the exact login and then a case-insensitive
421 421 # version. Exact matches will be given priority.
422 422 def self.find_by_login(login)
423 423 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
424 424 if login.present?
425 425 # First look for an exact match
426 426 user = where(:login => login).detect {|u| u.login == login}
427 427 unless user
428 428 # Fail over to case-insensitive if none was found
429 429 user = where("LOWER(login) = ?", login.downcase).first
430 430 end
431 431 user
432 432 end
433 433 end
434 434
435 435 def self.find_by_rss_key(key)
436 436 Token.find_active_user('feeds', key)
437 437 end
438 438
439 439 def self.find_by_api_key(key)
440 440 Token.find_active_user('api', key)
441 441 end
442 442
443 443 # Makes find_by_mail case-insensitive
444 444 def self.find_by_mail(mail)
445 445 having_mail(mail).first
446 446 end
447 447
448 448 # Returns true if the default admin account can no longer be used
449 449 def self.default_admin_account_changed?
450 450 !User.active.find_by_login("admin").try(:check_password?, "admin")
451 451 end
452 452
453 453 def to_s
454 454 name
455 455 end
456 456
457 457 CSS_CLASS_BY_STATUS = {
458 458 STATUS_ANONYMOUS => 'anon',
459 459 STATUS_ACTIVE => 'active',
460 460 STATUS_REGISTERED => 'registered',
461 461 STATUS_LOCKED => 'locked'
462 462 }
463 463
464 464 def css_classes
465 465 "user #{CSS_CLASS_BY_STATUS[status]}"
466 466 end
467 467
468 468 # Returns the current day according to user's time zone
469 469 def today
470 470 if time_zone.nil?
471 471 Date.today
472 472 else
473 473 Time.now.in_time_zone(time_zone).to_date
474 474 end
475 475 end
476 476
477 477 # Returns the day of +time+ according to user's time zone
478 478 def time_to_date(time)
479 479 if time_zone.nil?
480 480 time.to_date
481 481 else
482 482 time.in_time_zone(time_zone).to_date
483 483 end
484 484 end
485 485
486 486 def logged?
487 487 true
488 488 end
489 489
490 490 def anonymous?
491 491 !logged?
492 492 end
493 493
494 494 # Returns user's membership for the given project
495 495 # or nil if the user is not a member of project
496 496 def membership(project)
497 497 project_id = project.is_a?(Project) ? project.id : project
498 498
499 499 @membership_by_project_id ||= Hash.new {|h, project_id|
500 500 h[project_id] = memberships.where(:project_id => project_id).first
501 501 }
502 502 @membership_by_project_id[project_id]
503 503 end
504 504
505 505 # Returns the user's bult-in role
506 506 def builtin_role
507 507 @builtin_role ||= Role.non_member
508 508 end
509 509
510 510 # Return user's roles for project
511 511 def roles_for_project(project)
512 512 # No role on archived projects
513 513 return [] if project.nil? || project.archived?
514 514 if membership = membership(project)
515 515 membership.roles.dup
516 516 elsif project.is_public?
517 517 project.override_roles(builtin_role)
518 518 else
519 519 []
520 520 end
521 521 end
522 522
523 523 # Returns a hash of user's projects grouped by roles
524 524 def projects_by_role
525 525 return @projects_by_role if @projects_by_role
526 526
527 527 hash = Hash.new([])
528 528
529 529 group_class = anonymous? ? GroupAnonymous : GroupNonMember
530 530 members = Member.joins(:project, :principal).
531 531 where("#{Project.table_name}.status <> 9").
532 532 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
533 533 preload(:project, :roles).
534 534 to_a
535 535
536 536 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
537 537 members.each do |member|
538 538 if member.project
539 539 member.roles.each do |role|
540 540 hash[role] = [] unless hash.key?(role)
541 541 hash[role] << member.project
542 542 end
543 543 end
544 544 end
545 545
546 546 hash.each do |role, projects|
547 547 projects.uniq!
548 548 end
549 549
550 550 @projects_by_role = hash
551 551 end
552 552
553 553 # Returns the ids of visible projects
554 554 def visible_project_ids
555 555 @visible_project_ids ||= Project.visible(self).pluck(:id)
556 556 end
557 557
558 558 # Returns true if user is arg or belongs to arg
559 559 def is_or_belongs_to?(arg)
560 560 if arg.is_a?(User)
561 561 self == arg
562 562 elsif arg.is_a?(Group)
563 563 arg.users.include?(self)
564 564 else
565 565 false
566 566 end
567 567 end
568 568
569 569 # Return true if the user is allowed to do the specified action on a specific context
570 570 # Action can be:
571 571 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
572 572 # * a permission Symbol (eg. :edit_project)
573 573 # Context can be:
574 574 # * a project : returns true if user is allowed to do the specified action on this project
575 575 # * an array of projects : returns true if user is allowed on every project
576 576 # * nil with options[:global] set : check if user has at least one role allowed for this action,
577 577 # or falls back to Non Member / Anonymous permissions depending if the user is logged
578 578 def allowed_to?(action, context, options={}, &block)
579 579 if context && context.is_a?(Project)
580 580 return false unless context.allows_to?(action)
581 581 # Admin users are authorized for anything else
582 582 return true if admin?
583 583
584 584 roles = roles_for_project(context)
585 585 return false unless roles
586 586 roles.any? {|role|
587 587 (context.is_public? || role.member?) &&
588 588 role.allowed_to?(action) &&
589 589 (block_given? ? yield(role, self) : true)
590 590 }
591 591 elsif context && context.is_a?(Array)
592 592 if context.empty?
593 593 false
594 594 else
595 595 # Authorize if user is authorized on every element of the array
596 596 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
597 597 end
598 598 elsif context
599 599 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
600 600 elsif options[:global]
601 601 # Admin users are always authorized
602 602 return true if admin?
603 603
604 604 # authorize if user has at least one role that has this permission
605 605 roles = memberships.collect {|m| m.roles}.flatten.uniq
606 606 roles << (self.logged? ? Role.non_member : Role.anonymous)
607 607 roles.any? {|role|
608 608 role.allowed_to?(action) &&
609 609 (block_given? ? yield(role, self) : true)
610 610 }
611 611 else
612 612 false
613 613 end
614 614 end
615 615
616 616 # Is the user allowed to do the specified action on any project?
617 617 # See allowed_to? for the actions and valid options.
618 618 #
619 619 # NB: this method is not used anywhere in the core codebase as of
620 620 # 2.5.2, but it's used by many plugins so if we ever want to remove
621 621 # it it has to be carefully deprecated for a version or two.
622 622 def allowed_to_globally?(action, options={}, &block)
623 623 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
624 624 end
625 625
626 626 # Returns true if the user is allowed to delete the user's own account
627 627 def own_account_deletable?
628 628 Setting.unsubscribe? &&
629 629 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
630 630 end
631 631
632 632 safe_attributes 'login',
633 633 'firstname',
634 634 'lastname',
635 635 'mail',
636 636 'mail_notification',
637 637 'notified_project_ids',
638 638 'language',
639 639 'custom_field_values',
640 640 'custom_fields',
641 641 'identity_url'
642 642
643 643 safe_attributes 'status',
644 644 'auth_source_id',
645 645 'generate_password',
646 646 'must_change_passwd',
647 647 :if => lambda {|user, current_user| current_user.admin?}
648 648
649 649 safe_attributes 'group_ids',
650 650 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
651 651
652 652 # Utility method to help check if a user should be notified about an
653 653 # event.
654 654 #
655 655 # TODO: only supports Issue events currently
656 656 def notify_about?(object)
657 657 if mail_notification == 'all'
658 658 true
659 659 elsif mail_notification.blank? || mail_notification == 'none'
660 660 false
661 661 else
662 662 case object
663 663 when Issue
664 664 case mail_notification
665 665 when 'selected', 'only_my_events'
666 666 # user receives notifications for created/assigned issues on unselected projects
667 667 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
668 668 when 'only_assigned'
669 669 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
670 670 when 'only_owner'
671 671 object.author == self
672 672 end
673 673 when News
674 674 # always send to project members except when mail_notification is set to 'none'
675 675 true
676 676 end
677 677 end
678 678 end
679 679
680 680 def self.current=(user)
681 681 RequestStore.store[:current_user] = user
682 682 end
683 683
684 684 def self.current
685 685 RequestStore.store[:current_user] ||= User.anonymous
686 686 end
687 687
688 688 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
689 689 # one anonymous user per database.
690 690 def self.anonymous
691 691 anonymous_user = AnonymousUser.first
692 692 if anonymous_user.nil?
693 693 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
694 694 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
695 695 end
696 696 anonymous_user
697 697 end
698 698
699 699 # Salts all existing unsalted passwords
700 700 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
701 701 # This method is used in the SaltPasswords migration and is to be kept as is
702 702 def self.salt_unsalted_passwords!
703 703 transaction do
704 704 User.where("salt IS NULL OR salt = ''").find_each do |user|
705 705 next if user.hashed_password.blank?
706 706 salt = User.generate_salt
707 707 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
708 708 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
709 709 end
710 710 end
711 711 end
712 712
713 713 protected
714 714
715 715 def validate_password_length
716 716 return if password.blank? && generate_password?
717 717 # Password length validation based on setting
718 718 if !password.nil? && password.size < Setting.password_min_length.to_i
719 719 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
720 720 end
721 721 end
722 722
723 723 def instantiate_email_address
724 724 email_address || build_email_address
725 725 end
726 726
727 727 private
728 728
729 729 def generate_password_if_needed
730 730 if generate_password? && auth_source.nil?
731 731 length = [Setting.password_min_length.to_i + 2, 10].max
732 732 random_password(length)
733 733 end
734 734 end
735 735
736 736 # Delete all outstanding password reset tokens on password change.
737 737 # Delete the autologin tokens on password change to prohibit session leakage.
738 738 # This helps to keep the account secure in case the associated email account
739 739 # was compromised.
740 740 def destroy_tokens
741 741 if hashed_password_changed?
742 742 tokens = ['recovery', 'autologin']
743 743 Token.where(:user_id => id, :action => tokens).delete_all
744 744 end
745 745 end
746 746
747 747 # Removes references that are not handled by associations
748 748 # Things that are not deleted are reassociated with the anonymous user
749 749 def remove_references_before_destroy
750 750 return if self.id.nil?
751 751
752 752 substitute = User.anonymous
753 753 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
754 754 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
755 755 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
756 756 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
757 757 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
758 758 JournalDetail.
759 759 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
760 760 update_all(['old_value = ?', substitute.id.to_s])
761 761 JournalDetail.
762 762 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
763 763 update_all(['value = ?', substitute.id.to_s])
764 764 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
765 765 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
766 766 # Remove private queries and keep public ones
767 767 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
768 768 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
769 769 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
770 770 Token.delete_all ['user_id = ?', id]
771 771 Watcher.delete_all ['user_id = ?', id]
772 772 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
773 773 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
774 774 end
775 775
776 776 # Return password digest
777 777 def self.hash_password(clear_password)
778 778 Digest::SHA1.hexdigest(clear_password || "")
779 779 end
780 780
781 781 # Returns a 128bits random salt as a hex string (32 chars long)
782 782 def self.generate_salt
783 783 Redmine::Utils.random_hex(16)
784 784 end
785 785
786 786 end
787 787
788 788 class AnonymousUser < User
789 789 validate :validate_anonymous_uniqueness, :on => :create
790 790
791 791 def validate_anonymous_uniqueness
792 792 # There should be only one AnonymousUser in the database
793 793 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
794 794 end
795 795
796 796 def available_custom_fields
797 797 []
798 798 end
799 799
800 800 # Overrides a few properties
801 801 def logged?; false end
802 802 def admin; false end
803 803 def name(*args); I18n.t(:label_user_anonymous) end
804 804 def mail=(*args); nil end
805 805 def mail; nil end
806 806 def time_zone; nil end
807 807 def rss_key; nil end
808 808
809 809 def pref
810 810 UserPreference.new(:user => self)
811 811 end
812 812
813 813 # Returns the user's bult-in role
814 814 def builtin_role
815 815 @builtin_role ||= Role.anonymous
816 816 end
817 817
818 818 def membership(*args)
819 819 nil
820 820 end
821 821
822 822 def member_of?(*args)
823 823 false
824 824 end
825 825
826 826 # Anonymous user can not be destroyed
827 827 def destroy
828 828 false
829 829 end
830 830
831 831 protected
832 832
833 833 def instantiate_email_address
834 834 end
835 835 end
General Comments 0
You need to be logged in to leave comments. Login now