require 'digest/sha1' require_dependency 'user_permissions' class User < ActiveRecord::Base has_many :post_votes, :foreign_key => 'voter_id' has_many :topic_subscriptions, :dependent => true has_many :posts has_many :topic_reads, :dependent => true serialize :additional_information, Hash def self.inheritance_column() 'role' end include ErrorRaising, RForum::Localization include UserPermissions attr_accessor :old_password, :new_password, :retyped_password # CLASS METHODS # Find a user by user name and password. def self.find_by_login(name, unencrypted_password) raise ArgumentError if name.nil? raise ArgumentError if unencrypted_password.nil? # find_first ["name='%s' AND password='%s'", name, encrypt(password)] u = find_by_name(name) u && u.authenticated?(unencrypted_password) ? u : nil end def self.find_by_token(id, token) raise ArgumentError if id.nil? raise ArgumentError if token.nil? user = find_first ["id='%s' AND security_token='%s'", id, token] if user.nil? or user.token_expired? return nil else return user end end # Encrypts some data with the salt. def self.encrypt(password, salt) Digest::SHA1.hexdigest("--#{salt}--#{password}--") end # CALLBACKS def after_initialize reset_password_fields end def before_validation_on_create %w(name email name firstname surname).each do |attr| self[attr] = self[attr].to_s.strip.squeeze(' ').chomp end self.name.downcase! self.email.downcase! self.role = 'User' @unencrypted_password = makepass encrypt_stored_password end def validate_on_create # Hub integration; nickname comes from e-mail address to try and # guarantee it is unique. We trust it so long as it isn't a duplicate. # The creation routine makes sure it fits within field length by turning # the address into a SHA1 hex digest (40 characters). # # # Nickname cannot be changed, so we only need to validate it on create # unless self.name =~ /^[a-z0-9\-]{3,15}$/i # errors.add 'name', :user_name_invalid # end # Already existing nick is not allowed errors.add_on_duplicate 'name', :user_name_duplicate end def validate # All commented out; Hub integration means we're # sufficiently happy with the data source to live with it. #errors.add 'email', :user_email_invalid unless valid_email?(self.email) #errors.add 'firstname', :user_firstname_invalid unless self.firstname =~ /^.{2,20}$/i #errors.add 'surname', :user_surname_invalid unless self.surname =~ /^.{2,20}$/i #errors.add_on_duplicate 'email', :user_email_duplicate #if (@new_password or self.password.nil?) and password.size < 3 # errors.add 'new_password', :user_password_invalid #end # nick cannot be changed unless self.new_record? old_record = User.find(self.id) errors.add 'name', l(:user_cannot_change_nick) unless old_record.name == self.name end end # NORMAL METHODS def guest? false end # Generates a temporary security token that can be passed in a URL to # authenticate this user without a password. Typical use - to put that # URL in an email sent to the user who forgotten his password and # needs to reset it. # # This method will return an already existing token, if it is not # older than half the maximum token lifetime. This is to avoid # situations where user somehow regenerates the token (by clicking on # the same link twice, or by browser back button), and then tries to # use the first token. I don't want to create a new table for # temporary security tokens. def generate_security_token if self.security_token.nil? or self.token_expiry.nil? or (Time.now.to_i + token_lifetime / 2) >= self.token_expiry.to_i return new_security_token else return self.security_token end end def authenticated?(unencrypted_password) password == encrypt(unencrypted_password) end def encrypt_password(new_password) self['password'] = self.class.encrypt(new_password, salt) end # Encrypts the password with the user salt def encrypt(unencrypted_password) self.class.encrypt(unencrypted_password, salt) end def guest_email nil end def guest_name nil end # Create a random password. # TODO: rewrite def makepass chars = ("a".."z").to_a + (1..9).to_a chars = chars.sort_by { rand } s = chars[0..7].to_s end # TODO test me def last_read_time(topic) t = TopicRead.find(:first, :conditions => "user_id = #{self.id} AND topic_id = #{topic.id}") if t t.updated_at else nil end end # Gets the time the topics were last read by this user # TODO test me def topic_read_times topic_read_times_hash = Hash.new reads = self.topic_reads.find_all.each { |read| topic_read_times_hash[read.topic_id] = read.updated_at } topic_read_times_hash end # Enter a new vote or update an old vote. # TODO: test me def vote_post(post, value) raise ArgumentError unless post.is_a?(Post) transaction do vote = find_all_in_post_votes("post_id = #{post.id}").first || PostVote.new vote.voter = self vote.post = post vote.value = value vote.save end end # Update the last time a topic was read. def update_read_time(topic) topic_read = self.topic_reads.find_all("topic_id = #{topic.id}") if topic_read.empty? # first time this topic is read by this user topic_read = TopicRead.new('topic_id' => topic.id, 'user_id' => self.id) else topic_read = topic_read[0] end topic_read.save return topic_read end def reset_password_fields @old_password = @new_password = @retyped_password = '' end # Subscribe to a topic to get notifications on new posts. def subscribe_topic(topic) subscription = TopicSubscription.new subscription.user = self subscription.topic = topic if TopicSubscription.count("topic_id = #{topic.id} AND user_id = #{self.id}") > 0 # subscription already exists return false else subscription.save return true end end # Returns true if successful, else false. The latter indicates that the # subscription information wasn't found - the user wasn't subscribed. # This can happen if someone follows a notification e-mail link more # than once, or tries to unsubscribe from a topic they were subscribed # to under a different user name, but not under the current user name. def unsubscribe_topic(topic) found = TopicSubscription.find_first("topic_id = #{topic.id} AND user_id = #{self.id}") if found found.destroy return true else return false end end # Unencrypted password field is needed in the controller to send an email notification. # It is populated on creation, and not written to the database, so only a freshly created # user instance has it set. def tell_and_forget_unencrypted_password raise "Unencrypted password not available" if @unencrypted_password.nil? p = @unencrypted_password @unencrypted_password = nil return p end def admin? false end def token_expired? self.security_token and self.token_expiry and (Time.now > self.token_expiry) end def get_display_name "#{self.firstname}" # Don't need to add " #{self.surname}" under Hub since firstname already contains a full unique name end private def new_security_token self['security_token'] = Digest::SHA1.hexdigest(self['password'] + (Time.now.to_i.to_s) + rand.to_s) self.token_expiry = Time.at(Time.now.to_i + token_lifetime) self.save return self['security_token'] end def token_lifetime RForum::CONFIG[:security_token_life_hours] * 60 * 60 end def encrypt_stored_password return if @unencrypted_password.blank? self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{name}--") if new_record? self.password = encrypt(@unencrypted_password) end end