Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 12
- Log:
Configured for temporary /rails/rforum location on Alpha. Updated
several layouts to suit RISC OS Open Limited site style in addition
to producing a theme stylesheet. View changes included a few fixes
to invalid HTML 'class' attribute values (they contained spaces).
Some examples may remain. Note temporary hacks to force in
"/rails/rforum" prefix to some URLs where they are either hard coded
or the user helpers but for currently unknown reasons the helpers are
giving an incorrect result.Extended menu item renderer to allow a separator string to be
supplied; needed to allow horizontal navigation regions as well as
sidebar-based vertical navigation regions ("|" vs "</li><li>").Fix in models.rb to allow attempts to unsubscribe from a topic from
which the user has already subscribed - e.g. clicking on the relevant
link in a notification e-mail message twice. Previously, an application
error would be raised.Extended permissions system to include whether or not a user can post
(can_post?()) or reply (can_reply?()) to a message. Normal and admin
users always can. Guests follow new options token "guests_cannot_post"
(note that this is not the same as "anon_posting_allowed" which asks
that guests supply at least their name rather than be anonymous - of
course that's pretty useless as a guest can put in any name they want,
real or otherwise). The navbar and post_menu controllers know about
the new options and won't generate links for prohibited actions. Topic
controller enforces permissions in case someone builds links by hand.Changed user_controller.rb to add flash[:attention] tokens when a user
logs in or out. Fixed broken hard-coded URL generation to use url_for
helper method instead. Used the flash[:attention] mechanism to produce
the notification about an e-mail message sent for a forgotten password
rather than writing just that text into a blank page - redirects back
to the forum controller, list action.This revision is believed to be the functional equivalent of revision
906 of RForum merged with changes made to an unknown prior revision of
RForum along with acts_as_authenticated modification plus the above
set of fixes and documentation. At the time of writing this specific
combination has not been tested live on the RISC OS Open Limited
prototype site.
- Author:
- adh
- Date:
- Sat Jul 22 21:15:03 +0100 2006
- Size:
- 7589 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 | self.role = 'User' |
60 | @unencrypted_password = makepass |
61 | encrypt_stored_password |
62 | end |
63 | |
64 | def validate_on_create |
65 | # Nickname cannot be changed, so we only need to validate it on create |
66 | unless self.name =~ /^[a-z0-9\-]{3,15}$/i |
67 | errors.add 'name', :user_name_invalid |
68 | end |
69 | # Already existing nick is not allowed |
70 | errors.add_on_duplicate 'name', :user_name_duplicate |
71 | end |
72 | |
73 | def validate |
74 | errors.add 'email', :user_email_invalid unless valid_email?(self.email) |
75 | errors.add 'firstname', :user_firstname_invalid unless self.firstname =~ /^.{2,20}$/i |
76 | errors.add 'surname', :user_surname_invalid unless self.surname =~ /^.{2,20}$/i |
77 | errors.add_on_duplicate 'email', :user_email_duplicate |
78 | |
79 | if (@new_password or self.password.nil?) and password.size < 3 |
80 | errors.add 'new_password', :user_password_invalid |
81 | end |
82 | |
83 | # nick cannot be changed |
84 | unless self.new_record? |
85 | old_record = User.find(self.id) |
86 | errors.add 'name', l(:user_cannot_change_nick) unless old_record.name == self.name |
87 | end |
88 | end |
89 | |
90 | # NORMAL METHODS |
91 | |
92 | def guest? |
93 | false |
94 | end |
95 | |
96 | # Generates a temporary security token that can be passed in a URL to |
97 | # authenticate this user without a password. Typical use - to put that |
98 | # URL in an email sent to the user who forgotten his password and |
99 | # needs to reset it. |
100 | # |
101 | # This method will return an already existing token, if it is not |
102 | # older than half the maximum token lifetime. This is to avoid |
103 | # situations where user somehow regenerates the token (by clicking on |
104 | # the same link twice, or by browser back button), and then tries to |
105 | # use the first token. I don't want to create a new table for |
106 | # temporary security tokens. |
107 | def generate_security_token |
108 | if self.security_token.nil? or self.token_expiry.nil? or |
109 | (Time.now.to_i + token_lifetime / 2) >= self.token_expiry.to_i |
110 | return new_security_token |
111 | else |
112 | return self.security_token |
113 | end |
114 | end |
115 | |
116 | def authenticated?(unencrypted_password) |
117 | password == encrypt(unencrypted_password) |
118 | end |
119 | |
120 | def encrypt_password(new_password) |
121 | self['password'] = self.class.encrypt(new_password, salt) |
122 | end |
123 | |
124 | # Encrypts the password with the user salt |
125 | def encrypt(unencrypted_password) |
126 | self.class.encrypt(unencrypted_password, salt) |
127 | end |
128 | |
129 | def guest_email |
130 | nil |
131 | end |
132 | |
133 | def guest_name |
134 | nil |
135 | end |
136 | |
137 | # Create a random password. |
138 | # TODO: rewrite |
139 | def makepass |
140 | chars = ("a".."z").to_a + (1..9).to_a |
141 | chars = chars.sort_by { rand } |
142 | s = chars[0..7].to_s |
143 | end |
144 | |
145 | # TODO test me |
146 | def last_read_time(topic) |
147 | t = TopicRead.find(:first, :conditions => "user_id = #{self.id} AND topic_id = #{topic.id}") |
148 | if t |
149 | t.updated_at |
150 | else |
151 | nil |
152 | end |
153 | end |
154 | |
155 | # Gets the time the topics were last read by this user |
156 | # TODO test me |
157 | def topic_read_times |
158 | topic_read_times_hash = Hash.new |
159 | reads = self.topic_reads.find_all.each { |read| |
160 | topic_read_times_hash[read.topic_id] = read.updated_at |
161 | } |
162 | topic_read_times_hash |
163 | end |
164 | |
165 | # Enter a new vote or update an old vote. |
166 | # TODO: test me |
167 | def vote_post(post, value) |
168 | raise ArgumentError unless post.is_a?(Post) |
169 | transaction do |
170 | vote = find_all_in_post_votes("post_id = #{post.id}").first || PostVote.new |
171 | vote.voter = self |
172 | vote.post = post |
173 | vote.value = value |
174 | vote.save |
175 | end |
176 | end |
177 | |
178 | # Update the last time a topic was read. |
179 | def update_read_time(topic) |
180 | topic_read = self.topic_reads.find_all("topic_id = #{topic.id}") |
181 | |
182 | if topic_read.empty? |
183 | # first time this topic is read by this user |
184 | topic_read = TopicRead.new('topic_id' => topic.id, 'user_id' => self.id) |
185 | else |
186 | topic_read = topic_read[0] |
187 | end |
188 | topic_read.save |
189 | return topic_read |
190 | end |
191 | |
192 | def reset_password_fields |
193 | @old_password = @new_password = @retyped_password = '' |
194 | end |
195 | |
196 | # Subscribe to a topic to get notifications on new posts. |
197 | def subscribe_topic(topic) |
198 | subscription = TopicSubscription.new |
199 | subscription.user = self |
200 | subscription.topic = topic |
201 | |
202 | if TopicSubscription.count("topic_id = #{topic.id} AND user_id = #{self.id}") > 0 |
203 | # subscription already exists |
204 | return false |
205 | else |
206 | subscription.save |
207 | return true |
208 | end |
209 | end |
210 | |
211 | # Returns true if successful, else false. The latter indicates that the |
212 | # subscription information wasn't found - the user wasn't subscribed. |
213 | # This can happen if someone follows a notification e-mail link more |
214 | # than once, or tries to unsubscribe from a topic they were subscribed |
215 | # to under a different user name, but not under the current user name. |
216 | |
217 | def unsubscribe_topic(topic) |
218 | found = TopicSubscription.find_first("topic_id = #{topic.id} AND user_id = #{self.id}") |
219 | |
220 | if found |
221 | found.destroy |
222 | return true |
223 | else |
224 | return false |
225 | end |
226 | end |
227 | |
228 | # Unencrypted password field is needed in the controller to send an email notification. |
229 | # It is populated on creation, and not written to the database, so only a freshly created |
230 | # user instance has it set. |
231 | def tell_and_forget_unencrypted_password |
232 | raise "Unencrypted password not available" if @unencrypted_password.nil? |
233 | p = @unencrypted_password |
234 | @unencrypted_password = nil |
235 | return p |
236 | end |
237 | |
238 | def admin? |
239 | false |
240 | end |
241 | |
242 | def token_expired? |
243 | self.security_token and self.token_expiry and (Time.now > self.token_expiry) |
244 | end |
245 | |
246 | def get_display_name |
247 | "#{self.firstname} #{self.surname}" |
248 | end |
249 | |
250 | private |
251 | def new_security_token |
252 | self['security_token'] = |
253 | Digest::SHA1.hexdigest(self['password'] + (Time.now.to_i.to_s) + rand.to_s) |
254 | self.token_expiry = Time.at(Time.now.to_i + token_lifetime) |
255 | self.save |
256 | return self['security_token'] |
257 | end |
258 | |
259 | def token_lifetime |
260 | RForum::CONFIG[:security_token_life_hours] * 60 * 60 |
261 | end |
262 | |
263 | def encrypt_stored_password |
264 | return if @unencrypted_password.blank? |
265 | self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{name}--") if new_record? |
266 | self.password = encrypt(@unencrypted_password) |
267 | end |
268 | end |
269 |