annotate app/models/.svn/text-base/user.rb.svn-base @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 94944d00e43c
children af80e5618e9b 8661b858af72
rev   line source
Chris@0 1 # Redmine - project management software
Chris@0 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require "digest/sha1"
Chris@0 19
Chris@0 20 class User < Principal
Chris@0 21
Chris@0 22 # Account statuses
Chris@0 23 STATUS_ANONYMOUS = 0
Chris@0 24 STATUS_ACTIVE = 1
Chris@0 25 STATUS_REGISTERED = 2
Chris@0 26 STATUS_LOCKED = 3
Chris@0 27
Chris@0 28 USER_FORMATS = {
Chris@0 29 :firstname_lastname => '#{firstname} #{lastname}',
Chris@0 30 :firstname => '#{firstname}',
Chris@0 31 :lastname_firstname => '#{lastname} #{firstname}',
Chris@0 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
Chris@0 33 :username => '#{login}'
Chris@0 34 }
Chris@0 35
chris@37 36 MAIL_NOTIFICATION_OPTIONS = [
chris@37 37 [:all, :label_user_mail_option_all],
chris@37 38 [:selected, :label_user_mail_option_selected],
chris@37 39 [:none, :label_user_mail_option_none],
chris@37 40 [:only_my_events, :label_user_mail_option_only_my_events],
chris@37 41 [:only_assigned, :label_user_mail_option_only_assigned],
chris@37 42 [:only_owner, :label_user_mail_option_only_owner]
chris@37 43 ]
chris@37 44
Chris@0 45 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
Chris@0 46 :after_remove => Proc.new {|user, group| group.user_removed(user)}
Chris@0 47 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
Chris@0 48 has_many :changesets, :dependent => :nullify
Chris@0 49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
Chris@0 50 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
Chris@0 51 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
Chris@0 52 belongs_to :auth_source
Chris@0 53
Chris@0 54 # Active non-anonymous users scope
Chris@0 55 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
Chris@0 56
Chris@0 57 acts_as_customizable
Chris@0 58
Chris@0 59 attr_accessor :password, :password_confirmation
Chris@0 60 attr_accessor :last_before_login_on
Chris@0 61 # Prevents unauthorized assignments
Chris@0 62 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
Chris@0 63
Chris@0 64 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
Chris@0 65 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
Chris@0 66 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
Chris@0 67 # Login must contain lettres, numbers, underscores only
Chris@0 68 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
Chris@0 69 validates_length_of :login, :maximum => 30
Chris@0 70 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
Chris@0 71 validates_length_of :firstname, :lastname, :maximum => 30
Chris@0 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
Chris@0 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
Chris@0 74 validates_confirmation_of :password, :allow_nil => true
Chris@0 75
Chris@0 76 def before_create
chris@37 77 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
Chris@0 78 true
Chris@0 79 end
Chris@0 80
Chris@0 81 def before_save
Chris@0 82 # update hashed_password if password was set
Chris@0 83 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
Chris@0 84 end
Chris@0 85
Chris@0 86 def reload(*args)
Chris@0 87 @name = nil
Chris@0 88 super
Chris@0 89 end
Chris@0 90
Chris@1 91 def mail=(arg)
Chris@1 92 write_attribute(:mail, arg.to_s.strip)
Chris@1 93 end
Chris@1 94
Chris@0 95 def identity_url=(url)
Chris@0 96 if url.blank?
Chris@0 97 write_attribute(:identity_url, '')
Chris@0 98 else
Chris@0 99 begin
Chris@0 100 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
Chris@0 101 rescue OpenIdAuthentication::InvalidOpenId
Chris@0 102 # Invlaid url, don't save
Chris@0 103 end
Chris@0 104 end
Chris@0 105 self.read_attribute(:identity_url)
Chris@0 106 end
Chris@0 107
Chris@0 108 # Returns the user that matches provided login and password, or nil
Chris@0 109 def self.try_to_login(login, password)
Chris@0 110 # Make sure no one can sign in with an empty password
Chris@0 111 return nil if password.to_s.empty?
Chris@0 112 user = find_by_login(login)
Chris@0 113 if user
Chris@0 114 # user is already in local database
Chris@0 115 return nil if !user.active?
Chris@0 116 if user.auth_source
Chris@0 117 # user has an external authentication method
Chris@0 118 return nil unless user.auth_source.authenticate(login, password)
Chris@0 119 else
Chris@0 120 # authentication with local password
Chris@0 121 return nil unless User.hash_password(password) == user.hashed_password
Chris@0 122 end
Chris@0 123 else
Chris@0 124 # user is not yet registered, try to authenticate with available sources
Chris@0 125 attrs = AuthSource.authenticate(login, password)
Chris@0 126 if attrs
Chris@0 127 user = new(attrs)
Chris@0 128 user.login = login
Chris@0 129 user.language = Setting.default_language
Chris@0 130 if user.save
Chris@0 131 user.reload
Chris@0 132 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
Chris@0 133 end
Chris@0 134 end
Chris@0 135 end
Chris@0 136 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
Chris@0 137 user
Chris@0 138 rescue => text
Chris@0 139 raise text
Chris@0 140 end
Chris@0 141
Chris@0 142 # Returns the user who matches the given autologin +key+ or nil
Chris@0 143 def self.try_to_autologin(key)
Chris@0 144 tokens = Token.find_all_by_action_and_value('autologin', key)
Chris@0 145 # Make sure there's only 1 token that matches the key
Chris@0 146 if tokens.size == 1
Chris@0 147 token = tokens.first
Chris@0 148 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
Chris@0 149 token.user.update_attribute(:last_login_on, Time.now)
Chris@0 150 token.user
Chris@0 151 end
Chris@0 152 end
Chris@0 153 end
Chris@0 154
Chris@0 155 # Return user's full name for display
Chris@0 156 def name(formatter = nil)
Chris@0 157 if formatter
Chris@0 158 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
Chris@0 159 else
Chris@0 160 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
Chris@0 161 end
Chris@0 162 end
Chris@0 163
Chris@0 164 def active?
Chris@0 165 self.status == STATUS_ACTIVE
Chris@0 166 end
Chris@0 167
Chris@0 168 def registered?
Chris@0 169 self.status == STATUS_REGISTERED
Chris@0 170 end
Chris@0 171
Chris@0 172 def locked?
Chris@0 173 self.status == STATUS_LOCKED
Chris@0 174 end
Chris@0 175
Chris@14 176 def activate
Chris@14 177 self.status = STATUS_ACTIVE
Chris@14 178 end
Chris@14 179
Chris@14 180 def register
Chris@14 181 self.status = STATUS_REGISTERED
Chris@14 182 end
Chris@14 183
Chris@14 184 def lock
Chris@14 185 self.status = STATUS_LOCKED
Chris@14 186 end
Chris@14 187
Chris@14 188 def activate!
Chris@14 189 update_attribute(:status, STATUS_ACTIVE)
Chris@14 190 end
Chris@14 191
Chris@14 192 def register!
Chris@14 193 update_attribute(:status, STATUS_REGISTERED)
Chris@14 194 end
Chris@14 195
Chris@14 196 def lock!
Chris@14 197 update_attribute(:status, STATUS_LOCKED)
Chris@14 198 end
Chris@14 199
Chris@0 200 def check_password?(clear_password)
Chris@0 201 if auth_source_id.present?
Chris@0 202 auth_source.authenticate(self.login, clear_password)
Chris@0 203 else
Chris@0 204 User.hash_password(clear_password) == self.hashed_password
Chris@0 205 end
Chris@0 206 end
Chris@0 207
Chris@0 208 # Does the backend storage allow this user to change their password?
Chris@0 209 def change_password_allowed?
Chris@0 210 return true if auth_source_id.blank?
Chris@0 211 return auth_source.allow_password_changes?
Chris@0 212 end
Chris@0 213
Chris@0 214 # Generate and set a random password. Useful for automated user creation
Chris@0 215 # Based on Token#generate_token_value
Chris@0 216 #
Chris@0 217 def random_password
Chris@0 218 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
Chris@0 219 password = ''
Chris@0 220 40.times { |i| password << chars[rand(chars.size-1)] }
Chris@0 221 self.password = password
Chris@0 222 self.password_confirmation = password
Chris@0 223 self
Chris@0 224 end
Chris@0 225
Chris@0 226 def pref
Chris@0 227 self.preference ||= UserPreference.new(:user => self)
Chris@0 228 end
Chris@0 229
Chris@0 230 def time_zone
Chris@0 231 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
Chris@0 232 end
Chris@0 233
Chris@0 234 def wants_comments_in_reverse_order?
Chris@0 235 self.pref[:comments_sorting] == 'desc'
Chris@0 236 end
Chris@0 237
Chris@0 238 # Return user's RSS key (a 40 chars long string), used to access feeds
Chris@0 239 def rss_key
Chris@0 240 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
Chris@0 241 token.value
Chris@0 242 end
Chris@0 243
Chris@0 244 # Return user's API key (a 40 chars long string), used to access the API
Chris@0 245 def api_key
Chris@0 246 token = self.api_token || self.create_api_token(:action => 'api')
Chris@0 247 token.value
Chris@0 248 end
Chris@0 249
Chris@0 250 # Return an array of project ids for which the user has explicitly turned mail notifications on
Chris@0 251 def notified_projects_ids
Chris@0 252 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
Chris@0 253 end
Chris@0 254
Chris@0 255 def notified_project_ids=(ids)
Chris@0 256 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
Chris@0 257 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
Chris@0 258 @notified_projects_ids = nil
Chris@0 259 notified_projects_ids
Chris@0 260 end
Chris@0 261
chris@37 262 # Only users that belong to more than 1 project can select projects for which they are notified
chris@37 263 def valid_notification_options
chris@37 264 # Note that @user.membership.size would fail since AR ignores
chris@37 265 # :include association option when doing a count
chris@37 266 if memberships.length < 1
chris@37 267 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected}
chris@37 268 else
chris@37 269 MAIL_NOTIFICATION_OPTIONS
chris@37 270 end
chris@37 271 end
chris@37 272
Chris@0 273 # Find a user account by matching the exact login and then a case-insensitive
Chris@0 274 # version. Exact matches will be given priority.
Chris@0 275 def self.find_by_login(login)
Chris@0 276 # force string comparison to be case sensitive on MySQL
Chris@0 277 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
Chris@0 278
Chris@0 279 # First look for an exact match
Chris@0 280 user = first(:conditions => ["#{type_cast} login = ?", login])
Chris@0 281 # Fail over to case-insensitive if none was found
Chris@0 282 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
Chris@0 283 end
Chris@0 284
Chris@0 285 def self.find_by_rss_key(key)
Chris@0 286 token = Token.find_by_value(key)
Chris@0 287 token && token.user.active? ? token.user : nil
Chris@0 288 end
Chris@0 289
Chris@0 290 def self.find_by_api_key(key)
Chris@0 291 token = Token.find_by_action_and_value('api', key)
Chris@0 292 token && token.user.active? ? token.user : nil
Chris@0 293 end
Chris@0 294
Chris@0 295 # Makes find_by_mail case-insensitive
Chris@0 296 def self.find_by_mail(mail)
Chris@0 297 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
Chris@0 298 end
Chris@0 299
Chris@0 300 def to_s
Chris@0 301 name
Chris@0 302 end
Chris@0 303
Chris@0 304 # Returns the current day according to user's time zone
Chris@0 305 def today
Chris@0 306 if time_zone.nil?
Chris@0 307 Date.today
Chris@0 308 else
Chris@0 309 Time.now.in_time_zone(time_zone).to_date
Chris@0 310 end
Chris@0 311 end
Chris@0 312
Chris@0 313 def logged?
Chris@0 314 true
Chris@0 315 end
Chris@0 316
Chris@0 317 def anonymous?
Chris@0 318 !logged?
Chris@0 319 end
Chris@0 320
Chris@0 321 # Return user's roles for project
Chris@0 322 def roles_for_project(project)
Chris@0 323 roles = []
Chris@0 324 # No role on archived projects
Chris@0 325 return roles unless project && project.active?
Chris@0 326 if logged?
Chris@0 327 # Find project membership
Chris@0 328 membership = memberships.detect {|m| m.project_id == project.id}
Chris@0 329 if membership
Chris@0 330 roles = membership.roles
Chris@0 331 else
Chris@0 332 @role_non_member ||= Role.non_member
Chris@0 333 roles << @role_non_member
Chris@0 334 end
Chris@0 335 else
Chris@0 336 @role_anonymous ||= Role.anonymous
Chris@0 337 roles << @role_anonymous
Chris@0 338 end
Chris@0 339 roles
Chris@0 340 end
Chris@0 341
Chris@0 342 # Return true if the user is a member of project
Chris@0 343 def member_of?(project)
Chris@0 344 !roles_for_project(project).detect {|role| role.member?}.nil?
Chris@0 345 end
Chris@0 346
chris@37 347 # Return true if the user is allowed to do the specified action on a specific context
chris@37 348 # Action can be:
Chris@0 349 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
Chris@0 350 # * a permission Symbol (eg. :edit_project)
chris@37 351 # Context can be:
chris@37 352 # * a project : returns true if user is allowed to do the specified action on this project
chris@37 353 # * a group of projects : returns true if user is allowed on every project
chris@37 354 # * nil with options[:global] set : check if user has at least one role allowed for this action,
chris@37 355 # or falls back to Non Member / Anonymous permissions depending if the user is logged
chris@37 356 def allowed_to?(action, context, options={})
chris@37 357 if context && context.is_a?(Project)
Chris@0 358 # No action allowed on archived projects
chris@37 359 return false unless context.active?
Chris@0 360 # No action allowed on disabled modules
chris@37 361 return false unless context.allows_to?(action)
Chris@0 362 # Admin users are authorized for anything else
Chris@0 363 return true if admin?
Chris@0 364
chris@37 365 roles = roles_for_project(context)
Chris@0 366 return false unless roles
chris@37 367 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
Chris@0 368
chris@37 369 elsif context && context.is_a?(Array)
chris@37 370 # Authorize if user is authorized on every element of the array
chris@37 371 context.map do |project|
chris@37 372 allowed_to?(action,project,options)
chris@37 373 end.inject do |memo,allowed|
chris@37 374 memo && allowed
chris@37 375 end
Chris@0 376 elsif options[:global]
Chris@0 377 # Admin users are always authorized
Chris@0 378 return true if admin?
Chris@0 379
Chris@0 380 # authorize if user has at least one role that has this permission
Chris@0 381 roles = memberships.collect {|m| m.roles}.flatten.uniq
Chris@0 382 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
Chris@0 383 else
Chris@0 384 false
Chris@0 385 end
Chris@0 386 end
chris@22 387
chris@22 388 # Is the user allowed to do the specified action on any project?
chris@22 389 # See allowed_to? for the actions and valid options.
chris@22 390 def allowed_to_globally?(action, options)
chris@22 391 allowed_to?(action, nil, options.reverse_merge(:global => true))
chris@22 392 end
Chris@0 393
chris@37 394 # Utility method to help check if a user should be notified about an
chris@37 395 # event.
chris@37 396 #
chris@37 397 # TODO: only supports Issue events currently
chris@37 398 def notify_about?(object)
chris@37 399 case mail_notification.to_sym
chris@37 400 when :all
chris@37 401 true
chris@37 402 when :selected
chris@37 403 # Handled by the Project
chris@37 404 when :none
chris@37 405 false
chris@37 406 when :only_my_events
chris@37 407 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
chris@37 408 true
chris@37 409 else
chris@37 410 false
chris@37 411 end
chris@37 412 when :only_assigned
chris@37 413 if object.is_a?(Issue) && object.assigned_to == self
chris@37 414 true
chris@37 415 else
chris@37 416 false
chris@37 417 end
chris@37 418 when :only_owner
chris@37 419 if object.is_a?(Issue) && object.author == self
chris@37 420 true
chris@37 421 else
chris@37 422 false
chris@37 423 end
chris@37 424 else
chris@37 425 false
chris@37 426 end
chris@37 427 end
chris@37 428
Chris@0 429 def self.current=(user)
Chris@0 430 @current_user = user
Chris@0 431 end
Chris@0 432
Chris@0 433 def self.current
Chris@0 434 @current_user ||= User.anonymous
Chris@0 435 end
Chris@0 436
Chris@0 437 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
Chris@0 438 # one anonymous user per database.
Chris@0 439 def self.anonymous
Chris@0 440 anonymous_user = AnonymousUser.find(:first)
Chris@0 441 if anonymous_user.nil?
Chris@0 442 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
Chris@0 443 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
Chris@0 444 end
Chris@0 445 anonymous_user
Chris@0 446 end
Chris@0 447
Chris@0 448 protected
Chris@0 449
Chris@0 450 def validate
Chris@0 451 # Password length validation based on setting
Chris@0 452 if !password.nil? && password.size < Setting.password_min_length.to_i
Chris@0 453 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
Chris@0 454 end
Chris@0 455 end
Chris@0 456
Chris@0 457 private
Chris@0 458
Chris@0 459 # Return password digest
Chris@0 460 def self.hash_password(clear_password)
Chris@0 461 Digest::SHA1.hexdigest(clear_password || "")
Chris@0 462 end
Chris@0 463 end
Chris@0 464
Chris@0 465 class AnonymousUser < User
Chris@0 466
Chris@0 467 def validate_on_create
Chris@0 468 # There should be only one AnonymousUser in the database
Chris@0 469 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
Chris@0 470 end
Chris@0 471
Chris@0 472 def available_custom_fields
Chris@0 473 []
Chris@0 474 end
Chris@0 475
Chris@0 476 # Overrides a few properties
Chris@0 477 def logged?; false end
Chris@0 478 def admin; false end
Chris@0 479 def name(*args); I18n.t(:label_user_anonymous) end
Chris@0 480 def mail; nil end
Chris@0 481 def time_zone; nil end
Chris@0 482 def rss_key; nil end
Chris@0 483 end