Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 120
- Log:
Hub integration. RForum Users are lazy-created when Hub users, logged in,
visit RForum for the first time. RForum navigation links to log in and
out or change settings all point at Hub. Webmaster and adminstrator roles
in Hub map to administrator permissions in RForum; in addition to the
RForum permission management system, though, added Hub permissions hashes
to various controllers to include Hub authorisation too. Two layers can't
hurt and it means sensible return-to'd redirections to Hub for some
action types.Corrected grammar on deletion confirmation messages and tidied up a few
templates here and there, too.
- Author:
- adh
- Date:
- Sat Oct 28 23:53:03 +0100 2006
- Size:
- 8092 Bytes
1 | require 'digest/sha1' |
2 | require_dependency 'user_permissions' |
3 | |
4 | class User < ActiveRecord::Base |
5 | has_many :post_votes, :foreign_key => 'voter_id' |
6 | has_many :topic_subscriptions, :dependent => true |
7 | has_many :posts |
8 | has_many :topic_reads, :dependent => true |
9 | serialize :additional_information, Hash |
10 | |
11 | def self.inheritance_column() 'role' end |
12 | |
13 | include ErrorRaising, RForum::Localization |
14 | |
15 | include UserPermissions |
16 | |
17 | attr_accessor :old_password, :new_password, :retyped_password |
18 | |
19 | # CLASS METHODS |
20 | |
21 | # Find a user by user name and password. |
22 | def self.find_by_login(name, unencrypted_password) |
23 | raise ArgumentError if name.nil? |
24 | raise ArgumentError if unencrypted_password.nil? |
25 | # find_first ["name='%s' AND password='%s'", name, encrypt(password)] |
26 | u = find_by_name(name) |
27 | u && u.authenticated?(unencrypted_password) ? u : nil |
28 | end |
29 | |
30 | def self.find_by_token(id, token) |
31 | raise ArgumentError if id.nil? |
32 | raise ArgumentError if token.nil? |
33 | user = find_first ["id='%s' AND security_token='%s'", id, token] |
34 | if user.nil? or user.token_expired? |
35 | return nil |
36 | else |
37 | return user |
38 | end |
39 | end |
40 | |
41 | # Encrypts some data with the salt. |
42 | def self.encrypt(password, salt) |
43 | Digest::SHA1.hexdigest("--#{salt}--#{password}--") |
44 | end |
45 | |
46 | # CALLBACKS |
47 | |
48 | def after_initialize |
49 | reset_password_fields |
50 | end |
51 | |
52 | def before_validation_on_create |
53 | %w(name email name firstname surname).each do |attr| |
54 | self[attr] = self[attr].to_s.strip.squeeze(' ').chomp |
55 | end |
56 | |
57 | self.name.downcase! |
58 | self.email.downcase! |
59 | |
60 | self.role = 'User' |
61 | @unencrypted_password = makepass |
62 | encrypt_stored_password |
63 | end |
64 | |
65 | def validate_on_create |
66 | # Hub integration; nickname comes from e-mail address to try and |
67 | # guarantee it is unique. We trust it so long as it isn't a duplicate. |
68 | # The creation routine makes sure it fits within field length by turning |
69 | # the address into a SHA1 hex digest (40 characters). |
70 | # |
71 | # # Nickname cannot be changed, so we only need to validate it on create |
72 | # unless self.name =~ /^[a-z0-9\-]{3,15}$/i |
73 | # errors.add 'name', :user_name_invalid |
74 | # end |
75 | |
76 | # Already existing nick is not allowed |
77 | errors.add_on_duplicate 'name', :user_name_duplicate |
78 | end |
79 | |
80 | def validate |
81 | # All commented out; Hub integration means we're |
82 | # sufficiently happy with the data source to live with it. |
83 | #errors.add 'email', :user_email_invalid unless valid_email?(self.email) |
84 | #errors.add 'firstname', :user_firstname_invalid unless self.firstname =~ /^.{2,20}$/i |
85 | #errors.add 'surname', :user_surname_invalid unless self.surname =~ /^.{2,20}$/i |
86 | #errors.add_on_duplicate 'email', :user_email_duplicate |
87 | |
88 | #if (@new_password or self.password.nil?) and password.size < 3 |
89 | # errors.add 'new_password', :user_password_invalid |
90 | #end |
91 | |
92 | # nick cannot be changed |
93 | unless self.new_record? |
94 | old_record = User.find(self.id) |
95 | errors.add 'name', l(:user_cannot_change_nick) unless old_record.name == self.name |
96 | end |
97 | end |
98 | |
99 | # NORMAL METHODS |
100 | |
101 | def guest? |
102 | false |
103 | end |
104 | |
105 | # Generates a temporary security token that can be passed in a URL to |
106 | # authenticate this user without a password. Typical use - to put that |
107 | # URL in an email sent to the user who forgotten his password and |
108 | # needs to reset it. |
109 | # |
110 | # This method will return an already existing token, if it is not |
111 | # older than half the maximum token lifetime. This is to avoid |
112 | # situations where user somehow regenerates the token (by clicking on |
113 | # the same link twice, or by browser back button), and then tries to |
114 | # use the first token. I don't want to create a new table for |
115 | # temporary security tokens. |
116 | def generate_security_token |
117 | if self.security_token.nil? or self.token_expiry.nil? or |
118 | (Time.now.to_i + token_lifetime / 2) >= self.token_expiry.to_i |
119 | return new_security_token |
120 | else |
121 | return self.security_token |
122 | end |
123 | end |
124 | |
125 | def authenticated?(unencrypted_password) |
126 | password == encrypt(unencrypted_password) |
127 | end |
128 | |
129 | def encrypt_password(new_password) |
130 | self['password'] = self.class.encrypt(new_password, salt) |
131 | end |
132 | |
133 | # Encrypts the password with the user salt |
134 | def encrypt(unencrypted_password) |
135 | self.class.encrypt(unencrypted_password, salt) |
136 | end |
137 | |
138 | def guest_email |
139 | nil |
140 | end |
141 | |
142 | def guest_name |
143 | nil |
144 | end |
145 | |
146 | # Create a random password. |
147 | # TODO: rewrite |
148 | def makepass |
149 | chars = ("a".."z").to_a + (1..9).to_a |
150 | chars = chars.sort_by { rand } |
151 | s = chars[0..7].to_s |
152 | end |
153 | |
154 | # TODO test me |
155 | def last_read_time(topic) |
156 | t = TopicRead.find(:first, :conditions => "user_id = #{self.id} AND topic_id = #{topic.id}") |
157 | if t |
158 | t.updated_at |
159 | else |
160 | nil |
161 | end |
162 | end |
163 | |
164 | # Gets the time the topics were last read by this user |
165 | # TODO test me |
166 | def topic_read_times |
167 | topic_read_times_hash = Hash.new |
168 | reads = self.topic_reads.find_all.each { |read| |
169 | topic_read_times_hash[read.topic_id] = read.updated_at |
170 | } |
171 | topic_read_times_hash |
172 | end |
173 | |
174 | # Enter a new vote or update an old vote. |
175 | # TODO: test me |
176 | def vote_post(post, value) |
177 | raise ArgumentError unless post.is_a?(Post) |
178 | transaction do |
179 | vote = find_all_in_post_votes("post_id = #{post.id}").first || PostVote.new |
180 | vote.voter = self |
181 | vote.post = post |
182 | vote.value = value |
183 | vote.save |
184 | end |
185 | end |
186 | |
187 | # Update the last time a topic was read. |
188 | def update_read_time(topic) |
189 | topic_read = self.topic_reads.find_all("topic_id = #{topic.id}") |
190 | |
191 | if topic_read.empty? |
192 | # first time this topic is read by this user |
193 | topic_read = TopicRead.new('topic_id' => topic.id, 'user_id' => self.id) |
194 | else |
195 | topic_read = topic_read[0] |
196 | end |
197 | topic_read.save |
198 | return topic_read |
199 | end |
200 | |
201 | def reset_password_fields |
202 | @old_password = @new_password = @retyped_password = '' |
203 | end |
204 | |
205 | # Subscribe to a topic to get notifications on new posts. |
206 | def subscribe_topic(topic) |
207 | subscription = TopicSubscription.new |
208 | subscription.user = self |
209 | subscription.topic = topic |
210 | |
211 | if TopicSubscription.count("topic_id = #{topic.id} AND user_id = #{self.id}") > 0 |
212 | # subscription already exists |
213 | return false |
214 | else |
215 | subscription.save |
216 | return true |
217 | end |
218 | end |
219 | |
220 | # Returns true if successful, else false. The latter indicates that the |
221 | # subscription information wasn't found - the user wasn't subscribed. |
222 | # This can happen if someone follows a notification e-mail link more |
223 | # than once, or tries to unsubscribe from a topic they were subscribed |
224 | # to under a different user name, but not under the current user name. |
225 | |
226 | def unsubscribe_topic(topic) |
227 | found = TopicSubscription.find_first("topic_id = #{topic.id} AND user_id = #{self.id}") |
228 | |
229 | if found |
230 | found.destroy |
231 | return true |
232 | else |
233 | return false |
234 | end |
235 | end |
236 | |
237 | # Unencrypted password field is needed in the controller to send an email notification. |
238 | # It is populated on creation, and not written to the database, so only a freshly created |
239 | # user instance has it set. |
240 | def tell_and_forget_unencrypted_password |
241 | raise "Unencrypted password not available" if @unencrypted_password.nil? |
242 | p = @unencrypted_password |
243 | @unencrypted_password = nil |
244 | return p |
245 | end |
246 | |
247 | def admin? |
248 | false |
249 | end |
250 | |
251 | def token_expired? |
252 | self.security_token and self.token_expiry and (Time.now > self.token_expiry) |
253 | end |
254 | |
255 | def get_display_name |
256 | "#{self.firstname}" # Don't need to add " #{self.surname}" under Hub since firstname already contains a full unique name |
257 | end |
258 | |
259 | private |
260 | def new_security_token |
261 | self['security_token'] = |
262 | Digest::SHA1.hexdigest(self['password'] + (Time.now.to_i.to_s) + rand.to_s) |
263 | self.token_expiry = Time.at(Time.now.to_i + token_lifetime) |
264 | self.save |
265 | return self['security_token'] |
266 | end |
267 | |
268 | def token_lifetime |
269 | RForum::CONFIG[:security_token_life_hours] * 60 * 60 |
270 | end |
271 | |
272 | def encrypt_stored_password |
273 | return if @unencrypted_password.blank? |
274 | self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{name}--") if new_record? |
275 | self.password = encrypt(@unencrypted_password) |
276 | end |
277 | end |
278 |